@@ -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+
280467def main () -> None :
281468 """Entry point for ``model-ledger mcp`` command.
282469
0 commit comments