@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.
- package/CHANGELOG.md +30 -0
- package/README.md +146 -21
- package/adoption/move-app-tables.sql +100 -0
- package/dist/contract.d.ts +13 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +58 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -5
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +28 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +28 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +56 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +28 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/actions.d.ts +3 -1
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +23 -2
- package/dist/next/actions.js.map +1 -1
- package/dist/next/context.d.ts +7 -1
- package/dist/next/context.d.ts.map +1 -1
- package/dist/next/context.js +12 -10
- package/dist/next/context.js.map +1 -1
- package/dist/next/index.d.ts +4 -1
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +5 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/oauth-page.d.ts +7 -0
- package/dist/next/oauth-page.d.ts.map +1 -0
- package/dist/next/oauth-page.js +36 -0
- package/dist/next/oauth-page.js.map +1 -0
- package/dist/next/oauth-routes.d.ts +19 -0
- package/dist/next/oauth-routes.d.ts.map +1 -0
- package/dist/next/oauth-routes.js +53 -0
- package/dist/next/oauth-routes.js.map +1 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +13 -2
- package/dist/next/pages.js.map +1 -1
- package/dist/options.d.ts +40 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +94 -1
- package/dist/options.js.map +1 -1
- package/dist/origins.d.ts +27 -0
- package/dist/origins.d.ts.map +1 -0
- package/dist/origins.js +32 -0
- package/dist/origins.js.map +1 -0
- package/dist/schema.d.ts +486 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +45 -3
- package/dist/schema.js.map +1 -1
- package/dist/server/context.d.ts +4 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +13 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/endpoint.d.ts +2 -4
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +14 -9
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +5 -3
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.d.ts +127 -0
- package/dist/server/oauth-http.d.ts.map +1 -0
- package/dist/server/oauth-http.js +397 -0
- package/dist/server/oauth-http.js.map +1 -0
- package/dist/server/oauth-validation.d.ts +28 -0
- package/dist/server/oauth-validation.d.ts.map +1 -0
- package/dist/server/oauth-validation.js +88 -0
- package/dist/server/oauth-validation.js.map +1 -0
- package/dist/server/oauth.d.ts +106 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +235 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/server/options.d.ts +11 -2
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +11 -4
- package/dist/server/options.js.map +1 -1
- package/dist/server/origins.d.ts +8 -0
- package/dist/server/origins.d.ts.map +1 -0
- package/dist/server/origins.js +15 -0
- package/dist/server/origins.js.map +1 -0
- package/dist/server/pkce.d.ts +7 -0
- package/dist/server/pkce.d.ts.map +1 -0
- package/dist/server/pkce.js +24 -0
- package/dist/server/pkce.js.map +1 -0
- package/dist/server/privacy.d.ts +7 -0
- package/dist/server/privacy.d.ts.map +1 -1
- package/dist/server/privacy.js +19 -9
- package/dist/server/privacy.js.map +1 -1
- package/dist/server/scopes.d.ts +5 -0
- package/dist/server/scopes.d.ts.map +1 -0
- package/dist/server/scopes.js +6 -0
- package/dist/server/scopes.js.map +1 -0
- package/dist/server/tokens.d.ts +18 -6
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js +60 -25
- package/dist/server/tokens.js.map +1 -1
- package/dist/ui/consent-form.d.ts +31 -0
- package/dist/ui/consent-form.d.ts.map +1 -0
- package/dist/ui/consent-form.js +24 -0
- package/dist/ui/consent-form.js.map +1 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/token-manager.d.ts +16 -2
- package/dist/ui/token-manager.d.ts.map +1 -1
- package/dist/ui/token-manager.js +6 -2
- package/dist/ui/token-manager.js.map +1 -1
- package/migrations/0002_create_oauth_grants.sql +68 -0
- package/module.json +18 -4
- package/package.json +2 -1
- package/src/contract.ts +16 -1
- package/src/index.ts +33 -4
- package/src/messages/en.ts +28 -0
- package/src/messages/pl.ts +28 -0
- package/src/next/actions.ts +23 -3
- package/src/next/context.ts +15 -8
- package/src/next/index.ts +12 -2
- package/src/next/oauth-page.tsx +68 -0
- package/src/next/oauth-routes.ts +73 -0
- package/src/next/pages.tsx +16 -3
- package/src/options.ts +107 -1
- package/src/origins.ts +48 -0
- package/src/schema.ts +51 -3
- package/src/server/context.ts +14 -0
- package/src/server/endpoint.ts +14 -9
- package/src/server/health.ts +5 -3
- package/src/server/index.ts +56 -0
- package/src/server/oauth-http.ts +480 -0
- package/src/server/oauth-validation.ts +106 -0
- package/src/server/oauth.ts +315 -0
- package/src/server/options.ts +20 -4
- package/src/server/origins.ts +19 -0
- package/src/server/pkce.ts +27 -0
- package/src/server/privacy.ts +26 -9
- package/src/server/scopes.ts +6 -0
- package/src/server/tokens.ts +62 -25
- package/src/ui/consent-form.tsx +103 -0
- package/src/ui/index.ts +3 -0
- 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,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
| `
|
|
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
|
-
`
|
|
74
|
-
|
|
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`,
|
|
151
|
-
|
|
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
|
|
168
|
-
created, expiry and last-use dates
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
-
|
|
175
|
-
|
|
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);
|
package/dist/contract.d.ts
CHANGED
|
@@ -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
|
package/dist/contract.d.ts.map
CHANGED
|
@@ -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 {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;;
|
|
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.
|
|
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: {
|
|
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 {
|
|
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;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|