Skip to content

Repository files navigation

py2mcp

Quick MCP (Model Context Protocol) server creation from Python functions.

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).

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).

License

MIT

Releases

Packages

Contributors

Languages