Loading...
Loading...
Host an MCP server that exposes an AG2 `Agent` (plus prompts and resources) to MCP clients like Claude Desktop, Cursor, or the MCP Inspector. Wrap the agent with `MCPServer(agent)` — it surfaces `Agent.ask()` as a single conversational tool and serves over stdio (`run_stdio()`) or streamable HTTP (it is itself an ASGI app for uvicorn). Covers `MCPServer`, `SessionConfig` (multi-turn history), `Prompt`/`PromptArgument`/`PromptMessage`, `Resource`/`ResourceTemplate`, `AskContext`/`ContextProvider` (per-request injection), `build_ask_tool`, OAuth2 `security=`, and in-process `testing.connect`/`testing.serve` helpers. Use when you want OTHER MCP clients to call YOUR agent. This is the SERVER side — for CONSUMING external MCP servers from an agent (client side) see `ag2-use-builtin-tools` (`MCPServerTool`).
npx skill4agent add ag2ai/ag2-skills ag2-mcpag2.mcp.MCPServerAgent| Direction | You want… | Use |
|---|---|---|
| Server (this skill) | other MCP clients to call your AG2 agent | |
| Client | your AG2 agent to call an external MCP server's tools | |
ag2-use-builtin-toolsaskAgent.ask()pip install "ag2[mcp]"Required. Run this install before delivering the code. Without theextra,mcpresolves to a stub that raises a "missing optional dependency" error on use.from ag2.mcp import MCPServer
import asyncio
from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.mcp import MCPServer
agent = Agent(
name="assistant",
prompt="You are a helpful assistant.",
config=OpenAIConfig(model="gpt-4o-mini"),
)
# The agent is exposed as ONE conversational tool, named "ask" by default,
# taking a required `message` and an optional `context` string.
server = MCPServer(
agent,
name="assistant-mcp", # serverInfo.name in the handshake
instructions="Ask me anything.", # client-facing usage hint (NOT the agent prompt)
)
if __name__ == "__main__":
asyncio.run(server.run_stdio())claude_desktop_config.jsoncommandargs{
"mcpServers": {
"assistant": {
"command": "python",
"args": ["/absolute/path/to/serve_stdio.py"],
"env": { "OPENAI_API_KEY": "sk-..." }
}
}
}The agent must have a modelset. Serving an agent with no config raisesconfig=on the first tool call.MCPAgentConfigError
MCPServeruvicornimport uvicorn
from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.mcp import MCPServer
agent = Agent(name="assistant", prompt="You help users.", config=OpenAIConfig(model="gpt-4o-mini"))
app = MCPServer(agent, path="/mcp") # MCP endpoint mounted at /mcp
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)| Param | Default | Effect |
|---|---|---|
| | URL path the MCP endpoint is served at. |
| | When |
| | Return plain JSON instead of SSE for responses. |
| | OAuth2 bearer enforcement (see below). |
askserver = MCPServer(
agent,
tool_name="consult_expert",
tool_description="Consult the expert agent about a question.",
stream_progress=True, # forward agent stream events as MCP progress/log notifications (default True)
)messagecontextAgent.as_tool()prompts/listprompts/get{name: value}struserPromptMessagefrom ag2.mcp import MCPServer, Prompt, PromptArgument, PromptMessage
def render_review(args: dict[str, str]) -> list[PromptMessage]:
return [
PromptMessage(role="user", text=f"Review this {args['language']} code:"),
PromptMessage(role="user", text=args.get("code", "")),
]
server = MCPServer(
agent,
prompts=[
Prompt(
name="code_review",
description="Generate a code-review prompt.",
render=render_review,
arguments=(
PromptArgument(name="language", description="Programming language", required=True),
PromptArgument(name="code", description="The code to review", required=False),
),
),
# A bare-string renderer becomes a single user message.
Prompt(name="greet", render=lambda args: f"Say hello to {args['who']}"),
],
)promptsresources/listresources/readresources/templates/listreadstrbytesfrom pathlib import Path
from ag2.mcp import MCPServer, Resource, ResourceTemplate
server = MCPServer(
agent,
resources=[
Resource(
uri="config://app",
name="app-config",
description="Static app config.",
mime_type="application/json",
read=lambda: '{"env": "prod"}',
),
],
resource_templates=[
# RFC 6570 templates: {var} matches one path segment, {+var} spans '/'.
ResourceTemplate(
uri_template="file:///{+path}",
name="file",
description="Read a file by path.",
read=lambda vars: Path(vars["path"]).read_text(),
),
],
)mime_typetext/plainstrapplication/octet-streambytesNoneresourcessessions=Truemcp-session-idtools/callSessionConfigsessions=Falsefrom ag2.mcp import MCPServer, SessionConfig
server = MCPServer(
agent,
sessions=SessionConfig(
max_sessions=1024, # LRU cap; least-recently-used session's history is dropped past the cap
ttl=3600, # optional idle-expiry in seconds (None = never expire)
storage=None, # pluggable history backend; defaults to in-memory MemoryStorage
),
)
# Or stateless — every call independent:
stateless = MCPServer(agent, sessions=False)storageag2.history.Storagestateless=Truesessions=structuredContentresponse_schemaMCPServeroutputSchemastructuredContentfrom pydantic import BaseModel
from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.mcp import MCPServer
class Weather(BaseModel):
city: str
temp_c: float
agent = Agent(name="weather", prompt="Report weather.", response_schema=Weather,
config=OpenAIConfig(model="gpt-4o-mini"))
server = MCPServer(agent)
# tool.outputSchema is set; call results carry result.structuredContent == {"city": ..., "temp_c": ...}AskContextContextProvidercontext_providermcp.server.auth.provider.AccessTokenNoneAskContextNoneAgent.ask()from typing import Any
from ag2.mcp import AskContext, ContextProvider, MCPServer
async def provide(token: Any) -> AskContext:
# Resolve the caller from `token`, then scope the turn to them.
tenant = "acme" # e.g. token.scopes / a claims lookup
return AskContext(
variables={"tenant": tenant}, # -> Agent.ask(variables=...)
tools=None, # -> Agent.ask(tools=...) (None = leave default)
prompt="Be concise.", # -> Agent.ask(prompt=...)
)
server = MCPServer(agent, context_provider=provide)AskContextvariables: dict | Nonetools: list | Noneprompt: list[str] | str | NoneNone/.well-known/oauth-protected-resourcefrom ag2.mcp import MCPServer
from ag2.mcp.security import oauth2_scheme, require
security = require(
oauth2_scheme(url="https://auth.example.com"), # absolute http(s) issuer URL
resource_url="https://api.example.com/mcp", # this server's public endpoint
verifier=my_token_verifier, # your mcp TokenVerifier implementation
required_scopes=["mcp.read"], # a token must carry every scope
)
app = MCPServer(agent, path="/mcp", security=security)security.resource_urlpath/mcpMCPServerValueError401WWW-Authenticate403verifiermcp.server.auth.provider.TokenVerifieroauth2_scheme(url=...)http(s)Requires external setup: a real authorization server to mint tokens and a concrete. Exercise the unauthenticated path in-process (see testing below); the token round-trip needs your OAuth provider.TokenVerifier
ag2.mcp.testingconnect()ClientSessionserve()httpx.AsyncClientTestConfigag2.testingimport asyncio
from ag2 import Agent
from ag2.testing import TestConfig
from ag2.mcp import MCPServer, Resource
from ag2.mcp import testing
async def main() -> None:
agent = Agent(name="assistant", prompt="p", config=TestConfig("Hello from the agent!"))
server = MCPServer(
agent,
resources=[Resource(uri="config://app", name="cfg", read=lambda: '{"env": "prod"}')],
)
# In-memory MCP client/server pair (the MCP analog of an ASGI test client).
async with testing.connect(server) as session:
await session.initialize()
tools = await session.list_tools()
assert [t.name for t in tools.tools] == ["ask"]
result = await session.call_tool("ask", {"message": "Hi"})
assert "Hello from the agent" in result.content[0].text
res = await session.read_resource("config://app")
assert res.contents[0].text == '{"env": "prod"}'
# Exercise the HTTP transport (initialize handshake, session id) in-memory:
async with testing.serve(server) as client:
resp = await client.post(
"/mcp",
headers={"Accept": "application/json, text/event-stream", "Content-Type": "application/json"},
json={
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}},
},
)
assert resp.status_code == 200
assert "mcp-session-id" in resp.headers
print("ok")
if __name__ == "__main__":
asyncio.run(main())testing.connect(server, raise_exceptions=..., **session_kwargs)logging_callbackmessage_handlercaveat for multi-turn:TestConfigbuilds a fresh response iterator per turn, so giving itTestConfig.create()will not showTestConfig("a", "b")then"a"across two separate MCP"b"s — each call replays from the first scripted response. That's a property of the mock, not the server: session history really does accumulate (verify it by inspecting the growing message list a custom test client receives, or use a real model).call_tool
ag2.mcp| Symbol | Kind | Purpose |
|---|---|---|
| class | Wrap an |
| dataclass | |
| dataclass | |
| dataclass | |
| dataclass | |
| dataclass | |
| dataclass | |
| dataclass | |
| type alias | `async (AccessToken |
| function | Build the single conversational |
ag2.mcp.securityoauth2_schemerequireSchemeRequirementag2.mcp.testingconnectservemcppip install "ag2[mcp]"MCPServerMCPAgentConfigErrorAgent(config=...)MCPServerMCPServerToolag2-use-builtin-toolsinstructions=instructionsstateless=Truemcp-session-idstateless=Falsesecurity.resource_urlpathMCPServer.__init__ValueErrorresponse_schemaoutputSchemastructuredContentag2/mcp/{server,sessions,prompts,resources,executor,info,security,testing}.pyreferences/test_server.pypython references/test_server.pyag2-use-builtin-tools