Repository navigation
Installation
pip install toolregistry-server==0.7.0Added
- A FastAPI-free OpenAPI document builder:
toolregistry_server.adapters.openapi.spec.build_openapi_schema(route_table)returns the OpenAPI 3.1 document straight from theRouteTable, emitting onePOSTper enabled route with the sameoperationId,summary(description truncated at 120 characters), namespace tag, inlined parameter schema,x-*execution-hint extensions and the/tools+/eventsmeta 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 —200with the tool's declaredoutputSchema(or an empty schema) and422with the inlined{"detail": [...]}validation envelope — andsecurity_schemes/securityarguments emitcomponents.securitySchemesand 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.jsonthrough FastAPI until the port lands. The parity test resolves FastAPI's$refs, drops pydantictitlekeys, and compares request and response bodies path by path. - MCP tool annotations and title (#280):
tools/listnow carriestitlefromTool.metadata.titleandannotations(readOnlyHint,destructiveHint,idempotentHint,openWorldHint) fromTool.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-worldandx-titlefor the hints a tool declares, matching the MCPannotations; undeclared hints are omitted.RouteEntrygainstitleandexecution_hintsproperties so both adapters read the same metadata instead of reaching intoroute.tool. --server-urlfor the MCP subcommand (#78): sets the public URL used in OAuth protected-resource metadata, which previously could only be passed as anadapter.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,::).--tokensfor the MCP subcommand (#99): the adapter already acceptedtokens_pathand the guide documented the flag, but only theopenapisubcommand defined it, somcp --transport http --tokens tokens.txtdied in argparse.API_BEARER_TOKENstill works as before.- MCP
serverInfocarries a version and usage instructions (#79):initializereturned an emptyserverInfo.version, because the adapter never passed one to the SDK. It now reports the installedtoolregistry-serverversion, and embedders can pass their own —Appand the CLI forwardServerIdentity.version, so the Hub advertises the Hub version instead of the server library's. Whendiscover_toolsis served, the response also carriesinstructionsdescribing the two-step deferred-tool flow (discover_tools, thencall_deferred), which a client could previously only infer from tool descriptions. subscriptions/listenis served on MCP SDK v2 (#107): the 2026-07-28 wire has no standing notification channel, so the SDK drops anotifications/tools/list_changedsent 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 refetchtools/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.0and the discovery/profile regression test is enforced (#115): the server now requires the core line that invalidates the discovery index on disable events, soapply_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/listis cached for five minutes by default (#114, behavior change): withsubscriptions/listenserved (#110), a listening client's cache is invalidated on every change, sottlMsnow defaults to the spec's example (300000) instead of0. A client that does not listen keeps the list for up to five minutes;--tools-list-ttl-ms 0restores refetch-every-time.cacheScoperemainsprivate.tools/listcache hints are configurable (#83):--tools-list-ttl-msand--tools-list-cache-scopeset thettlMs/cacheScopehints ontools/list(SDK v2), which previously could only be passed as adapter kwargs and never were. The default staysttlMs: 0("never cache") untilnotifications/tools/list_changedis verified on SDK v2 (#107) — caching without a working invalidation would leave clients showing tools that no longer exist.cacheScopedefaults toprivate, since the served set depends on profile and authorization.- Unknown or disabled tools are protocol errors (#81, behavior change for MCP clients):
tools/callfor a tool that does not exist, or that is currently disabled, now returns JSON-RPC error-32602(Invalid params) instead of aCallToolResultwithisError: true. The request itself was invalid, so hosts should not hand it back to the model as a tool result. Tool execution failures keep returningisError: trueso 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_resultusedjson.dumps(..., default=str), so a tool returninglist[SearchResult]reached the model as a JSON array of Pythonreprstrings — 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;strremains only the last resort. - A
tools/callresolves 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 anisErrorresult, v2 a-32602protocol 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 sessionlist_toolsregistered 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/listhandler trackssession._connection, but on the 2026-07-28 path that object is created per request and has no change-notification channel, so everytools/listadded one dead entry that was never released. Tracking now skips modern-era connections (single-exchange HTTP and modern duplex alike); those clients are served bysubscriptions/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. initializeno longer advertises a disabled discovery stack (#112, #113): the defaultinstructionswere attached wheneverdiscover_toolsexisted 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 bothdiscover_toolsandcall_deferredto be enabled.- Source-level
include/excludefilters are forwarded (#88): the config loader parsed them, butregister_mcp_source()andregister_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 installedtoolregistryprovides 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 atimport mcp.server.lowlevel. 1.21 fails and 1.22 imports, so the floor is the boundary. Thetest-mcp-v1CI 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, andprint_banner()accepts an optionalfileargument.--no-bannerstill suppresses it. The Hub calls the same helper, so it is fixed without a Hub change — but only once its installedtoolregistry-servercrosses the version that carries this.
Security
- Disable reasons no longer disclose the deployment profile (#84):
apply_profile()storedDisabled by profile 'remote'as the tool's disable reason, and that string reaches callers through the MCPtools/callerror and core'sRuntimeError. 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