Skip to content

Contract gaps blocking a host from dropping the tinymcp path dependency #4

Description

@senamakel

tinymcp-bus v0.3.1 publishes a complete names::METHODS (28 members), a
CONTRACT_VERSION and the payload types — enough that the survey started
optimistic. But OpenHuman cannot yet drop its tinymcp path dependency and reach
the module over the bus, because a set of things the host reaches for are not on
the wire. This issue enumerates them, verified against the code rather than
assumed.

Context: openhuman PR
#5689 lands the registry
entry pinning the v0.3.1 release, and documents this list next to the dependency
it cannot yet remove.

Members that do not exist

1. OAuthComplete(state, code) -> ConnectOutcome

The contract has OAuthBegin only. OpenHuman's loopback callback route calls
McpRegistry::oauth_complete, which does the token exchange and the reconnect,
and resolves state -> server_id through the module-private pending-authorization
map. It is not reconstructible from Connect.

2. The boot-connect pass and the reconnect supervisor

registry::connect_installed_servers(...) and Supervisor / SupervisorConfig /
.tick(store, connections, oauth, now) have no members — and the module does not
run either internally
. tinybus_module/mod.rs's setup() builds the service,
serve_ats it, request_names and returns.

Its own lazy = false comment says "a host that loaded this module wants its
servers connected, and deferring the load would defer that until the first call —
by which point an agent has already been told it has no tools." Nothing implements
that today.

I think the right answer is that the module should own both — start the connect
pass and the supervisor task in setup() — rather than exposing them as members
for a host to drive. The supervisor's backoff state and its probe_alive liveness
check (a real round trip, distinct from map membership) are module-internal, so a
host driving this over Status polling would be reimplementing it worse. But that
is a design call, so flagging rather than assuming.

3. A member returning ConnectedServerOverview

The type is exported by tinymcp-bus, but nothing on the interface returns it —
an exported type with no wire path. OpenHuman reads it from the orchestrator prompt
builder and the agent tool registry (via all_connected_tools, itself
connected_overview().flat_map). Composing it host-side from Status +
InstalledList + N x ListTools is an N+1 on a path that runs on every tool-list
build.

Types the contract does not publish

Four of the 28 members return types a host cannot name:

  • ServerDetail — returned by RegistryGet, SetupGet and ConfigAssist,
    but defined in crates/tinymcp/src/tinybus_module/service.rs, i.e. inside the
    module crate.
  • AuthDetection / AuthKind — returned by DetectAuth, defined in
    crates/tinymcp/src/registry/oauth/types.rs.

Both are decodable with hand-written mirror structs host-side, which is exactly the
"parallel set of payload types for hosts ... a conversion at every call site that
nothing checks" that tinymcp-bus's own lib.rs docs rule out. Moving them down
into the contract keeps the crate's stated invariant true.

Smaller, but each one real

4. registry::curation::{tag_official, float_official_first} are applied by
the caller after a search, and RegistrySearch deliberately does not apply them
("applied by crate::registry::curation on top of this, by a caller that wants
them", ops/types.rs). Neither is in the contract, and the official-list badging
is data-driven from curation's own list, so a host cannot reimplement it faithfully.
Either export them, or apply them inside RegistrySearch behind a flag.

5. render_tool_result and redact_endpoint live in tinymcp::transport, not
in the contract. OpenHuman's gitbooks documentation tool consumes
result.rendered and logs redact_endpoint(...); StaticCallTool returns
ToolCallOutcome { result: Value, is_error: bool } — the raw reply only. Either
export both, or add a rendered: McpToolResult field to ToolCallOutcome.

6. No error type is exported. OpenHuman's
classifies_mcp_connect_401_as_needs_auth is a live test that anchors the
McpServerNeedsAuth observability classifier on tinymcp::Error::Unauthorized's
Display output, precisely so the classifier cannot drift away from the wording it
matches. With no error type on the contract that coupling becomes an untethered
string literal. An error-name table (the shape tinymemory-bus uses) would keep it.

The architectural one

7. A per-data_dir object seam.

ModuleConfig carries a single data_dir, supplied once at load. OpenHuman's
mcp::host keys a HashMap<PathBuf, McpHost> by workspace, one Store +
AuditStore per workspace.

This is not a test-only concern. Login rewrites the active-user marker and reloads
config in-process, moving workspace_dir from users/local to users/<id>;
logout reverses it. An ordinary desktop session therefore opens at least two, and
repeated login/logout opens more. With one data_dir per loaded module, a
post-login RPC would be answered silently from the pre-login store — which is worse
than the direct call it replaced, because it is wrong rather than absent.

TinyMemory already solves exactly this: its root object takes a directory and
returns a new object path to talk to. An Open(data_dir) -> object_path member
here would follow the same shape.

Unrelated, but worth fixing in the same release

The v0.3.1 archives contain libtinymcp.so. OpenHuman's
modules::ops::platform_library_name derives libtinymcp_module.so from the
module id, matching every other first-party module. Downloads still work — tinybus
locates the library itself — but the warm-start installed_artifact fast path
never matches, so the module re-downloads on every process start. Renaming the
cdylib to tinymcp_module would fix it.

Not asked for

Prompt-injection detection over remote tool definitions, the RPC surface, the
agent-facing tools and proxy scoping all stay in OpenHuman — that split is right
and lib.rs already documents it. Nothing here asks the module to take on host
policy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions