chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:30 +08:00
commit 26382a7ac6
2353 changed files with 629146 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
"""lean-ctx Python SDK.
A thin, dependency-free client for the lean-ctx HTTP ``/v1`` contract. Mirrors
the TypeScript (`lean-ctx-client`) and Rust (`lean-ctx-client`) SDKs.
"""
from __future__ import annotations
from .client import LeanCtxClient
from .conformance import (
COVERED_ROUTES,
SUPPORTED_HTTP_CONTRACT_VERSIONS,
ConformanceCheck,
ConformanceScorecard,
run_conformance,
)
from .errors import (
LeanCtxConfigError,
LeanCtxError,
LeanCtxHTTPError,
LeanCtxTransportError,
)
from .tool_text import tool_result_to_text
__version__ = "0.1.0"
__all__ = [
"LeanCtxClient",
"LeanCtxError",
"LeanCtxConfigError",
"LeanCtxTransportError",
"LeanCtxHTTPError",
"tool_result_to_text",
"run_conformance",
"ConformanceCheck",
"ConformanceScorecard",
"COVERED_ROUTES",
"SUPPORTED_HTTP_CONTRACT_VERSIONS",
"__version__",
]
@@ -0,0 +1,32 @@
"""Framework adapters for lean-ctx tools (EPIC 12.6).
Thin, optional integrations that expose the lean-ctx tool surface to popular
agent frameworks. Each framework is an *optional* dependency, imported lazily —
installing the SDK pulls in none of them. The OpenAI adapter is a pure
transformation and needs no extra package.
from leanctx import LeanCtxClient
from leanctx.adapters import to_openai_tools, to_langchain_tools
client = LeanCtxClient("http://127.0.0.1:8080")
tools = to_openai_tools(client)
"""
from __future__ import annotations
from ._common import ToolSpec, coerce_arguments, normalized_tool_specs
from .crewai import to_crewai_tools
from .langchain import to_langchain_tools
from .llamaindex import to_llamaindex_tools
from .openai import run_openai_tool_call, to_openai_tools
__all__ = [
"ToolSpec",
"normalized_tool_specs",
"coerce_arguments",
"to_openai_tools",
"run_openai_tool_call",
"to_langchain_tools",
"to_llamaindex_tools",
"to_crewai_tools",
]
@@ -0,0 +1,83 @@
"""Shared helpers for framework adapters (EPIC 12.6).
All adapters normalize lean-ctx tools into a common [`ToolSpec`] and reuse the
same SDK call path (`call_tool_text`), so every framework integration behaves
identically and stays correct as the tool surface evolves.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any, Callable, Dict, List
from ..client import LeanCtxClient
@dataclass
class ToolSpec:
"""A framework-neutral description of one lean-ctx tool."""
name: str
description: str
parameters: Dict[str, Any] # JSON Schema for the arguments object
def _default_schema() -> Dict[str, Any]:
return {"type": "object", "properties": {}}
def normalized_tool_specs(client: LeanCtxClient, *, limit: int = 500) -> List[ToolSpec]:
"""Fetch and normalize the server's tool surface into [`ToolSpec`]s."""
listing = client.list_tools(limit=limit)
tools = listing.get("tools", []) if isinstance(listing, dict) else []
specs: List[ToolSpec] = []
for entry in tools:
if not isinstance(entry, dict):
continue
name = entry.get("name")
if not isinstance(name, str) or not name:
continue
description = entry.get("description")
if not isinstance(description, str):
description = ""
schema = (
entry.get("input_schema")
or entry.get("inputSchema")
or entry.get("parameters")
or _default_schema()
)
if not isinstance(schema, dict):
schema = _default_schema()
specs.append(ToolSpec(name=name, description=description, parameters=schema))
return specs
def coerce_arguments(raw: Any) -> Dict[str, Any]:
"""Coerce tool-call arguments (JSON string or dict) into a dict."""
if raw is None:
return {}
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
text = raw.strip()
if not text:
return {}
parsed = json.loads(text)
return parsed if isinstance(parsed, dict) else {}
return {}
def make_json_runner(client: LeanCtxClient, name: str) -> Callable[[str], str]:
"""A `(arguments: str) -> str` runner used by string-input frameworks.
The single JSON-string argument is the most portable shape across framework
versions and avoids brittle per-tool signature synthesis.
"""
def run(arguments: str = "") -> str:
return client.call_tool_text(name, coerce_arguments(arguments))
run.__name__ = name
run.__doc__ = f"Invoke the lean-ctx tool '{name}' with a JSON object string."
return run
+53
View File
@@ -0,0 +1,53 @@
"""CrewAI adapter (EPIC 12.6).
Exposes lean-ctx tools as CrewAI `BaseTool`s. The framework is an optional
dependency, imported lazily.
"""
from __future__ import annotations
from typing import Any, List
from ..client import LeanCtxClient
from ._common import coerce_arguments, normalized_tool_specs
def to_crewai_tools(client: LeanCtxClient) -> List[Any]:
"""Return lean-ctx tools as CrewAI `BaseTool` instances.
Each tool takes a single ``arguments`` field: a JSON object string of the
tool's arguments. This is portable across CrewAI versions and avoids
synthesizing a distinct pydantic schema per tool.
"""
try:
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
except ImportError as exc: # pragma: no cover - exercised only without dep
raise ImportError(
"CrewAI is required for this adapter: pip install crewai"
) from exc
class _ArgsSchema(BaseModel):
arguments: str = Field(
default="",
description="JSON object string of the tool's arguments.",
)
class _LeanCtxTool(BaseTool):
args_schema: type[BaseModel] = _ArgsSchema
def __init__(self, tool_name: str, tool_description: str) -> None:
super().__init__(name=tool_name, description=tool_description)
self._tool_name = tool_name
def _run(self, arguments: str = "") -> str:
return client.call_tool_text(self._tool_name, coerce_arguments(arguments))
tools: List[Any] = []
for spec in normalized_tool_specs(client):
description = (
f"{spec.description} "
"Input: a JSON object string matching this tool's argument schema."
).strip()
tools.append(_LeanCtxTool(spec.name, description))
return tools
@@ -0,0 +1,36 @@
"""LangChain adapter (EPIC 12.6).
Exposes lean-ctx tools as LangChain `Tool`s. The framework is an optional
dependency, imported lazily so the SDK has no hard coupling.
"""
from __future__ import annotations
from typing import Any, List
from ..client import LeanCtxClient
from ._common import make_json_runner, normalized_tool_specs
def to_langchain_tools(client: LeanCtxClient) -> List[Any]:
"""Return lean-ctx tools as `langchain_core.tools.Tool` instances.
Each tool accepts a JSON object string of arguments — the most portable
shape across LangChain versions.
"""
try:
from langchain_core.tools import Tool
except ImportError as exc: # pragma: no cover - exercised only without dep
raise ImportError(
"LangChain is required for this adapter: pip install langchain-core"
) from exc
tools: List[Any] = []
for spec in normalized_tool_specs(client):
runner = make_json_runner(client, spec.name)
description = (
f"{spec.description} "
"Input: a JSON object string matching this tool's argument schema."
).strip()
tools.append(Tool(name=spec.name, description=description, func=runner))
return tools
@@ -0,0 +1,36 @@
"""LlamaIndex adapter (EPIC 12.6).
Exposes lean-ctx tools as LlamaIndex `FunctionTool`s. The framework is an
optional dependency, imported lazily.
"""
from __future__ import annotations
from typing import Any, List
from ..client import LeanCtxClient
from ._common import make_json_runner, normalized_tool_specs
def to_llamaindex_tools(client: LeanCtxClient) -> List[Any]:
"""Return lean-ctx tools as `llama_index.core.tools.FunctionTool` instances."""
try:
from llama_index.core.tools import FunctionTool
except ImportError as exc: # pragma: no cover - exercised only without dep
raise ImportError(
"LlamaIndex is required for this adapter: pip install llama-index-core"
) from exc
tools: List[Any] = []
for spec in normalized_tool_specs(client):
runner = make_json_runner(client, spec.name)
description = (
f"{spec.description} "
"Input: a JSON object string matching this tool's argument schema."
).strip()
tools.append(
FunctionTool.from_defaults(
fn=runner, name=spec.name, description=description
)
)
return tools
+46
View File
@@ -0,0 +1,46 @@
"""OpenAI function-calling adapter (EPIC 12.6).
Pure transformation — no `openai` package required. Turns the lean-ctx tool
surface into OpenAI ``tools=[...]`` specs and executes the tool calls the model
returns. Works with both the Chat Completions and Responses tool-call shapes.
"""
from __future__ import annotations
from typing import Any, Dict, List
from ..client import LeanCtxClient
from ._common import coerce_arguments, normalized_tool_specs
def to_openai_tools(client: LeanCtxClient) -> List[Dict[str, Any]]:
"""Return lean-ctx tools as OpenAI function-tool specs."""
return [
{
"type": "function",
"function": {
"name": spec.name,
"description": spec.description,
"parameters": spec.parameters,
},
}
for spec in normalized_tool_specs(client)
]
def run_openai_tool_call(client: LeanCtxClient, tool_call: Any) -> str:
"""Execute one OpenAI tool call and return the tool's text result.
Accepts either the dict shape (``{"function": {"name", "arguments"}}``) or
an SDK object exposing ``.function.name`` / ``.function.arguments``.
"""
function = tool_call.get("function") if isinstance(tool_call, dict) else getattr(tool_call, "function", None)
if function is None:
raise ValueError("tool_call has no 'function'")
name = function.get("name") if isinstance(function, dict) else getattr(function, "name", None)
if not name:
raise ValueError("tool_call.function has no 'name'")
raw_args = (
function.get("arguments") if isinstance(function, dict) else getattr(function, "arguments", None)
)
return client.call_tool_text(name, coerce_arguments(raw_args))
+351
View File
@@ -0,0 +1,351 @@
"""Thin, dependency-free HTTP client for the lean-ctx ``/v1`` contract.
Uses only the Python standard library (``urllib``) so it installs and runs
anywhere with no transitive dependencies. It speaks the wire protocol only — it
never links the engine or re-implements compression — so it stays stable as
lean-ctx evolves. Mirrors the TypeScript (`lean-ctx-client`) and Rust
(`lean-ctx-client`) SDKs.
"""
from __future__ import annotations
import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, Iterator, Optional
from .errors import LeanCtxConfigError, LeanCtxHTTPError, LeanCtxTransportError
from .tool_text import tool_result_to_text
class LeanCtxClient:
"""A blocking client for a running lean-ctx HTTP server."""
def __init__(
self,
base_url: str,
*,
bearer_token: Optional[str] = None,
workspace_id: Optional[str] = None,
channel_id: Optional[str] = None,
timeout: float = 30.0,
) -> None:
trimmed = (base_url or "").strip()
if not trimmed:
raise LeanCtxConfigError("base_url is required")
self.base_url = trimmed[:-1] if trimmed.endswith("/") else trimmed
self._bearer_token = (bearer_token or "").strip() or None
self._workspace_id = (workspace_id or "").strip() or None
self._channel_id = (channel_id or "").strip() or None
self._timeout = timeout
# -- discovery ---------------------------------------------------------
def health(self) -> str:
return self._request_text("GET", "/health")
def manifest(self) -> Any:
return self._get_json("/v1/manifest")
def capabilities(self) -> Dict[str, Any]:
return self._get_json("/v1/capabilities")
def openapi(self) -> Dict[str, Any]:
return self._get_json("/v1/openapi.json")
# -- tools -------------------------------------------------------------
def list_tools(
self, *, offset: Optional[int] = None, limit: Optional[int] = None
) -> Dict[str, Any]:
query: Dict[str, str] = {}
if offset is not None:
query["offset"] = str(offset)
if limit is not None:
query["limit"] = str(limit)
return self._get_json("/v1/tools", query)
def call_tool(
self,
name: str,
arguments: Optional[Dict[str, Any]] = None,
*,
workspace_id: Optional[str] = None,
channel_id: Optional[str] = None,
) -> Any:
"""Call a tool and return its raw ``result``."""
if not name:
raise LeanCtxConfigError("tool name is required")
if arguments is not None and not isinstance(arguments, dict):
raise LeanCtxConfigError("arguments must be a dict (JSON object)")
body: Dict[str, Any] = {"name": name}
if arguments is not None:
body["arguments"] = arguments
ws = (workspace_id or "").strip() or self._workspace_id
ch = (channel_id or "").strip() or self._channel_id
if ws:
body["workspaceId"] = ws
if ch:
body["channelId"] = ch
payload = self._request_json(
"POST", "/v1/tools/call", body=body, workspace_id=ws
)
if not isinstance(payload, dict):
raise LeanCtxConfigError("call_tool: unexpected response shape")
return payload.get("result")
def call_tool_text(
self,
name: str,
arguments: Optional[Dict[str, Any]] = None,
**ctx: Any,
) -> str:
"""Call a tool and flatten its result into text."""
return tool_result_to_text(self.call_tool(name, arguments, **ctx))
# -- context views -------------------------------------------------------
def context_summary(
self,
*,
workspace_id: Optional[str] = None,
channel_id: Optional[str] = None,
limit: Optional[int] = None,
) -> Dict[str, Any]:
"""Materialized workspace/channel summary (``GET /v1/context/summary``)."""
query: Dict[str, str] = {}
ws = (workspace_id or "").strip() or self._workspace_id
ch = (channel_id or "").strip() or self._channel_id
if ws:
query["workspaceId"] = ws
if ch:
query["channelId"] = ch
if limit is not None:
query["limit"] = str(limit)
return self._get_json("/v1/context/summary", query)
def search_events(
self,
q: str,
*,
workspace_id: Optional[str] = None,
channel_id: Optional[str] = None,
limit: Optional[int] = None,
) -> Dict[str, Any]:
"""Full-text search over event payloads (``GET /v1/events/search``)."""
if not q:
raise LeanCtxConfigError("search query is required")
query: Dict[str, str] = {"q": q}
ws = (workspace_id or "").strip() or self._workspace_id
ch = (channel_id or "").strip() or self._channel_id
if ws:
query["workspaceId"] = ws
if ch:
query["channelId"] = ch
if limit is not None:
query["limit"] = str(limit)
return self._get_json("/v1/events/search", query)
def event_lineage(
self,
event_id: int,
*,
depth: Optional[int] = None,
workspace_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Causal lineage chain for an event (``GET /v1/events/lineage``)."""
query: Dict[str, str] = {"id": str(event_id)}
if depth is not None:
query["depth"] = str(depth)
ws = (workspace_id or "").strip() or self._workspace_id
if ws:
query["workspaceId"] = ws
return self._get_json("/v1/events/lineage", query)
def metrics(self) -> Dict[str, Any]:
"""JSON metrics snapshot (``GET /v1/metrics``)."""
return self._get_json("/v1/metrics")
# -- events (SSE) ------------------------------------------------------
def subscribe_events(
self,
*,
workspace_id: Optional[str] = None,
channel_id: Optional[str] = None,
since: Optional[int] = None,
limit: Optional[int] = None,
) -> Iterator[Dict[str, Any]]:
"""Yield ``ContextEventV1`` dicts from the SSE stream until the server closes it."""
ws = (workspace_id or "").strip() or self._workspace_id
ch = (channel_id or "").strip() or self._channel_id
query: Dict[str, str] = {}
if ws:
query["workspaceId"] = ws
if ch:
query["channelId"] = ch
if since is not None:
query["since"] = str(since)
if limit is not None:
query["limit"] = str(limit)
req = self._build_request(
"GET", "/v1/events", query, accept="text/event-stream", workspace_id=ws
)
try:
resp = urllib.request.urlopen(req, timeout=self._timeout)
except urllib.error.HTTPError as exc:
raise self._http_error_from(exc, "GET", "/v1/events") from exc
except urllib.error.URLError as exc:
raise LeanCtxTransportError(str(exc.reason)) from exc
with resp:
buf = ""
for raw in resp:
buf += raw.decode("utf-8", "replace")
while "\n\n" in buf:
chunk, buf = buf.split("\n\n", 1)
data = _parse_sse_data(chunk)
if data is None:
continue
try:
event = json.loads(data)
except json.JSONDecodeError:
continue
if isinstance(event, dict):
yield event
def events_probe(self) -> str:
"""Open ``GET /v1/events`` and return its ``Content-Type`` header.
Returns as soon as response headers arrive (the body is never read),
so it cannot block on an idle stream. Used by the conformance kit to
prove the SSE endpoint exists and speaks ``text/event-stream``.
"""
req = self._build_request(
"GET", "/v1/events", {"limit": "1"}, accept="text/event-stream"
)
try:
resp = urllib.request.urlopen(req, timeout=self._timeout)
except urllib.error.HTTPError as exc:
raise self._http_error_from(exc, "GET", "/v1/events") from exc
except urllib.error.URLError as exc:
raise LeanCtxTransportError(str(exc.reason)) from exc
with resp:
return resp.headers.get("content-type", "") or ""
# -- internals ---------------------------------------------------------
def _get_json(self, path: str, query: Optional[Dict[str, str]] = None) -> Any:
return self._request_json("GET", path, query=query)
def _request_text(self, method: str, path: str) -> str:
req = self._build_request(method, path, accept="text/plain")
return self._send(req, method, path).decode("utf-8", "replace")
def _request_json(
self,
method: str,
path: str,
*,
query: Optional[Dict[str, str]] = None,
body: Optional[Dict[str, Any]] = None,
workspace_id: Optional[str] = None,
) -> Any:
req = self._build_request(
method,
path,
query,
accept="application/json",
body=body,
workspace_id=workspace_id,
)
raw = self._send(req, method, path)
if not raw:
return None
return json.loads(raw.decode("utf-8", "replace"))
def _build_request(
self,
method: str,
path: str,
query: Optional[Dict[str, str]] = None,
*,
accept: str,
body: Optional[Dict[str, Any]] = None,
workspace_id: Optional[str] = None,
) -> urllib.request.Request:
url = self.base_url + path
if query:
url += "?" + urllib.parse.urlencode(query)
data = None
headers = {"Accept": accept}
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
if self._bearer_token:
headers["Authorization"] = f"Bearer {self._bearer_token}"
if workspace_id:
headers["x-leanctx-workspace"] = workspace_id
return urllib.request.Request(url, data=data, headers=headers, method=method)
def _send(self, req: urllib.request.Request, method: str, path: str) -> bytes:
try:
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
return resp.read()
except urllib.error.HTTPError as exc:
raise self._http_error_from(exc, method, path) from exc
except urllib.error.URLError as exc:
raise LeanCtxTransportError(str(exc.reason)) from exc
def _http_error_from(
self, exc: urllib.error.HTTPError, method: str, path: str
) -> LeanCtxHTTPError:
url = self.base_url + path
message = f"HTTP {exc.code} {method} {url}"
error_code = None
body: Any = None
try:
raw = exc.read()
except Exception: # pragma: no cover - defensive
raw = b""
content_type = exc.headers.get("content-type", "") if exc.headers else ""
if raw:
if "application/json" in content_type:
try:
body = json.loads(raw.decode("utf-8", "replace"))
if isinstance(body, dict):
if isinstance(body.get("error"), str) and body["error"].strip():
message = body["error"]
if isinstance(body.get("error_code"), str):
error_code = body["error_code"].strip() or None
except json.JSONDecodeError:
body = raw.decode("utf-8", "replace")
else:
text = raw.decode("utf-8", "replace")
body = text
if text.strip():
message = text.strip()
return LeanCtxHTTPError(
status=exc.code,
method=method,
url=url,
message=message,
error_code=error_code,
body=body,
)
def _parse_sse_data(chunk: str) -> Optional[str]:
"""Join the ``data:`` lines of one SSE frame; ignore id/event/comments."""
data_lines = []
for line in chunk.split("\n"):
line = line.rstrip("\r")
if not line or line.startswith(":"):
continue
if line.startswith("data:"):
data_lines.append(line[5:].lstrip())
return "\n".join(data_lines) if data_lines else None
+217
View File
@@ -0,0 +1,217 @@
"""Shared SDK conformance kit (EPIC 12.4/12.5, industrialized in GL #395).
A client-side check that proves the Python SDK + a live server interoperate over
the **entire** frozen ``/v1`` contract. It is the exact mirror of the TypeScript
SDK's ``runConformance`` and the Rust client's ``run_conformance``, so every
first-party SDK proves the same contract and they stay in lockstep.
Two checks make this a drift gate (GL #395):
* ``route_coverage`` — every path the server's OpenAPI document advertises must
be covered by an SDK method (``COVERED_ROUTES``). A new server route without
SDK support fails conformance in the next CI run.
* ``engine_compat`` — the server's ``http_mcp`` contract version must be one
this SDK release supports (``SUPPORTED_HTTP_CONTRACT_VERSIONS``).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List
from .client import LeanCtxClient
from .errors import LeanCtxHTTPError
#: ``METHOD path`` → client method name. The conformance kit fails if the live
#: server's OpenAPI document lists a route that is missing here.
COVERED_ROUTES: Dict[str, str] = {
"GET /health": "health",
"GET /v1/manifest": "manifest",
"GET /v1/capabilities": "capabilities",
"GET /v1/openapi.json": "openapi",
"GET /v1/tools": "list_tools",
"POST /v1/tools/call": "call_tool",
"GET /v1/events": "subscribe_events",
"GET /v1/context/summary": "context_summary",
"GET /v1/events/search": "search_events",
"GET /v1/events/lineage": "event_lineage",
"GET /v1/metrics": "metrics",
}
#: ``http_mcp`` contract versions this SDK release speaks (SemVer coupling:
#: the SDK major follows the engine contract major).
SUPPORTED_HTTP_CONTRACT_VERSIONS = (1,)
@dataclass
class ConformanceCheck:
name: str
passed: bool
detail: str = ""
@dataclass
class ConformanceScorecard:
checks: List[ConformanceCheck] = field(default_factory=list)
@property
def passed(self) -> int:
return sum(1 for c in self.checks if c.passed)
@property
def total(self) -> int:
return len(self.checks)
@property
def all_passed(self) -> bool:
return all(c.passed for c in self.checks)
def _add(card: ConformanceScorecard, name: str, probe: Callable[[], Any]) -> None:
"""Run one probe; contract/network failures become failed checks."""
try:
ok, detail = probe()
card.checks.append(ConformanceCheck(name, bool(ok), detail))
except Exception as exc: # noqa: BLE001 - capture as a failed check
card.checks.append(ConformanceCheck(name, False, str(exc)))
def run_conformance(client: LeanCtxClient) -> ConformanceScorecard:
"""Run the conformance kit against a live client.
Network/contract failures become failed checks rather than exceptions, so
the returned scorecard is always complete and comparable across SDKs.
"""
card = ConformanceScorecard()
def health() -> Any:
return isinstance(client.health(), str), ""
def manifest_shape() -> Any:
m = client.manifest()
return isinstance(m, dict) and bool(m), ""
def capabilities_shape() -> Any:
caps = client.capabilities()
server = caps.get("server", {}) if isinstance(caps, dict) else {}
ok = (
isinstance(caps, dict)
and isinstance(caps.get("contract_version"), int)
and isinstance(server, dict)
and bool(server.get("version"))
and isinstance(caps.get("plane"), str)
and isinstance(caps.get("transports"), list)
and isinstance(caps.get("features"), dict)
and isinstance(caps.get("contracts"), dict)
)
return ok, ""
def contract_status_map() -> Any:
# GL #394: stability per contract is part of the discovery document.
status = client.capabilities().get("contract_status")
ok = isinstance(status, dict) and status.get("http-mcp") in (
"frozen",
"stable",
)
return ok, "" if ok else f"contract_status={status!r}"
def engine_compat() -> Any:
contracts = client.capabilities().get("contracts", {})
version = contracts.get("leanctx.contract.http_mcp.contract_version")
ok = version in SUPPORTED_HTTP_CONTRACT_VERSIONS
return ok, "" if ok else f"server http_mcp contract v{version!r} unsupported"
def openapi_shape() -> Any:
doc = client.openapi()
version = doc.get("openapi", "") if isinstance(doc, dict) else ""
ok = (
isinstance(version, str)
and version.startswith("3.")
and isinstance(doc.get("paths"), dict)
)
return ok, ""
def route_coverage() -> Any:
# The drift gate: every advertised route needs an SDK method.
doc = client.openapi()
paths = doc.get("paths", {}) if isinstance(doc, dict) else {}
uncovered = [
f"{method.upper()} {path}"
for path, ops in paths.items()
if isinstance(ops, dict)
for method in ops
if f"{method.upper()} {path}" not in COVERED_ROUTES
]
return not uncovered, ", ".join(sorted(uncovered))
def tools_list() -> Any:
listing = client.list_tools(limit=1)
ok = (
isinstance(listing, dict)
and isinstance(listing.get("tools"), list)
and isinstance(listing.get("total"), int)
and listing["total"] >= 0
)
return ok, ""
def tool_call_error_contract() -> Any:
# Typed-error semantics: an unknown tool must produce a structured
# 4xx with a machine-readable error_code, not a 5xx or free text.
try:
client.call_tool("definitely_not_a_tool_conformance_probe")
except LeanCtxHTTPError as exc:
ok = 400 <= exc.status < 500 and bool(exc.error_code)
return ok, "" if ok else f"status={exc.status} error_code={exc.error_code!r}"
return False, "unknown tool call unexpectedly succeeded"
def events_stream() -> Any:
content_type = client.events_probe()
ok = content_type.startswith("text/event-stream")
return ok, "" if ok else f"content-type={content_type!r}"
def context_summary_shape() -> Any:
summary = client.context_summary(limit=1)
ok = (
isinstance(summary, dict)
and isinstance(summary.get("workspaceId"), str)
and isinstance(summary.get("totalEvents"), int)
and isinstance(summary.get("eventCountsByKind"), dict)
)
return ok, ""
def events_search_shape() -> Any:
res = client.search_events("conformance-probe", limit=1)
ok = (
isinstance(res, dict)
and isinstance(res.get("results"), list)
and isinstance(res.get("count"), int)
)
return ok, ""
def event_lineage_shape() -> Any:
res = client.event_lineage(1, depth=1)
ok = (
isinstance(res, dict)
and "eventId" in res
and isinstance(res.get("chain"), list)
)
return ok, ""
def metrics_shape() -> Any:
return isinstance(client.metrics(), dict), ""
_add(card, "health", health)
_add(card, "manifest_shape", manifest_shape)
_add(card, "capabilities_shape", capabilities_shape)
_add(card, "contract_status_map", contract_status_map)
_add(card, "engine_compat", engine_compat)
_add(card, "openapi_shape", openapi_shape)
_add(card, "route_coverage", route_coverage)
_add(card, "tools_list", tools_list)
_add(card, "tool_call_error_contract", tool_call_error_contract)
_add(card, "events_stream", events_stream)
_add(card, "context_summary_shape", context_summary_shape)
_add(card, "events_search_shape", events_search_shape)
_add(card, "event_lineage_shape", event_lineage_shape)
_add(card, "metrics_shape", metrics_shape)
return card
+47
View File
@@ -0,0 +1,47 @@
"""Error types for the lean-ctx Python client."""
from __future__ import annotations
from typing import Any, Optional
class LeanCtxError(Exception):
"""Base class for all lean-ctx client errors."""
class LeanCtxConfigError(LeanCtxError):
"""Raised for invalid client configuration or arguments (no I/O performed)."""
class LeanCtxTransportError(LeanCtxError):
"""Raised when the request never produced an HTTP response (network/DNS/TLS)."""
class LeanCtxHTTPError(LeanCtxError):
"""Raised for a non-2xx HTTP response from the server.
Carries enough structured detail (status, method, url, server error code and
body) for programmatic handling, mirroring the Rust/TS SDK error shape.
"""
def __init__(
self,
*,
status: int,
method: str,
url: str,
message: str,
error_code: Optional[str] = None,
body: Any = None,
) -> None:
super().__init__(message)
self.status = status
self.method = method
self.url = url
self.message = message
self.error_code = error_code
self.body = body
def __str__(self) -> str: # pragma: no cover - trivial
code = f" [{self.error_code}]" if self.error_code else ""
return f"HTTP {self.status} {self.method} {self.url}{code}: {self.message}"
+31
View File
@@ -0,0 +1,31 @@
"""Extract plain text from an MCP tool result.
Mirrors `toolText.ts` / `tool_text.rs` so all SDKs flatten tool results the same
way: concatenate the ``text`` of every ``{"type": "text", "text": ...}`` content
block. Non-text blocks are ignored; a bare string result is returned as-is.
"""
from __future__ import annotations
from typing import Any
def tool_result_to_text(result: Any) -> str:
"""Flatten an MCP tool result into a single text string."""
if result is None:
return ""
if isinstance(result, str):
return result
if isinstance(result, dict):
content = result.get("content")
if isinstance(content, list):
parts = []
for block in content:
if (
isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
):
parts.append(block["text"])
return "".join(parts)
return ""