feat(mcp): let the connector address carry its own key
verdande could not be added to Claude at all. The documented setup — POST /api/v1/mcp with an Authorization header — cannot be configured from Claude's custom-connector dialog, which asks for a name, a URL and OAuth client credentials and has no field for a bearer token. The instructions described something the interface does not offer. Worse, the obvious guess failed silently in the most misleading way available: /mcp was not a route, so the SPA fallback answered 200 with a page of HTML. The connector reported a successful connection and then failed to parse the response, which reads as a broken client rather than a missing endpoint. So /mcp exists now and takes its credential in the query string, exactly as the calendar feed does and for the same reason: a client that cannot be told to send a header means the address is the credential. The request logger records paths without their query strings, so the key does not reach the log. It is deliberately outside /api/v1 and accepts the key and nothing else. Reading a session cookie here would make it a state-changing POST outside the CSRF check — a cross-site request that acts as whoever is signed in — and there is a test that fails if that ever becomes true. The API path is unchanged for anything that can send a header. The tokens page now shows the connector address next to the token it belongs to, since that is the one moment the token exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Kristian Wind
co-authored by
Claude Opus 5
parent
d7e250f860
commit
e63d852847
@@ -1787,6 +1787,53 @@ paths:
|
||||
responses:
|
||||
"302": { description: "Back into the app, with the outcome as a query parameter." }
|
||||
|
||||
/mcp:
|
||||
post:
|
||||
tags: [integrations]
|
||||
summary: The MCP endpoint, with the token in the query string.
|
||||
description: |
|
||||
The same server as `/api/v1/mcp`, for a client that can only be given a
|
||||
URL. Claude's custom-connector dialog asks for a name, an address and
|
||||
OAuth client credentials — there is no field for a bearer token — so the
|
||||
API path cannot be configured from it.
|
||||
|
||||
The key is the only credential accepted here. A session cookie is
|
||||
ignored: this route sits outside the CSRF check, and honouring a cookie
|
||||
would make it a cross-site request that acts as whoever is signed in.
|
||||
security: []
|
||||
parameters:
|
||||
- name: key
|
||||
in: query
|
||||
required: true
|
||||
schema: { type: string, example: vrd_… }
|
||||
description: A personal API token. Treat the whole address as a password.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [jsonrpc, method]
|
||||
properties:
|
||||
jsonrpc: { type: string, const: "2.0" }
|
||||
id: { oneOf: [{ type: string }, { type: integer }] }
|
||||
method: { type: string }
|
||||
params: { type: object }
|
||||
responses:
|
||||
"200":
|
||||
description: A JSON-RPC response.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jsonrpc: { type: string, const: "2.0" }
|
||||
id: { oneOf: [{ type: string }, { type: integer }] }
|
||||
result: { type: object }
|
||||
error: { type: object }
|
||||
"202": { description: A notification. No body. }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
|
||||
/.well-known/caldav:
|
||||
get:
|
||||
tags: [integrations]
|
||||
|
||||
Reference in New Issue
Block a user