Overview
MCPDeployer is the server side of MCP in Swarms. MCPManager connects an agent to an MCP server; MCPDeployer turns an agent into one.
Give it one target or several. A target is an Agent, any swarm with a run() method (SequentialWorkflow, SwarmRouter, HierarchicalSwarm, …), or a plain Python callable. Each target becomes one MCP tool. Every HTTP request passes through an auth layer before it reaches the MCP transport, and a server with no auth configured refuses to build.
MCPDeployer ships in swarms 15.0.3 and needs mcp>=2.0.0. It builds on mcp.server.mcpserver.MCPServer, which the 1.x releases of mcp do not have.Import
How a target becomes a tool
The call runs on a worker thread, so a blocking agent does not stall the server. Set
timeout to cap how long one call may take; see Timeouts.
What a client sees
A client that lists the tools of a server servingword_count gets this schema, shown here as MCPManager.get_tools() returns it:
instructions on connect: one name: description line per target. Functions passed in extra_tools keep their own signatures as their schemas and are listed before the targets. tool_names, tools, and /health cover the targets only.
Serving several targets
Pass a list to name each tool after its target, or a dict to choose the names yourself. Each entry becomes its own tool on the same server, behind the same auth.ValueError, so use a dict when names could collide. add_tool() registers more targets before the server starts.
Serving swarms
Anything with arun(task) method is served the same way as an agent: SequentialWorkflow, ConcurrentWorkflow, SwarmRouter, HierarchicalSwarm, and the rest. The tool name comes from the swarm’s name, and the description from its description.
State between calls
Constructor
Targets and naming
Any | List[Any] | Dict[str, Any]
required
What to serve. One target, a list of targets, or a dict of tool name to target. A target is an
Agent, any object with a run(task, ...) method, or a callable taking the task string. Each becomes one tool.Optional[str]
default:"None"
Server name advertised to MCP clients. Defaults to the first tool’s name.
Optional[str]
default:"None"
Tool name for a single target. With several targets, pass a dict instead. Passing it with a list or dict raises
ValueError.Optional[str]
default:"None"
Tool description for a single target. Same restriction as
tool_name.Optional[Iterable[Callable]]
default:"None"
More plain functions to expose beside the targets. Each function’s signature becomes its schema and its docstring its description, so give every one a docstring.
Transport
str
default:"127.0.0.1"
Bind address. Binding anywhere other than
127.0.0.1 or localhost turns off the mcp package’s DNS-rebinding guard, which would otherwise reject every request whose Host header is not localhost.int
default:"8000"
Bind port.
str
default:"streamable-http"
"streamable-http", "sse", or "stdio". Anything else raises ValueError. Auth applies to the two HTTP transports only. A swarms client reaching an SSE server must set transport="sse" on its MCPConnection (or mcp_transport="sse" on the agent), because clients default to streamable HTTP.Optional[str]
default:"None"
URL path of the MCP endpoint. Defaults to
/mcp, or /sse for the SSE transport.bool
default:"False"
Streamable HTTP only. Reply with plain JSON instead of an event stream.
bool
default:"True"
Streamable HTTP only. Keep no per-session state, which is what you want behind a load balancer.
Optional[float]
default:"None"
Seconds one tool call may run before it fails.
None or 0 means no limit. See Timeouts.Auth
Optional[Iterable[str]]
default:"None"
Static keys. Compared in constant time. Blank entries are dropped and duplicates removed.
Optional[str]
default:"None"
Name of an environment variable holding more keys, comma-separated. Read once, at construction.
str
default:"x-api-key"
Header a client may send a raw key in. A
Bearer prefix in it is stripped. Authorization: Bearer <key> is always accepted as well.Callable[[Optional[str], Headers], bool | dict | None]
default:"None"
Your own check, sync or async. It receives the credential (or
None) and the request headers. A truthy return admits the request; a dict return is also kept as the request’s claims, with sub/subject and scopes read from it. A falsy return or an exception refuses the request. Takes precedence over every other auth setting.Optional[TokenVerifier]
default:"None"
An
mcp.server.auth.provider.TokenVerifier. Used when auth is not set. A token that fails verification, has expired, or lacks any of required_scopes is refused.Optional[Iterable[str]]
default:"None"
Scopes a verified token must carry.
bool
default:"False"
Serve with no auth at all. Off by default: with no
api_keys, api_key_env keys, auth, or token_verifier, the constructor raises ValueError.Optional[Iterable[str]]
default:"('/health',)"
Paths that skip auth.
Output
bool
default:"False"
Log every admitted tool call.
bool
default:"True"
Print the startup banner from
run() and start().Auth, in order of precedence
authenticate() checks these in order and stops at the first that applies:
allow_anonymous=Trueadmits everything.authdecides alone. The static keys and token verifier are not consulted.- No credential in either header: refused.
token_verifierverifies the token, its expiry, andrequired_scopes. When a verifier is set, the static keys are not consulted.- The credential is compared against the static keys.
MCPConnection(api_key=...) and MCPManager(api_key=...) handle this for you.
Custom auth
MCPConnection(headers={"x-tenant": "acme"}, api_key="sk-acme").
Token verifier
Any object with an asyncverify_token(token) method that returns an mcp.server.auth.provider.AccessToken, or None, works as a verifier.
Who made the request
An admitted request carries anAuthResult in its ASGI scope, at scope["state"]["mcp_auth"]:
The module-level helper
credential_from_headers(headers, api_key_header="x-api-key") in swarms.structs.mcp_deployer returns the credential exactly as the auth layer reads it, which is handy in tests.
Timeouts
timeout bounds one tool call on the server. When it expires, the client gets a tool result with is_error set instead of an answer, even if the target is still blocked inside a model call.
The worker thread running the target is abandoned, not killed: Python cannot stop a thread, so the agent finishes its run in the background and its result is discarded. Budget model spend with that in mind.
The client has its own limit. A swarms client waits tool_timeout seconds for one tool call (default 120, set on MCPConnection), so serving a slow swarm with timeout=300 also needs a client that waits that long:
stdio
Desktop MCP hosts launch servers as subprocesses and talk over stdin and stdout. There are no headers, so the host is the security boundary. The constructor still requires an auth setting, so passallow_anonymous=True.
start() and build_app() need an HTTP transport and raise ValueError on stdio.
Methods
run
uvicorn; for stdio it hands stdin/stdout to the MCP server and, unless allow_anonymous=True, logs a warning that auth settings are ignored, since stdio carries no headers.
start
RuntimeError if the server has not started within wait seconds (a port already in use ends up here), and ValueError for the stdio transport. Calling it again while running is a no-op. Uvicorn logs at warning level here, so per-request access lines are not printed; run() logs at log_level.
stop
start(), waiting up to wait seconds for the thread to finish.
Context manager
with MCPDeployer(...) as deployer: calls start() on entry and stop() on exit. This is the easiest way to run a server and a client in one script or test.
add_tool
run() or start(); after start() it raises RuntimeError. A name that is already registered raises ValueError. Returns the ServedTool.
authenticate
AuthResult when the request is admitted and None when it is refused. Useful for testing an auth setup without starting a server.
build_app
run() and start() are not what you need, for example uvicorn.run(deployer.app, host="0.0.0.0", port=8000). Only HTTP requests are checked; lifespan events pass straight through, so the MCP session manager starts normally. If you mount it inside another app, that app must run this app’s lifespan. The app property builds it once and caches it. Raises ValueError for stdio.
print_banner
rich.console.Console to print somewhere other than stdout. run() and start() call it unless show_banner=False.
Properties
str
http://{host}:{port}{path}, the address to give a client. With host="0.0.0.0" this shows the bind address, so give remote clients the machine’s own hostname instead.List[str]
Every registered tool name, in registration order.
Dict[str, ServedTool]
Tool name to
ServedTool (name, target, description, target_type).str
The first registered tool’s name.
Any
The first registered target.
str
The first registered tool’s description.
List[str]
The resolved static keys, from
api_keys and api_key_env together.mcp.server.mcpserver.MCPServer
The underlying
mcp server, with every tool already registered.ASGI app
The cached result of
build_app().Health check
GET /health is always public and returns:
deploy_as_mcp
MCPDeployer from target and the same keyword arguments, then calls run(). Blocks until the server stops, then returns the deployer. target takes the same forms as targets: one target, a list, or a dict.
Errors
Errors inside a tool call never reach the client as exceptions. A target that raises, or a call that passes
timeout, comes back as a tool result with is_error set and the error text as its content.
Examples
Serve an agent over MCP
One agent behind an API key, called by a second agent.
Serve a team from one server
Several agents, a workflow, and a function as separate tools.
MCPManager
The client side: how agents connect to servers like this one.
examples/mcp/mcp_deployer/.
Source
swarms/structs/mcp_deployer.py