py2mcp

py2mcp: Quick MCP server creation from Python functions.

This package provides a simple, Pythonic way to create Model Context Protocol (MCP) servers from ordinary Python functions. Built on FastMCP, it handles all the protocol complexity while letting you focus on your business logic.

Basic usage:
>>> from py2mcp import mk_mcp_server
>>>
>>> def add(a: int, b: int) -> int:
...     '''Add two numbers'''
...     return a + b
>>>
>>> mcp = mk_mcp_server([add])
>>> # mcp.run()  # Start the server
py2mcp.import_object(ref: str) Any[source]

Resolve a 'module.path:attr' (preferred) or 'module.path.attr' reference.

Useful for building MCP servers from configuration strings (e.g. tool references declared in a file), so callers don’t reimplement the importlib dance.

>>> import_object('json:dumps')
<function dumps at ...>
>>> import_object('os.path.join')
<function join at ...>
py2mcp.load_server_config(path: str | Path) dict[source]

Load a server config JSON of the form {"name": str, "refs": [str, ...]}.

Raises ValueError if the file is not a JSON object carrying a refs list — an actionable error beats a server that starts with no tools.

py2mcp.mk_auth_provider(auth: dict | None) Any | None[source]

Build a FastMCP resource-server auth provider from an auth-config dict.

auth is None/falsy (no auth) or a dict with a type key:

type='jwt' (default) — validate JWTs issued by a managed IdP. Keys:

  • jwks_uri or public_key — where to get the IdP’s signing key(s).

  • issuer — the IdP issuer URL (the token’s iss).

  • audience (required) — this server’s resource id (the token’s aud). RFC 8707 audience binding is mandatory: it stops a token minted for another service being replayed here (the confused-deputy defense), so this helper refuses to build a verifier that would skip it.

  • authorization_servers (or a single issuer) — IdP issuer URL(s) advertised in the RFC 9728 protected-resource metadata.

  • base_url — this server’s public base URL.

  • required_scopes (optional) — scopes every request must carry.

Returns a RemoteAuthProvider (a resource server), or None when auth is falsy. Raises ValueError on an unknown type or a missing required key. Building the provider performs no network I/O (key fetching is lazy, on the first request), so this is safe to call at scaffold/import time.

py2mcp.mk_http_app(refs: Iterable[str], *, name: str = 'py2mcp Server', auth: dict | None = None, input_trans: Callable[[dict], dict] | None = None, transport: str = 'streamable-http', path: str | None = None, stateless_http: bool | None = None) Any[source]

Build a Streamable-HTTP ASGI app from refs (+ optional OAuth).

Returns the ASGI application (a Starlette app), so any ASGI server can run it:

# server/app.py
from py2mcp.http import mk_http_app
app = mk_http_app(['mypkg.tools:summarize'], name='My Connector', auth=AUTH)
# then:  uvicorn server.app:app --host 0.0.0.0 --port 8000

auth is resolved by mk_auth_provider() (None → no auth; a remote connector should always set it). stateless_http=True is recommended behind a load balancer (MCP sessions are stateful, so default in-memory sessions break across replicas — go stateless or externalize session state). Builds the app with no network I/O.

py2mcp.mk_input_trans(name_func_relationships: Mapping | None = None) Callable[[dict], dict][source]

Create an input transformation function from name->func mappings.

>>> def to_int(x): return int(x)
>>> trans = mk_input_trans({'x': to_int})
>>> trans({'x': '42', 'y': 'hello'})
{'x': 42, 'y': 'hello'}
py2mcp.mk_mcp_from_refs(refs: Iterable[str], *, name: str = 'py2mcp Server', input_trans: Callable[[dict], dict] | None = None, auth: Any | None = None) FastMCP[source]

Create an MCP server from 'module:function' reference strings.

Resolves each reference to a callable via py2mcp.util.import_object() and delegates to mk_mcp_server(). One call from config strings to a runnable server — what tools that read tool references from a file (e.g. coact’s mcp backend) need. auth is forwarded to mk_mcp_server() (the remote/HTTP path attaches an OAuth provider here).

Examples

>>> mcp = mk_mcp_from_refs(['os.path:basename', 'os.path:dirname'], name='Paths')
>>> mcp.name
'Paths'
py2mcp.mk_mcp_from_store(store: MutableMapping[Any, Any], *, name: str = 'item', plural: str = '', server_name: str | None = None) FastMCP[source]

Create an MCP server from a MutableMapping with CRUD operations.

Automatically generates list, get, set, and delete functions for the store.

Parameters:
  • store – A MutableMapping to expose via MCP

  • name – Singular name for items (e.g., ‘project’, ‘user’)

  • plural – Plural form (defaults to name + ‘s’)

  • server_name – Name of the MCP server (defaults to “{name} Store”)

Returns:

A FastMCP server with CRUD operations

Examples

>>> projects = {'p1': {'name': 'Project 1'}, 'p2': {'name': 'Project 2'}}
>>> mcp = mk_mcp_from_store(projects, name='project')
>>> mcp.name
'project Store'
py2mcp.mk_mcp_server(funcs: Callable | Iterable[Callable], *, name: str = 'py2mcp Server', input_trans: Callable[[dict], dict] | None = None, auth: Any | None = None) FastMCP[source]

Create an MCP server from Python functions.

This is the main entry point for py2mcp. Pass one or more functions, and get back a FastMCP server ready to run.

Parameters:
  • funcs – A function or iterable of functions to expose as MCP tools

  • name – Name of the MCP server

  • input_trans – Optional function to transform input kwargs before calling tools

  • auth – Optional fastmcp.server.auth provider attached at construction — used by the remote (HTTP) path for OAuth 2.1 (see py2mcp.http). None (the default) leaves the server unauthenticated, which is correct for the local stdio path.

Returns:

A FastMCP server instance ready to run

Examples

>>> def add(a: int, b: int) -> int:
...     '''Add two numbers'''
...     return a + b
>>> mcp = mk_mcp_server(add)
>>> mcp.name
'py2mcp Server'
>>> def greet(name: str) -> str:
...     return f"Hello, {name}!"
>>> mcp = mk_mcp_server([add, greet], name="Math & Greetings")
>>> mcp.name
'Math & Greetings'
py2mcp.resolve_server_config(*, config: str | Path | None = None, refs: Iterable[str] = (), name: str | None = None) tuple[list[str], str][source]

Merge a config file and explicit refs/name into (refs, name).

Refs from --ref are appended after any from the config file; an explicit name wins over the config’s. Pure (no I/O beyond reading config), so it is unit-testable without standing up a server.

>>> resolve_server_config(refs=['os.path:basename'], name='Paths')
(['os.path:basename'], 'Paths')
py2mcp.serve_http(refs: Iterable[str], *, name: str = 'py2mcp Server', host: str = '127.0.0.1', port: int = 8000, auth: dict | None = None, input_trans: Callable[[dict], dict] | None = None, transport: str = 'streamable-http', stateless_http: bool | None = None) None[source]

Build and run a Streamable-HTTP MCP server (blocking) via FastMCP/uvicorn.

For a self-hosted process. Binds 127.0.0.1 by default — expose a public interface only behind a TLS-terminating reverse proxy (a remote connector must be reachable over public HTTPS, and binding locally is the spec’s DNS-rebinding-safe default). auth is resolved by mk_auth_provider().

py2mcp.serve_stdio(refs: Iterable[str], *, name: str = 'py2mcp Server', input_trans: Callable[[dict], dict] | None = None) None[source]

Build an MCP server from 'module:function' refs and run it over stdio.

Blocks, serving the MCP protocol on stdin/stdout until the host disconnects. Thin wrapper over py2mcp.mk_mcp_from_refs() + FastMCP.run so that packaged integrations have one command to launch.