mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
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>
463 lines
15 KiB
Python
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
|