diff --git a/hatch_build.py b/hatch_build.py
index 0bacf21..a529613 100644
--- a/hatch_build.py
+++ b/hatch_build.py
@@ -39,6 +39,7 @@
"packages/catalog/src/mdl_catalog": "mdl_catalog",
"packages/server/src/mdl_server": "mdl_server",
"packages/lsp/src/mdl_lsp": "mdl_lsp",
+ "packages/mcp/src/mdl_mcp": "mdl_mcp",
"packages/adapters/collibra/src/mdl_adapter_collibra": "mdl_adapter_collibra",
}
diff --git a/packages/cli/pyproject.toml b/packages/cli/pyproject.toml
index 72a60ca..bc1ab74 100644
--- a/packages/cli/pyproject.toml
+++ b/packages/cli/pyproject.toml
@@ -18,6 +18,7 @@ dependencies = [
"modelith-adapter-collibra",
"modelith-server",
"modelith-lsp",
+ "modelith-mcp",
"typer>=0.12",
]
@@ -45,3 +46,4 @@ modelith-catalog = { workspace = true }
modelith-adapter-collibra = { workspace = true }
modelith-server = { workspace = true }
modelith-lsp = { workspace = true }
+modelith-mcp = { workspace = true }
diff --git a/packages/cli/src/mdl_cli/main.py b/packages/cli/src/mdl_cli/main.py
index 7b77c1d..1bda4f3 100644
--- a/packages/cli/src/mdl_cli/main.py
+++ b/packages/cli/src/mdl_cli/main.py
@@ -1677,6 +1677,80 @@ def lsp() -> None:
lsp_main()
+model_app = typer.Typer(
+ help="Read the model as JSON (the shared query layer behind the AI surfaces)."
+)
+app.add_typer(model_app, name="model")
+
+
+@model_app.command("context")
+def model_context(model_dir: Path = typer.Option(Path("."), "--model-dir", "-m")) -> None:
+ """Print a condensed model summary (project, counts, subject areas, relationships)
+ as JSON — sized for a chat context window. Backs `@modelith` in VS Code Ask mode
+ and the MCP `get_model_context` tool."""
+ from mdl_core.query import get_model_context
+
+ typer.echo(json.dumps(get_model_context(_load(model_dir).model), default=str))
+
+
+@model_app.command("entities")
+def model_entities(
+ model_dir: Path = typer.Option(Path("."), "--model-dir", "-m"),
+ subject_area: str = typer.Option(None, "--subject-area", help="Scope by area name or ULID"),
+) -> None:
+ """List logical entities (name, definition, attribute count, subject area) as JSON."""
+ from mdl_core.query import list_entities
+
+ typer.echo(json.dumps(list_entities(_load(model_dir).model, subject_area), default=str))
+
+
+@model_app.command("entity")
+def model_entity(
+ name: str = typer.Argument(..., help="Entity name or ULID"),
+ model_dir: Path = typer.Option(Path("."), "--model-dir", "-m"),
+) -> None:
+ """Print one entity's full detail (attributes, keys, conceptual layer,
+ relationships) as JSON. Exits 1 with an error object if no entity matches."""
+ from mdl_core.query import get_entity
+
+ hit = get_entity(_load(model_dir).model, name)
+ if hit is None:
+ typer.echo(json.dumps({"error": f"no entity named {name!r}"}))
+ raise typer.Exit(1)
+ typer.echo(json.dumps(hit, default=str))
+
+
+@model_app.command("detail")
+def model_detail(
+ model_dir: Path = typer.Option(Path("."), "--model-dir", "-m"),
+ limit: int = typer.Option(40, "--limit", help="Max entities (alphabetical) to include"),
+) -> None:
+ """Print compact detail for EVERY entity (attributes, keys, relationships) as JSON,
+ for grounding a model-wide question. Terse by design and capped at --limit, with a
+ `truncated` flag so a large model doesn't overflow a chat context window."""
+ from mdl_core.query import entities_detail
+
+ typer.echo(json.dumps(entities_detail(_load(model_dir).model, limit=limit), default=str))
+
+
+@app.command()
+def mcp(
+ repo: Path = typer.Option(
+ Path("."), "--repo", "-m", help="Model repo directory (contains mdl-project.yaml)"
+ ),
+) -> None:
+ """Start the Modelith MCP server (stdio) — model + ontology tools for an AI
+ agent (VS Code Copilot Chat in agent mode, Claude Desktop, Cursor).
+
+ Reads (list_entities, get_entity, search_ontology, get_model_context, validate)
+ and writes (create_entity, update_entity) share the same core query + command
+ engine as the CLI and canvas. Writes go straight to this checkout — the
+ engineer's own trust boundary, not the SME propose-as-PR flow."""
+ from mdl_mcp.server import run as mcp_run
+
+ mcp_run(repo)
+
+
@app.command()
def serve(
model_dir: Path = typer.Option(Path("."), "--model-dir", "-m"),
diff --git a/packages/core/src/mdl_core/query.py b/packages/core/src/mdl_core/query.py
new file mode 100644
index 0000000..6461c9b
--- /dev/null
+++ b/packages/core/src/mdl_core/query.py
@@ -0,0 +1,276 @@
+"""The shared read/query layer for the AI surfaces (MCP server + chat participant).
+
+Both the VS Code MCP server (agent mode) and the `@modelith` chat participant
+answer questions from the same functions here, so the two frontends never drift.
+The shapes are deliberately CONDENSED — sized for a chat context window — not the
+full canvas projection (`mdl_server.projection`), which pre-joins everything the
+React graph needs. A grounding tool wants "what exists and how it connects" in a
+few hundred tokens, not the whole wire model.
+
+Everything here is a pure read over an in-memory `Model`; nothing loads or writes.
+The CLI/MCP wrappers do the `ModelRepo.load` and hand the model in.
+"""
+
+from __future__ import annotations
+
+from mdl_core.ir import ConceptualEntity, LogicalEntity, Model, Term
+
+
+def _entity_by_name(model: Model, name: str) -> LogicalEntity | None:
+ """Resolve a logical entity by name or ULID. There is no name index in the IR
+ (only ULID-keyed dicts and domain/code-set name lookups), so match by name
+ case-insensitively, then fall back to a ULID hit."""
+ lname = name.lower()
+ for le in model.logical_entities.values():
+ if le.name.lower() == lname:
+ return le
+ obj = model.logical_entities.get(name)
+ return obj
+
+
+def _ontology_summary(obj: ConceptualEntity | Term | LogicalEntity) -> str | None:
+ """The object's canonical alignment as a single prefixed IRI, or None. Reads the
+ `primary_ref` accessor (source of truth post-migration)."""
+ ref = getattr(obj, "primary_ref", None)
+ if ref is None:
+ return None
+ return ref.uri or None
+
+
+def list_entities(model: Model, subject_area: str | None = None) -> list[dict]:
+ """Grounding: every logical entity as {name, definition, attribute_count,
+ subject_area}, sorted by name. With `subject_area` (name or ULID), scope to
+ entities homed in / realising that area."""
+ scope: set[str] | None = None
+ if subject_area:
+ sa_id = _resolve_subject_area_id(model, subject_area)
+ # An unknown area name scopes to NOTHING (empty set), not to everything —
+ # returning the whole model on a typo'd filter would silently mislead.
+ if sa_id is None:
+ return []
+ sa = model.subject_areas.get(sa_id)
+ ce_ids = set(sa.members) if sa else set()
+ ce_ids |= {
+ ce.id for ce in model.conceptual_entities.values() if ce.subject_area == sa_id
+ }
+ scope = {le.id for le in model.logical_entities.values() if le.realises in ce_ids}
+
+ out: list[dict] = []
+ for le in sorted(model.logical_entities.values(), key=lambda e: e.name):
+ if scope is not None and le.id not in scope:
+ continue
+ ce = model.conceptual_entities.get(le.realises) if le.realises else None
+ sa = (
+ model.subject_areas.get(ce.subject_area)
+ if ce and ce.subject_area
+ else None
+ )
+ out.append(
+ {
+ "name": le.name,
+ "definition": (le.definition or (ce.definition if ce else None)),
+ "attribute_count": len(le.attributes),
+ "subject_area": sa.name if sa else None,
+ }
+ )
+ return out
+
+
+def _keys_for(model: Model, le: LogicalEntity) -> list[dict]:
+ """The entity's key groups (erwin Key Groups) — pk / unique / alternate / index —
+ with member attribute names in key order. This is what lets a caller reason about
+ normalization (candidate keys, functional dependencies) and identity.
+
+ When no explicit `pk` KeyGroup is present, fall back to the legacy per-attribute
+ `role: business_key` convention (which the IR documents as still supported): the
+ business-key attributes form an inferred primary key, flagged `inferred: True` so
+ a caller knows it came from the convention, not a declared KeyGroup."""
+ attr_name = {a.id: a.name for a in le.attributes}
+ keys: list[dict] = []
+ for kg in sorted(model.key_groups.values(), key=lambda k: (k.type, k.name)):
+ if kg.entity != le.id:
+ continue
+ keys.append(
+ {
+ "name": kg.name,
+ "type": kg.type,
+ "columns": [attr_name.get(m, m) for m in kg.members],
+ }
+ )
+ # Legacy fallback: no declared pk KeyGroup, but business_key attributes exist.
+ if not any(k["type"] == "pk" for k in keys):
+ bk = [a.name for a in le.attributes if a.role == "business_key"]
+ if bk:
+ keys.insert(0, {"name": "pk (from business_key)", "type": "pk",
+ "columns": bk, "inferred": True})
+ return keys
+
+
+def get_entity(model: Model, name: str) -> dict | None:
+ """Full detail for one entity by name or ULID: attributes (with types + ontology
+ alignment), key groups (pk/unique/alternate), the conceptual layer, and the
+ relationships it participates in. Returns None when no entity matches."""
+ le = _entity_by_name(model, name)
+ if le is None:
+ return None
+ ce = model.conceptual_entities.get(le.realises) if le.realises else None
+
+ attributes = [
+ {
+ "name": a.name,
+ "definition": a.definition,
+ "domain": a.domain,
+ "nullable": a.nullable,
+ "ontology": _ontology_summary(a),
+ }
+ for a in le.attributes
+ ]
+ keys = _keys_for(model, le)
+
+ rels: list[dict] = []
+ for r in sorted(model.relationships.values(), key=lambda x: x.name):
+ from_e = model.logical_entities.get(r.from_.entity)
+ to_e = model.logical_entities.get(r.to.entity)
+ if le.id not in (r.from_.entity, r.to.entity):
+ continue
+ rels.append(
+ {
+ "name": r.name,
+ "from": from_e.name if from_e else r.from_.entity,
+ "to": to_e.name if to_e else r.to.entity,
+ "definition": r.definition,
+ }
+ )
+
+ return {
+ "name": le.name,
+ "definition": le.definition or (ce.definition if ce else None),
+ "pattern": le.pattern,
+ "unmanaged": bool(le.unmanaged),
+ "conceptual": (
+ {
+ "name": ce.name,
+ "definition": ce.definition,
+ "synonyms": list(ce.synonyms),
+ "ontology": _ontology_summary(ce),
+ "ontology_layer": ce.ontology_layer,
+ }
+ if ce
+ else None
+ ),
+ "keys": keys,
+ "attributes": attributes,
+ "relationships": rels,
+ }
+
+
+def get_model_context(model: Model) -> dict:
+ """A condensed model summary sized for a chat context window: the project name,
+ counts, subject areas with their entity lists, and every relationship as a
+ from->to sentence. Enough to ground an answer without shipping the whole model."""
+ ce_by_id = model.conceptual_entities
+
+ # subject areas -> the logical entities homed there
+ areas: list[dict] = []
+ for sa in sorted(model.subject_areas.values(), key=lambda s: s.name):
+ ce_ids = set(sa.members)
+ ce_ids |= {ce.id for ce in ce_by_id.values() if ce.subject_area == sa.id}
+ names = sorted(
+ le.name for le in model.logical_entities.values() if le.realises in ce_ids
+ )
+ areas.append({"name": sa.name, "definition": sa.definition, "entities": names})
+
+ unhomed = sorted(
+ le.name
+ for le in model.logical_entities.values()
+ if not le.realises or ce_by_id.get(le.realises) is None
+ )
+
+ relationships = []
+ for r in sorted(model.relationships.values(), key=lambda x: x.name):
+ from_e = model.logical_entities.get(r.from_.entity)
+ to_e = model.logical_entities.get(r.to.entity)
+ relationships.append(
+ {
+ "name": r.name,
+ "from": from_e.name if from_e else r.from_.entity,
+ "to": to_e.name if to_e else r.to.entity,
+ }
+ )
+
+ return {
+ "project": model.config.name,
+ "counts": {
+ "entities": len(model.logical_entities),
+ "relationships": len(model.relationships),
+ "subject_areas": len(model.subject_areas),
+ "terms": len(model.terms),
+ },
+ "subject_areas": areas,
+ "unassigned_entities": unhomed,
+ "relationships": relationships,
+ }
+
+
+def entities_detail(model: Model, limit: int = 40) -> dict:
+ """Compact detail for EVERY entity — attributes (name:domain, nullability), key
+ groups, and relationship names — for grounding a model-wide question ("are all
+ entities in BCNF?", "which lack a primary key?"). Deliberately terse: a real
+ model can have 150+ entities, so each is one small record, not the full nested
+ `get_entity` shape, keeping the whole thing within a chat context window.
+
+ Capped at `limit` entities (alphabetical). `truncated` is True when the model has
+ more, with `total` and `shown` so the caller can tell the user the answer covers
+ only the first N and to narrow the question."""
+ all_le = sorted(model.logical_entities.values(), key=lambda e: e.name)
+ shown = all_le[:limit]
+ ce_by_id = model.conceptual_entities
+
+ # relationship names per entity, one pass
+ rels_by_entity: dict[str, list[str]] = {}
+ for r in model.relationships.values():
+ from_e = model.logical_entities.get(r.from_.entity)
+ to_e = model.logical_entities.get(r.to.entity)
+ label = f"{from_e.name if from_e else '?'}→{to_e.name if to_e else '?'}"
+ for end in (r.from_.entity, r.to.entity):
+ rels_by_entity.setdefault(end, []).append(label)
+
+ records = []
+ for le in shown:
+ ce = ce_by_id.get(le.realises) if le.realises else None
+ records.append(
+ {
+ "name": le.name,
+ # "col:domain" (+ "?" when nullable) — enough to reason about
+ # attributes and dependencies without the full attribute objects.
+ "attributes": [
+ f"{a.name}:{a.domain or '?'}{'?' if a.nullable else ''}"
+ for a in le.attributes
+ ],
+ "keys": [
+ {"type": kg["type"], "columns": kg["columns"]}
+ for kg in _keys_for(model, le)
+ ],
+ "relationships": sorted(set(rels_by_entity.get(le.id, []))),
+ "ontology": _ontology_summary(ce) if ce else None,
+ }
+ )
+
+ return {
+ "project": model.config.name,
+ "total": len(all_le),
+ "shown": len(shown),
+ "truncated": len(all_le) > limit,
+ "entities": records,
+ }
+
+
+def _resolve_subject_area_id(model: Model, ref: str) -> str | None:
+ """Accept a subject-area name or ULID; return its ULID, or None if unknown."""
+ if ref in model.subject_areas:
+ return ref
+ lref = ref.lower()
+ for sa in model.subject_areas.values():
+ if sa.name.lower() == lref:
+ return sa.id
+ return None
diff --git a/packages/mcp/pyproject.toml b/packages/mcp/pyproject.toml
new file mode 100644
index 0000000..a6d8e26
--- /dev/null
+++ b/packages/mcp/pyproject.toml
@@ -0,0 +1,25 @@
+[project]
+name = "modelith-mcp"
+version = "0.1.0"
+description = "Modelith MCP server — model + ontology tools for AI agents (Copilot Chat agent mode, Claude, etc.)"
+requires-python = ">=3.11"
+license = { text = "Apache-2.0" }
+dependencies = [
+ "modelith-core",
+ "modelith-ontology",
+ # v1 FastMCP API. mcp 2.x renamed FastMCP -> MCPServer and is still very new;
+ # editor MCP clients (VS Code, Claude Desktop, Cursor) target the v1 stdio
+ # server today, so pin below 2 until the ecosystem moves.
+ "mcp>=1.2,<2",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/mdl_mcp"]
+
+[tool.uv.sources]
+modelith-core = { workspace = true }
+modelith-ontology = { workspace = true }
diff --git a/packages/mcp/src/mdl_mcp/__init__.py b/packages/mcp/src/mdl_mcp/__init__.py
new file mode 100644
index 0000000..34079a2
--- /dev/null
+++ b/packages/mcp/src/mdl_mcp/__init__.py
@@ -0,0 +1,11 @@
+"""Modelith MCP server.
+
+Exposes the model as MCP tools for an AI agent (VS Code Copilot Chat in *agent*
+mode, Claude Desktop, Cursor, …). Reads come from `mdl_core.query`; writes go
+through `mdl_core.commands.apply_command`, so they take the same validation and
+concurrency path as every other Modelith write. Run it with `mdl mcp --repo
`.
+"""
+
+from mdl_mcp.server import build_server, run
+
+__all__ = ["build_server", "run"]
diff --git a/packages/mcp/src/mdl_mcp/server.py b/packages/mcp/src/mdl_mcp/server.py
new file mode 100644
index 0000000..737783e
--- /dev/null
+++ b/packages/mcp/src/mdl_mcp/server.py
@@ -0,0 +1,245 @@
+"""The Modelith MCP server (stdio).
+
+Seven tools, mirroring the AI spec. Reads (`list_entities`, `get_entity`,
+`search_ontology`, `get_model_context`, `validate`) come from the shared query
+layer; writes (`create_entity`, `update_entity`) go through
+`mdl_core.commands.apply_command` so they are validated and fingerprint-guarded
+exactly like a canvas or CLI edit.
+
+Direct local write is intentional (spec §1): this runs against the engineer's own
+checkout, a different trust boundary than the SME app's propose-as-PR flow.
+
+The server is bound to ONE model directory, passed at startup (`--repo`). A
+programmatically registered MCP server has no implicit repo scope the way a
+committed `.vscode/mcp.json` would, so VS Code passes the workspace folder through.
+"""
+
+from __future__ import annotations
+
+import json
+from functools import lru_cache
+from pathlib import Path
+from typing import Any
+
+from mcp.server.fastmcp import FastMCP
+
+from mdl_core import query
+from mdl_core.commands import CommandError, StaleModelError, apply_command
+from mdl_core.diagnostics import Severity
+from mdl_core.repo import ModelRepo
+from mdl_core.validate import validate as run_validate
+
+
+def build_server(repo_dir: Path) -> FastMCP:
+ """Assemble the MCP server bound to `repo_dir`. Split out from `run()` so tests
+ can drive the tools without stdio."""
+ repo_dir = repo_dir.resolve()
+ mcp = FastMCP("modelith")
+
+ def _model():
+ # Reload per call: an agent's own writes (and a human editing alongside it)
+ # must be visible to the next read. Cheap on the demo; the LSP/server carry
+ # their own caches for the hot paths.
+ return ModelRepo.load(repo_dir).model
+
+ # --- reads ------------------------------------------------------------------
+
+ @mcp.tool()
+ def list_entities(subject_area: str | None = None) -> str:
+ """List the model's logical entities (name, definition, attribute count,
+ subject area). Grounding: call this first to see what already exists.
+ Optionally scope to a subject area by name or ULID."""
+ return _json(query.list_entities(_model(), subject_area))
+
+ @mcp.tool()
+ def get_entity(name: str) -> str:
+ """Full detail for one entity by name (or ULID): its attributes with types
+ and ontology alignment, its key groups (pk / unique / alternate), the
+ conceptual layer, and the relationships it takes part in. Returns an error
+ object if no entity matches."""
+ hit = query.get_entity(_model(), name)
+ if hit is None:
+ return _json({"error": f"no entity named {name!r}"})
+ return _json(hit)
+
+ @mcp.tool()
+ def get_model_context() -> str:
+ """A condensed summary of the whole model — project, counts, subject areas
+ with their entities, and every relationship — sized for a chat context
+ window. Use it to orient before answering a broad question."""
+ return _json(query.get_model_context(_model()))
+
+ @mcp.tool()
+ def search_ontology(text: str, within: str | None = None, limit: int = 10) -> str:
+ """Search the model's configured industry/enterprise vocabularies for terms
+ matching `text` (e.g. to find a FIBO class to align an entity to). `within`
+ scopes to a single vocabulary id (see the source's ontology list). Returns
+ ranked hits with their prefixed IRI, label and definition."""
+ reg = _registry(repo_dir)
+ if reg is None:
+ return _json({"warning": "no ontology sources configured", "results": []})
+ results = reg.search(text, within=within, limit=limit)
+ return _json(
+ {
+ "results": [
+ {
+ "prefixed": r.prefixed,
+ "iri": r.iri,
+ "label": r.label,
+ "definition": r.definition,
+ "source": r.source,
+ }
+ for r in results
+ ]
+ }
+ )
+
+ @mcp.tool()
+ def validate() -> str:
+ """Validate the model (schema, references, ontology rules, naming) and return
+ the diagnostics. Use this before writing, or to check the model's health."""
+ diags = run_validate(_model())
+ return _json(
+ {
+ "ok": not diags.has(Severity.error),
+ "diagnostics": [
+ {"code": d.code, "severity": d.severity.value, "message": d.message}
+ for d in diags.items
+ ],
+ }
+ )
+
+ # --- writes (validated + fingerprint-guarded via apply_command) -------------
+
+ @mcp.tool()
+ def create_entity(
+ name: str,
+ definition: str | None = None,
+ subject_area: str | None = None,
+ layer: str | None = None,
+ ) -> str:
+ """Create a conceptual + logical entity pair. Writes to the local model on
+ disk (validated before it lands). `subject_area` is a subject-area ULID;
+ `layer` is one of industry|core|domain|specialised. Returns the new entity's
+ ULID or an error object."""
+ return _write(
+ repo_dir,
+ "create_entity",
+ {
+ "name": name,
+ "definition": definition,
+ "subject_area": subject_area,
+ "layer": layer,
+ },
+ )
+
+ @mcp.tool()
+ def update_entity(name: str, changes: dict[str, Any]) -> str:
+ """Update an existing entity by name (or ULID). `changes` may contain:
+ `definition` (str), `rename` (str, the new name), `add_attribute`
+ ({name, domain?, definition?, nullable?}), or `subject_area` (ULID). Each is
+ applied through the validated command engine; the whole update is rejected
+ (nothing written) if any step is invalid. Returns ok or an error object."""
+ le = query._entity_by_name(_model(), name) # noqa: SLF001 - shared helper
+ if le is None:
+ return _json({"error": f"no entity named {name!r}"})
+ return _apply_updates(repo_dir, le.id, le.realises, changes)
+
+ return mcp
+
+
+# --- helpers -------------------------------------------------------------------
+
+
+def _apply_updates(
+ repo_dir: Path, entity_id: str, conceptual_id: str | None, changes: dict[str, Any]
+) -> str:
+ """Map the `update_entity` change bag onto field-scoped commands. Applied in a
+ fixed order; each goes through apply_command, so validation runs after every
+ step and a bad change stops the sequence. Definition and subject area live on the
+ CONCEPTUAL node, so those ops take the conceptual ULID; rename and attributes are
+ logical."""
+ applied: list[str] = []
+ try:
+ if "definition" in changes:
+ if conceptual_id is None:
+ return _json({"error": "entity has no conceptual layer to set a definition on"})
+ apply_command(
+ repo_dir,
+ "set_definition",
+ {"id": conceptual_id, "definition": changes["definition"]},
+ )
+ applied.append("definition")
+ if "rename" in changes:
+ apply_command(repo_dir, "rename_entity", {"id": entity_id, "name": changes["rename"]})
+ applied.append("rename")
+ if "subject_area" in changes:
+ if conceptual_id is None:
+ return _json({"error": "entity has no conceptual layer to home in a subject area"})
+ apply_command(
+ repo_dir,
+ "set_subject_area",
+ {"id": conceptual_id, "subject_area": changes["subject_area"]},
+ )
+ applied.append("subject_area")
+ if "add_attribute" in changes:
+ attr = changes["add_attribute"]
+ if not isinstance(attr, dict) or "name" not in attr:
+ return _json({"error": "add_attribute must be an object with at least a name"})
+ apply_command(repo_dir, "add_attribute", {"entity_id": entity_id, **attr})
+ applied.append(f"add_attribute:{attr['name']}")
+ except StaleModelError as e:
+ return _json({"error": f"model changed on disk mid-update: {e}", "applied": applied})
+ except (CommandError, FileNotFoundError, ValueError) as e:
+ return _json({"error": str(e), "applied": applied})
+ if not applied:
+ return _json({"error": "no recognised keys in changes", "applied": []})
+ return _json({"ok": True, "applied": applied})
+
+
+def _write(repo_dir: Path, op: str, payload: dict) -> str:
+ """Run one write command and shape the result (created id + diagnostics), or an
+ error object the agent can read."""
+ try:
+ result = apply_command(repo_dir, op, {k: v for k, v in payload.items() if v is not None})
+ except StaleModelError as e:
+ return _json({"error": f"model changed on disk: {e}"})
+ except (CommandError, FileNotFoundError, ValueError) as e:
+ return _json({"error": str(e)})
+ return _json(
+ {
+ "ok": result.ok,
+ "created_id": result.created_id,
+ # CommandResult.diagnostics is already a list of {code, severity, message}
+ # dicts (validated on the reload apply_command does after each write).
+ "diagnostics": result.diagnostics,
+ "error": result.error,
+ }
+ )
+
+
+@lru_cache(maxsize=8)
+def _registry(repo_dir: Path):
+ """Build + load the ontology registry once per repo dir. Returns None when the
+ model has no ontology stack configured (search then returns an empty result)."""
+ try:
+ from mdl_ontology import build_registry
+
+ model = ModelRepo.load(repo_dir).model
+ stack = getattr(model.config, "ontology_stack", None)
+ if not stack:
+ return None
+ reg = build_registry(repo_dir, stack)
+ reg.load()
+ return reg
+ except Exception: # noqa: BLE001 - a bad ontology config must not crash the server
+ return None
+
+
+def _json(obj) -> str:
+ return json.dumps(obj, indent=2, default=str)
+
+
+def run(repo_dir: Path) -> None:
+ """Start the server on stdio (the transport VS Code / Claude Desktop use)."""
+ build_server(repo_dir).run()
diff --git a/packages/mcp/tests/conftest.py b/packages/mcp/tests/conftest.py
new file mode 100644
index 0000000..08dec24
--- /dev/null
+++ b/packages/mcp/tests/conftest.py
@@ -0,0 +1,15 @@
+import sys
+from pathlib import Path
+
+import pytest
+
+_CORE_TESTS = Path(__file__).resolve().parents[2] / "core" / "tests"
+sys.path.insert(0, str(_CORE_TESTS))
+
+from model_builders import write_model # noqa: E402
+
+
+@pytest.fixture
+def model_dir(tmp_path):
+ write_model(tmp_path)
+ return tmp_path
diff --git a/packages/mcp/tests/test_query.py b/packages/mcp/tests/test_query.py
new file mode 100644
index 0000000..aff123b
--- /dev/null
+++ b/packages/mcp/tests/test_query.py
@@ -0,0 +1,86 @@
+"""The shared query layer (mdl_core.query) that both AI frontends read from."""
+
+from __future__ import annotations
+
+from mdl_core.query import entities_detail, get_entity, get_model_context, list_entities
+from mdl_core.repo import ModelRepo
+
+
+def _model(model_dir):
+ return ModelRepo.load(model_dir).model
+
+
+def test_list_entities_names_and_counts(model_dir):
+ ents = list_entities(_model(model_dir))
+ by_name = {e["name"]: e for e in ents}
+ assert {"counterparty", "trade"} <= set(by_name)
+ # counterparty has two attributes in the builder
+ assert by_name["counterparty"]["attribute_count"] == 2
+ assert by_name["counterparty"]["subject_area"] == "Trading"
+
+
+def test_list_entities_scoped_to_subject_area(model_dir):
+ # Both entities are homed in Trading; an unknown area scopes to nothing.
+ assert list_entities(_model(model_dir), subject_area="Trading")
+ assert list_entities(_model(model_dir), subject_area="no-such-area") == []
+
+
+def test_get_entity_by_name_carries_relationships(model_dir):
+ e = get_entity(_model(model_dir), "counterparty")
+ assert e is not None
+ assert e["name"] == "counterparty"
+ assert e["conceptual"]["name"] == "Counterparty"
+ # the trade_has_counterparty relationship touches counterparty
+ assert any(r["name"] == "trade_has_counterparty" for r in e["relationships"])
+ assert {a["name"] for a in e["attributes"]} == {"counterparty_id", "legal_name"}
+
+
+def test_get_entity_unknown_returns_none(model_dir):
+ assert get_entity(_model(model_dir), "does_not_exist") is None
+
+
+def test_get_entity_accepts_a_ulid(model_dir):
+ model = _model(model_dir)
+ le = next(e for e in model.logical_entities.values() if e.name == "trade")
+ assert get_entity(model, le.id)["name"] == "trade"
+
+
+def test_get_entity_infers_pk_from_business_key(model_dir):
+ # The builder uses the legacy role:business_key convention (no KeyGroup objects).
+ # _keys_for should synthesise an inferred pk from it, so normalization questions
+ # can reason about the key even without a declared KeyGroup.
+ e = get_entity(_model(model_dir), "counterparty")
+ assert isinstance(e["keys"], list)
+ pk = next((k for k in e["keys"] if k["type"] == "pk"), None)
+ assert pk is not None
+ assert pk["inferred"] is True
+ assert pk["columns"] == ["counterparty_id"]
+
+
+def test_entities_detail_is_compact_and_complete(model_dir):
+ d = entities_detail(_model(model_dir))
+ assert d["total"] == 2
+ assert d["truncated"] is False
+ names = {e["name"] for e in d["entities"]}
+ assert {"counterparty", "trade"} == names
+ cp = next(e for e in d["entities"] if e["name"] == "counterparty")
+ # attributes are compact "name:domain[?]" strings
+ assert any(a.startswith("counterparty_id:") for a in cp["attributes"])
+ assert isinstance(cp["keys"], list)
+
+
+def test_entities_detail_truncates_at_limit(model_dir):
+ d = entities_detail(_model(model_dir), limit=1)
+ assert d["total"] == 2
+ assert d["shown"] == 1
+ assert d["truncated"] is True
+ assert len(d["entities"]) == 1
+
+
+def test_get_model_context_summary(model_dir):
+ ctx = get_model_context(_model(model_dir))
+ assert ctx["project"] == "testmodel"
+ assert ctx["counts"]["entities"] == 2
+ assert ctx["counts"]["relationships"] == 1
+ assert any(a["name"] == "Trading" for a in ctx["subject_areas"])
+ assert any(r["name"] == "trade_has_counterparty" for r in ctx["relationships"])
diff --git a/packages/mcp/tests/test_server.py b/packages/mcp/tests/test_server.py
new file mode 100644
index 0000000..aeb2557
--- /dev/null
+++ b/packages/mcp/tests/test_server.py
@@ -0,0 +1,96 @@
+"""The MCP server tools: reads over the query layer, writes through apply_command.
+
+The FastMCP tool-call API is async; we drive it with a small asyncio.run helper so
+the suite needs no async-test plugin (the project has none, and adding one is a
+separate decision)."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+
+from mdl_mcp.server import build_server
+
+
+def _call(srv, name, args):
+ """Invoke a FastMCP tool synchronously and return its parsed JSON result."""
+
+ async def _run():
+ res = await srv.call_tool(name, args)
+ content = res[0] if isinstance(res, tuple) else res
+ return content[0].text if content and hasattr(content[0], "text") else str(content)
+
+ return json.loads(asyncio.run(_run()))
+
+
+def _tool_names(srv):
+ return {t.name for t in asyncio.run(srv.list_tools())}
+
+
+def test_tools_are_registered(model_dir):
+ names = _tool_names(build_server(model_dir))
+ assert {
+ "list_entities",
+ "get_entity",
+ "get_model_context",
+ "search_ontology",
+ "validate",
+ "create_entity",
+ "update_entity",
+ } <= names
+
+
+def test_get_entity_tool_reads(model_dir):
+ srv = build_server(model_dir)
+ e = _call(srv, "get_entity", {"name": "counterparty"})
+ assert e["conceptual"]["name"] == "Counterparty"
+
+
+def test_get_entity_tool_unknown_is_error_object(model_dir):
+ srv = build_server(model_dir)
+ e = _call(srv, "get_entity", {"name": "nope"})
+ assert "error" in e
+
+
+def test_create_entity_writes_to_disk(model_dir):
+ srv = build_server(model_dir)
+ r = _call(srv, "create_entity", {"name": "region", "definition": "A geographic region."})
+ assert r["ok"] is True
+ assert r["created_id"]
+ # visible to the very next read (the server reloads per call)
+ e = _call(srv, "get_entity", {"name": "region"})
+ assert e["name"] == "region"
+ assert (model_dir / "logical" / "entities" / "region.yaml").exists()
+
+
+def test_update_entity_add_attribute_and_definition(model_dir):
+ srv = build_server(model_dir)
+ _call(srv, "create_entity", {"name": "region"})
+ r = _call(
+ srv,
+ "update_entity",
+ {
+ "name": "region",
+ "changes": {
+ "definition": "A named geographic area.",
+ "add_attribute": {"name": "region_code", "domain": "string", "nullable": False},
+ },
+ },
+ )
+ assert r["ok"] is True
+ assert "definition" in r["applied"]
+ e = _call(srv, "get_entity", {"name": "region"})
+ assert e["definition"] == "A named geographic area."
+ assert any(a["name"] == "region_code" for a in e["attributes"])
+
+
+def test_update_entity_unknown_is_error(model_dir):
+ srv = build_server(model_dir)
+ r = _call(srv, "update_entity", {"name": "ghost", "changes": {"definition": "x"}})
+ assert "error" in r
+
+
+def test_validate_tool_reports_ok(model_dir):
+ srv = build_server(model_dir)
+ r = _call(srv, "validate", {})
+ assert r["ok"] is True
diff --git a/pyproject.toml b/pyproject.toml
index e3370ff..8c2b13f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,6 +27,7 @@ dependencies = [
"python-multipart>=0.0.18",
"pygls>=2.0",
"sqlglot>=25",
+ "mcp>=1.2,<2",
]
# Optional OpenTelemetry export for the audit log (spec §20). `pip install
@@ -70,6 +71,7 @@ dev-mode-dirs = [
"packages/catalog/src",
"packages/server/src",
"packages/lsp/src",
+ "packages/mcp/src",
"packages/adapters/collibra/src",
]
diff --git a/uv.lock b/uv.lock
index e03d5a3..634d690 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,8 @@ version = 1
revision = 1
requires-python = ">=3.11"
resolution-markers = [
- "python_full_version >= '3.12'",
+ "python_full_version >= '3.14'",
+ "python_full_version >= '3.12' and python_full_version < '3.14'",
"python_full_version < '3.12'",
]
@@ -21,6 +22,7 @@ members = [
"modelith-emit-semantic",
"modelith-governance",
"modelith-lsp",
+ "modelith-mcp",
"modelith-ontology",
"modelith-reverse",
"modelith-server",
@@ -88,6 +90,104 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 },
]
+[[package]]
+name = "cffi"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838 },
+ { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168 },
+ { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805 },
+ { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716 },
+ { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569 },
+ { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907 },
+ { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807 },
+ { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252 },
+ { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214 },
+ { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408 },
+ { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470 },
+ { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096 },
+ { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941 },
+ { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821 },
+ { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719 },
+ { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799 },
+ { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389 },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249 },
+ { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775 },
+ { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822 },
+ { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232 },
+ { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597 },
+ { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292 },
+ { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919 },
+ { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093 },
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248 },
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908 },
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805 },
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764 },
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722 },
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369 },
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175 },
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670 },
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824 },
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148 },
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564 },
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263 },
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688 },
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078 },
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064 },
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720 },
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964 },
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962 },
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328 },
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985 },
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530 },
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525 },
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053 },
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213 },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682 },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949 },
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947 },
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504 },
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259 },
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864 },
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538 },
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688 },
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803 },
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763 },
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688 },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868 },
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104 },
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402 },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043 },
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737 },
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933 },
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002 },
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271 },
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919 },
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529 },
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630 },
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134 },
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197 },
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683 },
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897 },
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935 },
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464 },
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262 },
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779 },
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520 },
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673 },
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835 },
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705 },
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539 },
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707 },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772 },
+ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360 },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.5.1"
@@ -345,6 +445,62 @@ toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
+[[package]]
+name = "cryptography"
+version = "50.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153 },
+ { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133 },
+ { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478 },
+ { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726 },
+ { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524 },
+ { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720 },
+ { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866 },
+ { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028 },
+ { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405 },
+ { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596 },
+ { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082 },
+ { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826 },
+ { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525 },
+ { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817 },
+ { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300 },
+ { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039 },
+ { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388 },
+ { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293 },
+ { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031 },
+ { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344 },
+ { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201 },
+ { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023 },
+ { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362 },
+ { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247 },
+ { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806 },
+ { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307 },
+ { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900 },
+ { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357 },
+ { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474 },
+ { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862 },
+ { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942 },
+ { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347 },
+ { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050 },
+ { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955 },
+ { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694 },
+ { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413 },
+ { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355 },
+ { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429 },
+ { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350 },
+ { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675 },
+ { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410 },
+ { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378 },
+ { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889 },
+ { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006 },
+]
+
[[package]]
name = "dbt-artifacts-parser"
version = "0.14.0"
@@ -431,6 +587,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 },
+]
+
[[package]]
name = "hypothesis"
version = "6.163.0"
@@ -538,6 +703,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
+]
+
[[package]]
name = "lsprotocol"
version = "2025.0.0"
@@ -637,6 +829,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
]
+[[package]]
+name = "mcp"
+version = "1.30.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581 },
+]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -683,6 +900,7 @@ dependencies = [
{ name = "modelith-emit-semantic" },
{ name = "modelith-governance" },
{ name = "modelith-lsp" },
+ { name = "modelith-mcp" },
{ name = "modelith-ontology" },
{ name = "modelith-reverse" },
{ name = "modelith-server" },
@@ -701,6 +919,7 @@ requires-dist = [
{ name = "modelith-emit-semantic", editable = "packages/emit-semantic" },
{ name = "modelith-governance", editable = "packages/governance" },
{ name = "modelith-lsp", editable = "packages/lsp" },
+ { name = "modelith-mcp", editable = "packages/mcp" },
{ name = "modelith-ontology", editable = "packages/ontology" },
{ name = "modelith-reverse", editable = "packages/reverse" },
{ name = "modelith-server", editable = "packages/server" },
@@ -730,6 +949,7 @@ dependencies = [
{ name = "dbt-artifacts-parser" },
{ name = "fastapi" },
{ name = "jinja2" },
+ { name = "mcp" },
{ name = "pydantic" },
{ name = "pygls" },
{ name = "pyshacl" },
@@ -761,6 +981,7 @@ requires-dist = [
{ name = "dbt-artifacts-parser", specifier = ">=0.5" },
{ name = "fastapi", specifier = ">=0.110" },
{ name = "jinja2", specifier = ">=3.1" },
+ { name = "mcp", specifier = ">=1.2,<2" },
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'audit'", specifier = ">=1.20" },
{ name = "opentelemetry-sdk", marker = "extra == 'audit'", specifier = ">=1.20" },
{ name = "pydantic", specifier = ">=2.6" },
@@ -896,6 +1117,23 @@ requires-dist = [
{ name = "pygls", specifier = ">=2.0" },
]
+[[package]]
+name = "modelith-mcp"
+version = "0.1.0"
+source = { editable = "packages/mcp" }
+dependencies = [
+ { name = "mcp" },
+ { name = "modelith-core" },
+ { name = "modelith-ontology" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "mcp", specifier = ">=1.2,<2" },
+ { name = "modelith-core", editable = "packages/core" },
+ { name = "modelith-ontology", editable = "packages/ontology" },
+]
+
[[package]]
name = "modelith-ontology"
version = "0.1.0"
@@ -1106,6 +1344,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/ca/c47f91d3cab175b01fd8c4f0d80fdf8613be876cc616e66ad281a59c5ddf/protobuf-7.36.1-py3-none-any.whl", hash = "sha256:7d951e46b3f963d6c264c367c437921de9d5aedd9c3f9612b9077736b4e3ad5c", size = 179813 },
]
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 },
+]
+
[[package]]
name = "pydantic"
version = "2.13.4"
@@ -1223,6 +1470,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071 },
]
+[[package]]
+name = "pydantic-settings"
+version = "2.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413 },
+]
+
[[package]]
name = "pygls"
version = "2.1.1"
@@ -1246,6 +1507,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 },
]
+[[package]]
+name = "pyjwt"
+version = "2.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896 },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
[[package]]
name = "pyparsing"
version = "3.3.2"
@@ -1301,6 +1576,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 },
]
+[[package]]
+name = "python-dotenv"
+version = "1.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780 },
+]
+
[[package]]
name = "python-multipart"
version = "0.0.32"
@@ -1310,6 +1594,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042 },
]
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659 },
+ { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825 },
+ { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875 },
+ { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877 },
+ { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841 },
+ { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901 },
+ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184 },
+ { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298 },
+ { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640 },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928 },
+ { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157 },
+ { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598 },
+ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159 },
+ { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293 },
+ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337 },
+]
+
[[package]]
name = "rdflib"
version = "7.6.0"
@@ -1327,6 +1633,20 @@ html = [
{ name = "html5rdf" },
]
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
+]
+
[[package]]
name = "requests"
version = "2.34.2"
@@ -1355,6 +1675,129 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 },
]
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174 },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513 },
+ { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783 },
+ { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316 },
+ { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423 },
+ { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077 },
+ { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315 },
+ { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502 },
+ { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673 },
+ { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964 },
+ { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446 },
+ { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975 },
+ { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453 },
+ { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219 },
+ { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137 },
+ { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691 },
+ { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542 },
+ { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180 },
+ { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067 },
+ { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509 },
+ { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754 },
+ { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189 },
+ { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750 },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576 },
+ { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807 },
+ { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187 },
+ { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030 },
+ { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185 },
+ { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394 },
+ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753 },
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012 },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203 },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984 },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815 },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545 },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828 },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678 },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811 },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382 },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832 },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011 },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431 },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710 },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454 },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063 },
+ { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510 },
+ { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495 },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454 },
+ { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583 },
+ { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919 },
+ { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725 },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255 },
+ { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060 },
+ { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960 },
+ { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356 },
+ { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319 },
+ { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508 },
+ { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504 },
+ { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380 },
+ { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976 },
+ { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840 },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282 },
+ { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403 },
+ { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055 },
+ { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419 },
+ { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848 },
+ { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369 },
+ { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673 },
+ { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500 },
+ { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978 },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350 },
+ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486 },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068 },
+ { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600 },
+ { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726 },
+ { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587 },
+ { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585 },
+ { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479 },
+ { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418 },
+ { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123 },
+ { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351 },
+ { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827 },
+ { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966 },
+ { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680 },
+ { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853 },
+ { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715 },
+ { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864 },
+ { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430 },
+ { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877 },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933 },
+ { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274 },
+ { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763 },
+ { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467 },
+ { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689 },
+ { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340 },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179 },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993 },
+ { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909 },
+ { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584 },
+ { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357 },
+ { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533 },
+ { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204 },
+ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719 },
+ { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791 },
+ { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842 },
+ { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094 },
+ { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662 },
+ { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896 },
+ { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545 },
+ { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059 },
+ { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235 },
+ { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008 },
+ { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118 },
+ { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138 },
+ { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486 },
+]
+
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
@@ -1416,6 +1859,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/9f/3dd2ec8dd84a33e0092f1c815267bc055073f9e833b93e389c59c10f62cc/sqlglot-30.18.0-py3-none-any.whl", hash = "sha256:ee0f9a9f3e2193e763c326e52dfb377b96fdb4292f6305c3ff2d9a220e71c601", size = 748788 },
]
+[[package]]
+name = "sse-starlette"
+version = "3.4.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size = 34972 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size = 17122 },
+]
+
[[package]]
name = "starlette"
version = "1.3.1"
diff --git a/vscode/CHANGELOG.md b/vscode/CHANGELOG.md
index c45af60..6de9037 100644
--- a/vscode/CHANGELOG.md
+++ b/vscode/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## Unreleased
+
+- **AI assistance.** Two Copilot Chat integrations backed by the same `mdl` engine:
+ a bundled MCP server (`mdl mcp`) exposing model + ontology tools in Agent mode
+ (`list_entities`, `get_entity`, `search_ontology`, `get_model_context`, `validate`,
+ `create_entity`, `update_entity`), and an `@modelith` chat participant for Ask mode
+ (`/list`, `/explain `). Registered automatically; no `mcp.json` to write.
+- **Import.** *Modelith: Import to Model* — right-click a `.sql`/`.mmd` file (or the
+ command palette) to open the Import wizard on the embedded canvas.
+
## 0.1.0
First release.
diff --git a/vscode/LICENSE b/vscode/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/vscode/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vscode/README.md b/vscode/README.md
index 7fcbde2..06fbb56 100644
--- a/vscode/README.md
+++ b/vscode/README.md
@@ -43,6 +43,27 @@ embeds the live canvas, so whatever the CLI understands, the editor shows.
locations. A standard `uv tool install modelith-dbt` needs no configuration. If nothing
matches, run `which mdl` in the integrated terminal and set that as `modelith.mdlPath`.
+## AI assistance (Copilot Chat)
+
+Modelith registers two AI integrations, one per Copilot Chat mode. Both use the same
+`mdl` engine already detected for everything else — no `mcp.json` to write, no separate
+install.
+
+- **Agent mode — Modelith MCP tools.** In Copilot Chat's *Agent* mode the model is available
+ as tools: `list_entities`, `get_entity`, `search_ontology`, `get_model_context`, `validate`,
+ and the write tools `create_entity` / `update_entity`. The agent can ground itself in what
+ already exists, search the ontology for an alignment, and write validated entities straight
+ into your checkout (direct-write — you review the git diff, the same trust boundary as the
+ CLI). The tools **only run in Agent mode**: tool invocation needs the plan/act/observe loop
+ that Ask mode does not have. The server is the `mdl mcp` subcommand, registered automatically
+ on activation and scoped to your workspace model. Requires VS Code 1.99+ (older hosts keep
+ every other feature; only the tools are absent).
+- **Ask mode — `@modelith`.** In *Ask* mode, where MCP tools cannot run, type `@modelith` and
+ ask about the model: name an entity for its attributes / ontology alignment / relationships,
+ `@modelith /list` for the entity list, `@modelith /explain ` for one in full, or a
+ general question for a model summary. It reads the model through the same query layer the MCP
+ tools use, so the two modes answer consistently.
+
## Devcontainers
The extension declares `"extensionKind": ["workspace"]`, so in a devcontainer it runs inside
diff --git a/vscode/package.json b/vscode/package.json
index 3ca6ab9..1064b9f 100644
--- a/vscode/package.json
+++ b/vscode/package.json
@@ -30,7 +30,8 @@
"theme": "dark"
},
"engines": {
- "vscode": "^1.85.0"
+ "vscode": "^1.85.0",
+ "node": ">=18"
},
"categories": [
"Data Science",
@@ -42,9 +43,34 @@
"workspace"
],
"activationEvents": [
- "workspaceContains:**/mdl-project.yaml"
+ "workspaceContains:**/mdl-project.yaml",
+ "onChatParticipant:modelith.chat"
],
"contributes": {
+ "mcpServerDefinitionProviders": [
+ {
+ "id": "modelith.mcp",
+ "label": "Modelith"
+ }
+ ],
+ "chatParticipants": [
+ {
+ "id": "modelith.chat",
+ "name": "modelith",
+ "fullName": "Modelith",
+ "description": "Ask about entities, relationships, and ontology alignment in this model.",
+ "commands": [
+ {
+ "name": "explain",
+ "description": "Explain one entity: its attributes, ontology alignment, and relationships."
+ },
+ {
+ "name": "list",
+ "description": "List the model's entities."
+ }
+ ]
+ }
+ ],
"commands": [
{
"command": "modelith.validate",
@@ -191,12 +217,13 @@
}
},
"scripts": {
+ "check-node": "node -e \"if(+process.versions.node.split('.')[0]<18){console.error('\\n Node '+process.versions.node+' is too old for the build toolchain (vsce/esbuild need Node 18+).\\n Use Node 20: nvm use 20 (or: export NVM_DIR=$HOME/.nvm; . $NVM_DIR/nvm.sh; nvm use 20)\\n');process.exit(1)}\"",
"vscode:prepublish": "npm run build -- --minify",
- "build": "tsc --noEmit && node esbuild.mjs",
+ "build": "npm run check-node && tsc --noEmit && node esbuild.mjs",
"watch": "node esbuild.mjs --watch",
"typecheck": "tsc --noEmit",
- "package": "npx @vscode/vsce package --no-dependencies",
- "publish": "npx @vscode/vsce publish --no-dependencies"
+ "package": "npm run check-node && npx @vscode/vsce package --no-dependencies",
+ "publish": "npm run check-node && npx @vscode/vsce publish --no-dependencies"
},
"devDependencies": {
"@types/node": "^20.11.0",
diff --git a/vscode/src/chatParticipant.ts b/vscode/src/chatParticipant.ts
new file mode 100644
index 0000000..4639b91
--- /dev/null
+++ b/vscode/src/chatParticipant.ts
@@ -0,0 +1,326 @@
+import * as vscode from "vscode";
+import { findMdl, findModelDir, runMdl } from "./mdl";
+
+/** The `@modelith` chat participant, for Copilot Chat's ASK mode — where MCP tools
+ * structurally cannot run (no agentic loop to invoke them from). It answers about
+ * the model itself: entities, relationships, ontology alignment.
+ *
+ * Intent handling is LLM-backed. The chat-participant API hands us `request.model`
+ * — the user's own already-authenticated Copilot model — so for free-text asks we
+ * GROUND with the real model facts (fetched through the same `mdl model` query
+ * layer the MCP server uses) and let that model compose the answer, pinned to the
+ * facts so it can't invent entities. Exact `/list` and `/explain ` keep a
+ * fast deterministic template; anything else — a question, a typo'd name, a
+ * multi-part ask — goes through the model. If no model is available (rare in
+ * Copilot Chat), a heuristic name match is the fallback, so the participant still
+ * works. One query layer in `packages/core`, two frontends, no drift. */
+
+interface ModelContext {
+ project: string;
+ counts: { entities: number; relationships: number; subject_areas: number; terms: number };
+ subject_areas: { name: string; definition: string | null; entities: string[] }[];
+ unassigned_entities: string[];
+ relationships: { name: string; from: string; to: string }[];
+}
+
+interface EntityRow {
+ name: string;
+ definition: string | null;
+ attribute_count: number;
+ subject_area: string | null;
+}
+
+interface EntityDetail {
+ name: string;
+ definition: string | null;
+ pattern: string | null;
+ conceptual: { name: string; ontology: string | null; ontology_layer: string | null } | null;
+ keys: { name: string; type: string; columns: string[] }[];
+ attributes: { name: string; domain: string | null; nullable: boolean; ontology: string | null }[];
+ relationships: { name: string; from: string; to: string }[];
+}
+
+interface EntitiesDetail {
+ project: string;
+ total: number;
+ shown: number;
+ truncated: boolean;
+ entities: {
+ name: string;
+ attributes: string[];
+ keys: { type: string; columns: string[] }[];
+ relationships: string[];
+ ontology: string | null;
+ }[];
+}
+
+/** A model-WIDE question needs every entity's attributes + keys, not just named
+ * ones — normalization ("BCNF", "3NF", "normalis"), coverage ("which entities",
+ * "any entity", "all"/"every"/"each"), or simply a question that names no entity at
+ * all. Everything else is targeted and grounds on the mentioned entities. */
+function isModelWide(prompt: string, mentioned: string[]): boolean {
+ const p = prompt.toLowerCase();
+ if (/\b(bcnf|[1-6]nf|normal(is|iz)|denormal)/.test(p)) return true;
+ if (/\b(which|any|all|every|each|no)\b.*\b(entit|model|table|key|attribute)/.test(p)) return true;
+ return mentioned.length === 0;
+}
+
+function escapeRegExp(s: string): string {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/** Run an `mdl model …` read and parse its JSON stdout. Throws with the CLI's own
+ * message on a non-zero exit so the handler can surface it. */
+async function readModel(dir: string, args: string[]): Promise {
+ const bin = await findMdl(dir);
+ const r = await runMdl(bin, [...args, "-m", dir], dir);
+ if (r.code !== 0) {
+ try {
+ const obj = JSON.parse(r.stdout);
+ if (obj?.error) throw new Error(obj.error);
+ } catch {
+ /* not a JSON error object — fall through */
+ }
+ // A too-old `mdl` (a stale global install predating these commands) doesn't
+ // know `mdl model …` and prints Typer's usage dump. Turn that into an
+ // actionable message instead of leaking the raw usage text to chat.
+ const blob = (r.stdout + r.stderr).toLowerCase();
+ if (blob.includes("no such command") || blob.includes("usage: mdl")) {
+ throw new Error(
+ "Your installed `mdl` is out of date — it doesn't have the `model` command " +
+ "these features need. Upgrade it (`uv tool install --force modelith-dbt`, or " +
+ "reinstall from your working tree), then reload VS Code.",
+ );
+ }
+ throw new Error(r.stderr.trim() || r.stdout.trim() || `mdl ${args.join(" ")} failed`);
+ }
+ return JSON.parse(r.stdout) as T;
+}
+
+export function registerChatParticipant(ctx: vscode.ExtensionContext): void {
+ // The chat API is stable from VS Code 1.90+, but guard so activation never
+ // crashes on an older host that lacks it (as the MCP guard does for 1.99+).
+ if (!vscode.chat?.createChatParticipant) return;
+
+ const handler: vscode.ChatRequestHandler = async (request, _context, stream, token) => {
+ const dir = await findModelDir();
+ if (!dir) {
+ stream.markdown(
+ "No Modelith model found in this workspace (looked for `mdl-project.yaml`). " +
+ "Set `modelith.modelDir` if it lives somewhere unusual.",
+ );
+ return;
+ }
+
+ try {
+ if (request.command === "list") {
+ await renderList(dir, stream);
+ return;
+ }
+
+ const prompt = request.prompt.trim();
+ const rows = await readModel(dir, ["model", "entities"]);
+ const names = rows.map((r) => r.name);
+
+ // Fast path: the prompt (after an optional /explain) IS an exact entity name.
+ // Render the deterministic card without an LLM round-trip.
+ const exact = names.find((n) => n.toLowerCase() === prompt.toLowerCase());
+ if (exact) {
+ await renderEntity(dir, exact, stream);
+ return;
+ }
+
+ // Everything else — a question, a name buried in a sentence, a typo — is
+ // resolved by the user's Copilot model, grounded in the real model facts.
+ if (request.model) {
+ await answerWithModel(dir, request, rows, stream, token);
+ return;
+ }
+
+ // No language model available: fall back to a heuristic name match, then a
+ // structural summary, so the participant still does something useful.
+ const guessed = guessEntity(prompt, names);
+ if (guessed) {
+ await renderEntity(dir, guessed, stream);
+ return;
+ }
+ await renderSummary(dir, stream);
+ } catch (e) {
+ stream.markdown(`\n\n⚠️ ${e instanceof Error ? e.message : String(e)}`);
+ }
+ };
+
+ const participant = vscode.chat.createChatParticipant("modelith.chat", handler);
+ participant.iconPath = undefined;
+ ctx.subscriptions.push(participant);
+}
+
+/** Ground the user's Copilot model with the model's facts and stream its answer.
+ *
+ * We assemble a compact fact sheet — the entity roster, the condensed model
+ * context, and (when the question clearly concerns specific entities) their full
+ * detail — and instruct the model to answer ONLY from it. Deterministic data,
+ * natural-language reasoning: it handles "what connects to instrument?", "which
+ * entities have no ontology alignment?", and a fuzzy "explain the instrument
+ * entity, everything about it" alike. */
+async function answerWithModel(
+ dir: string,
+ request: vscode.ChatRequest,
+ rows: EntityRow[],
+ stream: vscode.ChatResponseStream,
+ token: vscode.CancellationToken,
+): Promise {
+ const names = rows.map((r) => r.name);
+ const context = await readModel(dir, ["model", "context"]);
+
+ // Two grounding strategies, chosen by scope. A model-wide question (normalization,
+ // "which entities…", "all/every…") needs every entity's attributes + keys; a
+ // targeted one needs only the named entities in full. We never dump the whole
+ // model at full fidelity — `mdl model detail` is compact and capped, so a 150-
+ // entity model stays inside the context window (with a truncation notice).
+ const mentioned = names.filter((n) =>
+ new RegExp(`\\b${escapeRegExp(n)}\\b`, "i").test(request.prompt),
+ );
+ let facts: string;
+ let truncationNote = "";
+ if (isModelWide(request.prompt, mentioned)) {
+ const detail = await readModel(dir, ["model", "detail", "--limit", "120"]);
+ if (detail.truncated) {
+ truncationNote =
+ `\n\n_(This model has ${detail.total} entities; I grounded on the first ` +
+ `${detail.shown}. For a complete answer, ask about a subject area or specific ` +
+ "entities.)_";
+ }
+ facts = JSON.stringify({ context, entities_detail: detail }, null, 2);
+ } else {
+ const details: EntityDetail[] = [];
+ for (const n of mentioned.slice(0, 6)) {
+ try {
+ details.push(await readModel(dir, ["model", "entity", n]));
+ } catch {
+ /* skip an entity that vanished between the list and the read */
+ }
+ }
+ facts = JSON.stringify({ entities: names, context, detail: details }, null, 2);
+ }
+
+ const system =
+ "You are Modelith, a data-modeling assistant answering about ONE specific model " +
+ "inside VS Code. Answer the user's question using ONLY the JSON facts provided — " +
+ "the model's entities, subject areas, relationships, attributes and keys. Attribute " +
+ "strings are `name:domain` with a trailing `?` for nullable. Keys list pk / unique / " +
+ "alternate key groups; a pk marked inferred came from the legacy business_key " +
+ "convention but is still the entity's primary key. For normalization questions: you " +
+ "can reason from candidate keys (e.g. an entity whose only key is a single attribute, " +
+ "with all other attributes dependent on it, is trivially in BCNF for key-determined " +
+ "dependencies), but the model does NOT capture arbitrary functional dependencies — so " +
+ "say plainly that a full BCNF/3NF proof needs FDs the model doesn't record, and give " +
+ "the key-based assessment you can. Do not invent entities, attributes, keys, or " +
+ "alignments that are not in the facts. Be concise, use Markdown, and prefer the " +
+ "model's own names verbatim. This is read-only: for editing, point the user to " +
+ "Copilot Agent mode where the Modelith tools run.";
+
+ const messages = [
+ vscode.LanguageModelChatMessage.User(`${system}\n\nMODEL FACTS:\n${facts}`),
+ vscode.LanguageModelChatMessage.User(request.prompt),
+ ];
+
+ try {
+ const res = await request.model.sendRequest(messages, {}, token);
+ for await (const chunk of res.text) stream.markdown(chunk);
+ if (truncationNote) stream.markdown(truncationNote);
+ } catch (e) {
+ // The model call can fail (consent not granted, quota, offline). Fall back to
+ // the heuristic so the user still gets an answer, not a dead end.
+ const guessed = guessEntity(request.prompt, names);
+ if (guessed) {
+ await renderEntity(dir, guessed, stream);
+ return;
+ }
+ await renderSummary(dir, stream);
+ stream.markdown(
+ `\n\n_(Couldn't reach a language model — ${e instanceof Error ? e.message : String(e)} — ` +
+ "so I answered from the model's structure directly.)_",
+ );
+ }
+}
+
+/** Heuristic entity pick for the no-LLM fallback: the longest entity name that
+ * appears as a word-ish substring of the prompt. Deliberately simple. */
+function guessEntity(prompt: string, names: string[]): string | undefined {
+ const p = prompt.toLowerCase();
+ return names
+ .filter((n) => p.includes(n.toLowerCase()))
+ .sort((a, b) => b.length - a.length)[0];
+}
+
+async function renderEntity(
+ dir: string,
+ name: string,
+ stream: vscode.ChatResponseStream,
+): Promise {
+ const e = await readModel(dir, ["model", "entity", name]);
+ stream.markdown(`### ${e.name}\n`);
+ if (e.definition) stream.markdown(`${e.definition}\n\n`);
+ if (e.conceptual?.ontology) {
+ stream.markdown(`**Aligned to** \`${e.conceptual.ontology}\``);
+ if (e.conceptual.ontology_layer) stream.markdown(` _(${e.conceptual.ontology_layer} layer)_`);
+ stream.markdown("\n\n");
+ }
+ if (e.keys?.length) {
+ stream.markdown("**Keys**\n");
+ for (const k of e.keys) {
+ stream.markdown(`- ${k.type}: ${k.columns.join(", ") || "—"} (${k.name})\n`);
+ }
+ stream.markdown("\n");
+ }
+ if (e.attributes.length) {
+ stream.markdown("**Attributes**\n");
+ for (const a of e.attributes) {
+ const bits = [a.domain ?? "—", a.nullable ? "nullable" : "required"];
+ if (a.ontology) bits.push(`→ \`${a.ontology}\``);
+ stream.markdown(`- \`${a.name}\` (${bits.join(", ")})\n`);
+ }
+ stream.markdown("\n");
+ }
+ if (e.relationships.length) {
+ stream.markdown("**Relationships**\n");
+ for (const r of e.relationships) stream.markdown(`- ${r.from} → ${r.to} (${r.name})\n`);
+ }
+}
+
+async function renderList(dir: string, stream: vscode.ChatResponseStream): Promise {
+ const rows = await readModel(dir, ["model", "entities"]);
+ if (!rows.length) {
+ stream.markdown("The model has no entities yet.");
+ return;
+ }
+ stream.markdown(`The model has **${rows.length}** entities:\n\n`);
+ for (const r of rows) {
+ const area = r.subject_area ? ` _(${r.subject_area})_` : "";
+ stream.markdown(`- **${r.name}**${area} — ${r.attribute_count} attribute(s)\n`);
+ }
+}
+
+async function renderSummary(dir: string, stream: vscode.ChatResponseStream): Promise {
+ const ctx = await readModel(dir, ["model", "context"]);
+ stream.markdown(
+ `**${ctx.project}** — ${ctx.counts.entities} entities, ` +
+ `${ctx.counts.relationships} relationships, ${ctx.counts.subject_areas} subject area(s).\n\n`,
+ );
+ if (ctx.subject_areas.length) {
+ stream.markdown("**Subject areas**\n");
+ for (const sa of ctx.subject_areas) {
+ stream.markdown(`- **${sa.name}**: ${sa.entities.join(", ") || "(empty)"}\n`);
+ }
+ stream.markdown("\n");
+ }
+ if (ctx.unassigned_entities.length) {
+ stream.markdown(`_Not in any subject area:_ ${ctx.unassigned_entities.join(", ")}\n\n`);
+ }
+ stream.markdown(
+ "Ask about a specific entity by name, or use `@modelith /explain ` and " +
+ "`@modelith /list`. For editing, switch Copilot Chat to **Agent mode** — the " +
+ "Modelith tools (create/update entities, ontology search) run there.",
+ );
+}
diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts
index 8150ddf..c447d91 100644
--- a/vscode/src/extension.ts
+++ b/vscode/src/extension.ts
@@ -2,14 +2,18 @@ import * as path from "node:path";
import * as vscode from "vscode";
import type { LanguageClient } from "vscode-languageclient/node";
import { CanvasManager } from "./canvasPanel";
+import { registerChatParticipant } from "./chatParticipant";
import { executeLspCommand, startLsp } from "./lspClient";
+import { registerMcpProvider } from "./mcpProvider";
import {
findDbtProjectDir,
findManifestPath,
findMdl,
findModelDir,
+ hasAiCommands,
resetMdlCache,
runMdl,
+ upgradeHint,
} from "./mdl";
import { registerSchemas } from "./schemas";
@@ -29,6 +33,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise {
canvas = new CanvasManager(out);
ctx.subscriptions.push({ dispose: () => canvas.dispose() });
+ // AI surfaces: the bundled MCP server (Copilot Chat agent mode) and the
+ // `@modelith` chat participant (ask mode). Both are internally guarded against
+ // hosts that lack their API, so a failure here never blocks the LSP or canvas.
+ registerMcpProvider(ctx);
+ registerChatParticipant(ctx);
+
// After a window reload VS Code restores our webview panels, but the `mdl serve`
// child died with the old extension host, so the restored iframe points at a
// dead port (blank). Re-hydrate the canvas (restart server + rewrite iframe);
@@ -99,6 +109,24 @@ export async function activate(ctx: vscode.ExtensionContext): Promise {
ctx.environmentVariableCollection.description =
"Adds the Modelith `mdl` CLI to integrated terminals";
}
+ // Version-skew handshake. The extension and the `mdl` CLI install separately,
+ // so a stale CLI can lack commands the AI surfaces (chat participant, MCP
+ // server) call. Probe once; if it's too old, tell the user how to upgrade
+ // THEIR install (inferred from the resolved path) — once, dismissibly, and
+ // without blocking anything. The per-call guards remain the backstop.
+ void hasAiCommands(bin, root).then((ok) => {
+ if (ok) return;
+ out.appendLine(`[mdl] ${bin.label} is missing the 'model'/'mcp' commands (out of date)`);
+ void vscode.window.showWarningMessage(
+ `Your Modelith CLI (${bin.label}) is out of date — it lacks commands the ` +
+ `chat and MCP features need. To fix, ${upgradeHint(bin)}, then reload VS Code.`,
+ "Reload Window",
+ ).then((pick) => {
+ if (pick === "Reload Window") {
+ void vscode.commands.executeCommand("workbench.action.reloadWindow");
+ }
+ });
+ });
} catch (e) {
out.appendLine(`[mdl] detection failed: ${e}`);
}
diff --git a/vscode/src/mcpProvider.ts b/vscode/src/mcpProvider.ts
new file mode 100644
index 0000000..df21747
--- /dev/null
+++ b/vscode/src/mcpProvider.ts
@@ -0,0 +1,54 @@
+import * as vscode from "vscode";
+import { findMdl, findModelDir } from "./mdl";
+
+/** Register the bundled Modelith MCP server for Copilot Chat AGENT mode.
+ *
+ * Unlike the spec's Node-module sketch, the server IS the Python `mdl mcp`
+ * subcommand — `packages/core` is Python, so a Node module cannot compile against
+ * it, and every other integration here (LSP, canvas) already runs by spawning the
+ * `mdl` CLI. We register an stdio server that runs `mdl mcp --repo `,
+ * resolving the binary through the same `findMdl` detection the LSP uses. One
+ * engine, invoked the one way this extension invokes it everywhere.
+ *
+ * A programmatically registered server has no implicit repo scope (a committed
+ * .vscode/mcp.json would), so we pass the workspace model dir explicitly.
+ *
+ * `registerMcpServerDefinitionProvider` only exists from VS Code 1.99+. An
+ * unguarded call has been seen crashing activation on older hosts and taking the
+ * LSP + canvas down with it — so optional-chain the API and no-op when absent. */
+export function registerMcpProvider(ctx: vscode.ExtensionContext): void {
+ const lm = vscode.lm as typeof vscode.lm & {
+ registerMcpServerDefinitionProvider?: (
+ id: string,
+ provider: {
+ provideMcpServerDefinitions: () => vscode.ProviderResult;
+ },
+ ) => vscode.Disposable;
+ };
+ const Stdio = (
+ vscode as unknown as { McpStdioServerDefinition?: new (...a: unknown[]) => unknown }
+ ).McpStdioServerDefinition;
+ if (!lm?.registerMcpServerDefinitionProvider || !Stdio) return;
+
+ ctx.subscriptions.push(
+ lm.registerMcpServerDefinitionProvider("modelith.mcp", {
+ provideMcpServerDefinitions: async () => {
+ const dir = (await findModelDir()) ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+ if (!dir) return [];
+ // Resolve the mdl invocation the same way the LSP does (explicit setting →
+ // .venv → PATH → well-known → `uv run mdl`). `bin.args` is the prefix (e.g.
+ // ["run","mdl"] for uv); the server command follows.
+ let cmd: string;
+ let args: string[];
+ try {
+ const bin = await findMdl(dir);
+ cmd = bin.cmd;
+ args = [...bin.args, "mcp", "--repo", dir];
+ } catch {
+ return []; // mdl not found — the LSP path already surfaces this to the user
+ }
+ return [new (Stdio as new (...a: unknown[]) => unknown)("modelith", cmd, args)];
+ },
+ }),
+ );
+}
diff --git a/vscode/src/mdl.ts b/vscode/src/mdl.ts
index fa0687f..dcb7280 100644
--- a/vscode/src/mdl.ts
+++ b/vscode/src/mdl.ts
@@ -24,6 +24,17 @@ export async function findMdl(root: string): Promise {
if (explicit) candidates.push({ cmd: explicit, args: [], label: explicit });
const venv = path.join(root, ".venv", "bin", "mdl");
if (fs.existsSync(venv)) candidates.push({ cmd: venv, args: [], label: ".venv/bin/mdl" });
+ // A `.venv/bin/mdl` in a workspace folder, ranked ABOVE the global PATH install.
+ // This is what lets someone developing Modelith itself use their working-tree
+ // (editable) mdl instead of a stale `uv tool` global — the model dir is usually a
+ // demo/ subfolder with no venv, so the model-dir probe above misses the repo-root
+ // editable install. For a normal user (no workspace .venv) this adds nothing.
+ for (const folder of vscode.workspace.workspaceFolders ?? []) {
+ const wsVenv = path.join(folder.uri.fsPath, ".venv", "bin", "mdl");
+ if (wsVenv !== venv && fs.existsSync(wsVenv)) {
+ candidates.push({ cmd: wsVenv, args: [], label: `${folder.name}/.venv/bin/mdl` });
+ }
+ }
candidates.push({ cmd: "mdl", args: [], label: "mdl (PATH)" });
// GUI-launched VS Code on macOS/Linux has a minimal PATH that excludes the
// per-user install locations a shell profile would add, so `mdl (PATH)` misses
@@ -92,6 +103,39 @@ export function runMdl(bin: MdlBin, args: string[], cwd: string): Promise {
+ const r = await runMdl(bin, ["model", "--help"], cwd);
+ return r.code === 0;
+}
+
// --- workspace discovery -------------------------------------------------------
/** The model repo dir (contains mdl-project.yaml). Setting wins; else first hit. */