A Simple Python MCP Solution
Project description
aiomcp
A Simple Python MCP Solution
Mission of aiomcp
Start with a smooth experience, end with a compliant solution.
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 invokesMcpServerwith minimal MCP protocol overhead.McpMemoryTransport: Single event loop, in-memory transport.McpHttpTransportMcpStdioClientTransport/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: returnstructuredContentonly, ignoringcontent.convert_mcp_tool_result_content_format: return MCP content blocks, parsing bothcontentandstructuredContentas 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 thetoolscapability before the client sendstools/list.enforce_mcp_tool_result_content: reject tool results that omitcontent.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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiomcp-0.0.12.tar.gz.
File metadata
- Download URL: aiomcp-0.0.12.tar.gz
- Upload date:
- Size: 55.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1949823958ccfa8e040616dee293e7ba125110363121fbc6b3e46ee29009584f
|
|
| MD5 |
165d848b36c7bd0d36ba6a5292e49daf
|
|
| BLAKE2b-256 |
e005a1ac839fce7065cb1ab608136de8914ae2f407d8b22fb90c1b914a1296c2
|
Provenance
The following attestation bundles were made for aiomcp-0.0.12.tar.gz:
Publisher:
publish.yml on yangyuan/aiomcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiomcp-0.0.12.tar.gz -
Subject digest:
1949823958ccfa8e040616dee293e7ba125110363121fbc6b3e46ee29009584f - Sigstore transparency entry: 1525628310
- Sigstore integration time:
-
Permalink:
yangyuan/aiomcp@c61c9be6e5fbd14019b2453bf5ebd47050b4a2c2 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/yangyuan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c61c9be6e5fbd14019b2453bf5ebd47050b4a2c2 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file aiomcp-0.0.12-py3-none-any.whl.
File metadata
- Download URL: aiomcp-0.0.12-py3-none-any.whl
- Upload date:
- Size: 41.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d682b9b859ce1046b510e198b2fbf6a5b5ea46771193637f8a760a47a0994767
|
|
| MD5 |
3d6f50d1afaacc6a7ebb091433875753
|
|
| BLAKE2b-256 |
7cd0dfea75730592ebb37f56309c5b5c689646893f81a50f4a9df76d892bf99a
|
Provenance
The following attestation bundles were made for aiomcp-0.0.12-py3-none-any.whl:
Publisher:
publish.yml on yangyuan/aiomcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiomcp-0.0.12-py3-none-any.whl -
Subject digest:
d682b9b859ce1046b510e198b2fbf6a5b5ea46771193637f8a760a47a0994767 - Sigstore transparency entry: 1525628370
- Sigstore integration time:
-
Permalink:
yangyuan/aiomcp@c61c9be6e5fbd14019b2453bf5ebd47050b4a2c2 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/yangyuan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c61c9be6e5fbd14019b2453bf5ebd47050b4a2c2 -
Trigger Event:
workflow_dispatch
-
Statement type: