Design: docs/connector-auth.md.
The gap
The connector auth model (api-key | oauth2 | app | none) assumes the operator knows the remote system at build time. oauth2 requires secretRef resolved from operator env — a pre-registered, confidential client.
That breaks for the first connector where a subscriber names the endpoint at runtime (outbound MCP, mcp_url as a per-instance setting). The operator cannot pre-register with a server they have never heard of, so secretRef is unfillable: there is no value that could go in it.
Consequence: the mcp connector shipped auth:"token" — a vault bearer the user pastes in — which the first real target (FreeWebStore) explicitly documents as "a compatibility fallback" alongside "do not ask users to paste access tokens."
This also blocks #143 Phase 3 (creator-supplied manifests): a creator cannot add a client secret to the operator env, so any creator connector needing OAuth requires an operator deploy — exactly what #143 exists to abolish.
The fix
Add auth.type: "dcr-oauth2". The connector declares nothing about the remote server; the resource URL is the input.
| Step |
Spec |
Removes |
| Discover auth server from resource |
RFC 9728 protected-resource metadata |
hardcoded authUrl/tokenUrl |
| Discover endpoints |
RFC 8414 AS metadata |
manifest endpoint config |
| Register the client at runtime |
RFC 7591 dynamic client registration |
secretRef, pre-registration |
| Authorize without a secret |
PKCE S256, public client |
the client secret entirely |
This is the stack MCP mandates. Verified live against FreeWebStore:
POST https://agent.freewebstore.online/register
-> 201 {"client_id":"6bda1761-...","redirect_uris":["https://api.proagentstore.online/v1/connectors/mcp/oauth/callback"]}
PAGS registered itself, for its own callback, with no operator involvement and no secret.
Scope
- Discovery chain: resource URL -> RFC 9728 -> RFC 8414, through
safeFetch (SSRF-guarded, https-only).
- DCR client registration cached per (user, resource origin) — one registration per server per user, reused across instances; envelope-encrypted like every other credential.
- PKCE S256 authorize + callback, reusing the HMAC-signed state in
lib/connector-oauth.ts.
- Grant storage keyed by resource origin, not connector id alone (a user may connect several MCP servers).
connectorClient dispatch for the new type; mcp connector switches to it when discovery finds an auth server, falling back to vault bearer only when it does not.
Acceptance
- A subscriber can connect an arbitrary OAuth-protected MCP server with no operator action and no pasted token.
- Registration is performed once per (user, origin) and reused.
- A server offering no auth metadata still works via the existing vault-bearer path.
- Discovery cannot be pointed at internal addresses (SSRF guard covers the metadata fetches).
- Tests cover: discovery, DCR, PKCE round-trip, cached registration reuse, and the no-metadata fallback.
Notes
Getting connected is necessary but not sufficient for unattended chains — see the companion issue on credential survivability.
Design:
docs/connector-auth.md.The gap
The connector auth model (
api-key | oauth2 | app | none) assumes the operator knows the remote system at build time.oauth2requiressecretRefresolved from operator env — a pre-registered, confidential client.That breaks for the first connector where a subscriber names the endpoint at runtime (outbound MCP,
mcp_urlas a per-instance setting). The operator cannot pre-register with a server they have never heard of, sosecretRefis unfillable: there is no value that could go in it.Consequence: the
mcpconnector shippedauth:"token"— a vault bearer the user pastes in — which the first real target (FreeWebStore) explicitly documents as "a compatibility fallback" alongside "do not ask users to paste access tokens."This also blocks #143 Phase 3 (creator-supplied manifests): a creator cannot add a client secret to the operator env, so any creator connector needing OAuth requires an operator deploy — exactly what #143 exists to abolish.
The fix
Add
auth.type: "dcr-oauth2". The connector declares nothing about the remote server; the resource URL is the input.authUrl/tokenUrlsecretRef, pre-registrationThis is the stack MCP mandates. Verified live against FreeWebStore:
PAGS registered itself, for its own callback, with no operator involvement and no secret.
Scope
safeFetch(SSRF-guarded, https-only).lib/connector-oauth.ts.connectorClientdispatch for the new type;mcpconnector switches to it when discovery finds an auth server, falling back to vault bearer only when it does not.Acceptance
Notes
Getting connected is necessary but not sufficient for unattended chains — see the companion issue on credential survivability.