feat: HTTP-mode mcplocal container + mcpctl test mcp + token-auth preHandler

Delivers the final piece of the mcptoken stack: a containerized,
network-accessible mcplocal that serves Streamable-HTTP MCP to off-host
clients (the vLLM use case), authenticated by project-scoped McpTokens.

New binary (same package, new entry):
  - src/mcplocal/src/serve.ts — HTTP-only entry. Reads MCPLOCAL_MCPD_URL,
    MCPLOCAL_MCPD_TOKEN, MCPLOCAL_HTTP_HOST/PORT, MCPLOCAL_CACHE_DIR from
    env. No StdioProxyServer, no --upstream.
  - src/mcplocal/src/http/token-auth.ts — Fastify preHandler that
    validates mcpctl_pat_ bearers via mcpd's /api/v1/mcptokens/introspect.
    30s positive / 5s negative TTL. Rejects wrong-project with 403.

Shared HTTP MCP client:
  - src/shared/src/mcp-http/ — reusable McpHttpSession with initialize,
    listTools, callTool, close. Handles http+https, SSE, id correlation,
    distinct McpProtocolError / McpTransportError. Plus mcpHealthCheck
    and deriveBaseUrl helpers.

New CLI verb `mcpctl test mcp <url>`:
  - Flags: --token (also $MCPCTL_TOKEN), --tool, --args (JSON),
    --expect-tools, --timeout, -o text|json, --no-health.
  - Exit codes: 0 PASS, 1 TRANSPORT/AUTH FAIL, 2 CONTRACT FAIL.

Container + deploy:
  - deploy/Dockerfile.mcplocal (Node 20 alpine, multi-stage, pnpm
    workspace, CMD node src/mcplocal/dist/serve.js, VOLUME
    /var/lib/mcplocal/cache, HEALTHCHECK on :3200/healthz).
  - scripts/build-mcplocal.sh mirrors build-mcpd.sh.
  - fulldeploy.sh is now a 4-step pipeline that also builds + rolls out
    mcplocal (gated on `kubectl get deployment/mcplocal` so the script
    stays green before the Pulumi stack lands).

Audit + cache:
  - project-mcp-endpoint.ts passes MCPLOCAL_CACHE_DIR into FileCache at
    both construction sites and, when request.mcpToken is present, calls
    collector.setSessionMcpToken(id, ...) so audit events carry the
    tokenName/tokenSha.

Tests:
  - 9 unit cases on `mcpctl test mcp` (happy path, health miss,
    expect-tools hit/miss, transport throw, tool isError, json report,
    $MCPCTL_TOKEN env fallback, invalid --args).
  - Smoke test src/mcplocal/tests/smoke/mcptoken.smoke.test.ts —
    gated on healthz($MCPGW_URL), skipped cleanly when unreachable.
    Covers happy path, wrong-project 403, --expect-tools contract
    failure, and revocation 401 within the negative-cache window.

1773/1773 workspace tests pass. Pulumi resources (Deployment, Service,
Ingress, PVC, Secret, NetworkPolicy) still need to land in
../kubernetes-deployment before the smoke gate flips on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-17 01:21:42 +01:00
parent a151b2e756
commit 2127b41d9f
16 changed files with 1202 additions and 9 deletions

View File

@@ -0,0 +1,60 @@
# HTTP-only mcplocal for k8s deploy (Service `mcp`, Ingress `mcp.ad.itaz.eu`).
# Container CMD runs the `serve.ts` entry which — unlike the systemd/STDIO
# entry — has no stdin/stdout MCP client and bootstraps exclusively from env.
# Stage 1: Build TypeScript
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace config and package manifests
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
COPY src/mcplocal/package.json src/mcplocal/tsconfig.json src/mcplocal/
COPY src/shared/package.json src/shared/tsconfig.json src/shared/
COPY src/db/package.json src/db/tsconfig.json src/db/
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY src/mcplocal/src/ src/mcplocal/src/
COPY src/shared/src/ src/shared/src/
COPY src/db/src/ src/db/src/
COPY src/db/prisma/ src/db/prisma/
# Build (mcplocal depends on shared; db is pulled transitively by shared/... actually
# mcplocal does not depend on db at runtime — prisma client is only used by mcpd).
RUN pnpm -F @mcpctl/shared build && pnpm -F @mcpctl/mcplocal build
# Stage 2: Production runtime
FROM node:20-alpine
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace config, manifests, and lockfile
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
COPY src/mcplocal/package.json src/mcplocal/
COPY src/shared/package.json src/shared/
# Install deps (production only — no db / prisma runtime here).
RUN pnpm install --frozen-lockfile
# Copy built output
COPY --from=builder /app/src/shared/dist/ src/shared/dist/
COPY --from=builder /app/src/mcplocal/dist/ src/mcplocal/dist/
EXPOSE 3200
# Cache directory — expected to be mounted as a PVC in k8s.
VOLUME /var/lib/mcplocal/cache
HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=10s \
CMD wget -q --spider http://localhost:3200/healthz || exit 1
# MCPLOCAL_MCPD_URL and MCPLOCAL_MCPD_TOKEN are required and must come from
# the Pulumi-managed Secret. Other env vars default sensibly.
CMD ["node", "src/mcplocal/dist/serve.js"]