@softure-ai/mcp-access 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +146 -21
  3. package/adoption/move-app-tables.sql +100 -0
  4. package/dist/contract.d.ts +13 -1
  5. package/dist/contract.d.ts.map +1 -1
  6. package/dist/index.d.ts +58 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +26 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/messages/en.d.ts +28 -0
  11. package/dist/messages/en.d.ts.map +1 -1
  12. package/dist/messages/en.js +28 -0
  13. package/dist/messages/en.js.map +1 -1
  14. package/dist/messages/index.d.ts +56 -0
  15. package/dist/messages/index.d.ts.map +1 -1
  16. package/dist/messages/pl.d.ts.map +1 -1
  17. package/dist/messages/pl.js +28 -0
  18. package/dist/messages/pl.js.map +1 -1
  19. package/dist/next/actions.d.ts +3 -1
  20. package/dist/next/actions.d.ts.map +1 -1
  21. package/dist/next/actions.js +23 -2
  22. package/dist/next/actions.js.map +1 -1
  23. package/dist/next/context.d.ts +7 -1
  24. package/dist/next/context.d.ts.map +1 -1
  25. package/dist/next/context.js +12 -10
  26. package/dist/next/context.js.map +1 -1
  27. package/dist/next/index.d.ts +4 -1
  28. package/dist/next/index.d.ts.map +1 -1
  29. package/dist/next/index.js +5 -2
  30. package/dist/next/index.js.map +1 -1
  31. package/dist/next/oauth-page.d.ts +7 -0
  32. package/dist/next/oauth-page.d.ts.map +1 -0
  33. package/dist/next/oauth-page.js +36 -0
  34. package/dist/next/oauth-page.js.map +1 -0
  35. package/dist/next/oauth-routes.d.ts +19 -0
  36. package/dist/next/oauth-routes.d.ts.map +1 -0
  37. package/dist/next/oauth-routes.js +53 -0
  38. package/dist/next/oauth-routes.js.map +1 -0
  39. package/dist/next/pages.d.ts.map +1 -1
  40. package/dist/next/pages.js +13 -2
  41. package/dist/next/pages.js.map +1 -1
  42. package/dist/options.d.ts +40 -0
  43. package/dist/options.d.ts.map +1 -1
  44. package/dist/options.js +94 -1
  45. package/dist/options.js.map +1 -1
  46. package/dist/origins.d.ts +27 -0
  47. package/dist/origins.d.ts.map +1 -0
  48. package/dist/origins.js +32 -0
  49. package/dist/origins.js.map +1 -0
  50. package/dist/schema.d.ts +486 -0
  51. package/dist/schema.d.ts.map +1 -1
  52. package/dist/schema.js +45 -3
  53. package/dist/schema.js.map +1 -1
  54. package/dist/server/context.d.ts +4 -0
  55. package/dist/server/context.d.ts.map +1 -0
  56. package/dist/server/context.js +13 -0
  57. package/dist/server/context.js.map +1 -0
  58. package/dist/server/endpoint.d.ts +2 -4
  59. package/dist/server/endpoint.d.ts.map +1 -1
  60. package/dist/server/endpoint.js +14 -9
  61. package/dist/server/endpoint.js.map +1 -1
  62. package/dist/server/health.d.ts.map +1 -1
  63. package/dist/server/health.js +5 -3
  64. package/dist/server/health.js.map +1 -1
  65. package/dist/server/index.d.ts +8 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +7 -0
  68. package/dist/server/index.js.map +1 -1
  69. package/dist/server/oauth-http.d.ts +127 -0
  70. package/dist/server/oauth-http.d.ts.map +1 -0
  71. package/dist/server/oauth-http.js +397 -0
  72. package/dist/server/oauth-http.js.map +1 -0
  73. package/dist/server/oauth-validation.d.ts +28 -0
  74. package/dist/server/oauth-validation.d.ts.map +1 -0
  75. package/dist/server/oauth-validation.js +88 -0
  76. package/dist/server/oauth-validation.js.map +1 -0
  77. package/dist/server/oauth.d.ts +106 -0
  78. package/dist/server/oauth.d.ts.map +1 -0
  79. package/dist/server/oauth.js +235 -0
  80. package/dist/server/oauth.js.map +1 -0
  81. package/dist/server/options.d.ts +11 -2
  82. package/dist/server/options.d.ts.map +1 -1
  83. package/dist/server/options.js +11 -4
  84. package/dist/server/options.js.map +1 -1
  85. package/dist/server/origins.d.ts +8 -0
  86. package/dist/server/origins.d.ts.map +1 -0
  87. package/dist/server/origins.js +15 -0
  88. package/dist/server/origins.js.map +1 -0
  89. package/dist/server/pkce.d.ts +7 -0
  90. package/dist/server/pkce.d.ts.map +1 -0
  91. package/dist/server/pkce.js +24 -0
  92. package/dist/server/pkce.js.map +1 -0
  93. package/dist/server/privacy.d.ts +7 -0
  94. package/dist/server/privacy.d.ts.map +1 -1
  95. package/dist/server/privacy.js +19 -9
  96. package/dist/server/privacy.js.map +1 -1
  97. package/dist/server/scopes.d.ts +5 -0
  98. package/dist/server/scopes.d.ts.map +1 -0
  99. package/dist/server/scopes.js +6 -0
  100. package/dist/server/scopes.js.map +1 -0
  101. package/dist/server/tokens.d.ts +18 -6
  102. package/dist/server/tokens.d.ts.map +1 -1
  103. package/dist/server/tokens.js +60 -25
  104. package/dist/server/tokens.js.map +1 -1
  105. package/dist/ui/consent-form.d.ts +31 -0
  106. package/dist/ui/consent-form.d.ts.map +1 -0
  107. package/dist/ui/consent-form.js +24 -0
  108. package/dist/ui/consent-form.js.map +1 -0
  109. package/dist/ui/index.d.ts +2 -1
  110. package/dist/ui/index.d.ts.map +1 -1
  111. package/dist/ui/index.js +1 -0
  112. package/dist/ui/index.js.map +1 -1
  113. package/dist/ui/token-manager.d.ts +16 -2
  114. package/dist/ui/token-manager.d.ts.map +1 -1
  115. package/dist/ui/token-manager.js +6 -2
  116. package/dist/ui/token-manager.js.map +1 -1
  117. package/migrations/0002_create_oauth_grants.sql +68 -0
  118. package/module.json +18 -4
  119. package/package.json +2 -1
  120. package/src/contract.ts +16 -1
  121. package/src/index.ts +33 -4
  122. package/src/messages/en.ts +28 -0
  123. package/src/messages/pl.ts +28 -0
  124. package/src/next/actions.ts +23 -3
  125. package/src/next/context.ts +15 -8
  126. package/src/next/index.ts +12 -2
  127. package/src/next/oauth-page.tsx +68 -0
  128. package/src/next/oauth-routes.ts +73 -0
  129. package/src/next/pages.tsx +16 -3
  130. package/src/options.ts +107 -1
  131. package/src/origins.ts +48 -0
  132. package/src/schema.ts +51 -3
  133. package/src/server/context.ts +14 -0
  134. package/src/server/endpoint.ts +14 -9
  135. package/src/server/health.ts +5 -3
  136. package/src/server/index.ts +56 -0
  137. package/src/server/oauth-http.ts +480 -0
  138. package/src/server/oauth-validation.ts +106 -0
  139. package/src/server/oauth.ts +315 -0
  140. package/src/server/options.ts +20 -4
  141. package/src/server/origins.ts +19 -0
  142. package/src/server/pkce.ts +27 -0
  143. package/src/server/privacy.ts +26 -9
  144. package/src/server/scopes.ts +6 -0
  145. package/src/server/tokens.ts +62 -25
  146. package/src/ui/consent-form.tsx +103 -0
  147. package/src/ui/index.ts +3 -0
  148. package/src/ui/token-manager.tsx +79 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`mcp-access@x.y.z`).
6
6
 
7
+ ## 0.1.8
8
+
9
+ - OAuth URLs follow the request: `resolveAppOrigin(request)` (with `readRequestOrigin`, which reads
10
+ `Host` and `X-Forwarded-Proto`) sets the app origin per request for the issuer, the endpoints in
11
+ the metadata, `resource`, `resource_metadata` in the endpoint's `401`, `iss` and the consent
12
+ decision's `Origin` check. Without it nothing changes.
13
+ - `resourceOrigins`: other public hosts; the root protected resource metadata asked on one names it
14
+ as `resource`, and the authorization and token endpoints accept it.
15
+ - `oauth.metadata.authorizationServer` / `.protectedResource`: extra discovery keys (an object, or
16
+ a function of the request's origins); generated keys win, `resource_name` can be replaced.
17
+ - Discovery documents carry `Vary: host, x-forwarded-proto`; `getAuthorizationServerMetadataRoute`
18
+ now reads the request (re-exporting it as `GET` keeps working).
19
+ - New exports: `createMcpAccessContext` and `resolveMcpOrigins` (`/server`), `getMcpAccessContext`
20
+ (`/next`), `OAuthClientRow`, `McpOrigins` and `readRequestOrigin` (root and `/server`). URL
21
+ builders take the origins as an optional last argument.
22
+
23
+ ## 0.1.7
24
+
25
+ - OAuth 2.1 for MCP clients, opt-in (`oauth.enabled`): protected resource and authorization server
26
+ metadata, `resource_metadata` in the endpoint's `401`, dynamic client registration, a consent
27
+ page with PKCE `S256`, a token endpoint with rotating refresh tokens and replay detection, and a
28
+ connected apps list whose revoke deletes the grant's access tokens. Mount the new routes and the
29
+ consent page (README §4) and add `"mcp-oauth"` from `MCP_RATE_LIMIT_BUCKETS` to the security buckets.
30
+ - Separate lifetimes: `tokenLifetimeDays` for hand-issued tokens, `oauth.accessTokenLifetimeMinutes`,
31
+ `oauth.refreshTokenLifetimeDays` and `oauth.authorizationCodeLifetimeMinutes` for OAuth.
32
+ - `legacyTokenPattern`: tokens an app issued before adopting the module keep working.
33
+ - Migration 2 (`create_oauth_grants`) adds the OAuth tables and `access_tokens.grant_id`.
34
+ - `adoption/move-app-tables.sql`: moves an app's own token and OAuth tables into `mcp` for
35
+ `baseline: { "mcp-access": 2 }` (README §5).
36
+
7
37
  ## 0.1.6
8
38
 
9
39
  - Adapters use the configured database handle; `@softure-ai/ui` is a peer dependency.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Lets the users of a Next.js app connect an AI assistant (Claude Code, Claude Desktop, Cursor) to
4
4
  their own data over MCP: hashed, scoped, expiring access tokens, a rate-limited Bearer endpoint
5
- around the app's own MCP server, and a token page with ready setup instructions. Built from
6
- FIRE_TRACKER's assistant access (`src/db/access-tokens.ts`, `src/lib/{mcp-auth,access-token-status,mcp-client-config}.ts`,
7
- `src/app/api/mcp/route.ts`, `src/app/actions/{manage-tokens,tokens,tokens-contract}.ts`), with the
8
- domain server left to the app and the limit check made race-free.
5
+ around the app's own MCP server, a token page with ready setup instructions and, opt-in, OAuth 2.1
6
+ so assistants such as claude.ai or ChatGPT connect with a consent screen instead of a pasted token.
7
+ Built from an adopting app's assistant access, with the domain server left to the app and the
8
+ limit check made race-free.
9
9
 
10
10
  ## 1. What it provides
11
11
 
@@ -22,11 +22,23 @@ domain server left to the app and the limit check made race-free.
22
22
  setup (a prompt for Claude Code and a `claude-cli://` link, the `claude mcp add` command, an
23
23
  `mcpServers` file, Claude Desktop through `mcp-remote`, the header), and the user's tokens with
24
24
  their status (active, expiring in n days, expired), last use and a revoke button.
25
+ - **OAuth 2.1** (`oauth.enabled`, off by default), as the MCP authorization spec asks for it:
26
+ - a `401` from the endpoint names the protected resource metadata (`resource_metadata` in the
27
+ challenge, RFC 9728), which points at the authorization server metadata (RFC 8414);
28
+ - dynamic client registration (RFC 7591, `POST /api/oauth/register`), public or confidential;
29
+ - `/oauth/authorize`: authorization code with PKCE `S256` only, on a consent page that shows the
30
+ app, where the code goes, the signed-in account and the access; writes need the `mcp:write`
31
+ scope, `allowWrites` and the person's tick;
32
+ - `POST /api/oauth/token`: `authorization_code` and `refresh_token`. Refresh tokens rotate on
33
+ every use; presenting a rotated one again (`previous_refresh_token_hash`) revokes the whole
34
+ grant, and so does reusing a code (RFC 6749 §4.1.2). `resource` (RFC 8707) must name the
35
+ endpoint when sent; the redirect carries `iss` (RFC 9207);
36
+ - one grant per account and client, listed on the token page as "connected apps"; revoking it
37
+ deletes its access tokens at once (`grant_id`, `ON DELETE CASCADE`). OAuth access tokens do not
38
+ count against `maxTokensPerUser` and are not listed with the hand-issued ones.
39
+ - Tokens an app issued before adopting the module keep working through `legacyTokenPattern`.
25
40
  - Server functions for scripts and other hosts, and a health check for `GET /api/health`.
26
41
 
27
- OAuth for MCP (discovery, dynamic client registration) is not part of this module: clients that
28
- send a static header cover the personal-token case.
29
-
30
42
  ## 2. Installation
31
43
 
32
44
  ```bash
@@ -64,14 +76,28 @@ mcpAccess({
64
76
  | `serverName` | `string` | required | The name clients list the server under (lowercase, digits, inner dashes; no quotes needed in a shell). |
65
77
  | `tools` | `{ name, access: "read" \| "write", description: { en, pl? } }[]` | `[]` | The catalog the token page shows. Keep it next to the factory; the example app tests that both list the same tools. |
66
78
  | `allowWrites` | `boolean` | `false` | Lets write tokens be issued and used. Off, the page offers no write tokens and every token reads only. |
67
- | `tokenLifetimeDays` | `integer` 1–365 | `90` | How long a token works. There is no renewal: the user issues a new one. |
79
+ | `tokenLifetimeDays` | `integer` 1–365 | `90` | How long a hand-issued token works. There is no renewal: the user issues a new one. |
68
80
  | `maxTokensPerUser` | `integer` 1–100 | `20` | Unexpired tokens one account may hold. Counted under a per-user lock, so parallel requests cannot overshoot. |
69
81
  | `expiryWarningDays` | `integer` 0–60 | `14` | From how many calendar days before the expiry the list warns. |
70
- | `routes` | `{ page?, endpoint? }` | `/account/mcp`, `/api/mcp` | Move the page or the endpoint; the endpoint URL in the setup is `appOrigin` + `endpoint`. |
82
+ | `legacyTokenPattern` | `RegExp` | — | Also accept tokens of this shape, issued by the app before it adopted the module (e.g. `/^[0-9a-f]{64}$/`), looked up by the same sha256. Must be anchored (`^…$`), without the `g` or `y` flag; tokens longer than 512 characters are refused before it runs. New tokens are always `sftmcp_`. |
83
+ | `oauth.enabled` | `boolean` | `false` | Turns on the OAuth routes, the consent page, the connected apps list and `resource_metadata`. Off, the OAuth routes answer `404`. |
84
+ | `oauth.accessTokenLifetimeMinutes` | `integer` 5–1440 | `60` | Lifetime of an access token issued through OAuth; the client refreshes it. |
85
+ | `oauth.refreshTokenLifetimeDays` | `integer` 1–365 | `90` | Lifetime of a refresh token, renewed on every rotation. An unused connection ends after it. |
86
+ | `oauth.authorizationCodeLifetimeMinutes` | `integer` 1–10 | `10` | Lifetime of an authorization code (single use). |
87
+ | `oauth.metadata` | `{ authorizationServer?, protectedResource? }`, each an object or `(origins) => object` | — | Extra keys of the discovery documents (`jwks_uri`, `service_documentation`, `resource_documentation`, `agent_auth`…). Generated keys win; a static object that sets one is refused at startup. The protected resource document takes the app's `resource_name` (default: `serverName`). |
88
+ | `resolveAppOrigin` | `(request) => string \| null` | — | The app origin of one request, when it is not the fixed `appOrigin` (an image built once and served elsewhere, a proxy). `readRequestOrigin` reads it from `Host`. Null keeps `appOrigin`; anything but a bare http(s) origin throws. |
89
+ | `resourceOrigins` | `string[]` (≤ 16 http(s) origins) | `[]` | Other public hosts of the app, e.g. the apex next to `app.`: the root protected resource metadata asked on one of them names it as `resource`, and the OAuth endpoints accept it as `resource`. |
90
+ | `routes` | `{ page?, endpoint?, oauthConsent?, oauthDecision?, oauthToken?, oauthRegister? }` | `/account/mcp`, `/api/mcp`, `/oauth/authorize`, `/api/oauth/authorize`, `/api/oauth/token`, `/api/oauth/register` | Move the page, the endpoint or the OAuth paths; URLs in the setup and the metadata are the app origin (`appOrigin`, or `resolveAppOrigin`'s) + the path. |
71
91
  | `messages` | partial `en` / `pl` | — | Copy overrides. |
72
92
 
73
- `MCP_RATE_LIMIT_BUCKETS` is `{ mcp: { limit: 200, windowMinutes: 15 } }` per client address. Every
74
- request counts, valid or not, and before the token lookup; assistants behind one address share it.
93
+ `allowWrites` is a deploy decision, so read it from the environment rather than hard-coding it:
94
+ `allowWrites: process.env.MCP_ALLOW_WRITES === "1"` is the intended way. Any other value, or none,
95
+ keeps every token read-only.
96
+
97
+ `MCP_RATE_LIMIT_BUCKETS` is `{ mcp: { limit: 200, windowMinutes: 15 }, "mcp-oauth": { limit: 60, windowMinutes: 15 } }`
98
+ per client address. Every endpoint request counts, valid or not, and before the token lookup;
99
+ assistants behind one address share it. `mcp-oauth` counts the public OAuth requests:
100
+ registrations per address, token requests per address and client.
75
101
 
76
102
  ## 4. Mounting
77
103
 
@@ -111,7 +137,70 @@ mid-call notifications (progress, logging) are dropped. Keep `/api/mcp` out of a
111
137
  redirects to the login page: the endpoint authenticates by token, not by cookie.
112
138
 
113
139
  `TokenManager` (`@softure-ai/mcp-access/ui`) takes the actions `issueTokenAction` and
114
- `revokeTokenAction` (`/next`) as props for an app that composes its own page.
140
+ `revokeTokenAction` (`/next`) as props for an app that composes its own page, and `grants` with
141
+ `revokeGrantAction` for the connected apps list.
142
+
143
+ With `oauth.enabled`, mount the OAuth routes and the consent page too (the paths for the default
144
+ `routes`):
145
+
146
+ ```ts
147
+ // app/oauth/authorize/page.tsx
148
+ export { OAuthConsentPage as default } from "@softure-ai/mcp-access/next";
149
+ export const dynamic = "force-dynamic";
150
+
151
+ // app/api/oauth/authorize/route.ts
152
+ export { decideOAuthAuthorizationRoute as POST } from "@softure-ai/mcp-access/next";
153
+
154
+ // app/api/oauth/token/route.ts and app/api/oauth/register/route.ts
155
+ export { exchangeOAuthTokenRoute as POST, answerOAuthPreflight as OPTIONS } from "@softure-ai/mcp-access/next";
156
+ export { registerOAuthClientRoute as POST, answerOAuthPreflight as OPTIONS } from "@softure-ai/mcp-access/next";
157
+
158
+ // app/.well-known/oauth-authorization-server/route.ts
159
+ export { getAuthorizationServerMetadataRoute as GET, answerOAuthPreflight as OPTIONS } from "@softure-ai/mcp-access/next";
160
+
161
+ // app/.well-known/oauth-protected-resource/route.ts and
162
+ // app/.well-known/oauth-protected-resource/api/mcp/route.ts (the endpoint's path after the prefix)
163
+ export { getProtectedResourceMetadataRoute as GET, answerOAuthPreflight as OPTIONS } from "@softure-ai/mcp-access/next";
164
+ ```
165
+
166
+ `/.well-known/oauth-*`, `/api/oauth/token` and `/api/oauth/register` are public: keep them out of
167
+ a proxy guard, like `/api/mcp`. The consent page and the decision route check the session
168
+ themselves (the page sends a signed-out person to the login page and back); the decision route
169
+ also refuses a request whose `Origin` is not the app origin.
170
+ TypeScript's `**` skips dot folders: with an `include` list in `tsconfig.json`, add
171
+ `"app/.well-known/**/*.ts"`.
172
+
173
+ **Origins.** Every OAuth URL (issuer, endpoints, `resource`, `resource_metadata`, `iss`) and the
174
+ decision's `Origin` check use the app origin of the request: `appOrigin`, unless
175
+ `resolveAppOrigin` answers. The discovery routes read the request, so Next never renders them at
176
+ build time, and they answer with `Vary: host, x-forwarded-proto`.
177
+
178
+ - *An image built once and served under another origin* (a test stack on another port, a
179
+ production image against a local stack): `resolveAppOrigin: process.env.APP_ORIGIN ? undefined :
180
+ readRequestOrigin`. `readRequestOrigin` trusts `Host` and `X-Forwarded-Proto`, so use it only
181
+ behind a proxy that passes `Host` through and refuses hosts it does not serve. The consent page,
182
+ its decision and the token endpoint then agree on the origin the browser used.
183
+ - *A second public host* (product on `https://app.example.com`, site on `https://example.com`):
184
+ `resourceOrigins: ["https://example.com"]`, and route that host's `/.well-known/oauth-*` to the
185
+ app. Its root protected resource metadata names `https://example.com` as `resource` (RFC 9728
186
+ §3.3) with the authorization server on the app origin; `Host` only picks among the listed
187
+ origins.
188
+ - *Discovery extras:* keys the app adds and serves itself, computed from the request's origins:
189
+
190
+ ```ts
191
+ oauth: {
192
+ enabled: true,
193
+ metadata: {
194
+ authorizationServer: ({ appOrigin }) => ({ jwks_uri: `${appOrigin}/.well-known/jwks.json` }),
195
+ protectedResource: { resource_name: "Acme MCP server", resource_documentation: "https://example.com/docs/mcp" },
196
+ },
197
+ },
198
+ ```
199
+
200
+ An app that composes its own routes from `/server` builds the context with
201
+ `createMcpAccessContext(config)` (`getMcpAccessContext()` in `/next` reads the registered config),
202
+ resolves origins with `resolveMcpOrigins(config, request)` and passes them to the URL builders,
203
+ whose last argument they are; `OAuthClientRow` is exported from the package root.
115
204
 
116
205
  ## 5. Migrations and tables
117
206
 
@@ -130,6 +219,33 @@ redirects to the login page: the endpoint authenticates by token, not by cookie.
130
219
  Issuing deletes the issuer's expired tokens; `pruneAccessTokens(ctx)` (`/server`) deletes every
131
220
  expired token, for a scheduled job. Rollback: in the migration header.
132
221
 
222
+ `migrations/0002_create_oauth_grants.sql` creates `mcp.oauth_clients` (client id, name, redirect
223
+ URIs, auth method, sha256 of a confidential client's secret), `mcp.oauth_authorization_codes`
224
+ (sha256, PKCE challenge, redirect URI, single use through `used_at`) and `mcp.oauth_grants` (one
225
+ per account and client: write flag, sha256 of the current and the previous refresh token, refresh
226
+ expiry, last use), and adds `access_tokens.grant_id` (null for hand-issued tokens). Every row goes
227
+ with the account. `pruneOAuthRecords(ctx)` deletes expired codes, grants whose refresh token has
228
+ expired (with their access tokens) and clients registered over a day ago that hold no grant.
229
+
230
+ ### Adopting an app's own tables
231
+
232
+ An app that ran its own `public.access_tokens` (with `grant_id`) and `oauth_clients`,
233
+ `oauth_authorization_codes` and `oauth_grants` tables, with the module's column names, adopts them
234
+ without losing a row. `adoption/move-app-tables.sql` (shipped in the package, tested in
235
+ `tests/adoption.test.ts`) moves the four tables into `mcp`, drops the app's constraints, indexes
236
+ and defaults whatever their names, cuts names to 60 characters, drops authorization codes the
237
+ module cannot verify, and creates the module's constraints and indexes. Run it as the app's own
238
+ migration in `before`, then declare the baseline ([adoption playbook](../../docs/05-adoption-playbook.md) step 3):
239
+
240
+ ```ts
241
+ app: { before, baseline: { "mcp-access": 2 } }
242
+ ```
243
+
244
+ Old tokens keep working with `legacyTokenPattern`; connected apps keep their grants, and their old
245
+ refresh tokens rotate into module-shaped ones on the next refresh. An app without OAuth tables
246
+ drops its `grant_id` column (if any), deletes the OAuth part of the script and uses
247
+ `baseline: { "mcp-access": 1 }`; file 2 then runs as a normal migration.
248
+
133
249
  ## 6. Environment variables
134
250
 
135
251
  None of its own. The endpoint URL comes from `appOrigin` in `softure.config.ts`.
@@ -147,8 +263,9 @@ None. `allowWrites` is configuration, not a runtime switch: changing who may wri
147
263
  ## 9. Copy
148
264
 
149
265
  `mcpAccessMessages.{en,pl}`: `page`, `tools`, `issue`, `issued`, `setup.assistantPrompt`
150
- (placeholders `{serverName}` and `{command}`), `list`, and `errors` for the codes
151
- `mcp-access.name_required`, `name_too_long`, `token_limit_reached`, `token_not_found`,
266
+ (placeholders `{serverName}` and `{command}`), `list`, `grants` (connected apps), `consent` (the
267
+ consent page and its errors), and `errors` for the codes
268
+ `mcp-access.name_required`, `name_too_long`, `token_limit_reached`, `token_not_found`, `grant_not_found`,
152
269
  `auth.unauthenticated`, `core.database_failed` and `core.unexpected`. Tool descriptions come from
153
270
  the `tools` option, per locale.
154
271
 
@@ -157,22 +274,30 @@ the `tools` option, per locale.
157
274
  The MCP server factory passed to `createMcpRoute` is the module's only hook. `/server` also exports
158
275
  `createMcpEndpoint({ createServer })`, a `(ctx, request) => Response` for hosts other than Next, and
159
276
  `issueAccessToken`, `listAccessTokens`, `revokeAccessToken`, `verifyAccessToken` and
160
- `pruneAccessTokens` for scripts.
277
+ `pruneAccessTokens` for scripts. For OAuth: `listOAuthGrants`, `revokeOAuthGrant`,
278
+ `pruneOAuthRecords`, the protocol steps (`registerMcpClient`, `createAuthorizationCode`,
279
+ `exchangeAuthorizationCode`, `refreshMcpGrant`) and the HTTP handlers behind the routes
280
+ (`handleClientRegistration`, `handleTokenRequest`, `handleAuthorizationDecision`,
281
+ `validateAuthorizationRequest`, `serveDiscoveryDocument`) for hosts other than Next.
161
282
 
162
283
  ## 11. GDPR
163
284
 
164
285
  The rows hold no personal data beyond the owner's id and the token names. The module contributes
165
286
  to `@softure-ai/privacy` (`privacy` flags on):
166
287
 
167
- - **Export** (`exportMcpAccessUserData`): `accessTokens`, each token's name, write flag and
168
- created, expiry and last-use dates. Never the token hash.
169
- - **Deletion** (`deleteMcpAccessUserData`): every token of the user, before auth deletes the
170
- account (the foreign key's cascade would remove them too).
288
+ - **Export** (`exportMcpAccessUserData`): `accessTokens`, each hand-issued token's name, write
289
+ flag and created, expiry and last-use dates, and `connectedApps`, each grant's client name, write
290
+ flag, created and last-use dates. Never a hash.
291
+ - **Deletion** (`deleteMcpAccessUserData`): the user's authorization codes, grants and tokens,
292
+ before auth deletes the account (the foreign keys' cascade would remove them too). Registered
293
+ clients hold no personal data and are pruned when unused.
171
294
 
172
295
  ## 12. Limitations / known gaps
173
296
 
174
- - No OAuth for MCP: clients without a header field use `mcp-remote` (Claude Desktop) or another
175
- bridge.
297
+ - OAuth has no token introspection or revocation endpoint (RFC 7009) for clients: the person
298
+ disconnects an app on the token page. Access tokens are opaque; the package serves no JWKS (an
299
+ app that wants `jwks_uri` adds it through `oauth.metadata` and serves the empty set itself).
300
+ - Only `S256` PKCE and the `authorization_code` and `refresh_token` grants.
176
301
  - Write access is per token, not per tool; finer scopes would be a new option.
177
302
  - The rate limit is per client address, not per token.
178
303
  - `responseMode: "json"` drops mid-call notifications; a streaming tool would need a different mode.
@@ -0,0 +1,100 @@
1
+ -- Example adoption migration (README §5): an app that ran its own MCP tokens and OAuth tables in
2
+ -- `public` moves them into the module's schema, so `migrate` with `baseline: { "mcp-access": 2 }`
3
+ -- (or `softure migrate --adopt mcp-access@<version> --through 2`) can adopt files 1 and 2 without
4
+ -- running them. Run it as the app's own migration, in its `before` hook, once. It keeps every row:
5
+ -- issued tokens, connected assistants and their refresh tokens keep working.
6
+ --
7
+ -- Assumed app shape: public.access_tokens (id, user_id, name, token_hash, can_write, created_at,
8
+ -- expires_at, last_used_at, grant_id), public.oauth_clients, public.oauth_authorization_codes and
9
+ -- public.oauth_grants with the module's column names, user_id referencing auth.users. Constraint
10
+ -- and index names, column defaults and checks may differ: the script drops the app's and creates
11
+ -- the module's. An app without OAuth tables deletes the OAuth part and adopts `--through 1` after
12
+ -- dropping its grant_id column (or has none).
13
+ -- Rollback: move the tables back with ALTER TABLE mcp.<table> SET SCHEMA public before anything
14
+ -- else writes to them.
15
+
16
+ CREATE SCHEMA IF NOT EXISTS mcp;
17
+ ALTER TABLE public.oauth_clients SET SCHEMA mcp;
18
+ ALTER TABLE public.oauth_authorization_codes SET SCHEMA mcp;
19
+ ALTER TABLE public.oauth_grants SET SCHEMA mcp;
20
+ ALTER TABLE public.access_tokens SET SCHEMA mcp;
21
+
22
+ -- The app's constraints (but the primary keys) and indexes, whatever their names.
23
+ DO $$
24
+ DECLARE
25
+ item record;
26
+ BEGIN
27
+ FOR item IN
28
+ SELECT con.conrelid::regclass AS table_name, con.conname
29
+ FROM pg_constraint con JOIN pg_class c ON c.oid = con.conrelid JOIN pg_namespace n ON n.oid = c.relnamespace
30
+ WHERE n.nspname = 'mcp' AND c.relname IN ('access_tokens', 'oauth_grants', 'oauth_authorization_codes', 'oauth_clients')
31
+ AND con.contype IN ('f', 'u', 'c', 'x')
32
+ ORDER BY con.contype = 'f' DESC
33
+ LOOP
34
+ EXECUTE format('ALTER TABLE %s DROP CONSTRAINT %I', item.table_name, item.conname);
35
+ END LOOP;
36
+ FOR item IN
37
+ SELECT i.relname
38
+ FROM pg_index x JOIN pg_class i ON i.oid = x.indexrelid JOIN pg_class c ON c.oid = x.indrelid JOIN pg_namespace n ON n.oid = c.relnamespace
39
+ WHERE n.nspname = 'mcp' AND c.relname IN ('access_tokens', 'oauth_grants', 'oauth_authorization_codes', 'oauth_clients') AND NOT x.indisprimary
40
+ LOOP
41
+ EXECUTE format('DROP INDEX mcp.%I', item.relname);
42
+ END LOOP;
43
+ END $$;
44
+
45
+ -- Defaults: the module sets every value itself, ids aside.
46
+ ALTER TABLE mcp.access_tokens ALTER COLUMN can_write DROP DEFAULT, ALTER COLUMN created_at DROP DEFAULT;
47
+ ALTER TABLE mcp.oauth_clients ALTER COLUMN created_at DROP DEFAULT;
48
+ ALTER TABLE mcp.oauth_authorization_codes ALTER COLUMN can_write DROP DEFAULT, ALTER COLUMN created_at DROP DEFAULT;
49
+ ALTER TABLE mcp.oauth_grants ALTER COLUMN can_write DROP DEFAULT, ALTER COLUMN created_at DROP DEFAULT;
50
+
51
+ -- Data the module's checks refuse: names over 60 characters (a client name is also the name of
52
+ -- its access tokens) or with outer spaces.
53
+ UPDATE mcp.oauth_clients SET client_name = btrim(left(btrim(client_name), 60)) WHERE client_name <> btrim(left(btrim(client_name), 60));
54
+ UPDATE mcp.access_tokens SET name = btrim(left(btrim(name), 60)) WHERE name <> btrim(left(btrim(name), 60));
55
+ -- Codes live minutes: one the module could not verify (not an S256 challenge) is dropped, and the
56
+ -- client starts its authorization again.
57
+ DELETE FROM mcp.oauth_authorization_codes WHERE code_challenge !~ '^[A-Za-z0-9_-]{43}$' OR expires_at <= created_at;
58
+
59
+ -- The module's constraints and indexes (migrations/0001 and 0002), by the names it gives them.
60
+ ALTER TABLE mcp.oauth_clients
61
+ ADD CONSTRAINT oauth_clients_client_id_check CHECK (char_length(client_id) BETWEEN 1 AND 255),
62
+ ADD CONSTRAINT oauth_clients_client_name_check CHECK (char_length(client_name) BETWEEN 1 AND 60 AND client_name = btrim(client_name)),
63
+ ADD CONSTRAINT oauth_clients_redirect_uris_check CHECK (jsonb_typeof(redirect_uris) = 'array' AND jsonb_array_length(redirect_uris) BETWEEN 1 AND 10),
64
+ ADD CONSTRAINT oauth_clients_token_endpoint_auth_method_check CHECK (token_endpoint_auth_method IN ('none', 'client_secret_post', 'client_secret_basic')),
65
+ ADD CONSTRAINT oauth_clients_client_secret_hash_check CHECK (client_secret_hash ~ '^[0-9a-f]{64}$'),
66
+ ADD CONSTRAINT oauth_clients_client_id_key UNIQUE (client_id),
67
+ ADD CONSTRAINT oauth_clients_secret_matches_method CHECK ((token_endpoint_auth_method = 'none') = (client_secret_hash IS NULL));
68
+
69
+ ALTER TABLE mcp.oauth_authorization_codes
70
+ ADD CONSTRAINT oauth_authorization_codes_code_hash_check CHECK (code_hash ~ '^[0-9a-f]{64}$'),
71
+ ADD CONSTRAINT oauth_authorization_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES mcp.oauth_clients (id) ON DELETE CASCADE,
72
+ ADD CONSTRAINT oauth_authorization_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE,
73
+ ADD CONSTRAINT oauth_authorization_codes_code_challenge_check CHECK (code_challenge ~ '^[A-Za-z0-9_-]{43}$'),
74
+ ADD CONSTRAINT oauth_authorization_codes_check CHECK (expires_at > created_at),
75
+ ADD CONSTRAINT oauth_authorization_codes_code_hash_key UNIQUE (code_hash);
76
+ CREATE INDEX oauth_authorization_codes_client_id_idx ON mcp.oauth_authorization_codes (client_id);
77
+ CREATE INDEX oauth_authorization_codes_user_id_idx ON mcp.oauth_authorization_codes (user_id);
78
+ CREATE INDEX oauth_authorization_codes_expires_at_idx ON mcp.oauth_authorization_codes (expires_at);
79
+
80
+ ALTER TABLE mcp.oauth_grants
81
+ ADD CONSTRAINT oauth_grants_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE,
82
+ ADD CONSTRAINT oauth_grants_client_id_fkey FOREIGN KEY (client_id) REFERENCES mcp.oauth_clients (id) ON DELETE CASCADE,
83
+ ADD CONSTRAINT oauth_grants_refresh_token_hash_check CHECK (refresh_token_hash ~ '^[0-9a-f]{64}$'),
84
+ ADD CONSTRAINT oauth_grants_previous_refresh_token_hash_check CHECK (previous_refresh_token_hash ~ '^[0-9a-f]{64}$'),
85
+ ADD CONSTRAINT oauth_grants_refresh_token_hash_key UNIQUE (refresh_token_hash),
86
+ ADD CONSTRAINT oauth_grants_user_id_client_id_key UNIQUE (user_id, client_id);
87
+ CREATE INDEX oauth_grants_client_id_idx ON mcp.oauth_grants (client_id);
88
+ CREATE INDEX oauth_grants_previous_refresh_token_hash_idx ON mcp.oauth_grants (previous_refresh_token_hash);
89
+ CREATE INDEX oauth_grants_refresh_expires_at_idx ON mcp.oauth_grants (refresh_expires_at);
90
+
91
+ ALTER TABLE mcp.access_tokens
92
+ ADD CONSTRAINT access_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE,
93
+ ADD CONSTRAINT access_tokens_name_check CHECK (char_length(name) BETWEEN 1 AND 60 AND name = btrim(name)),
94
+ ADD CONSTRAINT access_tokens_token_hash_check CHECK (token_hash ~ '^[0-9a-f]{64}$'),
95
+ ADD CONSTRAINT access_tokens_check CHECK (expires_at > created_at),
96
+ ADD CONSTRAINT access_tokens_token_hash_key UNIQUE (token_hash),
97
+ ADD CONSTRAINT access_tokens_grant_id_fkey FOREIGN KEY (grant_id) REFERENCES mcp.oauth_grants (id) ON DELETE CASCADE;
98
+ CREATE INDEX access_tokens_user_id_created_at_idx ON mcp.access_tokens (user_id, created_at DESC);
99
+ CREATE INDEX access_tokens_expires_at_idx ON mcp.access_tokens (expires_at);
100
+ CREATE INDEX access_tokens_grant_id_idx ON mcp.access_tokens (grant_id);
@@ -1,5 +1,5 @@
1
1
  import type { CoreErrorCode } from "@softure-ai/core";
2
- export type McpAccessErrorCode = "mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached" | "mcp-access.token_not_found";
2
+ export type McpAccessErrorCode = "mcp-access.name_required" | "mcp-access.name_too_long" | "mcp-access.token_limit_reached" | "mcp-access.token_not_found" | "mcp-access.grant_not_found";
3
3
  /** Every code the token page can show: its own, a missing session and the generic ones. */
4
4
  export type TokenFormErrorCode = McpAccessErrorCode | "auth.unauthenticated" | CoreErrorCode;
5
5
  /** What the app's MCP server factory receives for one request. */
@@ -68,6 +68,16 @@ export type IssueTokenFormState = {
68
68
  readonly status: "error";
69
69
  readonly error: TokenFormErrorCode;
70
70
  };
71
+ /** A connected app (an OAuth grant) as its owner sees it. */
72
+ export interface OAuthGrantView {
73
+ readonly id: string;
74
+ /** The name the client registered with, cut to 60 characters. */
75
+ readonly clientName: string;
76
+ /** Granted with write access (it still writes only while the app allows writes). */
77
+ readonly canWrite: boolean;
78
+ readonly createdAt: Date;
79
+ readonly lastUsedAt: Date | null;
80
+ }
71
81
  /** What the revoke action returns to one token's form. */
72
82
  export type RevokeTokenFormState = {
73
83
  readonly status: "idle";
@@ -77,4 +87,6 @@ export type RevokeTokenFormState = {
77
87
  readonly status: "error";
78
88
  readonly error: TokenFormErrorCode;
79
89
  };
90
+ /** What the disconnect action returns to one connected app's form. */
91
+ export type RevokeGrantFormState = RevokeTokenFormState;
80
92
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,15 @@ export declare const MCP_RATE_LIMIT_BUCKETS: {
9
9
  readonly limit: 200;
10
10
  readonly windowMinutes: 15;
11
11
  };
12
+ /**
13
+ * OAuth registration (per address) and token requests (per address and client id), when
14
+ * `oauth.enabled`. Enough for an assistant platform refreshing many users' tokens per client,
15
+ * too little for a script filling the clients table.
16
+ */
17
+ readonly "mcp-oauth": {
18
+ readonly limit: 60;
19
+ readonly windowMinutes: 15;
20
+ };
12
21
  };
13
22
  /**
14
23
  * Enables MCP access in `softure.config.ts` (after `security` and `auth`):
@@ -17,6 +26,10 @@ export declare const MCP_RATE_LIMIT_BUCKETS: {
17
26
  export declare const mcpAccess: import("@softure-ai/core").ModuleFactory<{
18
27
  page: string;
19
28
  endpoint: string;
29
+ oauthConsent: string;
30
+ oauthDecision: string;
31
+ oauthToken: string;
32
+ oauthRegister: string;
20
33
  }, {
21
34
  page: {
22
35
  title: string;
@@ -77,12 +90,40 @@ export declare const mcpAccess: import("@softure-ai/core").ModuleFactory<{
77
90
  revoke: string;
78
91
  revoking: string;
79
92
  };
93
+ grants: {
94
+ title: string;
95
+ lead: string;
96
+ empty: string;
97
+ connectedOn: string;
98
+ disconnect: string;
99
+ disconnecting: string;
100
+ };
101
+ consent: {
102
+ title: string;
103
+ lead: string;
104
+ app: string;
105
+ returnsTo: string;
106
+ account: string;
107
+ access: string;
108
+ readAccess: string;
109
+ allowWrite: string;
110
+ allowWriteHint: string;
111
+ allow: string;
112
+ deny: string;
113
+ errorTitle: string;
114
+ unknownClient: string;
115
+ redirectMismatch: string;
116
+ invalidRequest: string;
117
+ nothingGranted: string;
118
+ backToApp: string;
119
+ };
80
120
  errors: {
81
121
  "mcp-access": {
82
122
  name_required: string;
83
123
  name_too_long: string;
84
124
  token_limit_reached: string;
85
125
  token_not_found: string;
126
+ grant_not_found: string;
86
127
  };
87
128
  auth: {
88
129
  unauthenticated: string;
@@ -109,11 +150,25 @@ export declare const mcpAccess: import("@softure-ai/core").ModuleFactory<{
109
150
  tokenLifetimeDays: import("zod").ZodDefault<import("zod").ZodInt>;
110
151
  maxTokensPerUser: import("zod").ZodDefault<import("zod").ZodInt>;
111
152
  expiryWarningDays: import("zod").ZodDefault<import("zod").ZodInt>;
153
+ legacyTokenPattern: import("zod").ZodOptional<import("zod").ZodInstanceOf<RegExp>>;
154
+ resolveAppOrigin: import("zod").ZodOptional<import("zod").ZodCustom<import("./options.js").McpAppOriginResolver, import("./options.js").McpAppOriginResolver>>;
155
+ resourceOrigins: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<string, string>>>>;
156
+ oauth: import("zod").ZodPrefault<import("zod").ZodObject<{
157
+ enabled: import("zod").ZodDefault<import("zod").ZodBoolean>;
158
+ accessTokenLifetimeMinutes: import("zod").ZodDefault<import("zod").ZodInt>;
159
+ refreshTokenLifetimeDays: import("zod").ZodDefault<import("zod").ZodInt>;
160
+ authorizationCodeLifetimeMinutes: import("zod").ZodDefault<import("zod").ZodInt>;
161
+ metadata: import("zod").ZodPrefault<import("zod").ZodObject<{
162
+ authorizationServer: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodCustom<(origins: import("./origins.js").McpOrigins) => Readonly<Record<string, unknown>>, (origins: import("./origins.js").McpOrigins) => Readonly<Record<string, unknown>>>, import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>]>>;
163
+ protectedResource: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodCustom<(origins: import("./origins.js").McpOrigins) => Readonly<Record<string, unknown>>, (origins: import("./origins.js").McpOrigins) => Readonly<Record<string, unknown>>>, import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>]>>;
164
+ }, import("zod/v4/core").$strict>>;
165
+ }, import("zod/v4/core").$strict>>;
112
166
  }, import("zod/v4/core").$strict>>;
113
167
  export { getAuthorizationHeader, getClaudeCodeCommand, getClaudeCodeLink, getDesktopConfig, getJsonConfig, getMcpClientSetup, type McpClientSetupInput, } from "./client-setup.js";
114
- export type { AccessTokenStatus, AccessTokenView, IssuedToken, IssueTokenFormState, McpAccessErrorCode, McpClientSetup, McpServerIdentity, RevokeTokenFormState, TokenFormErrorCode, } from "./contract.js";
168
+ export type { AccessTokenStatus, AccessTokenView, IssuedToken, IssueTokenFormState, McpAccessErrorCode, McpClientSetup, McpServerIdentity, OAuthGrantView, RevokeGrantFormState, RevokeTokenFormState, TokenFormErrorCode, } from "./contract.js";
115
169
  export { getTokenErrorMessage, mcpAccessMessages, type McpAccessMessages } from "./messages/index.js";
116
- export { MAX_TOKEN_NAME_LENGTH, SERVER_NAME_PATTERN, TOOL_NAME_PATTERN, type McpAccessOptions, type McpAccessOptionsInput, type McpToolAccess, type McpToolDefinition, type McpToolDefinitionInput, } from "./options.js";
117
- export { accessTokens } from "./schema.js";
170
+ export { MAX_PRESENTED_TOKEN_LENGTH, MAX_TOKEN_NAME_LENGTH, SERVER_NAME_PATTERN, TOOL_NAME_PATTERN, GENERATED_AUTHORIZATION_SERVER_KEYS, GENERATED_PROTECTED_RESOURCE_KEYS, type McpAccessOptions, type McpAccessOptionsInput, type McpAppOriginResolver, type McpMetadataExtension, type McpOAuthOptions, type McpToolAccess, type McpToolDefinition, type McpToolDefinitionInput, } from "./options.js";
171
+ export { readRequestOrigin, type McpOriginRequest, type McpOrigins } from "./origins.js";
172
+ export { accessTokens, oauthAuthorizationCodes, oauthClients, oauthGrants, type OAuthClientRow, type OAuthTokenEndpointAuthMethod } from "./schema.js";
118
173
  export { getAccessTokenStatus, type AccessTokenStatusOptions } from "./token-status.js";
119
174
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,eAAe,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;CAEzB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAqBpB,CAAC;AAEH,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACtG,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,eAAe,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;IAEjC;;;;OAIG;;;;;CAEK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAmCpB,CAAC;AAEH,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACtG,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,mCAAmC,EACnC,iCAAiC,EACjC,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,4BAA4B,EAAE,MAAM,aAAa,CAAC;AACvJ,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -15,6 +15,12 @@ export const MODULE_ID = "mcp-access";
15
15
  */
16
16
  export const MCP_RATE_LIMIT_BUCKETS = {
17
17
  mcp: { limit: 200, windowMinutes: 15 },
18
+ /**
19
+ * OAuth registration (per address) and token requests (per address and client id), when
20
+ * `oauth.enabled`. Enough for an assistant platform refreshing many users' tokens per client,
21
+ * too little for a script filling the clients table.
22
+ */
23
+ "mcp-oauth": { limit: 60, windowMinutes: 15 },
18
24
  };
19
25
  /**
20
26
  * Enables MCP access in `softure.config.ts` (after `security` and `auth`):
@@ -23,16 +29,30 @@ export const MCP_RATE_LIMIT_BUCKETS = {
23
29
  export const mcpAccess = defineModule({
24
30
  manifest: {
25
31
  id: MODULE_ID,
26
- version: "0.1.6",
32
+ version: "0.1.8",
27
33
  dependsOn: { security: "^0.1.0", auth: "^0.1.0" },
28
34
  dbSchema: "mcp",
29
- tables: ["access_tokens"],
35
+ tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants"],
30
36
  env: [],
31
37
  switches: [],
32
- routes: { page: "/account/mcp", endpoint: "/api/mcp" },
38
+ routes: {
39
+ page: "/account/mcp",
40
+ endpoint: "/api/mcp",
41
+ oauthConsent: "/oauth/authorize",
42
+ oauthDecision: "/api/oauth/authorize",
43
+ oauthToken: "/api/oauth/token",
44
+ oauthRegister: "/api/oauth/register",
45
+ },
33
46
  mount: [
34
47
  { kind: "page", path: "app/account/mcp/page.tsx", export: "McpAccessPage" },
35
48
  { kind: "route-handler", path: "app/api/mcp/route.ts", export: "createMcpRoute" },
49
+ { kind: "page", path: "app/oauth/authorize/page.tsx", export: "OAuthConsentPage" },
50
+ { kind: "route-handler", path: "app/api/oauth/authorize/route.ts", export: "decideOAuthAuthorizationRoute" },
51
+ { kind: "route-handler", path: "app/api/oauth/token/route.ts", export: "exchangeOAuthTokenRoute" },
52
+ { kind: "route-handler", path: "app/api/oauth/register/route.ts", export: "registerOAuthClientRoute" },
53
+ { kind: "route-handler", path: "app/.well-known/oauth-authorization-server/route.ts", export: "getAuthorizationServerMetadataRoute" },
54
+ { kind: "route-handler", path: "app/.well-known/oauth-protected-resource/route.ts", export: "getProtectedResourceMetadataRoute" },
55
+ { kind: "route-handler", path: "app/.well-known/oauth-protected-resource/api/mcp/route.ts", export: "getProtectedResourceMetadataRoute" },
36
56
  ],
37
57
  privacy: { exports: true, deletes: true },
38
58
  },
@@ -44,7 +64,8 @@ export const mcpAccess = defineModule({
44
64
  });
45
65
  export { getAuthorizationHeader, getClaudeCodeCommand, getClaudeCodeLink, getDesktopConfig, getJsonConfig, getMcpClientSetup, } from "./client-setup.js";
46
66
  export { getTokenErrorMessage, mcpAccessMessages } from "./messages/index.js";
47
- export { MAX_TOKEN_NAME_LENGTH, SERVER_NAME_PATTERN, TOOL_NAME_PATTERN, } from "./options.js";
48
- export { accessTokens } from "./schema.js";
67
+ export { MAX_PRESENTED_TOKEN_LENGTH, MAX_TOKEN_NAME_LENGTH, SERVER_NAME_PATTERN, TOOL_NAME_PATTERN, GENERATED_AUTHORIZATION_SERVER_KEYS, GENERATED_PROTECTED_RESOURCE_KEYS, } from "./options.js";
68
+ export { readRequestOrigin } from "./origins.js";
69
+ export { accessTokens, oauthAuthorizationCodes, oauthClients, oauthGrants } from "./schema.js";
49
70
  export { getAccessTokenStatus } from "./token-status.js";
50
71
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,kGAAkG;AAClG,kGAAkG;AAClG,mDAAmD;AACnD,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,EAAE;CAC9B,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC;IACpC,QAAQ,EAAE;QACR,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE;QACjD,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,CAAC,eAAe,CAAC;QACzB,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;QACtD,KAAK,EAAE;YACL,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,eAAe,EAAE;YAC3E,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,gBAAgB,EAAE;SAClF;QACD,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;KAC1C;IACD,QAAQ,EAAE,iBAAiB;IAC3B,OAAO,EAAE,sBAAsB;IAC/B,UAAU,EAAE,EAAE,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE;IAC5E,OAAO,EAAE,2BAA2B;IACpC,MAAM,EAAE,sBAAsB;CAC/B,CAAC,CAAC;AAEH,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAElB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAA0B,MAAM,qBAAqB,CAAC;AACtG,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,GAMlB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAiC,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}