mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-06 06:24:12 +00:00
66 lines
3.2 KiB
Markdown
66 lines
3.2 KiB
Markdown
# Interface: MCP Client identity and registration (OIDC)
|
|
|
|
Contract for registering an MCP Client and the OIDC token it presents to the Gateway.
|
|
Implementation: [`src/emcp_bus/identity/models.py`](../../src/emcp_bus/identity/models.py),
|
|
[`identity/authn.py`](../../src/emcp_bus/identity/authn.py),
|
|
[`identity/shared_client_lint.py`](../../src/emcp_bus/identity/shared_client_lint.py). See
|
|
[`AS-BUILT.md`](../AS-BUILT.md#identidade-do-mcp-client-ep-01).
|
|
|
|
## Registering an MCP Client
|
|
|
|
One YAML file per client in `config/clients/*.yaml`, validated against `ClientRegistration`:
|
|
|
|
```yaml
|
|
client_id: finance-payments-agent # exactly the token's azp/client_id claim
|
|
profile: Finance.Payments # name of a ClientProfile under config/clients/profiles/
|
|
environment: production # "dev" | "ci" | "production"
|
|
description: "Finance agent authorized to create/execute payments (R3, approval above threshold)"
|
|
```
|
|
|
|
(real example: `config/clients/finance-payments-agent.yaml`)
|
|
|
|
`client_id` is the sole link between an authenticated identity and a `ClientProfile` — two
|
|
distinct `client_id`s never share a `profile` unless that is an explicit and auditable decision
|
|
(the shared-client rule, `identity/shared_client_lint.py`, wired into
|
|
`make validate-schemas`).
|
|
|
|
## Registering the client with the IdP
|
|
|
|
The client must exist in the IdP realm with the `client_credentials` grant enabled and an
|
|
audience mapper pointing at the Gateway's `OIDC_AUDIENCE` (`emcp-gateway` by default). See
|
|
`deploy/keycloak/realm-export.json` for the 3 example clients already configured this way
|
|
(`sales-read-agent`, `sales-write-agent`, `finance-payments-agent`).
|
|
|
|
## Access token contract
|
|
|
|
Any OIDC-compliant IdP works (`TokenValidator` is provider-agnostic). Claims required and
|
|
validated by `TokenValidator.validate`:
|
|
|
|
| Claim | Use |
|
|
|---|---|
|
|
| `exp` | Expiration — required, validated with `leeway_seconds=5`. |
|
|
| `iss` | Must match `OIDC_ISSUER` exactly. |
|
|
| `aud` | Must contain `OIDC_AUDIENCE`. |
|
|
| `azp` (preferred) or `client_id` | Sole source of client identity — never `clientInfo` from the MCP request nor any body field. |
|
|
|
|
Signature verified via JWKS (`OIDC_JWKS_URL`), `RS256` algorithm. Failure of any check raises
|
|
`AuthenticationError`, treated by the Gateway as `UNAUTHENTICATED` (fail-closed).
|
|
|
|
## How a new client registers, end to end
|
|
|
|
1. Create/update the client in the IdP (`client_credentials` grant, audience mapper).
|
|
2. Create `config/clients/<client-id>.yaml` (`ClientRegistration`).
|
|
3. Point `profile` at an existing `ClientProfile`, or create a new one under
|
|
`config/clients/profiles/` (see [`registry-capability-manifest.md`](registry-capability-manifest.md)
|
|
for how a capability references a profile via `entitlements`).
|
|
4. `make validate-schemas`.
|
|
5. Restart the Gateway (client registrations are loaded at boot, no hot-reload).
|
|
|
|
## Errors
|
|
|
|
| Situation | Behavior |
|
|
|---|---|
|
|
| Missing/malformed/expired token or invalid signature | `AuthenticationError` → Gateway treats it as `UNAUTHENTICATED`. |
|
|
| Incorrect `aud`/`iss` | `AuthenticationError` → `UNAUTHENTICATED` (README.md §45 Test 8). |
|
|
| Valid token, but `client_id` without a matching `ClientRegistration` | `UnknownClientError` → Gateway responds `UNKNOWN_CLIENT` on `tools/call`, empty list on `tools/list`. |
|