Skip to content

Getting Started with Lauren MCP

What is MCP?

The Model Context Protocol (MCP) is an open standard that defines how AI assistants (clients) discover and call tools hosted by external services (servers). It ships a JSON-RPC 2.0 wire format layered over several transports:

Transport Protocol version Use case
stdio any Local tools and subprocesses
WebSocket any Persistent, bidirectional connections
HTTP + SSE (legacy) MCP 2024-11-05 Stateless or browser-friendly deployments
Streamable HTTP MCP 2025-03-26 Modern HTTP, single-endpoint, streaming

MCP lets an AI client — such as Claude, a custom agent, or any MCP-aware application — call your service's tools without knowing anything about its internal implementation. The server advertises a schema for each tool (name, description, JSON Schema for parameters) and the client calls them using a standard handshake.

Why lauren-mcp?

Lauren is a lightweight Python web framework focused on type-safe routing, dependency injection, and modular application composition. lauren-mcp extends it in two directions:

As a server: Any @mcp_server class becomes an MCP endpoint that AI clients can connect to over WebSocket, Streamable HTTP, legacy HTTP+SSE, or stdio. You get automatic JSON Schema generation from Python type annotations, DI-aware tool dispatch, per-call context injection, server lifecycle hooks, and full protocol lifecycle management — all without boilerplate.

As a client: The McpServer factory returns an McpClientProtocol that connects to any MCP server. Use it standalone, wire multiple clients into a Lauren agent module, or compose remote tools into a local server via McpServerModule.for_root(proxies=[...]).

Two modes at a glance

Server mode

You decorate a class with @mcp_server("/path") and individual methods with @mcp_tool(). Lauren introspects the type annotations and docstrings to build a valid MCP tools/list response. When an AI client calls a tool the framework dispatches to the correct method, runs any DI resolvers, and returns the result as a valid MCP content block.

AI client  ──── WebSocket / Streamable HTTP / SSE ────▶  Lauren app
                                                           └─ @mcp_server("/mcp")
                                                               ├─ @mcp_lifespan    (startup/shutdown)
                                                               ├─ @mcp_tool()      search(...)
                                                               ├─ @mcp_tool()      add_item(...)
                                                               └─ @mcp_resource()  items://{id}

The transport is selected by passing transport= to @mcp_server or to McpServerModule.for_root(). The default is "ws" (WebSocket). Use "streamable" for the 2025-03-26 Streamable HTTP transport, "all" for both WebSocket and Streamable HTTP simultaneously, or "sse" / "both" for legacy deployments.

Client mode

You construct a client via the McpServer factory and call it directly or wire it into a Lauren agent module. Every factory method accepts optional handlers for progress notifications, server log messages, tool-list changes, sampling requests, and elicitation requests.

Lauren app ──── stdio / WebSocket / Streamable HTTP ────▶  External MCP server
  AgentModule                                               └─ tools: ["read_file", "write_file"]
    └─ agent sees: "fs__read_file", "fs__write_file"

After connect() you can inspect client.protocol_version to see which MCP version was negotiated, subscribe to notifications with client.on_progress() / client.on_log() / client.on_list_changed(), and call client.notify_roots_changed() when your client-side file-system roots change.

Key concepts

JSON Schema generation

@mcp_tool builds the inputSchema advertised in tools/list directly from the method's Python type annotations. Standard types (str, int, float, bool, list[str], dict[str, int]) map to their JSON Schema equivalents. With the optional [pydantic] or [msgspec] extras you also get full schema generation from Pydantic BaseModel, msgspec.Struct, @dataclass, and TypedDict parameters — including nested models, discriminated unions, and Annotated[T, Field(...)] constraints.

McpToolContext

Declare a parameter annotated as McpToolContext in any @mcp_tool method and the framework injects a per-call context object. It is excluded from the advertised JSON Schema — clients never see it or need to supply it.

@mcp_tool()
async def process(self, data: str, ctx: McpToolContext) -> str:
    await ctx.report_progress(0, 100)
    await ctx.info("Starting processing")
    result = heavy_work(data)
    await ctx.report_progress(100, 100)
    return result

Lifespan

@mcp_lifespan on an async generator method runs once at server startup and once at shutdown. The dict the generator yields is available as ctx.lifespan_context inside every tool call — a clean pattern for sharing database connections, HTTP clients, or any other startup-time resource.

Server composition

McpServerModule.for_root() accepts mounts= (merge tools from sibling @mcp_server classes) and proxies= (forward to remote MCP clients). Combined, this lets you aggregate many logical servers behind one endpoint with name-prefixed namespacing and collision detection at startup.

Next steps