Skip to content

v0.7.0

Latest

Choose a tag to compare

@github-actions github-actions released this 23 Oct 18:17
· 22 commits to master since this release

Installation

pip install toolregistry-server==0.7.0

Added

  • A FastAPI-free OpenAPI document builder: toolregistry_server.adapters.openapi.spec.build_openapi_schema(route_table) returns the OpenAPI 3.1 document straight from the RouteTable, emitting one POST per enabled route with the same operationId, summary (description truncated at 120 characters), namespace tag, inlined parameter schema, x-* execution-hint extensions and the /tools + /events meta operations as the FastAPI app publishes. Disabled routes are simply absent, so no post-filtering step is needed. Each operation also carries its response contract — 200 with the tool's declared outputSchema (or an empty schema) and 422 with the inlined {"detail": [...]} validation envelope — and security_schemes / security arguments emit components.securitySchemes and the document-level requirement, so bearer auth survives the port. This is the spec half of removing FastAPI from the adapter (epic #101); the app still serves /openapi.json through FastAPI until the port lands. The parity test resolves FastAPI's $refs, drops pydantic title keys, and compares request and response bodies path by path.
  • MCP tool annotations and title (#280): tools/list now carries title from Tool.metadata.title and annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) from Tool.execution_hints. Hints that are not declared stay absent, so a tool that claims nothing keeps the minimal {name, description, inputSchema} definition. The Python field spelling differs between MCP SDK v1 (camelCase) and v2 (snake_case); both serialize to the spec's camelCase JSON keys.
  • OpenAPI operation extensions and route accessors (#280): generated operations carry x-read-only, x-destructive, x-idempotent, x-open-world and x-title for the hints a tool declares, matching the MCP annotations; undeclared hints are omitted. RouteEntry gains title and execution_hints properties so both adapters read the same metadata instead of reaching into route.tool.
  • --server-url for the MCP subcommand (#78): sets the public URL used in OAuth protected-resource metadata, which previously could only be passed as an adapter.run(server_url=...) kwarg — so CLI deployments behind a reverse proxy had no way to advertise a reachable origin and fell back to the bind address. The server now also logs a warning at startup when the advertised URL would be derived from a wildcard bind address (0.0.0.0, ::).
  • --tokens for the MCP subcommand (#99): the adapter already accepted tokens_path and the guide documented the flag, but only the openapi subcommand defined it, so mcp --transport http --tokens tokens.txt died in argparse. API_BEARER_TOKEN still works as before.
  • MCP serverInfo carries a version and usage instructions (#79): initialize returned an empty serverInfo.version, because the adapter never passed one to the SDK. It now reports the installed toolregistry-server version, and embedders can pass their own — App and the CLI forward ServerIdentity.version, so the Hub advertises the Hub version instead of the server library's. When discover_tools is served, the response also carries instructions describing the two-step deferred-tool flow (discover_tools, then call_deferred), which a client could previously only infer from tool descriptions.
  • subscriptions/listen is served on MCP SDK v2 (#107): the 2026-07-28 wire has no standing notification channel, so the SDK drops a notifications/tools/list_changed sent on the connection and modern-era clients never learned that the tool list changed. The adapter now registers the SDK's listen handler over an in-process bus and publishes a tools-list-changed event on every route-table change, so a client that opens a listen stream (SEP-2575) receives it and can refetch tools/list. Legacy-era clients keep the existing session notification path; SDK builds without the listen hook are unaffected.

Changed

  • Core floor is toolregistry>=0.19.0 and the discovery/profile regression test is enforced (#115): the server now requires the core line that invalidates the discovery index on disable events, so apply_profile() can no longer leave a disabled tool discoverable. The test no longer probes the installed core and skips on older releases; it runs unconditionally.
  • tools/list is cached for five minutes by default (#114, behavior change): with subscriptions/listen served (#110), a listening client's cache is invalidated on every change, so ttlMs now defaults to the spec's example (300000) instead of 0. A client that does not listen keeps the list for up to five minutes; --tools-list-ttl-ms 0 restores refetch-every-time. cacheScope remains private.
  • tools/list cache hints are configurable (#83): --tools-list-ttl-ms and --tools-list-cache-scope set the ttlMs / cacheScope hints on tools/list (SDK v2), which previously could only be passed as adapter kwargs and never were. The default stays ttlMs: 0 ("never cache") until notifications/tools/list_changed is verified on SDK v2 (#107) — caching without a working invalidation would leave clients showing tools that no longer exist. cacheScope defaults to private, since the served set depends on profile and authorization.
  • Unknown or disabled tools are protocol errors (#81, behavior change for MCP clients): tools/call for a tool that does not exist, or that is currently disabled, now returns JSON-RPC error -32602 (Invalid params) instead of a CallToolResult with isError: true. The request itself was invalid, so hosts should not hand it back to the model as a tool result. Tool execution failures keep returning isError: true so the model can read the message and adapt. Works on both MCP SDK v1 and v2.

Fixed

  • Dataclass tool results are serialized structurally (#187, #192): _serialize_result used json.dumps(..., default=str), so a tool returning list[SearchResult] reached the model as a JSON array of Python repr strings — not machine-readable, and un-indexable by field. Dataclasses (top-level or nested), datetimes, enums, paths, byte strings and sets now encode to their JSON equivalents; str remains only the last resort.
  • A tools/call resolves its route once (#97): the route was looked up twice — once in the SDK v1 pre-dispatch hook, once in the handler. A disable landing between the lookups made the two SDK generations answer the same request differently: v1 an isError result, v2 a -32602 protocol error. The hook's result is now carried to the handler.
  • Tool-list change notifications track the connection, not the request (#108): on MCP SDK v2 every request gets a fresh ServerSession, so the session list_tools registered was garbage-collected before any change event could use it, and the tracker was always empty. It now records the underlying connection, which lives as long as the client. Delivery to modern-era connections still needs a back-channel (#107), so this is a prerequisite rather than a complete fix.
  • Only connections that can receive a change event are tracked (#111): since #108 the tools/list handler tracks session._connection, but on the 2026-07-28 path that object is created per request and has no change-notification channel, so every tools/list added one dead entry that was never released. Tracking now skips modern-era connections (single-exchange HTTP and modern duplex alike); those clients are served by subscriptions/listen (#110). Handshake-era clients are still tracked.
  • A route change outside an event loop no longer leaves a coroutine behind (#117): the notification scheduler built its coroutine before checking for a running loop, so building a server or mutating a route table from synchronous code emitted RuntimeWarning: coroutine ... was never awaited. The coroutine is now created only when there is a loop to run it.
  • initialize no longer advertises a disabled discovery stack (#112, #113): the default instructions were attached whenever discover_tools existed in the route table, including when a profile had disabled it, so a client was told to call a tool that answers -32602. The guidance now requires both discover_tools and call_deferred to be enabled.
  • Source-level include/exclude filters are forwarded (#88): the config loader parsed them, but register_mcp_source() and register_openapi_source() never passed them on, so a filtered remote source registered every tool. They are now forwarded unchanged (an explicit empty tuple still means "match nothing"). Takes effect once the installed toolregistry provides the fields; on an older core the server behaves as before.
  • MCP extra floor raised to mcp>=1.22.0 (#95): SDK releases below 1.22 import a pydantic internal that pydantic 2.12+ removed (eval_type_backport), so a resolver landing anywhere in 1.17–1.21 produced an install that fails at import mcp.server.lowlevel. 1.21 fails and 1.22 imports, so the floor is the boundary. The test-mcp-v1 CI job now installs the declared floor and prints the version it tested.
  • Startup banner no longer corrupts the stdio stream (#80): print_banner() wrote to stdout, which over the MCP stdio transport is the JSON-RPC channel — clients read banner art between their messages, sometimes after the first responses. The banner now goes to stderr, alongside the logs, and print_banner() accepts an optional file argument. --no-banner still suppresses it. The Hub calls the same helper, so it is fixed without a Hub change — but only once its installed toolregistry-server crosses the version that carries this.

Security

  • Disable reasons no longer disclose the deployment profile (#84): apply_profile() stored Disabled by profile 'remote' as the tool's disable reason, and that string reaches callers through the MCP tools/call error and core's RuntimeError. The stored reason is now the neutral "not available on this server"; the profile name is only logged server-side.

Full Changelog: v0.6.0...v0.7.0