Files
computer-playbook/roles/svc-ai-mcp-adapter/tasks/upstream_key.yml
kevinveenbirkenbachandClaude Opus 5 683de0ab9a feat(svc-ai-mcp-adapter): add the reusable per-provider MCP adapter
One audited implementation, instantiated once per provider application rather
than deployed as a single process holding every application's credential. Each
instance joins only its provider's network, carries only that provider's
least-privileged credential and serves a distinct endpoint.

Every decision lives in the policy layer, which validates the contract at
startup rather than at first call, so an instance missing a limit, an
allowlist or the schema hash never reaches a client. The server itself only
moves bytes over three JSON-RPC methods, with no stream state to get wrong.
Its upstream header is configurable because only half the providers accept an
RFC 6750 bearer.

The contract probe runs at deploy time rather than in Playwright, because it
needs the provider's real credential and a Playwright trace would capture it.
It proves, over both transports, that an unauthenticated caller is refused,
that a wrongly authenticated one is refused, that the handshake completes,
that the served tool list is exactly the contracted one, and that one
deterministic read answers without an error. Steps one and two are what
separate a guarded endpoint from one that merely happens to be unreachable.

The isolation probe counts the sidecar's network interfaces from inside it.
Reachability cannot be used here: an unknown name does not fail to resolve in
this environment, it falls through to a wildcard resolver that answers every
name with one reachable address.

The role is inert until a provider includes it; those inclusions follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:04:08 +02:00

28 lines
1.3 KiB
YAML

---
# Hand a captured upstream credential to the adapter sidecar.
#
# Some providers mint their own API secret and return it exactly once, so the
# value cannot exist when compose first renders. Writing it to an env file and
# recreating the service is what delivers it: `compose up` resolves `env_file`
# while creating the container, so the new value lands in its environment.
# A restart would not, and re-rendering the template is not part of a flush.
#
# Param: mcp_upstream_key_path env file the sidecar reads, on the stack host.
# Param: mcp_upstream_key credential the provider handed back.
- name: "🔑 Write the upstream credential of {{ application_id }}"
ansible.builtin.copy:
content: "ADAPTER_UPSTREAM_KEY={{ mcp_upstream_key }}\n"
dest: "{{ mcp_upstream_key_path }}"
mode: "0600"
register: mcp_upstream_written
delegate_to: "{{ STACK_HOST }}"
become: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔄 Recreate the {{ application_id }} adapter with its upstream credential"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/tasks/rebuild.yml') }}"
when: mcp_upstream_written.changed
- name: "🚿 Flush compose handlers for {{ application_id }}"
ansible.builtin.meta: flush_handlers