MCP servers
The Model Context Protocol standardises
how tools, prompts, and resources expose themselves to LLMs.
mantis-agent-sdk is both an MCP client (any agent can talk to MCP
servers) and an MCP server-runtime (you can author servers in-process
using the same @tool decorator).
In-process server
The fastest way to expose a set of tools as an MCP server:
from mantis_agent import MantisAgentOptions, create_sdk_mcp_server, tool
@tool
async def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
calc = create_sdk_mcp_server(
name="calculator",
version="0.1.0",
tools=[add],
)
options = MantisAgentOptions(
mcp_servers=[calc],
)The server runs in the same process — no subprocess, no socket — but exposes the full MCP protocol surface to the agent loop. From the model's point of view it's identical to an out-of-process server.
External servers (stdio / sse / http)
options = MantisAgentOptions(
mcp_servers=[
# stdio: spawn a subprocess
{"transport": "stdio", "command": "uvx", "args": ["mcp-server-fetch"]},
# sse: connect to a Server-Sent Events endpoint
{"transport": "sse", "url": "https://mcp.example.com/sse"},
# http: connect via streamable-http transport
{"transport": "http", "url": "https://mcp.example.com/mcp"},
],
)Each transport starts its handshake at session start and tears down at
session end. Failures during handshake surface as McpServerError hooks;
failures mid-call surface as tool errors.
Elicitation
Servers can prompt the user mid-tool-call. The ctx.elicit() API
inside a server-side tool blocks the call until the agent gathers a
response:
# server side
@tool
async def book_flight(destination: str) -> str:
"""Book a flight."""
seat = await ctx.elicit(
prompt="Window or aisle?",
options=["window", "aisle"],
)
return f"Booked {destination}, {seat} seat."The agent loop pauses, surfaces a system message of subtype
elicit_request, gathers a response (from your UI / human-in-the-loop /
config-driven default), and returns it to the server.
Sampling
Servers can call back into the agent's model to do their own generation. Useful when a server tool needs an LLM but shouldn't ship its own model dependency:
# server side
@tool
async def summarise(text: str) -> str:
"""Summarise text using the calling agent's model."""
result = await ctx.sample(
messages=[{"role": "user", "content": f"Summarise: {text}"}],
system_prompt="You are a concise summariser.",
max_tokens=200,
)
return result.content[0]["text"]The agent receives a sampling_request system message, runs it through
its current model + options, and returns the result to the server.
To allow sampling, register a handler:
sampling_handler is a constructor argument on MCPClient, not an options
key — a "sampling_handler" entry in an options dict is silently ignored:
from mantis_agent.mcp.client import MCPClient, SamplingResult
async def my_sampler(request):
# request.messages carries what the server wants sampled.
return SamplingResult(content="…", model="qwen2.5:7b")
client = MCPClient(config, sampling_handler=my_sampler)MCPClient also takes elicitation_handler, notification_handler, and
request_timeout_s. If a server requests sampling and no handler is set, the
server is told sampling is unsupported and decides how to proceed.
Serving your own tools over MCP
SdkServer is in-process: it exposes @tool functions to the MCP
machinery without a subprocess or a socket. Build one with
create_sdk_server:
from mantis_agent import tool
from mantis_agent.mcp import create_sdk_server
@tool
async def echo(text: str) -> str:
"""Echo the input back."""
return text
config = create_sdk_server(name="echo", tools=[echo])create_sdk_server returns an SdkServerConfig you hand to MCPClient (or
list in mcp_servers) exactly like a remote server's config. Note that
SdkServer.run(inbox, outbox) speaks anyio memory streams, not stdin/stdout —
there is no stdio server runtime in this package, so a serve_stdio() call (as
earlier versions of this page showed) does not exist.
For an out-of-process server, write it with any MCP implementation and connect to it — the client side handles stdio, SSE, and HTTP:
from mantis_agent.mcp import HttpServerConfig, StdioServerConfig
local = StdioServerConfig(command="uv", args=["run", "my_server.py"])
remote = HttpServerConfig(url="https://mcp.example.com", headers={"authorization": "Bearer …"})From the agent side, the same servers as a plain options entry:
options = MantisAgentOptions(
model="qwen2.5:7b",
backend="http://localhost:11434",
mcp_servers=[
{"transport": "stdio", "command": "uv", "args": ["run", "my_server.py"]},
],
)Resources and prompts
Beyond tools, the MCP client speaks the other two halves of the protocol: servers can expose resources (documents, tables, file trees the agent can list and read) and prompts (parameterized prompt templates). Both are available through the client methods on the session — list what a server offers, read a resource by URI, or expand a named prompt with arguments — so any server that publishes them works out of the box.
Terminal integration (.mcp.json)
The mantis terminal discovers MCP servers automatically — Claude Code's
config format, verbatim:
// ./.mcp.json (project) or ~/.mantis-agent/mcp.json (user)
{
"mcpServers": {
"github": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_TOKEN": "..."}},
"internal": {"type": "http", "url": "https://mcp.example.com/api"},
"legacy": {"type": "sse", "url": "https://old.example.com/sse"}
}
}settings.json may also carry a top-level mcpServers object (lowest
priority; project .mcp.json wins by name). Servers connect in the
background at launch — a slow or broken server never delays your prompt, and
each failure is isolated. Tools land namespaced as mcp__{server}__{tool}
and survive model switches. /mcp shows per-server status + tools.
Handshakes time out at 10s and tool calls at 120s, so a mute server fails fast instead of hanging the terminal.
SDK: options.mcp_servers
External transports work through the Claude-SDK-shaped API too:
from mantis_agent import MantisAgentOptions
from mantis_agent.compat_query import query
options = MantisAgentOptions(
model="qwen2.5:7b",
mcp_servers={
"tiny": {"command": "python3", "args": ["./my_mcp_server.py"]}, # stdio
"calc": create_sdk_mcp_server("calc", tools=[echo]), # in-process
},
)
async for msg in query(prompt="call mcp__tiny__ping", options=options):
...Lifecycle is handled for you (MCPManager.start()/stop() — connect at query
start, clean close at the end, safe from async generators).