@12-apps/mcp 1.20.0 → 3.0.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,7 +104,7 @@ 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
@@ -211,7 +211,7 @@ Two things about the gates worth knowing before you adopt them:
211
211
  committed exclusions file the moment it adopted the package, so it needs its own
212
212
  burn-down).
213
213
 
214
- ## Phase B — adopting into a host that ALREADY has these tables (future-pay)
214
+ ## Phase B — adopting into a host that ALREADY has these tables (the origin host)
215
215
 
216
216
  **Nothing to baseline.** Every statement in the package migration is guarded
217
217
  (`CREATE TABLE IF NOT EXISTS`, `CREATE [UNIQUE] INDEX IF NOT EXISTS`, `ADD COLUMN
@@ -223,8 +223,8 @@ a host that already has `oauth_clients` / `oauth_refresh_tokens` /
223
223
  Deliberate deltas to reconcile:
224
224
 
225
225
  - **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
226
+ migration** — host vocabulary. The origin host keeps its `ON DELETE CASCADE`.
227
+ - **`onboarding_states` is not here.** The origin host's migration created it beside
228
228
  `mcp_connections`; it belongs to `@12-apps/onboarding` (12-23).
229
229
  - The host's `lib/mcp/oauth/**` (~1.5k LOC) and its four route files are replaced
230
230
  by the mount plus, where a coverage gate forces the file to exist, a one-line
@@ -242,7 +242,7 @@ Deliberate deltas to reconcile:
242
242
  and redactions, is the host's catalogue. The package generates, dispatches and
243
243
  gates it.
244
244
  - **`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
245
+ the origin host MCP scripts. Only the two the reusable CI workflows shell out to moved
246
246
  (12-23's scope).
247
247
  - **Authorization codes as rows.** They are stateless signed blobs, so there is no
248
248
  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": "1.20.0",
3
+ "version": "3.0.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": "^1.20.0",
28
- "@12-apps/ui": "^4.0.0",
26
+ "@12-apps/onboarding": "^2.0.0",
27
+ "@12-apps/rbac": "^4.0.0",
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
  *
package/src/guide.ts CHANGED
@@ -99,104 +99,125 @@ const CONNECTOR_TAIL: readonly string[] = [
99
99
  * stage 2 creates the connector and signs in — which registers the connection on
100
100
  * the store side, so no prompt needs to be pasted afterwards.
101
101
  */
102
- const CHATGPT_CONFIGURE_STAGES: readonly AiHostConfigureStage[] = [
103
- {
104
- id: "enable-dev-mode",
105
- label: "enable developer mode",
106
- link: {
107
- url: "https://chatgpt.com/plugins#settings/Security",
108
- label: "Abrir Segurança e login",
102
+ function chatgptConfigureStages(
103
+ platformName: string,
104
+ ): readonly AiHostConfigureStage[] {
105
+ return [
106
+ {
107
+ id: "enable-dev-mode",
108
+ label: "enable developer mode",
109
+ link: {
110
+ url: "https://chatgpt.com/plugins#settings/Security",
111
+ label: "Abrir Segurança e login",
112
+ },
113
+ steps: [
114
+ "Ative o Modo desenvolvedor em Settings › Security and login (Segurança e login).",
115
+ ],
109
116
  },
110
- steps: [
111
- "Ative o Modo desenvolvedor em Settings › Security and login (Segurança e login).",
112
- ],
113
- },
114
- {
115
- id: "configurar",
116
- label: "configurar",
117
- link: {
118
- url: "https://chatgpt.com/plugins#settings/Connectors?create-connector=true&redirectAfter=%2Fplugins",
119
- label: "Criar o conector",
117
+ {
118
+ id: "configurar",
119
+ label: "configurar",
120
+ link: {
121
+ url: "https://chatgpt.com/plugins#settings/Connectors?create-connector=true&redirectAfter=%2Fplugins",
122
+ label: "Criar o conector",
123
+ },
124
+ steps: [
125
+ "Isso vai abrir um popup para você criar um plugin novo. Coloque como nome o nome da sua loja e, no campo MCP, o link copiado no passo anterior.",
126
+ 'Marque a caixa "I understand and want to continue" — a OpenAI não revisou este servidor MCP; ela avisa que sites podem tentar roubar seus dados ou induzir o modelo a ações indevidas, incluindo destruir dados.',
127
+ `Clique em "Sign in with ${platformName}" e entre com a sua conta de lojista para autorizar o acesso. Pronto: a conexão é registrada automaticamente.`,
128
+ ],
120
129
  },
121
- steps: [
122
- "Isso vai abrir um popup para você criar um plugin novo. Coloque como nome o nome da sua loja e, no campo MCP, o link copiado no passo anterior.",
123
- 'Marque a caixa "I understand and want to continue" — a OpenAI não revisou este servidor MCP; ela avisa que sites podem tentar roubar seus dados ou induzir o modelo a ações indevidas, incluindo destruir dados.',
124
- 'Clique em "Sign in with Future Drink" e entre com a sua conta de lojista para autorizar o acesso. Pronto: a conexão é registrada automaticamente.',
125
- ],
126
- },
127
- ];
130
+ ];
131
+ }
128
132
 
129
133
  /**
130
134
  * The AI hosts a store owner can connect, in recommended order. Same OAuth flow
131
135
  * everywhere (the host drives it) — only the menu path differs per app.
136
+ *
137
+ * A FUNCTION of the platform's name, because one step is not generic: the
138
+ * ChatGPT connector's consent screen shows an OAuth button labelled with
139
+ * whoever operates the server, and the owner is told which button to click. It
140
+ * used to name one particular STORE on one particular deployment — not even the
141
+ * product, a tenant of it — so every other adopter instructed its owners to
142
+ * click a button that does not exist.
132
143
  */
133
- export const AI_HOST_GUIDES: readonly AiHostGuide[] = [
134
- {
135
- id: "claude",
136
- label: "Claude.ai",
137
- brand: "claude",
138
- kind: "No navegador",
139
- link: {
140
- url: "https://claude.ai/new?modal=add-custom-connector#settings/customize-connectors",
141
- label: "Abrir os conectores do Claude",
142
- },
143
- docs: {
144
- url: "https://support.anthropic.com/en/articles/11175166-how-do-i-connect-mcp-servers-to-claude-ai",
145
- label: "documentação oficial da Anthropic — conectores personalizados",
144
+ export function aiHostGuides(platformName: string): readonly AiHostGuide[] {
145
+ const chatgptStages = chatgptConfigureStages(platformName);
146
+ return [
147
+ {
148
+ id: "claude",
149
+ label: "Claude.ai",
150
+ brand: "claude",
151
+ kind: "No navegador",
152
+ link: {
153
+ url: "https://claude.ai/new?modal=add-custom-connector#settings/customize-connectors",
154
+ label: "Abrir os conectores do Claude",
155
+ },
156
+ docs: {
157
+ url: "https://support.anthropic.com/en/articles/11175166-how-do-i-connect-mcp-servers-to-claude-ai",
158
+ label: "documentação oficial da Anthropic — conectores personalizados",
159
+ },
160
+ steps: [
161
+ "Clique no botão acima (ou vá em Settings › Customize › Connectors) e escolha Add custom connector.",
162
+ "Dê um nome ao conector (ex.: o nome da sua loja) e cole a URL do servidor MCP da sua loja (copie acima) no campo de URL.",
163
+ ...CONNECTOR_TAIL,
164
+ ],
146
165
  },
147
- steps: [
148
- "Clique no botão acima (ou vá em Settings › Customize › Connectors) e escolha Add custom connector.",
149
- " um nome ao conector (ex.: o nome da sua loja) e cole a URL do servidor MCP da sua loja (copie acima) no campo de URL.",
150
- ...CONNECTOR_TAIL,
151
- ],
152
- },
153
- {
154
- id: "claude-desktop",
155
- label: "Claude Desktop",
156
- brand: "claude",
157
- kind: "Aplicativo (Windows/Mac)",
158
- docs: {
159
- url: "https://support.anthropic.com/en/articles/11175166-how-do-i-connect-mcp-servers-to-claude-ai",
160
- label: "documentação oficial da Anthropic — conectores personalizados",
166
+ {
167
+ id: "claude-desktop",
168
+ label: "Claude Desktop",
169
+ brand: "claude",
170
+ kind: "Aplicativo (Windows/Mac)",
171
+ docs: {
172
+ url: "https://support.anthropic.com/en/articles/11175166-how-do-i-connect-mcp-servers-to-claude-ai",
173
+ label: "documentação oficial da Anthropic — conectores personalizados",
174
+ },
175
+ steps: [
176
+ "Abra o Claude Desktop e vá em Settings (⚙️) › Connectors.",
177
+ "Clique em Add custom connector e cole a URL do servidor MCP da sua loja (copie acima).",
178
+ ...CONNECTOR_TAIL,
179
+ ],
161
180
  },
162
- steps: [
163
- "Abra o Claude Desktop e vá em Settings (⚙️) › Connectors.",
164
- "Clique em Add custom connector e cole a URL do servidor MCP da sua loja (copie acima).",
165
- ...CONNECTOR_TAIL,
166
- ],
167
- },
168
- {
169
- id: "chatgpt",
170
- label: "ChatGPT",
171
- brand: "openai",
172
- kind: "No navegador",
173
- link: { url: "https://chatgpt.com/plugins", label: "Abrir os plugins do ChatGPT" },
174
- docs: {
175
- url: "https://developers.openai.com/apps-sdk/deploy/connect-chatgpt",
176
- label: "documentação oficial da OpenAI — conectar um servidor MCP ao ChatGPT",
181
+ {
182
+ id: "chatgpt",
183
+ label: "ChatGPT",
184
+ brand: "openai",
185
+ kind: "No navegador",
186
+ link: {
187
+ url: "https://chatgpt.com/plugins",
188
+ label: "Abrir os plugins do ChatGPT",
189
+ },
190
+ docs: {
191
+ url: "https://developers.openai.com/apps-sdk/deploy/connect-chatgpt",
192
+ label:
193
+ "documentação oficial da OpenAI — conectar um servidor MCP ao ChatGPT",
194
+ },
195
+ configureStages: chatgptStages,
196
+ // Mirrors the flattened stage instructions so the MCP connect guide
197
+ // (`connectToChatGpt`) can never drift from what owners see in the wizard.
198
+ steps: chatgptStages.flatMap((stage) => stage.steps),
177
199
  },
178
- configureStages: CHATGPT_CONFIGURE_STAGES,
179
- // Mirrors the flattened stage instructions so the MCP connect guide
180
- // (`connectToChatGpt`) can never drift from what owners see in the wizard.
181
- steps: CHATGPT_CONFIGURE_STAGES.flatMap((stage) => stage.steps),
182
- },
183
- {
184
- id: "codex",
185
- label: "Codex",
186
- brand: "openai",
187
- kind: "App / CLI de desenvolvedor",
188
- link: { url: "https://developers.openai.com/codex", label: "Documentação do Codex" },
189
- docs: {
190
- url: "https://developers.openai.com/apps-sdk/deploy/connect-chatgpt",
191
- label: "documentação oficial da OpenAI — conectar um servidor MCP",
200
+ {
201
+ id: "codex",
202
+ label: "Codex",
203
+ brand: "openai",
204
+ kind: "App / CLI de desenvolvedor",
205
+ link: {
206
+ url: "https://developers.openai.com/codex",
207
+ label: "Documentação do Codex",
208
+ },
209
+ docs: {
210
+ url: "https://developers.openai.com/apps-sdk/deploy/connect-chatgpt",
211
+ label: "documentação oficial da OpenAI — conectar um servidor MCP",
212
+ },
213
+ steps: [
214
+ "No Codex, abra as configurações de MCP (Settings › MCP no app, ou o arquivo de configuração na CLI).",
215
+ "Adicione um servidor MCP e cole a URL do servidor MCP da sua loja (copie acima) como um conector remoto (HTTP).",
216
+ ...CONNECTOR_TAIL,
217
+ ],
192
218
  },
193
- steps: [
194
- "No Codex, abra as configurações de MCP (Settings › MCP no app, ou o arquivo de configuração na CLI).",
195
- "Adicione um servidor MCP e cole a URL do servidor MCP da sua loja (copie acima) como um conector remoto (HTTP).",
196
- ...CONNECTOR_TAIL,
197
- ],
198
- },
199
- ];
219
+ ];
220
+ }
200
221
 
201
222
  export interface AiCapability {
202
223
  /** Stable id — maps to an icon in the component. */
@@ -217,7 +238,8 @@ export const AI_CAPABILITIES: readonly AiCapability[] = [
217
238
  {
218
239
  id: "inventory",
219
240
  title: "Controle o estoque",
220
- detail: '"Quanto ainda tenho do produto X? Registre a entrada de 20 unidades."',
241
+ detail:
242
+ '"Quanto ainda tenho do produto X? Registre a entrada de 20 unidades."',
221
243
  },
222
244
  {
223
245
  id: "catalog",
@@ -239,15 +261,39 @@ export const AI_CAPABILITIES: readonly AiCapability[] = [
239
261
  export const AI_PERMISSION_MODEL =
240
262
  "O assistente age em seu nome, com exatamente as suas permissões: ele pode fazer o que você pode fazer na sua loja — nada além disso. Não é preciso criar nenhuma chave ou credencial extra; a autorização usa o seu próprio login.";
241
263
 
264
+ /** The two tools the paste-in prompt drives, and what to call the store id. */
265
+ export interface AiConnectPromptSpec {
266
+ /**
267
+ * The tool that REGISTERS the connection server-side, so the store learns
268
+ * which assistant connected.
269
+ */
270
+ announceTool: string;
271
+ /** A real READ tool, called straight after, to prove the access works. */
272
+ probeTool: string;
273
+ /** What that read returns, in the owner's own words ("o estoque da loja"). */
274
+ probeSubject: string;
275
+ /** What the assistant should ask for if it needs to identify the store. */
276
+ identifierName: string;
277
+ }
278
+
242
279
  /**
243
- * The message the owner pastes into the assistant's chat right after connecting.
244
- * It makes the assistant (1) call `announceAiConnection` reporting which host it
245
- * is (chatgpt / claude / codex) — the tool that registers the connection on the
246
- * server side so the store learns which assistant connected — and (2) call a
247
- * real read tool to confirm access. The owner types nothing.
280
+ * The message the owner pastes into the assistant's chat right after
281
+ * connecting: announce the connection, then read something real to prove it
282
+ * works. The owner types nothing.
283
+ *
284
+ * BUILT from the host's tool names rather than shipped with them. This was a
285
+ * constant naming two tools — `announceAiConnection` and `listInventory` — that
286
+ * THIS PACKAGE does not define or serve; they belong to one adopter's surface.
287
+ * Any other host handed its owner a prompt instructing the assistant to call
288
+ * two tools that do not exist, and because nothing registered the connection,
289
+ * the wizard's confirm step then waited forever for a state that could never
290
+ * arrive.
248
291
  */
249
- export const AI_CONNECT_PROMPT =
250
- "Você agora tem acesso ao conector MCP da minha loja. Faça, nesta ordem:\n" +
251
- '1) Execute a ferramenta announceAiConnection informando qual assistente você é (host: "chatgpt", "claude" ou "codex") para registrar a conexão com a minha loja.\n' +
252
- "2) Execute a ferramenta listInventory para confirmar o acesso ao estoque da minha loja.\n" +
253
- "Se precisar do identificador da loja, me pergunte o tenantSlug.";
292
+ export function aiConnectPrompt(spec: AiConnectPromptSpec): string {
293
+ return (
294
+ "Você agora tem acesso ao conector MCP da minha loja. Faça, nesta ordem:\n" +
295
+ `1) Execute a ferramenta ${spec.announceTool} informando qual assistente você é (host: "chatgpt", "claude" ou "codex") para registrar a conexão com a minha loja.\n` +
296
+ `2) Execute a ferramenta ${spec.probeTool} para confirmar o acesso a ${spec.probeSubject}.\n` +
297
+ `Se precisar do identificador da loja, me pergunte o ${spec.identifierName}.`
298
+ );
299
+ }
package/src/index.ts CHANGED
@@ -13,10 +13,11 @@ export * from "./types";
13
13
  // truth for BOTH the `@12-apps/mcp/react` onboarding UI and the server-side connect
14
14
  // tools, so what an agent reads via MCP cannot drift from what owners see.
15
15
  export {
16
- AI_HOST_GUIDES,
16
+ aiHostGuides,
17
17
  AI_CAPABILITIES,
18
18
  AI_PERMISSION_MODEL,
19
- AI_CONNECT_PROMPT,
19
+ aiConnectPrompt,
20
+ type AiConnectPromptSpec,
20
21
  providerForHostId,
21
22
  type AiHostBrand,
22
23
  type AiHostLink,
@@ -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
  */
@@ -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.
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:
@@ -10,8 +10,7 @@ import {
10
10
 
11
11
  import {
12
12
  AI_CAPABILITIES,
13
- AI_CONNECT_PROMPT,
14
- AI_HOST_GUIDES,
13
+ aiHostGuides,
15
14
  AI_PERMISSION_MODEL,
16
15
  type AiCapability,
17
16
  type AiHostGuide,
@@ -44,14 +43,33 @@ export interface AiIntegrationOnboardingProps {
44
43
  featureKey?: string;
45
44
  /** Show the dev-only "reset onboarding" button. @default false */
46
45
  devReset?: boolean;
47
- /** Assistants offered in the flow. @default the shared AI_HOST_GUIDES */
46
+ /**
47
+ * The platform operating this MCP server, as its OAuth consent button names
48
+ * it. REQUIRED, and it is the reason `hosts` can have a default at all: one
49
+ * ChatGPT step tells the owner which "Sign in with …" button to click, and
50
+ * that button carries whoever runs the server. It used to be a hard-coded
51
+ * name — of a single STORE on one deployment, not even the product — so every
52
+ * other adopter pointed its owners at a button that does not exist.
53
+ */
54
+ platformName: string;
55
+ /** Assistants offered in the flow. @default aiHostGuides(platformName) */
48
56
  hosts?: readonly AiHostGuide[];
49
57
  /** Capability cards on the landing. @default the shared AI_CAPABILITIES */
50
58
  capabilities?: readonly AiCapability[];
51
59
  /** Permission reassurance copy on the landing. @default AI_PERMISSION_MODEL */
52
60
  permissionModel?: string;
53
- /** Message pasted into the assistant on the Conectar step. @default AI_CONNECT_PROMPT */
54
- connectPrompt?: string;
61
+ /**
62
+ * Message the owner pastes into the assistant on the Conectar step.
63
+ *
64
+ * REQUIRED, with no default, because the useful version of it names TOOLS —
65
+ * one to register the connection, one to read something real — and this
66
+ * package neither defines nor serves any. It shipped a constant naming two
67
+ * tools from one adopter's surface, so another host handed its owner a prompt
68
+ * that called two things that did not exist, and the confirm step then waited
69
+ * forever for a registration that could never happen. Build it with
70
+ * `aiConnectPrompt({ … })`.
71
+ */
72
+ connectPrompt: string;
55
73
  /**
56
74
  * Re-check the live connection on the verify step's "Testar conexão" button —
57
75
  * apps pass a router refresh (e.g. Next's `router.refresh`). @default a full
@@ -100,10 +118,19 @@ function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
100
118
  devReset,
101
119
  } = props;
102
120
  const { state } = useOnboarding();
103
- const selectedHost = hosts.find((h) => h.id === state.data.selectedHost) ?? hosts[0]!;
121
+ const selectedHost =
122
+ hosts.find((h) => h.id === state.data.selectedHost) ?? hosts[0]!;
104
123
 
105
- const steps = buildFlowSteps({ host: selectedHost, hosts, endpointUrl, connectPrompt, connections, onRetest });
106
- const connectedHostId = (state.data.connectedHost ?? state.data.selectedHost) as string | undefined;
124
+ const steps = buildFlowSteps({
125
+ host: selectedHost,
126
+ hosts,
127
+ endpointUrl,
128
+ connectPrompt,
129
+ connections,
130
+ onRetest,
131
+ });
132
+ const connectedHostId = (state.data.connectedHost ??
133
+ state.data.selectedHost) as string | undefined;
107
134
 
108
135
  return (
109
136
  <GuidedSection
@@ -111,13 +138,22 @@ function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
111
138
  title="Conecte assistentes de IA à sua loja"
112
139
  startLabel="Ver como conectar"
113
140
  renderLanding={(start) => (
114
- <AiLanding onStart={start} permissionModel={permissionModel} capabilities={capabilities} />
141
+ <AiLanding
142
+ onStart={start}
143
+ permissionModel={permissionModel}
144
+ capabilities={capabilities}
145
+ />
115
146
  )}
116
147
  configuredTitle={connectedTitle(connections, hosts, connectedHostId)}
117
148
  configuredSummary={connectedSummary(connections)}
118
149
  editLabel="Conectar IA"
119
150
  completedContent={(nav) => (
120
- <StatusBoard nav={nav} connections={connections} hosts={hosts} onDisconnect={onDisconnect} />
151
+ <StatusBoard
152
+ nav={nav}
153
+ connections={connections}
154
+ hosts={hosts}
155
+ onDisconnect={onDisconnect}
156
+ />
121
157
  )}
122
158
  devReset={devReset}
123
159
  dataTestId="ai-onboarding"
@@ -143,17 +179,22 @@ export function AiIntegrationOnboarding({
143
179
  connections,
144
180
  featureKey = DEFAULT_FEATURE_KEY,
145
181
  devReset = false,
146
- hosts = AI_HOST_GUIDES,
182
+ platformName,
183
+ hosts = aiHostGuides(platformName),
147
184
  capabilities = AI_CAPABILITIES,
148
185
  permissionModel = AI_PERMISSION_MODEL,
149
- connectPrompt = AI_CONNECT_PROMPT,
186
+ connectPrompt,
150
187
  onRetest = () => {
151
188
  if (typeof window !== "undefined") window.location.reload();
152
189
  },
153
190
  onDisconnect,
154
191
  }: AiIntegrationOnboardingProps): React.JSX.Element {
155
192
  return (
156
- <OnboardingProvider featureKey={featureKey} store={store} initialState={initialState}>
193
+ <OnboardingProvider
194
+ featureKey={featureKey}
195
+ store={store}
196
+ initialState={initialState}
197
+ >
157
198
  <AiOnboardingFlow
158
199
  endpointUrl={endpointUrl}
159
200
  connections={connections}
@@ -29,10 +29,11 @@ export { McpEndpointUrl } from "./mcp-endpoint-url";
29
29
  export { HostBrandAvatar, CapabilityIcon } from "./ai-icons";
30
30
  export { FeatureBadge, type FeatureBadgeItem } from "./feature-badge";
31
31
  export {
32
- AI_HOST_GUIDES,
32
+ aiHostGuides,
33
33
  AI_CAPABILITIES,
34
34
  AI_PERMISSION_MODEL,
35
- AI_CONNECT_PROMPT,
35
+ aiConnectPrompt,
36
+ type AiConnectPromptSpec,
36
37
  providerForHostId,
37
38
  type AiHostBrand,
38
39
  type AiHostLink,
@@ -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