Minimal, spec-faithful Python framework for building Model Context Protocol (MCP) clients and servers.
Dedalus MCP wraps the official MCP reference SDK with ergonomic decorators, automatic schema inference, and production-grade operational features. Full compliance with MCP.
Dedalus MCP is for teams that have their infrastructure figured out. You have an IdP. You have logging. You have deployment pipelines. You don't need another framework's opinions on these things—you need MCP done correctly.
We don't bundle auth providers, CLI scaffolding, or opinionated middleware. If you want turnkey everything, FastMCP is solid. If you have your own stack and want spec-faithful MCP that integrates cleanly, you're here.
137 KB. FastMCP is 8.2 MB. 60x smaller. We ship code.
Correct. We track every MCP spec change at field granularity with PR citations. Zero fallback policy: you get exactly what you asked for, or an error. When the spec says a field was added in 2025-03-26, we know. When it was removed in 2025-06-18, we know. No silent misbehavior.
Secure. Security is a first-class concern, not an afterthought. We give you a principled auth framework that works with your existing security posture. Built for production, built for high-stakes environments.
Write functions with @tool, then call server.collect(fn). Script-style, no context managers, no nesting. Same function, multiple servers: server_a.collect(add) and server_b.collect(add) work. No hidden state.
Runtime changes work: call allow_tools, collect() new tools, emit notify_tools_list_changed(), clients refresh.
Every control surface points back to a spec citation (docs/mcp/...) so you can check what behavior we're matching before you ship it.
Transports and services are just factories. If you don't like ours, register your own without forking the server.
Context objects are plain async helpers (get_context().progress(), get_context().info()), not opaque singletons. You can stub them in tests.
Registration model. FastMCP uses @mcp.tool where the function binds to that server at decoration time. This couples your code to a single server instance at import. Testing requires teardown. Multi-server scenarios require workarounds. Dedalus MCP's @tool decorator only attaches metadata. Registration happens when you call server.collect(fn). Same function, multiple servers. No global state. Tests stay isolated. Design rationale.
Protocol versioning. MCP has multiple spec versions with real behavioral differences. Dedalus MCP implements the Version Profile pattern: typed ProtocolVersion objects, capability dataclasses per version, current_profile() that tells you what the client actually negotiated. FastMCP inherits from the SDK and exposes none of this. You cannot determine which protocol version your handler is serving. Version architecture.
Schema compliance. Dedalus MCP validates responses against JSON schemas for each protocol version. When MCP ships breaking changes, our tests catch structural drift. FastMCP has no version-specific test infrastructure.
Spec traceability. Every Dedalus MCP feature cites its MCP spec clause in docs/mcp/spec/. Debugging why a client rejects your response? Trace back to the exact protocol requirement. FastMCP docs cover usage. Ours cover correctness.
Size. 122 KB vs 8.2 MB. We're 70x smaller. They ship docs, tests, and PNG screenshots. We ship code.
Client ergonomics. client = await MCPClient.connect(url) returns a ready-to-use client. No nested context managers required. Explicit close() or optional async with for cleanup. weakref.finalize() safety net warns if you forget. FastMCP requires async with mcp.run_client(): context manager nesting.
Where FastMCP wins. More batteries: OpenAPI integration, auth provider marketplace, CLI tooling. If you want turnkey auth with Supabase and don't want to think about it, FastMCP is probably easier to start with.
from dedalus_mcp import MCPServer, tool
@tool(description="Add two numbers")
def add(a: int, b: int) -> int:
return a + b
server = MCPServer("my-server")
server.collect(add)
if __name__ == "__main__":
import asyncio
asyncio.run(server.serve()) # Streamable HTTP on :8000from dedalus_mcp.client import MCPClient
async def main():
client = await MCPClient.connect("http://127.0.0.1:8000/mcp")
tools = await client.list_tools()
result = await client.call_tool("add", {"a": 5, "b": 3})
print(result)
await client.close()
import asyncio
asyncio.run(main())For protected servers using DPoP (RFC 9449):
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
from dedalus_mcp.client import MCPClient, DPoPAuth
# Your DPoP key (same key used when obtaining the token)
dpop_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
auth = DPoPAuth(access_token="eyJ...", dpop_key=dpop_key)
client = await MCPClient.connect("https://mcp.example.com/mcp", auth=auth)MIT