Use servers from an MCP registry
This guide shows how to take MCP servers from a registry, such as the MCP
project's registry at https://registry.modelcontextprotocol.io, instead of
copying each server's URL into your configuration. It is for operators who
approve servers and builders who call them from workflows and instruction
documents. After it you can approve a set of names from a registry, call one
of them from a workflow step by name, and read in the logs which version and
endpoint each run used.
A registry is a directory of server.json records: a namespaced name
(io.github.github/github-mcp-server), a version, and the endpoints the
server is reached at. agentd reads a record when a server is first used,
dials the endpoint it names, and never treats the registry as an authority:
your configuration decides which names may be used and how far their tools
are trusted.
Approve servers from a registry
Declare the registry under mcp.registries and list the names you approve
in servers. A missing or empty servers approves nothing.
agent:
name: issue-desk
instruction: "Summarize the open issues in acme/webapp."
intelligence:
endpoints: https://api.openai.com/v1/chat/completions
token: "{{secret:OPENAI_API_KEY}}"
models:
smart: {model: gpt-5.1}
model: smart
mcp:
registries:
- name: official
url: https://registry.modelcontextprotocol.io
servers: ["io.github.github/*", "com.deepwiki/*"]
tags: {"*": [untrusted_input]}
servers:
- name: github
registry: official
server: io.github.github/github-mcp-server
version: latest
headers: {Authorization: "Bearer {{secret:GITHUB_TOKEN}}"}
allow: ["get_*", "list_*"]
serversare globs over registry names. A name isnamespace/server;*matches within one segment and never across the/, soio.github.github/*approves every server in that namespace and nothing else.*alone approves every name.tagsare the Rule-of-Two legs every server from the registry counts as. The default is{"*": [untrusted_input]}.trustedis refused on a registry: vouch for one server by giving it anmcp.serversentry of your own with its owntags.allowandexcludeon the registry are the widest tool surface any server from it offers. A server's ownallownarrows it.- The
mcp.serversentry names the server byregistry+serverin place of anendpoint. It is dialed on first use unless it saysconnect: boot.
Every key is in the configuration reference.
Find a server
agentd mcp registry search lists what a registry serves:
agentd mcp registry search github
agentd mcp registry search deepwiki --registry https://registry.modelcontextprotocol.io
agentd mcp registry search github -c agentd.yaml
Each line is a name, its latest version and its remotes:
io.github.github/github-mcp-server 2.0.3 streamable-http https://api.githubcopilot.com/mcp/
With -c, it searches the registries the configuration declares and adds
approved or not approved after each line, by their servers globs. A
server that publishes only packages (stdio, npm, OCI) says so: agentd cannot
dial it.
Call a registry server from a workflow
A step names the server where it is used, with no mcp.servers entry:
# fragment: under workflows:
- name: triage
steps:
start: {kind: manual}
issue:
kind: mcp.tool
registry: official
server: io.github.github/github-mcp-server
tool: get_issue
args: {owner: acme, repo: webapp, issue_number: "{{inputs.number}}"}
depends_on: [start]
done: {kind: finish, depends_on: [issue]}
- The registry's
serversmust approve the name, or the workflow is refused at load.registry,serverandversioncannot be templated. - The server is dialed when the step first runs. The step waits for the
connection durably, up to the registry's
timeout(ormcp.default_timeout), as it waits for anyconnect: on_useserver. - Every step naming the same server and version shares one connection,
named
<registry>/<server>(official/io.github.github/github-mcp-server), or<registry>/<server>@<version>for an exact version. - When you also declare an
mcp.serversentry for that server, the step's connection takes itsauth,headers,tagsand tool ceiling. mcp.resourceandwait {on: resource}take the sameregistry,serverandversionfields.- A server a step (or the model's
mcp.registry.connect) made stays across a reload while its registry still approves it; an applied reload whose registries no longer do drops it (mcp.registry.server.dropped). A refused reload changes nothing, so it drops nothing.
There is no search node: a workflow names its servers. To sign in or warm a server up ahead of the step that needs it, call a cheap tool on it in an early step.
A run keeps its version
A run calls the version of a server it first called. If the registry publishes a new version and the server is dialed again (after a restart or a reload), a run that started on the old version keeps calling the old one on a connection of its own, and new runs use the new one. The log says so:
{"event":"mcp.registry.pinned","run":"r-1","server":"io.acme/kb","version":"1.0.0","resolved":"2.0.0","name":"official/io.acme/kb@1.0.0"}
Let an instruction document name a server
A document names a registry server by the registry's URL and the server's name:
:::mcp{id=gh registry="https://registry.modelcontextprotocol.io" server=io.github.github/github-mcp-server tools="get_issue"}
GitHub, for issue lookups.
:::
- The
registryURL must equal one of yourmcp.registries[].url, and the name must pass itsservers. Otherwise the block stays text and the load says why, as ahost-policywarning. - The block becomes your
mcp.serversentry for that server when you have one, or a server of its own under the registry's tags and ceiling. Itstools=only narrows the tool list. - A
srcbesideregistryandservermust be one of the record's remotes, or the document's block is refused when the server is resolved: the document and the registry disagree (mcp.registry.refused). When the block became your own entry, only the document's steps that named it fail; your entry resolves and connects as it would without the document, and asrcthe record holds does not choose its remote. A server the document alone declared is refused. - The document's workflow steps that name its
idcall that server. It is dialed right after the load. accountandresolverare not taken from a document; each is ahost-policywarning.
Let the model find servers
With discover: true on a registry, the model gets two tools:
| Tool | Does |
|---|---|
mcp.registry.search {registry, query} | Lists the matching records whose names your servers approve: name, title, description, version, remote kinds. |
mcp.registry.connect {registry, server} | Connects an approved name at the registry's tags. Its tools are offered once it has connected. |
discover is off by default. It adds no reach the approval did not: the
lethal-trifecta check at load already counts every approved name at the
registry's tags, whether or not any is connected.
How a record is resolved
When a registry server is first dialed, agentd reads
GET {url}/v0.1/servers/{name}/versions/{version} and holds the record to
these rules:
| Rule | What happens |
|---|---|
The record's name differs from the name asked for | refused |
An exact version was asked for and the record's version differs | refused |
status: deleted | refused |
status: deprecated | connects, and warns mcp.registry.deprecated |
A streamable-http remote | dialed |
An sse remote | skipped, said in mcp.registry.resolved.skipped: agentd's client speaks Streamable HTTP only |
Only packages (stdio, npm, OCI, PyPI) | refused: agentd starts no local process |
A remote URL that is not https://, or whose host resolves to a private address | refused, unless the registry sets allow_private: true |
A remote header with a {variable} value | filled from the mcp.servers entry's header of that variable's name |
| A required remote header left unfilled | refused, naming it |
| A remote header marked secret with a fixed value | not sent: a registry never supplies a credential |
A refused server fails the steps that name it with the reason, and its
record is read again after refresh. The record is kept in the store and
reused for refresh (default 1h), across restarts.
A live connection is not torn down when its record goes stale. When it is
dialed again — its server lost the session — a record older than refresh
is read again first: a remote that moved is followed, and a record the
registry has since marked deleted is refused (mcp.registry.refused) and
the server stays down. The log says mcp.disconnect with registry: stale.
A run keeps its pinned version across the re-dial.
A server that needs authorization goes through its auth block, as any
mcp.servers entry does. See Connect MCP servers.
Closed egress
Under security.egress: closed, a registry entry declares both the
registry's API host and the servers it resolves: a resolved remote is
admitted because the registry entry approved its name. Nothing else changes.
Next steps
- Connect MCP servers: the
mcp.serversentry, auth and tool ceilings. - Configuration reference: every
mcp.registrieskey. - Security model: why a registry is a directory, not an authority.
- Telemetry reference: the
mcp.registry.*events.