agent-mcp-py
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@agent-mcp-pyShow me how to add a tool that requires confirmation"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
agent-mcp-py
The convention layer every app in the ecosystem uses to expose itself as an MCP
server. Wraps the official mcp Python SDK with bearer auth, a conversation-depth
guard, usage logging, and sync-store registration already wired in.
See CLAUDE.md for the design spec and the ecosystem context.
This repo implements v0.1.0 — all seven modules, 175 tests, verified end to end against a live server over streamable HTTP.
app author writes agent_mcp adds the SDK does
───────────────── ────────────── ────────────
@mcp.tool(read_only=True) → bearer auth (401 at the edge) → tools/list
def get_balance(...) depth guard (X-Agent-Depth) tools/call
confirmation gate (X-Confirmed) resources/read
usage row per call
$ref-free JSON schemaInstall
Consumed as a git dependency — never from PyPI — pinned to the default branch:
# pyproject.toml
agent-mcp-py = { git = "https://github.com/Johonnyy/agent-mcp-py", branch = "main" }# pip equivalent
pip install "agent-mcp-py @ git+https://github.com/Johonnyy/agent-mcp-py@main"The version is the commit. There are no release tags and no version bumps: a
commit on main that passes CI is a release, and consumers pick it up on their next
build. That is why ci.yml here is load-bearing rather than a formality — a red
commit on main is a red build in every app downstream.
One consequence worth knowing: pip will not notice that a branch moved. An already-satisfied requirement is left alone, so an update needs to be asked for:
pip install --force-reinstall --no-deps \
"agent-mcp-py @ git+https://github.com/Johonnyy/agent-mcp-py@main"Docker builds have the same problem one layer up, and solve it with a cache-busting
build arg carrying the resolved commit — see amber-infra/ci-templates/release.yml.
The distribution is agent-mcp-py; the import is agent_mcp. Mismatched on
purpose so the package name matches the repo.
Related MCP server: Golf
Usage
from fastapi import FastAPI
from contextlib import asynccontextmanager
from agent_mcp import AgentMCPServer
mcp = AgentMCPServer(
app_name="finance",
version="0.1.0",
sync_store_url=settings.sync_store_url,
auth_keys_env="FINANCE_MCP_KEYS", # comma-separated "name:token" pairs
)
@mcp.resource("finance://transactions/recent{?limit}")
def recent_transactions(limit: int = 20) -> list[dict]:
"""Every resource URI mirrors a real dashboard view — this is what the
transactions table on the frontend renders from too."""
return db.get_recent_transactions(limit)
@mcp.tool(read_only=True)
def get_balance(account: str) -> float:
return db.get_balance(account)
@mcp.tool(read_only=False, requires_confirmation=True)
def create_invoice(customer_id: str, amount: float) -> dict:
return db.create_invoice(customer_id, amount)
@asynccontextmanager
async def lifespan(app: FastAPI):
async with mcp.lifespan(): # required — see "Mounting" below
yield
app = FastAPI(lifespan=lifespan)
app.router.routes.extend(mcp.routes()) # MCP at /mcp
app.router.routes.extend(mcp.usage_routes()) # summary at /agent/usageBoth decorators return the function unwrapped, so the app's own HTTP handler calls the very same object the model calls — a tool mirrors a real user action rather than a parallel code path that drifts from it.
Mounting
Use mcp.routes(). app.mount("/mcp", mcp.asgi_app()) also works but has a sharp
edge: Mount("/mcp") only matches /mcp/..., so a bare POST /mcp falls through
to the host router's redirect_slashes and gets a 307 — and the real MCP client
does not follow that redirect. routes() claims the exact path too, so both forms
work. If you mount by hand, make sure every caller uses the trailing slash;
agent_mcp.mcp_url() always produces it.
The lifespan is not optional. A mounted sub-app's own lifespan never runs, so the
host must enter session_manager.run() or the very first request fails.
Layout
Path | Role |
stdlib-only leaf. Header constants, | |
stdlib-only leaf. Bearer verification as pure functions | |
Tool-name rules, JSON-Schema | |
Peer discovery — | |
| |
| |
The error taxonomy: what becomes a protocol error vs. | |
The signature surgery and the guard path | |
Pure-ASGI auth gate, mount-path normalisation | |
| |
|
Conventions this enforces
Query tools are
read_only=True, risky onesrequires_confirmation=True. Both are visible on the wire:readOnlyHintin standard annotations, and both flags in_metaunderdev.johnny.agent-mcp/(the SDK'sToolAnnotationssilently drops unknown fields, so a custom one there would vanish). Enforcement always reads the in-process policy, never the wire.Depth is capped at 5 hops via
X-Conversation-Id/X-Agent-Depth.mcp.call_peer()threads them automatically and refuses an over-cap hop before it leaves the process.Tool names must be snake_case, ≤40 chars, no
__—agent_runtimenamespaces them<server>__<tool>, and the double underscore is the separator. Violations fail at import, not at call time.Input schemas ship
$ref-free. Pydantic emits$defs/$reffor nested models and several model providers reject them, so they are inlined at registration. A recursive model is refused with a message saying what to do instead.Usage stays local. Rows go to this app's own database, never a shared one. The spawner aggregates by querying each app's
/agent/usage.No model names anywhere, and no LLM SDK dependency. If a resource needs a model, the host app passes in a callable. A test enforces this.
Error taxonomy
Failure | What the caller sees |
Missing/bad bearer token | HTTP 401 at the ASGI edge |
Malformed |
|
Inbound call over the depth cap |
|
Outbound | ordinary exception → |
Missing | ordinary exception → |
Any exception in a tool body |
|
Sync store unreachable | a logged warning, nothing else |
The inbound/outbound asymmetry is deliberate: an outbound refusal happens inside a tool body, where a bad peer call must not take down an otherwise fine turn.
Interop with agent-runtime
agent_runtime imports agent_mcp.depth for the header constants and the cap, so
it can pre-check client-side. That module is stdlib-only, and agent_mcp's
__init__ is lazy (PEP 562) so importing it does not drag in mcp, pydantic, httpx2,
starlette and uvicorn. There is a test that fails if this regresses.
Both libraries can write one SQLite file (agent_mcp_usage / agent_runtime_usage),
sharing the join columns conversation_id, app_name, depth, and created_at
(ISO-8601 UTC seconds, never epoch floats). WAL plus a busy timeout makes the
co-tenancy safe.
Pin mcp>=2.0,<3. v2 renamed FastMCP to MCPServer and this package targets that
API exclusively — it will not import under 1.x.
Local development
git clone https://github.com/Johonnyy/agent-mcp-py
cd agent-mcp-py
python -m venv .venv
.venv/Scripts/activate # Windows; .venv/bin/activate on Linux/macOS
pip install -e ".[dev]"
cp .env.example .env
pytest # whole suite, no network required
pytest tests/test_decorators.py # the signature surgery
pytest tests/test_end_to_end.py # a real uvicorn server over HTTPThe suite needs no API key and makes no outbound calls: the sync store is a fake, auth uses known-good/known-bad tokens, and the end-to-end file binds an ephemeral local port.
This server cannot be deployed
Maintenance
Related MCP Connectors
Model Context Protocol server for the Apideck Unified API. Connect any MCP-compatible agent framework to 100+ accounting systems, HRIS platforms, file storage providers, and more through one integration. More information https://www.apideck.com/mcp-server
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
MCP server for Sendbird — chat users, channels, members, and messages from your AI client.
MCP server for Argo RPG Platform — connects AI assistants to campaign data via OAuth2
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA starter template for building MCP servers in Python using the streamable HTTP transport protocol. Provides a foundation with the MCP Python SDK and example configuration to quickly develop custom MCP servers.2-
- AlicenseNot gradedqualityCmaintenanceEasiest framework for building MCP servers with automatic discovery of tools, prompts, and resources, plus enterprise-grade authentication and telemetry.840Apache 2.0
- AlicenseAqualityCmaintenanceA production-ready foundation for building secure, observable MCP servers with built-in authentication, rate limiting, and reference tools like database-query and semantic-search.1511 npmMIT
- AlicenseNot gradedqualityBmaintenanceA Python framework for building MCP servers that makes it simple to expose application data and actions to AI clients like Claude Desktop, Cursor, and VS Code through the Model Context Protocol. It wraps protocol handling behind a clean, decorator-based API allowing focus on business logic.1MIT