everyapp 0.2.2 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -77
  3. package/bin/everyapp.js +235 -0
  4. package/dist/access-LGEN3ABJ.js +1 -0
  5. package/dist/activity-RSD4D66F.js +12 -0
  6. package/dist/apps-4RCFXT74.js +13 -0
  7. package/dist/chunk-23HJ5I2A.js +2 -0
  8. package/dist/chunk-2Y5Q6A5K.js +6 -0
  9. package/dist/chunk-4JRIS6S6.js +1 -0
  10. package/dist/chunk-5PTNYZK6.js +4 -0
  11. package/dist/chunk-6OGVO7QK.js +6 -0
  12. package/dist/chunk-6PGUCTJ6.js +11 -0
  13. package/dist/chunk-AGTCKFRB.js +22 -0
  14. package/dist/chunk-BHMRY53V.js +2 -0
  15. package/dist/chunk-CB4U7H2G.js +4 -0
  16. package/dist/chunk-CCWBPAKH.js +4 -0
  17. package/dist/chunk-CZTEDWZ2.js +22 -0
  18. package/dist/chunk-CZXHKPRZ.js +2 -0
  19. package/dist/chunk-E3EPK27P.js +10 -0
  20. package/dist/chunk-EGEWRFVY.js +1 -0
  21. package/dist/chunk-ELK7QZ2M.js +1 -0
  22. package/dist/chunk-FJJSSVET.js +33 -0
  23. package/dist/chunk-GKBPYVP3.js +25 -0
  24. package/dist/chunk-H6QQMY7B.js +18 -0
  25. package/dist/chunk-H7PDL3X5.js +1 -0
  26. package/dist/chunk-HOUHG236.js +20 -0
  27. package/dist/chunk-IFCPALVC.js +46 -0
  28. package/dist/chunk-IUOBIB5M.js +30 -0
  29. package/dist/chunk-J35DDCNU.js +1 -0
  30. package/dist/chunk-J455FAEN.js +7 -0
  31. package/dist/chunk-JYKRUS5C.js +1 -0
  32. package/dist/chunk-LXYMDZER.js +1 -0
  33. package/dist/chunk-LYNBZ7RV.js +57 -0
  34. package/dist/chunk-M2QQ2UD7.js +132 -0
  35. package/dist/chunk-N4E5YE4X.js +5 -0
  36. package/dist/chunk-N5GQLGEC.js +2 -0
  37. package/dist/chunk-NFU4YFXB.js +1 -0
  38. package/dist/chunk-OM2HDWVP.js +1 -0
  39. package/dist/chunk-ONLNJXVQ.js +32 -0
  40. package/dist/chunk-OP4DYXYI.js +2 -0
  41. package/dist/chunk-PNLU5U3P.js +1 -0
  42. package/dist/chunk-PS7S74DG.js +6 -0
  43. package/dist/chunk-RMF4YGD6.js +98 -0
  44. package/dist/chunk-T2GD7RHS.js +10 -0
  45. package/dist/chunk-TLRM3P7V.js +15 -0
  46. package/dist/chunk-W3JCQHHU.js +1 -0
  47. package/dist/chunk-WOVS3GV5.js +10 -0
  48. package/dist/chunk-YHJZMJYZ.js +1 -0
  49. package/dist/chunk-ZECBN24L.js +15 -0
  50. package/dist/chunk-ZEXVHB22.js +2 -0
  51. package/dist/chunk-ZFCUH5PK.js +7 -0
  52. package/dist/configure-F7UZCJUG.js +8 -0
  53. package/dist/create-2E5HVWYK.js +22 -0
  54. package/dist/db-HEQVTDTM.js +9 -0
  55. package/dist/delete-XCSIQIRS.js +3 -0
  56. package/dist/deploy-3QOZEZHC.js +1 -0
  57. package/dist/dev-JL3ZP5BT.js +86 -0
  58. package/dist/email-RLZFUFUN.js +10 -0
  59. package/dist/events-2V5VRBWP.js +15 -0
  60. package/dist/groups-HDG5DVFY.js +8 -0
  61. package/dist/index.js +97 -1
  62. package/dist/install-Z5FYXUYM.js +1 -0
  63. package/dist/login-KHJBC64Z.js +1 -0
  64. package/dist/mcp-EFDUKJ5V.js +1 -0
  65. package/dist/provider-EOI5PTFH.js +13 -0
  66. package/dist/release-74J5ZIL3.js +1 -0
  67. package/dist/restore-FFGYV7NO.js +3 -0
  68. package/dist/secret-7FKKTLHB.js +1 -0
  69. package/dist/setup-PAKRCAFO.js +1 -0
  70. package/dist/signin-AP3D4XM4.js +12 -0
  71. package/dist/status-RMWNHOEK.js +4 -0
  72. package/dist/teardown-E4JNKAZJ.js +22 -0
  73. package/dist/update-DWVZFNPS.js +1 -0
  74. package/dist/workspace-CJC5I66L.js +21 -0
  75. package/package.json +19 -16
  76. package/skills/build-every-app/SKILL.md +199 -0
  77. package/skills/build-every-app/patterns.md +140 -0
  78. package/skills/everyapp/SKILL.md +292 -0
  79. package/skills/everyapp/mcp.md +350 -0
  80. package/skills/everyapp/security-review.md +61 -0
  81. package/skills/everyapp/setup/README.md +274 -0
  82. package/skills/everyapp/setup/access.md +178 -0
  83. package/skills/everyapp/setup/context.md +117 -0
  84. package/skills/everyapp/setup/dataforseo.md +287 -0
  85. package/skills/everyapp/setup/email.md +366 -0
  86. package/skills/everyapp/setup/gateway.md +243 -0
  87. package/skills/everyapp/setup/google.md +360 -0
  88. package/skills/everyapp/setup/microsoft-365.md +281 -0
  89. package/skills/everyapp/setup/openai.md +318 -0
  90. package/skills/everyapp/setup/openrouter.md +309 -0
  91. package/skills/everyapp/setup/providers.md +1253 -0
  92. package/skills/everyapp/setup/remote-mcp.md +199 -0
  93. package/skills/everyapp/setup/signin-apple.md +178 -0
  94. package/skills/everyapp/setup/signin-cloudflare.md +148 -0
  95. package/skills/everyapp/setup/signin-github.md +139 -0
  96. package/skills/everyapp/setup/signin-google.md +178 -0
  97. package/skills/everyapp/setup/signin-microsoft.md +183 -0
  98. package/skills/everyapp/setup/signin-oidc.md +163 -0
  99. package/skills/everyapp/setup/slack.md +344 -0
  100. package/skills/everyapp/setup/workers-ai.md +138 -0
  101. package/bin/__everyapp_bash_complete.js +0 -12
  102. package/dist/bash-complete.js +0 -3
  103. package/dist/chunk-7BOQJFI5.js +0 -8
  104. package/dist/chunk-7YVJFS4A.js +0 -1
  105. package/dist/chunk-BIXKPCDH.js +0 -2
  106. package/dist/chunk-DLGPLHBH.js +0 -47
  107. package/dist/chunk-FD6K2YA7.js +0 -9
  108. package/dist/chunk-GAJEQYJH.js +0 -39
  109. package/dist/chunk-O2KDOP5R.js +0 -13
  110. package/dist/chunk-OI2MBGBG.js +0 -4
  111. package/dist/chunk-POSDLD3G.js +0 -8
  112. package/dist/chunk-RB5D2R45.js +0 -1
  113. package/dist/chunk-U4LIQ5VR.js +0 -1
  114. package/dist/chunk-VEYSNPDH.js +0 -3
  115. package/dist/chunk-VP6SKW44.js +0 -7
  116. package/dist/chunk-WO3NE7ZU.js +0 -7
  117. package/dist/chunk-XTO2VTPY.js +0 -6
  118. package/dist/chunk-YJ2DKIZS.js +0 -1
  119. package/dist/create-DYQ3NBWJ.js +0 -22
  120. package/dist/deploy-PYTCMDGV.js +0 -7
  121. package/dist/deploy-RMAHOA4Z.js +0 -55
  122. package/dist/dev-SAE237FS.js +0 -0
  123. package/dist/generateConfig-46Z3ERF3.js +0 -1
  124. package/dist/login-B6EF3SMX.js +0 -1
  125. package/dist/logout-XMKZBLNV.js +0 -1
  126. package/dist/remoteD1Shell-A67JMO3H.js +0 -6
  127. package/dist/setupLocal-TZJUW5ES.js +0 -6
  128. package/gateway/every-app-gateway-build.tar.gz +0 -0
@@ -0,0 +1,350 @@
1
+ # Agent tools (MCP)
2
+
3
+ An app never speaks MCP. It describes its tools with `defineTools()` in
4
+ src/tools.ts and passes them to `everyApp(handler, { tools })`; the Gateway
5
+ snapshots that catalog and serves AI clients at `https://<gateway>/mcp` (every
6
+ app the caller can use, tools named `<app-id>_<tool>`). An app's own host
7
+ serves `https://<app-host>/mcp` (that app alone) only when everyapp.config.ts
8
+ says `mcp: { by: "gateway", why: "..." }`; leave it out (the default), and
9
+ point clients at the Gateway. Never add an MCP server, an /mcp route, a login
10
+ or a token check to an app. Full reference: https://everyapp.com/docs/agent/mcp.md (in a
11
+ checkout landing-page/content/agent/mcp.md); the person's steps: https://everyapp.com/docs/connect-ai.
12
+
13
+ ## Define tools: src/tools.ts
14
+
15
+ The starter's shape (starter/src/tools.ts, shortened; the queries and their zod
16
+ schemas live in starter/src/server/todos.ts, shared with the server functions):
17
+
18
+ ```ts
19
+ import { ToolError, defineTools } from "@every-app/sdk/server";
20
+ import { z } from "zod";
21
+ import * as todos from "@/server/todos";
22
+
23
+ export const tools = defineTools(
24
+ {
25
+ list_todos: {
26
+ description: "List the user's todos, completed ones included.",
27
+ risk: "read",
28
+ input: z.object({}).strict(),
29
+ handler: (_input, { user }) => todos.listTodos(user.id),
30
+ },
31
+ create_todo: {
32
+ description: "Add a todo. The server assigns its id.",
33
+ risk: "write",
34
+ idempotent: false,
35
+ input: todos.createTodoInput,
36
+ handler: (input, { user }) => todos.createTodo(user.id, input),
37
+ },
38
+ delete_todo: {
39
+ description: "Delete one todo for good.",
40
+ risk: "destructive",
41
+ input: todos.deleteTodoInput,
42
+ handler: async (input, { user }) =>
43
+ (await todos.deleteTodo(user.id, input)) ?? raise("No todo with that id"),
44
+ },
45
+ },
46
+ {
47
+ instructions:
48
+ "Never guess todo ids: call list_todos first when an id isn't already known.",
49
+ },
50
+ );
51
+
52
+ /** A message the model may read; any other error reaches it as "Internal error". */
53
+ function raise(message: string): never {
54
+ throw new ToolError(message);
55
+ }
56
+ ```
57
+
58
+ Register them once, in src/entry.worker.ts:
59
+ `export default everyApp<Env>(async (request) => tanstackEntry.fetch(request), { tools });`
60
+
61
+ Names: lowercase snake_case, a letter first, at most 48 characters; on the
62
+ Gateway connector `<app-id>_<tool>` must fit 64 or the tool is skipped.
63
+ Fields: `description` (required; the model reads it; over 2048 characters the
64
+ Gateway skips the tool), `risk` (required), `input` (required: zod 4.2
65
+ or later, any Standard Schema with JSON Schema support, or a plain
66
+ `{ jsonSchema, validate }`; it must describe an object),
67
+ `handler(input, context)` (required), `idempotent` (boolean, sent as
68
+ `annotations.idempotentHint`), `title`, `output` (an object schema,
69
+ validated and sent as `structuredContent`), `openWorld`. Second
70
+ argument `{ instructions }`: guidance across the tools, at most 2048;
71
+ on the Gateway connector it is clipped to 1024 and placed after the
72
+ app list, and Claude Code shows the model only the first 4,096
73
+ characters of the whole (Codex's docs ask that the first 512 stand
74
+ alone), so put what matters first.
75
+ Risk: "read": listed and callable for every caller (`readOnlyHint: true`).
76
+ "write": hidden from read-only connections (`read_only_connection`).
77
+ "destructive": hidden from every AI client until approvals exist
78
+ (`destructive_requires_approval`): absent from tools/list, a call
79
+ answers `-32602 Unknown tool: <name>`. Keep the handler.
80
+ Context: the handler's second argument: `user` (the verified caller: `id`,
81
+ `email`, `orgId`, `orgRole`, `channel: "mcp"`, `actor.sub` of
82
+ `pat:<token id>`, `oauth:<connection id>` or `access:<email>`,
83
+ `scopes` of `["mcp:read"]` or `["mcp:read", "mcp:write"]`, and
84
+ `toolCall` with this call's `id` and `name`), `env`, `ctx`, `request`
85
+ (the internal `POST /__everyapp/tools/call`), `callId`. Scope every
86
+ read and write by `user.id`: a client never supplies a user id.
87
+ Results: a string (one text block), any JSON value (a JSON text block), a value
88
+ checked by `output` (text plus `structuredContent`), or
89
+ `toolResult({ text, structured })`. At most 1 MiB reaches the client.
90
+ Errors: throw `new ToolError("No todo with that id")` for a message the model
91
+ may read (`isError: true`). Any other error is logged in the app as
92
+ `[everyapp] tool "<name>" failed (ref <callId>): …` and reaches the
93
+ client only as `Internal error in <app-id> (ref <callId>)`.
94
+ A bad definition throws at module load (`defineTools: tool "<name>" needs a risk: "read", "write" or "destructive"`),
95
+ so the Worker fails rather than serving a catalog without the tool; a schema with
96
+ no object JSON Schema form only skips that tool with a warning (`TOOLS_CATALOG` in dev).
97
+
98
+ MCP apps: the Gateway also serves tools that aren't any app's, with no page
99
+ and nothing to deploy, on the same /mcp under the same access rules: a data
100
+ provider's built in (DataForSEO: `dataforseo_<tool>`, once the operator runs
101
+ `pnpm everyapp mcp on dataforseo` and sets DATAFORSEO_API_KEY), and any remote
102
+ MCP server added by URL (`pnpm everyapp mcp add <url> --auth bearer`, its tools
103
+ `<id>_<tool>`). For a remote server the Gateway holds the credential (the
104
+ Gateway setting `<ID>_MCP_TOKEN`, pinned to the URL's origin), forwards each
105
+ call, and decides each tool's risk from the server's annotations (no hint
106
+ counts as `write`; `mcp tool <id> <tool> --risk …` overrides one). `mcp off`
107
+ stops serving and keeps access; `mcp remove` deletes it. Never wrap a
108
+ remote MCP server in an app of your own to put it behind the Gateway: add it
109
+ (setup/remote-mcp.md, SKILL.md "MCP apps").
110
+
111
+ ## What the Gateway does with them
112
+
113
+ Snapshot: the Gateway stores each app's catalog in D1 with the app version it
114
+ came from, taken when the app is activated (the first deploy: the CLI waits
115
+ for the app to go live, so the catalog exists before anyone connects), when a
116
+ deploy through the Gateway (a browser install) finishes a new version, on the first app-host request each
117
+ isolate sees after a self-hosted redeploy, when `/mcp` or the consent page
118
+ finds a missing or outdated snapshot (synced within the request, 3 s budget),
119
+ and when a call reports a drifted `catalogHash` or `unknown_tool`. The CLI's
120
+ deploy syncs nothing itself. One sync: `POST /__everyapp/tools/list` (15 s, a
121
+ 5 MiB response; no count cap per app: a catalog is bounded by its per-tool
122
+ limits, the response size and one atomic D1 write); status `ok`, `empty`,
123
+ `unsupported` (SDK older than 0.3) or `error` (retried at most every 5
124
+ minutes); logged as `mcp.catalog.sync`.
125
+ Calls: `tools/list` never calls the app. `tools/call` checks the arguments
126
+ against the snapshot, mints a 120 s identity JWT for that one call
127
+ (`chan: "mcp"`, explicit scopes, `tc: { id, name }`), makes one
128
+ `POST /__everyapp/tools/call` (30 s) and audits one `mcp_tool_calls` row.
129
+ Built-ins (Gateway connector only; always loaded in Claude Code):
130
+ `everyapp_list_apps` (the apps this connection can use; one that
131
+ needs setup is `available: false` with the command an admin runs;
132
+ for an owner or admin of the install organization, an MCP app
133
+ that is off while its key is set, and no deployed app lists its
134
+ capability, is `offered: true` with `pnpm everyapp mcp on <id>`,
135
+ and the instructions' head says the same in one line),
136
+ `everyapp_whoami` (user, org, access), `everyapp_search_tools`
137
+ (`{ query?, app?, limit? }`: finds callable tools by words, by app
138
+ id or both, with their input schemas; read-only), and the two call
139
+ wrappers `everyapp_call_read` and `everyapp_call_write`
140
+ (`{ name, arguments? }`: run one app tool by its `<app-id>_<tool>`
141
+ name; the read wrapper runs read tools only with `mcp:read` alone,
142
+ the write wrapper is hidden from read-only connections). A wrapped
143
+ call is the same one call: same policy, checks and identity, and
144
+ one audit row naming the inner tool with `via` set to the wrapper.
145
+ Workspace tools (Gateway connector only; every admin action of the UI, listed
146
+ in full mode to a caller whose role reaches each; a read-only
147
+ connection never sees a write one; a token bound to one app or a
148
+ connection consented to some apps sees none of them; never a
149
+ credential that opens the Gateway; one audit row per call under app
150
+ `everyapp`): owners and admins get
151
+ `everyapp_workspace_show`, `everyapp_workspace_users`,
152
+ `everyapp_workspace_invite` (never the link), `everyapp_workspace_uninvite`,
153
+ `everyapp_workspace_role`, `everyapp_workspace_deactivate`,
154
+ `everyapp_workspace_reactivate`, `everyapp_workspace_reset_email`,
155
+ `everyapp_workspace_groups`, `everyapp_workspace_group_create`,
156
+ `everyapp_workspace_group_set`, `everyapp_workspace_group_delete`,
157
+ `everyapp_workspace_group_members`, `everyapp_workspace_group_members_set`,
158
+ `everyapp_workspace_group_apps`, `everyapp_workspace_group_apps_set`,
159
+ `everyapp_workspace_apps`, `everyapp_workspace_app_set`,
160
+ `everyapp_workspace_app_access`, `everyapp_workspace_app_access_set`,
161
+ `everyapp_workspace_app_cap`, `everyapp_workspace_cap_default`,
162
+ `everyapp_workspace_usage`,
163
+ `everyapp_workspace_activity`, `everyapp_workspace_rename`, and the
164
+ events tools `everyapp_workspace_events`, `everyapp_workspace_events_add`
165
+ (a generated signing secret comes back once, in that answer: it opens
166
+ nothing on the Gateway, so the person stores it in the receiver now),
167
+ `everyapp_workspace_events_set`, `everyapp_workspace_events_remove`
168
+ (asks `confirm: true`; Cap alerts is turned off instead),
169
+ `everyapp_workspace_events_test`, `everyapp_workspace_event_deliveries`,
170
+ `everyapp_workspace_event_redeliver`, `everyapp_workspace_remove`
171
+ and `everyapp_workspace_app_delete` (the last two ask `confirm: true`;
172
+ an admin never removes an owner); every member gets their own
173
+ `everyapp_my_tokens`, `everyapp_revoke_token`, `everyapp_my_connections`
174
+ and `everyapp_revoke_connection`. A user is an email or id, an app a
175
+ manifest id or row id, a group a name or `grp_` id, an event
176
+ subscription a name or `sub_` id. MCP apps, the
177
+ `mcp` commands as tools: `everyapp_mcp_list`, `everyapp_mcp_show`,
178
+ `everyapp_mcp_add` (a credential's setting name, never its value; the
179
+ answer names `secret set`), `everyapp_mcp_on`, `everyapp_mcp_off`,
180
+ `everyapp_mcp_refresh`, `everyapp_mcp_tool` and
181
+ `everyapp_mcp_remove` (`confirm: true`). Full reference:
182
+ /docs/agent/mcp#workspace-tools.
183
+ Provider tools (Gateway connector only; one audit row per call like the
184
+ workspace tools): every member gets `everyapp_provider_list` (the
185
+ API providers this Gateway brokers, catalog and rows, with their
186
+ settings and hints and which of this organization's apps list
187
+ each; set-ness and the key binding for owners and admins only);
188
+ owners and admins on a self-hosted Gateway also get
189
+ `everyapp_provider_add` (`{ spec }`: the spec object, never a URL,
190
+ never a replace of a row that is there) and
191
+ `everyapp_provider_remove` (`{ id }`: refuses a built-in, a row
192
+ whose key is set, and a row an app lists). A hosted Gateway hides
193
+ both writers. No key is ever set through a tool: `everyapp secret
194
+ set NAME` from the terminal. Full reference:
195
+ /docs/agent/mcp#provider-tools.
196
+ Sign-in provider tools (Gateway connector only, owners and admins, reads):
197
+ `everyapp_signin_list` (Manage workspace → Sign-in: each sign-in
198
+ provider on, off or needing setup, its settings by name with
199
+ whether each is set, the redirect URL to register and the
200
+ command) and `everyapp_signin_show` (`{ id }`). Never a value, and
201
+ no tool adds or removes a provider: `pnpm everyapp signin add
202
+ <id>` from the terminal. Full reference:
203
+ /docs/agent/mcp#sign-in-provider-tools.
204
+ Listing: full (the built-ins, the workspace tools, then every tool) until a connection's full list
205
+ would pass 256 tools or 1 MiB, then compact (the built-ins only:
206
+ find tools with `everyapp_search_tools`, run them through the
207
+ wrappers or by name); `X-EveryApp-Tools: full|compact` chooses
208
+ for any caller that can set a header, `?tools=` on /mcp for a
209
+ personal token, an Access caller or Claude Code (the header first
210
+ when both are given); claude.ai, Claude Desktop and ChatGPT need
211
+ the OAuth resource to equal the URL typed, so for them the query
212
+ form fails to connect and the bare /mcp gets the automatic rule.
213
+ Compact hides nothing: an unlisted tool is still callable; a
214
+ hidden one stays `Unknown tool`. A read-only connection is told
215
+ to use `everyapp_call_read` alone.
216
+ Changes: 2026-07-28 clients (Claude Code) hold a `subscriptions/listen` stream
217
+ re-read every 30 s, so a change reaches them within about half a
218
+ minute; the Gateway ends it after 30 minutes (an app host after 10,
219
+ and never past the token's expiry) and the client opens a new one,
220
+ few enough an hour that Claude Code keeps listening indefinitely;
221
+ 2025-era clients get `notifications/tools/list_changed` and a
222
+ new `Mcp-Session-Id` on their next request (a stale session's
223
+ `tools/list` answers 404, so the client initializes again).
224
+ `tools/list` carries `ttlMs: 60000` (a minute's cache).
225
+
226
+ ## Local: pnpm everyapp dev
227
+
228
+ `pnpm everyapp dev` prints `http://<app-id>.localhost:8787/mcp` under "MCP" in
229
+ its ready block: a local ToolBus with the Gateway's policy, argument checks and
230
+ identity contract, serving the app's live `defineTools()` catalog, re-read
231
+ every 2 s, so an edit shows on the next request. In the default stub mode any
232
+ bearer value acts as the seeded dev user; in `--mode mirror` it takes a
233
+ personal access token from the local Gateway. Each call prints
234
+ `MCP <tool> <outcome> <ms>ms` or `MCP <tool> blocked: <reason>` (two
235
+ spaces between parts; outcome and reason codes are printed as words: `tool
236
+ error`, `destructive requires approval`). Claude Code:
237
+
238
+ ```bash
239
+ claude mcp add --transport http todo-local http://<app-id>.localhost:8787/mcp --header "Authorization: Bearer dev"
240
+ ```
241
+
242
+ Caveat: the dev ToolBus declares `listChanged: false`, so Claude Code sees
243
+ tools added to `defineTools()` only after reconnecting (`/mcp`, the server).
244
+
245
+ ## Connecting a person
246
+
247
+ Claude Code (2.1.81 and later), then `/mcp`, choose **everyapp**, choose
248
+ **Authenticate**, allow in the browser; `everyapp_whoami` confirms the account:
249
+
250
+ ```bash
251
+ claude mcp add --transport http everyapp https://<gateway>/mcp
252
+ ```
253
+
254
+ claude.ai, Claude Desktop, ChatGPT: a custom connector with the URL
255
+ `https://<gateway>/mcp`; the client identifies itself with a Client ID
256
+ Metadata Document (CIMD), finds the OAuth server from the 401 challenge, and
257
+ the consent page opens. Cursor, Gemini CLI, MCP Inspector: the same URL, after
258
+ dynamic client registration (`/oauth/register`; `MCP_DCR_ENABLED` controls it).
259
+ Consent page: the organization, **All apps I can use, including future ones**
260
+ or **Only these apps**, read & write (default) or **Read only**, then
261
+ **Allow**. It grants `mcp:read` and, for read & write, `mcp:write`; an app
262
+ shown as `(no AI tools yet)` has a snapshot with no tools; one with no
263
+ snapshot yet shows no hint (its tools load on first use).
264
+ Scripts and anything that takes a header: a personal access token. In the
265
+ Gateway open **Admin** (the sidebar footer's **Manage workspace**; owners and
266
+ admins only), tab **Tokens**, heading **Personal access tokens**, button
267
+ **New token**: a name, **All my apps** or one app, an expiry (default
268
+ 90 days). The token is `epat_…`, shown once, read & write. Send it as
269
+ `Authorization: Bearer epat_…` to `https://<gateway>/mcp` (a token made for one
270
+ app sees that app alone):
271
+
272
+ ```bash
273
+ claude mcp add --transport http everyapp https://<gateway>/mcp --header "Authorization: Bearer $EVERYAPP_TOKEN"
274
+ ```
275
+
276
+ `https://<app-host>/mcp` takes the same header only where that app's manifest
277
+ sets `mcp: { by: "gateway", why }`; on any other app host the path is the app's
278
+ own private route and an MCP-only token gets 403 `token_scope` everywhere.
279
+
280
+ Disconnecting: **AI connections** in the account menu at the bottom of the
281
+ sidebar (`/settings/connections`) lists each OAuth connection; revoking one
282
+ makes the client's next request fail `invalid_token`. Removing or
283
+ deactivating a person ends all of theirs; tokens are revoked on Admin, Tokens.
284
+
285
+ Other servers worth having beside the Gateway's (Cloudflare's API server for
286
+ the owner, read before write; nothing else): [setup/gateway.md](setup/gateway.md),
287
+ "Optional: MCP servers worth having beside Every App". Offer one in a line;
288
+ never add it unasked.
289
+
290
+ ## Troubleshooting
291
+
292
+ Tools missing after a deploy, in order: (1) the client's cache: tools/list is
293
+ good for 60 s and a held stream polls every 30 s, so list tools again after a
294
+ minute; (2) the snapshot: read `mcp.catalog.sync` in the Gateway's logs
295
+ (`unsupported`: an SDK older than 0.3; `error`: the app's tools endpoint
296
+ failed, retried in 5 minutes; a self-hosted redeploy is noticed from the app
297
+ host's next request in each isolate); (3) hidden on purpose: destructive
298
+ tools are never listed, write tools are hidden from a read-only connection
299
+ (`everyapp_whoami` shows `access`), and a tool whose namespaced name passes 64
300
+ characters, whose description passes 2048 or whose schema isn't an object is
301
+ dropped with a warning (dev prints it as `TOOLS_CATALOG`); unlisted is not
302
+ hidden: a compact listing (the instructions' first line says the connection
303
+ "lists its tools compactly") leaves every app tool out of tools/list on
304
+ purpose, and each is found by `everyapp_search_tools` and callable by name or
305
+ through `everyapp_call_read`/`everyapp_call_write`, while a hidden tool stays
306
+ `Unknown tool` there too; (4) the person
307
+ cannot open the app: it is absent from `everyapp_list_apps`; grant access in
308
+ Admin (a connection made with **Only these apps** needs a reconnect to add one).
309
+ 401 vs 403 on `/mcp`: 401 means no credential or one that doesn't verify (an
310
+ expired or revoked token, a bearer that isn't `epat_` on an app host, the token
311
+ of a revoked connection); its body depends on the surface. On
312
+ `https://<gateway>/mcp` with OAuth on: an empty body and
313
+ `WWW-Authenticate: Bearer realm="OAuth", resource_metadata="…/.well-known/oauth-protected-resource/mcp", scope="mcp:read"`
314
+ (`error="invalid_token"` added when a token was sent); a revoked connection's
315
+ token gets `{"error":"invalid_token","error_description":"This connection was revoked or lost its access. Connect again."}`.
316
+ On `https://<app-host>/mcp` (where the manifest sets `mcp`), and on the Gateway with OAuth off:
317
+ `{"error":"unauthenticated"}` with `WWW-Authenticate: Bearer realm="every-app"`
318
+ (`error="invalid_token"` when a bearer was sent; the app host adds
319
+ `error_description="Use a personal access token here, or connect <gateway>/mcp"`).
320
+ An app host without `mcp` in its manifest answers `/mcp` as the app does (usually 404),
321
+ and one with `by: "gateway"` but no catalog to serve answers 503 `{"error":"mcp_unavailable"}`.
322
+ 403 `{"error":"forbidden","reason":…}` means the credential verified and was
323
+ refused: `token_scope` (scopes without `mcp:read` or `mcp:write`, an
324
+ MCP-only token on a page route, or any route of a host without `mcp`), `no_app_access` (app host: the person cannot
325
+ open this app), `not_connected`, `organization_unresolved`, `account_deactivated`, `organization_suspended`.
326
+ OAuth off: `MCP_OAUTH_ENABLED` is `false`, or `GATEWAY_URL` is neither https
327
+ nor http on exactly localhost or 127.x: `/oauth/*` and the discovery documents
328
+ answer 404 `oauth_disabled`, `/mcp` takes `epat_` tokens only and its 401 says
329
+ `WWW-Authenticate: Bearer realm="every-app"` with no authorization server. Turn
330
+ it on with `pnpm everyapp secret set MCP_OAUTH_ENABLED`, entering `true` (live at once).
331
+ DCR off or limited: with `MCP_DCR_ENABLED` `false`, `/oauth/register` answers
332
+ 404 `registration_disabled`; CIMD clients still connect, Cursor cannot. While
333
+ on: 5 registrations a minute and 20 a day per client address, 200 a day per
334
+ self-hosted Gateway, then `429 rate_limited` (wait, or use a CIMD client); past
335
+ 60 requests a minute per address on `/oauth/register` or `/oauth/token`, 429 `Too many attempts.`
336
+ Behind Cloudflare Access: `pnpm everyapp access on` lets `/mcp`, `/mcp/*`,
337
+ `/oauth/token`, `/oauth/register`, `/.well-known/*` and `/health` through on the Gateway
338
+ host, and `/mcp` on an app host only where its manifest sets `mcp`, so OAuth and tokens keep working; the consent
339
+ page stays behind Access. With Managed OAuth on the Gateway's Access application
340
+ instead, Access is the OAuth server: the person must have signed in to the Gateway
341
+ in a browser once (else `403 not_connected`), gets read & write to every app, no connection row.
342
+
343
+ ## Sources
344
+
345
+ - packages/sdk/src/server/tools.ts, toolsEndpoint.ts, everyApp.ts; packages/sdk/src/internal/tools.ts, protocol.ts
346
+ - packages/toolbus/src/server.ts, http.ts, builtins.ts, policy.ts, dev/index.ts
347
+ - apps/every-app-gateway/src/server/toolbus/catalog-sync.ts, gateway-mcp.ts, app-host-mcp.ts; src/server/app-activation.ts, deploy/run.ts; src/server/mcp-auth/routes.ts, config.ts, validate.ts, consent.ts, registration-limit.ts
348
+ - apps/every-app-gateway/src/routes/_app/admin/tokens.tsx, settings/connections.tsx; src/client/components/Sidebar.tsx, oauth/ConsentForm.tsx, admin/CreateUserTokenModal.tsx
349
+ - packages/perimeter/src/gateway.ts; packages/cli/src/commands/dev/devAppGateway.ts, ready.ts; packages/cli/src/commands/deploy/deployApp.ts; packages/cli/src/lib/app/skill.ts
350
+ - starter/src/tools.ts, entry.worker.ts, server/todos.ts, README.md; docs/runbooks/mcp-oauth.md; landing-page/content/agent/mcp.md; landing-page/content/docs/connect-ai.mdx
@@ -0,0 +1,61 @@
1
+ # Reviewing an app's changes for security problems
2
+
3
+ The Gateway in front of an app does the sign-in and passes identity in; the
4
+ app does the rest. Review the app's pending changes before a pull request
5
+ that touches identity, public routes, tools, secrets or queries: locally,
6
+ by the agent that made the change, with no CI job and no API key.
7
+
8
+ ## The app boundaries
9
+
10
+ What the review looks for (what the Gateway promises and what the app must
11
+ do itself is the threat model, `docs/threat-model.md` in the Every App
12
+ checkout, served at https://everyapp.com/docs/agent/repo/threat-model):
13
+
14
+ - A route, server function or tool that reads who the user is from anything
15
+ but the SDK (`everyApp()`, `getEveryAppUser()`, `requireEveryAppUser()`, a
16
+ tool call's identity): a client-supplied `x-everyapp-*` header, a cookie, a
17
+ query parameter or a body field naming a user or organization
18
+ - A route listed under `public` in `everyapp.config.ts` that returns member
19
+ data or changes state without a check of its own (a webhook signature, a
20
+ bearer token the app verifies): `public` means anyone on the internet
21
+ reaches it, and the Gateway forwards their `Authorization` header untouched
22
+ - A database query that doesn't scope by the user or organization from the
23
+ identity, so one person's rows reach another; SQL built from strings instead
24
+ of the query builder's parameters
25
+ - Agent tools (`defineTools()`): a tool that deletes or changes data without
26
+ declaring the matching risk, or that trusts an argument as a path, URL or
27
+ id without a check
28
+ - A secret read from anywhere but the Worker's bindings, or printed, logged
29
+ or sent to a client
30
+ - `fetch()` to a URL whose host or scheme comes from user input, and HTML
31
+ built from user input outside React (`dangerouslySetInnerHTML`, string
32
+ templates)
33
+
34
+ ## How to run it
35
+
36
+ - In Claude Code: `/security-review` (its built-in review of the branch's
37
+ pending changes), telling it to judge the diff against the boundaries
38
+ above (point it at this file). It reads the code; it does not need to run
39
+ anything.
40
+ - Any other agent: read `git diff <base>...HEAD` plus the working tree
41
+ against the list above, and report each finding as file, line, severity
42
+ (high: directly exploitable; medium: needs specific conditions; low:
43
+ defense in depth), what an attacker does, and the fix. Only report what
44
+ you are confident is exploitable.
45
+ - The model reads the diff as instructions too: the review is a second pair
46
+ of eyes on your own changes, never a gate on a stranger's. Read a
47
+ contributor's diff yourself first.
48
+ - Not findings: denial of service, rate limits, resource use, a missing
49
+ hardening measure, a dependency's version, test files, prose, anything
50
+ that needs a Cloudflare login or an environment variable the attacker
51
+ cannot have. The Gateway strips inbound `Cookie`, `Authorization`,
52
+ `x-everyapp-*` and `cf-access-*` headers before the app sees a private
53
+ request, and every private request carries a fresh identity token the
54
+ SDK verifies: a route with no authentication of its own is expected.
55
+
56
+ ## With a finding
57
+
58
+ Fix it on the branch (and add a test), or say in the pull request why it is
59
+ a false positive. A public route that must stay open (a webhook, a tracking
60
+ collector) is not a finding when it checks what it must itself; a public
61
+ route that returns member data without a check is.