M9: MCP Hub locale/correlation propagation, real Hub health check, fix stale test image

Fixed two concrete gaps in the MCP knowledge-search endpoint: no locale field
existed at all (now nl-BE/en-GB/fr-BE, wired to the knowledge provider's
existing language param), and the correlation ID was always freshly minted,
ignoring any inbound X-Correlation-Id header. Added a shared dependency and
applied it to all four MCP endpoints so Fleet Ops's own audit log preserves
the Hub's real correlation ID end to end.

MCP_HUB_BASE_URL/MCP_PROVIDER_ID were declared in .env.example but never read
anywhere. Since the Hub's own registration is catalog-driven (it never needs
Fleet Ops to push a registration call), wired mcp_hub_base_url for a real Hub
reachability health check instead of an unneeded self-registration call.

Renamed Fleet Ops's own internal audit tool labels mobilityops_* -> fleet_ops_*
(mirrored in contracts/mcp-tools.json with mobilityops_* kept as deprecated
aliases); documented that the live Hub connector's own dotted tool namespace
is a separate, Hub-owned naming layer, deliberately not touched.

Automation page's MCP card now shows real evidence (last tool/client/count/
timestamp, honest no-evidence state) instead of just the registration flag.

Also fixed a real methodology gap found mid-session: compose.yaml's api
service has no bind mount, so `docker compose run --rm api` silently tests a
stale image until rebuilt. Re-ran every local gate after rebuilding; fixed one
genuinely stale test assertion and two lint line-length errors surfaced by
that rebuild. 176 tests passing, ruff clean, mypy clean (50 files).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-05 13:30:24 +02:00
co-authored by Claude Sonnet 5
parent 2ae2044e3a
commit 727c19a779
17 changed files with 269 additions and 45 deletions
+1
View File
@@ -289,6 +289,7 @@ export interface McpHubIntegrationStatus {
last_tool: string | null;
last_client: string | null;
last_called_at: string | null;
hub_reachable: boolean | null;
}
export interface IntegrationStatus {
@@ -15,7 +15,11 @@
"gatewayKicker": "Tool gateway",
"mcpTitle": "MCP Hub",
"mcpEnabled": "Registration is enabled for this deployment.",
"mcpNotConnected": "Not yet connected — prepared for future controlled tool calls from the Hub."
"mcpNotConnected": "Not yet connected — prepared for future controlled tool calls from the Hub.",
"mcpNoEvidence": "Registered, but no tool call has been recorded yet.",
"mcpEvidence": "Last call: {{tool}} by {{client}} · {{count}} total calls",
"mcpHubReachable": "Hub reachable",
"mcpHubUnreachable": "Hub unreachable"
},
"statusLabels": {
"notConnected": "Not connected",
@@ -15,7 +15,11 @@
"gatewayKicker": "Passerelle d'outils",
"mcpTitle": "MCP Hub",
"mcpEnabled": "L'enregistrement est activé pour ce déploiement.",
"mcpNotConnected": "Pas encore connecté — préparé pour de futurs appels d'outils contrôlés depuis le Hub."
"mcpNotConnected": "Pas encore connecté — préparé pour de futurs appels d'outils contrôlés depuis le Hub.",
"mcpNoEvidence": "Enregistré, mais aucun appel d'outil n'a encore été consigné.",
"mcpEvidence": "Dernier appel : {{tool}} par {{client}} · {{count}} appels au total",
"mcpHubReachable": "Hub accessible",
"mcpHubUnreachable": "Hub inaccessible"
},
"statusLabels": {
"notConnected": "Non connecté",
@@ -15,7 +15,11 @@
"gatewayKicker": "Tool-gateway",
"mcpTitle": "MCP Hub",
"mcpEnabled": "Registratie is ingeschakeld voor deze omgeving.",
"mcpNotConnected": "Nog niet gekoppeld — voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub."
"mcpNotConnected": "Nog niet gekoppeld — voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub.",
"mcpNoEvidence": "Geregistreerd, maar er is nog geen tool-aanroep geregistreerd.",
"mcpEvidence": "Laatste aanroep: {{tool}} door {{client}} · {{count}} aanroepen in totaal",
"mcpHubReachable": "Hub bereikbaar",
"mcpHubUnreachable": "Hub onbereikbaar"
},
"statusLabels": {
"notConnected": "Niet gekoppeld",
+25 -5
View File
@@ -209,12 +209,32 @@ export function Automation() {
<div>
<span className="integration-kicker">{t("cards.gatewayKicker")}</span>
<h2>{t("cards.mcpTitle")}</h2>
<p>{integrationStatus?.mcp_hub.registration_enabled ? t("cards.mcpEnabled") : t("cards.mcpNotConnected")}</p>
<p>
{(() => {
const hub = integrationStatus?.mcp_hub;
if (!hub?.registration_enabled) return t("cards.mcpNotConnected");
if (hub.total_calls > 0) {
return t("cards.mcpEvidence", { tool: hub.last_tool, client: hub.last_client, count: hub.total_calls });
}
return t("cards.mcpNoEvidence");
})()}
</p>
{integrationStatus?.mcp_hub.last_called_at && (
<small>{formatDateTime(integrationStatus.mcp_hub.last_called_at)}</small>
)}
</div>
<div className="integration-badge-stack">
{(() => {
const meta = integrationStatus ? MCP_STATE_META[integrationStatus.mcp_hub.state] : null;
return <StatusBadge status={meta?.statusClass ?? "not_configured"} label={meta ? t(`statusLabels.${meta.labelKey}`) : undefined} />;
})()}
{integrationStatus?.mcp_hub.hub_reachable !== null && integrationStatus?.mcp_hub.hub_reachable !== undefined && (
<StatusBadge
status={integrationStatus.mcp_hub.hub_reachable ? "available" : "unavailable"}
label={t(integrationStatus.mcp_hub.hub_reachable ? "cards.mcpHubReachable" : "cards.mcpHubUnreachable")}
/>
)}
</div>
{(() => {
const meta = integrationStatus ? MCP_STATE_META[integrationStatus.mcp_hub.state] : null;
return <StatusBadge status={meta?.statusClass ?? "not_configured"} label={meta ? t(`statusLabels.${meta.labelKey}`) : undefined} />;
})()}
</article>
</section>
+3 -1
View File
@@ -337,7 +337,9 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details
.resolution-actions .button-tertiary:hover { color: var(--ink); background: var(--surface); border-color: var(--line); }
.resolution-actions .button-tertiary-destructive:hover { color: var(--critical); background: var(--surface); border-color: var(--line); }
.integration-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 28px; }.integration-cards article { min-height: 170px; display: grid; grid-template-columns: auto 1fr; gap: 12px; padding: 18px; background: white; border: 1px solid var(--line); border-radius: var(--radius); }.integration-cards .badge { grid-column: 1 / -1; width: max-content; align-self: end; }.integration-cards h2 { margin: 3px 0 7px; font-size: .95rem; }.integration-cards p { margin: 0; color: var(--muted); font-size: .7rem; line-height: 1.48; }.integration-kicker { color: var(--muted); font-size: .56rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; }
.integration-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 28px; }.integration-cards article { min-height: 170px; display: grid; grid-template-columns: auto 1fr; gap: 12px; padding: 18px; background: white; border: 1px solid var(--line); border-radius: var(--radius); }.integration-cards .badge { grid-column: 1 / -1; width: max-content; align-self: end; }
.integration-cards .integration-badge-stack { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; align-self: end; }
.integration-cards small { display: block; margin-top: 4px; color: var(--muted-light); font-size: .6rem; }.integration-cards h2 { margin: 3px 0 7px; font-size: .95rem; }.integration-cards p { margin: 0; color: var(--muted); font-size: .7rem; line-height: 1.48; }.integration-kicker { color: var(--muted); font-size: .56rem; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; }
.scenario-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }
.scenario-card { display: flex; flex-direction: column; gap: 10px; padding: 18px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }