m3d-core / m3d /module.py
jkorstad's picture
module SDK (MCP v2)
1fde9f6 verified
Raw History Blame Contribute Delete
1.84 kB
"""M3D module SDK.
A module = a module.json manifest + an MCP server exposing the same tools
in-process (used by the DAG runner) and over stdio/HTTP (used by external
agents and agentic harnesses). One class, one interface, every runtime.
Built on MCP Python SDK v2 (pip install mcp==2.2.0):
from mcp.server import MCPServer; @mcp.tool() registers a typed tool.
"""
from __future__ import annotations
import json
from typing import Any
from mcp.server import MCPServer
class M3DModule:
def __init__(self, manifest: dict):
assert "id" in manifest and "version" in manifest, "manifest needs id + version"
assert "license" in manifest, "every M3D module declares its license graph"
self.manifest = manifest
self.server = MCPServer(manifest["id"])
self._tools: dict[str, Any] = {}
def tool(self, name: str | None = None):
"""Register a typed function as both an M3D tool and an MCP tool."""
def deco(fn):
tool_name = name or fn.__name__
self._tools[tool_name] = fn
self.server.add_tool(fn, name=tool_name) # MCP v2 registration
return fn
return deco
def run_tool(self, name: str, args: dict) -> Any:
"""Direct in-process call used by the DAG runner (isolation principle)."""
if name not in self._tools:
raise KeyError(
f"module {self.manifest['id']} has no tool {name!r}; "
f"available: {sorted(self._tools)}"
)
return self._tools[name](**args)
def serve(self, transport: str = "stdio") -> None:
"""Serve over MCP — stdio/sse/streamable-http, any agentic harness can attach."""
self.server.run(transport=transport)
def manifest_json(self) -> str:
return json.dumps(self.manifest, indent=2)