Files
computer-playbook/roles/svc-ai-mcp-adapter/README.md
kevinveenbirkenbachandClaude Opus 5.5 3610e7b9ef fix(code-scanning): resolve the alerts that describe real code
Of the 76 open code scanning alerts, 39 point at code that can change.

svc-ai-mcp-adapter:
- A path argument that is empty or made only of dots is refused.
  Percent-encoding keeps a separator inside its segment, but "." and ".."
  need none, so an upstream that resolves dot segments could answer from a
  path the contract does not name.
- parse_duration splits on the units. The pattern it used before rescanned
  a long run of digits from every start position.

Tests and the README generators:
- Every Jinja environment built with autoescape=False and a "noqa: S701"
  uses select_autoescape now, as six test modules and
  utils/templating/jinja.py already did. It escapes templates named
  *.html, *.htm and *.xml, which none of these call sites loads.
- Dead code goes: a doubled "rel" block in the email lint, an unread
  _SKIP_DIRS, and a module that _resolve returned to no caller.
- Two "except ...: pass" blocks become contextlib.suppress and
  assertRaises.
- Nine test modules import unittest.mock one way instead of two, and one
  lookup test imports its module once.

Closes #807

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:18:00 +02:00

139 lines
6.7 KiB
Markdown

# MCP Adapter
## Description
A reusable [Model Context Protocol](https://modelcontextprotocol.io/) adapter, instantiated once per provider application. Each instance fronts exactly one upstream, holds only that upstream's least-privileged credential, and exposes only the operations its checked-in contract names.
## Overview
Several applications have a useful read API but no MCP server, and several projects ship an MCP server that authenticates *to* the application without authenticating the client calling it. Putting either behind this adapter gives the surface a bearer of its own, an exact tool allowlist, and enforced ceilings. The deployment renders one contract per instance, so the same image serves every provider without any of them sharing a credential or a trust boundary.
## Cosmos
The diagram places MCP Adapter in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
```mermaid
flowchart LR
subgraph role [svc-ai-mcp-adapter]
svc_mcp_adapter["mcp-adapter"]
end
```
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
## Features
- **Client-facing authentication:** The adapter issues its own bearer. The upstream credential authenticates the adapter to the provider and says nothing about who called the adapter, which is the gap the project-owned MCP sidecars leave open.
- **Allowlist on `tools/call`:** Filtering only `tools/list` would leave every unlisted operation callable by name.
- **Read-only by default:** A non-`GET`/`HEAD` operation is refused unless `mutating_tools_enabled` is explicitly true.
- **Path arguments stay in their segment:** A value that fills a `{placeholder}` of the contracted path is percent-encoded, and an empty value or one made only of dots is refused, so a call cannot resolve to a path the contract does not name.
- **Enforced ceilings:** Request size, response size, timeout, concurrency, page size and result count come from the contract and are applied, not merely declared.
- **Bounded ranges:** A tool whose schema declares `start`, `end` and `step` has its point count checked against `result_items` and is refused with the smallest step that fits, so the upstream never scans a range whose result the adapter would discard.
- **Fail-closed on drift:** A tool contract whose hash no longer matches its pinned `schema_sha256` refuses to start.
- **Redacted audit trail:** Every call emits provider, consumer, tool, credential subject, status, duration and correlation id, and never the arguments, the response body or the credential.
## Contract
The deployment renders one `ADAPTER_CONTRACT` JSON document per instance:
```json
{
"provider": "web-app-checkmk",
"upstream_url": "http://checkmk:5000",
"auth_subject": "service_account",
"mutating_tools_enabled": false,
"tools": {
"checkmk_list_hosts": {"method": "GET", "path": "/domain-types/host_config/collections/all"}
},
"limits": {
"request_bytes": 65536,
"response_bytes": 1048576,
"timeout_seconds": 15,
"concurrent_requests": 4,
"page_size": 100,
"result_items": 500,
"stream_seconds": 300
},
"schema_sha256": "sha256:…"
}
```
`files/python/policy.py` validates it at startup. A contract missing a limit, carrying an empty allowlist, naming a wildcard path or lacking a pinned schema hash is refused before the process serves anything.
## Endpoints
| Path | Method | Purpose |
|---|---|---|
| `/mcp` | POST | JSON-RPC: `initialize`, `tools/list`, `tools/call` |
| `/health` | GET | liveness, names the provider it fronts |
A tools-only server never has to stream, so every response is a plain JSON body and no SSE session state exists to get wrong.
## Upstream authentication
The adapter presents `ADAPTER_UPSTREAM_KEY` to the provider in the header named by `ADAPTER_UPSTREAM_AUTH_HEADER` (default `Authorization`), formatted by `ADAPTER_UPSTREAM_AUTH_FORMAT` (default `Bearer {key}`, where `{key}` is the credential).
| Provider style | Header | Format |
|---|---|---|
| RFC 6750 bearer | `Authorization` | `Bearer {key}` |
| Pretix | `Authorization` | `Token {key}` |
| Zammad | `Authorization` | `Token token={key}` |
| Jellyfin | `X-Emby-Token` | `{key}` |
An empty `ADAPTER_UPSTREAM_KEY` sends no header at all.
## Image
Self-built and stdlib-only: the served surface is three JSON-RPC methods, and a dependency-free image has no transitive supply chain to audit per release. The container runs as a non-root user with a read-only root filesystem and all Linux capabilities dropped.
## Quick Setup
### Development
Clone, set up the workstation, and deploy MCP Adapter onto the local stack:
```bash
git clone https://github.com/infinito-nexus/core.git
cd core
make onboard
make compose-deploy mode=reinstall apps=svc-ai-mcp-adapter full_cycle=false
```
### Production
Run the published image to provision the inventory and deploy MCP Adapter to a managed server (the mounted volume persists the inventory):
```bash
APP=svc-ai-mcp-adapter
HOST=<your-server>
DOMAIN=<your-domain>
TLS_MODE=self_signed
SSH_PUBLIC_KEY="<your-ssh-public-key>"
docker run --rm -it \
-v "$PWD/inventories:/etc/infinito.nexus/inventories" \
-e APP="$APP" -e HOST="$HOST" -e DOMAIN="$DOMAIN" -e TLS_MODE="$TLS_MODE" -e SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
ghcr.io/infinito-nexus/core/debian bash -c '
INVENTORY=/etc/infinito.nexus/inventories/production
infinito administration inventory provision "$INVENTORY" \
--inventory-file "$INVENTORY/devices.yml" \
--host "$HOST" \
--include "$APP" \
--vars "{\"TLS_MODE\": \"$TLS_MODE\", \"DOMAIN_PRIMARY\": \"$DOMAIN\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}" &&
infinito administration deploy dedicated "$INVENTORY/devices.yml" \
--password-file "$INVENTORY/.password" \
--diff -vv'
```
## Further resources
- [Model Context Protocol](https://modelcontextprotocol.io/)
- [MCP service block](../../docs/contributing/design/role/services/mcp.md)
- [MCP user delegation audit](../../docs/contributing/design/role/services/mcp/delegation.md)
## Credits
Implemented by **[Kevin Veen-Birkenbach](https://www.veen.world)**.
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).