@every-app/sdk 0.2.0 → 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 +608 -2
- package/dist/cloudflare/index.d.ts +22 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +21 -0
- package/dist/cloudflare/sentinel.d.ts +69 -0
- package/dist/cloudflare/sentinel.d.ts.map +1 -0
- package/dist/cloudflare/sentinel.js +50 -0
- package/dist/cloudflare/worker.d.ts +592 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +817 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +7 -0
- package/dist/dev/keypair.d.ts +24 -0
- package/dist/dev/keypair.d.ts.map +1 -0
- package/dist/dev/keypair.js +60 -0
- package/dist/internal/aiProviders.d.ts +67 -0
- package/dist/internal/aiProviders.d.ts.map +1 -0
- package/dist/internal/aiProviders.js +220 -0
- package/dist/internal/appSettings.d.ts +237 -0
- package/dist/internal/appSettings.d.ts.map +1 -0
- package/dist/internal/appSettings.js +374 -0
- package/dist/internal/capabilities.d.ts +3249 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +1532 -0
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +338 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +477 -0
- package/dist/internal/catalog/index.d.ts +1899 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +356 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +425 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +49 -0
- package/dist/internal/devProviders.d.ts.map +1 -0
- package/dist/internal/devProviders.js +62 -0
- package/dist/internal/email.d.ts +280 -0
- package/dist/internal/email.d.ts.map +1 -0
- package/dist/internal/email.js +613 -0
- package/dist/internal/events.d.ts +182 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +326 -0
- package/dist/internal/gatewaySettings.d.ts +83 -0
- package/dist/internal/gatewaySettings.d.ts.map +1 -0
- package/dist/internal/gatewaySettings.js +217 -0
- package/dist/internal/identity.d.ts +18 -3
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +53 -14
- package/dist/internal/index.d.ts +18 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +18 -1
- package/dist/internal/members.d.ts +35 -0
- package/dist/internal/members.d.ts.map +1 -0
- package/dist/internal/members.js +7 -0
- package/dist/internal/oauthClient.d.ts +63 -0
- package/dist/internal/oauthClient.d.ts.map +1 -0
- package/dist/internal/oauthClient.js +194 -0
- package/dist/internal/oauthScopes.d.ts +117 -0
- package/dist/internal/oauthScopes.d.ts.map +1 -0
- package/dist/internal/oauthScopes.js +416 -0
- package/dist/internal/organizationName.d.ts +18 -0
- package/dist/internal/organizationName.d.ts.map +1 -0
- package/dist/internal/organizationName.js +24 -0
- package/dist/internal/protocol.d.ts +86 -2
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +112 -1
- package/dist/internal/providerApps.d.ts +75 -0
- package/dist/internal/providerApps.d.ts.map +1 -0
- package/dist/internal/providerApps.js +59 -0
- package/dist/internal/providerRegistry.d.ts +147 -0
- package/dist/internal/providerRegistry.d.ts.map +1 -0
- package/dist/internal/providerRegistry.js +266 -0
- package/dist/internal/providerSpec.d.ts +55 -0
- package/dist/internal/providerSpec.d.ts.map +1 -0
- package/dist/internal/providerSpec.js +758 -0
- package/dist/internal/remoteMcp.d.ts +188 -0
- package/dist/internal/remoteMcp.d.ts.map +1 -0
- package/dist/internal/remoteMcp.js +543 -0
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +238 -0
- package/dist/internal/tools.d.ts +115 -0
- package/dist/internal/tools.d.ts.map +1 -0
- package/dist/internal/tools.js +80 -0
- package/dist/server/attribution.d.ts +39 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +67 -0
- package/dist/server/authorizeUrl.d.ts +43 -0
- package/dist/server/authorizeUrl.d.ts.map +1 -0
- package/dist/server/authorizeUrl.js +110 -0
- package/dist/server/emit.d.ts +63 -0
- package/dist/server/emit.d.ts.map +1 -0
- package/dist/server/emit.js +101 -0
- package/dist/server/everyApp.d.ts +38 -12
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +92 -28
- package/dist/server/gatewayFetch.d.ts +24 -8
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +81 -36
- package/dist/server/index.d.ts +10 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -1
- package/dist/server/integrationFetch.d.ts +131 -0
- package/dist/server/integrationFetch.d.ts.map +1 -0
- package/dist/server/integrationFetch.js +526 -0
- package/dist/server/listMembers.d.ts +32 -0
- package/dist/server/listMembers.d.ts.map +1 -0
- package/dist/server/listMembers.js +74 -0
- package/dist/server/nodeServer.d.ts +48 -0
- package/dist/server/nodeServer.d.ts.map +1 -0
- package/dist/server/nodeServer.js +45 -0
- package/dist/server/sendEmail.d.ts +39 -0
- package/dist/server/sendEmail.d.ts.map +1 -0
- package/dist/server/sendEmail.js +114 -0
- package/dist/server/tools.d.ts +184 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +347 -0
- package/dist/server/toolsEndpoint.d.ts +32 -0
- package/dist/server/toolsEndpoint.d.ts.map +1 -0
- package/dist/server/toolsEndpoint.js +231 -0
- package/package.json +19 -6
- package/dist/server/mcp.d.ts +0 -66
- package/dist/server/mcp.d.ts.map +0 -1
- 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.
|
|
3
|
+
"version": "0.3.0-rc.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,29 +11,42 @@
|
|
|
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
33
|
"peerDependencies": {
|
|
21
|
-
"jose": "^6.
|
|
34
|
+
"jose": "^6.2.5"
|
|
22
35
|
},
|
|
23
36
|
"devDependencies": {
|
|
24
37
|
"@cloudflare/workers-types": "^4.20251014.0",
|
|
25
38
|
"@types/node": "^22.18.13",
|
|
26
|
-
"jose": "^6.
|
|
39
|
+
"jose": "^6.2.12",
|
|
27
40
|
"prettier": "^3.6.2",
|
|
28
41
|
"typescript": "^5.9.3",
|
|
29
42
|
"vite": "^7.1.2",
|
|
30
43
|
"vite-tsconfig-paths": "^5.1.4",
|
|
31
|
-
"vitest": "^3.2.
|
|
44
|
+
"vitest": "^3.2.6",
|
|
45
|
+
"zod": "^4.2.0"
|
|
32
46
|
},
|
|
33
47
|
"scripts": {
|
|
34
48
|
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
35
49
|
"build": "tsc -p tsconfig.build.json",
|
|
36
|
-
"build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
|
|
37
50
|
"types:check": "tsc --noEmit",
|
|
38
51
|
"format:check": "prettier --check .",
|
|
39
52
|
"format:write": "prettier . --write",
|