MCP Discovery¶
When a workflow needs a capability nothing installed provides, the answer is not "I can't do that" — it is "install this server and set these two variables". Discovery is what turns a gap into an instruction.
Three things, and they are the same three whether a person or the Architect is asking: search, what would this cost me, and install it.
from neurosurfer.mcp.registry import search_registry, registry_detail, install_config
from neurosurfer.mcp.registry import credential_requirements
This client is FastAPI-free on purpose. The logic used to live inside a route module and raise HTTPException, which made it reachable from a browser and from nowhere else — while the Architect runs in-process for CLI and library callers that have no web server at all. Both callers now get the same answers from the same code; the route module maps McpRegistryError to HTTP.
Search¶
Results are cached for 15 minutes. The registry API is slow enough — it sits on a ~14s plateau for roughly a third of calls — that a short TTL is paid for in seconds of staring at a spinner.
What it needs before it will run¶
This is the piece that did not exist before, and the difference between "I can't do Gmail" and "install …/gmail-mcp and set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET".
reqs = credential_requirements(entry)
for r in reqs:
print(r.name, r.required, r.secret, r.description)
The registry declares what a server needs to run — environmentVariables on a package, headers on a remote, each with isRequired and isSecret — and that was being thrown away.
Ask for the placeholder, not the slot¶
A templated credential's name is where the value goes (Authorization); the thing a person can actually supply is the placeholder inside it (smithery_api_key).
A checklist that says "provide Authorization" is asking for something nobody possesses under that name. Conflating the two is how the Architect came to refuse a workflow over a header whose declared value was Bearer {smithery_api_key} — a placeholder naming a key already stored in settings, with the code to fill it already written.
Declared ≠ blocked¶
A registry entry declaring its requirements is a statement about the server, not about whether you can run it. Those are different questions and only the second should ever block a build.
from neurosurfer.mcp.credentials import blocking, satisfied_from, use_credentials
with use_credentials(my_secrets):
still_needed = blocking(reqs) # what this account genuinely cannot supply
Availability is ambient and account-scoped: the credentials an account holds are resolved once on the request thread and bound for the work, because an Architect build runs on a raw thread and inherits no context of its own.
Install¶
default_runtime(entry) picks the runtime when an entry offers several.
Discovery engines¶
Two sources, and which one is active is a setting.
Official — the default¶
The official MCP registry. Always available, no key, keyword search.
Smithery — opt-in, experimental¶
Faster and semantically stronger. Measured against the official registry on the same machine, six identical repeated queries each: median 0.68s vs 11.94s, and consistent rather than erratic.
It also answers two questions the official index structurally cannot:
- What a server exposes, before anything is installed —
toolswith input and output schemas, pluspromptsandresources. - How much a server is used (
useCount, observed 0 → 25,335) — the only popularity signal available anywhere in the ecosystem, and the one honest way to rank a search where thousands of entries share a keyword.
Its search is genuinely semantic: "send a text message" returns telnyx and two SMS gateways with no literal token match.
Why it stays opt-in: it needs the user's own API key, it is closed source, and servers found through it run on its infrastructure. Choosing it without a key is refused rather than silently returning nothing.
security.scanPassed is not a health check
It exists in the Smithery schema and came back null on every entry sampled, authenticated and not. Do not read it as a security verdict.
The runtime¶
Non-CLI callers — the workflow runner (synchronous), the Architect, the server — need MCP tools without owning an event loop that outlives the call.
from neurosurfer.mcp.runtime import ensure_mcp_tools
statuses = ensure_mcp_tools() # sync, idempotent, safe to call opportunistically
No configured servers → no-op; already running → returns the current statuses.
One manager per server. Each configured server gets its own McpManager driven by its own long-lived task: connect → wait for stop → aclose. That shape is required — the anyio stdio transports demand same-task setup and teardown — and it is what makes independent start/stop possible: stopping one server no longer tears down the others.
The host loop stays alive for the process, so a tool's cross-loop marshalling works from any thread or loop, graph-executor threads included.
Starting a run connects every enabled server
Which is why workflow requirements include enabled servers whether or not the workflow names one of their tools — one of them missing a value fails the run regardless.
Next¶
- MCP — connecting a server you already know about.
- Tool Registry — how an imported server's tools rank against core ones.
- The Architect — the caller this exists for.