@every-app/sdk 0.2.0 → 0.3.0-rc.1

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 (113) hide show
  1. package/README.md +608 -2
  2. package/dist/cloudflare/index.d.ts +22 -0
  3. package/dist/cloudflare/index.d.ts.map +1 -0
  4. package/dist/cloudflare/index.js +21 -0
  5. package/dist/cloudflare/sentinel.d.ts +69 -0
  6. package/dist/cloudflare/sentinel.d.ts.map +1 -0
  7. package/dist/cloudflare/sentinel.js +50 -0
  8. package/dist/cloudflare/worker.d.ts +511 -0
  9. package/dist/cloudflare/worker.d.ts.map +1 -0
  10. package/dist/cloudflare/worker.js +738 -0
  11. package/dist/dev/index.d.ts +8 -0
  12. package/dist/dev/index.d.ts.map +1 -0
  13. package/dist/dev/index.js +7 -0
  14. package/dist/dev/keypair.d.ts +24 -0
  15. package/dist/dev/keypair.d.ts.map +1 -0
  16. package/dist/dev/keypair.js +60 -0
  17. package/dist/internal/aiProviders.d.ts +67 -0
  18. package/dist/internal/aiProviders.d.ts.map +1 -0
  19. package/dist/internal/aiProviders.js +220 -0
  20. package/dist/internal/appSettings.d.ts +237 -0
  21. package/dist/internal/appSettings.d.ts.map +1 -0
  22. package/dist/internal/appSettings.js +374 -0
  23. package/dist/internal/capabilities.d.ts +1344 -0
  24. package/dist/internal/capabilities.d.ts.map +1 -0
  25. package/dist/internal/capabilities.js +1521 -0
  26. package/dist/internal/devProviders.d.ts +49 -0
  27. package/dist/internal/devProviders.d.ts.map +1 -0
  28. package/dist/internal/devProviders.js +62 -0
  29. package/dist/internal/email.d.ts +280 -0
  30. package/dist/internal/email.d.ts.map +1 -0
  31. package/dist/internal/email.js +613 -0
  32. package/dist/internal/events.d.ts +182 -0
  33. package/dist/internal/events.d.ts.map +1 -0
  34. package/dist/internal/events.js +324 -0
  35. package/dist/internal/gatewaySettings.d.ts +82 -0
  36. package/dist/internal/gatewaySettings.d.ts.map +1 -0
  37. package/dist/internal/gatewaySettings.js +209 -0
  38. package/dist/internal/identity.d.ts +18 -3
  39. package/dist/internal/identity.d.ts.map +1 -1
  40. package/dist/internal/identity.js +53 -14
  41. package/dist/internal/index.d.ts +17 -1
  42. package/dist/internal/index.d.ts.map +1 -1
  43. package/dist/internal/index.js +17 -1
  44. package/dist/internal/members.d.ts +35 -0
  45. package/dist/internal/members.d.ts.map +1 -0
  46. package/dist/internal/members.js +7 -0
  47. package/dist/internal/oauthClient.d.ts +63 -0
  48. package/dist/internal/oauthClient.d.ts.map +1 -0
  49. package/dist/internal/oauthClient.js +194 -0
  50. package/dist/internal/oauthScopes.d.ts +117 -0
  51. package/dist/internal/oauthScopes.d.ts.map +1 -0
  52. package/dist/internal/oauthScopes.js +416 -0
  53. package/dist/internal/organizationName.d.ts +18 -0
  54. package/dist/internal/organizationName.d.ts.map +1 -0
  55. package/dist/internal/organizationName.js +24 -0
  56. package/dist/internal/protocol.d.ts +86 -2
  57. package/dist/internal/protocol.d.ts.map +1 -1
  58. package/dist/internal/protocol.js +112 -1
  59. package/dist/internal/providerApps.d.ts +75 -0
  60. package/dist/internal/providerApps.d.ts.map +1 -0
  61. package/dist/internal/providerApps.js +59 -0
  62. package/dist/internal/providerRegistry.d.ts +147 -0
  63. package/dist/internal/providerRegistry.d.ts.map +1 -0
  64. package/dist/internal/providerRegistry.js +262 -0
  65. package/dist/internal/providerSpec.d.ts +55 -0
  66. package/dist/internal/providerSpec.d.ts.map +1 -0
  67. package/dist/internal/providerSpec.js +746 -0
  68. package/dist/internal/remoteMcp.d.ts +188 -0
  69. package/dist/internal/remoteMcp.d.ts.map +1 -0
  70. package/dist/internal/remoteMcp.js +543 -0
  71. package/dist/internal/tools.d.ts +115 -0
  72. package/dist/internal/tools.d.ts.map +1 -0
  73. package/dist/internal/tools.js +80 -0
  74. package/dist/server/attribution.d.ts +39 -0
  75. package/dist/server/attribution.d.ts.map +1 -0
  76. package/dist/server/attribution.js +67 -0
  77. package/dist/server/authorizeUrl.d.ts +43 -0
  78. package/dist/server/authorizeUrl.d.ts.map +1 -0
  79. package/dist/server/authorizeUrl.js +110 -0
  80. package/dist/server/emit.d.ts +63 -0
  81. package/dist/server/emit.d.ts.map +1 -0
  82. package/dist/server/emit.js +101 -0
  83. package/dist/server/everyApp.d.ts +38 -12
  84. package/dist/server/everyApp.d.ts.map +1 -1
  85. package/dist/server/everyApp.js +92 -28
  86. package/dist/server/gatewayFetch.d.ts +24 -8
  87. package/dist/server/gatewayFetch.d.ts.map +1 -1
  88. package/dist/server/gatewayFetch.js +81 -36
  89. package/dist/server/index.d.ts +10 -1
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/index.js +8 -1
  92. package/dist/server/integrationFetch.d.ts +131 -0
  93. package/dist/server/integrationFetch.d.ts.map +1 -0
  94. package/dist/server/integrationFetch.js +526 -0
  95. package/dist/server/listMembers.d.ts +32 -0
  96. package/dist/server/listMembers.d.ts.map +1 -0
  97. package/dist/server/listMembers.js +74 -0
  98. package/dist/server/nodeServer.d.ts +48 -0
  99. package/dist/server/nodeServer.d.ts.map +1 -0
  100. package/dist/server/nodeServer.js +45 -0
  101. package/dist/server/sendEmail.d.ts +39 -0
  102. package/dist/server/sendEmail.d.ts.map +1 -0
  103. package/dist/server/sendEmail.js +114 -0
  104. package/dist/server/tools.d.ts +184 -0
  105. package/dist/server/tools.d.ts.map +1 -0
  106. package/dist/server/tools.js +347 -0
  107. package/dist/server/toolsEndpoint.d.ts +32 -0
  108. package/dist/server/toolsEndpoint.d.ts.map +1 -0
  109. package/dist/server/toolsEndpoint.js +231 -0
  110. package/package.json +30 -17
  111. package/dist/server/mcp.d.ts +0 -66
  112. package/dist/server/mcp.d.ts.map +0 -1
  113. package/dist/server/mcp.js +0 -147
@@ -0,0 +1,347 @@
1
+ /**
2
+ * `defineTools()` — agent tools an app offers to MCP clients through the
3
+ * Every App gateway.
4
+ *
5
+ * export const tools = defineTools(
6
+ * {
7
+ * list_todos: {
8
+ * description: "List the user's todos.",
9
+ * risk: "read",
10
+ * input: z.object({}).strict(),
11
+ * handler: async (_input, { user }) => listTodos(user.id),
12
+ * },
13
+ * },
14
+ * { instructions: "Never guess todo ids; call list_todos first." },
15
+ * );
16
+ * export default everyApp(handler, { tools });
17
+ *
18
+ * Apps never speak MCP. The gateway snapshots this catalog at deploy, serves
19
+ * MCP clients from the snapshot, and calls a tool over the internal tools
20
+ * contract (../internal/tools.ts) with a verified `chan: "mcp"` identity.
21
+ *
22
+ * `input` and `output` accept any validator implementing Standard Schema and
23
+ * Standard JSON Schema (zod >= 4.2 does both), or a plain
24
+ * `{ jsonSchema, validate }` pair. The SDK itself takes no dependency.
25
+ */
26
+ import { TOOL_NAME_PATTERN, catalogHash, isToolRisk, } from "../internal/index.js";
27
+ /** Reported with every catalog; must match package.json. */
28
+ export const SDK_VERSION = "0.3.0";
29
+ const JSON_SCHEMA_TARGET = "draft-2020-12";
30
+ const DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
31
+ // The 2026-07-28 MCP revision lets a schema property ask clients to mirror an
32
+ // argument into an HTTP header. Apps never route on headers, and the gateway
33
+ // fronts many apps, so the hint is never passed on.
34
+ const MCP_HEADER_KEYWORD = "x-mcp-header";
35
+ /**
36
+ * Throw from a tool handler to return an error the model can read and act on
37
+ * ("No todo with that id"). Any other error is logged here and reaches the
38
+ * client only as "Internal error in <app> (ref <call id>)".
39
+ */
40
+ export class ToolError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "ToolError";
44
+ }
45
+ }
46
+ const TOOL_RESULT = Symbol.for("everyapp.toolResult");
47
+ /**
48
+ * Return a written result and the same result as data. The model reads
49
+ * `text` (a summary, a table); clients also get `structured`, checked
50
+ * against the tool's `output` schema. Without this, a tool with an `output`
51
+ * schema sends its value as JSON text.
52
+ */
53
+ export function toolResult(result) {
54
+ return {
55
+ [TOOL_RESULT]: true,
56
+ text: result.text,
57
+ structured: result.structured,
58
+ };
59
+ }
60
+ function isToolResult(value) {
61
+ return (isRecord(value) && value[TOOL_RESULT] === true);
62
+ }
63
+ const internals = new WeakMap();
64
+ /** @internal The everyApp() side of a tool set. */
65
+ export function toolSetInternals(toolSet) {
66
+ const found = internals.get(toolSet);
67
+ if (!found) {
68
+ throw new TypeError("tools must be the value returned by defineTools()");
69
+ }
70
+ return found;
71
+ }
72
+ /**
73
+ * Define an app's agent tools. Names are lowercase snake_case (at most 48
74
+ * characters); the gateway prefixes them with the app id where several apps
75
+ * share one MCP endpoint. Handler inputs are typed from each `input` schema.
76
+ */
77
+ export function defineTools(tools, options = {}) {
78
+ const definitions = new Map();
79
+ for (const name of Object.keys(tools).sort()) {
80
+ const definition = tools[name];
81
+ assertToolDefinition(name, definition);
82
+ definitions.set(name, definition);
83
+ }
84
+ if (options.instructions !== undefined &&
85
+ typeof options.instructions !== "string") {
86
+ throw new TypeError("defineTools: instructions must be a string");
87
+ }
88
+ let catalog;
89
+ const toolSet = Object.freeze({
90
+ names: Object.freeze([...definitions.keys()]),
91
+ });
92
+ internals.set(toolSet, {
93
+ tool: (name) => definitions.get(name),
94
+ catalog: () => {
95
+ catalog ??= buildCatalog(definitions, options.instructions).catch((error) => {
96
+ catalog = undefined;
97
+ throw error;
98
+ });
99
+ return catalog;
100
+ },
101
+ });
102
+ return toolSet;
103
+ }
104
+ function assertToolDefinition(name, definition) {
105
+ const problem = (message) => new TypeError(`defineTools: tool "${name}" ${message}`);
106
+ if (!TOOL_NAME_PATTERN.test(name)) {
107
+ throw problem("must be named in lowercase snake_case: a letter, then letters, digits or _ (at most 48 characters)");
108
+ }
109
+ if (!definition || typeof definition !== "object") {
110
+ throw problem("must be an object");
111
+ }
112
+ if (typeof definition.description !== "string" ||
113
+ !definition.description.trim()) {
114
+ throw problem("needs a description");
115
+ }
116
+ if (!isToolRisk(definition.risk)) {
117
+ throw problem('needs a risk: "read", "write" or "destructive"');
118
+ }
119
+ if (!isToolSchema(definition.input)) {
120
+ throw problem("input must be a Standard Schema with JSON Schema support (zod >= 4.2) or { jsonSchema, validate }");
121
+ }
122
+ if (definition.output !== undefined && !isToolSchema(definition.output)) {
123
+ throw problem("output must be a Standard Schema with JSON Schema support (zod >= 4.2) or { jsonSchema, validate }");
124
+ }
125
+ if (typeof definition.handler !== "function") {
126
+ throw problem("needs a handler function");
127
+ }
128
+ if (definition.title !== undefined && typeof definition.title !== "string") {
129
+ throw problem("title must be a string");
130
+ }
131
+ for (const flag of ["idempotent", "openWorld"]) {
132
+ if (definition[flag] !== undefined &&
133
+ typeof definition[flag] !== "boolean") {
134
+ throw problem(`${flag} must be a boolean`);
135
+ }
136
+ }
137
+ }
138
+ function isRecord(value) {
139
+ return ((typeof value === "object" || typeof value === "function") && value !== null);
140
+ }
141
+ function isPlainToolSchema(value) {
142
+ return (isRecord(value) &&
143
+ !("~standard" in value) &&
144
+ isRecord(value.jsonSchema) &&
145
+ typeof value.validate === "function");
146
+ }
147
+ function isStandardWithJson(value) {
148
+ if (!isRecord(value))
149
+ return false;
150
+ const standard = value["~standard"];
151
+ if (!isRecord(standard) || typeof standard.validate !== "function") {
152
+ return false;
153
+ }
154
+ const converter = standard.jsonSchema;
155
+ return (isRecord(converter) &&
156
+ typeof converter.input === "function" &&
157
+ typeof converter.output === "function");
158
+ }
159
+ function isToolSchema(value) {
160
+ return isStandardWithJson(value) || isPlainToolSchema(value);
161
+ }
162
+ /**
163
+ * Describe every tool that can be described. A tool whose schema has no
164
+ * object JSON Schema form is left out with a warning (logged here, and
165
+ * reported to the gateway so the deploy prints it) instead of taking the
166
+ * app's other tools down with it.
167
+ */
168
+ async function buildCatalog(definitions, instructions) {
169
+ const tools = [];
170
+ const warnings = [];
171
+ for (const [name, definition] of definitions) {
172
+ const described = describeTool(name, definition);
173
+ if (typeof described === "string") {
174
+ warnings.push(`${name}: skipped, ${described}`);
175
+ console.warn(`[everyapp] defineTools: tool "${name}" ${described}`);
176
+ }
177
+ else {
178
+ tools.push(described);
179
+ }
180
+ }
181
+ return {
182
+ ...(instructions !== undefined ? { instructions } : {}),
183
+ tools,
184
+ hash: await catalogHash({ instructions, tools }),
185
+ warnings,
186
+ };
187
+ }
188
+ /** A tool's descriptor, or why it can't be described. */
189
+ function describeTool(name, definition) {
190
+ const inputSchema = objectJsonSchema(definition.input, "input");
191
+ if (typeof inputSchema === "string")
192
+ return inputSchema;
193
+ const outputSchema = definition.output
194
+ ? objectJsonSchema(definition.output, "output")
195
+ : undefined;
196
+ if (typeof outputSchema === "string")
197
+ return outputSchema;
198
+ const annotations = {
199
+ ...(definition.idempotent !== undefined
200
+ ? { idempotentHint: definition.idempotent }
201
+ : {}),
202
+ ...(definition.openWorld !== undefined
203
+ ? { openWorldHint: definition.openWorld }
204
+ : {}),
205
+ };
206
+ return {
207
+ name,
208
+ ...(definition.title ? { title: definition.title } : {}),
209
+ description: definition.description,
210
+ risk: definition.risk,
211
+ inputSchema,
212
+ ...(outputSchema ? { outputSchema } : {}),
213
+ ...(Object.keys(annotations).length > 0 ? { annotations } : {}),
214
+ };
215
+ }
216
+ /**
217
+ * The JSON Schema for one side of a tool: the arguments a client sends
218
+ * (`input`) or the result it receives (`output`), or why there is none.
219
+ *
220
+ * Both sides must describe objects. Unions of objects (zod's `union` and
221
+ * `discriminatedUnion`) come out as a root `anyOf`/`oneOf` without a `type`,
222
+ * so like the MCP SDK this adds `type: "object"` to a typeless input root
223
+ * (MCP arguments are always an object) and to a typeless output root whose
224
+ * every branch is an object.
225
+ */
226
+ function objectJsonSchema(schema, side) {
227
+ let raw;
228
+ try {
229
+ raw = isPlainToolSchema(schema)
230
+ ? schema.jsonSchema
231
+ : schema["~standard"].jsonSchema[side]({ target: JSON_SCHEMA_TARGET });
232
+ // A JSON round trip copies the schema and drops anything JSON can't carry.
233
+ raw = JSON.parse(JSON.stringify(raw));
234
+ }
235
+ catch (error) {
236
+ return `${side} has no JSON Schema form: ${error.message}`;
237
+ }
238
+ if (!isRecord(raw) || Array.isArray(raw)) {
239
+ return `${side} has no JSON Schema form`;
240
+ }
241
+ const copy = withoutMcpHeaders(raw);
242
+ if (copy.$schema === DRAFT_2020_12)
243
+ delete copy.$schema;
244
+ if (copy.type === undefined &&
245
+ (side === "input" || isProvablyObjectShaped(copy))) {
246
+ return { type: "object", ...copy };
247
+ }
248
+ if (copy.type !== "object") {
249
+ return `${side} must describe an object (JSON Schema type "object")`;
250
+ }
251
+ return copy;
252
+ }
253
+ /**
254
+ * A typeless schema that can only match objects: it has object keywords, or
255
+ * every branch of its `oneOf`/`anyOf`/`allOf` is an object. `$ref` is not
256
+ * followed.
257
+ */
258
+ function isProvablyObjectShaped(schema) {
259
+ if ([
260
+ "properties",
261
+ "patternProperties",
262
+ "additionalProperties",
263
+ "required",
264
+ ].some((keyword) => keyword in schema)) {
265
+ return true;
266
+ }
267
+ for (const keyword of ["oneOf", "anyOf", "allOf"]) {
268
+ const branches = schema[keyword];
269
+ if (Array.isArray(branches) && branches.length > 0) {
270
+ return branches.every((branch) => isRecord(branch) &&
271
+ (branch.type === "object" || isProvablyObjectShaped(branch)));
272
+ }
273
+ }
274
+ return false;
275
+ }
276
+ function withoutMcpHeaders(value) {
277
+ if (Array.isArray(value))
278
+ return value.map(withoutMcpHeaders);
279
+ if (!isRecord(value))
280
+ return value;
281
+ const out = {};
282
+ for (const [key, member] of Object.entries(value)) {
283
+ if (key !== MCP_HEADER_KEYWORD)
284
+ out[key] = withoutMcpHeaders(member);
285
+ }
286
+ return out;
287
+ }
288
+ /** Run a tool schema's validator. */
289
+ export async function validateWithSchema(schema, value) {
290
+ const result = isPlainToolSchema(schema)
291
+ ? await schema.validate(value)
292
+ : await schema["~standard"].validate(value);
293
+ if (result.issues) {
294
+ return { ok: false, issues: result.issues.map(toIssue) };
295
+ }
296
+ return { ok: true, value: result.value };
297
+ }
298
+ function toIssue(issue) {
299
+ const path = (issue.path ?? [])
300
+ .map((segment) => typeof segment === "object" && segment !== null ? segment.key : segment)
301
+ .filter((key) => typeof key === "string" || typeof key === "number");
302
+ return {
303
+ message: String(issue.message),
304
+ ...(path.length > 0 ? { path } : {}),
305
+ };
306
+ }
307
+ /**
308
+ * Turn a handler's return value into a tool result. With an output schema,
309
+ * the value must validate and is sent as structured content as well as text.
310
+ */
311
+ export async function toToolResult(definition, value) {
312
+ if (isToolResult(value)) {
313
+ const structured = definition.output
314
+ ? (await toToolResult(definition, value.structured)).structuredContent
315
+ : value.structured;
316
+ if (!isRecord(structured) || Array.isArray(structured)) {
317
+ throw new Error("structured results must be JSON objects");
318
+ }
319
+ return {
320
+ content: [{ type: "text", text: value.text }],
321
+ structuredContent: structured,
322
+ };
323
+ }
324
+ if (definition.output) {
325
+ const checked = await validateWithSchema(definition.output, value);
326
+ if (!checked.ok) {
327
+ throw new Error(`result does not match the output schema: ${checked.issues
328
+ .map((issue) => issue.message)
329
+ .join("; ")}`);
330
+ }
331
+ if (!isRecord(checked.value) ||
332
+ Array.isArray(checked.value) ||
333
+ typeof checked.value === "function") {
334
+ throw new Error("structured results must be JSON objects");
335
+ }
336
+ return {
337
+ content: [{ type: "text", text: JSON.stringify(checked.value) }],
338
+ structuredContent: checked.value,
339
+ };
340
+ }
341
+ if (typeof value === "string") {
342
+ return { content: [{ type: "text", text: value }] };
343
+ }
344
+ if (value === undefined)
345
+ return { content: [] };
346
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
347
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The app side of the internal tools contract (../internal/tools.ts).
3
+ *
4
+ * `everyApp()` answers the two reserved paths here, before any app code runs
5
+ * and whether or not the app defines tools:
6
+ *
7
+ * - `/__everyapp/tools/list` needs the gateway's system token for this app
8
+ * (purpose "tools.list"). It describes tools; it never runs them.
9
+ * - `/__everyapp/tools/call` needs an identity token minted for this exact
10
+ * call: `chan: "mcp"` plus a `tc` claim naming the tool and call id. The
11
+ * token's scopes must include the scope the tool's risk requires; `*` does
12
+ * not count, because the ToolBus only ever mints explicit scopes.
13
+ *
14
+ * The perimeter also refuses these paths from outside and strips inbound
15
+ * `x-everyapp-*` headers, so only the gateway's ToolBus ever reaches them.
16
+ */
17
+ import { type RequestIdentityOptions } from "../internal/index.js";
18
+ import type { ExecutionContextLike } from "./everyApp.js";
19
+ import { type ToolSet } from "./tools.js";
20
+ export interface ToolsEndpointOptions {
21
+ toolSet?: ToolSet;
22
+ /** This app's id, for the masked-error label. */
23
+ appId: string;
24
+ /** How to verify gateway-minted tokens (keys, issuer, dev opt-in). */
25
+ identity: RequestIdentityOptions;
26
+ }
27
+ /**
28
+ * Answer a request to the reserved tools paths, or return null for any other
29
+ * path so the app's own handler runs.
30
+ */
31
+ export declare function handleToolsRequest(request: Request, env: unknown, ctx: ExecutionContextLike, options: ToolsEndpointOptions): Promise<Response | null>;
32
+ //# sourceMappingURL=toolsEndpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolsEndpoint.d.ts","sourceRoot":"","sources":["../../src/server/toolsEndpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAcL,KAAK,sBAAsB,EAO5B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAOL,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAoFD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyB1B"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The app side of the internal tools contract (../internal/tools.ts).
3
+ *
4
+ * `everyApp()` answers the two reserved paths here, before any app code runs
5
+ * and whether or not the app defines tools:
6
+ *
7
+ * - `/__everyapp/tools/list` needs the gateway's system token for this app
8
+ * (purpose "tools.list"). It describes tools; it never runs them.
9
+ * - `/__everyapp/tools/call` needs an identity token minted for this exact
10
+ * call: `chan: "mcp"` plus a `tc` claim naming the tool and call id. The
11
+ * token's scopes must include the scope the tool's risk requires; `*` does
12
+ * not count, because the ToolBus only ever mints explicit scopes.
13
+ *
14
+ * The perimeter also refuses these paths from outside and strips inbound
15
+ * `x-everyapp-*` headers, so only the gateway's ToolBus ever reaches them.
16
+ */
17
+ import { ConfigurationError, IDENTITY_HEADER, IdentityError, SYSTEM_HEADER, TOOLS_CALL_PATH, TOOLS_LIST_PATH, TOOLS_PROTOCOL_VERSION, catalogHash, requiredScopeForRisk, resolveVerifyOptions, verifyIdentityJwt, verifySystemJwt, } from "../internal/index.js";
18
+ import { SDK_VERSION, ToolError, toToolResult, toolSetInternals, validateWithSchema, } from "./tools.js";
19
+ function toolsJson(status, body, headers = {}) {
20
+ return Response.json(body, {
21
+ status,
22
+ headers: { "cache-control": "no-store", ...headers },
23
+ });
24
+ }
25
+ function toolsError(status, code, message, extra = {}) {
26
+ return toolsJson(status, {
27
+ v: TOOLS_PROTOCOL_VERSION,
28
+ error: {
29
+ code,
30
+ message,
31
+ ...(extra.issues ? { issues: extra.issues } : {}),
32
+ },
33
+ }, extra.headers);
34
+ }
35
+ async function readJsonBody(request) {
36
+ try {
37
+ return await request.json();
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function isPlainObject(value) {
44
+ return (typeof value === "object" &&
45
+ value !== null &&
46
+ !Array.isArray(value) &&
47
+ Object.getPrototypeOf(value) === Object.prototype);
48
+ }
49
+ let emptyCatalog;
50
+ /**
51
+ * The app's catalog, or the contract's "misconfigured" answer when it can't
52
+ * be built at all, so the deploy shows the developer what to fix. (A single
53
+ * tool that can't be described is left out of the catalog with a warning.)
54
+ */
55
+ async function catalogOrError(toolSet) {
56
+ try {
57
+ return await catalogFor(toolSet);
58
+ }
59
+ catch (error) {
60
+ console.error(error);
61
+ return toolsError(500, "misconfigured", error instanceof Error ? error.message : String(error));
62
+ }
63
+ }
64
+ function catalogFor(toolSet) {
65
+ if (toolSet)
66
+ return toolSetInternals(toolSet).catalog();
67
+ // An app without tools still answers the contract, with an empty catalog,
68
+ // so the gateway can tell "no tools" from "no contract".
69
+ emptyCatalog ??= catalogHash({ tools: [] }).then((hash) => ({
70
+ tools: [],
71
+ hash,
72
+ warnings: [],
73
+ }));
74
+ return emptyCatalog;
75
+ }
76
+ /**
77
+ * Answer a request to the reserved tools paths, or return null for any other
78
+ * path so the app's own handler runs.
79
+ */
80
+ export async function handleToolsRequest(request, env, ctx, options) {
81
+ const { pathname } = new URL(request.url);
82
+ if (pathname !== TOOLS_LIST_PATH && pathname !== TOOLS_CALL_PATH) {
83
+ return null;
84
+ }
85
+ if (request.method !== "POST") {
86
+ return toolsError(405, "method_not_allowed", "Use POST.", {
87
+ headers: { allow: "POST" },
88
+ });
89
+ }
90
+ let verify;
91
+ try {
92
+ verify = resolveVerifyOptions(options.identity);
93
+ }
94
+ catch (error) {
95
+ if (error instanceof ConfigurationError) {
96
+ console.error(error);
97
+ return toolsError(500, "misconfigured", error.message);
98
+ }
99
+ throw error;
100
+ }
101
+ return pathname === TOOLS_LIST_PATH
102
+ ? listTools(request, verify, options)
103
+ : callTool(request, env, ctx, verify, options);
104
+ }
105
+ async function listTools(request, verify, options) {
106
+ try {
107
+ await verifySystemJwt(request.headers.get(SYSTEM_HEADER) ?? "", {
108
+ ...verify,
109
+ purpose: "tools.list",
110
+ });
111
+ }
112
+ catch (error) {
113
+ if (error instanceof IdentityError) {
114
+ return toolsError(401, "unauthenticated", error.message);
115
+ }
116
+ throw error;
117
+ }
118
+ const body = await readJsonBody(request);
119
+ if (!isPlainObject(body) || body.v !== TOOLS_PROTOCOL_VERSION) {
120
+ return toolsError(400, "invalid_request", `Expected {"v":${TOOLS_PROTOCOL_VERSION}}.`);
121
+ }
122
+ const catalog = await catalogOrError(options.toolSet);
123
+ if (catalog instanceof Response)
124
+ return catalog;
125
+ const response = {
126
+ v: TOOLS_PROTOCOL_VERSION,
127
+ sdk: SDK_VERSION,
128
+ hash: catalog.hash,
129
+ ...(catalog.instructions !== undefined
130
+ ? { instructions: catalog.instructions }
131
+ : {}),
132
+ tools: catalog.tools,
133
+ ...(catalog.warnings.length > 0 ? { warnings: catalog.warnings } : {}),
134
+ };
135
+ return toolsJson(200, response);
136
+ }
137
+ function parseCallRequest(body) {
138
+ if (!isPlainObject(body) || body.v !== TOOLS_PROTOCOL_VERSION)
139
+ return null;
140
+ const { name, callId } = body;
141
+ const args = body.arguments ?? {};
142
+ if (typeof name !== "string" || !name)
143
+ return null;
144
+ if (typeof callId !== "string" || !callId)
145
+ return null;
146
+ if (!isPlainObject(args))
147
+ return null;
148
+ return { v: TOOLS_PROTOCOL_VERSION, name, arguments: args, callId };
149
+ }
150
+ async function callTool(request, env, ctx, verify, options) {
151
+ let user;
152
+ try {
153
+ user = await verifyIdentityJwt(request.headers.get(IDENTITY_HEADER) ?? "", verify);
154
+ }
155
+ catch (error) {
156
+ if (error instanceof IdentityError) {
157
+ return toolsError(401, "unauthenticated", error.message);
158
+ }
159
+ throw error;
160
+ }
161
+ if (user.channel !== "mcp" || !user.toolCall) {
162
+ return toolsError(403, "forbidden", "Tool calls need an identity the gateway minted for this call.");
163
+ }
164
+ const call = parseCallRequest(await readJsonBody(request));
165
+ if (!call) {
166
+ return toolsError(400, "invalid_request", `Expected {"v":${TOOLS_PROTOCOL_VERSION},"name","arguments","callId"}.`);
167
+ }
168
+ if (call.name !== user.toolCall.name || call.callId !== user.toolCall.id) {
169
+ return toolsError(403, "forbidden", "The identity was minted for a different tool call.");
170
+ }
171
+ const tool = options.toolSet
172
+ ? toolSetInternals(options.toolSet).tool(call.name)
173
+ : undefined;
174
+ if (!tool) {
175
+ return toolsError(404, "unknown_tool", `Unknown tool: ${call.name}`);
176
+ }
177
+ const scope = requiredScopeForRisk(tool.risk);
178
+ if (!user.scopes.includes(scope)) {
179
+ return toolsError(403, "forbidden", `Calling ${call.name} needs the ${scope} scope.`);
180
+ }
181
+ const input = await validateWithSchema(tool.input, call.arguments);
182
+ if (!input.ok) {
183
+ return toolsError(400, "invalid_arguments", `Invalid arguments for ${call.name}: ${input.issues
184
+ .map((issue) => issue.path?.length
185
+ ? `${issue.path.join(".")}: ${issue.message}`
186
+ : issue.message)
187
+ .join("; ")}`, { issues: input.issues });
188
+ }
189
+ // The hash only lets the gateway notice a newer catalog: a catalog that
190
+ // can't be built is logged here, and doesn't stop this tool from running.
191
+ const hash = await catalogFor(options.toolSet).then((catalog) => catalog.hash, (error) => {
192
+ console.error(error);
193
+ return "";
194
+ });
195
+ let result;
196
+ try {
197
+ const value = await tool.handler(input.value, {
198
+ user,
199
+ env: env,
200
+ ctx,
201
+ request,
202
+ callId: call.callId,
203
+ });
204
+ result = await toToolResult(tool, value);
205
+ }
206
+ catch (error) {
207
+ if (error instanceof ToolError) {
208
+ result = {
209
+ content: [{ type: "text", text: error.message }],
210
+ isError: true,
211
+ };
212
+ }
213
+ else {
214
+ console.error(`[everyapp] tool "${call.name}" failed (ref ${call.callId}):`, error);
215
+ result = {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: `Internal error in ${options.appId} (ref ${call.callId})`,
220
+ },
221
+ ],
222
+ isError: true,
223
+ };
224
+ }
225
+ }
226
+ return toolsJson(200, {
227
+ v: TOOLS_PROTOCOL_VERSION,
228
+ catalogHash: hash,
229
+ result,
230
+ });
231
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-app/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-rc.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,34 +11,47 @@
11
11
  "./internal": {
12
12
  "types": "./dist/internal/index.d.ts",
13
13
  "default": "./dist/internal/index.js"
14
- }
14
+ },
15
+ "./cloudflare": {
16
+ "types": "./dist/cloudflare/index.d.ts",
17
+ "default": "./dist/cloudflare/index.js"
18
+ },
19
+ "./cloudflare/sentinel": {
20
+ "types": "./dist/cloudflare/sentinel.d.ts",
21
+ "default": "./dist/cloudflare/sentinel.js"
22
+ },
23
+ "./dev": {
24
+ "types": "./dist/dev/index.d.ts",
25
+ "default": "./dist/dev/index.js"
26
+ },
27
+ "./package.json": "./package.json"
15
28
  },
16
29
  "files": [
17
30
  "dist",
18
31
  "README.md"
19
32
  ],
20
- "peerDependencies": {
21
- "jose": "^6.0.0"
22
- },
23
- "devDependencies": {
24
- "@cloudflare/workers-types": "^4.20251014.0",
25
- "@types/node": "^22.18.13",
26
- "jose": "^6.0.12",
27
- "prettier": "^3.6.2",
28
- "typescript": "^5.9.3",
29
- "vite": "^7.1.2",
30
- "vite-tsconfig-paths": "^5.1.4",
31
- "vitest": "^3.2.4"
32
- },
33
33
  "scripts": {
34
34
  "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
35
35
  "build": "tsc -p tsconfig.build.json",
36
- "build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
37
36
  "types:check": "tsc --noEmit",
38
37
  "format:check": "prettier --check .",
39
38
  "format:write": "prettier . --write",
40
39
  "test": "vitest run --silent",
41
40
  "test:watch": "vitest",
42
41
  "check:esm-relative-imports": "node ./scripts/check-esm-relative-imports.mjs"
42
+ },
43
+ "peerDependencies": {
44
+ "jose": "^6.2.5"
45
+ },
46
+ "devDependencies": {
47
+ "@cloudflare/workers-types": "^4.20251014.0",
48
+ "@types/node": "^22.18.13",
49
+ "jose": "^6.2.12",
50
+ "prettier": "^3.6.2",
51
+ "typescript": "^5.9.3",
52
+ "vite": "^7.1.2",
53
+ "vite-tsconfig-paths": "^5.1.4",
54
+ "vitest": "^3.2.6",
55
+ "zod": "^4.2.0"
43
56
  }
44
- }
57
+ }