Files
computer-playbook/utils/roles/applications/mcp.py
kevinveenbirkenbachandClaude Opus 5 346ad875c6 refactor(mcp): drop the two provenance fields nothing reads
supported_version and minimum_version sat in 24 roles and were read by no template, no task and no lookup. Their only consumer was the audit report version column, which turned false the moment an automated image bump ran: qdrant reported a verified version nobody had verified, two releases long. Both leave MCP_KEYS, so a role that declares them again fails the schema lint instead of carrying a field with no effect.

Requirement 025 mandated minimum_version as a MUST and 035 carried it in an example; both are corrected here, otherwise the repository would contradict its own code.

The gate then surfaced five dead docs.mattermost.com links. Mattermost moved to /administration-guide/ and /deployment-guide/, and every replacement was probed for 200 rather than guessed. The MCP page is gone with no successor, so mattermost now points at mcpserver/README.md in the Agents repository, the source rather than a rendered mirror.

Both concerns share three files - mattermost's meta/mcp.yml, requirement 025, and the generated audit.md, which derives from a source of each - so splitting them would commit an artifact that does not match its sources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 19:31:08 +02:00

463 lines
15 KiB
Python

"""Vocabulary of the ``mcp`` block in ``meta/services.yml``.
Every role with an ``application_id`` declares exactly one ``mcp:`` entry.
The block is an audit disposition first and a deployable surface second:
``classification`` states what the role is, and only a deployable
classification may also carry ``enabled``/``shared`` and a served contract.
Field vocabulary (see ``docs/contributing/design/role/services/mcp.md``):
* ``classification``: what this role is in the MCP audit. Required.
* ``reason``: why a non-deployable classification cannot serve.
* ``direction``: whether the role exposes MCP, consumes it, or both.
* ``transport``: wire protocol of the endpoint.
* ``exposure``: who may reach the endpoint.
* ``auth``: authentication scheme guarding the endpoint.
* ``auth_subject``: identity MCP calls execute as. ``user`` and ``oidc``
additionally require a ``delegation`` block, because a rendered deployment
bearer is a service account no matter what the metadata claims.
* ``delegation``: recorded proof, at one exact upstream version, that
per-user token acquisition, refresh, audience binding and revocation exist.
* ``implementation``: how the surface is provided, ordered by precedence.
* ``credential``: which principal the provider authenticates as, and
where its secret is read from. Replaces the administrator token path.
Admission is not declared here. A client marks itself once, in its own
``meta/services.yml`` self-entry, as ``mcp_consumer: true``; a provider
deviates by carrying ``mcp_consumer: false`` on that client's entry in its
own ``meta/services.yml``. ``derive_allowed_consumers`` resolves the pair.
* ``supported_transports`` / ``supported_auths``: what a client can present.
* ``endpoint``: connection metadata for clients (server roles only).
* ``adapter``: immutable source contract of a repository-owned adapter.
``category_allowlist`` suits an upstream that switches whole categories and
offers no per-tool filter, where a tool allowlist would be unenforceable;
``mcp_passthrough`` fronts an upstream that already speaks MCP instead of
translating REST. ``upstream_path`` names that upstream where it lives on the
application's own vhost, so the proxy can hide it: reaching it directly walks
past the allowlist the adapter exists to enforce. A provider whose upstream is
a separate sidecar declares ``upstream_network`` instead and has nothing to
hide there.
* ``limits``: the request, response, timeout, concurrency, pagination
and stream ceilings the surface enforces.
* ``tools``: ``allowlist`` is what a reader may reach and
``writer_allowlist`` what a writer may reach; the deployment renders one
contract per role, each with its own bearer, because the gateway tells
callers apart by credential and by nothing else. ``allowlist`` is policy,
what the platform lets a consumer
reach, and is only a constraint where a gateway enforces it. ``upstream_serves``
is the observation: every tool the upstream offers at the deployed version.
State it only where it DIFFERS from the allowlist; absence means the two are
identical, which is the normal case for a provider that serves exactly what is
allowed. The difference between them measures unenforced exposure, and a
provider whose difference is non-empty needs a gateway in front. Plus schema
hash and mutation policy.
* ``mutating_proofs``: required once ``tools.mutating_tools_enabled`` is true.
Names the artifact proving each of confirmation, authorization, idempotency,
audit and reversal for the mutating variant, separately from the read-only
one.
* ``source_url``: upstream documentation or source of the MCP surface.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from utils.roles.entity.name import get_entity_name
if TYPE_CHECKING:
from collections.abc import Mapping
MCP_DIRECTIONS = frozenset({"server", "client", "both"})
MCP_TRANSPORTS = frozenset({"streamable_http", "sse"})
MCP_EXPOSURES = frozenset({"internal", "public"})
MCP_AUTHS = frozenset(
{"oidc", "app_password", "basic_auth", "bearer_token", "upstream_session", "none"}
)
MCP_AUTH_SUBJECTS = frozenset({"user", "service_account", "administrator", "none"})
MCP_IMPLEMENTATIONS = frozenset({"native", "plugin", "sidecar", "adapter", "external"})
MCP_CLASSIFICATIONS = frozenset(
{
"native_server",
"native_client",
"native_both",
"plugin_server",
"sidecar_server",
"adapter_server",
"adapter_candidate",
"blocked",
"enabler",
"subordinate",
"no_surface",
}
)
MCP_DEPLOYABLE_CLASSIFICATIONS = frozenset(
{
"native_server",
"native_client",
"native_both",
"plugin_server",
"sidecar_server",
"adapter_server",
}
)
MCP_AUDIT_ONLY_CLASSIFICATIONS = MCP_CLASSIFICATIONS - MCP_DEPLOYABLE_CLASSIFICATIONS
MCP_CLIENT_CLASSIFICATIONS = frozenset({"native_client", "native_both"})
MCP_REASONS = frozenset(
{
"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",
"missing_dependency",
}
)
MCP_ADAPTER_TYPES = frozenset(
{
"openapi_allowlist",
"graphql_allowlist",
"named_query",
"s3_prefix",
"prometheus_readonly",
"n8n_workflow",
"resource_readonly",
"category_allowlist",
"mcp_passthrough",
}
)
MCP_UPSTREAM_MCP_ADAPTER_TYPES = frozenset({"mcp_passthrough"})
MCP_SCOPED_ADAPTER_TYPES = frozenset(
{"graphql_allowlist", "named_query", "s3_prefix", "resource_readonly"}
)
MCP_SPECIFICATION_ADAPTER_TYPES = frozenset({"openapi_allowlist", "graphql_allowlist"})
MCP_CREDENTIAL_SOURCES = frozenset({"token_store", "credentials"})
MCP_SOURCE_TOKEN_STORE = "token_store" # noqa: S105 - a source name, not a secret
MCP_SOURCE_CREDENTIALS = "credentials"
MCP_FORBIDDEN_CREDENTIAL_OWNER = "administrator"
MCP_KEYS = frozenset(
{
"enabled",
"shared",
"classification",
"reason",
"direction",
"transport",
"exposure",
"auth",
"auth_subject",
"delegation",
"implementation",
"credential",
"supported_transports",
"supported_auths",
"endpoint",
"adapter",
"limits",
"tools",
"mutating_proofs",
"source_url",
}
)
MCP_ENDPOINT_KEYS = frozenset(
{
"service_key",
"path",
"port_key",
"host_header",
}
)
MCP_CREDENTIAL_KEYS = frozenset({"owner", "source", "key"})
MCP_DELEGATION_KEYS = frozenset(
{
"verified_version",
"source_url",
"refresh",
"revocation",
"audience_binding",
}
)
MCP_DELEGATION_PROOF_KEYS = frozenset({"refresh", "revocation", "audience_binding"})
MCP_ADAPTER_KEYS = frozenset(
{
"type",
"upstream_api_version",
"upstream_network",
"specification_path",
"specification_sha256",
"scope",
"upstream_transport",
"upstream_path",
}
)
MCP_ADAPTER_REQUIRED_KEYS = frozenset({"type"})
MCP_LIMITS_KEYS = frozenset(
{
"request_bytes",
"response_bytes",
"timeout_seconds",
"concurrent_requests",
"page_size",
"result_items",
"stream_seconds",
}
)
MCP_TOOLS_KEYS = frozenset(
{
"allowlist",
"mutating",
"writer_allowlist",
"upstream_serves",
"categories",
"schema_sha256",
"read_only_default",
"mutating_tools_enabled",
"read_probe",
}
)
MCP_READ_PROBE_KEYS = frozenset({"tool", "arguments"})
MCP_TOOLS_BOOLEAN_KEYS = frozenset({"read_only_default", "mutating_tools_enabled"})
MCP_SERVER_DIRECTIONS = frozenset({"server", "both"})
MCP_CLIENT_DIRECTIONS = frozenset({"client", "both"})
MCP_REQUIRED_ENDPOINT_KEYS = frozenset({"service_key", "path", "port_key"})
MCP_PRIVILEGED_AUTH_SUBJECTS = frozenset({"service_account", "administrator"})
MCP_DELEGATED_AUTH_SUBJECTS = frozenset({"user"})
MCP_DELEGATED_AUTHS = frozenset({"oidc"})
MCP_FORBIDDEN_SURFACE_MARKERS = (
"docker.sock",
"/var/run/",
"unix:",
"file://",
"sh -c",
"bash -c",
"${",
"*",
)
MCP_SHA256_PREFIX = "sha256:"
DEFAULT_MCP_TRANSPORT = "streamable_http"
MCP_CONSUMER_FLAG = "mcp_consumer"
def value_is_templated(value: object) -> bool:
return isinstance(value, str) and "{{" in value
def declares_mcp_consumer(role_id: str, services: object) -> bool:
"""Return whether a role marks itself an MCP client in its own services.
Args:
role_id: the role's ``application_id``.
services: that role's ``services`` mapping, or anything else.
"""
if not isinstance(services, dict):
return False
entry = services.get(get_entity_name(role_id))
return isinstance(entry, dict) and entry.get(MCP_CONSUMER_FLAG) is True
def admits_mcp_consumer(provider_services: object, consumer_id: str) -> bool:
"""Return whether a provider admits one client.
An omitted flag inherits the client's own declaration; only an explicit
``mcp_consumer: false`` excludes, so a deliberate exclusion is
distinguishable from a forgotten entry.
Args:
provider_services: the provider's ``services`` mapping.
consumer_id: the client's ``application_id``.
"""
if not isinstance(provider_services, dict):
return True
entry = provider_services.get(get_entity_name(consumer_id))
if isinstance(entry, dict) and MCP_CONSUMER_FLAG in entry:
return entry[MCP_CONSUMER_FLAG] is True
return True
def derive_allowed_consumers(
provider_id: str, services_by_role: dict[str, object]
) -> list[str]:
"""Return the client application ids a provider admits, sorted.
Args:
provider_id: the provider's ``application_id``.
services_by_role: every role's ``services`` mapping, keyed by role id.
"""
provider_services = services_by_role.get(provider_id)
return sorted(
role_id
for role_id, services in services_by_role.items()
if role_id != provider_id
and declares_mcp_consumer(role_id, services)
and admits_mcp_consumer(provider_services, role_id)
)
def derive_mcp_presence(applications: dict[str, Any]) -> dict[str, Any]:
"""Materialise every ``mcp`` topic's enable state from its declared peers.
Args:
applications: ``{application_id: config}``, mutated in place.
Returns:
The same mapping.
A surface is reachable only while a peer that uses it is deployed, so both
keys resolve to the ``in group_names`` form the rest of the tree uses, and
to ``False`` when a role has no peer at all. An explicitly configured
``enabled``/``shared`` always wins, so config stays the override channel.
Keying on peer PRESENCE rather than on a peer's own value is required: a
re-entrant read under ``_RENDER_GUARD`` returns the unrendered dict.
The resolved block replaces the mapping rather than being written into it.
``_build_role_base_config`` parks the YAML cache's own object under ``mcp``,
so an in-place write would inject these keys into every later raw read of
``meta/mcp.yml``.
"""
blocks = {
role_id: config["mcp"]
for role_id, config in applications.items()
if isinstance(config, dict) and isinstance(config.get("mcp"), dict)
}
services_by_role = {
role_id: (config or {}).get("services")
for role_id, config in applications.items()
if isinstance(config, dict)
}
for role_id, block in blocks.items():
direction = str(block.get("direction") or "").lower()
peers: set[str] = set()
if direction in ("server", "both"):
peers |= set(derive_allowed_consumers(role_id, services_by_role))
if direction in ("client", "both") and declares_mcp_consumer(
role_id, services_by_role.get(role_id)
):
peers |= {
peer
for peer, peer_block in blocks.items()
if peer != role_id
and str(peer_block.get("direction") or "").lower() in ("server", "both")
and admits_mcp_consumer(services_by_role.get(peer), role_id)
}
value: Any = (
"{{ " + " or ".join(f"'{p}' in group_names" for p in sorted(peers)) + " }}"
if peers
else False
)
resolved = dict(block)
resolved.setdefault("enabled", value)
resolved.setdefault("shared", value)
applications[role_id]["mcp"] = resolved
return applications
def is_deployable(mcp: object) -> bool:
"""Return whether this block may carry a served surface and be discovered.
Args:
mcp: the role's ``mcp`` mapping, or anything else.
"""
if not isinstance(mcp, dict):
return False
return mcp.get("classification") in MCP_DEPLOYABLE_CLASSIFICATIONS
def declares_delegation(mcp: object) -> bool:
"""Return whether this block claims to act as the requesting end user.
Args:
mcp: the role's ``mcp`` mapping, or anything else.
"""
if not isinstance(mcp, dict):
return False
return (
mcp.get("auth_subject") in MCP_DELEGATED_AUTH_SUBJECTS
or mcp.get("auth") in MCP_DELEGATED_AUTHS
)
def delegation_is_proven(mcp: object) -> bool:
"""Return whether a recorded per-version audit backs the delegation claim.
Args:
mcp: the role's ``mcp`` mapping, or anything else.
"""
if not isinstance(mcp, dict):
return False
delegation = mcp.get("delegation")
if not isinstance(delegation, dict):
return False
if not str(delegation.get("verified_version") or "").strip():
return False
if not str(delegation.get("source_url") or "").strip():
return False
return all(delegation.get(key) is True for key in MCP_DELEGATION_PROOF_KEYS)
def resolve_credential(
server: Mapping[str, Any],
users: Mapping[str, Any],
role_credentials: Mapping[str, Any],
) -> tuple[str, str]:
"""Return the provider's declared secret and the owner it belongs to.
Args:
server: a discovered ``direction=server`` entry.
users: the merged users mapping, carrying each principal's tokens.
role_credentials: the provider role's own ``secrets.credentials``
mapping, which is where every other consumer addresses them.
Returns an empty token when the declaration is incomplete or the principal
holds nothing; the caller turns that into ``credential_missing``.
"""
credential = server.get("credential") or {}
owner = str(credential.get("owner") or "").strip()
source = str(credential.get("source") or "").strip()
key = str(credential.get("key") or "").strip()
if not owner or not source or not key or owner == MCP_FORBIDDEN_CREDENTIAL_OWNER:
return "", owner
if source == MCP_SOURCE_TOKEN_STORE:
principal = users.get(owner)
tokens = principal.get("tokens") if isinstance(principal, dict) else None
value = (tokens or {}).get(key) if isinstance(tokens, dict) else None
return str(value or "").strip(), owner
if source == MCP_SOURCE_CREDENTIALS:
return str(role_credentials.get(key) or "").strip(), owner
return "", owner