@12-apps/mcp 2.0.0 → 3.1.0

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/ADOPTING.md CHANGED
@@ -34,7 +34,7 @@ library updates, every host updates with **no app changes**. Same contract
34
34
  `enabled: false` authorize/token/jwks/discovery answer **404** — a probe cannot
35
35
  tell a disabled AS from an app that has none — while registration answers
36
36
  **403 `access_denied`**, because RFC 7591 has a code for "the endpoint is here,
37
- registration is closed". future-pay passes
37
+ registration is closed". The origin host passes
38
38
  `enabled: () => process.env.MCP_BEARER_ENABLED === 'true'`, so the surface stays
39
39
  OFF until an operator opts in.
40
40
  4. **No signing key, no tokens.** `signingKey` defaults to the env-backed provider
@@ -51,7 +51,7 @@ library updates, every host updates with **no app changes**. Same contract
51
51
  the internal origin rather than to an attacker's. Issuance and verification read
52
52
  the same resolver from the same request, which is what stops "minted for A,
53
53
  verified against B" from rejecting valid tokens.
54
- `trustedOriginsFromEnv('MCP_OAUTH_TRUSTED_ORIGINS')` keeps future-pay's wiring.
54
+ `trustedOriginsFromEnv('MCP_OAUTH_TRUSTED_ORIGINS')` keeps the origin host's wiring.
55
55
  6. **The stores are narrow ports; Prisma fills them in one line.**
56
56
  `createPrismaMcpStores(async () => prisma as unknown as McpOauthPrisma)`. A
57
57
  non-Prisma host implements `OAuthClientStore` / `RefreshTokenStore` /
@@ -104,16 +104,19 @@ library updates, every host updates with **no app changes**. Same contract
104
104
  default, and it is deliberately the inconvenient one.
105
105
  10. **Connections are per USER, not per tenant** — an MCP bearer is
106
106
  auth-passthrough. `connections.resolveUserId(email)` maps the token's email to
107
- the host's user id (future-pay resolves it by email because `session.user.id` is
107
+ the host's user id (the origin host resolves it by email because `session.user.id` is
108
108
  the OAuth `sub`); returning `null` records nothing. Recording is best-effort and
109
109
  FENCED: a failing directory can never turn a valid grant into a 500, and nothing
110
110
  about the attempt is logged, because the only values in hand are an email and a
111
111
  client id.
112
- 11. **Disconnecting means BOTH halves.** `connections.revokeByHost(...)` returns the
113
- OAuth client ids it revoked, and the caller must then
114
- `refreshTokens.revokeLiveForClient(email, clientId)` for each a host holding a
115
- live refresh token simply rotates its way back in and the card lights green on
116
- the next grant. Neither half invalidates an outstanding ACCESS token: those are
112
+ 11. **Disconnecting means BOTH halves — call `disconnectAiHost`, not the stores.**
113
+ `connections.revokeByHost(...)` ends the rows and returns the OAuth client ids
114
+ behind them, and every live refresh token of those clients must be ended in the
115
+ same act — a host holding a live refresh token simply rotates its way back in
116
+ and the card lights green on the next grant. Since 12-48 the rule IS a
117
+ function: `disconnectAiHost(stores, { userId, email }, host)` does both and
118
+ reports what it ended, so a host cannot import one half without the other.
119
+ Neither half invalidates an outstanding ACCESS token: those are
117
120
  self-contained JWTs, so a disconnected host keeps working for at most their
118
121
  15-minute TTL and can then obtain nothing further.
119
122
  12. **These bodies are NOT the `{ data }` envelope.** A 302 with a `Location`, RFC
@@ -211,7 +214,7 @@ Two things about the gates worth knowing before you adopt them:
211
214
  committed exclusions file the moment it adopted the package, so it needs its own
212
215
  burn-down).
213
216
 
214
- ## Phase B — adopting into a host that ALREADY has these tables (future-pay)
217
+ ## Phase B — adopting into a host that ALREADY has these tables (the origin host)
215
218
 
216
219
  **Nothing to baseline.** Every statement in the package migration is guarded
217
220
  (`CREATE TABLE IF NOT EXISTS`, `CREATE [UNIQUE] INDEX IF NOT EXISTS`, `ADD COLUMN
@@ -223,8 +226,8 @@ a host that already has `oauth_clients` / `oauth_refresh_tokens` /
223
226
  Deliberate deltas to reconcile:
224
227
 
225
228
  - **The FK from `mcp_connections.user_id` to `users` is not in the package
226
- migration** — host vocabulary. future-pay keeps its `ON DELETE CASCADE`.
227
- - **`onboarding_states` is not here.** future-pay's migration created it beside
229
+ migration** — host vocabulary. The origin host keeps its `ON DELETE CASCADE`.
230
+ - **`onboarding_states` is not here.** The origin host's migration created it beside
228
231
  `mcp_connections`; it belongs to `@12-apps/onboarding` (12-23).
229
232
  - The host's `lib/mcp/oauth/**` (~1.5k LOC) and its four route files are replaced
230
233
  by the mount plus, where a coverage gate forces the file to exist, a one-line
@@ -234,15 +237,23 @@ Deliberate deltas to reconcile:
234
237
 
235
238
  ## What deliberately did NOT move into the package
236
239
 
237
- - **The account/connection SCREENS' endpoints** (`GET/DELETE
238
- /api/account/mcp-connections`) — they mix session resolution, published plugin
239
- URLs and a logger. The stores they need (`listActive`, `revokeByHost`,
240
- `revokeLiveForClient`) are all here; the route is a follow-up.
240
+ - **The account/connection SCREENS' route files** (`GET/DELETE
241
+ /api/account/mcp-connections`) — they answer in the HOST's app-wide response
242
+ envelope and mix its session resolution, published plugin URLs and logger, so
243
+ the handler stays host code (unlike the OAuth endpoints, whose shapes are
244
+ fixed by RFC — rule 12 — these are ordinary host API routes). What DID move
245
+ (12-48) is the operations under them: `listAiConnections` (the stored open
246
+ `host` string narrowed to the package's own `AiProvider` union) and
247
+ `disconnectAiHost`, which owns the disconnect's both-halves rule — revoke the
248
+ connection rows AND end every live refresh token of each returned client id in
249
+ one call. A host that imports the disconnect cannot get only half of it; half
250
+ is the failure mode where the assistant rotates its live token and the card
251
+ the user just disconnected lights green again on the next grant (rule 11).
241
252
  - **The MCP registry itself** — which endpoints become tools, their annotations
242
253
  and redactions, is the host's catalogue. The package generates, dispatches and
243
254
  gates it.
244
255
  - **`mcp:lint`, `mcp:parity`, `mcp:smoke`, `mcp:test-coverage`** — the remaining
245
- future-pay MCP scripts. Only the two the reusable CI workflows shell out to moved
256
+ the origin host MCP scripts. Only the two the reusable CI workflows shell out to moved
246
257
  (12-23's scope).
247
258
  - **Authorization codes as rows.** They are stateless signed blobs, so there is no
248
259
  table and nothing to sweep — only the replay store (rule 8).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/mcp",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic MCP server core: generate one MCP tool per OpenAPI operation and proxy each call carrying the caller's bearer token (permission passthrough). Also ships the OAuth 2.1 authorization server (./oauth, ./hono: register/authorize/token, JWKS and both .well-known documents), the package-owned Prisma partial + migration for its three tables, the mcp:generate/mcp:check (./generate) and mcp:coverage (./coverage) gates, and the reusable AI-connect onboarding UI (./react).",
6
6
  "exports": {
@@ -23,9 +23,9 @@
23
23
  "prisma:sync:check": "node scripts/sync-mcp-schema.mjs --check"
24
24
  },
25
25
  "dependencies": {
26
- "@12-apps/onboarding": "^1.20.0",
27
- "@12-apps/rbac": "^3.0.0",
28
- "@12-apps/ui": "^4.1.0",
26
+ "@12-apps/onboarding": "^2.0.0",
27
+ "@12-apps/rbac": "^4.0.1",
28
+ "@12-apps/ui": "^5.0.0",
29
29
  "@mui/icons-material": "^6.5.0",
30
30
  "jose": "^6.1.3",
31
31
  "react": "^19.2.0"
package/prisma/mcp.prisma CHANGED
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // Host-agnostic by design (the entity-lifecycle / rbac doctrine): `user_id` is a
13
13
  // by-value scalar with NO relation, because this package cannot know the name of
14
- // the host's user model. The host's own migration may add the FK (future-pay's
14
+ // the host's user model. The host's own migration may add the FK (the origin host's
15
15
  // is ON DELETE CASCADE). Note there is deliberately no `oauth_codes` table:
16
16
  // authorization codes are STATELESS signed blobs, so there is nothing to store
17
17
  // and nothing to sweep.
@@ -18,13 +18,13 @@
18
18
  -- Authorization codes are deliberately absent: they are STATELESS signed blobs,
19
19
  -- so there is no table to create and nothing to sweep.
20
20
  --
21
- -- The columns, defaults, indexes and CHECK are future-pay's
21
+ -- The columns, defaults, indexes and CHECK are the origin host's
22
22
  -- `20260713120000_add_oauth_client_refresh`,
23
23
  -- `20260715180000_add_onboarding_state_mcp_connection` (the mcp_connections half
24
24
  -- — the onboarding half belongs to @12-apps/onboarding) and
25
25
  -- `20260720120000_add_mcp_connection_host` verbatim, minus the FK to `users`:
26
26
  -- this package cannot know the name of a host's user table, and a host that has
27
- -- one keeps its own constraint (future-pay's is ON DELETE CASCADE).
27
+ -- one keeps its own constraint (the origin host's is ON DELETE CASCADE).
28
28
  --
29
29
  -- EVERY statement is guarded (`IF NOT EXISTS`, and a conrelid-scoped DO block for
30
30
  -- the CHECK, which has no IF NOT EXISTS form). That is what makes adoption by a
@@ -35,7 +35,7 @@
35
35
  -- the difference bites exactly the host this file is written for: `CREATE TABLE IF
36
36
  -- NOT EXISTS` skips the whole table, columns included, so a host holding an OLDER
37
37
  -- shape of one of these tables silently keeps it. Each table below is therefore
38
- -- followed by a guarded `ADD COLUMN` for every column that reached future-pay in a
38
+ -- followed by a guarded `ADD COLUMN` for every column that reached the origin host in a
39
39
  -- LATER migration than its own CREATE. The full audit: `oauth_refresh_tokens
40
40
  -- .user_sub` (`20260713150000_add_oauth_refresh_user_sub`) and `mcp_connections
41
41
  -- .host` (`20260720120000_add_mcp_connection_host`). `oauth_clients` needs none —
@@ -112,10 +112,10 @@ CREATE INDEX IF NOT EXISTS "oauth_refresh_tokens_user_email_client_id_idx"
112
112
  -- `CREATE TABLE IF NOT EXISTS` skips the WHOLE table, so a host that already holds
113
113
  -- `oauth_refresh_tokens` in an OLDER SHAPE gets none of the columns declared above
114
114
  -- — statement-level guarding is not the same as column-level guarding. That is
115
- -- precisely how future-pay's own history ran: `user_sub` arrived in a SECOND
115
+ -- precisely how the origin host's own history ran: `user_sub` arrived in a SECOND
116
116
  -- migration (FUT-105, `20260713150000_add_oauth_refresh_user_sub`), so a host
117
117
  -- frozen before it would adopt this file, skip the CREATE, never get the column,
118
- -- and then fail on every refresh the package serves. Mirror future-pay's pair
118
+ -- and then fail on every refresh the package serves. Mirror the origin host's pair
119
119
  -- verbatim — guarded add with a backfill default to satisfy NOT NULL, then drop
120
120
  -- the default so the column matches the Prisma schema (`String`, no default).
121
121
  -- Both statements are no-ops on a fresh host and on a replay.
@@ -147,6 +147,6 @@ CREATE INDEX IF NOT EXISTS "mcp_connections_last_active_at_idx"
147
147
  ON "mcp_connections"("last_active_at");
148
148
 
149
149
  -- A host adopting this migration where `mcp_connections` predates the `host`
150
- -- column (future-pay added it in a later migration) gets it here; a fresh host
150
+ -- column (the origin host added it in a later migration) gets it here; a fresh host
151
151
  -- already has it from the CREATE above, so the guard makes both cases a no-op.
152
152
  ALTER TABLE "mcp_connections" ADD COLUMN IF NOT EXISTS "host" TEXT;
@@ -14,14 +14,14 @@
14
14
  *
15
15
  * Only the schema partial. MIGRATIONS ARE NOT HANDLED HERE — the host
16
16
  * discovers and copies them structurally, by looking for a `prisma/migrations`
17
- * directory inside every installed `@12-apps/*` package (see future-pay's
17
+ * directory inside every installed `@12-apps/*` package (see the origin host's
18
18
  * packages/prisma/scripts/sync-prisma-plugins.mjs).
19
19
  *
20
20
  * The host package that owns the schema folder MUST also declare this package
21
21
  * as a dependency, so the source of the copy is present in every build
22
22
  * context.
23
23
  *
24
- * Default host path follows the future-pay layout
24
+ * Default host path follows the origin host layout
25
25
  * (`packages/prisma/prisma/schema/`); another repo passes its own schema
26
26
  * folder as the positional argument, or sets MCP_HOST_SCHEMA_DIR.
27
27
  */
@@ -6,7 +6,7 @@ import { collectRouteMethods } from "./route-methods";
6
6
 
7
7
  /**
8
8
  * `@12-apps/mcp/coverage` — the MCP route/action coverage gate (12-23), moved out
9
- * of future-pay's `apps/web/scripts/mcp/coverage.ts` so a host's own script is a
9
+ * of the origin host's `apps/web/scripts/mcp/coverage.ts` so a host's own script is a
10
10
  * one-line re-export and the CI workflow that shells out to the consumer's
11
11
  * `mcp:coverage` package script (`12-apps/ci`'s `mcp-contract.yml`) keeps working
12
12
  * unchanged.
@@ -11,7 +11,7 @@ import { exportedNamesOf, urlPathOf, walkRouteFiles } from "@12-apps/rbac/covera
11
11
  * walk (`walkRouteFiles`), the URL mapping (`urlPathOf`) AND the export-head
12
12
  * parser (`exportedNamesOf`) — and that is deliberate: both gates assert a
13
13
  * COMPLETENESS property over the same two surfaces (`app/**` route files and
14
- * `*actions.ts` modules), and future-pay's own comment on the shared scanner says
14
+ * `*actions.ts` modules), and the origin host's own comment on the shared scanner says
15
15
  * why they must share it — "so the two gates can never disagree about what the
16
16
  * surface is". Two copies would agree on the day they were written and drift
17
17
  * silently after, in the direction of not looking. What is left here is the one
@@ -13,7 +13,7 @@ import type { ToolManifest } from "../types";
13
13
 
14
14
  /**
15
15
  * `@12-apps/mcp/generate` — the `mcp:generate` / `mcp:check` gate (12-23), moved
16
- * out of future-pay's `apps/web/scripts/mcp/generate.ts` so a host's own script is
16
+ * out of the origin host's `apps/web/scripts/mcp/generate.ts` so a host's own script is
17
17
  * a one-line call and `12-apps/ci`'s `mcp-contract.yml`, which shells out to the
18
18
  * consumer's `mcp:check` package script, keeps working unchanged.
19
19
  *
@@ -4,7 +4,7 @@ import { issuer, resourceAudience, DEFAULT_MCP_RESOURCE_PATH, type McpScope } fr
4
4
  import { SIGNING_ALG, type McpSigningKeyProvider } from "./keys";
5
5
 
6
6
  /**
7
- * JWT access-token issuer + verifier (12-23, ported from future-pay's
7
+ * JWT access-token issuer + verifier (12-23, ported from the origin host's
8
8
  * `lib/mcp/oauth/jwt.ts` — behaviour unchanged; the signing key arrives through a
9
9
  * provider and the resource path is config).
10
10
  *
@@ -4,7 +4,7 @@ import { issuer } from "./config";
4
4
  import { SIGNING_ALG, type McpSigningKeyProvider } from "./keys";
5
5
 
6
6
  /**
7
- * Stateless authorization-code mint/verify (12-23, ported from future-pay's
7
+ * Stateless authorization-code mint/verify (12-23, ported from the origin host's
8
8
  * `lib/mcp/oauth/authorization-code.ts` — behaviour unchanged; the signing key
9
9
  * arrives through a provider instead of an env read).
10
10
  *
@@ -6,7 +6,7 @@ import type { StoredOAuthClient } from "./stores";
6
6
 
7
7
  /**
8
8
  * The OAuth 2.1 Authorization Code + PKCE authorization endpoint (12-23, ported
9
- * from future-pay's `app/api/oauth/authorize/route.ts`).
9
+ * from the origin host's `app/api/oauth/authorize/route.ts`).
10
10
  *
11
11
  * It renders no UI: it authenticates the caller against the host's cookie session
12
12
  * (through `resolveSession`), validates the request, and either 302-redirects an
@@ -8,7 +8,7 @@ import type {
8
8
 
9
9
  /**
10
10
  * Client registration and the open-redirect guard (12-23, ported from
11
- * future-pay's `lib/mcp/oauth/clients.ts`).
11
+ * the origin host's `lib/mcp/oauth/clients.ts`).
12
12
  *
13
13
  * A registered client is an external host (a Claude.ai / ChatGPT connector) from
14
14
  * RFC 7591 dynamic client registration, or a static registration an operator
@@ -122,7 +122,7 @@ export interface ProviderAttributionRule {
122
122
  provider: string;
123
123
  }
124
124
 
125
- /** future-pay's rules, and a sane default for any host talking to the same two. */
125
+ /** The origin host's rules, and a sane default for any host talking to the same two. */
126
126
  export const DEFAULT_PROVIDER_ROOTS: readonly ProviderAttributionRule[] = [
127
127
  { roots: ["claude.ai", "anthropic.com"], provider: "claude" },
128
128
  { roots: ["chatgpt.com", "openai.com"], provider: "chatgpt" },
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Single-use guard for the stateless authorization codes (12-23, ported from
3
- * future-pay's `lib/mcp/oauth/token-replay.ts`).
3
+ * the origin host's `lib/mcp/oauth/token-replay.ts`).
4
4
  *
5
5
  * A code is a signed blob with a `jti`, so "already redeemed" has to be remembered
6
6
  * somewhere. The in-process option remembers it IN THIS PROCESS: a small map of
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The OAuth 2.1 authorization-server foundation: the shared scope source, the
3
3
  * issuer/audience derivation, and the trusted-origin resolver every URL in the
4
- * surface is built from (12-23, ported from future-pay's
4
+ * surface is built from (12-23, ported from the origin host's
5
5
  * `lib/mcp/oauth/config.ts`).
6
6
  *
7
7
  * Keeping the scopes and the origin resolution in ONE place is what stops the
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * What was env-reading in the host is CONFIG here (the package must not learn a
13
13
  * host's variable names); `trustedOriginsFromEnv` is the one-line helper that
14
- * keeps future-pay's wiring identical.
14
+ * keeps the origin host's wiring identical.
15
15
  */
16
16
 
17
17
  /** Scopes advertised by both discovery documents. `mcp:write` gates mutating tools. */
@@ -66,7 +66,7 @@ function normalizeOrigins(origins: readonly string[]): string[] {
66
66
  }
67
67
 
68
68
  /**
69
- * Read a comma-separated allowlist out of an environment variable — future-pay
69
+ * Read a comma-separated allowlist out of an environment variable — the origin host
70
70
  * passes `trustedOriginsFromEnv('MCP_OAUTH_TRUSTED_ORIGINS')`, so the behaviour
71
71
  * is identical while the variable's NAME stays the host's.
72
72
  */
@@ -0,0 +1,104 @@
1
+ import { providerForHostId, type AiProvider } from "../guide";
2
+ import type {
3
+ McpConnectionStore,
4
+ RefreshTokenStore,
5
+ StoredMcpConnection,
6
+ } from "./stores";
7
+
8
+ /**
9
+ * The account surface's connection OPERATIONS (12-48) — the half of the
10
+ * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather
11
+ * than host vocabulary.
12
+ *
13
+ * The ROUTE stays in the host on purpose: it mixes the host's session
14
+ * resolution, its response envelope, its published plugin URLs and its logger,
15
+ * and injecting all four here would make the config surface bigger than the
16
+ * handler it replaces. What must NOT stay in each host is the disconnect's
17
+ * both-halves rule, because getting it half right LOOKS right:
18
+ *
19
+ * `connections.revokeByHost` ends the connection rows and returns the OAuth
20
+ * client ids behind them — and a host that stops there has revoked nothing that
21
+ * matters. The assistant still holds a live refresh token for each of those
22
+ * clients, rotates it on schedule, and the very next grant records fresh
23
+ * activity: the card the user just disconnected lights green again on its own.
24
+ * So the rule is one function: revoke the rows AND end every live refresh token
25
+ * of each returned client, in the same call, with no way to import one half
26
+ * without the other.
27
+ *
28
+ * Deliberately NOT invalidated here: the assistant's current ACCESS token.
29
+ * Those are self-contained JWTs the server does not track; a just-disconnected
30
+ * host keeps working for at most their TTL (15 minutes by default) and can then
31
+ * obtain nothing further.
32
+ */
33
+
34
+ /** An active AI connection, narrowed for display. */
35
+ export interface AiConnectionSnapshot {
36
+ oauthClientId: string;
37
+ clientName: string | null;
38
+ /** The provider this connection is attributed to (`null` = pre-attribution). */
39
+ host: AiProvider | null;
40
+ connectedAt: Date;
41
+ lastActiveAt: Date;
42
+ }
43
+
44
+ /** The caller the operations act for — always the session's own user. */
45
+ export interface AiConnectionCaller {
46
+ /** The host's user id — what `mcp_connections` rows are keyed by. */
47
+ userId: string;
48
+ /** The identity refresh tokens are bound to (the AS binds by email). */
49
+ email: string;
50
+ }
51
+
52
+ /** What one disconnect actually ended, for the host's log and response. */
53
+ export interface AiDisconnectResult {
54
+ /** OAuth client ids whose connection rows were revoked. */
55
+ disconnectedClientIds: string[];
56
+ /** Live refresh tokens ended across those clients — the half that cuts access. */
57
+ revokedRefreshTokens: number;
58
+ }
59
+
60
+ /** Narrow a stored `host` string to a known provider, or `null`. */
61
+ function asProvider(host: string | null): AiProvider | null {
62
+ return host === null ? null : providerForHostId(host);
63
+ }
64
+
65
+ /**
66
+ * A user's active connections, most-recently-active first, with the stored open
67
+ * `host` string narrowed to the package's closed {@link AiProvider} union — the
68
+ * store cannot know which assistants have screens, but the union is this
69
+ * package's own vocabulary (`guide.ts`), so the narrowing lives beside it
70
+ * rather than being re-derived in every host.
71
+ */
72
+ export async function listAiConnections(
73
+ connections: McpConnectionStore,
74
+ userId: string,
75
+ ): Promise<AiConnectionSnapshot[]> {
76
+ const rows: StoredMcpConnection[] = await connections.listActive(userId);
77
+ return rows.map((row) => ({ ...row, host: asProvider(row.host) }));
78
+ }
79
+
80
+ /**
81
+ * Disconnect one provider for this user — BOTH halves, atomically from the
82
+ * caller's point of view (see the module doc for why one half alone is a
83
+ * disconnect that undoes itself).
84
+ *
85
+ * Idempotent: disconnecting a provider that was never connected returns zero
86
+ * counts rather than failing, so a double-click is harmless. Repeat calls also
87
+ * report zero — `revokeLiveForClient` skips already-revoked tokens by contract.
88
+ */
89
+ export async function disconnectAiHost(
90
+ stores: { connections: McpConnectionStore; refreshTokens: RefreshTokenStore },
91
+ caller: AiConnectionCaller,
92
+ host: AiProvider,
93
+ ): Promise<AiDisconnectResult> {
94
+ const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);
95
+ const revoked = await Promise.all(
96
+ disconnectedClientIds.map((clientId) =>
97
+ stores.refreshTokens.revokeLiveForClient(caller.email, clientId),
98
+ ),
99
+ );
100
+ return {
101
+ disconnectedClientIds,
102
+ revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0),
103
+ };
104
+ }
@@ -25,7 +25,7 @@ import type { McpOauthStores, StoredOAuthClient } from "./stores";
25
25
  /** The identity an authorize request binds a code to. From the SESSION only. */
26
26
  export interface McpOauthSession {
27
27
  /**
28
- * The OAuth subject (future-pay passes the Google `sub`, falling back to the
28
+ * The OAuth subject (the origin host passes the Google `sub`, falling back to the
29
29
  * email). Carried through every rotation so a refreshed token keeps the same
30
30
  * stable `sub`.
31
31
  */
@@ -45,7 +45,7 @@ export interface McpOauthPaths {
45
45
  }
46
46
 
47
47
  export const DEFAULT_OAUTH_PATHS: McpOauthPaths = {
48
- // future-pay's paths, and the ones the RFC 8414 document has always advertised.
48
+ // the origin host's paths, and the ones the RFC 8414 document has always advertised.
49
49
  authorize: "/api/oauth/authorize",
50
50
  token: "/api/oauth/token",
51
51
  register: "/api/oauth/register",
@@ -78,12 +78,12 @@ export interface McpOauthConfig {
78
78
  resolveSession: (request: Request) => Promise<McpOauthSession | null> | McpOauthSession | null;
79
79
  /**
80
80
  * The operator gate. `false` makes the whole surface inert — authorize/token/jwks
81
- * answer 404 and registration answers 403 — which is how future-pay ships it OFF
81
+ * answer 404 and registration answers 403 — which is how the origin host ships it OFF
82
82
  * by default (`MCP_BEARER_ENABLED`). Default: enabled (mounting is the opt-in).
83
83
  */
84
84
  enabled?: boolean | (() => boolean);
85
85
  /**
86
- * Signing material. Default: the env-backed provider with future-pay's variable
86
+ * Signing material. Default: the env-backed provider with the origin host's variable
87
87
  * names. `null` from the provider means "not provisioned": nothing is minted and
88
88
  * the JWKS answers 503 rather than falling back to a weaker mode.
89
89
  */
@@ -216,7 +216,7 @@ export function resolveMcpOauthConfig(config: McpOauthConfig): McpOauthContext {
216
216
  stores: config.stores,
217
217
  resolveSession: config.resolveSession,
218
218
  // Mounting is the opt-in, so the gate defaults to ON; a host that ships the
219
- // surface dark passes its own flag (future-pay: `MCP_BEARER_ENABLED`).
219
+ // surface dark passes its own flag (the origin host: `MCP_BEARER_ENABLED`).
220
220
  enabled: typeof enabled === "function" ? enabled : () => enabled,
221
221
  // `null` from the provider means "not provisioned": nothing is minted and the
222
222
  // JWKS answers 503 rather than falling back to a weaker mode.
@@ -115,3 +115,10 @@ export type {
115
115
  StoredRefreshToken,
116
116
  TokenEndpointAuthMethod,
117
117
  } from "./stores";
118
+ export {
119
+ disconnectAiHost,
120
+ listAiConnections,
121
+ type AiConnectionCaller,
122
+ type AiConnectionSnapshot,
123
+ type AiDisconnectResult,
124
+ } from "./connections";
package/src/oauth/keys.ts CHANGED
@@ -2,7 +2,7 @@ import { exportJWK, importPKCS8, type CryptoKey, type JWK } from "jose";
2
2
 
3
3
  /**
4
4
  * Signing-key / JWK loading for the OAuth authorization server (12-23, ported
5
- * from future-pay's `lib/mcp/oauth/keys.ts`).
5
+ * from the origin host's `lib/mcp/oauth/keys.ts`).
6
6
  *
7
7
  * ES256 (P-256) from PEM material, the published public JWK (with `kid` for
8
8
  * rotation), and a safe-by-default absence signal (`null`) when no key is
@@ -10,7 +10,7 @@ import { exportJWK, importPKCS8, type CryptoKey, type JWK } from "jose";
10
10
  * rather than falling back to a weaker mode while the surface is mounted.
11
11
  *
12
12
  * WHERE the PEM comes from is the host's business: `loadSigningKeyFromEnv` keeps
13
- * future-pay's env-var wiring, and any other provider (a secrets manager, a KMS
13
+ * the origin host's env-var wiring, and any other provider (a secrets manager, a KMS
14
14
  * export) satisfies the same `McpSigningKeyProvider` shape.
15
15
  */
16
16
 
@@ -87,13 +87,13 @@ export function signingKeyProvider(
87
87
  };
88
88
  }
89
89
 
90
- /** Env var carrying the ES256 private key as a PKCS#8 PEM (future-pay's name). */
90
+ /** Env var carrying the ES256 private key as a PKCS#8 PEM (the origin host's name). */
91
91
  export const DEFAULT_SIGNING_KEY_ENV = "MCP_OAUTH_SIGNING_KEY";
92
92
  /** Env var carrying the key id (`kid`) used to select the key during rotation. */
93
93
  export const DEFAULT_SIGNING_KEY_ID_ENV = "MCP_OAUTH_SIGNING_KEY_ID";
94
94
 
95
95
  /**
96
- * The env-backed provider — future-pay's wiring, kept identical, with the
96
+ * The env-backed provider — the origin host's wiring, kept identical, with the
97
97
  * variable names as arguments so the package states no host's vocabulary.
98
98
  */
99
99
  export function loadSigningKeyFromEnv(
package/src/oauth/pkce.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * PKCE (RFC 7636) S256 challenge helpers for the OAuth authorization server
3
- * (12-23, ported verbatim from future-pay's `lib/mcp/oauth/pkce.ts`).
3
+ * (12-23, ported verbatim from the origin host's `lib/mcp/oauth/pkce.ts`).
4
4
  *
5
5
  * OAuth 2.1 mandates the `S256` code-challenge method and forbids `plain`, so
6
6
  * this module computes `BASE64URL(SHA-256(code_verifier))` and compares it to
@@ -3,7 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
3
3
  import type { NewRefreshToken, RefreshTokenStore, StoredRefreshToken } from "./stores";
4
4
 
5
5
  /**
6
- * Refresh-token issue + rotation (12-23, ported from future-pay's
6
+ * Refresh-token issue + rotation (12-23, ported from the origin host's
7
7
  * `lib/mcp/oauth/refresh.ts` — behaviour unchanged; Prisma calls became the
8
8
  * `RefreshTokenStore` port).
9
9
  *
@@ -3,7 +3,7 @@ import type { McpOauthContext } from "./context";
3
3
  import type { TokenEndpointAuthMethod } from "./stores";
4
4
 
5
5
  /**
6
- * RFC 7591 Dynamic Client Registration (12-23, ported from future-pay's
6
+ * RFC 7591 Dynamic Client Registration (12-23, ported from the origin host's
7
7
  * `app/api/oauth/register/route.ts`).
8
8
  *
9
9
  * An external host (a Claude.ai / ChatGPT connector) self-registers by POSTing RFC
@@ -19,7 +19,7 @@ import {
19
19
  } from "./token-response";
20
20
 
21
21
  /**
22
- * The two grant handlers of the token endpoint (12-23, ported from future-pay's
22
+ * The two grant handlers of the token endpoint (12-23, ported from the origin host's
23
23
  * `lib/mcp/oauth/token-grants.ts`).
24
24
  *
25
25
  * Security invariants enforced here, unchanged:
@@ -123,7 +123,7 @@ function readAuthorizationCodeParams(form: URLSearchParams): AuthorizationCodePa
123
123
  /**
124
124
  * Redeem a presented code, or refuse.
125
125
  *
126
- * The ORDER is the security contract, and it is the order future-pay established:
126
+ * The ORDER is the security contract, and it is the order the origin host established:
127
127
  * verify the code's signature, authenticate the presenting client against the
128
128
  * client the code was bound to, check the bound `redirect_uri`, check PKCE — and
129
129
  * only THEN consume the single-use `jti`. Consuming earlier would let a failed
@@ -5,7 +5,7 @@ import type { OAuthClientStore } from "./stores";
5
5
  /**
6
6
  * The token endpoint's wire helpers: the RFC 6749 §5.1/§5.2 bodies and client
7
7
  * authentication (12-23, split out of the grant handlers so each file stays under
8
- * the size gate — the same split future-pay made).
8
+ * the size gate — the same split the origin host made).
9
9
  *
10
10
  * These bodies are NOT the house `{ data }` envelope, deliberately: they are read
11
11
  * by OAuth clients that expect the RFC shapes at the top level, and `Cache-Control:
@@ -11,7 +11,7 @@ import type { GeneratedTool, ToolManifest } from "../types";
11
11
  export interface BuildManifestOptions {
12
12
  /** Bumped intentionally on any tool-shape change (mirrors the golden catalog). */
13
13
  version: number;
14
- /** Human label for the spec, e.g. "future-pay web @ openapi.json". */
14
+ /** Human label for the spec, e.g. "acme web @ openapi.json". */
15
15
  source: string;
16
16
  }
17
17
 
@@ -16,7 +16,7 @@ import type { GeneratedTool } from "../types";
16
16
  * ask again: a tool that shipped stays invisible to every ALREADY CONNECTED
17
17
  * client for as long as that connection lives.
18
18
  *
19
- * That is not a hypothetical. In `future-pay` a new tool reached production,
19
+ * That is not a hypothetical. In the origin host a new tool reached production,
20
20
  * answered on its route, and did not appear in a live connector — behind a
21
21
  * `serverInfo.version` frozen at its initial value while ~280 tools were added
22
22
  * underneath it. Nothing was broken; the only thing asking anyone to bump it was