Skip to main content

A Simple, High-Performance Python MCP Solution

Project description

aiomcp

A Simple, High-Performance Python MCP Solution — Simple to use, built for performance

Quick Start

A Simple STDIO Example

MCP server server.py

import asyncio

from aiomcp import McpServer, McpStdioServerTransport


async def add(a: int, b: int) -> int:
    return a + b


async def main() -> None:
    mcp_server = McpServer("mcp-server-name")
    await mcp_server.register_tool(add)
    await mcp_server.host(McpStdioServerTransport())


if __name__ == "__main__":
    asyncio.run(main())

MCP client client.py

import asyncio
import sys

from aiomcp import McpClient, McpStdioClientTransport


async def main() -> None:
    mcp_client = McpClient("mcp-client-name")
    await mcp_client.initialize(
        McpStdioClientTransport([sys.executable, "server.py"])
    )

    try:
        sum_value = await mcp_client.invoke("add", {"a": 1, "b": 2})
        print(sum_value)
        assert sum_value == 3
    finally:
        await mcp_client.close()


if __name__ == "__main__":
    asyncio.run(main())

Run the client from the same directory as server.py:

python client.py

A Simple HTTP Example

MCP server server.py

import asyncio

from aiomcp import McpServer


async def add(a: int, b: int) -> int:
    return a + b


async def main() -> None:
    mcp_server = McpServer("mcp-server-name")
    await mcp_server.register_tool(add)
    await mcp_server.host("http://127.0.0.1:8000/mcp")


if __name__ == "__main__":
    asyncio.run(main())

Run the server, it will host on http://127.0.0.1:8000/mcp

python server.py

MCP client client.py

import asyncio

from aiomcp import McpClient


async def main() -> None:
    mcp_client = McpClient("mcp-client-name")
    await mcp_client.initialize("http://127.0.0.1:8000/mcp")

    try:
        sum_value = await mcp_client.invoke("add", {"a": 1, "b": 2})
        assert sum_value == 3
    finally:
        await mcp_client.close()


if __name__ == "__main__":
    asyncio.run(main())

Run the client from anywhere in the same machine:

python client.py

A Complete LLM-Friendly HTTP MCP Server

For best compatibility with LLM clients, tools can return an array of MCP content blocks. Each item in the array is one content block, such as text, image, audio, resource link, or embedded resource.

import asyncio

from aiomcp import McpServer


async def screenshot():
    image_data = get_screenshot_image_data()

    return [
        {"type": "text", "text": "Here is the captured screenshot."},
        {"type": "image", "data": image_data, "mimeType": "image/png"},
    ]


async def main() -> None:
    mcp_server = McpServer("computer-use-server")
    await mcp_server.register_tool(screenshot)
    await mcp_server.host("http://127.0.0.1:8000/mcp")


if __name__ == "__main__":
    asyncio.run(main())

Server Tool Registration

Register Python Callables as Tools

register_tool accepts ordinary Python callables. Use a function, async function, bound method, class method, or static method; aiomcp reads the type hints and exposes the callable as an MCP tool.

def add(a: int, b: int) -> int:
    return a + b


async def fetch_status(service: str) -> str:
    return f"{service}: ok"


class Calculator:
    def scale(self, value: float, factor: float = 1.0) -> float:
        return value * factor

    @classmethod
    async def add_offset(cls, value: int, offset: int) -> int:
        return value + offset

    @staticmethod
    async def total(items: list[int]) -> int:
        return sum(items)


calculator = Calculator()

await mcp_server.register_tool(add)
await mcp_server.register_tool(fetch_status)
await mcp_server.register_tool(calculator.scale, alias="calculator_scale")
await mcp_server.register_tool(Calculator.add_offset)
await mcp_server.register_tool(Calculator.total)

Clients invoke registered tools by name with a dict of arguments, similar to Python keyword arguments.

assert await mcp_client.invoke("add", {"a": 1, "b": 2}) == 3
assert await mcp_client.invoke("calculator_scale", {"value": 3, "factor": 2}) == 6.0
assert await mcp_client.invoke("total", [1, 2, 3]) == 6

Customize Parameters and Schemas

Use Annotated with Pydantic Field for parameter descriptions. Use Pydantic models when a tool takes a structured object.

from enum import StrEnum
from typing import Annotated

from pydantic import BaseModel, Field


class SearchRequest(BaseModel):
    query: str
    limit: int = 5


class Tone(StrEnum):
    WARM = "warm"
    PRECISE = "precise"


async def search(
    request: SearchRequest,
    tone: Annotated[Tone, Field(description="Response tone")] = Tone.PRECISE,
) -> list[str]:
    return [f"{tone.value}: {request.query}"][: request.limit]


await mcp_server.register_tool(
    search,
    description="Search documents for the current workspace.",
    annotations={"readOnlyHint": True, "openWorldHint": False},
)

Advanced Tool Registration

Use mcp_tools_register when you already have JSON schemas or want exact control over the tool contract.

def add(a: int, b: int) -> int:
    return a + b


await mcp_server.mcp_tools_register(
    "add",
    add,
    input_schema={
        "type": "object",
        "properties": {
            "a": {"type": "integer", "description": "The first number"},
            "b": {"type": "integer", "description": "The second number"},
        },
        "required": ["a", "b"],
    },
    output_schema={"type": "integer"},
    title="Add Numbers",
    icons=[{"src": "https://example.com/add.png", "mimeType": "image/png"}],
)

MCP Transports

Using McpTransports

While McpClient and McpServer handle client/server behavior, they are implemented in a platform-agnostic way by using McpClientTransport/McpServerTransport to handle message streams. Client and server transports usually come in pairs as McpTransport implementations.

mcp_server = McpServer()
await mcp_server.host(McpHttpTransport("127.0.0.1", 8000, "/mcp"))
# await mcp_server.host("http://127.0.0.1:8000/mcp")
mcp_client = McpClient()
await mcp_client.initialize(McpHttpTransport("127.0.0.1", 8000, "/mcp"))
# await mcp_client.initialize("http://127.0.0.1:8000/mcp")

Supported McpTransport implementations:

  • McpDirectTransport: Simple client driven transport that invokes McpServer with minimal MCP protocol overhead.
  • McpMemoryTransport: Single event loop, in-memory transport.
  • McpHttpTransport
  • McpStdioClientTransport / McpStdioServerTransport: stdio based transport

Sample: Using McpMemoryTransport for local debugging.

transport = McpMemoryTransport()

mcp_server = McpServer()
asyncio.create_task(mcp_server.host(transport))

mcp_client = McpClient()
await mcp_client.initialize(transport)

Tool Results and LLM Integration

MCP Tool Result and Invoke Options

The MCP specification defines tool results with both content and structuredContent. The content field should be a list of text, image, audio, resource link, or embedded resource blocks. The optional structuredContent field is a structured object that follows the tool's output schema when one exists.

McpClient.invoke(...) accepts per-call result shaping options.

  • use_structured_content: return structuredContent only, ignoring content.
  • convert_mcp_tool_result_content_format: return MCP content blocks, parsing both content and structuredContent as MCP content or lists of MCP content when possible.

LLM Integration Client Best Practice

As an MCP client, aiomcp follows the MCP specification and uses content and structuredContent as is by default.

If an MCP server does not produce standard MCP content blocks, aiomcp can optionally convert and merge content and structuredContent into an LLM-friendly content list with convert_mcp_tool_result_content_format, making it optimal for LLM integration.

from aiomcp import McpClient, McpInvokeError


# This function returns a payload suitable for most LLM API tool results.
async def call_mcp_tool(
    client: McpClient,
    tool_call_id: str,
    tool_name: str,
    arguments: dict,
) -> dict:
    try:
        output = await client.invoke(
            tool_name,  # such as "screenshot"
            arguments,  # such as {"window": "active"}
            convert_mcp_tool_result_content_format=True,
        )
    except McpInvokeError as error:
        # The complete raw MCP result is available for logging or recovery.
        raw_result = error.result
        output = raw_result.model_dump_json(exclude_none=True, by_alias=True)

    # Use the format that fits best to your LLM API.
    return {
        "role": "tool",
        "tool_call_id": tool_call_id,
        "content": output,
    }

For custom tool result handling, use McpClient.invoke_result(...) to get the full McpCallToolResult.

LLM Integration Server Best Practice

As an MCP server, aiomcp generates output schemas from return type hints by default. Some MCP clients work best with content-first results. For broad LLM compatibility, omit the return annotation or set skip_mcp_tool_output_schema, then return a list of MCP content blocks. A simple text block can be a dict like {"type": "text", "text": "..."} or a McpTextContent; richer responses can use McpImageContent, McpAudioContent, McpResourceLink, or McpEmbeddedResource.

from aiomcp import McpCallToolResult, McpImageContent, McpServer, McpTextContent


mcp_server = McpServer(flags={"skip_mcp_tool_output_schema": True})


async def use_dict_content():
    return [
        # This follows the MCP content contract, not any provider-specific LLM API shape.
        {"type": "text", "text": "Chart generated."},
        {"type": "image", "data": "base64-image-data", "mimeType": "image/png"},
    ]


async def use_mcp_contracts():
    return [
        McpTextContent(text="Chart generated."),
        McpImageContent(data="base64-image-data", mimeType="image/png"),
    ]


async def use_call_tool_result():
    return McpCallToolResult(
        content=[McpTextContent(text="Chart generated.")],
        structuredContent={"chartId": "revenue-q1"},
    )

Authorization

Connect to an OAuth 2.1 protected remote MCP server.

from aiomcp import McpClient, McpAuthorizationClient

authorization = await McpAuthorizationClient.discover("http://remote-server/mcp")

# Or use a bearer token
# authorization = McpAuthorizationClient("your-access-token")

client = McpClient("mcp-client-name")
await client.initialize("http://remote-server/mcp", authorization=authorization)

Use GitHub as the OAuth provider to connect to GitHub's MCP server.

from aiomcp import McpClient, McpAuthorizationClient

server_url = "https://api.githubcopilot.com/mcp/"

authorization = await McpAuthorizationClient.discover(
    server_url,
    client_id="{YOUR_CLIENT_ID}",
    client_secret="{YOUR_CLIENT_SECRET}",
)

client = McpClient("mcp-client-name")
await client.initialize(server_url, authorization=authorization)

Protect your MCP server with built-in OAuth 2.1 + Dynamic Client Registration.

from aiomcp import McpServer, McpAuthorizationServer

server = McpServer("mcp-server-name")
await server.register_tool(add, alias="add")

authorization = McpAuthorizationServer()
await server.host("http://127.0.0.1:8000/mcp", authorization=authorization)

Compatibility flags

aiomcp defaults to broad compatibility, so it can connect to a wider range of MCP clients and servers. Compatibility flags let you opt into stricter protocol checks when you want closer MCP enforcement.

from aiomcp import McpClient, McpServer

mcp_client = McpClient(
    flags={"enforce_mcp_tool_result_content": True}
)

mcp_server = McpServer(
    flags={"enforce_mcp_initialize_sequence": True}
)

Available client flags

  • throw_mcp_contract_errors: raise error when incoming MCP messages miss required contract fields.
  • throw_mcp_parse_errors: raise when a transport receives malformed MCP JSON-RPC.
  • enforce_mcp_tools_capability: require the server to advertise the tools capability before the client sends tools/list.
  • enforce_mcp_tool_result_content: reject tool results that omit content.
  • enforce_mcp_version_negotiation: reject unsupported negotiated protocol versions.
  • enforce_mcp_session_header: require HTTP session headers where applicable.
  • enforce_mcp_protocol_header: require HTTP protocol version headers where applicable.
  • enforce_mcp_transport_version_consistency: require HTTP header and initialize body protocol versions to match.

Available server flags

  • throw_mcp_parse_errors: raise when a transport receives malformed MCP JSON-RPC instead of ignoring it.
  • enforce_mcp_initialize_sequence: reject requests sent before MCP initialization completes.
  • enforce_mcp_version_negotiation: negotiate only supported protocol versions.
  • enforce_mcp_session_header: require HTTP session headers where applicable.
  • enforce_mcp_protocol_header: require HTTP protocol version headers where applicable.
  • skip_mcp_tool_output_schema: skip output schema generation for registered tools.
  • enforce_mcp_tool_result_content_format: validate tool result content against MCP content block.
  • allow_mcp_tool_result_empty_content: allow tool results to omit the content.

Other Customizations

McpServer and McpClient accept a few constructor parameters to tune runtime behavior.

from aiomcp import McpClient, McpServer

mcp_server = McpServer(
    "mcp-server-name",
    max_sessions=1000,
    max_expired_sessions=1000,
)

mcp_client = McpClient(
    "mcp-client-name",
    request_timeout=60.0,
)

McpServer parameters

  • name: server name reported during initialization. Defaults to "aiomcp-server".
  • max_sessions: maximum number of live sessions to retain. This counts all sessions kept alive, including idle ones, not just concurrently active requests, so a session can stay dormant (e.g. a client that sleeps and resumes later) and still occupy a slot. When the cap is reached, the least recently used session is evicted and its in-flight requests are cancelled. Sessionless (transient) requests are not counted toward this cap. Defaults to 1000.
  • max_expired_sessions: maximum number of terminated/evicted session ids to remember as tombstones (so their later requests get a clean 404 instead of being silently re-created). Set to 0 to keep no tombstones. Defaults to 1000.

The default of 1000 is tuned for personal and local use. For production or multi-user deployments, raise these limits to match your designed capacity, i.e. how many sessions you intend to retain, including long-lived idle ones that may resume after hours or days. Each retained session and each open connection costs aiomcp well under 1 KB of its own bookkeeping, and the core session lookup/eviction/delivery paths are O(1) — independent of how many sessions you hold.

McpClient parameters

  • name: client name reported during initialization. Defaults to "aiomcp-client".
  • request_timeout: per-request timeout in seconds for calls awaiting a server response. Set to None to wait indefinitely. Defaults to 60.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aiomcp-0.0.13.tar.gz (59.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aiomcp-0.0.13-py3-none-any.whl (44.7 kB view details)

Uploaded Python 3

File details

Details for the file aiomcp-0.0.13.tar.gz.

File metadata

  • Download URL: aiomcp-0.0.13.tar.gz
  • Upload date:
  • Size: 59.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aiomcp-0.0.13.tar.gz
Algorithm Hash digest
SHA256 29bca513ca150b3915555bd6faaee08c53b91a5e3549105a43957d2e24c6cd3c
MD5 71a247fd3758646168e58b8da5bd6b35
BLAKE2b-256 8b3d4c7659fc203aae5b54a70c5ba8e8c9e6b3b912f14a69a12a70c1df1c97e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for aiomcp-0.0.13.tar.gz:

Publisher: publish.yml on yangyuan/aiomcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aiomcp-0.0.13-py3-none-any.whl.

File metadata

  • Download URL: aiomcp-0.0.13-py3-none-any.whl
  • Upload date:
  • Size: 44.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aiomcp-0.0.13-py3-none-any.whl
Algorithm Hash digest
SHA256 9895c348f49677ff8deb82da59f11ff9c656ba1f927629350e159bb06262b269
MD5 3902dc15847239f4effb3ca9340b8841
BLAKE2b-256 ebd5e8dd0131392ab177b7be562feef66e5a97e81d1e1bff5c02a7fa1c37eb1f

See more details on using hashes here.

Provenance

The following attestation bundles were made for aiomcp-0.0.13-py3-none-any.whl:

Publisher: publish.yml on yangyuan/aiomcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page