MCP server¶
The mirror of the MCP connector. Where the connector turns an
external MCP server into lazybridge.Tool entries, the MCP server
does the opposite: it exposes LazyTools' own tool providers over MCP so
any MCP host — Claude Desktop, Claude Code, ChatGPT Codex — can call
datahub_*, statistical_*, regime_* and web search/crawl as native
tools.
Status: alpha. Install:
pip install "lazytoolkit[mcp] @ git+https://github.com/selvaz/LazyTools.git".
The bridge is thin because LazyBridge already normalises every tool behind
one abstraction: tool.definition() yields the JSON Schema MCP wants for
inputSchema, and await tool.run(**kwargs) dispatches with argument
validation built in. The whole server is expand providers → list them →
dispatch calls.
Quick start¶
Run it over stdio (the transport MCP hosts launch):
lazytools-mcp # all read-only providers
lazytools-mcp datahub statistical # a subset (positional provider ids)
python -m lazytools.mcp_server # equivalent module form
Point Claude Desktop / Claude Code at it:
That's it — the host now sees LazyTools' read-only surface as native tools.
Providers whose optional extra is missing are simply skipped, so a bare
[mcp] install serves datahub + statistical; install
lazystats[regimes] / the [web] extra to light up regimes / web.
Providers¶
lazytools-mcp serves the read-only provider menu. Pass ids to select a
subset, or set LAZYTOOLS_MCP_PROVIDERS=datahub,statistical in the env.
| id | Provider | Tools | Needs |
|---|---|---|---|
datahub |
DataHubTools() |
datahub_* discovery, resolution, financial facts |
market-data-hub installed |
statistical |
StatisticalAnalysisTools() |
volatility, correlation, outliers, regression | market-data-hub installed |
regimes |
RegimeTools() |
regime_* read-only inspection |
lazystats[regimes] |
web |
WebTools() |
search / crawl / get-page | [web] extra |
Safety model¶
The server is read-only by default, enforced in two layers:
- Provider-level configuration (authoritative).
default_providers()constructs every provider in its read-only shape —DataHubTools()withoutallow_raw_series/allow_refresh,RegimeTools()withallow_write=False, which never even emit their write tools. - A name-based guard (secondary).
read_only=Trueadditionally drops any tool whose name matchesUNSAFE_TOOL_PATTERNS(e.g.*_send,*_write,*_delete,*_ensure_*,*_fit) with a logged warning — a coarse net for the case where a write-enabled provider is passed in by mistake.
There is no interactive ConfirmationGate over MCP, so mutating tools stay
off the default surface. To expose them you must opt in explicitly:
- CLI —
--allow-unsafeconstructs the providers in write-enabled mode (default_providers(ids, allow_write=True)) and disables the name guard, so the menu's writers (datahub refresh/register, regime fit/persist/delete) are emitted and served. No gating is applied. - Programmatic — build write-enabled providers yourself and pass
read_only=Falsetobuild_server, ideally with your own allow-list / confirmation wrapper around the mutating tools.
Programmatic use¶
import asyncio
from lazytools.mcp_server import build_server, serve_stdio, default_providers
# Read-only by default.
server = build_server(default_providers())
asyncio.run(serve_stdio(server))
build_server accepts any mix of ToolProviders, Tools, and plain
callables — so you can serve a custom, curated surface:
from lazytools.connectors.datahub import DataHubTools
from lazytools.statistical_analysis import StatisticalAnalysisTools
server = build_server(
[DataHubTools(), StatisticalAnalysisTools()],
name="lazytools-finance",
instructions="Read-only market data + statistics.",
)
API¶
lazytools.mcp_server ¶
LazyTools MCP server — expose LazyTools' tool providers over MCP.
The mirror of :mod:lazytools.connectors.mcp (the MCP client). Where the
client turns an external MCP server into lazybridge.Tool entries, this
server turns LazyTools' own providers into an MCP endpoint any host
(Claude Desktop, Claude Code, Codex, …) can call.
Quick start (stdio, the transport MCP hosts launch)::
python -m lazytools.mcp_server # all read-only providers
python -m lazytools.mcp_server datahub statistical # a subset
Claude Desktop / Claude Code config::
{
"mcpServers": {
"lazytools": { "command": "lazytools-mcp" }
}
}
Programmatic use::
import asyncio
from lazytools.mcp_server import build_server, serve_stdio, default_providers
server = build_server(default_providers()) # read-only by default
asyncio.run(serve_stdio(server))
Install with::
pip install lazytoolkit[mcp]
The server is read-only by default; see :func:build_server for the
safety model.
UNSAFE_TOOL_PATTERNS
module-attribute
¶
UNSAFE_TOOL_PATTERNS: tuple[str, ...] = ('_send', '_write', '_delete', '_register', '_ensure_', '_refresh', '_persist', '_save', '_export_', '_fit')
build_server ¶
build_server(providers: Sequence[Any], *, name: str = 'lazytools', version: str | None = None, read_only: bool = True, unsafe_patterns: Iterable[str] = UNSAFE_TOOL_PATTERNS, instructions: str | None = None) -> Server
Build a low-level MCP :class:~mcp.server.lowlevel.Server for providers.
The returned server has two handlers wired:
list_tools→ one MCPToolper expanded LazyBridge tool, withinputSchemataken verbatim fromtool.definition().parameters;call_tool→await tool.run(**arguments), the return value serialized via :func:result_to_text. Tool errors are returned as MCP error results (isError=True) instead of crashing the session.
Requires the mcp extra (pip install lazytoolkit[mcp]).
Source code in src/lazytools/mcp_server/server.py
serve_stdio
async
¶
Run server over stdio until the client disconnects.
This is the transport Claude Desktop / Claude Code launch: the host spawns the process and speaks JSON-RPC over its stdin/stdout.
Source code in src/lazytools/mcp_server/server.py
default_providers ¶
Instantiate the default providers.
ids selects a subset (validated against :data:PROVIDER_FACTORIES);
None builds them all. allow_write is threaded to every factory —
False (default) yields read-only providers, True yields the
write-enabled shapes (the CLI's --allow-unsafe path). A factory that
raises at construction time (rare — most only fail later, at
as_tools()) is skipped so one missing dependency never sinks the
server.
Source code in src/lazytools/mcp_server/providers.py
expand_tools ¶
expand_tools(providers: Sequence[Any], *, read_only: bool = True, unsafe_patterns: Iterable[str] = UNSAFE_TOOL_PATTERNS) -> dict[str, Tool]
Expand providers into a name -> Tool map, isolating failures.
Each item may be a :class:lazybridge.Tool, a plain callable, an Agent,
or a ToolProvider (anything with as_tools()). Providers are
expanded independently: if one raises on expansion — typically an
ImportError because its optional extra is not installed (e.g.
RegimeTools without lazystats[regimes]) — it is skipped with a
warning and the rest still load. This is what lets a bare
pip install lazytoolkit[mcp] serve datahub + statistical while
regimes/web light up only once their extras are present.
On a name collision the last registration wins (with a warning),
mirroring build_tool_map(collision_policy="replace").
Source code in src/lazytools/mcp_server/server.py
result_to_text ¶
Serialize a tool's return value to a single text payload for MCP.
Strings pass through untouched; bytes are decoded as UTF-8; everything
else is rendered as pretty JSON (pydantic model_dump_json /
to_json fast-paths first), never raising.