py2mcp MCP server
Commit Oct 26, 2026Converts Python functions into Model Context Protocol servers with support for local stdio and remote HTTP with OAuth 2.1.
Also known as i2mint/py2mcp
- Stars
- 0
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 26, 2026
- License
- MIT
What is py2mcp MCP?
py2mcp converts Python functions into Model Context Protocol servers with support for local stdio and remote HTTP with OAuth 2.1.
Summary generated from the project's README
About py2mcp MCP server
Transforming Python functions into Model Context Protocol servers is what py2mcp enables for developers building tools for AI agents. By passing standard Python functions to the mk_mcp_server command, you can instantly expose them as tools. The library supports local execution via stdio for desktop applications or remote hosting using HTTP with OAuth 2.1 for cloud-based connectors. It is designed to be pythonic, avoiding unnecessary decorators while providing deep integration with FastMCP.
Developers can extend server functionality through input transformations, such as converting lists to numpy arrays using mk_input_trans. The library also includes mk_mcp_from_store to automatically generate CRUD operations from any mutable mapping. For production environments, you can implement middleware for usage metering, logging, or rate limiting. It also provides specialized tools like UsageLogger to track tool calls and instructions to guide model behavior. This makes it a versatile choice for creating both simple local tools and secure, managed remote resource servers.
What you can do with it
- 1Expose Python functions as toolsConvert a simple addition function into an MCP tool for an AI agent.
- 2Automate CRUD from data storesTurn a dictionary of projects into a server with list, get, set, and delete capabilities.
- 3Secure remote MCP hostingDeploy a hosted server that validates JWTs from an identity provider using OAuth 2.1.
- 4Monitor tool usage and costsAttach middleware to every tool call to track usage metering and audit trails.
Written from the project's README. Every use case is checked against a quote from it.
README
View on GitHubpy2mcp
Quick MCP (Model Context Protocol) server creation from Python functions.
For AI agents
py2mcp publishes its documentation in forms made for coding agents. If you are one, start here.
The documentation, machine-readable: llms.txt indexes every page; py2mcp.md is the whole documentation in one file; every page has a .md twin; objects.inv maps symbols to URLs.
If you identify as a dinosaur, the rest of this README is written for you, starting at Installation.
Installation
pip install py2mcp
Quick Start
from py2mcp import mk_mcp_server
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
def greet(name: str = "world") -> str:
"""Greet someone"""
return f"Hello, {name}!"
# Create and run MCP server
mcp = mk_mcp_server([add, greet])
if __name__ == "__main__":
mcp.run()
That's it! Your functions are now available as MCP tools.
Features
- Simple: Just pass functions to
mk_mcp_server() - Flexible: Supports input/output transformations
- Pythonic: Clean, decorator-free function definitions
- Powerful: Built on FastMCP for production-ready servers
Input Transformations
Transform inputs before they reach your functions:
from py2mcp import mk_mcp_server, mk_input_trans
import numpy as np
def add_arrays(a, b):
"""Add two numpy arrays"""
return (a + b).tolist()
# Convert list inputs to numpy arrays
input_trans = mk_input_trans({"a": np.array, "b": np.array})
mcp = mk_mcp_server([add_arrays], input_trans=input_trans)
From Stores (MutableMapping)
Automatically expose CRUD operations from any mapping:
from py2mcp import mk_mcp_from_store
projects = {"proj1": {"name": "Project 1"}, "proj2": {"name": "Project 2"}}
mcp = mk_mcp_from_store(projects, name="project")
# Automatically creates: list_projects, get_project, set_project, delete_project
Serving: local (stdio) and remote (HTTP + OAuth)
mk_mcp_* build a server object; py2mcp also gives you two ways to run one.
Local (stdio) — for a one-click bundle (e.g. a Claude Desktop .mcpb):
from py2mcp import serve_stdio
serve_stdio(["mypkg.tools:summarize", "mypkg.tools:translate"], name="My Tools")
# or: python -m py2mcp --config py2mcp_config.json
Remote (Streamable HTTP + OAuth 2.1) — for a hosted MCP server reached from a vendor's cloud (e.g. a claude.ai custom connector). The server is an OAuth 2.1 resource server: it validates a managed IdP's JWTs (audience-bound per RFC 8707) and never issues tokens itself.
from py2mcp.http import mk_http_app
AUTH = {
"type": "jwt", # resource-server: validate the IdP's JWTs
"jwks_uri": "https://idp.example.com/.well-known/jwks.json",
"issuer": "https://idp.example.com",
"audience": "https://my-connector.example.com/mcp", # THIS server (RFC 8707)
"authorization_servers": ["https://idp.example.com"],
"base_url": "https://my-connector.example.com",
"required_scopes": ["mcp:read"],
}
# An ASGI app you run under any ASGI server (uvicorn, gunicorn, serverless):
app = mk_http_app(["mypkg.tools:summarize"], name="My Connector", auth=AUTH)
# uvicorn server.app:app --host 0.0.0.0 --port 8000 (behind TLS)
serve_http(...) builds and runs it in-process (FastMCP/uvicorn). Both wrap
FastMCP's native transports/OAuth — py2mcp does not reinvent them.
Middleware (metering, logging, rate-limiting)
Every builder accepts middleware= — a single FastMCP middleware or an iterable of them — attached at construction, exactly as auth= is. It's the one clean seam for cross-cutting concerns that must wrap every tool call (usage metering, cost logging, audit trails, rate limiting), so you don't decorate each function individually — and can't forget one (a missed decorator on a paid tool means untracked cost):
from fastmcp.server.middleware import Middleware
class UsageMeter(Middleware):
async def on_call_tool(self, context, call_next):
result = await call_next(context) # the tool runs here
record(context.message.name) # ... then meter it
return result
mcp = mk_mcp_server([render, estimate], middleware=[UsageMeter()])
# same on mk_mcp_from_refs(...), mk_mcp_from_store(...), mk_http_app(...),
# serve_http(...), serve_stdio(...)
On the remote path auth= (transport-level) runs first, so a middleware can read
the authenticated caller via fastmcp.server.dependencies.get_access_token().
Middleware is a programmatic hook — it takes Python objects, so it isn't wired
through the python -m py2mcp CLI / JSON-config path (unlike refs/name/auth).
Usage logging (who called which tool, with what, and what came back)
A deployed connector's web-server log says only that something POSTed /mcp. UsageLogger is a middleware that writes one record per tool call — time, caller (from the OAuth token), tool, arguments, outcome (ok / empty / error), result size, latency, connector name and version — plus one flagged record per initialize handshake, so "a client has it enabled" and "a client actually used it" stop looking the same:
from py2mcp import mk_http_app, UsageLogger
app = mk_http_app(
refs,
name="snout",
auth=AUTH,
middleware=[UsageLogger(name="snout", version="1.4.0", redact=("api_key",))],
)
With no sink=, records go to one JSON-lines file per day under $XDG_DATA_HOME/py2mcp/usage/<name>/ (never the app directory), pruned to 90 days. Any callable is a sink — records.append in a test, mapping_sink(dol_store) to lay records out one-per-key in a dol store (and so S3) — and a failing sink never fails the tool call. The arguments are your users' own questions, so logging is off unless you attach the middleware, include_args=False drops them, redact= masks named fields, max_args_chars caps the rest, and error text (which echoes arguments) is kept only when every argument is. Read it back with py2mcp usage <dir> [--since YYYY-MM-DD] [--json | --records], or iter_records → summarize in Python. Design and prior art: misc/docs/decisions/0001-usage-logging-is-a-py2mcp-middleware.md.
Instructions (the server's model-facing description)
Every builder also accepts instructions= — a natural-language string surfaced to
the connecting client/model as the server's instructions,
attached at construction exactly like auth=/middleware=. It's the place to say
what the tools are for and the intended workflow, so a model can orient itself
without calling a tool:
mcp = mk_mcp_server(
[render, estimate],
instructions="Turn source docs into narrated audio. Always estimate_cost before a render.",
)
# same keyword on mk_mcp_from_refs(...), mk_mcp_from_store(...), mk_http_app(...),
# serve_http(...), serve_stdio(...)
Like middleware=, it's a programmatic argument (not yet wired through the
python -m py2mcp CLI / JSON-config path).
Prompts and resources
Every builder also accepts prompts= and resources=, so a server that ships MCP
prompts and
resources alongside its tools can be
built declaratively in one call, instead of reaching past the builder to register
them by hand on the returned FastMCP object:
def summarize_request(topic: str) -> str:
return f"Summarize the latest on {topic}."
def schema() -> dict:
return {"type": "object"}
mcp = mk_mcp_server(
[render, estimate],
prompts=summarize_request, # a callable, or an iterable of them
resources={"schema://analysis": schema}, # {uri: callable}
)
# same keywords on mk_mcp_from_refs(...), mk_mcp_from_store(...), mk_http_app(...),
# serve_http(...), serve_stdio(...)
prompts accepts a single callable or an iterable, normalized the same way funcs
is for tools. resources is a {uri: callable} mapping — each callable is invoked
to produce that resource's content when a client reads its URI.
"Add to Claude" install links
Once a server is hosted, the last mile is getting a human to add it. There's no true one-click install for an unlisted connector (listing requires Anthropic review), but a prefilled link opens the add-connector modal with the name and URL already filled in, so the user only has to confirm:
from py2mcp import claude_install_link, markdown_install_badge
claude_install_link("snout", "https://example.com/api/snout_mcp/mcp")
# 'https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=snout&...'
markdown_install_badge("snout", "https://example.com/api/snout_mcp/mcp")
# '[Add snout to Claude](https://claude.ai/customize/connectors?...)' <- paste into a README
claude_install_link("snout", "...", admin=True) # org-wide page, not per-user
Both are pure string functions (stdlib only, no server needed). Three caveats the link itself can't express:
- Custom connectors are a paid-plan feature, so the link goes nowhere for a Free-plan user.
admin=Truetargets the org-wide install page — the right one when an admin is rolling a connector out to a workspace, the wrong one for a personal install.- A link is not an access grant. If the server is an OAuth resource server with an allowlist (see above), someone not on it can follow the link, complete the flow, and still be refused. Hand out the link together with whatever adds them to the allowlist.
License
MIT
py2mcp MCP questions
How do I install py2mcp?
You can install it using pip.
Does py2mcp support remote hosting with authentication?
Yes, it supports remote serving via Streamable HTTP and OAuth 2.1, acting as a resource server that validates JWTs.