0003: One MCP surface per provider, and a disposition for every role 🔌¶
Status: accepted
Supersedes the design half of requirement 025, which assumed an empty baseline, treated metadata as enforcement, and marked client provisioning and end-to-end authorization complete.
Work: TODO — the acceptance criteria and the order of implementation live in 035 - MCP Proxy Expansion and Application Interconnection. This record holds the decision, the forces behind it, the standing contract and the per-role audit; that document holds what is still to be built and which artifact proves each criterion. A disposition change belongs here, because the audit below is what two lints read. A newly met criterion belongs there.
Context 🎯¶
An MCP client turns an application’s API into tools a model may call. The cheap way to build that is one process holding administrator credentials for every application and translating an OpenAPI document into tools on demand. That process is a single credential store, a single network position reaching every application, and a tool surface that grows with every upstream release nobody reviewed.
Requirement 025 established the first metadata and the Open WebUI integration. Reading the code against it showed the gap between what was declared and what ran:
plugins/lookup/mcp_servers.pyread every provider token fromusers.administrator.tokensand copied that deployment credential to every selected client.auth_subjectwas returned as metadata and selected nothing.plugins/filter/mcp/authorization.pyrenderedoidcas a stored bearer. It acquired, refreshed, exchanged and revoked nothing, so a rendered deployment bearer was user delegation in name only.plugins/lookup/roles_with_service.pydiscovered all shared providers with no consumer allowlist, and several role flags named concrete peer roles ingroup_names, which does not scale past the first few clients.Absence of an
mcp:block was the documented way to say “no upstream path”, which cannot distinguish a reviewed refusal from a role nobody examined.
Three measurements decided the shape rather than any preference:
allowed_consumerscreates no network edge. A client reaches a provider only when the provider declares akind: mcp_clientconsumer inmeta/networks.yml, or when the client happens to declare that provider for an unrelated reason. Four of twenty providers declared the opening; the other sixteen admitted consumers they could not serve, four of them enabled. Deploying Hermes failed at its own probe withAll connection attempts failedagainsthttp://prometheusmcp:8080/mcp, becausehermesjoins[gitlab, hermes, homeassistant, litellm, moodle]whileprometheusmcpjoins[prometheus]alone.A shared infrastructure network carries lateral reach the application layer does not control. On the compose stack, Flowise and Baserow both join
postgresbecause both use the central database, and Flowise opensbaserow:80at192.168.200.5although Baserow does not admit Flowise. Reach has to be measured by resolved address: an unknown name falls through to a wildcard resolver that answers with one reachable address, so a name-based probe reports every peer as reachable and proves nothing.Opening an adapter provider’s network to its clients collides with the sidecar isolation invariant. A provider that declares the
mcp_clientoverlay puts its own containers on it. The same declaration on an adapter provider would give its sidecar a second interface, andisolation.shrejects anything but exactly one, because a sidecar holding the provider’s credential must reach that provider only.
Two further properties of the environment shaped the reconciliation design. Open WebUI stores tool servers in its own configuration and answers every later read with them, so a client that adopts that answer as the desired state leaves a newly declared provider registered nowhere and a rotated bearer registered stale, in both cases without a failure. And roles/web-app-openwebui/templates/env.j2 sets ENABLE_PERSISTENT_CONFIG=false, so API changes to environment-backed connection state are runtime state that has to be reconciled after every container restart.
Decision ✅¶
The platform does not deploy one all-powerful OpenAPI-to-MCP process. It provides one reusable, immutable adapter implementation and instantiates it once per provider application or per equally isolated trust domain. Each instance attaches only to the provider’s own network, receives only that provider’s least-privileged credential, exposes a distinct endpoint, and stays addressable by the provider’s application_id. Every exposed provider remains a separate logical MCP server even when a common implementation serves it, which preserves the mapping between roles/<provider-application-id>/mcp, the client connection, the upstream credential, the tool allowlist and the audit record.
Integration paths are tried in this order, and a lower one is used only when every higher one fails to preserve the application’s authorization boundary:
a supported native MCP endpoint with an enforceable tool policy;
a pinned, source-audited upstream plugin;
a pinned, isolated upstream sidecar;
a repository-owned allowlist adapter or a repository-owned n8n workflow;
a documented blocker.
Flowise and Open WebUI are clients, not generic security gateways. n8n may adapt a workflow to MCP when its project, credential, input schema and exposed workflow tools are deployment-managed. LiteLLM may become a routing layer only after the exact selected image is pinned and proven, and must not become the sole holder of every application’s administrator credential.
Every role carries a disposition¶
Every role with an application_id carries one role-local mcp: mapping in meta/services.yml. Discovery selects only a deployable classification with explicit enabled: true and shared: true; an audit-only mapping never becomes an endpoint:
mcp:
classification: no_surface
reason: host_execution_boundary
notes: This desktop role has no isolated remote application identity or endpoint.
Allowed classifications are native_server, native_client, native_both, plugin_server, sidecar_server, adapter_server, adapter_candidate, blocked, enabler, subordinate, and no_surface.
Allowed reasons MUST be documented and linted, including no_remote_surface, host_execution_boundary, privileged_control_plane, shared_engine_isolation, administrative_surface, duplicate_owner, version_unverified, licence, hosted_only, stdio_only, interactive_browser_session, unreviewed_third_party, and missing_dependency.
A deployable mapping additionally carries the source URL and the exact supported version or commit; a negative classification carries a concrete reason and notes.
An enabled adapter declares everything it is allowed to do¶
There are no implicit security defaults: every field below is required for an enabled adapter surface, and a missing value fails lint or deployment.
mcp:
classification: adapter_server
enabled: true
shared: true
direction: server
transport: sse
exposure: internal
auth: bearer_token
auth_subject: service_account
credential:
owner: mcp-service-web-app-example
source: token_store
key: web-app-example
implementation: adapter
maturity: experimental
source_url: https://example.invalid/project/source/tree/v4.5.6
allowed_consumers:
- web-app-openwebui
- web-app-flowise
endpoint:
service_key: example-mcp
path: /mcp
port_key: http
adapter:
type: openapi_allowlist
image: registry.example.invalid/infinito-mcp-adapter
version: 1.2.3
digest: sha256:0123456789abcdef
upstream_api_version: v1
specification_path: files/mcp/openapi-v1.json
specification_sha256: sha256:fedcba9876543210
limits:
request_bytes: 65536
response_bytes: 1048576
timeout_seconds: 15
concurrent_requests: 4
page_size: 100
result_items: 500
stream_seconds: 300
tools:
allowlist:
- example_search
- example_get
schema_sha256: sha256:abcdef0123456789
read_only_default: true
mutating_tools_enabled: false
Allowed implementation values are native, plugin, sidecar, adapter, and external. An adapter declares exactly one type:
openapi_allowlist: a checked-in, versioned, hash-pinned API description plus explicit operation identifiers;graphql_allowlist: checked-in persisted operations only, with runtime introspection and arbitrary query text rejected;named_query: checked-in parameterized read queries or views through an application-specific read-only database principal, never arbitrary SQL;s3_prefix: list, head and get through a dedicated read-only policy restricted to named buckets and prefixes;prometheus_readonly: query, query-range, labels, metadata and targets with explicit lookback, sample, response and timeout limits;n8n_workflow: a checked-in workflow with a stable ownership marker, explicit input schema, explicit connected tools, and a bearer-protected trigger;resource_readonly: checked-in rules for exposing a bounded, non-secret published content set as MCP resources rather than pretending it is an action API.
The schema rejects a generic URL, a runtime OpenAPI document, free-form GraphQL, a raw SQL tool, an unrestricted bucket, an arbitrary filesystem root, a shell command, a Docker socket, and implicit exposure of every operation an upstream upgrade adds.
Identity is per provider, never the deployment administrator¶
Each service-account integration provisions a dedicated non-login identity holding only the upstream permissions the declared allowlist needs. The lookup resolves mcp.credential.owner, source and key; deployment fails when the resolved principal is missing, empty, more privileged than auth_subject declares, or shared with an unrelated provider.
auth_subject: user may be set only after the exact client and server prove authorization-code or token-exchange behaviour, token refresh, audience binding, revocation and expiry. A static bearer issued to the deployment is not on-behalf-of and stays service_account. Client-side group gating controls access to that service account; it does not recreate the caller’s permissions.
Credentials are unique per provider and, where a provider cannot distinguish consumers, per provider-consumer pair. Rotation is two-phase: issue new, reconcile every consumer, prove new works, revoke old, prove old fails. Disabling a surface or removing a consumer revokes the credential rather than hiding the connection.
Every client connection maps to exactly one provider group named roles/<provider-application-id>/mcp. The declarative user schema carries an application-scoped role form, so mcp can be granted for one application without granting it everywhere:
users:
alice:
application_roles:
web-app-baserow:
- mcp
web-app-zammad:
- mcp
web-app-keycloak does not expose its administration API as a tool server. It provides OAuth infrastructure for MCP servers by provisioning an explicit resource, client policy, scope and redirect contract per provider, with experimental token exchange disabled until the exact versions pass user-delegation and revocation tests.
Discovery reconciles, and owns only what it named¶
One post-application reconciliation stage runs after every selected provider role has provisioned and probed its endpoint, builds one immutable discovery snapshot, and reconciles every client from it. No provider or client enabled or shared expression enumerates concrete peer role names. The discovery result carries selected entries and rejected ones with a stable code such as consumer_not_allowed, transport_unsupported, auth_unsupported, credential_missing or endpoint_unreachable.
Reconciliation updates or removes only entries carrying its own ownership marker and preserves human-created configuration. Zero matches creates, one updates, more than one fails rather than guessing which duplicate to keep. The marker is per client, because the name is not always free: Flowise registry entries are infinito:<provider-application-id>, Open WebUI groups carry the RBAC group path Keycloak also issues, and n8n uses its configured workflow name.
The stage runs before the end-to-end tests that read the converged state, and not behind a task a red test aborts. Placed in the destructor stage it ran after the Playwright suite, and any_errors_fatal meant any failing role skipped reconciliation entirely, so the suite asserted a state nothing had produced.
The two clients with version-specific behaviour¶
Flowise 3.1.4 is reconciled through the authenticated /api/v1/custom-mcp-servers routes with a scoped identity limited to tools:create, tools:view, tools:update and tools:delete. It registers only Streamable HTTP providers: its SSE fallback cannot complete a connection at this pin, so an SSE provider is reported incompatible rather than claimed as registered. The role builds its own image from the pinned npm package, because the published flowiseai/flowise tags 3.1.3, 3.1.4 and latest all ship the flowise package at version 3.1.2, which serves none of the required routes. Internal connectivity is never solved by globally disabling SSRF protection for all flow authors.
n8n 1.100.1 is direction: both: an SSE client and a Streamable HTTP server at MCP Server Trigger typeVersion 2. Managed client nodes use Bearer or Header authentication and an explicit include list, never upstream’s include: all. Managed server triggers require Bearer authentication, connect only explicitly selected workflow tools with checked-in input schemas, stay disabled until the operator enables them, and expose read-only operations first.
Safety contract for every adapter¶
Every adapter enforces request size, response size, timeout, concurrency, pagination, result-row and stream-duration limits with explicit values in role metadata, and logs provider, consumer, tool name, credential subject, result status, duration and correlation identifier without logging credentials or payload bodies.
Tool discovery is allowlisted by exact name and JSON schema hash, and an added, removed or changed upstream tool fails closed until the checked-in contract is reviewed. The policy gateway enforces the same allowlist on tools/call; filtering only tools/list is insufficient. Resources, prompts, sampling, elicitation, roots and any capability a future MCP release adds are classified independently, because the absence of a dangerous tool list does not authorize an unreviewed non-tool capability.
Mutations are classified as reversible, irreversible, external-communication, financial, identity/permission, code-execution or infrastructure-control. Any enabled mutation requires a separate opt-in variant, a distinct RBAC role where practical, an idempotency strategy, a human confirmation boundary and a tested audit event. Infrastructure control, arbitrary code execution, unrestricted filesystem access, identity administration and raw database mutation stay forbidden for shared clients.
Sidecars and adapters use an immutable version and digest, documented provenance and licence, a non-root user, a read-only root filesystem, dropped capabilities, no host or control-plane socket, explicit CPU/memory/PID limits, a provider-only network, and no unrelated outbound access.
Per-provider constraints¶
The eighteen roles that already carried an mcp: block when this record was written each hold a standing constraint, because their surface was declared before the contract above existed:
Role |
Standing constraint |
|---|---|
|
Native server only with a dedicated least-privilege identity, a verified read-only allowlist, per-consumer grants, and an actual client tool call. |
|
Self-hosted stays blocked while the verified Atlassian path is Cloud-only; an external connector is explicit and is never represented as the self-hosted role’s endpoint. |
|
|
|
Reconciles the 3.1.4 Streamable HTTP registry and a managed flow. No global-security-relaxation-as-integration claim, and a deterministic tool call rather than an anonymous-rejection test. |
|
Exact project-owned package, image pin, endpoint auth, dedicated service account, tool allowlist, read-only behaviour. |
|
Tier and exact self-managed endpoint availability are hard gates. Project/group-scoped token, mutation tools excluded until separately enabled. |
|
|
|
Exact native endpoint, transport, scopes, entity exposure and service-call mutations verified. A long-lived administrator token is not acceptable. |
|
Plugin and Jenkins compatibility pinned; job/status/log reads first; build, config and script actions outside the default tool list. |
|
Self-hosted stays blocked while the verified Atlassian path is Cloud-only; an external connector carries its own credentials, URL and data-boundary documentation. |
|
A dedicated bot/service identity instead of administrator credentials, and read/search tools separated from post/channel management. |
|
A Moodle-compatible plugin pinned, an exact web-service function allowlist, a restricted service user, and proof that unlisted functions cannot be invoked. |
|
Required apps pinned, application password scoped, files and tools constrained, and proof that the endpoint cannot escape that user’s shares. |
|
|
|
|
|
The Community Edition licensing blocker stands. An Enterprise variant supplies edition and licence explicitly and uses a restricted OAuth application. |
|
Per-server group grants, no hard-coded administrator token source, application-scoped user roles, last-group revocation, reconciliation after restart. |
|
The official WordPress MCP adapter pinned, its HTTP endpoint exposed, only reviewed Abilities with permission callbacks registered, authenticated as a real restricted user. |
Five roles carry a concrete upstream path that the audit files as blocked; the blocker table above names what stops each one, and these are the conditions under which it would move:
Role |
Condition for reclassification |
|---|---|
|
Keep the current pin inference-only. An MCP-capable release is selected only after source inspection proves per-server routes, exact tool allowlists, credential injection and the chosen OAuth lifecycle, with separate provider credentials rather than a global administrator token. |
|
Replace the unversioned preview image with an explicit MCP-capable headless version, render only deployment-curated servers, disable arbitrary per-request URLs, and test tool allowlists. |
|
One sidecar per consuming application with a database-scoped read-only principal and only schema/SELECT/SHOW/DESCRIBE tools. Never a global server on the shared engine or the root credential. |
|
Pin the experimental Frappe component in a small managed app, expose a few named read tools, reject generic DocType/method/SQL execution, and use a restricted API user or proven per-user OAuth, verified against the exact Frappe v16 pin. |
|
Upgrade only to an explicit tested release containing |
Exhaustive Application-ID Audit¶
Every role with a literal application_id is filed exactly once below. A role missing from these lists would read as “not yet looked at” and as “decided against” at the same time, with nothing to tell the two apart.
The section titles are the classification vocabulary itself, and each role that ships a meta/mcp.yml is filed under the value that file declares. The authoritative disposition of a deployed surface is that file; this list is the record of the review, including the roles that deliberately carry no surface.
The list was held against the repository by three lints that parsed this section. They are gone, and the trade is deliberate: a lint that reads prose ties every role addition to an edit here, and the audit is a review record rather than a runtime registry. What they proved while they ran is worth keeping: the classification check named twenty-four roles whose audit entry contradicted their own metadata the first time it ran, nine of them shipping an adapter while the list still called them untried candidates. Without them, a role added later is absent here until someone notices.
native_server (1)¶
web-app-homeassistant.
native_client (4)¶
web-app-flowise, web-app-hermes, web-app-openclaw, web-app-openwebui.
native_both (1)¶
web-app-n8n.
plugin_server (2)¶
web-app-moodle, web-app-wordpress.
adapter_server (16)¶
svc-db-qdrant, web-app-baserow, web-app-checkmk, web-app-fider, web-app-gitea, web-app-gitlab, web-app-jellyfin, web-app-jenkins, web-app-listmonk, web-app-mattermost, web-app-nextcloud, web-app-pretix, web-app-prometheus, web-app-snipe-it, web-app-zammad, web-svc-libretranslate.
blocked (11)¶
An upstream path exists or is claimed, and something concrete stops it here. Each row names what, using the reason vocabulary above, so “blocked” never reads as “nobody looked”.
svc-ai-litellm, svc-ai-lmstudio, svc-db-elasticsearch, svc-db-mariadb, svc-db-redis, web-app-discourse, web-app-erpnext, web-app-matomo, web-app-odoo, web-app-penpot, web-app-shopware.
role |
reason |
blocker |
|---|---|---|
|
|
The pinned release is inference-only; an MCP-capable one may be selected only after source inspection proves per-server routes, exact tool allowlists and the credential lifecycle. |
|
|
The image is an unversioned preview; an explicit MCP-capable headless version has to replace it before a surface can be curated. |
|
|
The maintained native path needs Kibana Agent Builder with its index privileges and licence; the older sidecar is deprecated. |
|
|
A global MCP server would attach to the shared engine; the alternative is one sidecar per consuming application with a database-scoped read-only principal. |
|
|
The upstream server is stdio-oriented and broadly mutating over a cache several applications share. |
|
|
The project-owned server passes ownership, licence, pinning and read-only default, but its HTTP transport serves one session per process, which upstream asserts in |
|
|
The Frappe MCP component is experimental and unverified against the pinned Frappe v16. |
|
|
The project-owned plugin is unaudited: endpoint, transport, auth and tool scope are unverified in source. |
|
|
Neither Odoo SA nor the OCA publishes a server; the leading third party carries 197 of 231 commits from one author, keeps its access control in a separate Odoo Apps store module outside any pinnable tag, and defaults to a mutating tool scope. |
|
|
The server needs an active browser tab and plugin connection, and can execute powerful design-context operations. |
|
|
The pinned 6.7.8.2 predates the experimental native |
adapter_candidate (39)¶
An adapter could reach these, and none has been curated yet. The five that used to sit under “current MCP metadata requiring revalidation” are here because that revalidation found no shipped surface to revalidate.
svc-db-typesense, web-app-akaunting, web-app-bigbluebutton, web-app-bluesky, web-app-bookwyrm, web-app-bridgy-fed, web-app-confluence, web-app-decidim, web-app-espocrm, web-app-friendica, web-app-funkwhale, web-app-jira, web-app-jitsi, web-app-joomla, web-app-kix, web-app-magento, web-app-mailu, web-app-mastodon, web-app-matrix, web-app-mediawiki, web-app-minio, web-app-mobilizon, web-app-opencloud, web-app-openproject, web-app-opentalk, web-app-peertube, web-app-pihole, web-app-pixelfed, web-app-postmarks, web-svc-seaweedfs, web-app-semaphore, web-app-socialhome, web-app-stalwart, web-app-suitecrm, web-app-taiga, web-app-xwiki, web-app-yourls, web-svc-api, web-svc-xmpp.
Each candidate starts with three to five named read tools rather than a generated copy of an API, and the first investigation uses the starter contract recorded in Adapter starter contracts. If the exact pinned version cannot implement the named contract through a stable authenticated API, the role becomes blocked with the observed reason rather than receiving a generic substitute.
enabler (2)¶
svc-ai-mcp-adapter, web-app-keycloak.
svc-ai-mcp-adapter carries no MCP surface of its own. It is the adapter runtime a provider role instantiates, so its disposition is enabler and it never appears in discovery as a provider or a consumer.
subordinate (2)¶
web-svc-collabora, web-svc-onlyoffice.
These are reached through the owning document application’s MCP boundary, such as Nextcloud or OpenCloud, rather than receiving a second independent service-account tool surface.
no_surface (112)¶
These roles stay out of shared MCP discovery. A future exception requires a new record with a fixed operation list, dedicated identity, isolation boundary, human approval for mutations, and audit trail.
host_execution_boundary (47): dev-arduino, dev-core, dev-java, dev-locales, dev-make, dev-nix, dev-nodejs, dev-python, drv-epson-multiprinter, drv-intel, drv-lid-switch, drv-non-free, dsk-base, dsk-bluray-player, dsk-chromium, dsk-code, dsk-copyq, dsk-docker, dsk-dotlinker, dsk-firefox, dsk-git, dsk-gnome, dsk-gnome-caffeine, dsk-gnome-extensions, dsk-gnome-terminal, dsk-gnt-claude, dsk-gnt-codex, dsk-gnt-cursor, dsk-gnt-pi, dsk-gnucash, dsk-jrnl, dsk-keepassxc, dsk-laya, dsk-libreoffice, dsk-micro, dsk-neovim, dsk-nextcloud, dsk-obs, dsk-qbittorrent, dsk-retroarch, dsk-spotify, dsk-ssh, dsk-torbrowser, dsk-virtualbox, dsk-zoom, gen-hunspell, user-workstation.
These roles operate a workstation, developer toolchain, device, or host package; bridging them would amount to shared shell, filesystem, browser-session, device, or host execution without an application-specific remote identity.
dsk-base and user-workstation provision the login account the other desktop roles write for, which is the host execution boundary itself rather than an application behind it.
privileged_control_plane (28): svc-ai-agent-broker, svc-ai-jupyter, svc-bkp-local-2-device, svc-bkp-nfs-2-local, svc-bkp-remote-2-local, svc-bkp-secrets-2-local, svc-bkp-volume-2-local, svc-dns-unbound, svc-net-firewall, svc-net-tor, svc-net-tor-smtp, svc-net-wireguard-core, svc-net-wireguard-firewalled, svc-net-wireguard-plain, svc-opt-keyboard-color, svc-opt-ssd-hdd, svc-opt-swapfile, svc-prx-openresty, svc-registry-cache, svc-registry-docker, svc-runner, svc-storage-nfs-client, svc-storage-nfs-server, svc-swarm-manager, svc-swarm-node, svc-virt-kata, update, web-app-openbao.
These roles can recover secrets, alter routing or host state, run code, change deployment state, or reach storage/control-plane sockets; they require a separate audited operations gateway and human approval rather than general-purpose application MCP.
shared_engine_isolation (4): svc-db-memcached, svc-db-openldap, svc-db-postgres, svc-db-rabbitmq.
A generic endpoint would bypass application and tenant authorization on a shared engine.
Any future exception requires a provider-application-specific account, namespace/database/schema/queue restriction, fixed named operations, and no engine administrator credential.
administrative_surface (5): web-app-fusiondirectory, web-app-lam, web-app-pgadmin, web-app-phpldapadmin, web-app-phpmyadmin.
These UIs expose identity or database administration rather than a bounded application-domain API and are never represented by a shared service-account tool surface.
duplicate_owner (1): web-app-litellm.
This role is only the UI of the separately classified LiteLLM service role, which owns any future MCP gateway contract.
no_remote_surface (27): svc-ai-ollama, svc-ai-robot, svc-ai-s1, svc-ai-searxng, svc-ai-tika, web-app-chess, web-app-dashboard, web-app-docs, web-app-fediwall, web-app-hugo, web-app-littlejs, web-app-meta, web-app-mini-qr, web-app-navigator, web-app-roulette-wheel, web-opt-rdr-domains, web-opt-rdr-www, web-svc-asset, web-svc-cdn, web-svc-coturn, web-svc-design, web-svc-file, web-svc-html, web-svc-legal, web-svc-logout, web-svc-mirror, web-svc-simpleicons.
These roles have no independently useful authenticated remote action contract at the pinned implementation.
Static published content may later be consumed through one owning application’s bounded resource_readonly adapter, but a new MCP sidecar that merely reimplements a static site’s behaviour does not count as integration.
Model tool calling in Ollama does not by itself make Ollama an MCP client or server.
Consequences ⚖️¶
What the decision cost¶
Instantiating per provider means the adapter is deployed as many times as there are surfaces, each with its own credential, network attachment and endpoint. That is the price of the boundary: a shared instance would be one process to run and one credential to steal.
Requiring a disposition for every role turned a silent default into 172 explicit decisions that a lint now keeps current. A new role fails that lint until somebody disposes of it, which is the intended friction.
Declaring every adapter field explicitly means a surface cannot be enabled by adding one flag. Eight of the enabled serving surfaces reached that bar; the remaining thirty-eight candidates did not, and the audit says so rather than shipping them with defaults.
What enforcing it found¶
The rules were written first and the code audited against them afterwards. Each of the following was live at the time it was found, not a hypothetical the design ruled out:
Nextcloud minted a new app password on every run and revoked none. The name was fixed and managed, so each run that found the stored password rejected added another valid token to the account while every previous one kept full access. The mint is now preceded by a listing and the revocation of every token carrying that name.
Qdrant ran with no authentication at all. Its environment set the HTTP port and disabled telemetry, so every container reaching its network held full read, write and delete access to every collection, and the surface’s read-only promise rested entirely on the adapter’s contract naming only GET routes. The engine now declares
read_only_api_key, the adapter presents it, and a write route is refused by the engine rather than only by the adapter.The adapter leaked its upstream credential on a redirect. It opened the upstream with
urlopen, which follows redirects, and urllib copies the original request’s headers onto the target, so an upstream answering302with an off-host location would have handed the credential to whoever it named. The opener now refuses redirects outright, since the target is contracted.Published tool schemas were a suggestion, not a boundary. An argument the contract does not name was appended to the upstream request as a query parameter, and a tool declaring no schema accepted anything. Arguments are now validated against the contract, after the size ceiling so a large payload is still refused for being large.
Prometheus ran with no
--query.timeout, so a query the adapter abandoned after 15s kept the server busy for its two-minute default, and a range query’s point count was unbounded although the response was truncated to 100 rows. The timeout is now derived frommcp.limits.timeout_seconds, andpolicy.assert_rangerefuses a range whose point count exceeds the contract’sresult_items, naming the smallest step that would fit.n8n shipped with no
allowed_groupswhile declaring anmcprole nothing consulted, and minted a public-API key per run that it then left standing. The gate is closed and the key is revoked before the run ends.Flowise’s published images do not contain the version their tag claims:
3.1.3,3.1.4andlatestall shipflowise3.1.2, because upstream’s Dockerfile runs an unpinnednpm install -g flowisewhose layer the release CI reuses. The role builds from an explicit npm pin and verifies the served route set.Two proofs were vacuous. Flowise tool-name verification compared against a hardcoded empty list, and every role-local MCP spec gated itself on the service flag, so a surface that kept serving after being switched off was indistinguishable from one never deployed. Both now fail when they should.
Twenty-four roles’ audit entries contradicted their own metadata the first time the classification lint ran, nine of them shipping an adapter while the list called them untried candidates.
What it does not yet hold¶
Credentials are kept out of logs, task output and Playwright traces by three static lints. Generated non-secret configuration needs a secret/non-secret classification of rendered files that does not exist; API responses and exception text are runtime observations.
Endpoint DNS and routing from each client, in compose and in swarm, is unproven; only a running cluster can show it. The isolation half is covered, by a probe that compares the sidecar’s own addresses against the declared subnets.
No client executes a deterministic real tool call in every deployment. Flowise does, through its managed Agentflow fixture; the others assert less.
The adapter
digestpins nothing. All tenimplementation: adapterroles carry the identical placeholdersha256:followed by sixty-four zeros, the schema only checks that prefix, and the running image is resolved throughlookup('container_image', …)instead. A lint that rejects the placeholder has to land together with real digests rather than before them.Per-pair networks have to replace
kind: mcp_client, which opens a provider to every client rather than to the admitted one. Either the sidecar joins only the pair network and the provider joins it too, or the isolation check becomes an identity check over the admitted peers rather than a count.Hermes renders its agent config and its env from two evaluations of one discovery list, and they disagree: the config carried
Bearer ${env:WEB_APP_PROMETHEUS_MCP_TOKEN}while the env defined no*_MCP_TOKEN, so the client presented an empty bearer and the adapter correctly answered401. Any fix has to make both render from one evaluation.Mutating variants have no tests, because no surface enables mutations. The gate exists instead: a role setting
tools.mutating_tools_enabledwithout anmcp.mutating_proofsblock naming an existing artifact for each of the five proofs fails lint.
Remaining rollout¶
Qdrant and the high-value read adapters are in place. What is left, in order: the remaining business, content, social and operations adapters, once the shared contract and the first adapters pass compose and swarm tests; and separately reviewed evaluations of the Shopware upgrade, the ERPNext plugin, the Matomo plugin, the LiteLLM gateway and the LM Studio client, because each moves an upstream version or maturity boundary.
Adapter starter contracts¶
These are proposed repository contracts for the adapter_candidate roles, not claims that upstream already uses these names.
Family |
Roles |
Initial permitted surface |
Mandatory additional guard |
|---|---|---|---|
Search and observability |
|
Collection search; host/service status; PromQL query, query range, labels, metadata, and targets |
Per-application collection/index, explicit lookback and sample caps, no configuration or administration endpoint |
Content and storage |
|
Catalog/page/library search and get; S3 list/head/get; translate and detect |
User or service identity restricted to the intended library/site/bucket/prefix; no edit/upload/delete by default |
Business applications |
|
Named list, search, and get tools for the application’s primary records |
Dedicated role with field/tenant restrictions; no generic CRUD, no arbitrary filter language, and no Adobe Commerce claim for Magento Open Source |
Federated social and messaging |
|
Search or read public/user-visible content and account state |
Per-user token is required for private feeds, rooms, messages, or mail; a shared service account never merges users’ private data |
High-side-effect operations |
|
Meeting/status/job/log reads only |
Create/end meeting, DNS blocking changes, playbook/Terraform/shell execution, and configuration changes require a separate role, human confirmation, idempotency key, and audit event |
Role |
Proposed adapter |
Initial tool contract |
Identity and hard boundary |
|---|---|---|---|
|
|
|
One search-only key and explicit collection list per application; no collection/schema/key administration |
|
|
|
Restricted company user/token; no payment, ledger, tax, user, or configuration mutation |
|
|
|
Dedicated integration secret; no create, join-as-moderator, end, publish, or delete |
|
|
|
Per-user OAuth/app password for non-public data; no post, like, follow, moderation, or account mutation |
|
|
|
Public access or restricted user token; no shelf, review, follow, or federation mutation |
|
|
|
Public data only; block if the pinned service has no stable bounded read API |
|
|
|
Automation user with monitoring-read permissions; no activation, acknowledge, downtime, host, rule, or password mutation |
|
|
|
Public reads first; no proposal, vote, moderation, identity, or assembly mutation |
|
|
|
Restricted CRM API user with field and team visibility; no generic entity endpoint or write |
|
|
|
Public/restricted read token; no vote, response, status, tag, user, or tenant mutation |
|
|
|
Per-user token for timeline data; no posting, messaging, contact, moderation, or account mutation |
|
|
|
Public or library-scoped identity; no upload, favorite, playlist, federation, or administration mutation |
|
|
|
Dedicated non-admin user restricted by library policy; no stream URL, delete, playback control, user, or server mutation |
|
|
|
Proceed only if the exact deployment exposes a stable authenticated status API; no room creation, moderation, recording, or token minting |
|
|
|
Read-only API user; no article, extension, template, configuration, or user mutation |
|
|
|
Agent identity restricted to intended queues/organizations; no ticket, asset, user, or configuration mutation |
|
|
|
Reporting-only service identity; no subscriber export, message send, campaign start, template, or list mutation |
|
|
|
Storefront persisted queries only; do not treat Adobe Commerce preview MCP as available in Magento Open Source |
|
|
|
Separate read-only administration principal if upstream supports one; no mailbox content, password, alias, domain, or queue mutation |
|
|
|
Per-user OAuth for non-public timelines; no status, follow, favorite, direct-message, moderation, or account mutation |
|
|
|
Per-user access token and room membership; no cross-user service token, send, invite, kick, ban, power-level, or encryption-key action |
|
|
|
Anonymous/public or restricted bot read identity; no edit, upload, delete, block, rights, or configuration action |
|
|
|
Dedicated read-only policy for declared buckets and prefixes; never root credentials, write, delete, policy, lifecycle, or admin APIs |
|
|
|
Persisted public queries first; per-user token for private events; no create, join, invite, moderation, or federation mutation |
|
|
|
Restricted user/service identity and selected spaces; no unrestricted download URL, upload, share, permission, or user mutation |
|
|
|
Restricted API identity; no meeting start/end, invite, recording content, moderation, or tenant mutation |
|
|
|
Public reads or restricted account; no upload, comment, follow, federation, moderation, or administration mutation |
|
|
|
Reporting-only token if the exact API can enforce it; no query-log detail by default, blocking toggle, list, DNS, DHCP, or configuration mutation |
|
|
|
Per-user OAuth for non-public data; no post, message, follow, moderation, or account mutation |
|
|
|
Public collection only; the single owner secret is never exposed to a shared client |
|
|
|
Organizer-scoped read token; redact attendee/payment data; no order, check-in, voucher, refund, payout, or event mutation |
|
|
|
Fixed Prometheus origin, explicit lookback/sample/time caps; no admin, reload, snapshot, delete-series, or arbitrary URL proxy |
|
|
|
Dedicated read-only S3 identity and declared bucket/prefix; no filer traversal outside the prefix, write, delete, replication, or admin action |
|
|
|
Read-only project identity; no task start, playbook, Terraform, shell, repository, key, inventory, environment, or user mutation |
|
|
|
Read-only token with field restrictions; no checkout/checkin, asset, license, accessory, user, or settings mutation |
|
|
|
Public reads unless a proven per-user API exists; no content, follow, federation, moderation, or account mutation |
|
|
|
Restricted CRM API user and allowed modules/fields; no generic module CRUD, export, workflow, user, or configuration action |
|
|
|
Project-scoped read identity; no issue/story, sprint, membership, webhook, or project mutation |
|
|
|
Read-only user restricted to named wikis/spaces; no page, attachment, script, rights, extension, or user mutation |
|
|
|
Read-only API identity if enforceable; never disclose the global signature token or enable shorten, edit, or delete by default |
|
|
|
Agent token restricted by Zammad groups; redact sensitive articles; no ticket, article, user, role, or configuration mutation |
|
|
|
Dedicated API key, strict text/request/response limits, and no arbitrary URL/file fetch |
|
|
|
Per-user or room-scoped service identity; no unrestricted archive, send, invite, kick, ban, role, or server administration action |
Upstream sources¶
An upstream README or current default branch may identify a candidate, but only the source and tests of the exact selected tag or digest close a compatibility question.
Capability |
Authoritative source entry point |
|---|---|
Flowise registry at the deployed pin |
Flowise tag |
n8n client, trigger, and public provisioning API |
n8n tag |
WordPress plugin path |
|
Discourse sidecar path |
|
Qdrant sidecar path |
|
ERPNext/Frappe plugin path |
|
Matomo plugin candidate |
|
Penpot interactive server |
|
MariaDB isolated sidecar path |
|
Redis blocked sidecar path |
|
LiteLLM gateway candidate |