Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,6 @@
|
||||
"""Read-only MobilityOps (Fleet Ops) MCP connector."""
|
||||
|
||||
from .fake import FakeMobilityOpsServer
|
||||
from .server import MOBILITYOPS_TOOLS, MobilityOpsConnector
|
||||
|
||||
__all__ = ["MOBILITYOPS_TOOLS", "FakeMobilityOpsServer", "MobilityOpsConnector"]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Deterministic in-memory MobilityOps HTTP transport for contract tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class FakeMobilityOpsServer:
|
||||
"""A tiny MobilityOps API fixture with observable requests and no network."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
unauthorized: bool = False,
|
||||
redirect_to: str | None = None,
|
||||
vehicle_not_found: bool = False,
|
||||
unreachable: bool = False,
|
||||
) -> None:
|
||||
self.unauthorized = unauthorized
|
||||
self.redirect_to = redirect_to
|
||||
self.vehicle_not_found = vehicle_not_found
|
||||
self.unreachable = unreachable
|
||||
self.recorded_methods: list[str] = []
|
||||
self.recorded_service_tokens: list[str] = []
|
||||
self.recorded_client_ids: list[str] = []
|
||||
self.recorded_correlation_ids: list[str] = []
|
||||
self.recorded_locales: list[str | None] = []
|
||||
self._summary = {
|
||||
"tenant": "Northstar Mobility Demo",
|
||||
"metrics": {
|
||||
"available": 21,
|
||||
"rented": 11,
|
||||
"cleaning": 6,
|
||||
"maintenance": 5,
|
||||
"blocked": 7,
|
||||
"open_quality_issues": 3,
|
||||
"pending_or_failed_workflows": 1,
|
||||
},
|
||||
}
|
||||
self._attention = [
|
||||
{
|
||||
"vehicle_ref": "MO-016",
|
||||
"severity": "high",
|
||||
"rule_type": "vehicle_status_conflict",
|
||||
"summary": "Booking overlap detected for this vehicle.",
|
||||
"detected_at": "2026-08-01T09:00:00Z",
|
||||
"token": "fixture-token-must-not-leak",
|
||||
}
|
||||
]
|
||||
self._vehicle = {
|
||||
"public_ref": "MO-016",
|
||||
"make": "Volkswagen",
|
||||
"model": "ID.4",
|
||||
"model_year": 2024,
|
||||
"location": "Antwerp Depot",
|
||||
"operational_status": "available",
|
||||
"odometer_km": 18420,
|
||||
"next_service_km": 40000,
|
||||
"open_quality_issue_count": 1,
|
||||
"current_booking_ref": None,
|
||||
}
|
||||
self._answer = {
|
||||
"answer": "Report the damage and route the vehicle to maintenance.",
|
||||
"evidence_state": "grounded",
|
||||
"sources": [
|
||||
{
|
||||
"document_id": "damage-procedure",
|
||||
"title": "Damage handling procedure",
|
||||
"version": "1",
|
||||
"section": "Detection",
|
||||
"excerpt": "Inspect the vehicle for visible damage.",
|
||||
}
|
||||
],
|
||||
"provider": "demo",
|
||||
"correlation_id": "fixture-correlation-id",
|
||||
}
|
||||
|
||||
def transport(self) -> httpx.MockTransport:
|
||||
return httpx.MockTransport(self._handle)
|
||||
|
||||
def _handle(self, request: httpx.Request) -> httpx.Response:
|
||||
self.recorded_methods.append(request.method)
|
||||
self.recorded_service_tokens.append(request.headers.get("x-service-token", ""))
|
||||
self.recorded_client_ids.append(request.headers.get("x-client-id", ""))
|
||||
self.recorded_correlation_ids.append(request.headers.get("x-correlation-id", ""))
|
||||
if self.unreachable:
|
||||
raise httpx.ConnectError("fixture target is unreachable", request=request)
|
||||
if self.redirect_to is not None:
|
||||
return httpx.Response(302, headers={"location": self.redirect_to}, request=request)
|
||||
if self.unauthorized:
|
||||
return self._json(401, {"code": "UNAUTHORIZED_SERVICE"}, request)
|
||||
|
||||
parsed = urlparse(str(request.url))
|
||||
path = parsed.path.removeprefix("/api/v1/integrations/mcp")
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
if path == "/operations-summary" and request.method == "GET":
|
||||
return self._json(200, self._summary, request)
|
||||
if path == "/attention-vehicles" and request.method == "GET":
|
||||
limit = int(query.get("limit", ["20"])[0])
|
||||
return self._json(200, self._attention[:limit], request)
|
||||
if path.startswith("/vehicles/") and request.method == "GET":
|
||||
if self.vehicle_not_found:
|
||||
return self._json(404, {"code": "VEHICLE_NOT_FOUND"}, request)
|
||||
return self._json(200, self._vehicle, request, leaked_header=True)
|
||||
if path == "/search-knowledge" and request.method == "POST":
|
||||
body = json.loads(request.content or b"{}")
|
||||
self.recorded_locales.append(body.get("locale"))
|
||||
# MobilityOps echoes the correlation id it actually received.
|
||||
answer = dict(self._answer)
|
||||
answer["correlation_id"] = (
|
||||
request.headers.get("x-correlation-id") or answer["correlation_id"]
|
||||
)
|
||||
return self._json(200, answer, request)
|
||||
return self._json(404, {"code": "NOT_FOUND"}, request)
|
||||
|
||||
@staticmethod
|
||||
def _json(
|
||||
status: int, payload: object, request: httpx.Request, *, leaked_header: bool = False
|
||||
) -> httpx.Response:
|
||||
headers = {"content-type": "application/json"}
|
||||
if leaked_header:
|
||||
headers["x-fixture-token"] = "fixture-token-must-not-leak"
|
||||
return httpx.Response(
|
||||
status, headers=headers, content=json.dumps(payload).encode(), request=request
|
||||
)
|
||||
@@ -0,0 +1,318 @@
|
||||
"""Read-only MCP connector for the MobilityOps (Fleet Ops) operations API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from packages.connector_kit import (
|
||||
DEFAULT_KNOWLEDGE_LOCALE,
|
||||
KNOWLEDGE_LOCALES,
|
||||
ConnectorContext,
|
||||
MobilityOpsClient,
|
||||
MobilityOpsError,
|
||||
ResourcePublication,
|
||||
ToolPublication,
|
||||
safe_snippet,
|
||||
)
|
||||
from packages.hub_policy import Governance, governed_tool
|
||||
|
||||
|
||||
class MobilityOpsConnector:
|
||||
"""Expose MobilityOps's four read-only operational tools.
|
||||
|
||||
Unlike Gitea, there is exactly one configured MobilityOps target per
|
||||
environment -- no per-project repository registry, every call is
|
||||
authorized purely by the caller's project/environment context via the
|
||||
governance layer, matching MobilityOps's own narrow read-only API surface
|
||||
(`docs/10-mcp-hub-integration.md` in the MobilityOps repository).
|
||||
"""
|
||||
|
||||
connector_type = "mobilityops"
|
||||
version = "1.0.0"
|
||||
#: Identifies readiness probes in MobilityOps's own service-request audit log.
|
||||
READINESS_CLIENT_ID = "itworx-mcp-hub:readiness"
|
||||
|
||||
def __init__(self, client: MobilityOpsClient, governance: Governance | None = None) -> None:
|
||||
self.client = client
|
||||
self.governance = governance
|
||||
|
||||
def _envelope(
|
||||
self,
|
||||
context: ConnectorContext,
|
||||
data: Any,
|
||||
*,
|
||||
source: str = "mobilityops",
|
||||
truncated: bool = False,
|
||||
warnings: tuple[str, ...] = (),
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"data": data,
|
||||
"meta": {
|
||||
"project": context.project_slug,
|
||||
"environment": context.environment,
|
||||
"source": source,
|
||||
"truncated": truncated,
|
||||
"nextCursor": None,
|
||||
"generatedAt": datetime.now(UTC).isoformat(),
|
||||
},
|
||||
"warnings": list(warnings),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _client_id(context: ConnectorContext) -> str:
|
||||
return f"itworx-mcp-hub:{context.project_slug}:{context.client_id or 'anonymous'}"
|
||||
|
||||
@staticmethod
|
||||
def _optional_text(value: Any, limit: int) -> str | None:
|
||||
"""Pass through exactly what MobilityOps returned, or nothing at all.
|
||||
|
||||
Never substitutes a placeholder: an absent `provider`/`correlationId` is
|
||||
reported as `null` rather than invented.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text[:limit] if text else None
|
||||
|
||||
@governed_tool("mobilityops.operations.summary")
|
||||
def operations_summary(self, context: ConnectorContext) -> dict[str, Any]:
|
||||
payload = self.client.operations_summary(
|
||||
self._client_id(context), correlation_id=context.correlation_id
|
||||
)
|
||||
raw_metrics = payload.get("metrics")
|
||||
metrics: dict[str, Any] = raw_metrics if isinstance(raw_metrics, dict) else {}
|
||||
data = {
|
||||
"tenant": safe_snippet(str(payload.get("tenant", "")), max_bytes=200),
|
||||
"metrics": {
|
||||
"available": int(metrics.get("available", 0)),
|
||||
"rented": int(metrics.get("rented", 0)),
|
||||
"cleaning": int(metrics.get("cleaning", 0)),
|
||||
"maintenance": int(metrics.get("maintenance", 0)),
|
||||
"blocked": int(metrics.get("blocked", 0)),
|
||||
"openQualityIssues": int(metrics.get("open_quality_issues", 0)),
|
||||
"pendingOrFailedWorkflows": int(metrics.get("pending_or_failed_workflows", 0)),
|
||||
},
|
||||
}
|
||||
return self._envelope(context, data)
|
||||
|
||||
@governed_tool("mobilityops.attention.list")
|
||||
def attention_list(
|
||||
self,
|
||||
context: ConnectorContext,
|
||||
*,
|
||||
minimum_severity: str = "medium",
|
||||
date: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
if minimum_severity not in {"low", "medium", "high"}:
|
||||
raise ValueError("minimum_severity must be one of low, medium, high")
|
||||
if not 1 <= limit <= 50:
|
||||
raise ValueError("limit must be between 1 and 50")
|
||||
items = self.client.attention_vehicles(
|
||||
self._client_id(context),
|
||||
minimum_severity=minimum_severity,
|
||||
date=date,
|
||||
limit=limit,
|
||||
correlation_id=context.correlation_id,
|
||||
)
|
||||
mapped = [
|
||||
{
|
||||
"vehicleRef": str(item.get("vehicle_ref", ""))[:64],
|
||||
"severity": str(item.get("severity", ""))[:32],
|
||||
"ruleType": str(item.get("rule_type", ""))[:120],
|
||||
"summary": safe_snippet(str(item.get("summary", "")), max_bytes=1000),
|
||||
"detectedAt": str(item.get("detected_at", ""))[:64],
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
data = {"items": mapped, "nextCursor": None, "truncated": False}
|
||||
return self._envelope(context, data, truncated=False)
|
||||
|
||||
@governed_tool("mobilityops.vehicle.get")
|
||||
def vehicle_get(self, context: ConnectorContext, vehicle_ref: str) -> dict[str, Any]:
|
||||
payload = self.client.vehicle_details(
|
||||
self._client_id(context), vehicle_ref, correlation_id=context.correlation_id
|
||||
)
|
||||
data = {
|
||||
"publicRef": str(payload.get("public_ref", ""))[:64],
|
||||
"make": str(payload.get("make", ""))[:120],
|
||||
"model": str(payload.get("model", ""))[:120],
|
||||
"modelYear": int(payload.get("model_year", 0)),
|
||||
"location": str(payload.get("location", ""))[:200],
|
||||
"operationalStatus": str(payload.get("operational_status", ""))[:64],
|
||||
"odometerKm": int(payload.get("odometer_km", 0)),
|
||||
"nextServiceKm": int(payload.get("next_service_km", 0)),
|
||||
"openQualityIssueCount": int(payload.get("open_quality_issue_count", 0)),
|
||||
"currentBookingRef": (
|
||||
str(payload["current_booking_ref"])[:64]
|
||||
if payload.get("current_booking_ref")
|
||||
else None
|
||||
),
|
||||
}
|
||||
return self._envelope(context, data, source=f"mobilityops:{vehicle_ref}")
|
||||
|
||||
@governed_tool("mobilityops.knowledge.search")
|
||||
def knowledge_search(
|
||||
self,
|
||||
context: ConnectorContext,
|
||||
question: str,
|
||||
*,
|
||||
max_sources: int = 4,
|
||||
locale: str = DEFAULT_KNOWLEDGE_LOCALE,
|
||||
) -> dict[str, Any]:
|
||||
if not 3 <= len(question) <= 1000:
|
||||
raise ValueError("question is outside the connector budget")
|
||||
if not 1 <= max_sources <= 8:
|
||||
raise ValueError("max_sources must be between 1 and 8")
|
||||
if locale not in KNOWLEDGE_LOCALES:
|
||||
raise ValueError("locale must be one of nl-BE, en-GB, fr-BE")
|
||||
payload = self.client.search_knowledge(
|
||||
self._client_id(context),
|
||||
question,
|
||||
max_sources=max_sources,
|
||||
locale=locale,
|
||||
correlation_id=context.correlation_id,
|
||||
)
|
||||
raw_sources = payload.get("sources")
|
||||
sources: list[Any] = raw_sources if isinstance(raw_sources, list) else []
|
||||
data = {
|
||||
"answer": safe_snippet(str(payload.get("answer", "")), max_bytes=4000),
|
||||
"evidenceState": str(payload.get("evidence_state", "unavailable"))[:32],
|
||||
"provider": self._optional_text(payload.get("provider"), 120),
|
||||
"correlationId": self._optional_text(payload.get("correlation_id"), 128),
|
||||
"sources": [
|
||||
{
|
||||
"documentId": str(source.get("document_id", ""))[:200],
|
||||
"title": str(source.get("title", ""))[:300],
|
||||
"version": str(source.get("version", ""))[:120],
|
||||
"section": str(source.get("section", ""))[:300],
|
||||
"excerpt": safe_snippet(str(source.get("excerpt", "")), max_bytes=1500),
|
||||
}
|
||||
for source in sources
|
||||
if isinstance(source, dict)
|
||||
],
|
||||
}
|
||||
return self._envelope(context, data, source="mobilityops:knowledge")
|
||||
|
||||
def tools(self) -> tuple[ToolPublication, ...]:
|
||||
return MOBILITYOPS_TOOLS
|
||||
|
||||
def resources(self) -> tuple[ResourcePublication, ...]:
|
||||
return ()
|
||||
|
||||
def invoke(self, tool: str, context: ConnectorContext, **arguments: Any) -> dict[str, Any]:
|
||||
methods: dict[str, Callable[..., dict[str, Any]]] = {
|
||||
"mobilityops.operations.summary": self.operations_summary,
|
||||
"mobilityops.attention.list": self.attention_list,
|
||||
"mobilityops.vehicle.get": self.vehicle_get,
|
||||
"mobilityops.knowledge.search": self.knowledge_search,
|
||||
}
|
||||
method = methods.get(tool)
|
||||
if method is None:
|
||||
raise ValueError("unknown mobilityops tool")
|
||||
return method(context, **arguments)
|
||||
|
||||
def health(self) -> dict[str, Any]:
|
||||
"""Liveness only: this process answers, independent of the MobilityOps upstream."""
|
||||
return {"status": "healthy", "component": "mobilityops-connector", "readOnly": True}
|
||||
|
||||
#: Upstream faults that mean MobilityOps answered but this connector cannot use it.
|
||||
_DEGRADED_CODES = frozenset(
|
||||
{
|
||||
"MOBILITYOPS_ACCESS_DENIED",
|
||||
"MOBILITYOPS_REQUEST_INVALID",
|
||||
"MOBILITYOPS_INVALID_RESPONSE",
|
||||
"MOBILITYOPS_RESPONSE_TOO_LARGE",
|
||||
"MOBILITYOPS_REDIRECT_DENIED",
|
||||
"MOBILITYOPS_SECRET_UNAVAILABLE",
|
||||
"MOBILITYOPS_SECRET_INVALID",
|
||||
"MOBILITYOPS_SECRET_REFERENCE_INVALID",
|
||||
}
|
||||
)
|
||||
|
||||
def readiness(self) -> dict[str, Any]:
|
||||
"""Ready only when the real MobilityOps upstream actually answers.
|
||||
|
||||
Uses one short, bounded, authenticated upstream read. Only the connector's own
|
||||
stable error code is reported -- never a raw exception, response body, header or
|
||||
secret.
|
||||
"""
|
||||
snapshot: dict[str, Any] = {
|
||||
"component": "mobilityops-connector",
|
||||
"readOnly": True,
|
||||
"upstream": "mobilityops",
|
||||
}
|
||||
try:
|
||||
self.client.readiness_probe(self.READINESS_CLIENT_ID)
|
||||
except MobilityOpsError as exc:
|
||||
snapshot["status"] = "degraded" if exc.code in self._DEGRADED_CODES else "unavailable"
|
||||
snapshot["reason"] = exc.code
|
||||
return snapshot
|
||||
except Exception: # noqa: BLE001 - readiness must never surface a raw exception.
|
||||
snapshot["status"] = "unavailable"
|
||||
snapshot["reason"] = "MOBILITYOPS_READINESS_FAILED"
|
||||
return snapshot
|
||||
snapshot["status"] = "ready"
|
||||
return snapshot
|
||||
|
||||
|
||||
_EMPTY_INPUT = {"type": "object", "additionalProperties": False, "properties": {}}
|
||||
_ENVELOPE_OUTPUT = {"type": "object", "additionalProperties": False}
|
||||
|
||||
_OPERATIONS_SUMMARY_INPUT = _EMPTY_INPUT
|
||||
_ATTENTION_LIST_INPUT = {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"minimum_severity": {"type": "string", "enum": ["low", "medium", "high"]},
|
||||
"date": {"type": ["string", "null"], "format": "date"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
|
||||
},
|
||||
}
|
||||
_VEHICLE_GET_INPUT = {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {"vehicleRef": {"type": "string", "minLength": 1, "maxLength": 64}},
|
||||
"required": ["vehicleRef"],
|
||||
}
|
||||
_KNOWLEDGE_SEARCH_INPUT = {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"question": {"type": "string", "minLength": 3, "maxLength": 1000},
|
||||
"max_sources": {"type": "integer", "minimum": 1, "maximum": 8},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"enum": list(KNOWLEDGE_LOCALES),
|
||||
"default": DEFAULT_KNOWLEDGE_LOCALE,
|
||||
},
|
||||
},
|
||||
"required": ["question"],
|
||||
}
|
||||
|
||||
_INPUT_SCHEMAS: dict[str, dict[str, Any]] = {
|
||||
"mobilityops.operations.summary": _OPERATIONS_SUMMARY_INPUT,
|
||||
"mobilityops.attention.list": _ATTENTION_LIST_INPUT,
|
||||
"mobilityops.vehicle.get": _VEHICLE_GET_INPUT,
|
||||
"mobilityops.knowledge.search": _KNOWLEDGE_SEARCH_INPUT,
|
||||
}
|
||||
|
||||
MOBILITYOPS_TOOLS = tuple(
|
||||
ToolPublication(
|
||||
canonical_name=name,
|
||||
version="1.0.0",
|
||||
connector_type="mobilityops",
|
||||
risk_class="R1",
|
||||
input_schema=_INPUT_SCHEMAS[name],
|
||||
output_schema=_ENVELOPE_OUTPUT,
|
||||
required_scopes=("project:{project}:read",),
|
||||
)
|
||||
for name in (
|
||||
"mobilityops.operations.summary",
|
||||
"mobilityops.attention.list",
|
||||
"mobilityops.vehicle.get",
|
||||
"mobilityops.knowledge.search",
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user