mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-06 06:24:12 +00:00
revisão e complementação da documentação do projeto
This commit is contained in:
90
RI/docs/interfaces/mcp-gateway.en-us.md
Normal file
90
RI/docs/interfaces/mcp-gateway.en-us.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Interface: MCP Gateway
|
||||
|
||||
The Gateway's MCP surface — what any integrating MCP Client calls. Implementation:
|
||||
[`src/emcp_bus/gateway/server.py`](../../src/emcp_bus/gateway/server.py),
|
||||
[`gateway/canonical_request.py`](../../src/emcp_bus/gateway/canonical_request.py),
|
||||
[`gateway/cache.py`](../../src/emcp_bus/gateway/cache.py). See [`AS-BUILT.md`](../AS-BUILT.md#pep--mcp-gateway-ep-05)
|
||||
for the component's role in the architecture.
|
||||
|
||||
## Authentication
|
||||
|
||||
OAuth2/OIDC bearer token in the `Authorization` header. Validated against the JWKS of the configured
|
||||
IdP (`OIDC_JWKS_URL`/`OIDC_ISSUER`/`OIDC_AUDIENCE`) — RS256 signature, `exp`, `iss`, `aud`. The
|
||||
trusted identity is the token's `azp` claim (or, in its absence, `client_id`) — never a
|
||||
`clientInfo` field or any part of the request body.
|
||||
|
||||
## Two deployment modes
|
||||
|
||||
| Mode | Condition | Behavior |
|
||||
|---|---|---|
|
||||
| With OIDC configured | `OIDC_JWKS_URL` + `OIDC_ISSUER` set | The `mcp` SDK's auth middleware rejects any connection without a valid bearer token before the `initialize` handshake. |
|
||||
| Without OIDC configured | Env vars absent (default) | No auth middleware installed; every handler runs with `get_access_token() is None` and denies by default. |
|
||||
|
||||
Both modes are deny-by-default; neither is an "incomplete mode" waiting on the other — see
|
||||
`tests/e2e/test_governed_gateway.py`.
|
||||
|
||||
## `tools/list`
|
||||
|
||||
Without a valid token: returns `tools: []`. With a valid token: returns the intersection of the
|
||||
entitled backends' catalog (`CapabilityRouter.distinct_backends_for`) with the client's
|
||||
`MaximumEntitlement` (`EntitlementManager.resolve(...).allowed_tools`) — never any backend's full
|
||||
catalog. Each call re-evaluates entitlement from scratch; there is no server-side catalog cache
|
||||
between clients.
|
||||
|
||||
**Cache hint (SEP-2549):** the response carries `ttl_ms`/`cache_scope`, always
|
||||
`cache_scope="private"` — never `"public"`, since the result depends on the caller's authorization
|
||||
(`gateway/cache.py:tools_list_cache_hint`). **Verified limitation**: on the classic session
|
||||
transport this RI uses everywhere, the SDK drops `ttl_ms` during serialization (the field is only
|
||||
delivered on the wire by the protocol's 2026-07-28 *stateless per-request* mode) — the client
|
||||
always sees the default (`ttl_ms=0`), which is the most conservative fallback possible, never an
|
||||
unsafe one. The actual security property (never leaking a catalog between clients) does not
|
||||
depend on this — it comes from the Gateway recomputing entitlement on every call.
|
||||
|
||||
## `tools/call`
|
||||
|
||||
Reauthorized completely independently of `tools/list` — the prior list is never consulted to
|
||||
decide whether a call is allowed. Flow:
|
||||
|
||||
1. No valid token → deny (see error table below).
|
||||
2. Unknown client (`ClientRegistration` not found) → deny.
|
||||
3. PDP consulted (`PDPClient.evaluate`, see [`pdp-opa.md`](pdp-opa.md)) with `client_profile`,
|
||||
`tool`, `arguments`.
|
||||
4. `DENY` → deny with the PDP's `reason_code`.
|
||||
5. `REQUIRE_APPROVAL` → consults `ApprovalService.check_and_consume`; without a granted approval,
|
||||
creates an `ApprovalRequest` (see [`approval-workflow.md`](approval-workflow.md)) and denies.
|
||||
6. `ALLOW` → resolves the backend via `CapabilityRouter.route` (see
|
||||
[`registry-capability-manifest.md`](registry-capability-manifest.md)) and executes the call
|
||||
against the domain server, using the outbound credential (never the client's inbound token — see
|
||||
[`downstream-identity.md`](downstream-identity.md)).
|
||||
|
||||
### Denial format
|
||||
|
||||
Every denial is a structured `CallToolResult` (`is_error: true`), never a protocol error:
|
||||
|
||||
```json
|
||||
{"is_error": true, "content": [{"type": "text", "text": "Access denied: <REASON_CODE>"}]}
|
||||
```
|
||||
|
||||
| `REASON_CODE` | When |
|
||||
|---|---|
|
||||
| `UNAUTHENTICATED` | No valid token. |
|
||||
| `UNKNOWN_CLIENT` | Valid token, but `client_id` without a `ClientRegistration`. |
|
||||
| `PDP_UNAVAILABLE` | OPA unreachable — fail-closed, never an implicit ALLOW. |
|
||||
| `NOT_IN_ENTITLEMENT` | The tool is not in `ClientProfile.allowed_tools` (PDP decision). |
|
||||
| `TRANSACTION_POLICY_DENIED` | Entitlement OK, but the arguments violate `transaction.rego` (limit, region, classification). |
|
||||
| `REQUIRE_APPROVAL:<reason>:<approval_id>` | Above the `approval_threshold` — use `approval_id` to track/grant out of band. |
|
||||
| `UNROUTABLE_CAPABILITY` | No `ACTIVE` manifest for the tool, or the backend is not resolvable. |
|
||||
| `UNSUPPORTED_BACKEND_TYPE:<type>` | The manifest points to a `backend.type` with no live dispatch in this RI (e.g. `rest`, see `AS-BUILT.md`). |
|
||||
|
||||
## Header/body consistency (EP-05-T08)
|
||||
|
||||
If the request carries the `Mcp-Method`/`Mcp-Name` headers (SEP-2243), they must agree with the
|
||||
`method`/resource name in the JSON-RPC body, or the request is rejected with HTTP 400
|
||||
(`{"error": "header_body_mismatch", "detail": "..."}`) before reaching authentication. Missing
|
||||
headers are not an error — it's a consistency check, not a mandate to adopt the SEP.
|
||||
|
||||
## Correlation
|
||||
|
||||
Every decision carries, and propagates as OTel span attributes and in every audit event:
|
||||
`decision_id`, `policy_decision_id`, `entitlement_version`, `policy_version`, `mcp_request_id`.
|
||||
See [`audit-events.md`](audit-events.md).
|
||||
Reference in New Issue
Block a user