This document shows how py2mcp follows the same design patterns as qh (the HTTP service builder), making it familiar and intuitive.
from qh import mk_http_service_app
def add(a: int, b: int) -> int:
return a + b
app = mk_http_service_app([add])
app.run()from py2mcp import mk_mcp_server
def add(a: int, b: int) -> int:
return a + b
mcp = mk_mcp_server([add])
mcp.run()Identical pattern! Pass functions, get a server.
from qh import mk_http_service_app
from qh.trans import mk_json_handler_from_name_mapping
import numpy as np
def add_arrays(a, b):
return (a + b).tolist()
input_trans = mk_json_handler_from_name_mapping({
'a': np.array,
'b': np.array
})
app = mk_http_service_app([add_arrays], input_trans=input_trans)from py2mcp import mk_mcp_server, mk_input_trans
import numpy as np
def add_arrays(a, b):
return (a + b).tolist()
input_trans = mk_input_trans({
'a': np.array,
'b': np.array
})
mcp = mk_mcp_server([add_arrays], input_trans=input_trans)Same transformation pattern! Map parameter names to conversion functions.
# qh has store dispatch patterns (see scrap/store_dispatch_*.py)
# where a MutableMapping is exposed via HTTP endpoints
from qh.scrap.store_dispatch_1 import StoreAccess
store = StoreAccess.from_uri('test_uri')
# Exposes: list(), read(), write(), delete()from py2mcp import mk_mcp_from_store
projects = {'proj1': {...}, 'proj2': {...}}
mcp = mk_mcp_from_store(projects, name='project')
# Automatically creates:
# - list_projects()
# - get_project(key)
# - set_project(key, value)
# - delete_project(key)Same CRUD pattern! Automatically generate operations from stores.
| Aspect | qh (py2http) | py2mcp |
|---|---|---|
| Foundation | py2http (bottle/FastAPI) | FastMCP |
| Philosophy | Don't reinvent HTTP | Don't reinvent MCP |
| Main Function | mk_http_service_app() |
mk_mcp_server() |
| Transformation | mk_json_handler_from_name_mapping() |
mk_input_trans() |
| Store Support | Store dispatch examples | mk_mcp_from_store() |
| Return Type | HTTP app object | FastMCP object |
| Run Method | app.run() |
mcp.run() |
qh/
├── __init__.py # Exports: mk_http_service_app
├── main.py # Core app creation
├── trans.py # Transformations
├── util.py # Helpers (flat_callable_for, etc.)
└── scrap/ # WIP patterns
py2mcp/
├── __init__.py # Exports: mk_mcp_server, mk_input_trans, mk_mcp_from_store
├── main.py # Core server creation
├── trans.py # Transformations
├── util.py # Helpers (store_to_funcs, etc.)
└── tests/ # Test suite
Nearly identical organization!
Both packages handle methods and functions uniformly:
# from qh.util import flat_callable_for
def flat_callable_for(func, func_name=None, cls=None):
"""Flatten cls->instance->method call pipeline"""
containing_cls = get_class_that_defined_method(func)
if not containing_cls:
return func # Already flat
# ... flatten the method# from py2mcp.base import _normalize_to_iterable
def _normalize_to_iterable(funcs):
"""Normalize input to an iterable of callables"""
if callable(funcs):
return [funcs]
elif isinstance(funcs, Iterable):
return list(funcs)
# ... validateBoth use the same pattern: name → function mapping
def transform_mapping_vals_with_name_func_map(mapping, name_func_map):
for name, val in mapping.items():
if name in name_func_map:
yield name, name_func_map[name](val)
else:
yield name, valdef _apply_transformations(kwargs, name_func_map):
for name, value in kwargs.items():
if name in name_func_map:
yield name, name_func_map[name](value)
else:
yield name, valueIdentical logic!
While the patterns are the same, there are protocol differences:
| Feature | HTTP (qh) | MCP (py2mcp) |
|---|---|---|
| Transport | HTTP endpoints | Stdio/HTTP/SSE |
| Client | curl, browsers | Claude, Cursor, MCP clients |
| Method Types | GET/POST/PUT/DELETE | Tools/Resources/Prompts |
| Request Format | JSON body, URL params | JSON-RPC |
| Response Format | JSON response | Structured tool results |
# HTTP server on port 8080
if __name__ == '__main__':
app.run(port=8080)Test with:
curl -X POST http://localhost:8080/add \
-H "Content-Type: application/json" \
-d '{"a": 3, "b": 5}'# Stdio server (default) or HTTP
if __name__ == '__main__':
mcp.run() # stdio
# mcp.run(transport='http', port=8000) # httpTest with:
fastmcp dev server.py # Opens web inspectorBoth require:
- Type hints (for schema generation)
- Docstrings (for documentation)
- JSON-serializable returns
# Works in both qh and py2mcp
def process_data(
text: str,
count: int = 10,
uppercase: bool = False
) -> dict:
"""Process text data.
Args:
text: Input text
count: Max length
uppercase: Convert to uppercase
Returns:
Processed result
"""
result = text[:count]
if uppercase:
result = result.upper()
return {"result": result, "length": len(result)}If you have a qh HTTP service and want to make it an MCP server:
-
Change import:
# from qh import mk_http_service_app from py2mcp import mk_mcp_server
-
Change function name:
# app = mk_http_service_app([...]) mcp = mk_mcp_server([...])
-
Update transformations (if used):
# from qh.trans import mk_json_handler_from_name_mapping from py2mcp import mk_input_trans # input_trans = mk_json_handler_from_name_mapping({...}) input_trans = mk_input_trans({...})
-
Change run call:
# app.run(port=8080) mcp.run() # or mcp.run(transport='http', port=8000)
That's it! Your functions stay exactly the same.
- You need HTTP/REST endpoints
- Building a web API
- Want browser/curl access
- Integrating with HTTP clients
- Building AI agent tools
- Integrating with Claude/Cursor
- Want LLM-friendly interfaces
- Following MCP standard
- You want both HTTP and MCP access
- Maximum flexibility
- Different client types
from qh import mk_http_service_app
from py2mcp import mk_mcp_server
# Same functions, two servers!
funcs = [add, multiply, greet]
http_app = mk_http_service_app(funcs)
mcp_server = mk_mcp_server(funcs)Both packages follow the same principles:
- Simplicity: Minimal API surface
- Convention over configuration: Sensible defaults
- Don't reinvent: Build on proven frameworks
- Pythonic: Feels natural to Python developers
- Functional: Prefer functions over classes
- SSOT: Single source of truth (the functions)
- Open/closed: Easy to extend, works out of the box
py2mcp is essentially "qh for MCP" - the same clean, simple pattern applied to a different protocol. If you liked how qh worked, you'll feel right at home with py2mcp.
The key insight: Functions are the universal interface. Whether exposing them via HTTP or MCP, the pattern is the same:
functions → wrapper → server → run
Simple, clean, Pythonic. ✨