Skip to main content

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.
Another agent then uses it like any other MCP server:
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 serving word_count gets this schema, shown here as MCPManager.get_tools() returns it:
The server also sends MCP 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.
Two targets that resolve to the same name raise ValueError, so use a dict when names could collide. add_tool() registers more targets before the server starts.

Serving swarms

Anything with a run(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.
A swarm that returns a dict or list reaches the client as indented JSON text.

State between calls

One target instance serves every call, from every client. An Agent keeps its conversation between run() calls, so each task lands in the same history, and calls that arrive together run on separate threads against the same object. When calls must not share context, serve a function that builds a fresh agent per call:

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:
  1. allow_anonymous=True admits everything.
  2. auth decides alone. The static keys and token verifier are not consulted.
  3. No credential in either header: refused.
  4. token_verifier verifies the token, its expiry, and required_scopes. When a verifier is set, the static keys are not consulted.
  5. The credential is compared against the static keys.
A refused request gets:
The client can send its key either way:
MCPConnection(api_key=...) and MCPManager(api_key=...) handle this for you.

Custom auth

A swarms client sends the extra header through MCPConnection(headers={"x-tenant": "acme"}, api_key="sk-acme").

Token verifier

Any object with an async verify_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 an AuthResult 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 pass allow_anonymous=True.
On stdio, stdout carries the protocol. Set show_banner=False, since the banner is printed to stdout, and keep print_on=False on served agents so nothing else is written there.
start() and build_app() need an HTTP transport and raise ValueError on stdio.

Methods

run

Serve until interrupted. Blocks. For the HTTP transports this runs 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

Serve on a background daemon thread and return once the socket is accepting connections. Returns the deployer. Raises 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

Stop a server started with start(), waiting up to wait seconds for the thread to finish.
A deployer serves once. On the streamable HTTP transport, calling start() again after stop() fails with RuntimeError: MCPDeployer did not start, because the underlying mcp session manager can only run once per instance. Build a new MCPDeployer to serve again.

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

Register one more target as a tool. Call it before run() or start(); after start() it raises RuntimeError. A name that is already registered raises ValueError. Returns the ServedTool.

authenticate

The check the auth layer runs on every request. Returns an AuthResult when the request is admitted and None when it is refused. Useful for testing an auth setup without starting a server.

build_app

The ASGI app: the MCP transport wrapped in the auth layer. Serve it with your own ASGI server when 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 the startup banner: server name, tools, endpoint, transport, and auth mode. Pass a 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

Builds an 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.
The framework repository has more, covering every auth mode and transport: examples/mcp/mcp_deployer/.

Source

swarms/structs/mcp_deployer.py