Files
mcpctl/src/mcpd/tests/stdio-mode.test.ts

45 lines
1.9 KiB
TypeScript
Raw Normal View History

fix(secrets): injector-delivered servers must attach, not exec With injected delivery the credentials exist ONLY in PID 1's environment: the container command is a shell that sources /vault/secrets/<name> and execs the real server, so the values live in that process and nowhere else — not in the pod spec, which is the whole point. mcpd picks its STDIO mode from the server's shape: an explicit command or a packageName selects `exec`, a bare dockerImage selects `attach`. `exec` spawns a NEW process inside the container, which never sourced the file and therefore starts with empty credentials. The server comes up, answers tools/list, and fails every authenticated call — precisely the silent empty-token failure this feature exists to prevent. Seen as `Readiness check (list_datasources) failed: process exited 1` on a pod whose PID 1 demonstrably held the token (verified via /proc/1/environ: GRAFANA_URL set, token 46 chars, nothing in the pod spec). So secretDelivery: injector now forces attach regardless of server shape. Safe because wrapCommandForInjector execs rather than forks, so PID 1 IS the server. It also means no bouncer or HTTP shim is needed — mcpd's PersistentStdio already holds one long-lived connection; it was simply pointed at the wrong process. Note this inverts an assumption I had earlier: image-entrypoint servers were already on the attach path and would have worked; it is the package-based ones that were broken. gitea remains the sole exception, and for the unrelated reason that it is distroless and has no shell to source the file. Extracts the decision as a pure `chooseStdioMode()` so it can be tested — it is subtle and fails silently. Six cases pinned, including that env delivery still execs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018vybEitX4FykeMatKe5Xki
2026-08-21 02:05:51 +01:00
/**
* How mcpd opens a STDIO session: attach to PID 1, or exec a new process.
*
* Subtle and silent when wrong. With injected secret delivery the credentials
* exist ONLY in PID 1's environment (a shell sourced /vault/secrets/<name> and
* exec'd the server), so an `exec` starts a process with empty credentials
* the server comes up, answers tools/list, and fails every authenticated call.
*/
import { describe, it, expect } from 'vitest';
import { chooseStdioMode } from '../src/services/mcp-proxy-service.js';
const base = { name: 's', id: 'id1' };
describe('chooseStdioMode', () => {
it('attaches for an injector server even though it has a packageName', () => {
// The regression: packageName would otherwise select exec, and exec loses
// the secrets entirely.
expect(chooseStdioMode({ ...base, secretDelivery: 'injector', packageName: '@leval/mcp-grafana' }))
.toEqual({ kind: 'attach' });
});
it('attaches for an injector server even though it has an explicit command', () => {
expect(chooseStdioMode({ ...base, secretDelivery: 'injector', command: ['node', 'x.js'] }))
.toEqual({ kind: 'attach' });
});
it('still execs a package server on the default env delivery', () => {
const m = chooseStdioMode({ ...base, secretDelivery: 'env', packageName: '@leval/mcp-grafana', runtime: 'node' });
expect(m.kind).toBe('exec');
});
it('still prefers an explicit command over packageName on env delivery', () => {
expect(chooseStdioMode({ ...base, secretDelivery: 'env', command: ['node', 'x.js'], packageName: 'p' }))
.toEqual({ kind: 'exec', command: ['node', 'x.js'] });
});
it('attaches for an image-entrypoint server, as before', () => {
expect(chooseStdioMode({ ...base, dockerImage: 'gitea/mcp:latest' })).toEqual({ kind: 'attach' });
});
it('rejects a server with no way to start', () => {
expect(() => chooseStdioMode({ ...base })).toThrow(/packageName, command, or dockerImage/);
});
});