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.
- package/LICENSE +21 -0
- package/README.md +6 -77
- package/bin/everyapp.js +235 -0
- package/dist/access-LGEN3ABJ.js +1 -0
- package/dist/activity-RSD4D66F.js +12 -0
- package/dist/apps-4RCFXT74.js +13 -0
- package/dist/chunk-23HJ5I2A.js +2 -0
- package/dist/chunk-2Y5Q6A5K.js +6 -0
- package/dist/chunk-4JRIS6S6.js +1 -0
- package/dist/chunk-5PTNYZK6.js +4 -0
- package/dist/chunk-6OGVO7QK.js +6 -0
- package/dist/chunk-6PGUCTJ6.js +11 -0
- package/dist/chunk-AGTCKFRB.js +22 -0
- package/dist/chunk-BHMRY53V.js +2 -0
- package/dist/chunk-CB4U7H2G.js +4 -0
- package/dist/chunk-CCWBPAKH.js +4 -0
- package/dist/chunk-CZTEDWZ2.js +22 -0
- package/dist/chunk-CZXHKPRZ.js +2 -0
- package/dist/chunk-E3EPK27P.js +10 -0
- package/dist/chunk-EGEWRFVY.js +1 -0
- package/dist/chunk-ELK7QZ2M.js +1 -0
- package/dist/chunk-FJJSSVET.js +33 -0
- package/dist/chunk-GKBPYVP3.js +25 -0
- package/dist/chunk-H6QQMY7B.js +18 -0
- package/dist/chunk-H7PDL3X5.js +1 -0
- package/dist/chunk-HOUHG236.js +20 -0
- package/dist/chunk-IFCPALVC.js +46 -0
- package/dist/chunk-IUOBIB5M.js +30 -0
- package/dist/chunk-J35DDCNU.js +1 -0
- package/dist/chunk-J455FAEN.js +7 -0
- package/dist/chunk-JYKRUS5C.js +1 -0
- package/dist/chunk-LXYMDZER.js +1 -0
- package/dist/chunk-LYNBZ7RV.js +57 -0
- package/dist/chunk-M2QQ2UD7.js +132 -0
- package/dist/chunk-N4E5YE4X.js +5 -0
- package/dist/chunk-N5GQLGEC.js +2 -0
- package/dist/chunk-NFU4YFXB.js +1 -0
- package/dist/chunk-OM2HDWVP.js +1 -0
- package/dist/chunk-ONLNJXVQ.js +32 -0
- package/dist/chunk-OP4DYXYI.js +2 -0
- package/dist/chunk-PNLU5U3P.js +1 -0
- package/dist/chunk-PS7S74DG.js +6 -0
- package/dist/chunk-RMF4YGD6.js +98 -0
- package/dist/chunk-T2GD7RHS.js +10 -0
- package/dist/chunk-TLRM3P7V.js +15 -0
- package/dist/chunk-W3JCQHHU.js +1 -0
- package/dist/chunk-WOVS3GV5.js +10 -0
- package/dist/chunk-YHJZMJYZ.js +1 -0
- package/dist/chunk-ZECBN24L.js +15 -0
- package/dist/chunk-ZEXVHB22.js +2 -0
- package/dist/chunk-ZFCUH5PK.js +7 -0
- package/dist/configure-F7UZCJUG.js +8 -0
- package/dist/create-2E5HVWYK.js +22 -0
- package/dist/db-HEQVTDTM.js +9 -0
- package/dist/delete-XCSIQIRS.js +3 -0
- package/dist/deploy-3QOZEZHC.js +1 -0
- package/dist/dev-JL3ZP5BT.js +86 -0
- package/dist/email-RLZFUFUN.js +10 -0
- package/dist/events-2V5VRBWP.js +15 -0
- package/dist/groups-HDG5DVFY.js +8 -0
- package/dist/index.js +97 -1
- package/dist/install-Z5FYXUYM.js +1 -0
- package/dist/login-KHJBC64Z.js +1 -0
- package/dist/mcp-EFDUKJ5V.js +1 -0
- package/dist/provider-EOI5PTFH.js +13 -0
- package/dist/release-74J5ZIL3.js +1 -0
- package/dist/restore-FFGYV7NO.js +3 -0
- package/dist/secret-7FKKTLHB.js +1 -0
- package/dist/setup-PAKRCAFO.js +1 -0
- package/dist/signin-AP3D4XM4.js +12 -0
- package/dist/status-RMWNHOEK.js +4 -0
- package/dist/teardown-E4JNKAZJ.js +22 -0
- package/dist/update-DWVZFNPS.js +1 -0
- package/dist/workspace-CJC5I66L.js +21 -0
- package/package.json +19 -16
- package/skills/build-every-app/SKILL.md +199 -0
- package/skills/build-every-app/patterns.md +140 -0
- package/skills/everyapp/SKILL.md +292 -0
- package/skills/everyapp/mcp.md +350 -0
- package/skills/everyapp/security-review.md +61 -0
- package/skills/everyapp/setup/README.md +274 -0
- package/skills/everyapp/setup/access.md +178 -0
- package/skills/everyapp/setup/context.md +117 -0
- package/skills/everyapp/setup/dataforseo.md +287 -0
- package/skills/everyapp/setup/email.md +366 -0
- package/skills/everyapp/setup/gateway.md +243 -0
- package/skills/everyapp/setup/google.md +360 -0
- package/skills/everyapp/setup/microsoft-365.md +281 -0
- package/skills/everyapp/setup/openai.md +318 -0
- package/skills/everyapp/setup/openrouter.md +309 -0
- package/skills/everyapp/setup/providers.md +1253 -0
- package/skills/everyapp/setup/remote-mcp.md +199 -0
- package/skills/everyapp/setup/signin-apple.md +178 -0
- package/skills/everyapp/setup/signin-cloudflare.md +148 -0
- package/skills/everyapp/setup/signin-github.md +139 -0
- package/skills/everyapp/setup/signin-google.md +178 -0
- package/skills/everyapp/setup/signin-microsoft.md +183 -0
- package/skills/everyapp/setup/signin-oidc.md +163 -0
- package/skills/everyapp/setup/slack.md +344 -0
- package/skills/everyapp/setup/workers-ai.md +138 -0
- package/bin/__everyapp_bash_complete.js +0 -12
- package/dist/bash-complete.js +0 -3
- package/dist/chunk-7BOQJFI5.js +0 -8
- package/dist/chunk-7YVJFS4A.js +0 -1
- package/dist/chunk-BIXKPCDH.js +0 -2
- package/dist/chunk-DLGPLHBH.js +0 -47
- package/dist/chunk-FD6K2YA7.js +0 -9
- package/dist/chunk-GAJEQYJH.js +0 -39
- package/dist/chunk-O2KDOP5R.js +0 -13
- package/dist/chunk-OI2MBGBG.js +0 -4
- package/dist/chunk-POSDLD3G.js +0 -8
- package/dist/chunk-RB5D2R45.js +0 -1
- package/dist/chunk-U4LIQ5VR.js +0 -1
- package/dist/chunk-VEYSNPDH.js +0 -3
- package/dist/chunk-VP6SKW44.js +0 -7
- package/dist/chunk-WO3NE7ZU.js +0 -7
- package/dist/chunk-XTO2VTPY.js +0 -6
- package/dist/chunk-YJ2DKIZS.js +0 -1
- package/dist/create-DYQ3NBWJ.js +0 -22
- package/dist/deploy-PYTCMDGV.js +0 -7
- package/dist/deploy-RMAHOA4Z.js +0 -55
- package/dist/dev-SAE237FS.js +0 -0
- package/dist/generateConfig-46Z3ERF3.js +0 -1
- package/dist/login-B6EF3SMX.js +0 -1
- package/dist/logout-XMKZBLNV.js +0 -1
- package/dist/remoteD1Shell-A67JMO3H.js +0 -6
- package/dist/setupLocal-TZJUW5ES.js +0 -6
- 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.
|