@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,543 @@
1
+ /**
2
+ * Remote MCP apps: any remote MCP server registered on a gateway by URL
3
+ * (`everyapp mcp add <url>`, Admin → Apps → Add app → "Add an MCP server",
4
+ * `everyapp_mcp_add`), served to members' AI clients as an MCP app of the
5
+ * Gateway's own, like a built-in provider app (./providerApps.ts) but with
6
+ * its tools read from the server and each call forwarded to it with a
7
+ * credential the Gateway holds (docs/design/2026-10-mcp-by-url.md).
8
+ *
9
+ * The registration record is the app row's manifest: `id`, `name`,
10
+ * `description` and a `remote` block ({@link RemoteMcpBlock}): the one URL
11
+ * the Gateway ever fetches, how it authenticates ({@link RemoteMcpAuth},
12
+ * the credential always the NAME of a Gateway setting, never a value), how
13
+ * a tool's risk is decided when the server's annotations don't say, the
14
+ * operator's per-tool overrides, and a revision the writers bump so the
15
+ * Gateway re-lists the tools on first use after a change.
16
+ *
17
+ * Everything here is pure and shared by the CLI, the perimeter's registry
18
+ * rules, the Gateway (which serves the row and validates again at every
19
+ * fetch) and the settings model (./appSettings.ts), so the rules cannot
20
+ * drift: what the URL may be, what the credential may be called, how a
21
+ * server's tool names become the ToolBus's, and what risk a tool gets.
22
+ */
23
+ import { CAPABILITIES } from "./capabilities.js";
24
+ import { isRemoteMcpSecretName } from "./gatewaySettings.js";
25
+ import { isProviderAppId } from "./providerApps.js";
26
+ import { TOOL_NAME_PATTERN, isToolRisk } from "./tools.js";
27
+ export const REMOTE_MCP_AUTH_KINDS = [
28
+ "none",
29
+ "bearer",
30
+ "header",
31
+ "basic",
32
+ "oauth",
33
+ ];
34
+ /**
35
+ * How a tool's risk is decided for the app. `annotations`: from the
36
+ * server's hints ({@link remoteToolRisk}); `read` or `write`: every tool,
37
+ * whatever it says. A per-tool override wins over all three.
38
+ */
39
+ export const REMOTE_MCP_RISK_DEFAULTS = [
40
+ "annotations",
41
+ "read",
42
+ "write",
43
+ ];
44
+ const ID_PATTERN = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
45
+ const ID_MAX_LENGTH = 32;
46
+ /** App ids a remote server can't take: the Gateway's own tool prefix. */
47
+ const RESERVED_IDS = ["everyapp"];
48
+ const SECRET_MAX_LENGTH = 64;
49
+ const HEADER_NAME_PATTERN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,100}$/;
50
+ const PREFIX_MAX_LENGTH = 32;
51
+ const URL_MAX_LENGTH = 2048;
52
+ const NAME_MAX_LENGTH = 255;
53
+ const DESCRIPTION_MAX_LENGTH = 1000;
54
+ const OVERRIDES_MAX = 500;
55
+ const UPSTREAM_NAME_MAX = 256;
56
+ /**
57
+ * Headers the `header` kind may not name: the ones the Gateway sets on the
58
+ * request, the ones that route it, and the ones that would carry something
59
+ * other than this app's credential.
60
+ */
61
+ const FORBIDDEN_AUTH_HEADERS = new Set([
62
+ "authorization",
63
+ "cookie",
64
+ "host",
65
+ "content-type",
66
+ "content-length",
67
+ "accept",
68
+ "connection",
69
+ "transfer-encoding",
70
+ "mcp-session-id",
71
+ "mcp-protocol-version",
72
+ "mcp-method",
73
+ "mcp-name",
74
+ ]);
75
+ /** The default credential name for an app id: `treg` → `TREG_MCP_TOKEN`. */
76
+ export function remoteMcpSecretName(id) {
77
+ return `${id.toUpperCase().replace(/-/g, "_")}_MCP_TOKEN`;
78
+ }
79
+ /** Whether `id` can be a remote MCP app's id (kebab-case, at most 32, not reserved, not a built-in's). */
80
+ export function remoteMcpIdProblem(id) {
81
+ if (typeof id !== "string" || !ID_PATTERN.test(id)) {
82
+ return "the id must be kebab-case: lowercase letters, digits and hyphens, starting with a letter";
83
+ }
84
+ if (id.length > ID_MAX_LENGTH) {
85
+ return `the id must be at most ${ID_MAX_LENGTH} characters`;
86
+ }
87
+ if (RESERVED_IDS.includes(id))
88
+ return `"${id}" is reserved`;
89
+ if (isProviderAppId(id)) {
90
+ return `"${id}" is the Gateway's built-in MCP app; a remote server can't take its id`;
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * An app id from the server's URL: the host's registrable name, `www`,
96
+ * `mcp` and `api` dropped (`https://mcp.deepwiki.com/mcp` → `deepwiki`,
97
+ * `https://treg.to/mcp/` → `treg`); null when nothing usable is left (an
98
+ * IP, `localhost`), in which case the caller asks for `--name`.
99
+ */
100
+ export function remoteMcpIdFromUrl(url) {
101
+ let host;
102
+ try {
103
+ host = new URL(url).hostname.toLowerCase().replace(/\.$/, "");
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ if (!host || /^[\d.]+$/.test(host) || host.includes(":"))
109
+ return null;
110
+ const labels = host.split(".").filter((label) => label.length > 0);
111
+ if (labels.length < 2)
112
+ return null;
113
+ // The label before the public suffix, roughly: the last label that
114
+ // isn't a known two-level suffix part (co.uk, com.au).
115
+ const SECOND_LEVEL = new Set(["co", "com", "net", "org", "gov", "edu", "ac"]);
116
+ let index = labels.length - 2;
117
+ if (index > 0 &&
118
+ SECOND_LEVEL.has(labels[index]) &&
119
+ labels[index + 1].length === 2) {
120
+ index -= 1;
121
+ }
122
+ const candidate = labels[index] ?? "";
123
+ const id = candidate
124
+ .replace(/[^a-z0-9-]/g, "-")
125
+ .replace(/^-+|-+$/g, "")
126
+ .replace(/-{2,}/g, "-")
127
+ .slice(0, ID_MAX_LENGTH)
128
+ .replace(/-+$/g, "");
129
+ if (!id || !ID_PATTERN.test(id))
130
+ return null;
131
+ return remoteMcpIdProblem(id) ? null : id;
132
+ }
133
+ function isSameOrSubdomain(host, domain) {
134
+ return host === domain || host.endsWith(`.${domain}`);
135
+ }
136
+ /** The hosts whose keys the Gateway holds: a remote server is never one of them. */
137
+ export function reservedUpstreamHosts() {
138
+ const hosts = new Set([
139
+ "api.openai.com",
140
+ "openrouter.ai",
141
+ "gateway.ai.cloudflare.com",
142
+ "api.cloudflare.com",
143
+ ]);
144
+ for (const capability of Object.values(CAPABILITIES)) {
145
+ if (capability.mode === "api_key") {
146
+ for (const host of capability.hosts)
147
+ hosts.add(host);
148
+ }
149
+ else if (capability.mode === "oauth_client") {
150
+ for (const endpoint of capability.token) {
151
+ if ("host" in endpoint && typeof endpoint.host === "string") {
152
+ hosts.add(endpoint.host);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return [...hosts].sort();
158
+ }
159
+ /**
160
+ * Why `url` can't be a remote MCP server's address, or null when it can:
161
+ * `https:` (or `http:` and loopback hosts only while the Gateway itself
162
+ * runs on `http://`, local dev), a host name rather than an address, no
163
+ * credentials or fragment in it, not the Gateway's own origin or one of
164
+ * its app hosts, and not a provider whose key the Gateway holds. The
165
+ * Gateway checks the stored URL again before every fetch, so a row edited
166
+ * by hand is refused the same way.
167
+ */
168
+ export function remoteMcpUrlProblem(url, env) {
169
+ if (typeof url !== "string" || !url.trim())
170
+ return "give the server's URL";
171
+ if (url.length > URL_MAX_LENGTH)
172
+ return "the URL is too long";
173
+ let parsed;
174
+ try {
175
+ parsed = new URL(url);
176
+ }
177
+ catch {
178
+ return "the URL doesn't parse";
179
+ }
180
+ const local = env.gatewayUrl.trim().toLowerCase().startsWith("http://");
181
+ if (parsed.username || parsed.password) {
182
+ return "the URL must not carry credentials; the Gateway sends the credential from its own setting";
183
+ }
184
+ if (parsed.hash)
185
+ return "the URL must not have a fragment";
186
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
187
+ const loopback = host === "localhost" ||
188
+ host.endsWith(".localhost") ||
189
+ host === "127.0.0.1" ||
190
+ host === "[::1]" ||
191
+ host === "::1";
192
+ if (parsed.protocol !== "https:") {
193
+ if (parsed.protocol === "http:" && local && loopback) {
194
+ // Local dev: a fake or local server on this machine.
195
+ }
196
+ else {
197
+ return local
198
+ ? "the URL must be https://, or http:// on localhost while the Gateway runs locally"
199
+ : "the URL must be https://";
200
+ }
201
+ }
202
+ if (!host)
203
+ return "the URL has no host";
204
+ if (!loopback && (/^[\d.]+$/.test(host) || host.startsWith("["))) {
205
+ return "the URL must name a host, not an IP address";
206
+ }
207
+ if (loopback && !local) {
208
+ return "the URL must name a public host";
209
+ }
210
+ // The Gateway's own host: any port and every subdomain of it in
211
+ // production (a Worker on the same zone would be the gateway or an app);
212
+ // in local dev the exact origin only, so a fake server on another
213
+ // localhost port is fine.
214
+ let gatewayHost = "";
215
+ let gatewayOrigin = "";
216
+ try {
217
+ const gateway = new URL(env.gatewayUrl);
218
+ gatewayHost = gateway.hostname.toLowerCase().replace(/\.$/, "");
219
+ gatewayOrigin = gateway.origin.toLowerCase();
220
+ }
221
+ catch {
222
+ gatewayHost = "";
223
+ }
224
+ if (gatewayHost) {
225
+ const ownHost = local
226
+ ? parsed.origin.toLowerCase() === gatewayOrigin
227
+ : isSameOrSubdomain(host, gatewayHost);
228
+ if (ownHost)
229
+ return "the URL is this Gateway's own address";
230
+ }
231
+ for (const domain of [env.appBaseDomain, env.thirdPartyDomain]) {
232
+ const normalized = (domain ?? "").trim().toLowerCase().replace(/\.$/, "");
233
+ if (normalized && isSameOrSubdomain(host, normalized)) {
234
+ return `the URL is on this Gateway's app domain (${normalized})`;
235
+ }
236
+ }
237
+ if (reservedUpstreamHosts().includes(host)) {
238
+ return `${host} is a provider this Gateway holds a key for; apps reach it through the Gateway's capabilities, not as an MCP app`;
239
+ }
240
+ return null;
241
+ }
242
+ function isRecord(value) {
243
+ return typeof value === "object" && value !== null && !Array.isArray(value);
244
+ }
245
+ /** The credential name's problem, or null: in the family, and bounded. */
246
+ export function remoteMcpSecretProblem(name) {
247
+ if (typeof name !== "string" || !name)
248
+ return "give the credential's setting name";
249
+ if (name.length > SECRET_MAX_LENGTH)
250
+ return "the credential's name is too long";
251
+ if (!isRemoteMcpSecretName(name)) {
252
+ return `the credential's name must end in _MCP_TOKEN (letters, digits and underscores), like ${remoteMcpSecretName("treg")}`;
253
+ }
254
+ return null;
255
+ }
256
+ /** The auth block's problem, or the block as typed. */
257
+ export function parseRemoteMcpAuth(raw) {
258
+ if (!isRecord(raw) || typeof raw["kind"] !== "string") {
259
+ return { ok: false, problem: "auth must name a kind" };
260
+ }
261
+ const kind = raw["kind"];
262
+ if (!REMOTE_MCP_AUTH_KINDS.includes(kind)) {
263
+ return {
264
+ ok: false,
265
+ problem: `auth.kind must be one of ${REMOTE_MCP_AUTH_KINDS.join(", ")}`,
266
+ };
267
+ }
268
+ if (kind === "none" || kind === "oauth") {
269
+ return { ok: true, auth: { kind } };
270
+ }
271
+ const secretProblem = remoteMcpSecretProblem(raw["secret"]);
272
+ if (secretProblem)
273
+ return { ok: false, problem: `auth.secret: ${secretProblem}` };
274
+ const secret = raw["secret"];
275
+ if (kind === "bearer" || kind === "basic") {
276
+ return { ok: true, auth: { kind, secret } };
277
+ }
278
+ const header = raw["header"];
279
+ if (typeof header !== "string" || !HEADER_NAME_PATTERN.test(header)) {
280
+ return { ok: false, problem: "auth.header must be an HTTP header name" };
281
+ }
282
+ if (FORBIDDEN_AUTH_HEADERS.has(header.toLowerCase())) {
283
+ return {
284
+ ok: false,
285
+ problem: `auth.header can't be ${header}; use kind "bearer" or "basic" for Authorization`,
286
+ };
287
+ }
288
+ const prefix = raw["prefix"];
289
+ if (prefix !== undefined) {
290
+ if (typeof prefix !== "string" ||
291
+ prefix.length > PREFIX_MAX_LENGTH ||
292
+ /[\r\n]/.test(prefix)) {
293
+ return { ok: false, problem: "auth.prefix must be a short string" };
294
+ }
295
+ }
296
+ return {
297
+ ok: true,
298
+ auth: {
299
+ kind: "header",
300
+ secret,
301
+ header,
302
+ ...(typeof prefix === "string" && prefix ? { prefix } : {}),
303
+ },
304
+ };
305
+ }
306
+ /**
307
+ * The `remote` block of a stored manifest, checked field by field (a row
308
+ * edited by hand must not make the Gateway send a credential anywhere the
309
+ * rules refuse), or why it can't be read. The URL is checked against the
310
+ * Gateway's environment by the caller ({@link remoteMcpUrlProblem}).
311
+ */
312
+ export function parseRemoteMcpBlock(raw) {
313
+ if (!isRecord(raw))
314
+ return { ok: false, problem: "remote must be an object" };
315
+ const url = raw["url"];
316
+ if (typeof url !== "string" || !url || url.length > URL_MAX_LENGTH) {
317
+ return { ok: false, problem: "remote.url must be a URL" };
318
+ }
319
+ const auth = parseRemoteMcpAuth(raw["auth"]);
320
+ if (!auth.ok)
321
+ return auth;
322
+ const risk = raw["risk"] ?? "annotations";
323
+ if (!REMOTE_MCP_RISK_DEFAULTS.includes(risk)) {
324
+ return {
325
+ ok: false,
326
+ problem: `remote.risk must be one of ${REMOTE_MCP_RISK_DEFAULTS.join(", ")}`,
327
+ };
328
+ }
329
+ const tools = {};
330
+ const rawTools = raw["tools"] ?? {};
331
+ if (!isRecord(rawTools))
332
+ return { ok: false, problem: "remote.tools must be an object" };
333
+ const entries = Object.entries(rawTools);
334
+ if (entries.length > OVERRIDES_MAX) {
335
+ return {
336
+ ok: false,
337
+ problem: `remote.tools has more than ${OVERRIDES_MAX} entries`,
338
+ };
339
+ }
340
+ for (const [name, override] of entries) {
341
+ if (!name || name.length > UPSTREAM_NAME_MAX) {
342
+ return {
343
+ ok: false,
344
+ problem: "remote.tools has a tool name that is empty or too long",
345
+ };
346
+ }
347
+ if (!isRecord(override)) {
348
+ return { ok: false, problem: `remote.tools.${name} must be an object` };
349
+ }
350
+ const out = {};
351
+ if (override["risk"] !== undefined) {
352
+ if (!isToolRisk(override["risk"])) {
353
+ return {
354
+ ok: false,
355
+ problem: `remote.tools.${name}.risk must be read, write or destructive`,
356
+ };
357
+ }
358
+ out.risk = override["risk"];
359
+ }
360
+ if (override["enabled"] !== undefined) {
361
+ if (typeof override["enabled"] !== "boolean") {
362
+ return {
363
+ ok: false,
364
+ problem: `remote.tools.${name}.enabled must be true or false`,
365
+ };
366
+ }
367
+ out.enabled = override["enabled"];
368
+ }
369
+ tools[name] = out;
370
+ }
371
+ const rev = raw["rev"] ?? 1;
372
+ if (typeof rev !== "number" || !Number.isInteger(rev) || rev < 1) {
373
+ return { ok: false, problem: "remote.rev must be a positive integer" };
374
+ }
375
+ return {
376
+ ok: true,
377
+ block: {
378
+ url,
379
+ auth: auth.auth,
380
+ risk: risk,
381
+ tools,
382
+ rev,
383
+ },
384
+ };
385
+ }
386
+ /**
387
+ * The `remote` block of a stored manifest, or null when the manifest has
388
+ * none (a built-in provider app's, a deployed app's) or it can't be read
389
+ * (such a row is a provider row the Gateway serves nothing for and lists
390
+ * as needing setup, see app-setup).
391
+ */
392
+ export function remoteMcpOf(manifest) {
393
+ if (!isRecord(manifest) || manifest["remote"] === undefined)
394
+ return null;
395
+ const parsed = parseRemoteMcpBlock(manifest["remote"]);
396
+ return parsed.ok ? parsed.block : null;
397
+ }
398
+ /** Whether a stored manifest declares a remote MCP server at all (well-formed or not). */
399
+ export function hasRemoteMcpBlock(manifest) {
400
+ return isRecord(manifest) && manifest["remote"] !== undefined;
401
+ }
402
+ /** The credential's setting name the block declares, if its kind takes one. */
403
+ export function remoteMcpSecretOf(auth) {
404
+ return "secret" in auth ? auth.secret : null;
405
+ }
406
+ /**
407
+ * A description for a row that gave none: the server's origin, read as a
408
+ * sentence. The origin only: a path or query may carry something the
409
+ * registrar meant for the server alone, and the description reaches every
410
+ * member with access and every AI client.
411
+ */
412
+ export function remoteMcpDefaultDescription(name, url) {
413
+ let where = "";
414
+ try {
415
+ where = new URL(url).origin;
416
+ }
417
+ catch {
418
+ where = "";
419
+ }
420
+ return `${name}'s MCP server${where ? ` at ${where}` : ""}, served to your AI clients through this Gateway.`;
421
+ }
422
+ /**
423
+ * The operator's override for a server tool: by the server's name, or by
424
+ * a name that becomes the same ToolBus name ({@link normalizeRemoteToolName}),
425
+ * so a server that renames `send-mail` to `send_mail` can't bring a tool the
426
+ * operator turned off back under the same listed name.
427
+ */
428
+ export function remoteToolOverride(block, upstreamName) {
429
+ const exact = block.tools[upstreamName];
430
+ if (exact)
431
+ return exact;
432
+ const local = normalizeRemoteToolName(upstreamName);
433
+ if (!local)
434
+ return undefined;
435
+ for (const [key, override] of Object.entries(block.tools)) {
436
+ if (normalizeRemoteToolName(key) === local)
437
+ return override;
438
+ }
439
+ return undefined;
440
+ }
441
+ /** Whether a server's tool name can travel in an HTTP header (`Mcp-Name`): visible ASCII, bounded. */
442
+ export function isHeaderSafeToolName(name) {
443
+ return /^[\x21-\x7e]{1,256}$/.test(name);
444
+ }
445
+ /** The manifest a remote MCP app's row stores, from what the registrar gave. */
446
+ export function remoteMcpAppManifest(input) {
447
+ const name = (input.name ?? "").trim().slice(0, NAME_MAX_LENGTH) || input.id;
448
+ const description = (input.description ?? "").trim().slice(0, DESCRIPTION_MAX_LENGTH) ||
449
+ remoteMcpDefaultDescription(name, input.remote.url);
450
+ return { id: input.id, name, description, remote: input.remote };
451
+ }
452
+ /**
453
+ * A tool's risk for the Gateway. An override wins; then the app's `read`
454
+ * or `write` default; then the server's hints, read conservatively: an
455
+ * explicit `readOnlyHint: true` is `read`, an explicit `destructiveHint:
456
+ * true` is `destructive` (hidden until approvals exist), and anything
457
+ * else, a missing hint included, is `write`. The spec's missing-means-true
458
+ * default for `destructiveHint` is not followed: it would hide every
459
+ * unhinted tool.
460
+ */
461
+ export function remoteToolRisk(hints, block, upstreamName) {
462
+ const override = remoteToolOverride(block, upstreamName)?.risk;
463
+ if (override)
464
+ return { risk: override, source: "override" };
465
+ if (block.risk === "read" || block.risk === "write") {
466
+ return { risk: block.risk, source: "app" };
467
+ }
468
+ if (hints?.readOnlyHint === true)
469
+ return { risk: "read", source: "hint" };
470
+ if (hints?.destructiveHint === true) {
471
+ return { risk: "destructive", source: "hint" };
472
+ }
473
+ return { risk: "write", source: "default" };
474
+ }
475
+ /** Whether the operator left the tool on (the default). */
476
+ export function remoteToolEnabled(block, upstreamName) {
477
+ return remoteToolOverride(block, upstreamName)?.enabled !== false;
478
+ }
479
+ /**
480
+ * A server's tool name as the ToolBus lists it: lowercase snake_case, at
481
+ * most 48 characters (`TOOL_NAME_PATTERN`). camelCase is split
482
+ * (`getUser` → `get_user`), `-`, `.`, `/` and spaces become `_`, anything
483
+ * else is dropped, runs of `_` collapse, and a leading digit gets a `t_`
484
+ * in front. Null when nothing usable is left.
485
+ */
486
+ export function normalizeRemoteToolName(name) {
487
+ if (typeof name !== "string")
488
+ return null;
489
+ let out = name
490
+ .trim()
491
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
492
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
493
+ .toLowerCase()
494
+ .replace(/[\s./:-]+/g, "_")
495
+ .replace(/[^a-z0-9_]/g, "")
496
+ .replace(/_{2,}/g, "_")
497
+ .replace(/^_+|_+$/g, "");
498
+ if (!out)
499
+ return null;
500
+ if (/^[0-9]/.test(out))
501
+ out = `t_${out}`;
502
+ out = out.slice(0, 48).replace(/_+$/g, "");
503
+ return TOOL_NAME_PATTERN.test(out) ? out : null;
504
+ }
505
+ /**
506
+ * The request headers that carry the credential for `auth`, from the
507
+ * setting's value. The value never appears anywhere else.
508
+ */
509
+ export function remoteMcpAuthHeaders(auth, value) {
510
+ switch (auth.kind) {
511
+ case "none":
512
+ case "oauth":
513
+ return {};
514
+ case "bearer":
515
+ return { authorization: `Bearer ${value}` };
516
+ case "basic":
517
+ return { authorization: `Basic ${base64(value)}` };
518
+ case "header":
519
+ return { [auth.header]: `${auth.prefix ?? ""}${value}` };
520
+ }
521
+ }
522
+ function base64(text) {
523
+ const bytes = new TextEncoder().encode(text);
524
+ let binary = "";
525
+ for (const byte of bytes)
526
+ binary += String.fromCharCode(byte);
527
+ return btoa(binary);
528
+ }
529
+ /** Plain words for an auth kind, for lists and dialogs. */
530
+ export function remoteMcpAuthLabel(auth) {
531
+ switch (auth.kind) {
532
+ case "none":
533
+ return "no credential";
534
+ case "bearer":
535
+ return "bearer token";
536
+ case "basic":
537
+ return "user and password";
538
+ case "header":
539
+ return `header ${auth.header}`;
540
+ case "oauth":
541
+ return "OAuth (not supported yet)";
542
+ }
543
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The internal tools contract (v1) between the gateway's ToolBus and an app's
3
+ * `everyApp()` wrapper. Two reserved endpoints, reached only over the
4
+ * gateway's service binding and never public:
5
+ *
6
+ * POST /__everyapp/tools/list x-everyapp-system JWT (purpose "tools.list")
7
+ * POST /__everyapp/tools/call x-everyapp-identity JWT, chan "mcp" + tc claim
8
+ *
9
+ * MCP itself (protocol eras, headers, envelopes) stays in the gateway; apps
10
+ * only ever speak this small versioned JSON shape. Both sides import the
11
+ * shapes, paths and the catalog hash from here so they cannot drift.
12
+ */
13
+ export declare const TOOLS_PROTOCOL_VERSION = 1;
14
+ export declare const TOOLS_PATH_PREFIX = "/__everyapp/tools";
15
+ export declare const TOOLS_LIST_PATH = "/__everyapp/tools/list";
16
+ export declare const TOOLS_CALL_PATH = "/__everyapp/tools/call";
17
+ export declare const TOOL_RISKS: readonly ["read", "write", "destructive"];
18
+ export type ToolRisk = (typeof TOOL_RISKS)[number];
19
+ /** Scopes the gateway mints on `chan: "mcp"` identities, one per risk. */
20
+ export declare const MCP_READ_SCOPE = "mcp:read";
21
+ export declare const MCP_WRITE_SCOPE = "mcp:write";
22
+ /** Never minted in v1: destructive tools are hidden until approvals exist. */
23
+ export declare const MCP_DESTRUCTIVE_SCOPE = "mcp:destructive";
24
+ /** Local tool names: lowercase snake_case, at most 48 characters. */
25
+ export declare const TOOL_NAME_PATTERN: RegExp;
26
+ export declare function isToolRisk(value: unknown): value is ToolRisk;
27
+ /** The exact scope a `chan: "mcp"` identity must carry to call a tool. */
28
+ export declare function requiredScopeForRisk(risk: ToolRisk): string;
29
+ export interface ToolAnnotationsV1 {
30
+ idempotentHint?: boolean;
31
+ openWorldHint?: boolean;
32
+ }
33
+ /** One tool as an app describes it to the gateway. */
34
+ export interface ToolDescriptorV1 {
35
+ name: string;
36
+ title?: string;
37
+ description: string;
38
+ risk: ToolRisk;
39
+ /** JSON Schema (draft 2020-12) of the arguments; always `type: "object"`. */
40
+ inputSchema: Record<string, unknown>;
41
+ outputSchema?: Record<string, unknown>;
42
+ annotations?: ToolAnnotationsV1;
43
+ }
44
+ export interface ToolsListRequestV1 {
45
+ v: 1;
46
+ }
47
+ export interface ToolsListResponseV1 {
48
+ v: 1;
49
+ /** The SDK version that produced the catalog. */
50
+ sdk: string;
51
+ /** {@link catalogHash} of `instructions` + `tools`. */
52
+ hash: string;
53
+ instructions?: string;
54
+ tools: ToolDescriptorV1[];
55
+ /** Tools the SDK left out because it can't describe them, and why. */
56
+ warnings?: string[];
57
+ }
58
+ /** The `tc` identity claim: binds a `chan: "mcp"` token to one call. */
59
+ export interface ToolCallClaim {
60
+ id: string;
61
+ name: string;
62
+ }
63
+ export interface ToolsCallRequestV1 {
64
+ v: 1;
65
+ name: string;
66
+ arguments: Record<string, unknown>;
67
+ /** Must equal the identity token's `tc.id`. */
68
+ callId: string;
69
+ }
70
+ export interface ToolContentBlockV1 {
71
+ type: string;
72
+ [key: string]: unknown;
73
+ }
74
+ export interface ToolCallResultV1 {
75
+ content: ToolContentBlockV1[];
76
+ structuredContent?: Record<string, unknown>;
77
+ isError?: boolean;
78
+ }
79
+ export type ToolsErrorCodeV1 = "unknown_tool" | "invalid_arguments" | "invalid_request" | "forbidden" | "unauthenticated" | "method_not_allowed" | "misconfigured";
80
+ export interface ToolIssueV1 {
81
+ message: string;
82
+ path?: (string | number)[];
83
+ }
84
+ export interface ToolsErrorV1 {
85
+ code: ToolsErrorCodeV1;
86
+ message: string;
87
+ issues?: ToolIssueV1[];
88
+ }
89
+ export interface ToolsCallSuccessV1 {
90
+ v: 1;
91
+ /** The app's current catalog hash; the gateway re-syncs on a mismatch. */
92
+ catalogHash: string;
93
+ result: ToolCallResultV1;
94
+ }
95
+ export interface ToolsErrorResponseV1 {
96
+ v: 1;
97
+ error: ToolsErrorV1;
98
+ }
99
+ export type ToolsCallResponseV1 = ToolsCallSuccessV1 | ToolsErrorResponseV1;
100
+ /**
101
+ * JSON with object keys sorted at every depth, so equal values always
102
+ * serialize identically. `undefined` members are dropped, as JSON.stringify
103
+ * does.
104
+ */
105
+ export declare function canonicalJson(value: unknown): string;
106
+ /**
107
+ * `sha256:<hex>` over the canonical JSON of a catalog. The SDK reports it with
108
+ * every list and call; the gateway stores it at sync and re-syncs when a call
109
+ * reports a different one.
110
+ */
111
+ export declare function catalogHash(catalog: {
112
+ instructions?: string;
113
+ tools: readonly ToolDescriptorV1[];
114
+ }): Promise<string>;
115
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/internal/tools.ts"],"names":[],"mappings":"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"}