Skip to content

Commit 0c1b485

Browse files
Vignesh Narayanaswamyclaude
authored andcommitted
Fix HTTP backend: MCP tools pass through to REST API directly
When using --backend http, the MCP server now calls the REST API endpoints directly instead of going through the Ledger SDK. The SDK's graph walking and snapshot querying can't fully work over HTTP CRUD — but the REST API already computes everything server-side. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 94518e6 commit 0c1b485

File tree

1 file changed

+188
-1
lines changed

1 file changed

+188
-1
lines changed

src/model_ledger/mcp/server.py

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,19 @@ def create_server(
3838
3939
Args:
4040
backend: Optional storage backend. Defaults to InMemoryLedgerBackend.
41+
If an HttpLedgerBackend is provided, tools call the remote REST API
42+
directly instead of going through the Ledger SDK.
4143
demo: If True, pre-populate with sample data (requires datasets.demo module).
4244
4345
Returns:
4446
A configured FastMCP server ready to ``run()``.
4547
"""
48+
from model_ledger.backends.http import HttpLedgerBackend
49+
50+
# HTTP backend → pass-through mode: call REST API directly
51+
if isinstance(backend, HttpLedgerBackend):
52+
return _create_http_server(backend)
53+
4654
if backend is None:
4755
backend = InMemoryLedgerBackend()
4856

@@ -54,7 +62,7 @@ def create_server(
5462

5563
load_demo_inventory(ledger)
5664
except ImportError:
57-
pass # demo module not yet available (Task 11)
65+
pass
5866

5967
mcp = FastMCP("model-ledger")
6068

@@ -277,6 +285,185 @@ def backends_resource() -> str:
277285
return mcp
278286

279287

288+
def _create_http_server(http_backend: Any) -> FastMCP:
289+
"""Create MCP server that passes through to a remote REST API.
290+
291+
Instead of going through the Ledger SDK (which can't fully work over
292+
HTTP), the tools call the REST API endpoints directly. The server-side
293+
REST API does all the computation.
294+
"""
295+
client = http_backend._client
296+
mcp = FastMCP("model-ledger")
297+
298+
@mcp.tool()
299+
def discover(
300+
source_type: str,
301+
models: list[dict] | None = None,
302+
connector_name: str | None = None,
303+
connector_config: dict | None = None,
304+
file_path: str | None = None,
305+
auto_connect: bool = True,
306+
) -> dict:
307+
"""Import models from external sources into the ledger.
308+
309+
Supports inline model dicts, JSON files, or named connectors.
310+
Returns counts of models added/skipped and links created.
311+
"""
312+
resp = client.post("/discover", json={
313+
"source_type": source_type,
314+
"models": models,
315+
"connector_name": connector_name,
316+
"connector_config": connector_config,
317+
"file_path": file_path,
318+
"auto_connect": auto_connect,
319+
})
320+
return resp.json()
321+
322+
@mcp.tool()
323+
def record(
324+
model_name: str,
325+
event: str,
326+
payload: dict | None = None,
327+
actor: str = "user",
328+
owner: str | None = None,
329+
model_type: str | None = None,
330+
purpose: str | None = None,
331+
) -> dict:
332+
"""Register a new model or record an event on an existing model.
333+
334+
Use event='registered' to create a new model. Any other event
335+
value appends to an existing model's history.
336+
"""
337+
resp = client.post("/record", json={
338+
"model_name": model_name,
339+
"event": event,
340+
"payload": payload or {},
341+
"actor": actor,
342+
"owner": owner,
343+
"model_type": model_type,
344+
"purpose": purpose,
345+
})
346+
return resp.json()
347+
348+
@mcp.tool()
349+
def investigate(
350+
model_name: str,
351+
detail: str = "summary",
352+
as_of: str | None = None,
353+
) -> dict:
354+
"""Deep-dive into a single model — history, metadata, dependencies.
355+
356+
Returns owner, type, status, recent events, upstream/downstream
357+
dependencies, and group memberships.
358+
"""
359+
params: dict[str, Any] = {"detail": detail}
360+
if as_of:
361+
params["as_of"] = as_of
362+
resp = client.get(f"/investigate/{model_name}", params=params)
363+
return resp.json()
364+
365+
@mcp.tool()
366+
def query(
367+
text: str | None = None,
368+
platform: str | None = None,
369+
model_type: str | None = None,
370+
owner: str | None = None,
371+
status: str | None = None,
372+
limit: int = 50,
373+
offset: int = 0,
374+
) -> dict:
375+
"""Search and filter the model inventory.
376+
377+
Supports text search (fuzzy name/purpose match) and structured
378+
filters (platform, model_type, owner, status) with pagination.
379+
"""
380+
params: dict[str, Any] = {"limit": limit, "offset": offset}
381+
if text:
382+
params["text"] = text
383+
if platform:
384+
params["platform"] = platform
385+
if model_type:
386+
params["model_type"] = model_type
387+
if owner:
388+
params["owner"] = owner
389+
if status:
390+
params["status"] = status
391+
resp = client.get("/query", params=params)
392+
return resp.json()
393+
394+
@mcp.tool()
395+
def trace(
396+
name: str,
397+
direction: str = "both",
398+
depth: int | None = None,
399+
) -> dict:
400+
"""Traverse a model's dependency graph.
401+
402+
Walks upstream (models this one depends on) and/or downstream
403+
(models that depend on this one). Returns dependency nodes with
404+
depth and relationship metadata.
405+
"""
406+
params: dict[str, Any] = {"direction": direction}
407+
if depth is not None:
408+
params["depth"] = depth
409+
resp = client.get(f"/trace/{name}", params=params)
410+
return resp.json()
411+
412+
@mcp.tool()
413+
def changelog(
414+
since: str | None = None,
415+
until: str | None = None,
416+
model_name: str | None = None,
417+
event_type: str | None = None,
418+
limit: int = 100,
419+
offset: int = 0,
420+
) -> dict:
421+
"""View cross-model event history with time range filtering.
422+
423+
Returns events sorted newest-first with pagination. Defaults
424+
to the last 7 days if no time range is specified.
425+
"""
426+
params: dict[str, Any] = {"limit": limit, "offset": offset}
427+
if since:
428+
params["since"] = since
429+
if until:
430+
params["until"] = until
431+
if model_name:
432+
params["model_name"] = model_name
433+
if event_type:
434+
params["event_type"] = event_type
435+
resp = client.get("/changelog", params=params)
436+
return resp.json()
437+
438+
@mcp.resource("ledger://overview")
439+
def overview() -> str:
440+
"""Inventory statistics — model count, event count, type breakdown."""
441+
resp = client.get("/overview")
442+
return json.dumps(resp.json(), indent=2)
443+
444+
@mcp.resource("ledger://schema")
445+
def schema_resource() -> str:
446+
"""JSON Schema definitions for all tool I/O models."""
447+
all_schemas: dict[str, Any] = {}
448+
for cls in [
449+
schemas.DiscoverInput, schemas.DiscoverOutput,
450+
schemas.RecordInput, schemas.RecordOutput,
451+
schemas.QueryInput, schemas.QueryOutput,
452+
schemas.InvestigateInput, schemas.InvestigateOutput,
453+
schemas.TraceInput, schemas.TraceOutput,
454+
schemas.ChangelogInput, schemas.ChangelogOutput,
455+
]:
456+
all_schemas[cls.__name__] = cls.model_json_schema()
457+
return json.dumps(all_schemas, indent=2)
458+
459+
@mcp.resource("ledger://backends")
460+
def backends_resource() -> str:
461+
"""Active backend configuration."""
462+
return json.dumps({"backend": "http", "url": str(client.base_url)}, indent=2)
463+
464+
return mcp
465+
466+
280467
def main() -> None:
281468
"""Entry point for ``model-ledger mcp`` command.
282469

0 commit comments

Comments
 (0)