diff --git a/README.md b/README.md index efbe54f88b..5dccda06fc 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Every invokable role in the tested lifecycle envelope, with its upstream homepag | [Matomo](roles/web-app-matomo/) | beta | Self-hosted, privacy-first web analytics platform for real-time visitor tracking and site metrics | [🌐](https://matomo.org/) [🎬](https://www.youtube.com/watch?v=Qc2kooLNDiU) [🛠️](roles/web-app-matomo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [MariaDB](roles/svc-db-mariadb/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) | | [Matrix](roles/web-app-matrix/) | beta | Federated, end-to-end-encrypted messaging platform with a web client and protocol bridges to external chat networks | [🌐](https://matrix.org/) [🎬](https://www.youtube.com/watch?v=3AVsNqH_-9M) [🛠️](roles/web-app-matrix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Universal Logout](roles/web-svc-logout/) | | [Mattermost](roles/web-app-mattermost/) | beta | Open-source, self-hosted team messaging platform for real-time collaboration via channels, threads, and integrations | [🌐](https://mattermost.com/) [🎬](https://www.youtube.com/watch?v=szfZfQFUSnU) [🛠️](roles/web-app-mattermost/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Universal Logout](roles/web-svc-logout/) | +| [MCP Adapter](roles/svc-ai-mcp-adapter/) | alpha | Reusable per-provider MCP adapter exposing a narrow allowlisted read surface | [🌐](https://modelcontextprotocol.io/) [🛠️](roles/svc-ai-mcp-adapter/README.md#quick-setup) | | | [MediaWiki](roles/web-app-mediawiki/) | beta | Collaborative wiki platform for user-driven documentation with an extensible extension ecosystem and content management | [🌐](https://www.mediawiki.org/) [🎬](https://www.youtube.com/watch?v=-JnIjpRvgNY) [🛠️](roles/web-app-mediawiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) | | [Memcached](roles/svc-db-memcached/) | beta | Central in-memory caching engine with per-consumer key-prefix isolation, usable centrally or as an embedded sidecar | [🌐](https://memcached.org/) [🛠️](roles/svc-db-memcached/README.md#quick-setup) | | | [MIG](roles/web-app-mig/) | alpha | Interactive graph visualization of all Infinito.Nexus roles and their dependencies for architecture transparency | [🛠️](roles/web-app-mig/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/) | diff --git a/docs/contributing/design/role/services/mcp.md b/docs/contributing/design/role/services/mcp.md index 18c134f465..73216f0d90 100644 --- a/docs/contributing/design/role/services/mcp.md +++ b/docs/contributing/design/role/services/mcp.md @@ -1,75 +1,133 @@ # MCP Service Block 🔌 -A role with a Model Context Protocol surface declares one `mcp:` entry in `meta/services.yml`. The block carries the standard consumer flags plus MCP-specific fields. Vocabulary lives in [`utils/roles/applications/services/mcp.py`](../../../../../utils/roles/applications/services/mcp.py); the hard lint is [`tests/lint/ansible/services/test_mcp_schema.py`](../../../../../tests/lint/ansible/services/test_mcp_schema.py). +A role declares `meta/mcp.yml` when it actually serves or consumes MCP. The file is a capability descriptor, not a service entry: no single role provides MCP, so the service registry cannot pair provider and consumer, and `allowed_consumers` does it instead. Vocabulary lives in [`utils/roles/applications/mcp.py`](../../../../../utils/roles/applications/mcp.py); the hard lint is [`test_mcp_schema.py`](../../../../../tests/lint/ansible/services/test_mcp_schema.py). -## Shape +`classification` is required and is one of `native_server`, `native_client`, `native_both`, `plugin_server`, `sidecar_server`, `adapter_server`. A role that cannot serve declares no `mcp` block at all. + +## Server shape ```yaml mcp: bond: 1 - enabled: false - shared: false - direction: server # server | client | both - transport: streamable_http # streamable_http | sse - exposure: internal # internal | public - auth: bearer_token # oidc | app_password | basic_auth | bearer_token | upstream_session | none - auth_subject: user # user | service_account | administrator | none - implementation: native # native | plugin | sidecar | external - endpoint: # required for direction: server | both - service_key: baserow # names a service entry in the same file + enabled: true + shared: true + classification: native_server + direction: server # server | client | both + transport: sse # streamable_http | sse + exposure: internal # internal | public + auth: app_password # oidc | app_password | basic_auth | bearer_token | upstream_session | none + auth_subject: service_account # user | service_account | administrator | none + implementation: native # native | plugin | sidecar | adapter | external + credential: + owner: mcp-web-app-example # a dedicated non-login identity, never `administrator` + source: token_store # token_store | credentials + key: web-app-example + allowed_consumers: # exact client application ids + - web-app-openwebui + endpoint: + service_key: example # names a service entry in the same file path: /mcp - port_key: http # resolves under that service's ports.local or ports.internal + port_key: http # resolves under that service's ports.local or ports.internal tools: read_only_default: true mutating_tools_enabled: false ``` +`exposure: public` additionally requires a complete `limits` block. + +## Client shape + +A client declares what it can present instead of what it serves: + +```yaml +mcp: + classification: native_client + direction: client + implementation: native + supported_transports: [streamable_http, sse] + supported_auths: [bearer_token, app_password, upstream_session] +``` + +A client-only block MUST NOT declare an `endpoint`. + +## Adapter shape + +`implementation: adapter` requires `classification: adapter_server` and a pinned, hash-checked contract: + +```yaml + adapter: + type: openapi_allowlist # see below + image: registry.example.invalid/infinito-mcp-adapter + version: 1.2.3 + digest: sha256:0123456789abcdef + upstream_api_version: v1 + specification_path: roles/web-app-example/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 +``` + +`type` is one of `openapi_allowlist`, `graphql_allowlist`, `named_query`, `s3_prefix`, `prometheus_readonly`, `n8n_workflow`, `resource_readonly`. `graphql_allowlist`, `named_query`, `s3_prefix` and `resource_readonly` additionally require a non-empty `adapter.scope` naming the exact buckets, prefixes, collections or queries reachable. `openapi_allowlist` and `graphql_allowlist` require a checked-in `specification_path` under the role's own `files/`. + ## Rules -- `direction` is required; all MCP-specific fields MUST be literals (only `enabled`/`shared` may carry Jinja). -- `direction: server | both` requires `auth` and a complete `endpoint` (`service_key`, `path`, `port_key`). +- `classification` is required and MUST be deployable; all MCP-specific fields MUST be literals (only `enabled`/`shared` may carry Jinja). +- `enabled`/`shared` MUST NOT name a peer role. Provider enablement is explicit operator or variant state; the variant axis lives in `meta/variants.yml`. +- `direction: server | both` requires `auth`, `credential`, `allowed_consumers` and a complete `endpoint`. +- `credential.owner` MUST NOT be `administrator`. Two providers resolving to the same owner and secret abort discovery. - `auth: none` requires `exposure: internal`. -- `auth_subject: service_account | administrator` requires an explicit `tools.mutating_tools_enabled: false`; omitting the key is rejected, because a privileged subject must state its tool policy rather than inherit a default. The field records the intent; where upstream exposes mutating tools unconditionally, the role README names the missing enforcement. +- `auth_subject: service_account | administrator` requires an explicit `tools.mutating_tools_enabled: false`. +- `auth_subject: user` or `auth: oidc` requires a `delegation` block; see [mcp-delegation.md](mcp-delegation.md). - `endpoint.service_key` MUST name a service in the same file; `endpoint.port_key` MUST resolve under its `ports.local` or `ports.internal`. - Suppress a finding with `# nocheck: mcp-schema` (file head = whole file, on/above a line = that finding). ## Public exposure -`exposure: public` puts the endpoint on the role's own vhost, so it inherits -what that vhost already provides: routing through the shared proxy, TLS from the -platform certificate, `client_max_body_size` from `server.client_max_body_size`, -and the proxy read timeout of the location it is mounted in. +`exposure: public` puts the endpoint on the role's own vhost, so it inherits what that vhost already provides: routing through the shared proxy, TLS from the platform certificate, `client_max_body_size` from `server.client_max_body_size`, and the proxy read timeout of the location it is mounted in. -Rate limiting is not among them. The proxy ships no `limit_req` or `limit_conn` -anywhere, so a public MCP endpoint is only as protected as its own -authentication. An operator exposing one to the open internet has to put a rate -limiter in front of the vhost; the platform will not supply one. +Rate limiting is not among them. The proxy ships no `limit_req` or `limit_conn` anywhere, so a public MCP endpoint is only as protected as its own authentication and its declared `limits`. An operator exposing one to the open internet has to put a rate limiter in front of the vhost; the platform will not supply one. -Prefer `exposure: internal`. A client inside the container network reaches the -endpoint without any of this, which is why five of the eight server roles stay -internal and only those whose upstream mounts MCP under the application's own -public routes are public. +Prefer `exposure: internal`. A client inside the container network reaches the endpoint without any of this. ## Why stdio is not deployed -`transport` accepts `streamable_http` and `sse` only. A stdio MCP server is a -local process the client spawns, so it inherits the client container's -filesystem and network with no endpoint to authenticate, no proxy to route -through, and nothing for a probe to reach. Every property the other rules rely -on (`auth`, `exposure`, `endpoint`) is undefined for it. Clients that support -stdio are pinned away from it in their own role: Flowise sets -`CUSTOM_MCP_PROTOCOL=sse` so no flow can spawn a command. +`transport` accepts `streamable_http` and `sse` only. A stdio MCP server is a local process the client spawns, so it inherits the client container's filesystem and network with no endpoint to authenticate, no proxy to route through, and nothing for a probe to reach. Every property the other rules rely on (`auth`, `exposure`, `endpoint`) is undefined for it. Clients that support stdio are pinned away from it in their own role: Flowise sets `CUSTOM_MCP_PROTOCOL=sse` so no flow can spawn a command. ## Client discovery -Client roles discover server-capable roles through the `roles_with_service` lookup with the `direction` kwarg: +Clients do not read the server list directly. [`lookup('mcp_servers')`](../../../../../plugins/lookup/mcp_servers.py) computes the intersection of the provider's `allowed_consumers` with the calling client's `supported_transports` and `supported_auths`, resolves each provider's declared credential, and returns: ```yaml -{{ lookup('roles_with_service', 'mcp', direction='server') }} +MCP_DISCOVERY: "{{ lookup('mcp_servers') }}" +MCP_DISCOVERED_SERVERS: "{{ MCP_DISCOVERY.selected }}" +MCP_REJECTED_SERVERS: "{{ MCP_DISCOVERY.rejected }}" ``` -Each returned entry carries `id`, `canonical_domain`, `canonical_url`, `iframe`, plus `transport`, `auth`, `auth_subject` and `endpoint` (`service_key`, `path`, resolved `port`). Callers that omit `direction` keep the original 4-key entries. +Every rejected entry carries a stable `reason`: `consumer_not_allowed`, `transport_unsupported`, `auth_unsupported`, `credential_missing` or `endpoint_unreachable`. + +## RBAC + +Reaching a tool server is a distinct grant from using the application, carried by the provider's `mcp` RBAC role. `mcp` is application-scoped: it is granted through `application_roles`, never through the unscoped `roles` list, so granting it on one application grants it nowhere else. + +```yaml +users: + alice: + application_roles: + web-app-baserow: [mcp] +``` ## See Also - Service metadata basics: [base.md](base.md) +- User delegation audit: [mcp-delegation.md](mcp-delegation.md) diff --git a/plugins/lookup/mcp_servers.py b/plugins/lookup/mcp_servers.py index 5beb782ba7..a19a263267 100644 --- a/plugins/lookup/mcp_servers.py +++ b/plugins/lookup/mcp_servers.py @@ -18,7 +18,7 @@ cannot be rendered, so they abort the run instead of disappearing quietly: a client that silently ends up with fewer tools than the deployment declared is indistinguishable from one that works. -The provider credential is whatever ``services.mcp.credential`` declares: +The provider credential is whatever ``mcp.credential`` declares: ``owner`` names the principal, ``source`` where its secret lives (``token_store`` or the role's own ``credentials``), ``key`` the entry. No provider inherits the administrator's token. @@ -32,7 +32,7 @@ from ansible.errors import AnsibleError from ansible.plugins.loader import lookup_loader from ansible.plugins.lookup import LookupBase -from utils.roles.applications.services.mcp import DEFAULT_MCP_TRANSPORT +from utils.roles.applications.mcp import DEFAULT_MCP_TRANSPORT if TYPE_CHECKING: from collections.abc import Mapping, Sequence @@ -113,7 +113,7 @@ def build_mcp_discovery( Args: servers: ``roles_with_service('mcp', direction='server')`` entries. consumer_id: ``application_id`` of the client doing the discovery. - consumer: the client's own ``services.mcp`` block, carrying + consumer: the client's own ``mcp`` block, carrying ``supported_transports`` and ``supported_auths``. credentials: resolved ``(token, owner)`` per provider role id. path_keys: resolved ``key_credential`` values, keyed by role id. @@ -253,7 +253,13 @@ class LookupModule(LookupBase): servers = lookup_loader.get( "roles_with_service", loader=self._loader, templar=templar - ).run(["mcp"], variables=vars_, direction="server", scope="deployment")[0] + ).run( + ["mcp"], + variables=vars_, + topic="mcp", + direction="server", + scope="deployment", + )[0] applications = lookup_loader.get( "applications", loader=self._loader, templar=templar @@ -262,9 +268,7 @@ class LookupModule(LookupBase): [], variables=vars_ )[0] - consumer = ((applications.get(consumer_id) or {}).get("services") or {}).get( - "mcp" - ) or {} + consumer = (applications.get(consumer_id) or {}).get("mcp") or {} credentials: dict[str, tuple[str, str]] = {} path_keys: dict[str, str] = {} diff --git a/plugins/lookup/roles_with_service.py b/plugins/lookup/roles_with_service.py index cde684baf3..a19cab8b40 100644 --- a/plugins/lookup/roles_with_service.py +++ b/plugins/lookup/roles_with_service.py @@ -45,7 +45,7 @@ from ansible.plugins.loader import lookup_loader from ansible.plugins.lookup import LookupBase from utils.domains.primary_domain import get_primary_domain -from utils.roles.applications.services.mcp import DEFAULT_MCP_TRANSPORT +from utils.roles.applications.mcp import DEFAULT_MCP_TRANSPORT from utils.roles.entity.name import get_entity_name if TYPE_CHECKING: @@ -125,6 +125,7 @@ class LookupModule(LookupBase): ).run([], variables=vars_)[0] scope = str(kwargs.get("scope", "host")).strip().lower() + topic = kwargs.get("topic") direction_raw = kwargs.get("direction") direction = ( str(direction_raw).strip().lower() if direction_raw is not None else None @@ -141,8 +142,8 @@ class LookupModule(LookupBase): continue services = app_config.get("services") if not isinstance(services, dict): - continue - block = services.get(service_name) + services = {} + block = app_config.get(topic) if topic else services.get(service_name) if not isinstance(block, dict): continue if not bool(block.get("enabled")): diff --git a/roles/svc-db-qdrant/README.md b/roles/svc-db-qdrant/README.md index 4f1aa5d25a..c2d9e32800 100644 --- a/roles/svc-db-qdrant/README.md +++ b/roles/svc-db-qdrant/README.md @@ -23,6 +23,7 @@ flowchart LR end subgraph role [svc-db-qdrant 🐳🐝] svc_qdrant["qdrant"] + svc_qdrantmcp["qdrantmcp"] svc_container_backup["container_backup"] end subgraph dependents [Dependents] diff --git a/roles/svc-db-qdrant/meta/mcp.yml b/roles/svc-db-qdrant/meta/mcp.yml new file mode 100644 index 0000000000..e70a2b0c7d --- /dev/null +++ b/roles/svc-db-qdrant/meta/mcp.yml @@ -0,0 +1,47 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: v1.18.2 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-svc-db-qdrant + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: qdrantmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/svc-db-qdrant/files/mcp/tools.json + specification_sha256: sha256:95037a1f1d060f8bbf092ac5943fb705d699ce0c30491a4b1c884fe1bf81d2f7 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - qdrant_list_collections + - qdrant_get_collection + schema_sha256: sha256:95037a1f1d060f8bbf092ac5943fb705d699ce0c30491a4b1c884fe1bf81d2f7 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/svc-db-qdrant/meta/services.yml b/roles/svc-db-qdrant/meta/services.yml index 51d270a6a8..ba77dc04f6 100644 --- a/roles/svc-db-qdrant/meta/services.yml +++ b/roles/svc-db-qdrant/meta/services.yml @@ -26,6 +26,24 @@ qdrant: internal: qdrant: 6333 lifecycle: beta +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP integration test +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +# nocheck: dynamic-flag the sidecar tracks this role's own mcp flag, not another role's presence; a group_names expression would decide its exposure from an unrelated deployment +qdrantmcp: + enabled: "{{ lookup('config', 'svc-db-qdrant', 'mcp.enabled') | bool }}" + shared: false + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: qdrantmcp + cpus: "0.25" + mem_reservation: 24m + mem_limit: 48m + pids_limit: 64 + ports: + internal: + http: 8080 # nocheck: playwright-service-flag, dynamic-flag infrastructure backup consumer container_backup: # nocheck: bond-baseline backup host joins outside the variant topology diff --git a/roles/svc-db-qdrant/meta/variants.yml b/roles/svc-db-qdrant/meta/variants.yml new file mode 100644 index 0000000000..dd28edaec2 --- /dev/null +++ b/roles/svc-db-qdrant/meta/variants.yml @@ -0,0 +1,12 @@ +--- +# Matrix-deploy variant overrides for svc-db-qdrant. +# See docs/contributing/artefact/files/role/variants.md for the file format. + +# variant 0: MCP adapter enabled (canonical default). + +- mcp: + enabled: true + shared: true +- mcp: + enabled: false + shared: false diff --git a/roles/web-app-baserow/README.md b/roles/web-app-baserow/README.md index 414e8b4bbf..7f0cf34b94 100644 --- a/roles/web-app-baserow/README.md +++ b/roles/web-app-baserow/README.md @@ -22,7 +22,6 @@ flowchart LR dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_mailu["web-app-mailu 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openwebui["web-app-openwebui 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] @@ -33,7 +32,6 @@ flowchart LR svc_dashboard["dashboard"] svc_matomo["matomo"] svc_email["email"] - svc_mcp["mcp"] svc_sso["sso"] svc_redis["redis"] svc_postgres["postgres"] @@ -56,7 +54,6 @@ flowchart LR dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openwebui -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs dep_web_svc_css -. "0..1" .-> svc_css @@ -148,7 +145,7 @@ Configuration is controlled via `applications..bootstrap_admin.*`: ## MCP Server Baserow ships a native MCP server in the OSS backend, mounted on the ASGI root -ahead of Django. The role turns it on with `services.mcp.enabled` and hands +ahead of Django. The role turns it on with `mcp.enabled` and hands clients the container-network origin (`exposure: internal`); `env.j2` then adds that origin to `BASEROW_EXTRA_PUBLIC_URLS` so the image's Caddy layer routes `/mcp/*` to the backend instead of falling through to the Nuxt frontend. @@ -190,7 +187,7 @@ Read tools: `list_databases`, `list_tables`, `get_table_schema`, Write tools: `create_rows`, `update_rows`, `delete_rows`. Baserow 2.3.1 exposes these unconditionally; the image carries no env var, setting or UI toggle that -removes them. `services.mcp.tools.mutating_tools_enabled` is declared `false` as +removes them. `mcp.tools.mutating_tools_enabled` is declared `false` as the deployment's intent, not as an enforced state: Baserow serves the three write tools regardless. What bounds them is the endpoint owner, whose reach is the one workspace that account owns. @@ -209,12 +206,12 @@ the role's `mcp` RBAC group, not on being signed in. ### Default state -Off. `services.mcp.enabled` is true only while `web-app-openwebui` is part of the +Off. `mcp.enabled` is true only while `web-app-openwebui` is part of the deployment. ### How to disable -Remove the MCP client role, or pin `services.mcp.enabled: false` for this role in +Remove the MCP client role, or pin `mcp.enabled: false` for this role in the inventory. The endpoint is then not provisioned and `BASEROW_EXTRA_PUBLIC_URLS` drops the MCP origin, so the image's Caddy layer stops routing `/mcp/*`. diff --git a/roles/web-app-baserow/meta/mcp.yml b/roles/web-app-baserow/meta/mcp.yml new file mode 100644 index 0000000000..40227bf96c --- /dev/null +++ b/roles/web-app-baserow/meta/mcp.yml @@ -0,0 +1,39 @@ +--- +enabled: true +shared: true +classification: native_server +direction: server +source_url: https://baserow.io/user-docs/mcp-server # nocheck: url — page is live (HEAD+GET 200); baserow.io edge intermittently 404s CI runner IPs +supported_version: 2.3.1 +minimum_version: "1.30" +transport: sse +exposure: internal +auth: app_password +auth_subject: service_account +implementation: native +credential: + owner: mcp-web-app-baserow + source: token_store + key: web-app-baserow +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw + - web-app-n8n +endpoint: + service_key: baserow + path: /mcp + port_key: http + key_credential: mcp_endpoint_key + suffix: sse +tools: + allowlist: + - create_rows + - delete_rows + - get_table_schema + - list_databases + - list_table_rows + - list_tables + - update_rows + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-baserow/meta/services.yml b/roles/web-app-baserow/meta/services.yml index 33b524c331..56865d1393 100644 --- a/roles/web-app-baserow/meta/services.yml +++ b/roles/web-app-baserow/meta/services.yml @@ -15,29 +15,6 @@ email: bond: 1 enabled: "{{ 'web-app-mailu' in group_names }}" shared: "{{ 'web-app-mailu' in group_names }}" -# nocheck: shared-provider -mcp: - bond: 1 - enabled: "{{ 'web-app-openwebui' in group_names }}" - shared: "{{ 'web-app-openwebui' in group_names }}" - direction: server - source_url: https://baserow.io/user-docs/mcp-server # nocheck: url — page is live (HEAD+GET 200); baserow.io edge intermittently 404s CI runner IPs - minimum_version: "1.30" - notes: Native built-in MCP server. - transport: sse - exposure: internal - auth: app_password - auth_subject: service_account - implementation: native - endpoint: - service_key: baserow - path: /mcp - port_key: http - key_credential: mcp_endpoint_key - suffix: sse - tools: - read_only_default: true - mutating_tools_enabled: false sso: bond: 1 enabled: "{{ 'web-app-keycloak' in group_names }}" diff --git a/roles/web-app-baserow/meta/variants.yml b/roles/web-app-baserow/meta/variants.yml index 6c2f61732a..44304146a7 100644 --- a/roles/web-app-baserow/meta/variants.yml +++ b/roles/web-app-baserow/meta/variants.yml @@ -19,9 +19,6 @@ matomo: enabled: true shared: true - mcp: - enabled: true - shared: true sso: enabled: true shared: true @@ -38,6 +35,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -54,9 +54,6 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -73,3 +70,6 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-baserow/templates/playwright.env.j2 b/roles/web-app-baserow/templates/playwright.env.j2 index f1afd6f6f3..5b273e70ee 100644 --- a/roles/web-app-baserow/templates/playwright.env.j2 +++ b/roles/web-app-baserow/templates/playwright.env.j2 @@ -21,7 +21,7 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }} MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/roles/web-app-baserow/vars/main.yml b/roles/web-app-baserow/vars/main.yml index dd1d36faa7..70ff80b710 100644 --- a/roles/web-app-baserow/vars/main.yml +++ b/roles/web-app-baserow/vars/main.yml @@ -33,9 +33,9 @@ BASEROW_MANAGE_PY: "/baserow/backend/src/baserow/manage.py" BASEROW_SERVICE_NAME: "baserow" BASEROW_INTERNAL_URL: "http://{{ BASEROW_CONTAINER }}:{{ lookup('config', application_id, 'services.baserow.ports.internal.http') }}" -BASEROW_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" +BASEROW_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" BASEROW_MCP_ENDPOINT_KEY: "{{ lookup('config', application_id, 'credentials.mcp_endpoint_key') }}" -BASEROW_MCP_ENDPOINT_PATH: "{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +BASEROW_MCP_ENDPOINT_PATH: "{{ lookup('config', application_id, 'mcp.endpoint.path') }}" BASEROW_MCP_ENDPOINT_NAME: "Infinito.Nexus" BASEROW_MCP_WORKSPACE_NAME: "Infinito.Nexus MCP" BASEROW_MCP_OWNER_EMAIL: "mcp@{{ lookup('domain', application_id) }}" diff --git a/roles/web-app-checkmk/README.md b/roles/web-app-checkmk/README.md index 6593a65971..0d15075c4c 100644 --- a/roles/web-app-checkmk/README.md +++ b/roles/web-app-checkmk/README.md @@ -37,6 +37,7 @@ flowchart LR svc_prometheus["prometheus"] svc_checkmk["checkmk"] svc_container_backup["container_backup"] + svc_checkmkmcp["checkmkmcp"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_openldap -. "0..1" .-> svc_ldap diff --git a/roles/web-app-checkmk/meta/mcp.yml b/roles/web-app-checkmk/meta/mcp.yml new file mode 100644 index 0000000000..9ae0470a7d --- /dev/null +++ b/roles/web-app-checkmk/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-checkmk + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: checkmkmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-checkmk/files/mcp/tools.json + specification_sha256: sha256:9a769b4524a2c55a12e06c6fb3d906cff1ccd7f1f3445e3543a94891a8d466be +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - checkmk_get_host_status + - checkmk_list_hosts + - checkmk_list_services + schema_sha256: sha256:9a769b4524a2c55a12e06c6fb3d906cff1ccd7f1f3445e3543a94891a8d466be + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-checkmk/meta/services.yml b/roles/web-app-checkmk/meta/services.yml index 46817c4022..c5ba02de52 100644 --- a/roles/web-app-checkmk/meta/services.yml +++ b/roles/web-app-checkmk/meta/services.yml @@ -88,3 +88,18 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +checkmkmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: checkmkmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-checkmk/meta/variants.yml b/roles/web-app-checkmk/meta/variants.yml index bce07d7d22..33969213a3 100644 --- a/roles/web-app-checkmk/meta/variants.yml +++ b/roles/web-app-checkmk/meta/variants.yml @@ -27,6 +27,9 @@ enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -56,6 +59,9 @@ enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -84,3 +90,6 @@ sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-confluence/README.md b/roles/web-app-confluence/README.md index 3defd4050b..e919023bdb 100644 --- a/roles/web-app-confluence/README.md +++ b/roles/web-app-confluence/README.md @@ -36,7 +36,6 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_postgres -. "0..1" .-> svc_postgres diff --git a/roles/web-app-confluence/meta/services.yml b/roles/web-app-confluence/meta/services.yml index 208c8a3855..1dbdbff4be 100644 --- a/roles/web-app-confluence/meta/services.yml +++ b/roles/web-app-confluence/meta/services.yml @@ -69,11 +69,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -mcp: - enabled: false - shared: false - blocker: hosted_only - source_url: https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/ - notes: Rovo MCP is Atlassian Cloud only; this role deploys an unsupported self-hosted container. diff --git a/roles/web-app-discourse/README.md b/roles/web-app-discourse/README.md index 69c839bf69..319da64743 100644 --- a/roles/web-app-discourse/README.md +++ b/roles/web-app-discourse/README.md @@ -42,7 +42,6 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end subgraph dependents [Dependents] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] diff --git a/roles/web-app-discourse/meta/services.yml b/roles/web-app-discourse/meta/services.yml index 7ae9637a58..025c324c05 100644 --- a/roles/web-app-discourse/meta/services.yml +++ b/roles/web-app-discourse/meta/services.yml @@ -104,12 +104,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -# nocheck: playwright-service-flag no MCP surface is served, so no spec can exercise one -mcp: - enabled: false - shared: false - blocker: hosted_only - source_url: https://meta.discourse.org/ - notes: An MCP repository exists, but no stable supported server contract has been confirmed. diff --git a/roles/web-app-fider/README.md b/roles/web-app-fider/README.md index b859b557bf..74f0d0a8c4 100644 --- a/roles/web-app-fider/README.md +++ b/roles/web-app-fider/README.md @@ -48,6 +48,7 @@ flowchart LR svc_javascript["javascript"] svc_prometheus["prometheus"] svc_seaweedfs["seaweedfs"] + svc_fidermcp["fidermcp"] end dep_svc_db_postgres -. "0..1" .-> svc_postgres dep_svc_db_redis -. "0..1" .-> svc_redis diff --git a/roles/web-app-fider/meta/mcp.yml b/roles/web-app-fider/meta/mcp.yml new file mode 100644 index 0000000000..e0797c466a --- /dev/null +++ b/roles/web-app-fider/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-fider + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: fidermcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-fider/files/mcp/tools.json + specification_sha256: sha256:50c9683dcad2b66b3a1c07b87fb553bdf5543103cfccb75522374362528fb524 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - fider_get_suggestion + - fider_list_tags + - fider_search_suggestions + schema_sha256: sha256:50c9683dcad2b66b3a1c07b87fb553bdf5543103cfccb75522374362528fb524 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-fider/meta/services.yml b/roles/web-app-fider/meta/services.yml index 842b992182..9761791198 100644 --- a/roles/web-app-fider/meta/services.yml +++ b/roles/web-app-fider/meta/services.yml @@ -74,3 +74,18 @@ seaweedfs: bond: 1 enabled: "{{ 'web-app-seaweedfs' in group_names }}" shared: "{{ 'web-app-seaweedfs' in group_names }}" +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +fidermcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: fidermcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-fider/meta/variants.yml b/roles/web-app-fider/meta/variants.yml index 5bf6043382..cba5f0d782 100644 --- a/roles/web-app-fider/meta/variants.yml +++ b/roles/web-app-fider/meta/variants.yml @@ -33,6 +33,9 @@ seaweedfs: enabled: true shared: true + mcp: + enabled: true + shared: true - services: dashboard: enabled: false @@ -62,3 +65,6 @@ seaweedfs: enabled: false shared: false + mcp: + enabled: false + shared: false diff --git a/roles/web-app-flowise/README.md b/roles/web-app-flowise/README.md index 3834a79467..61f8571c63 100644 --- a/roles/web-app-flowise/README.md +++ b/roles/web-app-flowise/README.md @@ -39,7 +39,6 @@ flowchart LR svc_sso["sso"] svc_ldap["ldap ❌"] svc_litellm["litellm"] - svc_mcp["mcp"] svc_qdrant["qdrant"] svc_postgres["postgres"] svc_flowise["flowise"] @@ -61,10 +60,8 @@ flowchart LR dep_svc_db_qdrant -. "0..1" .-> svc_qdrant dep_svc_db_redis -. "0..1" .-> svc_redis dep_web_app_baserow -. "0..1" .-> svc_baserow - dep_web_app_baserow -. "0..1" .-> svc_mcp dep_web_app_dashboard -. "0..1" .-> svc_dashboard dep_web_app_homeassistant -. "0..1" .-> svc_homeassistant - dep_web_app_homeassistant -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo @@ -157,12 +154,12 @@ is a deliberate operator step. ### Default state -Off. `services.mcp.enabled` is false unless an MCP server role is part of the +Off. `mcp.enabled` is false unless an MCP server role is part of the deployment. ### How to disable -Remove the MCP server roles, or pin `services.mcp.enabled: false` for this role. +Remove the MCP server roles, or pin `mcp.enabled: false` for this role. The security-check and deny-list overrides are then not rendered. ## Credits diff --git a/roles/web-app-flowise/meta/mcp.yml b/roles/web-app-flowise/meta/mcp.yml new file mode 100644 index 0000000000..2db8254285 --- /dev/null +++ b/roles/web-app-flowise/meta/mcp.yml @@ -0,0 +1,18 @@ +--- +enabled: true +shared: true +classification: native_client +direction: client +source_url: https://github.com/FlowiseAI/Flowise/tree/flowise%403.1.4/packages/server/src/routes/custom-mcp-servers +supported_version: 3.1.4 +minimum_version: 3.0.0 +implementation: native +supported_transports: + - streamable_http +supported_auths: + - bearer_token + - app_password + - upstream_session +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-flowise/meta/services.yml b/roles/web-app-flowise/meta/services.yml index ee6f6687dc..d3ece27225 100644 --- a/roles/web-app-flowise/meta/services.yml +++ b/roles/web-app-flowise/meta/services.yml @@ -32,23 +32,6 @@ litellm: bond: 1 enabled: "{{ 'svc-ai-litellm' in group_names }}" shared: "{{ 'svc-ai-litellm' in group_names }}" -mcp: - bond: 1 - enabled: "{{ 'web-app-homeassistant' in group_names or 'web-app-baserow' in group_names }}" - shared: "{{ 'web-app-homeassistant' in group_names or 'web-app-baserow' in group_names }}" - direction: client - source_url: https://docs.flowiseai.com/tutorials/tools-and-mcp - minimum_version: 3.0.0 - notes: Custom MCP tool node, configured per flow; no instance-level registry API, so servers cannot be preregistered. Arbitrary stdio commands stay off in - deployments. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: user - implementation: native - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: playwright-service-flag - DB engine, covered by role-local integration tests qdrant: bond: 1 @@ -71,10 +54,15 @@ flowise: shared: true backup: no_stop_required: false - image: flowiseai/flowise - version: 3.1.3 + image: node + # nocheck: docker-version + version: "24-bookworm-slim" + npm_version: 3.1.4 + sqlite_version: 12.11.1 + custom: true name: flowise - min_storage: 1195MB + # nocheck: min-storage-headroom Reason: the deployed image is built here from node:24-bookworm-slim and measures 4.8 GB, so the base image transfer is not its footprint + min_storage: 6GB cpus: "1.0" mem_reservation: 1g mem_limit: 2g diff --git a/roles/web-app-flowise/meta/variants.yml b/roles/web-app-flowise/meta/variants.yml index 439e2d41d0..8a4b8a498b 100644 --- a/roles/web-app-flowise/meta/variants.yml +++ b/roles/web-app-flowise/meta/variants.yml @@ -29,9 +29,6 @@ litellm: enabled: true shared: true - mcp: - enabled: true - shared: true qdrant: enabled: true shared: true @@ -51,6 +48,9 @@ baserow: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -76,9 +76,6 @@ litellm: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false # nocheck: variants-static-override backup shared stays literally true qdrant: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -98,3 +95,6 @@ baserow: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-flowise/vars/main.yml b/roles/web-app-flowise/vars/main.yml index 1f3d3a0799..4cae576f7c 100644 --- a/roles/web-app-flowise/vars/main.yml +++ b/roles/web-app-flowise/vars/main.yml @@ -5,6 +5,10 @@ http_port: "{{ lookup('config', application_id, 'services.flowise.ports.local.ht FLOWISE_CONTAINER: "{{ lookup('config', application_id, 'services.flowise.name') }}" FLOWISE_PORT_INTERNAL: 3000 +FLOWISE_NODE_MODULES: /opt/flowise/node_modules +FLOWISE_BUILD_DIR: "{{ [lookup('container', application_id, 'directories.instance'), 'build'] | path_join }}" +FLOWISE_NPM_VERSION: "{{ lookup('config', application_id, 'services.flowise.npm_version') }}" +FLOWISE_SQLITE_VERSION: "{{ lookup('config', application_id, 'services.flowise.sqlite_version') }}" FLOWISE_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}" @@ -13,7 +17,7 @@ FLOWISE_LITELLM_API_KEY: "{{ lookup('config', application_id, 'credentials.litel FLOWISE_QDRANT_INTERNAL_URL: "{{ lookup('engine', 'qdrant', application_id, 'url') }}" -FLOWISE_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" +FLOWISE_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" FLOWISE_MCP_SERVERS: >- {%- set out = [] -%} {%- if FLOWISE_MCP_ENABLED | bool -%} @@ -22,3 +26,19 @@ FLOWISE_MCP_SERVERS: >- {%- endfor -%} {%- endif -%} {{ out }} +FLOWISE_API_BOT_USER: "{{ lookup('users', 'flowise-api-bot') }}" + +FLOWISE_API_BOT_PASSWORD: "{{ lookup('config', application_id, 'credentials.api_bot_password') }}" +FLOWISE_MCP_API_KEY_NAME: "infinito:mcp-registry" +FLOWISE_MCP_DESIRED: >- + {%- set out = [] -%} + {%- for server in FLOWISE_MCP_SERVERS -%} + {%- set _ = out.append({ + 'id': server.id, + 'url': server.url, + 'transport': server.transport | replace('-', '_'), + 'header': 'Authorization', + 'token': server | mcp_authorization, + 'tools': []}) -%} + {%- endfor -%} + {{ out }} diff --git a/roles/web-app-gitea/README.md b/roles/web-app-gitea/README.md index f25bf8d206..7d46c9f37a 100644 --- a/roles/web-app-gitea/README.md +++ b/roles/web-app-gitea/README.md @@ -20,11 +20,9 @@ flowchart LR dep_svc_db_openldap["svc-db-openldap 🐳🐝"] dep_svc_db_redis["svc-db-redis 🐳🐝"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_mailu["web-app-mailu 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] @@ -40,7 +38,6 @@ flowchart LR svc_mariadb["mariadb"] svc_gitea["gitea"] svc_giteamcp["giteamcp"] - svc_mcp["mcp"] svc_redis["redis"] svc_minio["minio ❌"] svc_seaweedfs["seaweedfs"] @@ -53,11 +50,9 @@ flowchart LR dep_svc_db_openldap -. "0..1" .-> svc_ldap dep_svc_db_redis -. "0..1" .-> svc_redis dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openclaw -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs dep_web_svc_css -. "0..1" .-> svc_css @@ -155,7 +150,7 @@ repository, issue, pull request, search, file, branch, tag, commit and release. ### Default state -Off. `services.mcp.enabled` is false unless `web-app-hermes` or +Off. `mcp.enabled` is false unless `web-app-hermes` or `web-app-openclaw` is deployed alongside. ### Public vhost @@ -166,7 +161,7 @@ vhost is answered with an SSO redirect, never with an MCP response. ### How to disable Remove the MCP client roles from the deployment, or pin -`services.mcp.enabled: false` for this role. The `giteamcp` service is then not +`mcp.enabled: false` for this role. The `giteamcp` service is then not rendered into the compose file. ## Credits diff --git a/roles/web-app-gitea/meta/mcp.yml b/roles/web-app-gitea/meta/mcp.yml new file mode 100644 index 0000000000..ae2a850bea --- /dev/null +++ b/roles/web-app-gitea/meta/mcp.yml @@ -0,0 +1,52 @@ +--- +enabled: true +shared: true +classification: sidecar_server +direction: server +source_url: https://pkg.go.dev/gitea.com/gitea/gitea-mcp +minimum_version: "1.24" +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: sidecar +credential: + owner: mcp-web-app-gitea + source: token_store + key: web-app-gitea +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: giteamcp + path: /mcp + port_key: http +tools: + allowlist: + - attachment_read + - get_commit + - get_dir_contents + - get_file_contents + - get_latest_release + - get_me + - get_release + - get_repository_tree + - get_tag + - get_user_orgs + - issue_read + - list_branches + - list_commits + - list_issues + - list_my_repos + - list_org_repos + - list_pull_requests + - list_releases + - list_tags + - pull_request_read + - search_issues + - search_org_teams + - search_repos + - search_users + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-gitea/meta/services.yml b/roles/web-app-gitea/meta/services.yml index 1f4656a7d8..9214afcca3 100644 --- a/roles/web-app-gitea/meta/services.yml +++ b/roles/web-app-gitea/meta/services.yml @@ -93,26 +93,6 @@ giteamcp: ports: internal: http: 8080 -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - direction: server - source_url: https://pkg.go.dev/gitea.com/gitea/gitea-mcp - minimum_version: "1.24" - notes: Project-owned package; packaging and auth maturity gate it. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: administrator - implementation: sidecar - endpoint: - service_key: giteamcp - path: /mcp - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: dynamic-flag # nocheck: playwright-service-flag — DB engine, covered by role-local integration tests redis: diff --git a/roles/web-app-gitea/meta/variants.yml b/roles/web-app-gitea/meta/variants.yml index 281401765a..a4ea247561 100644 --- a/roles/web-app-gitea/meta/variants.yml +++ b/roles/web-app-gitea/meta/variants.yml @@ -26,9 +26,6 @@ matomo: enabled: true shared: true - mcp: - enabled: true - shared: true sso: enabled: true shared: true @@ -41,6 +38,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -64,9 +64,6 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -79,6 +76,9 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -104,12 +104,12 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true seaweedfs: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-gitea/templates/playwright.env.j2 b/roles/web-app-gitea/templates/playwright.env.j2 index bfe87087a3..5973992a6e 100644 --- a/roles/web-app-gitea/templates/playwright.env.j2 +++ b/roles/web-app-gitea/templates/playwright.env.j2 @@ -24,7 +24,7 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }} MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} PERSONA_ADMINISTRATOR_BLOCKED=true diff --git a/roles/web-app-gitea/vars/main.yml b/roles/web-app-gitea/vars/main.yml index 7979b8ff6e..d7b8d2f6e3 100644 --- a/roles/web-app-gitea/vars/main.yml +++ b/roles/web-app-gitea/vars/main.yml @@ -29,11 +29,11 @@ GITEA_REDIS_PASSWORD: "{{ lookup('engine', 'redis', application_id, 'password') GITEA_INTERNAL_API_URL: "http://{{ entity_name }}:{{ lookup('config', application_id, 'services.gitea.ports.internal.http') }}/api/v1" -GITEA_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" -GITEA_MCP_SERVICE_KEY: "{{ lookup('config', application_id, 'services.mcp.endpoint.service_key') }}" +GITEA_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" +GITEA_MCP_SERVICE_KEY: "{{ lookup('config', application_id, 'mcp.endpoint.service_key') }}" GITEA_MCP_CONTAINER: "{{ lookup('config', application_id, 'services.giteamcp.name') }}" GITEA_MCP_PORT: "{{ lookup('config', application_id, 'services.giteamcp.ports.internal.http') }}" -GITEA_MCP_URL: "http://{{ GITEA_MCP_SERVICE_KEY }}:{{ GITEA_MCP_PORT }}{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +GITEA_MCP_URL: "http://{{ GITEA_MCP_SERVICE_KEY }}:{{ GITEA_MCP_PORT }}{{ lookup('config', application_id, 'mcp.endpoint.path') }}" GITEA_MCP_UPSTREAM_URL: "http://{{ entity_name }}:{{ lookup('config', application_id, 'services.gitea.ports.internal.http') }}" GITEA_MCP_TOKEN_NAME: "gitea-mcp" GITEA_MCP_TOKEN_SCOPES: "read:repository,read:issue,read:user,read:organization,read:notification" diff --git a/roles/web-app-gitlab/README.md b/roles/web-app-gitlab/README.md index cc3e77cd1c..2cec4f1058 100644 --- a/roles/web-app-gitlab/README.md +++ b/roles/web-app-gitlab/README.md @@ -19,12 +19,9 @@ flowchart LR dep_svc_db_postgres["svc-db-postgres 🐳🐝"] dep_svc_db_redis["svc-db-redis 🐳🐝"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_mailu["web-app-mailu 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] - dep_web_app_openwebui["web-app-openwebui 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] @@ -36,7 +33,6 @@ flowchart LR svc_dashboard["dashboard"] svc_matomo["matomo"] svc_email["email"] - svc_mcp["mcp"] svc_redis["redis"] svc_postgres["postgres"] svc_gitlab["gitlab"] @@ -58,12 +54,9 @@ flowchart LR dep_svc_db_postgres -. "0..1" .-> svc_postgres dep_svc_db_redis -. "0..1" .-> svc_redis dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openclaw -. "0..1" .-> svc_mcp - dep_web_app_openwebui -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs dep_web_svc_css -. "0..1" .-> svc_css @@ -84,7 +77,7 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( ## MCP server -`services.mcp` declares the Model Context Protocol surface GitLab serves natively from its Rails API. +`mcp` declares the Model Context Protocol surface GitLab serves natively from its Rails API. | Property | Value | | --- | --- | @@ -92,13 +85,13 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( | Transport | streamable HTTP (JSON-RPC `initialize`, `tools/list`, `tools/call`) | | Auth | `Authorization: Bearer ` | | Token subject | the `root` account | -| Default state | off; `services.mcp.enabled` turns on when `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is in the deployment | +| Default state | off; `mcp.enabled` turns on when `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is in the deployment | With the service enabled, `tasks/03_mcp.yml` reads the token stored for `administrator` under this role's id, probes it against the running instance with a JSON-RPC `initialize` call, mints a replacement through `gitlab-rails runner` when the stored token is missing or rejected, writes the fresh token back through `sys-token-store`, and fails the deploy when the re-probe is still rejected. The minted token is a personal access token carrying the `mcp` scope with a 364-day expiry. That scope is filtered out of the interactive token picker, so tokens for this endpoint are created programmatically. The role attaches to the shared overlay declared in `meta/networks.yml` so client containers can reach the endpoint container-to-container; the overlay alias resolves to workhorse. -Tool categories exposed at the pinned version cover issues and work items, merge requests (including diffs and conflicts), pipelines and jobs, labels, project and group search, repository files and commits, and instance metadata. The set includes mutating tools (`create_merge_request`, `create_workitem_note`, `link_work_items`). GitLab enforces no server-side read-only mode: the `mcp` scope grants both read and create access, and the only restriction mechanism is the per-request `X-Gitlab-Enabled-Mcp-Server-Tools` header, which the clients in this repository do not send. `services.mcp.tools.read_only_default` and `services.mcp.tools.mutating_tools_enabled` are declarative metadata, not an enforced policy. Every tool call runs with the blast radius of the `root` account. +Tool categories exposed at the pinned version cover issues and work items, merge requests (including diffs and conflicts), pipelines and jobs, labels, project and group search, repository files and commits, and instance metadata. The set includes mutating tools (`create_merge_request`, `create_workitem_note`, `link_work_items`). GitLab enforces no server-side read-only mode: the `mcp` scope grants both read and create access, and the only restriction mechanism is the per-request `X-Gitlab-Enabled-Mcp-Server-Tools` header, which the clients in this repository do not send. `mcp.tools.read_only_default` and `mcp.tools.mutating_tools_enabled` are declarative metadata, not an enforced policy. Every tool call runs with the blast radius of the `root` account. A Playwright scenario asserts that an unauthenticated request to the endpoint is never answered with a 2xx. @@ -108,11 +101,11 @@ A Playwright scenario asserts that an unauthenticated request to the endpoint is ### Default state -Off. `services.mcp.enabled` is true only while `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is part of the deployment. The endpoint additionally requires a Premium or Ultimate licence, so a Community deployment leaves it unreachable regardless of the flag. +Off. `mcp.enabled` is true only while `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is part of the deployment. The endpoint additionally requires a Premium or Ultimate licence, so a Community deployment leaves it unreachable regardless of the flag. ### How to disable -Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. The token is then neither minted nor stored, and the overlay attachment is dropped. +Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The token is then neither minted nor stored, and the overlay attachment is dropped. ## Quick Setup diff --git a/roles/web-app-gitlab/meta/mcp.yml b/roles/web-app-gitlab/meta/mcp.yml new file mode 100644 index 0000000000..8ba5225dd8 --- /dev/null +++ b/roles/web-app-gitlab/meta/mcp.yml @@ -0,0 +1,55 @@ +--- +enabled: true +shared: true +classification: native_server +direction: server +source_url: https://docs.gitlab.com/user/gitlab_duo/model_context_protocol/mcp_server/ +minimum_version: "18.0" +transport: streamable_http +exposure: public +auth: bearer_token +auth_subject: service_account +implementation: native +credential: + owner: mcp-web-app-gitlab + source: token_store + key: web-app-gitlab +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: gitlab + path: /api/v4/mcp + port_key: http +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 500 + stream_seconds: 300 +tools: + allowlist: + - create_issue + - create_merge_request + - create_workitem_note + - get_issue + - get_job_log + - get_mcp_server_version + - get_merge_request + - get_merge_request_commits + - get_merge_request_conflicts + - get_merge_request_diffs + - get_merge_request_pipelines + - get_pipeline_jobs + - get_saved_view_work_items + - get_work_item_types + - get_workitem_notes + - link_work_items + - manage_pipeline + - search + - search_labels + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-gitlab/meta/services.yml b/roles/web-app-gitlab/meta/services.yml index c1ebfe41bf..ea77325d02 100644 --- a/roles/web-app-gitlab/meta/services.yml +++ b/roles/web-app-gitlab/meta/services.yml @@ -22,26 +22,6 @@ email: bond: 1 enabled: "{{ 'web-app-mailu' in group_names }}" shared: "{{ 'web-app-mailu' in group_names }}" -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names or 'web-app-openwebui' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names or 'web-app-openwebui' in group_names }}" - direction: server - source_url: https://docs.gitlab.com/user/gitlab_duo/model_context_protocol/mcp_server/ - minimum_version: "18.0" - notes: Beta and tier-gated to Premium or Ultimate; operator-gated here. - transport: streamable_http - exposure: public - auth: bearer_token - auth_subject: administrator - implementation: native - endpoint: - service_key: gitlab - path: /api/v4/mcp - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: shared # nocheck: dynamic-flag # nocheck: playwright-service-flag @@ -71,7 +51,6 @@ gitlab: enabled: true # nocheck: dynamic-flag shared: true min_storage: 30GB - # Exception: the entity runs no container of its own (webservice/sidekiq/workhorse/gitaly/shell/migrations carry the real budgets); these keys only satisfy the entity resource contract cpus: "0.1" mem_reservation: 128m mem_limit: 256m diff --git a/roles/web-app-gitlab/meta/variants.yml b/roles/web-app-gitlab/meta/variants.yml index 8e18756365..94ee8ac672 100644 --- a/roles/web-app-gitlab/meta/variants.yml +++ b/roles/web-app-gitlab/meta/variants.yml @@ -19,9 +19,6 @@ matomo: enabled: true shared: true - mcp: - enabled: true - shared: true sso: enabled: true shared: true @@ -38,6 +35,9 @@ seaweedfs: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -54,9 +54,6 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -73,3 +70,6 @@ seaweedfs: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-gitlab/templates/playwright.env.j2 b/roles/web-app-gitlab/templates/playwright.env.j2 index 9c566b7900..eeec60bfe9 100644 --- a/roles/web-app-gitlab/templates/playwright.env.j2 +++ b/roles/web-app-gitlab/templates/playwright.env.j2 @@ -16,9 +16,9 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }} MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} -MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'services.mcp.endpoint.path') | dotenv_quote }} +MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'mcp.endpoint.path') | dotenv_quote }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} PERSONA_ADMINISTRATOR_BLOCKED=true diff --git a/roles/web-app-gitlab/vars/main.yml b/roles/web-app-gitlab/vars/main.yml index c0845cd84f..01ac0c6150 100644 --- a/roles/web-app-gitlab/vars/main.yml +++ b/roles/web-app-gitlab/vars/main.yml @@ -31,8 +31,8 @@ GITLAB_OIDC_REDIRECT_URI: "{{ (GITLAB_URL | regex_replace('/$', '')) ~ '/users/a GITLAB_INIT_ROOT_PASSWORD: "{{ lookup('users', 'administrator').password }}" GITLAB_ROOT_USERNAME: "root" -GITLAB_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" -GITLAB_MCP_PATH: "{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +GITLAB_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" +GITLAB_MCP_PATH: "{{ lookup('config', application_id, 'mcp.endpoint.path') }}" GITLAB_MCP_TOKEN_NAME: "infinito-mcp" GITLAB_RAILS_BIN: "/srv/gitlab/bin/rails" # nocheck: literal-path CNG images ship no omnibus gitlab-rails wrapper GITLAB_MCP_TOKEN_LIFETIME_DAYS: 364 diff --git a/roles/web-app-hermes/README.md b/roles/web-app-hermes/README.md index c39e120381..49e10dc8d2 100644 --- a/roles/web-app-hermes/README.md +++ b/roles/web-app-hermes/README.md @@ -30,7 +30,6 @@ flowchart LR svc_sso["sso ❌"] svc_logout["logout ❌"] svc_litellm["litellm"] - svc_mcp["mcp"] svc_dashboard["dashboard"] svc_matomo["matomo"] svc_css["css"] @@ -40,30 +39,17 @@ flowchart LR end subgraph dependents [Dependents] dpt_svc_ai_robot["svc-ai-robot 💻"] - dpt_web_app_gitea["web-app-gitea 🐳🐝"] - dpt_web_app_gitlab["web-app-gitlab 🐳🐝"] - dpt_web_app_homeassistant["web-app-homeassistant 🐳🐝"] - dpt_web_app_jenkins["web-app-jenkins 🐳🐝"] - dpt_web_app_mattermost["web-app-mattermost 🐳🐝"] - dpt_web_app_moodle["web-app-moodle 🐳🐝"] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] end dep_svc_ai_litellm -. "0..1" .-> svc_litellm dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_web_app_dashboard -. "0..1" .-> svc_dashboard dep_web_app_homeassistant -. "0..1" .-> svc_homeassistant - dep_web_app_homeassistant -. "0..1" .-> svc_mcp dep_web_app_keycloak -- "1:1" --> svc_sso dep_web_app_matomo -. "0..1" .-> svc_matomo dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_svc_css -. "0..1" .-> svc_css svc_hermes -. "0..1" .-> dpt_svc_ai_robot - svc_hermes -. "0..1" .-> dpt_web_app_gitea - svc_hermes -. "0..1" .-> dpt_web_app_gitlab - svc_hermes -. "0..1" .-> dpt_web_app_homeassistant - svc_hermes -. "0..1" .-> dpt_web_app_jenkins - svc_hermes -. "0..1" .-> dpt_web_app_mattermost - svc_hermes -. "0..1" .-> dpt_web_app_moodle svc_hermes -. "0..1" .-> dpt_web_app_nextcloud ``` @@ -151,12 +137,12 @@ the MCP credentials refuses an unauthenticated caller. ### Default state -Off. `services.mcp.enabled` is false unless an MCP server role is part of the +Off. `mcp.enabled` is false unless an MCP server role is part of the deployment. ### How to disable -Remove the MCP server roles, or pin `services.mcp.enabled: false` for this role. +Remove the MCP server roles, or pin `mcp.enabled: false` for this role. The config then renders with an empty `mcp_servers` map and the verification step is skipped. diff --git a/roles/web-app-hermes/meta/mcp.yml b/roles/web-app-hermes/meta/mcp.yml new file mode 100644 index 0000000000..a59c83b866 --- /dev/null +++ b/roles/web-app-hermes/meta/mcp.yml @@ -0,0 +1,20 @@ +--- +enabled: true +shared: true +classification: native_client +direction: client +source_url: https://hermes-agent.nousresearch.com/ +supported_version: v2026.7.20 +minimum_version: 0.2.0 +implementation: native +supported_transports: + - streamable_http + - sse +supported_auths: + - bearer_token + - app_password + - upstream_session + - basic_auth +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-hermes/meta/services.yml b/roles/web-app-hermes/meta/services.yml index 5b66985f7e..313078280d 100644 --- a/roles/web-app-hermes/meta/services.yml +++ b/roles/web-app-hermes/meta/services.yml @@ -54,24 +54,6 @@ litellm: enabled: "{{ 'svc-ai-litellm' in group_names }}" shared: "{{ 'svc-ai-litellm' in group_names }}" -# nocheck: shared-provider -mcp: - bond: 1 - enabled: "{{ 'web-app-homeassistant' in group_names }}" - shared: "{{ 'web-app-homeassistant' in group_names }}" - direction: client - source_url: https://hermes-agent.nousresearch.com/ - minimum_version: 0.2.0 - notes: MCP client since v0.2.0, server since v0.6.0. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: user - implementation: native - tools: - read_only_default: true - mutating_tools_enabled: false - dashboard: bond: 1 enabled: "{{ 'web-app-dashboard' in group_names }}" diff --git a/roles/web-app-hermes/meta/variants.yml b/roles/web-app-hermes/meta/variants.yml index 4a1aff0140..de0d0e0520 100644 --- a/roles/web-app-hermes/meta/variants.yml +++ b/roles/web-app-hermes/meta/variants.yml @@ -8,9 +8,6 @@ litellm: enabled: true shared: true - mcp: - enabled: true - shared: true dashboard: enabled: true shared: true @@ -26,6 +23,9 @@ homeassistant: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -33,9 +33,6 @@ litellm: enabled: false shared: false # nocheck: variants-static-override variant pins a literal off-switch - mcp: - enabled: false - shared: false # nocheck: variants-static-override variant pins a literal off-switch dashboard: enabled: false shared: false # nocheck: variants-static-override variant pins a literal off-switch @@ -51,3 +48,6 @@ homeassistant: enabled: false shared: false # nocheck: variants-static-override variant pins a literal off-switch + mcp: + enabled: false + shared: false diff --git a/roles/web-app-hermes/templates/playwright.env.j2 b/roles/web-app-hermes/templates/playwright.env.j2 index 9f03ac9e88..c68a0249b7 100644 --- a/roles/web-app-hermes/templates/playwright.env.j2 +++ b/roles/web-app-hermes/templates/playwright.env.j2 @@ -23,6 +23,6 @@ LITELLM_SERVICE_ENABLED={{ lookup('config', application_id, 'services.litellm.en MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} # nocheck: playwright-service-gate -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} PERSONA_BIBER_BLOCKED=true diff --git a/roles/web-app-hermes/vars/main.yml b/roles/web-app-hermes/vars/main.yml index 0f2953fefa..b2e658dc2b 100644 --- a/roles/web-app-hermes/vars/main.yml +++ b/roles/web-app-hermes/vars/main.yml @@ -10,4 +10,4 @@ HERMES_LITELLM_ENABLED: "{{ lookup('config', application_id, 'services.litellm.e HERMES_LITELLM_API_KEY: "{{ lookup('config', application_id, 'credentials.litellm_api_key') }}" HERMES_CONFIG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'config.yaml' ] | path_join }}" HERMES_DOCKER_USER_ID: 10000 -HERMES_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'services.mcp.enabled') | bool) else [] }}" +HERMES_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'mcp.enabled') | bool) else [] }}" diff --git a/roles/web-app-homeassistant/README.md b/roles/web-app-homeassistant/README.md index ec482211b1..3c2f6418ee 100644 --- a/roles/web-app-homeassistant/README.md +++ b/roles/web-app-homeassistant/README.md @@ -17,14 +17,11 @@ flowchart LR subgraph deps [Dependencies] dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] end subgraph role [web-app-homeassistant 🐳🐝] svc_homeassistant["homeassistant"] - svc_mcp["mcp"] svc_sso["sso ❌"] svc_logout["logout ❌"] svc_dashboard["dashboard"] @@ -39,9 +36,7 @@ flowchart LR end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -- "1:1" --> svc_sso - dep_web_app_openclaw -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus svc_homeassistant -. "0..1" .-> dpt_web_app_flowise svc_homeassistant -. "0..1" .-> dpt_web_app_hermes @@ -103,7 +98,7 @@ docker run --rm -it \ ## MCP Server Home Assistant ships the MCP server as a native integration. The role enables it -through `services.mcp.enabled`, which stays true only while an MCP client role is +through `mcp.enabled`, which stays true only while an MCP client role is part of the deployment. ### Endpoint @@ -136,7 +131,7 @@ for the current state. ### Default state -Off. `services.mcp.enabled` is false unless `web-app-hermes` or +Off. `mcp.enabled` is false unless `web-app-hermes` or `web-app-openclaw` is deployed alongside. ### Tool scope @@ -148,7 +143,7 @@ or actuated through MCP even though the write tools are listed. ### How to disable Remove the MCP client roles from the deployment, or pin -`services.mcp.enabled: false` for this role in the inventory. The integration is +`mcp.enabled: false` for this role in the inventory. The integration is then not configured and the endpoint stops answering. ## Credits diff --git a/roles/web-app-homeassistant/meta/mcp.yml b/roles/web-app-homeassistant/meta/mcp.yml new file mode 100644 index 0000000000..9d90720275 --- /dev/null +++ b/roles/web-app-homeassistant/meta/mcp.yml @@ -0,0 +1,38 @@ +--- +enabled: true +shared: true +classification: native_server +direction: server +source_url: https://www.home-assistant.io/integrations/mcp_server/ +minimum_version: "2025.2" +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: native +credential: + owner: mcp-web-app-homeassistant + source: token_store + key: web-app-homeassistant +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: homeassistant + path: /api/mcp + port_key: http +tools: + allowlist: + - GetDateTime + - GetLiveContext + - HassBroadcast + - HassCancelAllTimers + - HassListAddItem + - HassListCompleteItem + - HassListRemoveItem + - HassTurnOff + - HassTurnOn + - todo_get_items + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-homeassistant/meta/services.yml b/roles/web-app-homeassistant/meta/services.yml index c35a0ad444..3e32f5deab 100644 --- a/roles/web-app-homeassistant/meta/services.yml +++ b/roles/web-app-homeassistant/meta/services.yml @@ -30,28 +30,6 @@ homeassistant: proxy: service_key: homeassistant -# nocheck: playwright-service-flag -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - direction: server - source_url: https://www.home-assistant.io/integrations/mcp_server/ - minimum_version: "2025.2" - notes: Native mcp_server integration, Streamable HTTP under /api/mcp. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: administrator - implementation: native - endpoint: - service_key: homeassistant - path: /api/mcp - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false - # nocheck: sso, dynamic-flag, playwright-service-flag sso: enabled: false diff --git a/roles/web-app-homeassistant/meta/variants.yml b/roles/web-app-homeassistant/meta/variants.yml index 2307d4d57a..d699f3d040 100644 --- a/roles/web-app-homeassistant/meta/variants.yml +++ b/roles/web-app-homeassistant/meta/variants.yml @@ -5,9 +5,6 @@ - services: container_backup: enabled: true - mcp: - enabled: true - shared: true dashboard: enabled: true shared: true @@ -15,16 +12,19 @@ enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false # nocheck: variants-static-override backup shared stays literally true dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-homeassistant/templates/playwright.env.j2 b/roles/web-app-homeassistant/templates/playwright.env.j2 index 3995e30cba..a37f6ab5dc 100644 --- a/roles/web-app-homeassistant/templates/playwright.env.j2 +++ b/roles/web-app-homeassistant/templates/playwright.env.j2 @@ -3,7 +3,7 @@ CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }} HOMEASSISTANT_BASE_URL={{ lookup('tls', application_id, 'url.base') | dotenv_quote }} # nocheck: playwright-service-gate -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} PERSONA_ADMINISTRATOR_BLOCKED=true diff --git a/roles/web-app-homeassistant/vars/main.yml b/roles/web-app-homeassistant/vars/main.yml index d92cd8f660..f5d7f3202e 100644 --- a/roles/web-app-homeassistant/vars/main.yml +++ b/roles/web-app-homeassistant/vars/main.yml @@ -6,7 +6,7 @@ http_port: "{{ lookup('config', application_id, 'services.homeassistant.ports.lo HOMEASSISTANT_CONTAINER: "{{ lookup('config', application_id, 'services.homeassistant.name') }}" HOMEASSISTANT_CONFIG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'configuration.yaml' ] | path_join }}" HOMEASSISTANT_PORT_INTERNAL: "{{ lookup('config', application_id, 'services.homeassistant.ports.internal.http') }}" -HOMEASSISTANT_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" +HOMEASSISTANT_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" HOMEASSISTANT_ADMIN_USERNAME: "{{ lookup('users', 'administrator').username }}" HOMEASSISTANT_ADMIN_PASSWORD: "{{ lookup('config', application_id, 'credentials.administrator_password') }}" HOMEASSISTANT_TOKEN_CLIENT_NAME: "infinito-mcp" diff --git a/roles/web-app-jellyfin/README.md b/roles/web-app-jellyfin/README.md index 2f301fd466..00b2acd3f0 100644 --- a/roles/web-app-jellyfin/README.md +++ b/roles/web-app-jellyfin/README.md @@ -34,6 +34,7 @@ flowchart LR svc_prometheus["prometheus"] svc_jellyfin["jellyfin"] svc_container_backup["container_backup"] + svc_jellyfinmcp["jellyfinmcp"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_openldap -. "0..1" .-> svc_ldap diff --git a/roles/web-app-jellyfin/meta/mcp.yml b/roles/web-app-jellyfin/meta/mcp.yml new file mode 100644 index 0000000000..268c5f6341 --- /dev/null +++ b/roles/web-app-jellyfin/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-jellyfin + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: jellyfinmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-jellyfin/files/mcp/tools.json + specification_sha256: sha256:08776fdcbed9f362d76e9954881a9184362da85b24e2e503f8259fd664fd85f5 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - jellyfin_get_item + - jellyfin_get_playback_status + - jellyfin_search_library + schema_sha256: sha256:08776fdcbed9f362d76e9954881a9184362da85b24e2e503f8259fd664fd85f5 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-jellyfin/meta/services.yml b/roles/web-app-jellyfin/meta/services.yml index 470f01c9e7..adb8fdd9b8 100644 --- a/roles/web-app-jellyfin/meta/services.yml +++ b/roles/web-app-jellyfin/meta/services.yml @@ -65,3 +65,18 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +jellyfinmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: jellyfinmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-jellyfin/meta/variants.yml b/roles/web-app-jellyfin/meta/variants.yml index 540e7a289f..8063fa32db 100644 --- a/roles/web-app-jellyfin/meta/variants.yml +++ b/roles/web-app-jellyfin/meta/variants.yml @@ -24,6 +24,9 @@ enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -50,6 +53,9 @@ enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -75,3 +81,6 @@ sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-jenkins/README.md b/roles/web-app-jenkins/README.md index d46000c034..ce1c0a2a79 100644 --- a/roles/web-app-jenkins/README.md +++ b/roles/web-app-jenkins/README.md @@ -18,10 +18,8 @@ flowchart LR dep_svc_bkp_volume_2_local["svc-bkp-volume-2-local 💻"] dep_svc_db_openldap["svc-db-openldap 🐳🐝"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] dep_web_svc_logout["web-svc-logout 🐳🐝"] @@ -34,17 +32,14 @@ flowchart LR svc_sso["sso"] svc_ldap["ldap"] svc_jenkins["jenkins"] - svc_mcp["mcp"] svc_css["css"] svc_container_backup["container_backup"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_openldap -. "0..1" .-> svc_ldap dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openclaw -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_svc_css -. "0..1" .-> svc_css dep_web_svc_logout -. "0..1" .-> svc_logout @@ -76,7 +71,7 @@ The role exposes Jenkins as an MCP server through the [MCP Server plugin](https: | Subject | administrator | | Tools | read-only by default, mutating tools off | -Enable the surface by deploying `web-app-hermes` or `web-app-openclaw` alongside Jenkins, or by forcing `services.mcp.enabled` on. Open WebUI skips the server because it cannot present basic auth. +Enable the surface by deploying `web-app-hermes` or `web-app-openclaw` alongside Jenkins, or by forcing `mcp.enabled` on. Open WebUI skips the server because it cannot present basic auth. At boot, `files/mcp-api-token.groovy` runs from `init.groovy.d` and mints an API token named `infinito-mcp` for the administrator account, writing the plain value to `/var/jenkins_home/secrets/infinito-mcp.token`. `tasks/01_mcp.yml` then reads that value out of the container, persists it through `sys-token-store`, and hard-fails the deploy when the controller rejects it. `MCP_DISCOVERED_SERVERS` picks the token up from the store and the client roles build the header via the `mcp_authorization` filter. @@ -92,7 +87,7 @@ curl -u ":" \ ### Default state -Off. `services.mcp.enabled` is true only while `web-app-hermes` or +Off. `mcp.enabled` is true only while `web-app-hermes` or `web-app-openclaw` is part of the deployment. ### Authorization subject @@ -110,7 +105,7 @@ unauthenticated `tools/list` is refused and returns no tool inventory. ### How to disable -Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. +Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The MCP Server plugin is then not installed and the boot hook mints no API token. ## Quick Setup diff --git a/roles/web-app-jenkins/meta/mcp.yml b/roles/web-app-jenkins/meta/mcp.yml new file mode 100644 index 0000000000..1978838850 --- /dev/null +++ b/roles/web-app-jenkins/meta/mcp.yml @@ -0,0 +1,46 @@ +--- +enabled: true +shared: true +classification: plugin_server +direction: server +source_url: https://plugins.jenkins.io/mcp-server/ +minimum_version: "2.462" +transport: streamable_http +exposure: internal +auth: basic_auth +auth_subject: service_account +implementation: plugin +credential: + owner: mcp-web-app-jenkins + source: token_store + key: web-app-jenkins +allowed_consumers: + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: jenkins + path: /mcp-server/mcp + port_key: http +tools: + allowlist: + - findJobsWithScmUrl + - getBuild + - getBuildChangeSets + - getBuildLog + - getBuildScm + - getFlakyFailures + - getJob + - getJobScm + - getJobs + - getQueueItem + - getReplayScripts + - getStatus + - getTestResults + - rebuildBuild + - replayBuild + - searchBuildLog + - triggerBuild + - updateBuild + - whoAmI + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-jenkins/meta/services.yml b/roles/web-app-jenkins/meta/services.yml index eaaa6ce0c1..013bd9018d 100644 --- a/roles/web-app-jenkins/meta/services.yml +++ b/roles/web-app-jenkins/meta/services.yml @@ -54,26 +54,6 @@ jenkins: mem_reservation: 1g mem_limit: 2g pids_limit: 1024 -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - direction: server - source_url: https://plugins.jenkins.io/mcp-server/ - minimum_version: "2.462" - notes: Plugin based; pinned like every other Jenkins plugin. - transport: streamable_http - exposure: internal - auth: basic_auth - auth_subject: administrator - implementation: plugin - endpoint: - service_key: jenkins - path: /mcp-server/mcp - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: playwright-service-flag — css consumer, no Playwright surface (provider: web-svc-css) css: bond: 1 diff --git a/roles/web-app-jenkins/meta/variants.yml b/roles/web-app-jenkins/meta/variants.yml index 5c86a33c09..008ab1dcd9 100644 --- a/roles/web-app-jenkins/meta/variants.yml +++ b/roles/web-app-jenkins/meta/variants.yml @@ -30,9 +30,9 @@ css: enabled: true shared: true - mcp: - enabled: true - shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -58,9 +58,9 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -86,6 +86,6 @@ prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false + mcp: + enabled: false + shared: false diff --git a/roles/web-app-jenkins/templates/playwright.env.j2 b/roles/web-app-jenkins/templates/playwright.env.j2 index 4c1dfc51e3..8733ec1bf0 100644 --- a/roles/web-app-jenkins/templates/playwright.env.j2 +++ b/roles/web-app-jenkins/templates/playwright.env.j2 @@ -20,7 +20,7 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }} MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} -MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'services.mcp.endpoint.path') | dotenv_quote }} +MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'mcp.endpoint.path') | dotenv_quote }} MCP_SERVICE_ENABLED={{ JENKINS_MCP_ENABLED | string | lower }} diff --git a/roles/web-app-jenkins/vars/main.yml b/roles/web-app-jenkins/vars/main.yml index 9e916f63c1..04fb3503d5 100644 --- a/roles/web-app-jenkins/vars/main.yml +++ b/roles/web-app-jenkins/vars/main.yml @@ -14,13 +14,13 @@ JENKINS_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enable JENKINS_LOOPBACK_URL: "http://localhost:{{ lookup('config', application_id, 'services.jenkins.ports.internal.http') }}" -JENKINS_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" -JENKINS_MCP_USERNAME: "{{ lookup('users', 'administrator').username }}" +JENKINS_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" +JENKINS_MCP_USERNAME: "{{ lookup('config', application_id, 'mcp.credential.owner') }}" JENKINS_MCP_TOKEN_NAME: "infinito-mcp" JENKINS_MCP_TOKEN_FILE: "/var/jenkins_home/secrets/infinito-mcp.token" JENKINS_MCP_IDENTITY_URL: "{{ JENKINS_LOOPBACK_URL }}/whoAmI/api/json" JENKINS_MCP_READY_URL: "{{ JENKINS_LOOPBACK_URL }}/login" -JENKINS_MCP_URL: "{{ JENKINS_LOOPBACK_URL }}{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +JENKINS_MCP_URL: "{{ JENKINS_LOOPBACK_URL }}{{ lookup('config', application_id, 'mcp.endpoint.path') }}" JENKINS_MCP_INITIALIZE_PAYLOAD: jsonrpc: "2.0" id: 1 diff --git a/roles/web-app-jira/README.md b/roles/web-app-jira/README.md index 8031d6d6cb..fe132660d0 100644 --- a/roles/web-app-jira/README.md +++ b/roles/web-app-jira/README.md @@ -36,7 +36,6 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_postgres -. "0..1" .-> svc_postgres diff --git a/roles/web-app-jira/meta/services.yml b/roles/web-app-jira/meta/services.yml index 40dc4b6a27..e90f6bbf58 100644 --- a/roles/web-app-jira/meta/services.yml +++ b/roles/web-app-jira/meta/services.yml @@ -68,11 +68,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -mcp: - enabled: false - shared: false - blocker: hosted_only - source_url: https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/ - notes: Rovo MCP is Atlassian Cloud only; this role deploys an unsupported self-hosted container. diff --git a/roles/web-app-listmonk/README.md b/roles/web-app-listmonk/README.md index 6d5e792a6f..0d3551b043 100644 --- a/roles/web-app-listmonk/README.md +++ b/roles/web-app-listmonk/README.md @@ -43,6 +43,7 @@ flowchart LR svc_css["css"] svc_hcaptcha["hcaptcha"] svc_prometheus["prometheus"] + svc_listmonkmcp["listmonkmcp"] end dep_svc_db_postgres -. "0..1" .-> svc_postgres dep_web_app_dashboard -. "0..1" .-> svc_dashboard diff --git a/roles/web-app-listmonk/meta/mcp.yml b/roles/web-app-listmonk/meta/mcp.yml new file mode 100644 index 0000000000..f16c7423e1 --- /dev/null +++ b/roles/web-app-listmonk/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-listmonk + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: listmonkmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-listmonk/files/mcp/tools.json + specification_sha256: sha256:3459dd13cfef868ed4de0123695292c8d75c8d3c612bef78fa8d5eaba297f06e +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - listmonk_get_campaign + - listmonk_get_stats + - listmonk_list_campaigns + schema_sha256: sha256:3459dd13cfef868ed4de0123695292c8d75c8d3c612bef78fa8d5eaba297f06e + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-listmonk/meta/services.yml b/roles/web-app-listmonk/meta/services.yml index 497bacb922..10a10df937 100644 --- a/roles/web-app-listmonk/meta/services.yml +++ b/roles/web-app-listmonk/meta/services.yml @@ -82,3 +82,18 @@ prometheus: bond: 1 enabled: "{{ 'web-app-prometheus' in group_names }}" shared: "{{ 'web-app-prometheus' in group_names }}" +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +listmonkmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: listmonkmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-listmonk/meta/variants.yml b/roles/web-app-listmonk/meta/variants.yml index 1c34469c1e..efc182029e 100644 --- a/roles/web-app-listmonk/meta/variants.yml +++ b/roles/web-app-listmonk/meta/variants.yml @@ -34,6 +34,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: asset: enabled: false @@ -64,3 +67,6 @@ css: enabled: false shared: false + mcp: + enabled: false + shared: false diff --git a/roles/web-app-mattermost/README.md b/roles/web-app-mattermost/README.md index 42b6aef528..19faa6c9f2 100644 --- a/roles/web-app-mattermost/README.md +++ b/roles/web-app-mattermost/README.md @@ -20,11 +20,9 @@ flowchart LR dep_svc_db_postgres["svc-db-postgres 🐳🐝"] dep_svc_db_redis["svc-db-redis 🐳🐝"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_mailu["web-app-mailu 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_app_seaweedfs["web-app-seaweedfs 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] @@ -39,7 +37,6 @@ flowchart LR svc_email["email"] svc_postgres["postgres"] svc_mattermost["mattermost"] - svc_mcp["mcp"] svc_redis["redis"] svc_minio["minio ❌"] svc_seaweedfs["seaweedfs"] @@ -56,11 +53,9 @@ flowchart LR dep_svc_db_postgres -. "0..1" .-> svc_postgres dep_svc_db_redis -. "0..1" .-> svc_redis dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openclaw -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs dep_web_svc_css -. "0..1" .-> svc_css @@ -142,7 +137,7 @@ Mattermost exposes a Model Context Protocol server through the prepackaged Agent ### Default state -`services.mcp.enabled` resolves to `true` only when `web-app-hermes` or `web-app-openclaw` is part of the same deployment, and is `false` otherwise. While it is `true` the deploy: +`mcp.enabled` resolves to `true` only when `web-app-hermes` or `web-app-openclaw` is part of the same deployment, and is `false` otherwise. While it is `true` the deploy: - sets `MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS=true`, - enables the `mattermost-ai` plugin through `mmctl --local`, @@ -170,14 +165,14 @@ The endpoint serves the Agents plugin's native Mattermost tool catalog: The catalog includes mutating entries (`create`, `update`, `archive`, `delete`, `send`, `upload`). The Agents plugin exposes no filter, scope or permission flag -that removes them, so `services.mcp.tools.mutating_tools_enabled: false` records +that removes them, so `mcp.tools.mutating_tools_enabled: false` records the deployment's intent rather than an enforced state. Every call is bounded by the permissions of the account the bearer token belongs to, which here is the administrator. ### How to disable -Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. The Agents plugin's MCP server is then left switched off and no personal access token is issued. +Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The Agents plugin's MCP server is then left switched off and no personal access token is issued. ## Configuration diff --git a/roles/web-app-mattermost/meta/mcp.yml b/roles/web-app-mattermost/meta/mcp.yml new file mode 100644 index 0000000000..163c38474e --- /dev/null +++ b/roles/web-app-mattermost/meta/mcp.yml @@ -0,0 +1,44 @@ +--- +enabled: true +shared: true +classification: plugin_server +direction: server +source_url: https://docs.mattermost.com/agents/mcpserver/README.html +minimum_version: "10.0" +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: plugin +credential: + owner: mcp-web-app-mattermost + source: token_store + key: web-app-mattermost +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: mattermost + path: /plugins/mattermost-ai/mcp-server/mcp + port_key: http +tools: + allowlist: + - add_user_to_channel + - create_channel + - create_post + - dm + - get_channel_info + - get_channel_members + - get_team_info + - get_team_members + - get_user_channels + - group_message + - list_agents + - read_channel + - read_file + - read_post + - search_posts + - search_users + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-mattermost/meta/services.yml b/roles/web-app-mattermost/meta/services.yml index a11f941ad1..c23617f255 100644 --- a/roles/web-app-mattermost/meta/services.yml +++ b/roles/web-app-mattermost/meta/services.yml @@ -58,26 +58,6 @@ mattermost: - web-app-keycloak - web-app-mailu lifecycle: beta -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - direction: server - source_url: https://docs.mattermost.com/agents/mcpserver/README.html - minimum_version: "10.0" - notes: Ships with the Agents plugin. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: administrator - implementation: plugin - endpoint: - service_key: mattermost - path: /plugins/mattermost-ai/mcp-server/mcp - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: dynamic-flag # nocheck: playwright-service-flag — DB engine, covered by role-local integration tests redis: diff --git a/roles/web-app-mattermost/meta/variants.yml b/roles/web-app-mattermost/meta/variants.yml index 626056d714..c945cebcae 100644 --- a/roles/web-app-mattermost/meta/variants.yml +++ b/roles/web-app-mattermost/meta/variants.yml @@ -19,9 +19,6 @@ matomo: enabled: true shared: true - mcp: - enabled: true - shared: true sso: enabled: true shared: true @@ -38,6 +35,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -54,9 +54,6 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -73,3 +70,6 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-mattermost/tasks/04_mcp/server.yml b/roles/web-app-mattermost/tasks/04_mcp/server.yml index a08b7e8744..9c4eea8d69 100644 --- a/roles/web-app-mattermost/tasks/04_mcp/server.yml +++ b/roles/web-app-mattermost/tasks/04_mcp/server.yml @@ -97,5 +97,5 @@ - (mattermost_mcp_toggle_final.rows[0][0] | trim) == 'true' fail_msg: >- The Agents plugin still reports mcp.enablePluginServer=false, so - {{ lookup('config', application_id, 'services.mcp.endpoint.path') }} + {{ lookup('config', application_id, 'mcp.endpoint.path') }} answers 404 and every MCP client would be pointed at a dead endpoint. diff --git a/roles/web-app-mattermost/templates/playwright.env.j2 b/roles/web-app-mattermost/templates/playwright.env.j2 index c6725de8e3..a3332d3364 100644 --- a/roles/web-app-mattermost/templates/playwright.env.j2 +++ b/roles/web-app-mattermost/templates/playwright.env.j2 @@ -24,7 +24,7 @@ MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enab MATTERMOST_BASE_URL={{ lookup('tls', 'web-app-mattermost', 'url.base') | dotenv_quote }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL | dotenv_quote }} diff --git a/roles/web-app-mattermost/vars/main.yml b/roles/web-app-mattermost/vars/main.yml index c4b2ff0c3f..e9de2399f1 100644 --- a/roles/web-app-mattermost/vars/main.yml +++ b/roles/web-app-mattermost/vars/main.yml @@ -7,10 +7,9 @@ container_port: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ MATTERMOST_CONTAINER: "{{ lookup('config', application_id, 'services.mattermost.name') }}" MATTERMOST_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, entity_name) }}" -# Exception: OIDC piggybacks on MM_GITLABSETTINGS_* -- Team Edition has no native OIDC provider for Keycloak MATTERMOST_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}" -MATTERMOST_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" +MATTERMOST_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" MATTERMOST_MCP_PLUGIN_ID: mattermost-ai MATTERMOST_MCP_TOKEN_DESCRIPTION: mcp diff --git a/roles/web-app-moodle/README.md b/roles/web-app-moodle/README.md index 3f0f976c12..ed8e29c0fe 100644 --- a/roles/web-app-moodle/README.md +++ b/roles/web-app-moodle/README.md @@ -19,12 +19,9 @@ flowchart LR dep_svc_db_mariadb["svc-db-mariadb 🐳🐝"] dep_svc_db_openldap["svc-db-openldap 🐳🐝"] dep_web_app_dashboard["web-app-dashboard 🐳🐝"] - dep_web_app_hermes["web-app-hermes 🐳🐝"] dep_web_app_keycloak["web-app-keycloak 🐳🐝"] dep_web_app_mailu["web-app-mailu 🐳🐝"] dep_web_app_matomo["web-app-matomo 🐳🐝"] - dep_web_app_openclaw["web-app-openclaw 🐳🐝"] - dep_web_app_openwebui["web-app-openwebui 🐳🐝"] dep_web_app_prometheus["web-app-prometheus 🐳🐝"] dep_web_svc_css["web-svc-css 💻"] dep_web_svc_logout["web-svc-logout 🐳🐝"] @@ -36,7 +33,6 @@ flowchart LR svc_matomo["matomo"] svc_email["email"] svc_ldap["ldap"] - svc_mcp["mcp"] svc_mariadb["mariadb"] svc_moodle["moodle"] svc_nginx["nginx"] @@ -49,12 +45,9 @@ flowchart LR dep_svc_db_mariadb -. "0..1" .-> svc_mariadb dep_svc_db_openldap -. "0..1" .-> svc_ldap dep_web_app_dashboard -. "0..1" .-> svc_dashboard - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo - dep_web_app_openclaw -. "0..1" .-> svc_mcp - dep_web_app_openwebui -. "0..1" .-> svc_mcp dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_svc_css -. "0..1" .-> svc_css dep_web_svc_logout -. "0..1" .-> svc_logout @@ -120,7 +113,7 @@ The role exposes a Model Context Protocol surface through the `webservice_mcp` p | Transport | Streamable HTTP (JSON-RPC 2.0 over `POST`) | | Auth | `Authorization: Bearer `, a Moodle web-service token | | Subject | the `administrator` account | -| Default state | off; `services.mcp.enabled` turns true when `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is in the inventory | +| Default state | off; `mcp.enabled` turns true when `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is in the inventory | Provisioning runs in `tasks/06_mcp.yml`: it copies the plugin into the code volume, runs `admin/cli/upgrade.php`, enables `enablewebservices`, appends `mcp` to `webserviceprotocols`, creates the `infinito_mcp` external service, attaches the read-only functions, mints the permanent token and stores it under `users.administrator.tokens['web-app-moodle']`. @@ -131,11 +124,11 @@ Tool categories exposed by the external service: - user lookup (`core_user_get_users_by_field`) - calendar events (`core_calendar_get_calendar_events`) -All of them are read-only; `services.mcp.tools.mutating_tools_enabled` is `false` and no write function is attached. +All of them are read-only; `mcp.tools.mutating_tools_enabled` is `false` and no write function is attached. ### Default state -Off. `services.mcp.enabled` is true only while `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is part of the deployment. +Off. `mcp.enabled` is true only while `web-app-hermes`, `web-app-openclaw` or `web-app-openwebui` is part of the deployment. ### Authorization subject @@ -151,7 +144,7 @@ Moodle compares every request against `$CFG->wwwroot`. A probe that reaches the ### How to disable -Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. The web-service token is then not issued and the protocol plugin stays unconfigured. +Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The web-service token is then not issued and the protocol plugin stays unconfigured. ## Image source diff --git a/roles/web-app-moodle/meta/mcp.yml b/roles/web-app-moodle/meta/mcp.yml new file mode 100644 index 0000000000..167d7f9ed9 --- /dev/null +++ b/roles/web-app-moodle/meta/mcp.yml @@ -0,0 +1,41 @@ +--- +enabled: true +shared: true +classification: plugin_server +direction: server +source_url: https://moodle.org/plugins/webservice_mcp +minimum_version: "4.3" +transport: streamable_http +exposure: public +auth: bearer_token +auth_subject: service_account +implementation: plugin +credential: + owner: mcp-web-app-moodle + source: token_store + key: web-app-moodle +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: nginx + path: /webservice/mcp/server.php + port_key: http +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 500 + stream_seconds: 300 +tools: + allowlist: + - core_calendar_get_calendar_events + - core_course_get_courses + - core_enrol_get_users_courses + - core_user_get_users_by_field + - core_webservice_get_site_info + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-moodle/meta/services.yml b/roles/web-app-moodle/meta/services.yml index 4088bbed03..7356ea28b8 100644 --- a/roles/web-app-moodle/meta/services.yml +++ b/roles/web-app-moodle/meta/services.yml @@ -25,26 +25,6 @@ ldap: bond: 1 enabled: "{{ 'svc-db-openldap' in group_names }}" shared: "{{ 'svc-db-openldap' in group_names }}" -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names or 'web-app-openwebui' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names or 'web-app-openwebui' in group_names }}" - direction: server - source_url: https://moodle.org/plugins/webservice_mcp - minimum_version: "4.3" - notes: Plugin based; must match the pinned Moodle release. - transport: streamable_http - exposure: public - auth: bearer_token - auth_subject: administrator - implementation: plugin - endpoint: - service_key: nginx - path: /webservice/mcp/server.php - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: playwright-service-flag — DB engine, covered by role-local integration tests mariadb: bond: 1 diff --git a/roles/web-app-moodle/meta/variants.yml b/roles/web-app-moodle/meta/variants.yml index b3a43afc7f..81e88a89e0 100644 --- a/roles/web-app-moodle/meta/variants.yml +++ b/roles/web-app-moodle/meta/variants.yml @@ -30,9 +30,6 @@ matomo: enabled: true shared: true - mcp: - enabled: true - shared: true sso: enabled: true shared: true @@ -40,6 +37,9 @@ enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -68,12 +68,12 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -98,12 +98,12 @@ matomo: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false sso: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-moodle/templates/playwright.env.j2 b/roles/web-app-moodle/templates/playwright.env.j2 index 379a35a366..48c9aa79c5 100644 --- a/roles/web-app-moodle/templates/playwright.env.j2 +++ b/roles/web-app-moodle/templates/playwright.env.j2 @@ -25,5 +25,5 @@ MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enab EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }} LDAP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.ldap.enabled') | string | lower }} LAM_SERVICE_ENABLED={{ lookup('config', application_id, 'services.lam.enabled', false) | string | lower }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} -MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'services.mcp.endpoint.path') | dotenv_quote }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} +MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'mcp.endpoint.path') | dotenv_quote }} diff --git a/roles/web-app-moodle/vars/main.yml b/roles/web-app-moodle/vars/main.yml index ecb61dbdb3..177735acb7 100644 --- a/roles/web-app-moodle/vars/main.yml +++ b/roles/web-app-moodle/vars/main.yml @@ -51,15 +51,15 @@ MOODLE_CRON_CONTAINER: "{{ lookup('config', application_id, 'services.cron.name' MOODLE_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}" MOODLE_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}" -MOODLE_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" +MOODLE_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" MOODLE_MCP_PLUGIN_VERSION: "v0.6.0" MOODLE_MCP_PLUGIN_ARCHIVE_URL: "https://github.com/onbirdev/moodle-webservice_mcp/archive/refs/tags/{{ MOODLE_MCP_PLUGIN_VERSION }}.zip" MOODLE_MCP_PLUGIN_RELPATH: "{{ [MOODLE_WEBSERVICE_SUBDIR, 'mcp'] | path_join }}" MOODLE_MCP_PLUGIN_SOURCE_DIR: "{{ [MOODLE_SOURCE_DIR, MOODLE_MCP_PLUGIN_RELPATH] | path_join }}" MOODLE_MCP_PLUGIN_DIR: "{{ [MOODLE_CODE_DIR, MOODLE_MCP_PLUGIN_RELPATH] | path_join }}" -MOODLE_MCP_SERVICE_KEY: "{{ lookup('config', application_id, 'services.mcp.endpoint.service_key') }}" +MOODLE_MCP_SERVICE_KEY: "{{ lookup('config', application_id, 'mcp.endpoint.service_key') }}" MOODLE_MCP_PORT: "{{ lookup('config', application_id, 'services.nginx.ports.internal.http') }}" -MOODLE_MCP_URL: "http://{{ MOODLE_MCP_SERVICE_KEY }}:{{ MOODLE_MCP_PORT }}{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +MOODLE_MCP_URL: "http://{{ MOODLE_MCP_SERVICE_KEY }}:{{ MOODLE_MCP_PORT }}{{ lookup('config', application_id, 'mcp.endpoint.path') }}" MOODLE_MCP_EXTERNAL_SERVICE_NAME: "Model Context Protocol" MOODLE_MCP_EXTERNAL_SERVICE_SHORTNAME: "infinito_mcp" MOODLE_MCP_EXTERNAL_FUNCTIONS: diff --git a/roles/web-app-n8n/meta/mcp.yml b/roles/web-app-n8n/meta/mcp.yml new file mode 100644 index 0000000000..4604c0b4f1 --- /dev/null +++ b/roles/web-app-n8n/meta/mcp.yml @@ -0,0 +1,34 @@ +--- +enabled: false +shared: false +classification: native_both +direction: both +source_url: https://github.com/n8n-io/n8n/tree/n8n%401.95.3/packages/%40n8n/nodes-langchain/nodes/mcp +supported_version: 1.95.3 +minimum_version: 1.88.0 +transport: sse +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: native +credential: + owner: mcp-web-app-n8n + source: token_store + key: web-app-n8n +allowed_consumers: + - web-app-hermes + - web-app-openclaw + - web-app-openwebui +supported_transports: + - sse +supported_auths: + - bearer_token + - app_password + - upstream_session +endpoint: + service_key: n8n + path: /webhook/infinito-mcp/sse + port_key: http +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-n8n/meta/variants.yml b/roles/web-app-n8n/meta/variants.yml index 6059eba8e3..0feb26a22b 100644 --- a/roles/web-app-n8n/meta/variants.yml +++ b/roles/web-app-n8n/meta/variants.yml @@ -26,6 +26,9 @@ enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -53,3 +56,6 @@ prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-nextcloud/README.md b/roles/web-app-nextcloud/README.md index e802a6f316..d810b0e885 100644 --- a/roles/web-app-nextcloud/README.md +++ b/roles/web-app-nextcloud/README.md @@ -57,7 +57,6 @@ flowchart LR svc_mariadb["mariadb"] svc_nextcloud["nextcloud"] svc_proxy["proxy"] - svc_mcp["mcp"] svc_context_agent["context_agent"] svc_cron["cron"] svc_talk["talk"] @@ -96,7 +95,6 @@ flowchart LR dep_web_app_discourse -. "0..1" .-> svc_discourse dep_web_app_gitlab -. "0..1" .-> svc_gitlab dep_web_app_hermes -. "0..1" .-> svc_context_agent - dep_web_app_hermes -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_mastodon -. "0..1" .-> svc_mastodon @@ -104,7 +102,6 @@ flowchart LR dep_web_app_matrix -. "0..1" .-> svc_matrix dep_web_app_mattermost -. "0..1" .-> svc_mattermost dep_web_app_openclaw -. "0..1" .-> svc_context_agent - dep_web_app_openclaw -. "0..1" .-> svc_mcp dep_web_app_openproject -. "0..1" .-> svc_openproject dep_web_app_openwebui -. "0..1" .-> svc_openwebui dep_web_app_peertube -. "0..1" .-> svc_peertube @@ -201,11 +198,11 @@ Nextcloud serves a Model Context Protocol endpoint through the AppAPI proxy of t | Auth | Nextcloud app password, sent as `Authorization: Bearer ` | | Identity | the Nextcloud user the app password belongs to; every tool call runs with that user's permissions | | Implementation | plugin (the `context_agent` ExApp container, registered through the `manual_install` deploy daemon) | -| Default state | off; `services.mcp.enabled` turns on when `web-app-hermes` or `web-app-openclaw` is deployed | +| Default state | off; `mcp.enabled` turns on when `web-app-hermes` or `web-app-openclaw` is deployed | ### Deployment -The `context_agent` service in [`meta/services.yml`](./meta/services.yml) renders only while `services.mcp.enabled` is true. It runs `ghcr.io/nextcloud/context_agent`, listens on its internal port for AppAPI only, and shares `credentials.context_agent_app_secret` with the ExApp registration as `APP_SECRET`. +The `context_agent` service in [`meta/services.yml`](./meta/services.yml) renders only while `mcp.enabled` is true. It runs `ghcr.io/nextcloud/context_agent`, listens on its internal port for AppAPI only, and shares `credentials.context_agent_app_secret` with the ExApp registration as `APP_SECRET`. [`tasks/03_mcp.yml`](./tasks/03_mcp.yml) waits for the ExApp heartbeat, registers the deploy daemon and the ExApp (route `^/mcp`, verbs `POST,GET,DELETE`, access level `1`), enables the ExApp, mints an app password via `occ user:auth-tokens:add`, persists it through `sys-token-store` under `users.administrator.tokens['web-app-nextcloud']`, and asserts that an authenticated `initialize` call answers `200`. @@ -215,7 +212,7 @@ The `context_agent` service in [`meta/services.yml`](./meta/services.yml) render ### Tool categories -The ExApp registers every tool category the Context Agent ships, read-only and mutating alike, and each call runs with the permissions of the app-password owner. The 2.7.0 image advertises 94 tools across 23 categories, which are application names (`calendar`, `files`, `mail`, `talk`, …), not operations: each bundles its read and its write tools. `tool_status` therefore switches whole categories, and no setting removes only the mutating ones, so `services.mcp.tools.mutating_tools_enabled: false` records the deployment's intent rather than an enforced state. Disabling a category to bar its write tools also removes its read tools. List the categories and narrow the exposed set with: +The ExApp registers every tool category the Context Agent ships, read-only and mutating alike, and each call runs with the permissions of the app-password owner. The 2.7.0 image advertises 94 tools across 23 categories, which are application names (`calendar`, `files`, `mail`, `talk`, …), not operations: each bundles its read and its write tools. `tool_status` therefore switches whole categories, and no setting removes only the mutating ones, so `mcp.tools.mutating_tools_enabled: false` records the deployment's intent rather than an enforced state. Disabling a category to bar its write tools also removes its read tools. List the categories and narrow the exposed set with: ```bash occ app_api:app:config:get context_agent tool_status @@ -237,11 +234,11 @@ An unauthenticated request to the same path answers `404`; [`files/playwright/te ### Default state -Off. `services.mcp.enabled` is true only while `web-app-hermes` or `web-app-openclaw` is part of the deployment. +Off. `mcp.enabled` is true only while `web-app-hermes` or `web-app-openclaw` is part of the deployment. ### How to disable -Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. The `context_agent` service is then not rendered, the ExApp is not registered with AppAPI, and no app password is minted. +Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The `context_agent` service is then not rendered, the ExApp is not registered with AppAPI, and no app password is minted. ## Documentation diff --git a/roles/web-app-nextcloud/meta/mcp.yml b/roles/web-app-nextcloud/meta/mcp.yml new file mode 100644 index 0000000000..69ba146100 --- /dev/null +++ b/roles/web-app-nextcloud/meta/mcp.yml @@ -0,0 +1,35 @@ +--- +enabled: true +shared: true +classification: plugin_server +direction: server +source_url: https://docs.nextcloud.com/server/latest/admin_manual/ai/app_context_agent.html +minimum_version: "30" +transport: streamable_http +exposure: public +auth: app_password +auth_subject: service_account +implementation: plugin +credential: + owner: mcp-web-app-nextcloud + source: token_store + key: web-app-nextcloud +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: proxy + path: /index.php/apps/app_api/proxy/context_agent/mcp/ + port_key: http +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 500 + stream_seconds: 300 +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-nextcloud/meta/services.yml b/roles/web-app-nextcloud/meta/services.yml index c3814d57fe..e24c261a42 100644 --- a/roles/web-app-nextcloud/meta/services.yml +++ b/roles/web-app-nextcloud/meta/services.yml @@ -115,26 +115,6 @@ proxy: ports: internal: http: 80 -mcp: - bond: 1 - enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}" - direction: server - source_url: https://docs.nextcloud.com/server/latest/admin_manual/ai/app_context_agent.html - minimum_version: "30" - notes: Context Agent ExApp behind the AppAPI proxy, app-password auth. - transport: streamable_http - exposure: public - auth: app_password - auth_subject: administrator - implementation: plugin - endpoint: - service_key: proxy - path: /index.php/apps/app_api/proxy/context_agent/mcp/ - port_key: http - tools: - read_only_default: true - mutating_tools_enabled: false # nocheck: playwright-service-flag context_agent: bond: 1 diff --git a/roles/web-app-nextcloud/meta/variants.yml b/roles/web-app-nextcloud/meta/variants.yml index baec48dbb3..d02efc22c7 100644 --- a/roles/web-app-nextcloud/meta/variants.yml +++ b/roles/web-app-nextcloud/meta/variants.yml @@ -30,9 +30,6 @@ dashboard: enabled: true shared: true - mcp: - enabled: true - shared: true context_agent: enabled: true shared: true @@ -100,6 +97,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -119,9 +119,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -191,6 +188,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -210,9 +210,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -282,6 +279,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -301,9 +301,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -373,6 +370,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -392,9 +392,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -464,6 +461,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -483,9 +483,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -555,6 +552,9 @@ peertube: enabled: true shared: true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -574,9 +574,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -646,6 +643,9 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -665,9 +665,6 @@ dashboard: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false context_agent: enabled: false shared: false @@ -737,3 +734,6 @@ peertube: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-nextcloud/templates/playwright.env.j2 b/roles/web-app-nextcloud/templates/playwright.env.j2 index 124b39827f..c1fa86f2d2 100644 --- a/roles/web-app-nextcloud/templates/playwright.env.j2 +++ b/roles/web-app-nextcloud/templates/playwright.env.j2 @@ -25,7 +25,7 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }} MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} MOODLE_BASE_URL={{ (lookup('tls', 'web-app-moodle', 'url.base') if (lookup('config', application_id, 'services.moodle.enabled') | bool) else '') | dotenv_quote }} @@ -33,7 +33,7 @@ NEXTCLOUD_BASE_URL={{ lookup('tls', 'web-app-nextcloud', 'url.base') | dotenv_qu NEXTCLOUD_DIRECT_LOGIN_PASSWORD={{ lookup('config', application_id, 'credentials.administrator_password') | dotenv_quote }} -NEXTCLOUD_MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'services.mcp.endpoint.path') | dotenv_quote }} +NEXTCLOUD_MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'mcp.endpoint.path') | dotenv_quote }} NEXTCLOUD_ONLYOFFICE_EXPECTED_DOCUMENT_SERVER_URL={{ NEXTCLOUD_ONLYOFFICE_URL | dotenv_quote }} diff --git a/roles/web-app-nextcloud/vars/main.yml b/roles/web-app-nextcloud/vars/main.yml index 087620bdab..c0e5994124 100644 --- a/roles/web-app-nextcloud/vars/main.yml +++ b/roles/web-app-nextcloud/vars/main.yml @@ -47,8 +47,8 @@ NEXTCLOUD_PROXY_SERVICE: "proxy" NEXTCLOUD_PROXY_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_PROXY_SERVICE ~ '.name') }}" NEXTCLOUD_PROXY_DNS_NAME: "{{ (entity_name ~ '_' ~ NEXTCLOUD_PROXY_SERVICE) if DEPLOYMENT_MODE == 'swarm' else NEXTCLOUD_PROXY_CONTAINER }}" -NEXTCLOUD_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}" -NEXTCLOUD_MCP_PATH: "{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}" +NEXTCLOUD_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}" +NEXTCLOUD_MCP_PATH: "{{ lookup('config', application_id, 'mcp.endpoint.path') }}" NEXTCLOUD_MCP_TOKEN_NAME: "infinito-mcp" NEXTCLOUD_CONTEXT_AGENT_SERVICE: "context_agent" diff --git a/roles/web-app-odoo/README.md b/roles/web-app-odoo/README.md index 90f2986eee..f4edb6e2de 100644 --- a/roles/web-app-odoo/README.md +++ b/roles/web-app-odoo/README.md @@ -41,7 +41,6 @@ flowchart LR svc_javascript["javascript"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_openldap -. "0..1" .-> svc_ldap diff --git a/roles/web-app-odoo/meta/services.yml b/roles/web-app-odoo/meta/services.yml index ca4744d1a4..f3f26352de 100644 --- a/roles/web-app-odoo/meta/services.yml +++ b/roles/web-app-odoo/meta/services.yml @@ -96,12 +96,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -# nocheck: playwright-service-flag no MCP surface is served, so no spec can exercise one -mcp: - enabled: false - shared: false - blocker: hosted_only - source_url: https://apps.odoo.com/apps - notes: Third-party add-ons only; none has been selected and reviewed. diff --git a/roles/web-app-openclaw/README.md b/roles/web-app-openclaw/README.md index 5c4d1819eb..50adec2270 100644 --- a/roles/web-app-openclaw/README.md +++ b/roles/web-app-openclaw/README.md @@ -30,7 +30,6 @@ flowchart LR svc_kata["kata"] svc_sso["sso"] svc_litellm["litellm"] - svc_mcp["mcp"] svc_logout["logout"] svc_dashboard["dashboard"] svc_matomo["matomo"] @@ -41,31 +40,18 @@ flowchart LR end subgraph dependents [Dependents] dpt_svc_ai_robot["svc-ai-robot 💻"] - dpt_web_app_gitea["web-app-gitea 🐳🐝"] - dpt_web_app_gitlab["web-app-gitlab 🐳🐝"] - dpt_web_app_homeassistant["web-app-homeassistant 🐳🐝"] - dpt_web_app_jenkins["web-app-jenkins 🐳🐝"] - dpt_web_app_mattermost["web-app-mattermost 🐳🐝"] - dpt_web_app_moodle["web-app-moodle 🐳🐝"] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] end dep_svc_ai_litellm -. "0..1" .-> svc_litellm dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_web_app_dashboard -. "0..1" .-> svc_dashboard dep_web_app_homeassistant -. "0..1" .-> svc_homeassistant - dep_web_app_homeassistant -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_matomo -. "0..1" .-> svc_matomo dep_web_app_prometheus -. "0..1" .-> svc_prometheus dep_web_svc_css -. "0..1" .-> svc_css dep_web_svc_logout -. "0..1" .-> svc_logout svc_openclaw -. "0..1" .-> dpt_svc_ai_robot - svc_openclaw -. "0..1" .-> dpt_web_app_gitea - svc_openclaw -. "0..1" .-> dpt_web_app_gitlab - svc_openclaw -. "0..1" .-> dpt_web_app_homeassistant - svc_openclaw -. "0..1" .-> dpt_web_app_jenkins - svc_openclaw -. "0..1" .-> dpt_web_app_mattermost - svc_openclaw -. "0..1" .-> dpt_web_app_moodle svc_openclaw -. "0..1" .-> dpt_web_app_nextcloud ``` @@ -148,12 +134,12 @@ gateway holding the MCP credentials refuses an unauthenticated caller. ### Default state -Off. `services.mcp.enabled` is false unless an MCP server role is part of the +Off. `mcp.enabled` is false unless an MCP server role is part of the deployment. ### How to disable -Remove the MCP server roles, or pin `services.mcp.enabled: false` for this role. +Remove the MCP server roles, or pin `mcp.enabled: false` for this role. The rendered config then contains no MCP server entry. ## Credits diff --git a/roles/web-app-openclaw/meta/mcp.yml b/roles/web-app-openclaw/meta/mcp.yml new file mode 100644 index 0000000000..8222276a35 --- /dev/null +++ b/roles/web-app-openclaw/meta/mcp.yml @@ -0,0 +1,20 @@ +--- +enabled: true +shared: true +classification: native_client +direction: client +source_url: https://openclaw.ai/ +supported_version: "2026.7.1" +minimum_version: "2025.1" +implementation: native +supported_transports: + - streamable_http + - sse +supported_auths: + - bearer_token + - app_password + - upstream_session + - basic_auth +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-openclaw/meta/services.yml b/roles/web-app-openclaw/meta/services.yml index 1895bd40d7..eefffda6f4 100644 --- a/roles/web-app-openclaw/meta/services.yml +++ b/roles/web-app-openclaw/meta/services.yml @@ -55,24 +55,6 @@ litellm: enabled: "{{ 'svc-ai-litellm' in group_names }}" shared: "{{ 'svc-ai-litellm' in group_names }}" -# nocheck: shared-provider -mcp: - bond: 1 - enabled: "{{ 'web-app-homeassistant' in group_names }}" - shared: "{{ 'web-app-homeassistant' in group_names }}" - direction: client - source_url: https://openclaw.ai/ - minimum_version: "2025.1" - notes: Native @modelcontextprotocol/sdk, client and server. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: user - implementation: native - tools: - read_only_default: true - mutating_tools_enabled: false - logout: bond: 1 enabled: "{{ 'web-svc-logout' in group_names }}" diff --git a/roles/web-app-openclaw/meta/variants.yml b/roles/web-app-openclaw/meta/variants.yml index edca2eb283..279ddc0413 100644 --- a/roles/web-app-openclaw/meta/variants.yml +++ b/roles/web-app-openclaw/meta/variants.yml @@ -11,9 +11,6 @@ litellm: enabled: true shared: true - mcp: - enabled: true - shared: true logout: enabled: true shared: true @@ -32,6 +29,9 @@ homeassistant: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -42,9 +42,6 @@ litellm: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false # nocheck: variants-static-override backup shared stays literally true logout: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true @@ -63,3 +60,6 @@ homeassistant: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-openclaw/templates/playwright.env.j2 b/roles/web-app-openclaw/templates/playwright.env.j2 index 2a79f5623f..e2907343fb 100644 --- a/roles/web-app-openclaw/templates/playwright.env.j2 +++ b/roles/web-app-openclaw/templates/playwright.env.j2 @@ -22,7 +22,7 @@ LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enab MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }} # nocheck: playwright-service-gate -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/roles/web-app-openclaw/vars/main.yml b/roles/web-app-openclaw/vars/main.yml index cf924ce6ab..06b2043382 100644 --- a/roles/web-app-openclaw/vars/main.yml +++ b/roles/web-app-openclaw/vars/main.yml @@ -7,4 +7,4 @@ OPENCLAW_CONTAINER: "{{ lookup('config', application_id, 'services.openclaw.name OPENCLAW_PORT_INTERNAL: "{{ lookup('config', application_id, 'services.openclaw.ports.internal.http') }}" OPENCLAW_URL: "{{ lookup('tls', application_id, 'url.base') }}" OPENCLAW_CONFIG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'openclaw.json' ] | path_join }}" -OPENCLAW_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'services.mcp.enabled') | bool) else [] }}" +OPENCLAW_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'mcp.enabled') | bool) else [] }}" diff --git a/roles/web-app-openproject/README.md b/roles/web-app-openproject/README.md index 56ba41ddfb..8271df02dc 100644 --- a/roles/web-app-openproject/README.md +++ b/roles/web-app-openproject/README.md @@ -47,7 +47,6 @@ flowchart LR svc_prometheus["prometheus"] svc_openproject["openproject"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end subgraph dependents [Dependents] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] diff --git a/roles/web-app-openproject/meta/services.yml b/roles/web-app-openproject/meta/services.yml index 2375a45a5b..6caef89c15 100644 --- a/roles/web-app-openproject/meta/services.yml +++ b/roles/web-app-openproject/meta/services.yml @@ -155,13 +155,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -# nocheck: playwright-service-flag no MCP surface is served, so no spec can exercise one -mcp: - enabled: false - shared: false - blocker: licence - source_url: https://www.openproject.org/docs/system-admin-guide/integrations/mcp-server/ - minimum_version: "16.0" - notes: Enterprise-only; upstream's own request specification asserts a 404 on Community Edition, which is what this role deploys. diff --git a/roles/web-app-openwebui/README.md b/roles/web-app-openwebui/README.md index ebc92cdfc3..91b39d3c4f 100644 --- a/roles/web-app-openwebui/README.md +++ b/roles/web-app-openwebui/README.md @@ -42,7 +42,6 @@ flowchart LR svc_css["css"] svc_javascript["javascript"] svc_litellm["litellm"] - svc_mcp["mcp"] svc_email["email"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] @@ -50,19 +49,14 @@ flowchart LR svc_baserow["baserow"] end subgraph dependents [Dependents] - dpt_web_app_baserow["web-app-baserow 🐳🐝"] - dpt_web_app_gitlab["web-app-gitlab 🐳🐝"] - dpt_web_app_moodle["web-app-moodle 🐳🐝"] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] end dep_svc_ai_litellm -. "0..1" .-> svc_litellm dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_openldap -. "0..1" .-> svc_ldap dep_web_app_baserow -. "0..1" .-> svc_baserow - dep_web_app_baserow -. "0..1" .-> svc_mcp dep_web_app_dashboard -. "0..1" .-> svc_dashboard dep_web_app_homeassistant -. "0..1" .-> svc_homeassistant - dep_web_app_homeassistant -. "0..1" .-> svc_mcp dep_web_app_keycloak -. "0..1" .-> svc_sso dep_web_app_mailu -. "0..1" .-> svc_email dep_web_app_matomo -. "0..1" .-> svc_matomo @@ -70,9 +64,6 @@ flowchart LR dep_web_app_seaweedfs -. "0..1" .-> svc_seaweedfs dep_web_svc_css -. "0..1" .-> svc_css dep_web_svc_logout -. "0..1" .-> svc_logout - svc_sso -. "0..1" .-> dpt_web_app_baserow - svc_sso -. "0..1" .-> dpt_web_app_gitlab - svc_sso -. "0..1" .-> dpt_web_app_moodle svc_sso -. "0..1" .-> dpt_web_app_nextcloud ``` @@ -230,12 +221,12 @@ with a bearer and a non-empty key. ### Default state -Off. `services.mcp.enabled` is false unless an MCP server role is part of the +Off. `mcp.enabled` is false unless an MCP server role is part of the deployment. ### How to disable -Remove the MCP server roles, or pin `services.mcp.enabled: false` for this role. +Remove the MCP server roles, or pin `mcp.enabled: false` for this role. `TOOL_SERVER_CONNECTIONS` then renders empty. ## Credits diff --git a/roles/web-app-openwebui/meta/mcp.yml b/roles/web-app-openwebui/meta/mcp.yml new file mode 100644 index 0000000000..3526f2f072 --- /dev/null +++ b/roles/web-app-openwebui/meta/mcp.yml @@ -0,0 +1,19 @@ +--- +enabled: true +shared: true +classification: native_client +direction: client +source_url: https://docs.openwebui.com/features/extensibility/mcp/ +supported_version: 0.11.0 +minimum_version: 0.6.31 +implementation: native +supported_transports: + - streamable_http + - sse +supported_auths: + - bearer_token + - app_password + - upstream_session +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-openwebui/meta/services.yml b/roles/web-app-openwebui/meta/services.yml index 8f56a565eb..1501f183ae 100644 --- a/roles/web-app-openwebui/meta/services.yml +++ b/roles/web-app-openwebui/meta/services.yml @@ -88,23 +88,6 @@ litellm: enabled: "{{ 'svc-ai-litellm' in group_names }}" shared: "{{ 'svc-ai-litellm' in group_names }}" -# nocheck: shared-provider -mcp: - bond: 1 - enabled: "{{ 'web-app-homeassistant' in group_names or 'web-app-baserow' in group_names }}" - shared: "{{ 'web-app-homeassistant' in group_names or 'web-app-baserow' in group_names }}" - direction: client - source_url: https://docs.openwebui.com/features/extensibility/mcp/ - minimum_version: 0.6.31 - notes: Native MCP client over Streamable HTTP. - transport: streamable_http - exposure: internal - auth: bearer_token - auth_subject: user - implementation: native - tools: - read_only_default: true - mutating_tools_enabled: false email: bond: 1 enabled: "{{ 'web-app-mailu' in group_names }}" diff --git a/roles/web-app-openwebui/meta/variants.yml b/roles/web-app-openwebui/meta/variants.yml index e0cb305dfb..0715125222 100644 --- a/roles/web-app-openwebui/meta/variants.yml +++ b/roles/web-app-openwebui/meta/variants.yml @@ -37,15 +37,15 @@ ldap: enabled: true shared: true - mcp: - enabled: true - shared: true homeassistant: enabled: true shared: true baserow: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -80,15 +80,15 @@ ldap: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false # nocheck: variants-static-override backup shared stays literally true homeassistant: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true baserow: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -123,12 +123,12 @@ seaweedfs: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true - mcp: - enabled: false - shared: false # nocheck: variants-static-override backup shared stays literally true homeassistant: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true baserow: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-openwebui/templates/playwright.env.j2 b/roles/web-app-openwebui/templates/playwright.env.j2 index d36f07ef7c..a6bb03d386 100644 --- a/roles/web-app-openwebui/templates/playwright.env.j2 +++ b/roles/web-app-openwebui/templates/playwright.env.j2 @@ -38,7 +38,7 @@ MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enab MCP_EXPECTED_SERVERS={{ (OPENWEBUI_MCP_SERVERS | mcp_tool_server_connections | map(attribute='info.id') | list | join(',')) | dotenv_quote }} -MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }} +MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'mcp.enabled') | string | lower }} OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL | dotenv_quote }} diff --git a/roles/web-app-openwebui/vars/main.yml b/roles/web-app-openwebui/vars/main.yml index 5b2652af3d..163cfca53b 100644 --- a/roles/web-app-openwebui/vars/main.yml +++ b/roles/web-app-openwebui/vars/main.yml @@ -9,7 +9,11 @@ OPENWEBUI_ENABLE_API_KEYS: "{{ lookup('config', application_id, 'services.openwe OPENWEBUI_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}" OPENWEBUI_LITELLM_ENABLED: "{{ lookup('config', application_id, 'services.litellm.enabled') }}" OPENWEBUI_LITELLM_API_KEY: "{{ lookup('config', application_id, 'credentials.litellm_api_key') }}" -OPENWEBUI_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'services.mcp.enabled') | bool) else [] }}" +OPENWEBUI_SECRET_KEY: "{{ lookup('config', application_id, 'credentials.webui_secret_key') }}" +OPENWEBUI_MCP_SERVERS: "{{ MCP_DISCOVERED_SERVERS if (lookup('config', application_id, 'mcp.enabled') | bool) else [] }}" +OPENWEBUI_MCP_GROUP_OWNER: "openwebui-mcp-groups" +OPENWEBUI_MCP_GROUP_IDS: "{{ (lookup('users', OPENWEBUI_MCP_GROUP_OWNER, {}) | default({}, true)).get('tokens', {}) }}" +OPENWEBUI_MCP_MEMBERS: "{{ lookup('users') | mcp_group_members(OPENWEBUI_MCP_SERVERS) }}" OPENWEBUI_INTERNAL_HTTP_PORT: "{{ lookup('config', application_id, 'services.openwebui.ports.internal.http') }}" OPENWEBUI_API_BOT_USER: "{{ lookup('users', 'openwebui-api-bot') }}" diff --git a/roles/web-app-pretix/README.md b/roles/web-app-pretix/README.md index 6be01632b9..d39745f530 100644 --- a/roles/web-app-pretix/README.md +++ b/roles/web-app-pretix/README.md @@ -38,6 +38,7 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] + svc_pretixmcp["pretixmcp"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_postgres -. "0..1" .-> svc_postgres diff --git a/roles/web-app-pretix/meta/mcp.yml b/roles/web-app-pretix/meta/mcp.yml new file mode 100644 index 0000000000..49a87f85e2 --- /dev/null +++ b/roles/web-app-pretix/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-pretix + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: pretixmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-pretix/files/mcp/tools.json + specification_sha256: sha256:0019dd8228113173b98d5069b183d7206bbcb999b8ac62c19f6a1e6dcaef7b14 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - pretix_get_event + - pretix_get_order_summary + - pretix_list_events + schema_sha256: sha256:0019dd8228113173b98d5069b183d7206bbcb999b8ac62c19f6a1e6dcaef7b14 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-pretix/meta/services.yml b/roles/web-app-pretix/meta/services.yml index fd9137b3f2..894be8dc42 100644 --- a/roles/web-app-pretix/meta/services.yml +++ b/roles/web-app-pretix/meta/services.yml @@ -83,3 +83,18 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +pretixmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: pretixmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-pretix/meta/variants.yml b/roles/web-app-pretix/meta/variants.yml index 584c45ea61..42be1500da 100644 --- a/roles/web-app-pretix/meta/variants.yml +++ b/roles/web-app-pretix/meta/variants.yml @@ -32,6 +32,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -61,3 +64,6 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-prometheus/README.md b/roles/web-app-prometheus/README.md index f2fff800c2..5c29bd4a61 100644 --- a/roles/web-app-prometheus/README.md +++ b/roles/web-app-prometheus/README.md @@ -37,6 +37,7 @@ flowchart LR svc_cadvisor["cadvisor"] svc_node_exporter["node-exporter"] svc_container_backup["container_backup"] + svc_prometheusmcp["prometheusmcp"] end subgraph dependents [Dependents] dpt_web_app_akaunting["web-app-akaunting 🐳🐝"] diff --git a/roles/web-app-prometheus/meta/mcp.yml b/roles/web-app-prometheus/meta/mcp.yml new file mode 100644 index 0000000000..f6c4a24d1c --- /dev/null +++ b/roles/web-app-prometheus/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-prometheus + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: prometheusmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-prometheus/files/mcp/tools.json + specification_sha256: sha256:08fe8ce18b8eb0e2268401294a58ca56c6ed0670693075e228603d156ba75538 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - prometheus_get_targets + - prometheus_query + - prometheus_query_range + schema_sha256: sha256:08fe8ce18b8eb0e2268401294a58ca56c6ed0670693075e228603d156ba75538 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-prometheus/meta/services.yml b/roles/web-app-prometheus/meta/services.yml index 43e3b7d073..b719d65e8c 100644 --- a/roles/web-app-prometheus/meta/services.yml +++ b/roles/web-app-prometheus/meta/services.yml @@ -88,8 +88,8 @@ blackbox-exporter: version: v0.28.0 name: prometheus-blackbox cpus: "0.1" - mem_reservation: 32m - mem_limit: 64m + mem_reservation: 16m + mem_limit: 24m pids_limit: 64 min_storage: 218MB ports: @@ -125,3 +125,21 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +# nocheck: dynamic-flag the sidecar tracks this role's own mcp flag, not another role's presence; a group_names expression would decide its exposure from an unrelated deployment +prometheusmcp: + enabled: "{{ lookup('config', 'web-app-prometheus', 'mcp.enabled') | bool }}" + shared: false + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: prometheusmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-prometheus/meta/variants.yml b/roles/web-app-prometheus/meta/variants.yml index 56a0623541..9b14622c1a 100644 --- a/roles/web-app-prometheus/meta/variants.yml +++ b/roles/web-app-prometheus/meta/variants.yml @@ -26,6 +26,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -48,3 +51,6 @@ css: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-snipe-it/README.md b/roles/web-app-snipe-it/README.md index d87721189a..1c7b33aecd 100644 --- a/roles/web-app-snipe-it/README.md +++ b/roles/web-app-snipe-it/README.md @@ -42,6 +42,7 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] + svc_snipeitmcp["snipeitmcp"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_mariadb -. "0..1" .-> svc_mariadb diff --git a/roles/web-app-snipe-it/meta/mcp.yml b/roles/web-app-snipe-it/meta/mcp.yml new file mode 100644 index 0000000000..d4b4033c89 --- /dev/null +++ b/roles/web-app-snipe-it/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-snipe-it + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: snipeitmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-snipe-it/files/mcp/tools.json + specification_sha256: sha256:2be4a919c8bb48856bef09101004818851add90d8d85fc7af5265343e8044766 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - snipeit_get_asset + - snipeit_get_license_summary + - snipeit_search_assets + schema_sha256: sha256:2be4a919c8bb48856bef09101004818851add90d8d85fc7af5265343e8044766 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-snipe-it/meta/services.yml b/roles/web-app-snipe-it/meta/services.yml index b56bb7e19a..6706d886bd 100644 --- a/roles/web-app-snipe-it/meta/services.yml +++ b/roles/web-app-snipe-it/meta/services.yml @@ -100,3 +100,18 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +snipeitmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: snipeitmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-snipe-it/meta/variants.yml b/roles/web-app-snipe-it/meta/variants.yml index 5e4688309e..8f49dfe131 100644 --- a/roles/web-app-snipe-it/meta/variants.yml +++ b/roles/web-app-snipe-it/meta/variants.yml @@ -38,6 +38,9 @@ seaweedfs: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -73,6 +76,9 @@ seaweedfs: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -106,3 +112,6 @@ prometheus: enabled: false shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false diff --git a/roles/web-app-wordpress/README.md b/roles/web-app-wordpress/README.md index 8cda24e601..6d25e566ea 100644 --- a/roles/web-app-wordpress/README.md +++ b/roles/web-app-wordpress/README.md @@ -59,7 +59,6 @@ flowchart LR svc_css["css"] svc_prometheus["prometheus"] svc_container_backup["container_backup"] - svc_mcp["mcp ❌"] end dep_svc_bkp_volume_2_local -. "0..1" .-> svc_container_backup dep_svc_db_mariadb -. "0..1" .-> svc_mariadb diff --git a/roles/web-app-wordpress/meta/addons/infinito-mcp-abilities.yml b/roles/web-app-wordpress/meta/addons/infinito-mcp-abilities.yml new file mode 100644 index 0000000000..ca93ba479b --- /dev/null +++ b/roles/web-app-wordpress/meta/addons/infinito-mcp-abilities.yml @@ -0,0 +1,5 @@ +--- +enabled: "{{ lookup('config', 'web-app-wordpress', 'mcp.enabled') | bool }}" +required: false +mechanism: mu_plugin +source: vendored diff --git a/roles/web-app-wordpress/meta/addons/mcp-adapter.yml b/roles/web-app-wordpress/meta/addons/mcp-adapter.yml new file mode 100644 index 0000000000..5ae6ef4fca --- /dev/null +++ b/roles/web-app-wordpress/meta/addons/mcp-adapter.yml @@ -0,0 +1,10 @@ +--- +enabled: "{{ lookup('config', 'web-app-wordpress', 'mcp.enabled') | bool }}" +required: false +mechanism: plugin +source: upstream +version: v0.5.0 +update: + monitored: true + catalog: github-releases + upstream_id: WordPress/mcp-adapter diff --git a/roles/web-app-wordpress/meta/mcp.yml b/roles/web-app-wordpress/meta/mcp.yml new file mode 100644 index 0000000000..dfe6e4f696 --- /dev/null +++ b/roles/web-app-wordpress/meta/mcp.yml @@ -0,0 +1,36 @@ +--- +enabled: false +shared: false +classification: plugin_server +direction: server +source_url: https://github.com/WordPress/mcp-adapter/tree/v0.5.0 +supported_version: 0.5.0 +minimum_version: 0.5.0 +transport: streamable_http +exposure: public +auth: app_password +auth_subject: service_account +implementation: plugin +credential: + owner: mcp-web-app-wordpress + source: token_store + key: web-app-wordpress +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: wordpress + path: /wp-json/mcp/mcp-adapter-default-server + port_key: http +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 20 + result_items: 20 + stream_seconds: 300 +tools: + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-wordpress/meta/services.yml b/roles/web-app-wordpress/meta/services.yml index 3bfecf5c9f..855c7569d4 100644 --- a/roles/web-app-wordpress/meta/services.yml +++ b/roles/web-app-wordpress/meta/services.yml @@ -72,12 +72,3 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true - -# nocheck: dynamic-flag a blocker block can never be enabled, so a group_names expression would be dead code -# nocheck: playwright-service-flag no MCP surface is served, so no spec can exercise one -mcp: - enabled: false - shared: false - blocker: hosted_only - source_url: https://wordpress.org/plugins/ - notes: Ecosystem plugins and hosted-provider tooling only; no maintained plugin has been selected and reviewed. diff --git a/roles/web-app-wordpress/meta/variants.yml b/roles/web-app-wordpress/meta/variants.yml index ffae518769..a159bad2cb 100644 --- a/roles/web-app-wordpress/meta/variants.yml +++ b/roles/web-app-wordpress/meta/variants.yml @@ -36,6 +36,9 @@ prometheus: enabled: true shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -72,3 +75,6 @@ - blog.{{ DOMAIN_PRIMARY }} - biberblog.{{ DOMAIN_PRIMARY }} - biber.blog.{{ DOMAIN_PRIMARY }} + mcp: + enabled: false + shared: false diff --git a/roles/web-app-zammad/README.md b/roles/web-app-zammad/README.md index e3419ca0c8..46eadd8db8 100644 --- a/roles/web-app-zammad/README.md +++ b/roles/web-app-zammad/README.md @@ -49,6 +49,7 @@ flowchart LR svc_scheduler["scheduler"] svc_nginx["nginx"] svc_container_backup["container_backup"] + svc_zammadmcp["zammadmcp"] end subgraph dependents [Dependents] dpt_web_app_nextcloud["web-app-nextcloud 🐳🐝"] diff --git a/roles/web-app-zammad/meta/mcp.yml b/roles/web-app-zammad/meta/mcp.yml new file mode 100644 index 0000000000..4d946fa62a --- /dev/null +++ b/roles/web-app-zammad/meta/mcp.yml @@ -0,0 +1,48 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-app-zammad + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: zammadmcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-app-zammad/files/mcp/tools.json + specification_sha256: sha256:68c48fc5d8e6ab0d705c2d773518f70c65f65ec9942a3bdeab6946d565ef9ed8 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - zammad_get_ticket + - zammad_list_organizations + - zammad_search_tickets + schema_sha256: sha256:68c48fc5d8e6ab0d705c2d773518f70c65f65ec9942a3bdeab6946d565ef9ed8 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-app-zammad/meta/services.yml b/roles/web-app-zammad/meta/services.yml index 28e4518cde..870ff0c1d4 100644 --- a/roles/web-app-zammad/meta/services.yml +++ b/roles/web-app-zammad/meta/services.yml @@ -148,3 +148,18 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant bond: 1 enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}" shared: true +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +zammadmcp: + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: zammadmcp + cpus: "0.25" + mem_reservation: 16m + mem_limit: 24m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-app-zammad/meta/variants.yml b/roles/web-app-zammad/meta/variants.yml index dfc91ca217..379aa41674 100644 --- a/roles/web-app-zammad/meta/variants.yml +++ b/roles/web-app-zammad/meta/variants.yml @@ -37,6 +37,9 @@ redis: shared: true + mcp: + enabled: true + shared: true - services: container_backup: enabled: false @@ -72,6 +75,9 @@ redis: shared: false # nocheck: variants-static-override backup shared stays literally true + mcp: + enabled: false + shared: false - services: container_backup: enabled: false @@ -106,3 +112,6 @@ shared: false # nocheck: variants-static-override backup shared stays literally true redis: shared: true + mcp: + enabled: false + shared: false diff --git a/roles/web-svc-libretranslate/README.md b/roles/web-svc-libretranslate/README.md index c3a511cf7d..ef5875a7fb 100644 --- a/roles/web-svc-libretranslate/README.md +++ b/roles/web-svc-libretranslate/README.md @@ -46,6 +46,7 @@ flowchart LR svc_recaptcha["recaptcha"] svc_javascript["javascript"] svc_prometheus["prometheus"] + svc_libretranslatemcp["libretranslatemcp"] end dep_svc_db_redis -. "0..1" .-> svc_redis dep_web_app_keycloak -. "0..1" .-> svc_sso diff --git a/roles/web-svc-libretranslate/meta/mcp.yml b/roles/web-svc-libretranslate/meta/mcp.yml new file mode 100644 index 0000000000..dc89dcaa3e --- /dev/null +++ b/roles/web-svc-libretranslate/meta/mcp.yml @@ -0,0 +1,47 @@ +--- +enabled: false +shared: false +classification: adapter_server +direction: server +source_url: https://s.infinito.nexus/code +supported_version: 1.0.0 +transport: streamable_http +exposure: internal +auth: bearer_token +auth_subject: service_account +implementation: adapter +credential: + owner: mcp-web-svc-libretranslate + source: credentials + key: mcp_bearer +allowed_consumers: + - web-app-openwebui + - web-app-hermes + - web-app-openclaw +endpoint: + service_key: libretranslatemcp + path: /mcp + port_key: http +adapter: + type: openapi_allowlist + image: infinito/mcp-adapter + version: 1.0.0 + digest: sha256:0000000000000000000000000000000000000000000000000000000000000000 + upstream_api_version: v1 + specification_path: roles/web-svc-libretranslate/files/mcp/tools.json + specification_sha256: sha256:fcd22e47d975583dfe838de76380b48c69ae1a322e332047fadaadbad45adab3 +limits: + request_bytes: 65536 + response_bytes: 1048576 + timeout_seconds: 15 + concurrent_requests: 4 + page_size: 100 + result_items: 100 + stream_seconds: 300 +tools: + allowlist: + - libretranslate_get_frontend_settings + - libretranslate_list_languages + schema_sha256: sha256:fcd22e47d975583dfe838de76380b48c69ae1a322e332047fadaadbad45adab3 + read_only_default: true + mutating_tools_enabled: false diff --git a/roles/web-svc-libretranslate/meta/services.yml b/roles/web-svc-libretranslate/meta/services.yml index f6d4c34389..b21ff8b2bb 100644 --- a/roles/web-svc-libretranslate/meta/services.yml +++ b/roles/web-svc-libretranslate/meta/services.yml @@ -82,3 +82,21 @@ prometheus: bond: 1 enabled: "{{ 'web-app-prometheus' in group_names }}" shared: "{{ 'web-app-prometheus' in group_names }}" +# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP guest spec +# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build +# nocheck: dynamic-flag the sidecar tracks this role's own mcp flag, not another role's presence; a group_names expression would decide its exposure from an unrelated deployment +libretranslatemcp: + enabled: "{{ lookup('config', 'web-svc-libretranslate', 'mcp.enabled') | bool }}" + shared: false + image: python + # nocheck: docker-version base of the self-built stdlib adapter, not the served application + version: "3.13-slim" + custom: true + name: libretranslatemcp + cpus: "0.25" + mem_reservation: 24m + mem_limit: 48m + pids_limit: 64 + ports: + internal: + http: 8080 diff --git a/roles/web-svc-libretranslate/meta/variants.yml b/roles/web-svc-libretranslate/meta/variants.yml index ca42b68878..59db71e211 100644 --- a/roles/web-svc-libretranslate/meta/variants.yml +++ b/roles/web-svc-libretranslate/meta/variants.yml @@ -20,6 +20,9 @@ css: enabled: true shared: true + mcp: + enabled: true + shared: true - services: matomo: enabled: false @@ -35,3 +38,6 @@ css: enabled: false shared: false + mcp: + enabled: false + shared: false diff --git a/tests/integration/roles/meta/services/test_mcp_disabled_has_no_surface.py b/tests/integration/roles/meta/services/test_mcp_disabled_has_no_surface.py index a5508dfeb6..339d904c41 100644 --- a/tests/integration/roles/meta/services/test_mcp_disabled_has_no_surface.py +++ b/tests/integration/roles/meta/services/test_mcp_disabled_has_no_surface.py @@ -10,26 +10,25 @@ import unittest from utils.cache.files import iter_project_files_with_content from utils.cache.yaml import load_yaml_any -from utils.roles.applications.services.mcp import MCP_SERVER_DIRECTIONS -from utils.roles.mapping import ROLE_FILE_META_SERVICES +from utils.roles.applications.mcp import MCP_SERVER_DIRECTIONS +from utils.roles.mapping import ROLE_FILE_META_MCP from . import PROJECT_ROOT ROLES_DIR = PROJECT_ROOT / "roles" -GATE_MARKERS = ("MCP_ENABLED", "services.mcp.enabled") +GATE_MARKERS = ("MCP_ENABLED", "mcp.enabled") SEARCHED_EXTENSIONS = (".yml", ".yaml", ".j2") -SERVICES_BASENAME = ROLE_FILE_META_SERVICES.split("/")[-1] +MCP_BASENAME = ROLE_FILE_META_MCP.split("/")[-1] def _server_roles() -> list[str]: roles = [] for role_dir in sorted(ROLES_DIR.iterdir()): - services_path = role_dir / ROLE_FILE_META_SERVICES - if not role_dir.is_dir() or not services_path.is_file(): + mcp_path = role_dir / ROLE_FILE_META_MCP + if not role_dir.is_dir() or not mcp_path.is_file(): continue - data = load_yaml_any(str(services_path)) - block = data.get("mcp") if isinstance(data, dict) else None + block = load_yaml_any(str(mcp_path)) if not isinstance(block, dict): continue if str(block.get("direction") or "") in MCP_SERVER_DIRECTIONS: @@ -43,7 +42,7 @@ def _roles_that_gate() -> set[str]: for path, text in iter_project_files_with_content( extensions=SEARCHED_EXTENSIONS, exclude_tests=True ): - if not path.startswith(prefix) or path.endswith(SERVICES_BASENAME): + if not path.startswith(prefix) or path.endswith(MCP_BASENAME): continue if any(marker in text for marker in GATE_MARKERS): gating.add(path[len(prefix) :].split("/", 1)[0]) @@ -61,7 +60,7 @@ class TestMcpDisabledHasNoSurface(unittest.TestCase): ungated, [], "MCP server roles that render their surface unconditionally, so " - f"`services.mcp.enabled=false` would not remove it: {ungated}", + f"`mcp.enabled=false` would not remove it: {ungated}", ) diff --git a/tests/integration/roles/meta/services/test_mcp_variant_coverage.py b/tests/integration/roles/meta/services/test_mcp_variant_coverage.py index 0013aae95b..0db960dc3e 100644 --- a/tests/integration/roles/meta/services/test_mcp_variant_coverage.py +++ b/tests/integration/roles/meta/services/test_mcp_variant_coverage.py @@ -25,7 +25,7 @@ from collections.abc import Mapping from utils.annotations.suppress import is_suppressed_in_head from utils.cache.files import read_text from utils.cache.yaml import load_yaml_any -from utils.roles.applications.services.mcp import MCP_DEPLOYABLE_CLASSIFICATIONS +from utils.roles.applications.mcp import MCP_DEPLOYABLE_CLASSIFICATIONS from utils.roles.mapping import ROLE_FILE_META_SERVICES, ROLE_FILE_META_VARIANTS from . import PROJECT_ROOT @@ -62,7 +62,7 @@ class TestMcpVariantCoverage(unittest.TestCase): variants_path = role_dir / ROLE_FILE_META_VARIANTS if not variants_path.is_file(): offenders.append( - f"{role_dir.name}: services.mcp is off in meta/services.yml " + f"{role_dir.name}: mcp is off in meta/services.yml " f"and the role ships no meta/variants.yml, so its adapter, " f"probe and endpoint can never run" ) @@ -77,7 +77,7 @@ class TestMcpVariantCoverage(unittest.TestCase): ) if not enabled: offenders.append( - f"{role_dir.name}: services.mcp is off in meta/services.yml " + f"{role_dir.name}: mcp is off in meta/services.yml " f"and no variant enables it, so its adapter, probe and " f"endpoint can never run" ) diff --git a/tests/integration/roles/meta/services/test_variants_no_static_override.py b/tests/integration/roles/meta/services/test_variants_no_static_override.py index 201b789a00..a891ae367e 100644 --- a/tests/integration/roles/meta/services/test_variants_no_static_override.py +++ b/tests/integration/roles/meta/services/test_variants_no_static_override.py @@ -141,7 +141,6 @@ def _variant_override_line_indices(variants_file): if not value_part or value_part.startswith("#"): continue - # Only register leaves under services.<...> path_keys = [k for _, k in stack] if not path_keys or path_keys[0] != "services": continue diff --git a/tests/lint/ansible/services/test_mcp_schema.py b/tests/lint/ansible/services/test_mcp_schema.py index f8e1c0c035..660281551d 100644 --- a/tests/lint/ansible/services/test_mcp_schema.py +++ b/tests/lint/ansible/services/test_mcp_schema.py @@ -1,9 +1,9 @@ -"""Schema lint for the ``services.mcp`` block. +"""Schema lint for the ``mcp`` block. A role declares ``mcp`` only when it actually serves or consumes MCP. Why a role cannot is recorded once in the design documentation, not in a metadata block per role restating an enum. What is declared here has to be complete, -with vocabulary from ``utils/roles/applications/services/mcp.py``. +with vocabulary from ``utils/roles/applications/mcp.py``. It is a hard lint. It rejects: @@ -55,7 +55,7 @@ from functools import partial from utils.annotations.suppress import is_suppressed_at, is_suppressed_in_head from utils.cache.files import read_text from utils.cache.yaml import load_yaml_any -from utils.roles.applications.services.mcp import ( +from utils.roles.applications.mcp import ( MCP_ADAPTER_KEYS, MCP_ADAPTER_REQUIRED_KEYS, MCP_ADAPTER_TYPES, @@ -87,7 +87,7 @@ from utils.roles.applications.services.mcp import ( delegation_is_proven, value_is_templated, ) -from utils.roles.mapping import ROLE_FILE_META_SERVICES +from utils.roles.mapping import ROLE_FILE_META_MCP, ROLE_FILE_META_SERVICES from . import PROJECT_ROOT @@ -172,7 +172,7 @@ def _is_exact_names(value: object) -> bool: class TestMcpSchema(unittest.TestCase): - """Hard lint: every deployable ``services.mcp`` block obeys the contract.""" + """Hard lint: every deployable ``mcp`` block obeys the contract.""" def test_mcp_schema(self) -> None: roles_root = PROJECT_ROOT / "roles" @@ -182,26 +182,29 @@ class TestMcpSchema(unittest.TestCase): errors: list[str] = [] for role_dir in sorted(p for p in roles_root.iterdir() if p.is_dir()): - services_path = role_dir / ROLE_FILE_META_SERVICES - if not services_path.is_file(): + mcp_path = role_dir / ROLE_FILE_META_MCP + if not mcp_path.is_file(): continue - rel = services_path.relative_to(PROJECT_ROOT).as_posix() - lines = read_text(str(services_path)).splitlines() + rel = mcp_path.relative_to(PROJECT_ROOT).as_posix() + lines = read_text(str(mcp_path)).splitlines() if is_suppressed_in_head(lines, _RULE): continue try: - services = load_yaml_any(str(services_path), default_if_missing={}) + mcp = load_yaml_any(str(mcp_path), default_if_missing={}) except Exception: continue + + services = load_yaml_any( + str(role_dir / ROLE_FILE_META_SERVICES), default_if_missing={} + ) if not isinstance(services, Mapping): - continue - mcp = services.get("mcp") + services = {} if mcp is None: continue - prefix = f"{role_dir.name}: services.mcp" + prefix = f"{role_dir.name}: mcp" flag = partial(_flag, errors, lines, rel) if not isinstance(mcp, Mapping): @@ -242,8 +245,7 @@ class TestMcpSchema(unittest.TestCase): if errors: self.fail( - f"services.mcp schema violations ({len(errors)}):\n" - + "\n".join(sorted(errors)) + f"mcp schema violations ({len(errors)}):\n" + "\n".join(sorted(errors)) ) def _check_deployable( @@ -589,11 +591,10 @@ class TestMcpConsumerCompatibility(unittest.TestCase): blocks: dict[str, Mapping] = {} for role_dir in sorted(p for p in roles_root.iterdir() if p.is_dir()): - services_path = role_dir / ROLE_FILE_META_SERVICES - if not services_path.is_file(): + mcp_path = role_dir / ROLE_FILE_META_MCP + if not mcp_path.is_file(): continue - services = load_yaml_any(str(services_path), default_if_missing={}) - mcp = services.get("mcp") if isinstance(services, Mapping) else None + mcp = load_yaml_any(str(mcp_path), default_if_missing={}) if isinstance(mcp, Mapping): blocks[role_dir.name] = mcp diff --git a/tests/unit/utils/networks/test_render.py b/tests/unit/utils/networks/test_render.py index 9008d62d3d..e606f3363a 100644 --- a/tests/unit/utils/networks/test_render.py +++ b/tests/unit/utils/networks/test_render.py @@ -85,9 +85,9 @@ class TestMcpClientConsumer(unittest.TestCase): self.assertTrue( self._consumer( **{ - "services.mcp.enabled": True, - "services.mcp.shared": True, - "services.mcp.direction": "client", + "mcp.enabled": True, + "mcp.shared": True, + "mcp.direction": "client", } ) ) @@ -96,9 +96,9 @@ class TestMcpClientConsumer(unittest.TestCase): self.assertTrue( self._consumer( **{ - "services.mcp.enabled": True, - "services.mcp.shared": True, - "services.mcp.direction": "both", + "mcp.enabled": True, + "mcp.shared": True, + "mcp.direction": "both", } ) ) @@ -107,16 +107,16 @@ class TestMcpClientConsumer(unittest.TestCase): self.assertFalse( self._consumer( **{ - "services.mcp.enabled": True, - "services.mcp.shared": True, - "services.mcp.direction": "server", + "mcp.enabled": True, + "mcp.shared": True, + "mcp.direction": "server", } ), "a provider on another provider's network is the mesh this kind removes", ) def test_disabled_stays_out(self): - self.assertFalse(self._consumer(**{"services.mcp.direction": "client"})) + self.assertFalse(self._consumer(**{"mcp.direction": "client"})) class TestSuppressDefault(unittest.TestCase): diff --git a/utils/networks/attachments.py b/utils/networks/attachments.py index e54cc4905c..024e64fd6a 100644 --- a/utils/networks/attachments.py +++ b/utils/networks/attachments.py @@ -39,16 +39,12 @@ def _is_consumer( return False return True if kind == "mcp_client": - if not _coerce_bool( - lookup_config(application_id, "services.mcp.enabled", False) - ): + if not _coerce_bool(lookup_config(application_id, "mcp.enabled", False)): return False - if not _coerce_bool( - lookup_config(application_id, "services.mcp.shared", False) - ): + if not _coerce_bool(lookup_config(application_id, "mcp.shared", False)): return False direction = str( - lookup_config(application_id, "services.mcp.direction", "") or "" + lookup_config(application_id, "mcp.direction", "") or "" ).strip() return direction in ("client", "both") if kind == "web_facing": diff --git a/utils/networks/render.py b/utils/networks/render.py index 8918133b0e..f38f7022e0 100644 --- a/utils/networks/render.py +++ b/utils/networks/render.py @@ -15,7 +15,7 @@ discovered into the service_registry by ``discover_role_services``. Keys: * ``consumer``: optional override * ``kind``: ``services_flags`` (default) | ``database`` | ``mcp_client`` | ``web_facing``. ``mcp_client`` admits only roles whose - ``services.mcp.direction`` is ``client`` or ``both``, so a provider's + ``mcp.direction`` is ``client`` or ``both``, so a provider's network carries its clients and not every other MCP server. * ``key``: services..* lookup base. Default: provides or entity_name * ``flags``: list of flags to AND. Default: ``[enabled, shared]`` diff --git a/utils/roles/applications/services/mcp.py b/utils/roles/applications/mcp.py similarity index 96% rename from utils/roles/applications/services/mcp.py rename to utils/roles/applications/mcp.py index 1387a249ac..4f3042da66 100644 --- a/utils/roles/applications/services/mcp.py +++ b/utils/roles/applications/mcp.py @@ -1,4 +1,4 @@ -"""Vocabulary of the ``services.mcp`` block in ``meta/services.yml``. +"""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: @@ -140,7 +140,6 @@ MCP_KEYS = frozenset( { "enabled", "shared", - "bond", "classification", "reason", "direction", @@ -254,7 +253,7 @@ def is_deployable(mcp: object) -> bool: """Return whether this block may carry a served surface and be discovered. Args: - mcp: the role's ``services.mcp`` mapping, or anything else. + mcp: the role's ``mcp`` mapping, or anything else. """ if not isinstance(mcp, dict): return False @@ -265,7 +264,7 @@ def declares_delegation(mcp: object) -> bool: """Return whether this block claims to act as the requesting end user. Args: - mcp: the role's ``services.mcp`` mapping, or anything else. + mcp: the role's ``mcp`` mapping, or anything else. """ if not isinstance(mcp, dict): return False @@ -279,7 +278,7 @@ def delegation_is_proven(mcp: object) -> bool: """Return whether a recorded per-version audit backs the delegation claim. Args: - mcp: the role's ``services.mcp`` mapping, or anything else. + mcp: the role's ``mcp`` mapping, or anything else. """ if not isinstance(mcp, dict): return False diff --git a/utils/roles/mapping.py b/utils/roles/mapping.py index 94a059f921..5388849684 100644 --- a/utils/roles/mapping.py +++ b/utils/roles/mapping.py @@ -177,6 +177,7 @@ ROLE_FILE_META_SERVER = "meta/server.yml" ROLE_FILE_META_CSP = "meta/csp.yml" ROLE_FILE_META_DOMAINS = "meta/domains.yml" ROLE_FILE_META_NETWORKS = "meta/networks.yml" +ROLE_FILE_META_MCP = "meta/mcp.yml" ROLE_FILE_META_RBAC = "meta/rbac.yml" ROLE_FILE_META_VOLUMES = "meta/volumes.yml" ROLE_FILE_META_SCHEMA = "meta/schema.yml" @@ -353,6 +354,17 @@ ROLE_FILES: dict[str, dict[str, object]] = { "description": ("Optional dashboard / UI metadata (logo, label) for the role."), "types": _all(mandatory=False), }, + ROLE_FILE_META_MCP: { + "description": ( + "Model Context Protocol capability of the role: direction, transport, " + "auth, credential, tool surface and admitted counterparts. Not a " + "service entry, because no single role provides MCP." + ), + "types": [ + {"type": ROLE_TYPE_APPLICATION, "mandatory": False, "entries": []}, + *_all(allowed=False), + ], + }, ROLE_FILE_META_USERS: { "description": ( "Reserved-username declarations consumed by the user-management layer."