API integration
This document covers how the UI Console talks to its two upstream services. Endpoint schemas and request/response details live in the upstream API docs and are referenced rather than duplicated here.
Endpoint references — the UI integrates against two HTTP services. For full endpoint schemas, error shapes, and semantics see the upstream docs:
- Control Plane —
control-plane/docs/API.md,control-plane/docs/INTEGRATION.md,control-plane/docs/ARCHITECTURE.md- Examples Service —
examples-service/docs/api-reference.md,examples-service/docs/architecture.mdWhat follows is UI-specific: the proxy model, the subset of endpoints the UI calls, the normalizers that bridge upstream shapes to UI types, and the SSE / demo-mode plumbing.
Overview
The UI integrates with two upstream services:
- Examples Service — scenario catalog, agent profiles, launch schema, launch compilation, optional one-shot bootstrap.
- Control Plane — observer-only run lifecycle, state projection, canonical events (per-run + cross-run), SSE streaming, metrics/traces/artifacts, runtime metadata + policy registry, webhooks, audit, admin.
Under the observer-only control-plane model, agents emit envelopes (messages, signals,
context updates) directly to the runtime via macp-sdk-python / macp-sdk-typescript.
The UI only reads from the CP; it never originates agent traffic. See
control-plane/docs/ARCHITECTURE.md § Request Flow
for the authority model and
examples-service/docs/direct-agent-auth.md
for how examples-service spawns those agents.
Proxy routes
The browser never calls upstream services directly. Two Next.js route handlers forward requests, inject auth, and keep secrets server-side.
| Route | File | Purpose |
|---|---|---|
/api/proxy/[service]/[...path] | app/api/proxy/[service]/[...path]/route.ts | Generic forwarder for example and control-plane services. Injects auth, strips hop-by-hop headers, streams response body unchanged. |
/api/jaeger/[...path] | app/api/jaeger/[...path]/route.ts | Forwards to JAEGER_BASE_URL/api/*. Used by the trace detail surface to resolve span waterfalls. Returns 502 if Jaeger is unreachable. |
Supported upstream service identifiers ([service] segment): example, control-plane.
Environment variables
EXAMPLE_SERVICE_BASE_URL=http://localhost:3000
EXAMPLE_SERVICE_API_KEY=
CONTROL_PLANE_BASE_URL=http://localhost:3001
CONTROL_PLANE_API_KEY=
JAEGER_BASE_URL=http://localhost:16686 # server-side (proxy target)
NEXT_PUBLIC_JAEGER_BASE_URL=http://localhost:16686 # client-side (UI deep links)
lib/server/integrations.ts throws when EXAMPLE_SERVICE_BASE_URL / CONTROL_PLANE_BASE_URL
are missing in production; empty API keys log a warning but do not block requests.
Auth forwarding
- Examples Service — the proxy adds
x-api-key: <EXAMPLE_SERVICE_API_KEY>when configured. - Control Plane — the proxy adds
authorization: Bearer <CONTROL_PLANE_API_KEY>when configured.
Headers host, connection, content-length are stripped before forwarding;
content-encoding is stripped on the response. Every proxied response carries
x-macp-ui-proxy: <service> for observability.
Examples Service endpoints used by the UI
Full schemas: examples-service/docs/api-reference.md.
The UI calls the following subset:
Scenario discovery
GET /packs— pack listing for the catalogGET /packs/:packSlug/scenarios— scenarios within a packGET /scenarios— cross-pack listing; each row includespackSlug,policyVersion,policyHints
Agent profiles
GET /agents— all agent profiles with pre-computed scenario coverage;metricsare best-effort (zero when CP is unavailable). The UI enriches latency / confidence client-side from CP/dashboard/agents/metrics.GET /agents/:agentRef— single profile; the client returnsundefinedon 404 and rethrows other errors
Launch setup
GET /packs/:packSlug/scenarios/:scenarioSlug/versions/:version/launch-schema?template=— drives the schema-driven launch form;launchSummary.policyHintsfeeds the policy badge and launch previewPOST /launch/compile— validates inputs against the scenario JSON Schema and returns a whitelisted-saferunDescriptor(for CPPOST /runs), ascenarioSpec(for agent bootstrap), and a pre-allocated UUID v4sessionId
Optional one-shot bootstrap
POST /examples/run— compiles, spawns the example agents with per-agent JWTs (minted via auth-service), and optionally submits to the CP. Response:{ compiled, hostedAgents[], sessionId }. Under observer-only CP,sessionIddoubles as the run ID for navigation.
Control Plane endpoints used by the UI
Full schemas: control-plane/docs/API.md.
The UI calls the following subset. Each bullet below documents what the UI sends
and expects — for the authoritative endpoint contract, follow the links.
Dashboard
GET /dashboard/overview— aggregated KPIs,recentRuns,runtimeHealth, chart series. The UI sendswindow/from+to/scenarioRef/environment. KPIs read:totalRuns,activeRuns,completedRuns,failedRuns,cancelledRuns,totalSignals,totalTokens,totalCostUsd,avgDurationMs. The UI consumesrecentRunsdirectly and only falls back toGET /runswhen an older CP build omits the field. Chart series the UI renders are listed under Chart series below.GET /dashboard/agents/metrics— per-agentruns/signals/messages/averageConfidence/averageLatencyMs(optional). CP returnsparticipantId; the client normalizes toagentRefbefore merging into the Examples Service agent profiles.
Run lifecycle
POST /runs/validate— the UI composesValidateRunResponse { ok, errors, warnings, runtime }from CP's{ valid, errors, warnings, runtime }by mappingvalid && errors.length === 0→ok.POST /runs— only run-creation path. The UI posts whitelisted-safe compiled descriptors from the Examples Service. Scenario-specific fields are rejected with 400 by CP.GET /runs— paginated{ data, total, limit, offset }. The UI always sendslimit/offsetdefaults (required by CP validation) plus the active filters (status,environment,search,tags,scenarioRef,sortBy,sortOrder,createdAfter,createdBefore,includeArchived).GET /runs/:id— run record; flatsourceKind/sourceRefare nested intosource: { kind, ref }bynormalizeRun().POST /runs/:id/cancel— opaque to the UI; CP chooses between the initiator's cancel-callback (default) and directCancelSession(policy-delegated).POST /runs/:id/clone— accepts optional{ tags, context }. Non-emptycontextoverrides are rejected by CP under observer-only rules; the clone form surfaces the error directly.POST /runs/:id/archive— full run record; the client extracts{ ok, runId, archived }. CP's dedicatedarchivedAtcolumn is passed through unchanged (no tag-synthesis bridge).POST /runs/:id/replay— returns a replay descriptor ({ runId, mode, speed, streamUrl, stateUrl }).POST /runs/compare— pairwise comparison.DELETE /runs/:id— permanent delete; only available for terminal runs.
Run state and streaming
GET /runs/:id/state— the projection. The UI consumes:runblock (+contextId,extensionKeys),participants,graph,decision.current(incl.proposals[],resolvedAt,resolvedBy,prompt,outcomePositive: boolean | null),signals,progress,timeline,trace,outboundMessages, optionalpolicy(+expectedCommitments,voteTally,quorumStatus), optionalllm({ calls[], totals }).GET /runs/:id/events— dual-shape response: bareCanonicalEvent[]on the fast path;{ data, total, limit, nextCursor }when any ofafterTs/beforeTs/typeis supplied. The client handles both and pipes every row throughnormalizeEvent().GET /events— cross-run stream for/logs. When CP returns 404 (older build), the client falls back to per-run fan-out and caches the decision for the browser session so later navigations skip the probe.GET /runs/:id/stream— SSE withincludeSnapshot=true&afterSeq=<n>. Named events:snapshot,canonical_event,heartbeat.GET /runs/:id/replay/state?seq=<n>— state projection at a specific sequence; powers the timeline scrubber.GET /runs/:id/export— full run bundle; query:includeCanonical,includeRaw,eventLimit,format(json | jsonl).
Session interaction (observer-only)
Under direct-agent-auth, agents emit envelopes directly to the runtime via the SDKs. The
HTTP bypass endpoints return 410 Gone and the UI does not render forms for them:
— agents usePOST /runs/:id/messagesDecisionSession(client).evaluate(...)orsession.send(...)(python-sdk, typescript-sdk)— agents usePOST /runs/:id/signalsession.signal(...)via the SDK— agents construct aPOST /runs/:id/contextContextUpdateenvelope via SDK helpers
Still supported (scenario-agnostic, CP-local):
POST /runs/:id/artifacts— create an artifact ({ kind, label, uri?, inline? })POST /runs/:id/projection/rebuild— admin: rebuild projection from events
Batch operations
POST /runs/batch/cancel,POST /runs/batch/archive,POST /runs/batch/delete—{ runIds: string[] }→{ results: [{ runId, ok }] }POST /runs/batch/export— returnsRunExportBundle[]
Observability and artifacts
GET /runs/:id/metrics— includespromptTokens,completionTokens,totalTokens,estimatedCostUsd. Cost is derived fromMODEL_COSTSon the CP side (see CP API.md § Token usage convention).GET /runs/:id/traces— summary (traceId,spanCount,linkedArtifacts,runStatus,scenarioRef).GET /runs/:id/artifactsGET /metrics— raw Prometheus exposition. The/observabilitypage parses it client-side vialib/utils/prometheus.ts(counters, gauges, histograms, summaries; percentile interpolation matches Grafana'shistogram_quantile).GET /audit— supportsactor,action,resource,resourceId,createdAfter,createdBefore,limit,offset. Default paging islimit=100&offset=0.
Runtime metadata (pass-through from the runtime)
GET /runtime/manifest,GET /runtime/modes,GET /runtime/roots,GET /runtime/health
Runtime-level semantics (what a "mode" is, what's in a manifest) are documented in the
runtime repo: runtime/docs/modes.md
and runtime/docs/API.md.
Runtime policy registry (RFC-MACP-0012, pass-through)
GET /runtime/policies?mode=<modeId>— filterable listGET /runtime/policies/:policyIdPOST /runtime/policies—{ policyId, mode, description, rules, schemaVersion? }DELETE /runtime/policies/:policyId
Rule schemas are opaque to the control plane; the UI renders them descriptively. The
authoritative per-mode schema lives in runtime/docs/policy.md.
Operational admin
GET /webhooks— subscriptions may includedeliveryStats(total,succeeded,failed,lastDeliveredAt)POST /webhooks,PATCH /webhooks/:id,DELETE /webhooks/:idPOST /admin/circuit-breaker/resetGET /admin/circuit-breaker/history?window=<alias>— state transitions (CLOSED | OPEN | HALF_OPEN) with enter timestamps and optional reasonGET /readyz—{ ok, database, runtime, streamConsumer, circuitBreaker }
Chart series
GET /dashboard/overview returns { labels, data } pairs; the client converts them to
UI ChartPoint[]. The series the UI renders:
runVolume, latency, errorClasses, signalVolume, throughput, queueDepth,
latencyP50 / P95 / P99, cost, successRate, decisionOutcome (single net series —
positive vs. negative encoded as +1/-1 per bucket, not split into two arrays),
perScenario.
Series semantics are documented in
control-plane/docs/API.md § GET /dashboard/overview.
Jaeger integration
The /traces surface resolves span waterfalls through a Jaeger instance when
configured:
- Server-side fetch:
GET /api/jaeger/traces/:traceId→${JAEGER_BASE_URL}/api/traces/:traceId. Used bygetJaegerTrace(traceId). - Client-side deep link:
getJaegerUiUrl(traceId)builds a URL fromNEXT_PUBLIC_JAEGER_BASE_URL, falling back towindow.location.originwith port swapped to16686.
Client-side integration functions
All UI-facing data access lives in lib/api/client.ts (~1180 lines). Every function
branches on NEXT_PUBLIC_MACP_UI_DEMO_MODE — demo returns mock data, real hits the
proxy.
Examples Service
listPacks,listScenariosgetLaunchSchema,compileLaunchrunExample— one-shot bootstrap via/examples/rungetAgentProfiles,getAgentProfile(returnsundefinedon 404)
Control Plane — run lifecycle
validateRun,createRunlistRuns— acceptsPartial<ListRunsQuery>, always sendslimit/offsetgetRun,cancelRun,cloneRun(accepts{ tags, context }),archiveRuncreateReplay,compareRuns,deleteRun,exportRunBundle
Control Plane — state, events, streaming
getRunStategetRunEvents— acceptsRunEventsQuery(limit,afterSeq,afterTs,beforeTs,type); legacy positional signature preservedlistEvents— cross-run wrapper with 404-fallback to per-run fan-outgetTimelineFrame—/runs/:id/replay/state?seq=<n>
Control Plane — observability
getRunMetrics,getRunTraces,getRunArtifacts,createArtifactgetObservabilityRawMetrics— streams raw/metricsexposition (no JSON parsing)getJaegerTrace,getJaegerUiUrlgetLogsData,getTraceData— convenience wrappers for the/logsand/tracespages
Control Plane — dashboard, audit, agents
getDashboardOverview— acceptsDashboardOverviewQuery; returnsdegraded: truewhen CP's/dashboard/overviewis unavailablegetAuditLogs—Partial<ListAuditQuery>getAgentMetrics— logs a warning and returns[]when CP is missing the endpoint
Control Plane — runtime and policies
getRuntimeManifest,getRuntimeModes,getRuntimeRoots,getRuntimeHealthlistRuntimePolicies,getRuntimePolicy,registerRuntimePolicy,unregisterRuntimePolicy
Control Plane — admin
getWebhooks,createWebhook,updateWebhook,deleteWebhookresetCircuitBreaker,getCircuitBreakerHistorygetReadinessProbe,rebuildProjectionbatchCancelRuns,batchArchiveRuns,batchDeleteRuns,batchExportRuns
Utility helpers (no network I/O)
getMockFrames— demo-mode replay frame sourcegetQuickCompareTarget— suggests a comparison target runlistScenarioRefs— all scenario refs from mock data
Response normalization
lib/api/client.ts bridges CP response shapes to UI types so the render layer sees a
consistent type vocabulary regardless of whether rows came from CP or from mock data.
normalizeRun()— maps flatsourceKind/sourceRefinto nestedsource: { kind, ref }; validatesid,status,runtimeKind; passesarchivedAtthrough unchanged from CP.normalizeEvent()— maps flatsourceKind/sourceName/subjectKind/subjectId/rawTypeinto nestedsourceandsubjectobjects. Applied bygetRunEvents,listEvents, and the SSEcanonical_eventhandler.- Pagination unwrapping —
GET /runsreturns{ data, total, limit, offset };listRunsunwraps.data. - Validate response mapping —
validateRuncomposesValidateRunResponsefrom CP's{ valid, errors, warnings, runtime }. - Cancel / archive envelope mapping — CP returns the full updated
RunRecord; the client extracts{ ok, runId, status }/{ ok, runId, archived }. - Dashboard chart conversion — CP's
{ labels, data }is converted to UIChartPoint[]. - Agent metrics field mapping — CP's
participantIdbecomes the UI'sagentRefbefore merging with Examples Service profiles. /eventsendpoint absence —listEventscaches aneventsEndpointMissingflag after a single 404 so older CP builds only get probed once per browser session.
Error handling
lib/api/fetcher.ts exports ApiError with status, statusText, service, path,
and an isNotFound getter. Client functions branch on ApiError.isNotFound to return
undefined (missing entity) or to mark a capability as degraded
(getDashboardOverview, listEvents fallback, getAgentMetrics). Non-404 errors
propagate and are caught by React Query / error boundaries.
Demo mode
When NEXT_PUBLIC_MACP_UI_DEMO_MODE=true, every client function short-circuits to mock
data from lib/data/mock-data.ts (~2000 lines). This keeps the entire product surface
exercisable with no backend. Live-run streaming is simulated with 1600ms frame ticks
over MOCK_RUN_FRAMES.
SSE integration
Live execution uses:
GET /api/proxy/control-plane/runs/:id/stream?includeSnapshot=true&afterSeq=<n>
lib/hooks/use-live-run.ts manages the subscription:
- Named events handled:
snapshot,canonical_event,heartbeat. - Auto-reconnect with exponential backoff (max 8 attempts).
- Heartbeat timeout detection (45s) — silent connections are treated as failed.
- Bounded event buffer (500 events); event IDs deduped.
- Incoming
canonical_eventpayloads run throughnormalizeEventbefore being appended. - Connection state surfaced to the UI:
idle | connecting | live | reconnecting | ended | error.
The CP-side stream contract (passive-subscribe frame, replay-from-afterSeq, heartbeat
cadence) is documented in
control-plane/docs/API.md § SSE Streaming
and control-plane/docs/INTEGRATION.md § Consuming SSE Streams.
Run launch sequence in the UI
Standard flow
- Load launch schema (Examples Service)
- Compile with Examples Service (
POST /launch/compile) — returns whitelisted-saferunDescriptor+ pre-allocatedsessionId - Validate with Control Plane (
POST /runs/validate) - Submit to Control Plane (
POST /runs) - Redirect to the live workbench at
/runs/live/[runId]
One-shot bootstrap flow
- Call Examples Service
POST /examples/run - Examples Service compiles, mints per-agent JWTs, spawns worker processes with bootstrap files, and optionally submits the run to the CP (see
examples-service/docs/direct-agent-auth.md) - UI redirects to the returned
sessionId(= run ID under observer-only CP)