Problem
Toolcraft exposes the low-level OAuth authorization-server primitives added in #519, but an app author still has to assemble production hosted MCP OAuth by hand. That assembly is security-sensitive and has now produced the same class of failures in multiple connectors.
The current primitives make it easy to accidentally ship:
- process-local dynamic client registration, authorization transactions, codes, grants, refresh-token families, and revocation state;
- a new signing key on every deploy, invalidating existing clients and tokens;
- public metadata derived from an internal HTTP socket behind Fly or another TLS proxy;
- empty access-token scopes when a client omits
scope;
- a successful cross-origin callback blocked by
form-action 'self' across the form redirect chain;
- an authenticated MCP subject that is never bound to the provider account used by tool handlers;
- process-local MCP transport sessions when ordinary JSON tool calls can be stateless;
- provider refresh-token races across multiple instances.
This is not connector business logic. Toolcraft should provide a production-shaped hosted OAuth configuration for its existing HTTP server that is difficult to configure incorrectly.
Proposed capability
Extend the existing HTTP server API while retaining the existing low-level escape hatches:
const server = await createHTTPMCPServer(root, {
name: "calendar",
version: "1.0.0",
oauth: hostedOAuth({
publicUrl: "https://calendar.example/mcp",
storage,
provider,
}),
});
Toolcraft should own discovery, OAuth routes, callback completion, subject propagation, production validation, and stateless protected-resource transport. The connector should implement only provider-specific hooks such as signing in, deriving an opaque stable subject, and opening a subject-scoped provider service.
This capability must be explicitly opt-in. Existing non-OAuth Toolcraft users must not need a database, signing key, encryption key, provider hooks, or any new configuration. Stdio MCP, local/insecure HTTP, pre-shared bearer-token HTTP, and externally managed OAuth remain supported independently.
The common API must also stay small: the existing server constructor, one opt-in hostedOAuth() helper, safe production defaults, and a focused provider hook. Protocol-level controls and custom storage behavior remain progressive-disclosure escape hatches rather than required top-level configuration.
Required pieces
Hosted OAuth configuration
- Serve both root and path-qualified RFC 9728 protected-resource metadata.
- Serve authorization-server metadata, JWKS, DCR, authorize, token, and revoke routes.
- Require a canonical public URL; never infer outward URLs from the backend socket when proxying.
- Trust forwarded scheme/host only through the existing explicit trusted-proxy policy.
- Default an omitted authorization
scope to configured defaults; never silently mint an empty-scope token.
- Publish the same supported scopes in both metadata documents and challenges.
- Complete authorization with a real 303 to the exact registered callback, preserving
code, state, and iss.
- Generate interaction CSP from the validated callback origin so Chromium permits the cross-origin form redirect chain without broadening
form-action unnecessarily.
- Make CSRF cookies safe for concurrent authorization tabs (transaction-specific cookie name or equivalent binding).
- Set
sessionIdGenerator: undefined by default when no stream/subscription capability requires stateful MCP sessions.
Durable authorization-server storage
- Define a single
HostedOAuthStorage contract covering OAuth records, encrypted provider sessions, signing-key persistence, atomic refresh coordination, revocation, and cleanup.
- Publish conformance-tested Redis and SQLite reference implementations separately from Toolcraft, or keep them application-owned; neither is imported, bundled, or installed by Toolcraft.
- Keep
AuthorizationServerStore public so applications can provide Postgres or another transactional backend.
- Provide conformance tests that every adapter must pass.
- Keep concrete storage clients outside the Toolcraft package so core and non-OAuth applications do not install Redis, SQLite, or Postgres dependencies.
Keys and production readiness
- Load stable signing keys from deployment secrets/KMS and publish current plus previous public JWKs during rotation.
- Fail production startup for ephemeral stores, ephemeral signing keys, non-HTTPS public URLs, inconsistent scope sets, or missing provider-session encryption.
- Expose an
assertProductionReady() diagnostic with actionable messages.
Subject-bound provider services
- Pass the verified
(issuer, subject, clientId, scopes, resource) into a request-scoped service factory.
- Fail closed when no provider account is linked; never fall back to a process-global credential.
- Define a generic encrypted provider-session repository hook with version/CAS and distributed refresh coordination. Passwords are never persisted.
- Make grant revocation observable so a connector can unlink provider credentials when the final active grant is revoked.
Acceptance criteria
- A reference connector needs no custom OAuth routing, discovery documents, callback HTML/CSP, proxy URL rewriting, DCR registry, token store, or signing-key lifecycle code.
- The reference connector's happy path uses the existing
createHTTPMCPServer() constructor with oauth: hostedOAuth({ storage, provider }); it does not repeat OAuth endpoint or token-lifecycle configuration.
- A non-OAuth connector compiles and runs unchanged, with no hosted-OAuth transitive runtime dependencies or startup checks.
- Importing or calling the ordinary Toolcraft HTTP/stdio APIs does not initialize hosted OAuth infrastructure; only supplying the
hostedOAuth() configuration does.
- ChatGPT DCR + authorization-code PKCE works with its current callback URL and with omitted
scope.
- The browser returns automatically to ChatGPT; no localhost copy/paste step is required.
- Existing registration, access token, and refresh token survive a deploy/restart with the same store and keys.
- Two accounts authorize concurrently and 100+ interleaved tool calls never cross provider credentials, frame/account caches, or refresh state.
- Two app instances racing an upstream rotating refresh token perform one refresh and both use the resulting credential.
- Refresh-token replay revokes its family and grant; revoking one grant does not affect another subject.
- Metadata and challenges contain the canonical HTTPS resource URL behind a Fly-style proxy.
- Logs and persisted records contain no plaintext passwords, provider access tokens, or provider refresh tokens.
Relationship to existing work
This issue is the missing production composition configuration and official persistence story above those primitives.
Problem
Toolcraft exposes the low-level OAuth authorization-server primitives added in #519, but an app author still has to assemble production hosted MCP OAuth by hand. That assembly is security-sensitive and has now produced the same class of failures in multiple connectors.
The current primitives make it easy to accidentally ship:
scope;form-action 'self'across the form redirect chain;This is not connector business logic. Toolcraft should provide a production-shaped hosted OAuth configuration for its existing HTTP server that is difficult to configure incorrectly.
Proposed capability
Extend the existing HTTP server API while retaining the existing low-level escape hatches:
Toolcraft should own discovery, OAuth routes, callback completion, subject propagation, production validation, and stateless protected-resource transport. The connector should implement only provider-specific hooks such as signing in, deriving an opaque stable subject, and opening a subject-scoped provider service.
This capability must be explicitly opt-in. Existing non-OAuth Toolcraft users must not need a database, signing key, encryption key, provider hooks, or any new configuration. Stdio MCP, local/insecure HTTP, pre-shared bearer-token HTTP, and externally managed OAuth remain supported independently.
The common API must also stay small: the existing server constructor, one opt-in
hostedOAuth()helper, safe production defaults, and a focused provider hook. Protocol-level controls and custom storage behavior remain progressive-disclosure escape hatches rather than required top-level configuration.Required pieces
Hosted OAuth configuration
scopeto configured defaults; never silently mint an empty-scope token.code,state, andiss.form-actionunnecessarily.sessionIdGenerator: undefinedby default when no stream/subscription capability requires stateful MCP sessions.Durable authorization-server storage
HostedOAuthStoragecontract covering OAuth records, encrypted provider sessions, signing-key persistence, atomic refresh coordination, revocation, and cleanup.AuthorizationServerStorepublic so applications can provide Postgres or another transactional backend.Keys and production readiness
assertProductionReady()diagnostic with actionable messages.Subject-bound provider services
(issuer, subject, clientId, scopes, resource)into a request-scoped service factory.Acceptance criteria
createHTTPMCPServer()constructor withoauth: hostedOAuth({ storage, provider }); it does not repeat OAuth endpoint or token-lifecycle configuration.hostedOAuth()configuration does.scope.Relationship to existing work
This issue is the missing production composition configuration and official persistence story above those primitives.