27 KiB
AS-BUILT — Enterprise MCP Service Bus (RI)
Technical reference for what exists today in the Reference Implementation (RI): the
responsibility of each component, where the code lives, high-level input/output contracts (the
fine-grained detail of each contract is in interfaces/), what was validated and
against which real dependency, and the real architectural limitations found during
implementation.
Target audience: those who will integrate with or extend the RI. For those who will run
the RI, see How-To.md. For those who will evaluate/adapt the RI for a real
scenario, see Assumptions.md and
Production-Recommendations.md.
This document succeeds the planning backlog docs/RI-PLANNING.md (removed from the repository
after the completion of milestone M4, with history preserved in git) — the factual information
from that backlog that is still relevant to whoever uses the finished product was migrated here,
to Assumptions.md, and to Production-Recommendations.md.
Status
The entire planned backlog (milestones M0 through M4) is implemented, tested, and validated.
261 automated tests (count verified via pytest --collect-only in this revision): 254 pass
with pytest --forked (unit + e2e + contract + adversarial), plus 7 security acceptance tests
(tests/e2e/test_security_acceptance.py) that require real Docker and are run separately due to
the build+Keycloak cost per run. Production logic with no mocks in the security and
orchestration core: real OPA (subprocess), real Keycloak (Docker), real LangGraph with SQLite
checkpointer, a real LLM (OpenAI-compatible endpoint), real HTTP JWKS (a lightweight stand-in for
the rest of the e2e suite) with real RS256 JWTs, real YAML from config/, real SQLite for the
Registry.
Epic index
Every EP-XX-TXX that appears in comments/docstrings in the source code references one of these
epics. This table is what replaces docs/RI-PLANNING.md §8.6 as the context source for those
references.
| Epic | Title | Main module/test |
|---|---|---|
| EP-00 | RI bootstrap and tooling | pyproject.toml, Makefile, .github/workflows/ci.yml, src/emcp_bus/common/config.py |
| EP-01 | MCP Client identity and authentication | src/emcp_bus/identity/*.py, deploy/keycloak/realm-export.json |
| EP-02 | Global Capability Registry and publishing pipeline | src/emcp_bus/registry/*.py |
| EP-03 | PDP / Policy Engine (OPA) | src/emcp_bus/pdp/*.py, config/policies/*.rego |
| EP-04 | Entitlement Manager | src/emcp_bus/entitlement/*.py |
| EP-05 | MCP Gateway / PEP | src/emcp_bus/gateway/*.py |
| EP-06 | Enterprise MCP Fabric and example domain servers | src/emcp_bus/fabric/*.py, services/example_mcp_servers/* |
| EP-07 | Downstream identity (inbound vs outbound) | src/emcp_bus/downstream/*.py |
| EP-08 | Auditing, correlation and observability | src/emcp_bus/audit/*.py, src/emcp_bus/common/otel.py |
| EP-09 | Profile Intelligence (generic contract) | src/emcp_bus/profile_intelligence/*.py |
| EP-10 | Offering Filter | src/emcp_bus/offering_filter/service.py |
| EP-11 | Service Orchestrator (NBA/NBO) | src/emcp_bus/orchestrator/*.py |
| EP-12 | Agent Runtime and channel adapters | src/emcp_bus/agent_runtime/*.py |
| EP-13 | Example agent and Langfuse instrumentation | agents/example_agent/* |
| EP-14 | Human-in-the-loop / Approval workflow | src/emcp_bus/approval/*.py |
| EP-15 | Security and adversarial tests | tests/contract/, tests/e2e/test_security_acceptance.py, tests/adversarial/, tests/e2e/test_fail_closed.py |
| EP-16 | Final deployment and documentation | docker-compose.yml, docs/adr/ |
Within each epic, TXX numbers the tasks (e.g., EP-05-T03 = third task of epic EP-05/Gateway —
the tools/list cache hint). The ADRs referenced by number (ADR-XXX) are individual files in
../../docs/adr/.
Components
MCP Client identity (EP-01)
Responsibility: authenticate the workload identity making the call, never the
self-declared identity in clientInfo or in any payload field. src/emcp_bus/identity/authn.py
(TokenValidator) validates an OAuth2/OIDC bearer token against the IdP's JWKS (RS256
signature, exp, iss, aud); the trusted client_id comes exclusively from the azp/
client_id claim of the verified token. src/emcp_bus/identity/models.py
(ClientRegistration) maps that client_id to exactly one ClientProfile (EP-04) via YAML in
config/clients/*.yaml. shared_client_lint.py (EP-01-T05) validates in CI that two distinct
agents never share the same client_id under different profiles.
Validated against: real Keycloak 26.7.3 via Docker (tests/e2e/test_real_keycloak.py) — the
emcp realm imported from deploy/keycloak/realm-export.json, 3 clients (sales-read-agent,
sales-write-agent, finance-payments-agent) with client_credentials grant; and a lighter
local JWKS HTTP server as a stand-in for the rest of the e2e suite (valid for validation logic,
not for real protocol integration with the IdP).
Production extension documented, not implemented: mTLS/workload identity —
docs/adr/ADR-019-identity-extension.md.
PDP / Policy Engine (EP-03)
Responsibility: decide ALLOW/DENY/REQUIRE_APPROVAL deterministically and auditably.
Two-step model (src/emcp_bus/pdp/client.py, PDPClient.evaluate): (1) entitlement.rego
decides whether the tool is in ClientProfile.allowed_tools — DENY here is definitive, no
argument is evaluated; (2) transaction.rego restricts by argument/resource (value limit,
region, client classification) and can downgrade an ALLOW to REQUIRE_APPROVAL above
approval_threshold. Every response is a PDPDecision (outcome, policy_version,
reason_code) — never a boolean. policy_version is a SHA-256 hash (12 chars) of the content of
the .rego files on disk, computed once at Gateway boot.
Validated against: real OPA via subprocess (opa run --server binary), 9 Rego tests
(opa test) + 6 Python integration tests.
Global Capability Registry (EP-02)
Responsibility: a control-plane asset with its own authority and lifecycle over the
capability catalog, separate from the execution view that the Gateway exposes to each client.
registry/models.py defines CapabilityManifest (canonical name <domain>.<capability>,
risk_tier R0–R4, data_classification, side_effects, idempotent, entitlements,
backend, approval, observability, lifecycle). registry/pipeline.py
(PublishingPipeline) is the sole entry path: it validates business gates (owner required,
backend declared, tiers R3/R4 require approval.required=true) and advances
NOMINATE → REVIEW → REGISTER; a separate publish() advances REGISTER → ACTIVE, when the
capability becomes resolvable by the Fabric. registry/lifecycle.py mechanically ensures that
nothing reaches ACTIVE without passing through REVIEW. registry/store.py persists to
SQLite; registry/seed.py populates the Registry from config/capabilities/*.yaml at Gateway
boot (one temporary file per process, not shared across runs).
Validated against: real SQLite, 9 example manifests (6 Sales + 3 Finance) going through the full pipeline without manual intervention.
PEP / MCP Gateway (EP-05)
Responsibility: the single Policy Enforcement Point. src/emcp_bus/gateway/server.py
implements two invariants: tools/list returns the intersection of the entitled backends'
catalog with the authenticated client's MaximumEntitlement (filtered discovery); tools/call
is reauthorized completely independently of what tools/list returned — the PDP is consulted on
every call. Without a valid token: tools/list empty, tools/call always Access denied: UNAUTHENTICATED. An unreachable PDP is DENY (PDP_UNAVAILABLE), never implicit ALLOW. An
AuditSink failure never becomes a 500 error on an already-decided operation (_emit_audit,
swallows exceptions at all 8 emission points). Routing by capability is resolved on every call
via CapabilityRouter (EP-06) against the Registry — a client can see tools from more than one
backend (e.g., Sales + Finance). See interfaces/mcp-gateway.md for
the full contract (headers, denial codes, cache hint).
Validated against: real OPA, real Keycloak (Docker), real RS256 JWTs, and a real host MCP
client against the containerized Gateway (docker compose --profile core [--profile identity] up).
Enterprise MCP Fabric and example domain servers (EP-06)
Responsibility: src/emcp_bus/fabric/router.py (CapabilityRouter) resolves a
<domain>.<capability> tool name to the backend that serves it, using only ACTIVE manifests
from the Registry — never a static mapping in the Gateway. services/example_mcp_servers/sales_domain/
(6 tools, R1+R2) and finance_domain/ (3 tools: invoice.get R1, payment.create R2,
payment.execute R3 with a full approval cycle) are the two example domains, protected by
BackendCredentialGate (EP-05-T07) — a request without the Gateway's outbound credential never
reaches a tool handler. fabric/adapters/rest_adapter.py (EP-06-T04) implements a generic REST
adapter, tested standalone (httpx.MockTransport); no seed manifest uses
backend.type: "rest", so there is no live path through the Gateway exercising it.
Validated against: real routing Sales→sales-domain / Finance→finance-domain never crossed, including cross-container via real Docker Compose (network segmentation confirmed: none of the domain servers or OPA publishes a host port).
Downstream / outbound identity (EP-07)
Responsibility: the credential the Fabric presents to the backend is never the inbound token
of the MCP Client (src/emcp_bus/downstream/identity.py, BackendCredentialProvider). Two modes
declared per backend in config/backends/*.yaml (BackendConfig.credential_mode):
service_account (static credential read from an env var at call time — the default mode of the
seed backends) and token_exchange (RFC 8693 via Keycloak Standard Token Exchange,
TokenExchangeClient — two HTTP round-trips per new exchange, with a per-backend cache until
shortly before expires_in). See interfaces/downstream-identity.md.
Validated against: real Keycloak 26.7.3 — the exchange's audience must be an actually
registered client id (a free-form string fails with "Audience not found"); requesting audience
without a scope that effectively adds that audience fails with "Requested audience not
available". The realm gained the gateway-token-exchange client and per-backend client scopes
with oidc-audience-mapper. 7 tests (4 unit with httpx.MockTransport, 3 e2e against real
Keycloak, including a full tools/call through the Gateway using a credential obtained via token
exchange).
Auditing, correlation and observability (EP-08)
Responsibility: audit/models.py defines EventEnvelope with a 3-category taxonomy adopted
from the Agent Platform OCI reference (ADR-022): IC (Client Interaction/business journey,
e.g. client_authenticated, tool_call_allowed), NOC (operational/availability error, e.g.
pdp_unavailable), GRL (guardrail/governance, e.g. tool_call_denied,
tools_list_filtered, bypass_attempt_detected). Every event recursively sanitizes any key
whose name contains token/secret/password/authorization/credential, replacing the value
with sha256:<hex> — a secret value never reaches a sink. audit/correlation.py propagates
decision_id/policy_decision_id/entitlement_version/policy_version/mcp_request_id as
first-class fields on every event and as OTel span attributes (ADR-017). audit/sink.py
(JSONLFileAuditSink) writes to an append-only JSONL file. common/otel.py installs automatic
tracing (OpenTelemetryMiddleware) with no manual span code in the Gateway. See
interfaces/audit-events.md.
Validated against: real trace_id correlation between distinct Docker containers (Gateway ↔
domain server) for a successful call. Local otel-collector + Jaeger stack (EP-08-T04,
observability profile): configuration validated (docker compose config), not exercised with
real OTLP traffic by an automated test — Langfuse (EP-13-T02) is the one that actually received
real OTLP traffic, validated manually (see the "Example agent" component below).
Entitlement Manager (EP-04)
Responsibility: entitlement/manager.py (EntitlementManager) resolves the
MaximumEntitlement by combining the ClientProfile (validated YAML, entitlement/models.py)
with the PDP's entitlement decision, producing a versioned ResolvedEntitlement
(entitlement_version) with allowed_tools/risk_ceiling/profile_name.
sync_profiles_to_pdp pushes the validated profiles to OPA's data document at boot — a failure
here prevents the Gateway from coming up "healthy" (fail-closed, README.md §38). Revocation
invalidates the entitlement cache and re-evaluates starting from the next call.
Validated against: real OPA + real YAML; live policy revocation against a running Gateway
(tests/adversarial/test_protocol_identity.py).
Approval workflow (EP-14)
Responsibility: approval/service.py (ApprovalService) gates REQUIRE_APPROVAL
decisions: check_and_consume (called by the Gateway on every attempt) only returns true for an
approval that has been granted, is not expired, and has not yet been consumed — single-use,
never "queue and release". An ApprovalRequest is bound to a SHA-256 hash of the exact triple
(client_profile, tool, arguments) (compute_operation_hash), not just the tool name: an
approval for one call never satisfies a different call with the same tool/profile.
check_and_consume is protected by a threading.Lock (a real concurrency finding, see
"Limitations" below). See interfaces/approval-workflow.md.
Validated against: full e2e cycle for Sales and Finance (deny → approve externally → execute
→ replay denied); replay under real concurrency (ThreadPoolExecutor, 20 threads).
Profile Intelligence (EP-09)
Responsibility: profile_intelligence/models.py defines ProfileView (pseudonymized
subject_ref, profile_version, segments, attributes, reason_codes, expires_at) as a
generic, replaceable contract — any ProfileIntelligenceProvider (the deterministic rule-based
stub in rule_based_provider.py, or a real clustering model in a production deployment) produces
the same contract. A ProfileView is never consulted by the PDP and can never grant a capability
outside the MaximumEntitlement — nothing here is connected to the authorization path.
governance.py implements governance guardrails: pseudonymization, lineage logging, a drift
monitor stub.
Validated against: determinism (same subject_ref + ISO window → same profileVersion).
Offering Filter (EP-10)
Responsibility: offering_filter/service.py implements
FilteredOfferings = ActiveCatalog ∩ MaximumEntitlement ∩ ConsentContext. The order of the
intersection is deliberate: allowed_tools (MaximumEntitlement, already resolved by EP-04) is
the only term treated as security-relevant and is applied first; a ConsentContext (here, just
allow_restricted_data over CapabilityManifest.data_classification) can only reduce what has
already survived that intersection, never reintroduce a capability outside the entitlement.
Validated against: subset invariant proven with Hypothesis, 2000 generated cases
(tests/unit/test_offering_filter_invariants.py).
Service Orchestrator (EP-11)
Responsibility: orchestrator/graph.py (build_journey_graph) is a real LangGraph
StateGraph with fixed nodes resolve_profile → resolve_entitlement → filter_offerings → decide_nba → approval_gate, plus a dedicated re-entry for the recalculation loop after DENY
(handle_denial_node, which never calls the Fabric directly — it only recalculates the NBA over
the remaining offerings). orchestrator/nba_model.py defines NBADecision
(decision_id, action, capability_version, subject_ref, channel, agent,
reason_codes, policy_context, expires_at) and the NBADecisionModel protocol — swapping
the reference implementation (decisioning.RuleBasedNBADecisionModel) for a ranking/AI model
changes nothing in the input/output contract nor in what EP-03/04/05 authorize, because the model
only orders/selects among DecisionContext.filtered_offerings, already intersected by EP-10
before reaching here. orchestrator/hitl_node.py uses langgraph.types.interrupt/
Command(resume=...) to genuinely pause the graph on approval — with no timeout that becomes an
implicit ALLOW.
Validated against: real OPA + real LangGraph + real SQLite checkpointer, including proof that resuming from a paused checkpoint does not reprocess already-executed nodes.
Agent Runtime (EP-12)
Responsibility: agent_runtime/dispatcher.py (AgentDispatcher) consumes an NBADecision
and dispatches it against the real Gateway, authenticated as the MCP Client already registered
for the target agent — never generating or storing its own credential (AgentTokenProvider,
typically a real client_credentials against the same IdP as any other MCP Client).
handoff_graph.py (EP-12-T03, execute_handoff/build_handoff_payload) implements handoff
between agents with context minimization and entitlement resolution always independent per agent
— see "Limitations" below for the finding that neither Mode A nor Mode B (EP-13-T05/T06) ended up
using this primitive. identity_resolver.py (EP-12-T04, IdentityResolver) normalizes field
names between channels (config/orchestrator/identity.yaml) and is never consulted by the PDP —
checked mechanically via AST analysis in the corresponding unit test, the same discipline
global_supervisor.py uses to prove it never imports emcp_bus.orchestrator.
Validated against: real Gateway + Keycloak — AgentDispatcher reaching the real Sales
backend and being denied on cross-domain; two real agents obtaining independent
client_credentials tokens.
Example agent (EP-13)
Responsibility: agents/example_agent/agent.py is a conversational agent with real
tool-calling against an OpenAI-compatible endpoint, whose catalog is restricted to the client's
own tools/list. common/pii_redaction.py redacts PII/secrets before any export.
prompts/store.py implements local prompt versioning (never overwrites a previous version) as
a testable core — synchronization with Langfuse's Prompt Management API is a thin adapter that
is documented, not implemented.
Validated against: a real LLM (meta.llama-3.3-70b-instruct via OCI Generative AI's
OpenAI-compatible endpoint), with tool-calling empirically confirmed; the prompt injection
scenario from README.md §29 (requesting a tool outside the entitlement) genuinely reproduced —
the call never executes, either because the model never saw the tool in its own catalog, or
because the Gateway/PDP denies it. Real self-hosted Langfuse (6 containers:
web/worker/postgres/clickhouse/redis/minio, deploy/langfuse/docker-compose.override.yml)
receiving real OTLP traffic from the agent — MCP spans and "generation" spans (with real
gen_ai.request.model/tokens) physically confirmed in ClickHouse's events_core table. This
validation is manual, not an automated e2e test (cost of bringing up 6 containers on every suite
run).
Mode A — external Global Supervisor (EP-13-T05, ADR-024)
Responsibility: agent_runtime/global_supervisor.py (GlobalSupervisor) is the deliberately
simple stand-in (keyword-based routing) for an external orchestration platform that decides
which backend agent handles a conversation — it never is, nor claims to be, the security
boundary; it never imports emcp_bus.orchestrator (checked via AST). The real security property
does not live in this module: it lies in the fact that each BackendAgent
(agents/example_agent/backends/{sales_agent,finance_agent}/) resolves its own MCP
Client/entitlement completely independently — its own token, tools/list, and reauthorization —
and the GlobalSupervisor never reads, stores, or passes a credential from one backend to
another.
Validated against: real LLM + real Gateway (tests/e2e/test_multi_agent_mode_a.py, 2 tests):
handoff from sales_agent to finance_agent with a completely distinct token; a prompt
injection via handoff attempting finance.payment.execute is still denied by the real
Gateway/PDP.
Mode B — Orchestrator as the sole NBA/NBO brain (EP-13-T06, could, ADR-024)
Responsibility: agents/example_agent/mode_b_orchestrator_driven/orchestrator.py
(ModeBOrchestrator) invokes the real EP-11-T02 journey StateGraph (unmodified) once per
relevant client_id for the same subject, combines the resulting NBADecisions (reference rule:
a pending Finance item always beats a discretionary Sales offer), and dispatches the winner via
the real AgentDispatcher — never creating its own authorization path. It deliberately does not
extend JourneyState/build_journey_graph to handle multiple client_ids natively; the
combination lives a layer above the graph (a design decision recorded in the module itself).
Validated against: real Keycloak + real OPA + real Sales/Finance
(tests/e2e/test_real_keycloak.py::test_mode_b_orchestrator_combines_offerings_and_dispatches_through_the_real_chain):
candidates from both domains genuinely combined, final decision from our Orchestrator, ALLOW
dispatch end-to-end.
What was validated, by real dependency
| Real dependency | What it exercises | Where |
|---|---|---|
| OPA (subprocess) | entitlement.rego/transaction.rego, PDPClient |
tests/unit/policies/, the whole e2e suite |
| Keycloak (Docker, 26.7.3) | client_credentials, real JWKS, RFC 8693 token exchange, Mode B | tests/e2e/test_real_keycloak.py |
| LangGraph 0.6 + SQLite checkpointer | journey graph, approval pause/resume | tests/unit/test_orchestrator_graph.py, M3 e2e |
| Real LLM (OpenAI-compatible) | example agent tool-calling, prompt injection | tests/e2e/test_example_agent.py, test_multi_agent_mode_a.py |
| Full Docker Compose | build+up+fail-closed+cross-container trace+network segmentation | tests/e2e/test_security_acceptance.py, documented manual validation |
| Self-hosted Langfuse (6 containers) | real OTLP export, generation spans | Manual validation (not automated) |
Limitations and real architectural findings
ttl_msof thetools/listcache hint (SEP-2549, EP-05-T03): correctly implemented ingateway/cache.py, but the field belongs to the 2026-07-28 protocol, which only exists in themcpSDK's stateless per-request mode (noinitializehandshake). This RI uses the classic session handshake everywhere (Gateway↔Client and Gateway↔backends), sottl_msnever reaches the wire in this architecture — the SDK drops the field during serialization for protocol versions before 2026-07-28 (verified empirically).cache_scope="private"is always set, but since it does not depend on the wire-mode it is also not something an e2e test can verify in a discriminating way. The real security property (never leaking the catalog between clients) is guaranteed independently: the Gateway never caches the filtered catalog server-side.- Header/body consistency (EP-05-T08): implemented as its own gate
(
gateway/canonical_request.py) reusing the SDK's field constants, not as the SDK's native validation ladder (which only applies to the same 2026-07-28 stateless per-request mode above) — for the same reason, this RI does not reach it via the native path. Missing headers (a client that does not implement this SEP) are not an error. - REST adapter (EP-06-T04): implemented and tested standalone
(
fabric/adapters/rest_adapter.py); no example backend in this RI is REST (both are MCP-native), so live routing through the Gateway to atype: "rest"backend was never exercised end-to-end. - Handoff between agents (EP-12-T03,
execute_handoff/build_handoff_payload): remains implemented and tested (tests/unit/test_agent_runtime_handoff.py), but neither Mode A (EP-13-T05) nor Mode B (EP-13-T06) ended up using this primitive. Mode A needs conversational routing (which backend responds to the next message), not dispatch of a specificNBADecision—GlobalSupervisorcallsrun_turn()directly on the backends. Mode B already combinesNBADecisions from multiple clients in a single step and dispatches the winner via plainAgentDispatcher.dispatch— there is no "origin" agent handing control to another forexecute_handoffto intercept. The primitive remains correct and reusable for a future handoff scenario within an Orchestrator-driven journey, but that specific usage form does not exist in this RI. - Request replay: no nonce/anti-replay in this RI — a captured request with a still-valid
token can be resent successfully. Mitigated by short-lived tokens
(
credential_policy.short_livedon every seedClientProfile) and by complete auditing (every attempt, even an identical one, generates its owndecision_id). Seetests/adversarial/test_protocol_identity.py. - Rate limiting / bulk exfiltration: no request rate limiting in the Gateway — N consecutive legitimate reads are all allowed. Mitigated only by after-the-fact detectability via auditing (every call generates a correlated event), never by real-time prevention.
- Real race condition fixed in
ApprovalService:check_and_consumedid a check-then-set in two non-atomic operations — under real thread concurrency, two attempts could both observe "granted" before either wrote "consumed" (double-spend of a single-use grant). Fixed with a dedicatedthreading.Lock, proven withThreadPoolExecutor(20 threads) intests/adversarial/test_protocol_identity.py. - Audit sink failure could become a real 500 error, fixed: none of the 8
audit_sink.emit(...)calls in the Gateway were protected — a sink failure (full disk, unavailable queue) could turn an already-ALLOWed operation into a 500 error for the caller, violating the policy of never blocking low-risk operations. Fixed with the_emit_audithelper (gateway/server.py), which swallows any exception from the sink at all 8 emission points, for both ALLOW and DENY. A sink failure in production needs its own observability (seeProduction-Recommendations.md) — this behavior is correct by design, but silent. GlobalSupervisorrouting (Mode A): simple keyword-based, deliberately not the security boundary nor a claim of routing quality — just an honest stand-in for what a real external platform would do.ModeBOrchestratorcombination rule (Mode B): "Finance always beats Sales" is a simple reference rule, not a claim of optimality — the same posture asRuleBasedNBADecisionModel(EP-11-T03).- Observability stack (EP-08-T04): configuration validated (
docker compose config), not exercised with real traffic by an automated test (the full startup with theobservabilityprofile was validated manually).