[pending-judge] feat(DC-162 rev2): "Connect your AI" — spec Streamable HTTP MCP + one-click keys

Judge r1 (grade D) blockers, each fixed and tested:

1. TRANSPORT: custom POST-only JSON-RPC → MCP 2025-03-10 Streamable HTTP
   (spec-canonical; replaced HTTP+SSE). POST answers application/json for
   requests; valid notifications -> 202 empty; malformed notifications ->
   no reply (204); batching removed in this revision -> arrays rejected
   -32600. GET (server->client stream) and DELETE (session termination)
   answer 405 + Allow: POST — the spec's explicit allowance for stateless
   request/response servers; documented in /info + modal + OpenAPI.
   Origin header validated when present (DNS-rebinding MUST); Accept
   negotiation (json required, else 406); MCP-Protocol-Version header
   checked when sent. Clients needing server-initiated streams use the
   stdio bridge (unchanged, shares mcp-core).

2. CREDENTIAL TYPE PRESERVATION: routes/mcp.js now forwards the SAME
   scheme the caller authenticated with (req.auth.type from
   middleware.js:549-586): apikey -> x-api-key, jwt -> Authorization
   Bearer (NOT dk_-coerced-x-api-key as r1 did). mcp-core createToolHandler
   accepts explicit headers. Tests assert BOTH shapes downstream
   (apikey carries x-api-key & no Authorization; jwt carries Bearer & no
   x-api-key; zero downstream calls for session denial).

3. JSON-RPC VALIDATION: validateJsonRpc — jsonrpc:'2.0', non-empty string
   method, params object-or-omitted, id string|number|null. Invalid
   Request -32600 (empty body, empty array, batch); tools/call without
   string params.name -> -32602; unknown method -> -32601; error envelopes
   carry id null when unpairable. Transport primitives rejected by the
   app-wide strict JSON parser (400; asserted).

4. REPRODUCIBILITY: status/dist/features.js + sw.js regenerated and
   committed IN THE PATCH (repo convention: DC-130/131 committed bundle
   rebuilds). Verifier greps the built bundle for aiconn- (23),
   mcp/connections (1), httpConfig (2) so a clean `git am` checkout serves
   the real frontend.

Judge r1 polish, all applied:
- verifier accepts 0_PATCH.diff / PATCH.diff naming (indexed artifact);
- connections list filtered server-side to "AI connection (...)" keys
  (new GET /mcp/connections, session-only; modal switched to it);
- URL-encoded tool path params (serviceId, containerId, action,
  templateId, category, zone) in mcp-core.

Config shape: POST /mcp/config (session-only; apikey 403) returns the
standard Streamable-HTTP card {url, transport: streamable-http, headers:
{x-api-key}, mcpProtocolVersion} + per-client note (ChatGPT note states
the public-https requirement honestly; no fabricated connector claims).

Tests: 57 green (34 route incl. auth matrix/type-preservation/transport
rules/JSON-RPC errors + 23 core). Composition-root e2e (8) boots the real
createApp with the mount. eslint 0 errors. Full suite: only the
pre-existing pdf-parse billing family red (unchanged since base 470dfaa).
This commit is contained in:
Hermes committed 2026-10-08 03:46:56 -07:00
1 parent 470dfaac2a
commit c9015b59ad
14 files changed
+2139 -747

No files matched your search

+85
View File
@@ -7773,6 +7773,91 @@ paths:
'200':
description: Probe result (ok flag inside — transport errors are reported, not thrown)
/api/v1/mcp:
post:
tags: [MCP]
summary: MCP Streamable HTTP request channel
description: >
DashCaddy Model Context Protocol server over the MCP 2025-03-10
Streamable HTTP transport. Body: ONE JSON-RPC 2.0 request (batching
removed in this protocol revision; arrays answered -32600).
Methods: initialize, tools/list, tools/call, resources/list, ping,
notifications/*. Authentication: TOTP session (+CSRF), JWT bearer,
or x-api-key dk_ key. tools/call executes against the loopback API
carrying the CALLER'S credential with its TYPE preserved (apikey
forwarded as x-api-key, jwt forwarded as Authorization Bearer), so
key scopes apply downstream. Without a machine credential, tools/call
and resources/list return JSON-RPC error -32001; metadata methods
(initialize/tools/list/ping) always serve. Valid notifications
answer 202 (empty); requests answer 200 application/json.
GET and DELETE on this path return 405 with Allow POST (stateless
request/response server — spec-permitted).
responses:
'200':
description: JSON-RPC response object (application/json)
'202':
description: Notification accepted — empty body
'204':
description: Malformed notification — intentionally no response
'400':
description: Invalid Request / unsupported MCP-Protocol-Version
'403':
description: Origin not allowed
'406':
description: Accept header does not allow application/json
'401':
description: Unauthenticated, or no machine credential for a tool method
get:
tags: [MCP]
summary: Server-initiated stream channel (not offered)
description: Stateless request/response server — spec permits 405 with Allow POST.
responses:
'405':
description: No SSE stream available at this endpoint
delete:
tags: [MCP]
summary: Session termination (not applicable)
description: Stateless server issues no Mcp-Session-Id — spec permits 405 with Allow POST.
responses:
'405':
description: Nothing to terminate
/api/v1/mcp/info:
get:
tags: [MCP]
summary: MCP connection card
description: Endpoint URL, protocol/server versions, tool list and the caller's scopes. Backs the "Connect your AI" modal.
responses:
'200':
description: Connection metadata
/api/v1/mcp/connections:
get:
tags: [MCP]
summary: Active AI connection keys (session only)
description: Lists only keys minted by this feature (names starting "AI connection"), not every API key.
responses:
'200':
description: Filtered connection list
'403':
description: Caller is not a dashboard session
/api/v1/mcp/config:
post:
tags: [MCP]
summary: Mint an AI-connection key + client config
description: >
Session-authenticated only. Creates a dedicated dk_ key
(named "AI connection" with the client, scopes read+write) and
returns it once plus the standard Streamable-HTTP connection card
(url, transport, headers) and per-client notes. An apikey-type
caller is refused (403) — keys cannot mint keys.
responses:
'200':
description: Key + config (shown once)
'403':
description: Caller is not a dashboard session
components:
schemas:
SuccessResponse: