Skip to content
GitHub

Architecture

Internal design of the lexigram-ai-mcp package — the Model Context Protocol (MCP) server/client bridge for AI ↔ external tool integration.


flowchart BT
    subgraph AI[AI Subsystem]
        Agents[lexigram-ai-agents]
        Skills[lexigram-ai-skills]
        LLM[lexigram-ai-llm]
    end
    subgraph MCP[lexigram-ai-mcp]
        Server[MCP Server<br/>ToolHandler · ResourceHandler<br/>PromptHandler · SamplingHandler]
        Client[MCP Client<br/>Tool Discovery · Invocation]
        Transport[Transport Layer<br/>StdioTransport · SSETransport]
        Connectors[Connectors<br/>Filesystem · GitHub · WebFetch<br/>WebSearch · Slack · GDrive · SQL]
    end
    subgraph External[External World]
        LocalTools[Local MCP Servers · stdio]
        RemoteTools[Remote MCP Servers · HTTP+SSE]
        APIs[External APIs<br/>GitHub · Slack · Google · Web]
    end

    Agents -->|uses tools| Server
    Agents -->|calls external tools| Client
    Skills -->|exposed as| Server
    LLM -->|sampling/createMessage| Server
    Server --> Transport
    Client --> Transport
    Connectors --> Server
    Transport -->|stdio| LocalTools
    Transport -->|HTTP+SSE| RemoteTools
    Connectors --> APIs

Import rule: lexigram-ai-mcp depends on lexigram and lexigram-contracts only. Bridges to agents, skills, and LLM are resolved via DI container protocols — never direct imports.


The MCP is a JSON-RPC 2.0 protocol for AI clients to discover and invoke tools, read resources, and use prompt templates.

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_user","arguments":{"user_id":"abc"}}}

Notifications omit id: {"jsonrpc":"2.0","method":"notifications/initialized"}

MethodDirectionDescription
initializeClient → ServerCapability negotiation, handshake
notifications/initializedClient → ServerHandshake complete
pingEitherKeepalive
tools/listClient → ServerList available tools
tools/callClient → ServerInvoke a tool
resources/listClient → ServerList available resources
resources/readClient → ServerRead a resource by URI
resources/templates/listClient → ServerList resource URI templates
prompts/listClient → ServerList prompt templates
prompts/getClient → ServerGet a rendered prompt
sampling/createMessageServer → ClientRequest LLM sampling (reverse)
logging/setLevelClient → ServerSet server logging level
flowchart LR
    R[MCPServer<br/>handle_message] --> Stdio[StdioTransport<br/>stdin/stdout · newline JSON]
    R --> SSE[SSETransport<br/>POST /mcp/messages · SSE /mcp/sse]
    Stdio --> CLI[Local MCP Process]
    SSE --> HTTP[HTTP Server<br/>ASGI]

    StdioCli[StdioClientTransport<br/>subprocess] --> CLI
    SSECli[SSEClientTransport<br/>POST+SSE] --> HTTP

The server is transport-agnostic — MCPServer.handle_message() receives parsed dict and returns dict. AbstractTransport defines start(), stop(), send(), receive().

  • StdioTransport — reads newline-delimited JSON from stdin, writes to stdout
  • SSETransport — receives via HTTP POST, streams via SSE. MCPServerHost provides the ASGI callable (mountable in Starlette/FastAPI/Uvicorn)

MCPServer routes JSON-RPC messages to registered handlers via a method-name dispatch table built in _register_handlers():

sequenceDiagram
    actor Client as MCP Client
    participant S as MCPServer
    participant TH as ToolHandler
    participant RH as ResourceHandler

    Client->>S: {"method":"initialize","id":1}
    S->>S: _register_handlers()
    S-->>Client: capabilities + serverInfo

    Client->>S: {"method":"tools/list","id":2}
    S->>TH: list_tools()
    TH-->>S: [MCPToolDefinition]
    S-->>Client: {"tools":[...]}

    Client->>S: {"method":"tools/call","id":3,"params":{...}}
    S->>TH: call_tool(name, args)
    TH->>TH: provider chain
    TH-->>S: MCPToolResult
    S-->>Client: {"content":[...]}

    Client->>S: {"method":"resources/list","id":4}
    S->>RH: list_resources()
    RH-->>S: [MCPResource]
    S-->>Client: {"resources":[...]}
ToolHandler → _CombinedToolProvider
├── ControllerToolProvider ← @tool-decorated MCPController methods
├── ServiceToolProvider ← Auto-exposed service methods
├── _ConnectorToolBundle ← Built-in connectors
└── SkillToolAdapter ← Optional lexigram-ai-skills bridge
class DataToolsController(MCPController):
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
@tool("get_user", description="Fetch user by ID")
async def get_user(self, user_id: str) -> dict:
return (await self.repo.get(user_id)).unwrap_or({})

Decorators attach _tool_config / _resource_config / _prompt_config metadata. MCPController.collect_tools() traverses the MRO. ControllerToolProvider resolves controller instances from the container and dispatches.


MCPClient handles the MCP handshake (initializeinitialized) and exposes list_tools(), call_tool(), list_resources(), list_prompts().

connect() → initialize → initialized notification → ready
list_tools() · call_tool() · list_resources() · list_prompts()
disconnect()
transport = StdioClientTransport(["uvx", "mcp-server-git", "--repository", "/repo"])
async with MCPClient(transport) as client:
tools = await client.list_tools()
result = await client.call_tool("git_log", {"max_count": 5})

MCPClientModule registers named clients in MCPClientRegistry (and the first connection directly as MCPClient):

class ReportService:
def __init__(self, registry: MCPClientRegistry) -> None:
self._git = registry.get("git")
self._analytics = registry.get("analytics")

sequenceDiagram
    participant C as Container
    participant P as MCPProvider

    C->>P: register()
    P->>C: singleton(MCPConfig)

    C->>P: boot()
    P->>P: _boot_controller_providers() → ControllerTool/Resource/PromptProvider
    P->>P: _boot_service_providers() → ServiceToolProvider
    P->>P: _boot_skill_bridge() → SkillToolAdapter (if skills in container)
    P->>P: _boot_connectors() → _ConnectorToolBundle + _ConnectorResourceBundle
    P->>P: _boot_handlers() → ToolHandler + ResourceHandler + PromptHandler
    Note over P: Merges all providers into combined providers
    P->>P: _boot_server() → MCPServer
    P->>P: _boot_transports() → StdioTransport + SSETransport
    alt client_url or client_stdio_command
        P->>P: _boot_client() → MCPClient
    end

    rect rgb(240,240,240)
        Note over P: shutdown() — stop transports, disconnect client
    end
sequenceDiagram
    participant C as Container
    participant CP as MCPClientProvider

    C->>CP: register()
    CP->>CP: build_client() per MCPConnection
    CP->>C: singleton(MCPClientRegistry)
    CP->>C: singleton(MCPClient) (first connection)

ContractLocationImplemented By
MCPServerProtocolcontracts/mcp/protocols.pyMCPServer
MCPTransportProtocolcontracts/mcp/protocols.pyStdioTransport, SSETransport
MCPToolProviderProtocolcontracts/mcp/protocols.pyControllerToolProvider, ServiceToolProvider, SkillToolAdapter, connector bundles
MCPToolHandlerProtocolcontracts/mcp/protocols.pyToolHandler
MCPResourceProviderProtocolcontracts/mcp/protocols.pyControllerResourceProvider, connector bundles
MCPResourceHandlerProtocolcontracts/mcp/protocols.pyResourceHandler
MCPPromptProviderProtocolcontracts/mcp/protocols.pyControllerPromptProvider
MCPPromptHandlerProtocolcontracts/mcp/protocols.pyPromptHandler
SkillRegistryProtocolcontracts/ai/skills.pySkillToolAdapter (container-resolved, optional)
SkillExecutorProtocolcontracts/ai/skills.pySkillToolAdapter (container-resolved, optional)
LLMClientProtocolcontracts/ai/llm.pySamplingHandler (container-resolved, optional)
AIMetricsProtocolcontracts/observability/ai.pyObservableToolHandler, ObservableResourceHandler (optional)
DatabaseProviderProtocolcontracts/data/__init__.pySQLConnector (container-resolved, optional)

Exceptions in contracts/mcp/exceptions.py: MCPError, MCPTransportError, MCPToolCallError, MCPInitializationError, MCPProtocolError, MCPMethodNotFoundError.

ConditionBehavior
SkillRegistryProtocol + SkillExecutorProtocol in containerSkills exposed as MCP tools via SkillToolAdapter
LLMClientProtocol in containerSamplingHandler enables sampling/createMessage
AIMetricsProtocol in containerObservableToolHandler/ObservableResourceHandler wrap handlers
DatabaseProviderProtocol in container + sql.dsn setSQLConnector activates

flowchart LR
    subgraph Contracts[lexigram-contracts /mcp/exceptions]
        MCPError --> MCPTransportError
        MCPError --> MCPToolCallError
        MCPError --> MCPInitializationError
        MCPError --> MCPProtocolError
        MCPError --> MCPMethodNotFoundError
    end
    subgraph Server[MCPServer]
        SH[handle_message] -->|converts to| JSONRPCError[JSON-RPC error response]
    end
    subgraph Client[MCPClient]
        CR[_request / call_tool] -->|raises| Exn[Python exception]
    end

The server never raises — it converts errors to JSON-RPC error responses. The client raises Python exceptions on protocol violations and tool errors.


PointMechanism
Custom transportSubclass AbstractTransport, implement start/stop/send/receive
Custom tools/resources/promptsMCPController subclass with @tool/@resource/@prompt
Custom tool/resource providerImplement MCPToolProviderProtocol or MCPResourceProviderProtocol
Service auto-exposureMCPModule.from_services(services=[UserService])
Built-in connectorsConfigure via MCPConfig.connectors (FS, GitHub, WebFetch, WebSearch, Slack, GDrive, SQL)
Skill bridgeInstall lexigram-ai-skillsSkillToolAdapter auto-wires
Agent tool bridgeMCPToolAdapter wraps external MCP tools as ToolProtocol
ObservabilityRegister AIMetricsProtocolObservableToolHandler wraps automatically
Sampling (server→LLM)Register LLMClientProtocolSamplingHandler activates