@frankieai/server 0.2.0 → 0.2.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.
- package/dist/{chunk-CYWTCM2O.js → chunk-UZSHFMJN.js} +264 -9
- package/dist/chunk-UZSHFMJN.js.map +1 -0
- package/dist/{chunk-R5XGMQR4.js → chunk-WFENEYOV.js} +384 -315
- package/dist/chunk-WFENEYOV.js.map +1 -0
- package/dist/{chunk-BA3FZ7JH.js → chunk-XCIM765D.js} +6 -5
- package/dist/chunk-XCIM765D.js.map +1 -0
- package/dist/control-server.js +2 -2
- package/dist/index.d.ts +32 -0
- package/dist/index.js +3 -3
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-BA3FZ7JH.js.map +0 -1
- package/dist/chunk-CYWTCM2O.js.map +0 -1
- package/dist/chunk-R5XGMQR4.js.map +0 -1
|
@@ -84,115 +84,6 @@ function modelHasApi(model, api) {
|
|
|
84
84
|
return hasApi(model, api);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// src/auth.ts
|
|
88
|
-
import { createRemoteJWKSet, jwtVerify, SignJWT } from "jose";
|
|
89
|
-
var AuthError = class extends Error {
|
|
90
|
-
constructor(message, code = "unauthorized") {
|
|
91
|
-
super(message);
|
|
92
|
-
this.code = code;
|
|
93
|
-
}
|
|
94
|
-
code;
|
|
95
|
-
};
|
|
96
|
-
var TokenService = class {
|
|
97
|
-
constructor(auth, secrets) {
|
|
98
|
-
this.auth = auth;
|
|
99
|
-
this.sessionKey = new TextEncoder().encode(secrets.sessionSecret);
|
|
100
|
-
this.userKey = secrets.jwtSecret ? new TextEncoder().encode(secrets.jwtSecret) : void 0;
|
|
101
|
-
if (!this.userKey && !auth.jwksUrl && !auth.allowAnonymous) {
|
|
102
|
-
throw new Error("Frankie auth needs either a JWT secret, a JWKS URL, or allowAnonymous.");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
auth;
|
|
106
|
-
sessionKey;
|
|
107
|
-
userKey;
|
|
108
|
-
jwks;
|
|
109
|
-
/** Verify a host-signed user token. Throws `AuthError` on any failure. */
|
|
110
|
-
async verifyUserToken(token) {
|
|
111
|
-
const options = {
|
|
112
|
-
...this.auth.jwtIssuer ? { issuer: this.auth.jwtIssuer } : {},
|
|
113
|
-
...this.auth.jwtAudience ? { audience: this.auth.jwtAudience } : {},
|
|
114
|
-
clockTolerance: 30
|
|
115
|
-
};
|
|
116
|
-
let payload;
|
|
117
|
-
try {
|
|
118
|
-
if (this.auth.jwksUrl) {
|
|
119
|
-
this.jwks ??= createRemoteJWKSet(new URL(this.auth.jwksUrl));
|
|
120
|
-
({ payload } = await jwtVerify(token, this.jwks, options));
|
|
121
|
-
} else if (this.userKey) {
|
|
122
|
-
({ payload } = await jwtVerify(token, this.userKey, { ...options, algorithms: ["HS256"] }));
|
|
123
|
-
} else {
|
|
124
|
-
throw new AuthError("This tenant is not configured to accept user tokens.");
|
|
125
|
-
}
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (error instanceof AuthError) throw error;
|
|
128
|
-
throw new AuthError(`Could not verify the user token: ${error.message}`);
|
|
129
|
-
}
|
|
130
|
-
const subject = typeof payload.sub === "string" ? payload.sub : void 0;
|
|
131
|
-
if (!subject) throw new AuthError("The user token has no `sub` claim.");
|
|
132
|
-
return {
|
|
133
|
-
id: subject,
|
|
134
|
-
displayName: stringClaim(payload, "name") ?? stringClaim(payload, "preferred_username"),
|
|
135
|
-
email: stringClaim(payload, "email"),
|
|
136
|
-
anonymous: false,
|
|
137
|
-
claims: payload
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/** Mint the bearer token the widget uses for the rest of the conversation. */
|
|
141
|
-
async issueSessionToken(input) {
|
|
142
|
-
const expiresAt = new Date(Date.now() + this.auth.sessionTtlSeconds * 1e3);
|
|
143
|
-
const token = await new SignJWT({
|
|
144
|
-
sid: input.sessionId,
|
|
145
|
-
uid: input.user.id,
|
|
146
|
-
anon: input.user.anonymous,
|
|
147
|
-
tid: input.tenantId
|
|
148
|
-
}).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer("frankie").setAudience("frankie-widget").setExpirationTime(Math.floor(expiresAt.getTime() / 1e3)).sign(this.sessionKey);
|
|
149
|
-
return { token, expiresAt };
|
|
150
|
-
}
|
|
151
|
-
async verifySessionToken(token) {
|
|
152
|
-
try {
|
|
153
|
-
const { payload } = await jwtVerify(token, this.sessionKey, {
|
|
154
|
-
algorithms: ["HS256"],
|
|
155
|
-
issuer: "frankie",
|
|
156
|
-
audience: "frankie-widget"
|
|
157
|
-
});
|
|
158
|
-
const sessionId = stringClaim(payload, "sid");
|
|
159
|
-
const userId = stringClaim(payload, "uid");
|
|
160
|
-
const tenantId = stringClaim(payload, "tid");
|
|
161
|
-
if (!sessionId || !userId || !tenantId) throw new AuthError("Malformed session token.");
|
|
162
|
-
return { sessionId, userId, tenantId };
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error instanceof AuthError) throw error;
|
|
165
|
-
throw new AuthError("Your chat session has expired. Reload the page to start a new one.");
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
function stringClaim(payload, key) {
|
|
170
|
-
const value = payload[key];
|
|
171
|
-
return typeof value === "string" ? value : void 0;
|
|
172
|
-
}
|
|
173
|
-
function isOriginAllowed(origin, allowed) {
|
|
174
|
-
if (allowed.length === 0) return origin === void 0;
|
|
175
|
-
if (!origin) return false;
|
|
176
|
-
return allowed.some((entry) => normaliseOrigin(entry) === normaliseOrigin(origin));
|
|
177
|
-
}
|
|
178
|
-
function normaliseOrigin(value) {
|
|
179
|
-
try {
|
|
180
|
-
const url = new URL(value);
|
|
181
|
-
return url.origin.toLowerCase();
|
|
182
|
-
} catch {
|
|
183
|
-
return value.trim().replace(/\/+$/, "").toLowerCase();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function safeEqual(a, b) {
|
|
187
|
-
if (a.length !== b.length) return false;
|
|
188
|
-
let diff = 0;
|
|
189
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
190
|
-
return diff === 0;
|
|
191
|
-
}
|
|
192
|
-
function anonymousUser(seed) {
|
|
193
|
-
return { id: `anon_${seed}`, anonymous: true, claims: {} };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
87
|
// src/agent/headers.ts
|
|
197
88
|
function buildForwardedHeaders(source, session) {
|
|
198
89
|
const headers = { ...source.headers };
|
|
@@ -211,8 +102,8 @@ function buildForwardedHeaders(source, session) {
|
|
|
211
102
|
return headers;
|
|
212
103
|
}
|
|
213
104
|
|
|
214
|
-
// src/agent/
|
|
215
|
-
import {
|
|
105
|
+
// src/agent/openapi.ts
|
|
106
|
+
import { readFile } from "fs/promises";
|
|
216
107
|
|
|
217
108
|
// src/connector-fetch.ts
|
|
218
109
|
import { lookup } from "dns/promises";
|
|
@@ -307,145 +198,9 @@ async function readLimited(stream, max) {
|
|
|
307
198
|
var trustedLocalConnectors = () => process.env["FRANKIE_TRUST_LOCAL_CONNECTORS"] === "true";
|
|
308
199
|
var connectorFetch = (input, init) => trustedLocalConnectors() ? fetch(input, init) : publicConnectorFetch(input, init);
|
|
309
200
|
|
|
310
|
-
// src/agent/
|
|
311
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
312
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
201
|
+
// src/agent/openapi.ts
|
|
313
202
|
import { Type } from "@earendil-works/pi-ai";
|
|
314
203
|
import { describeTool, evaluateToolPolicy } from "@frankieai/core";
|
|
315
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
316
|
-
async function connectMcpTools(options) {
|
|
317
|
-
const clients = [];
|
|
318
|
-
const tools = [];
|
|
319
|
-
const failed = [];
|
|
320
|
-
const results = await Promise.all(
|
|
321
|
-
options.servers.filter((server) => server.enabled).map(async (server) => {
|
|
322
|
-
try {
|
|
323
|
-
return { server, client: await connectOne(server, options) };
|
|
324
|
-
} catch (error) {
|
|
325
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
326
|
-
options.onWarning?.("mcp_connect_failed", { server: server.name, error: message });
|
|
327
|
-
failed.push({ name: server.name, error: message });
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
);
|
|
332
|
-
for (const result of results) {
|
|
333
|
-
if (!result) continue;
|
|
334
|
-
clients.push(result.client);
|
|
335
|
-
try {
|
|
336
|
-
const listed = await withTimeout(result.client.listTools(), options.connectTimeoutMs ?? 1e4, "MCP tool discovery timed out.");
|
|
337
|
-
for (const tool of listed.tools) {
|
|
338
|
-
if (result.server.selectedTools && !result.server.selectedTools.includes(tool.name)) continue;
|
|
339
|
-
const name = `${MCP_TOOL_PREFIX}${result.server.name}__${tool.name}`;
|
|
340
|
-
if (!evaluateToolPolicy(name, options.policy).allowed) continue;
|
|
341
|
-
tools.push(adaptTool(name, tool, result.client, options.callTimeoutMs));
|
|
342
|
-
}
|
|
343
|
-
} catch (error) {
|
|
344
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
345
|
-
options.onWarning?.("mcp_list_tools_failed", { server: result.server.name, error: message });
|
|
346
|
-
failed.push({ name: result.server.name, error: message });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return {
|
|
350
|
-
tools,
|
|
351
|
-
failed,
|
|
352
|
-
async close() {
|
|
353
|
-
await Promise.all(clients.map((client) => client.close().catch(() => void 0)));
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
async function connectOne(server, options) {
|
|
358
|
-
const headers = buildForwardedHeaders(server, options.session);
|
|
359
|
-
const url = new URL(server.url);
|
|
360
|
-
const transport = server.transport === "sse" ? new SSEClientTransport(url, { requestInit: { headers }, fetch: connectorFetch, eventSourceInit: { fetch: connectorFetch } }) : new StreamableHTTPClientTransport(url, { requestInit: { headers }, fetch: connectorFetch });
|
|
361
|
-
const client = new Client({ name: "frankie", version: "0.1.0" }, { capabilities: {} });
|
|
362
|
-
try {
|
|
363
|
-
await withTimeout(
|
|
364
|
-
client.connect(transport),
|
|
365
|
-
options.connectTimeoutMs ?? 1e4,
|
|
366
|
-
`MCP server "${server.name}" did not complete the handshake in time.`
|
|
367
|
-
);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
await client.close().catch(() => void 0);
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
372
|
-
return client;
|
|
373
|
-
}
|
|
374
|
-
function adaptTool(name, tool, client, callTimeoutMs = 6e4) {
|
|
375
|
-
return {
|
|
376
|
-
name,
|
|
377
|
-
label: describeTool(name, {}),
|
|
378
|
-
description: tool.description ?? `The ${tool.name} tool.`,
|
|
379
|
-
// MCP gives us JSON Schema; `Type.Unsafe` passes it through untouched
|
|
380
|
-
// rather than round-tripping it into a typebox equivalent and losing
|
|
381
|
-
// whatever the server actually declared.
|
|
382
|
-
parameters: Type.Unsafe(normaliseSchema(tool.inputSchema)),
|
|
383
|
-
async execute(_toolCallId, params, signal) {
|
|
384
|
-
const response = await withTimeout(
|
|
385
|
-
client.callTool(
|
|
386
|
-
{ name: tool.name, arguments: params ?? {} },
|
|
387
|
-
void 0,
|
|
388
|
-
signal ? { signal } : void 0
|
|
389
|
-
),
|
|
390
|
-
callTimeoutMs,
|
|
391
|
-
`The ${tool.name} tool did not respond in time.`
|
|
392
|
-
);
|
|
393
|
-
const content = toAgentContent(response.content);
|
|
394
|
-
if (response.isError) {
|
|
395
|
-
throw new Error(textOf(content) || `The ${tool.name} tool reported an error.`);
|
|
396
|
-
}
|
|
397
|
-
return { content, details: response.structuredContent ?? null };
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
function normaliseSchema(schema) {
|
|
402
|
-
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
403
|
-
return { type: "object", properties: {} };
|
|
404
|
-
}
|
|
405
|
-
const copy = { ...schema };
|
|
406
|
-
if (copy["type"] !== "object") copy["type"] = "object";
|
|
407
|
-
if (typeof copy["properties"] !== "object" || copy["properties"] === null) copy["properties"] = {};
|
|
408
|
-
return copy;
|
|
409
|
-
}
|
|
410
|
-
function toAgentContent(content) {
|
|
411
|
-
if (!Array.isArray(content)) return [{ type: "text", text: "" }];
|
|
412
|
-
const out = [];
|
|
413
|
-
for (const part of content) {
|
|
414
|
-
if (typeof part !== "object" || part === null) continue;
|
|
415
|
-
const block = part;
|
|
416
|
-
if (block["type"] === "text" && typeof block["text"] === "string") {
|
|
417
|
-
out.push({ type: "text", text: block["text"] });
|
|
418
|
-
} else if (block["type"] === "image" && typeof block["data"] === "string" && typeof block["mimeType"] === "string") {
|
|
419
|
-
out.push({ type: "image", data: block["data"], mimeType: block["mimeType"] });
|
|
420
|
-
} else if (block["type"] === "resource") {
|
|
421
|
-
const resource = block["resource"];
|
|
422
|
-
const text = typeof resource === "object" && resource !== null && typeof resource["text"] === "string" ? String(resource["text"]) : "[resource omitted]";
|
|
423
|
-
out.push({ type: "text", text });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return out.length > 0 ? out : [{ type: "text", text: "" }];
|
|
427
|
-
}
|
|
428
|
-
function textOf(content) {
|
|
429
|
-
return content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n").trim();
|
|
430
|
-
}
|
|
431
|
-
async function withTimeout(promise, ms, message) {
|
|
432
|
-
let timer;
|
|
433
|
-
try {
|
|
434
|
-
return await Promise.race([
|
|
435
|
-
promise,
|
|
436
|
-
new Promise((_resolve, reject) => {
|
|
437
|
-
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
438
|
-
})
|
|
439
|
-
]);
|
|
440
|
-
} finally {
|
|
441
|
-
if (timer) clearTimeout(timer);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// src/agent/openapi.ts
|
|
446
|
-
import { readFile } from "fs/promises";
|
|
447
|
-
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
448
|
-
import { describeTool as describeTool2, evaluateToolPolicy as evaluateToolPolicy2 } from "@frankieai/core";
|
|
449
204
|
import { parse as parseYaml } from "yaml";
|
|
450
205
|
var API_TOOL_PREFIX = "api__";
|
|
451
206
|
var OpenApiError = class extends Error {
|
|
@@ -465,7 +220,7 @@ async function inspectApiCapabilities(source, options = {}) {
|
|
|
465
220
|
const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : `${method}_${slug(path)}`;
|
|
466
221
|
if (source.operations.length > 0 && !source.operations.includes(operationId)) continue;
|
|
467
222
|
const toolName = `${API_TOOL_PREFIX}${source.name}__${sanitize(operationId)}`;
|
|
468
|
-
if (options.policy && !
|
|
223
|
+
if (options.policy && !evaluateToolPolicy(toolName, options.policy).allowed) continue;
|
|
469
224
|
const summary = typeof operation["summary"] === "string" ? operation["summary"] : void 0;
|
|
470
225
|
capabilities.push({ operationId, method: httpMethod, path, ...summary ? { summary } : {} });
|
|
471
226
|
}
|
|
@@ -509,14 +264,14 @@ async function buildOne(source, options) {
|
|
|
509
264
|
if (source.operations.length > 0 && !source.operations.includes(operationId)) continue;
|
|
510
265
|
if (source.selectedTools && !source.selectedTools.includes(operationId)) continue;
|
|
511
266
|
const name = `${API_TOOL_PREFIX}${source.name}__${sanitize(operationId)}`;
|
|
512
|
-
if (!
|
|
267
|
+
if (!evaluateToolPolicy(name, options.policy).allowed) continue;
|
|
513
268
|
const params = collectParameters(doc, item, operation);
|
|
514
269
|
const body = collectRequestBody(doc, operation);
|
|
515
270
|
tools.push({
|
|
516
271
|
name,
|
|
517
|
-
label:
|
|
272
|
+
label: describeTool(name, {}),
|
|
518
273
|
description: describeOperation(operation, method, path),
|
|
519
|
-
parameters:
|
|
274
|
+
parameters: Type.Unsafe(toolSchema(params, body)),
|
|
520
275
|
async execute(_id, args, signal) {
|
|
521
276
|
const input = isRecord(args) ? args : {};
|
|
522
277
|
const url = buildUrl(baseUrl, path, params, input);
|
|
@@ -616,75 +371,321 @@ function toolSchema(params, body) {
|
|
|
616
371
|
required.push("body");
|
|
617
372
|
}
|
|
618
373
|
return {
|
|
619
|
-
type: "object",
|
|
620
|
-
properties,
|
|
621
|
-
...required.length ? { required } : {},
|
|
622
|
-
additionalProperties: false
|
|
374
|
+
type: "object",
|
|
375
|
+
properties,
|
|
376
|
+
...required.length ? { required } : {},
|
|
377
|
+
additionalProperties: false
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildUrl(baseUrl, path, params, input) {
|
|
381
|
+
let resolved = path;
|
|
382
|
+
for (const param of params) {
|
|
383
|
+
if (param.location !== "path") continue;
|
|
384
|
+
const value = input[param.name];
|
|
385
|
+
resolved = resolved.replace(`{${param.name}}`, encodeURIComponent(String(value ?? "")));
|
|
386
|
+
}
|
|
387
|
+
const url = new URL(`${baseUrl}${resolved.startsWith("/") ? "" : "/"}${resolved}`);
|
|
388
|
+
for (const param of params) {
|
|
389
|
+
if (param.location !== "query") continue;
|
|
390
|
+
const value = input[param.name];
|
|
391
|
+
if (value === void 0 || value === null) continue;
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
for (const entry of value) url.searchParams.append(param.name, String(entry));
|
|
394
|
+
} else {
|
|
395
|
+
url.searchParams.set(param.name, String(value));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return url.toString();
|
|
399
|
+
}
|
|
400
|
+
function describeOperation(operation, method, path) {
|
|
401
|
+
const summary = typeof operation["summary"] === "string" ? operation["summary"] : "";
|
|
402
|
+
const description = typeof operation["description"] === "string" ? operation["description"] : "";
|
|
403
|
+
const text = [summary, description].filter(Boolean).join(" - ");
|
|
404
|
+
return text || `${method.toUpperCase()} ${path}`;
|
|
405
|
+
}
|
|
406
|
+
function resolveRef(doc, node, depth = 0) {
|
|
407
|
+
if (!isRecord(node) || depth > 10) return node;
|
|
408
|
+
const ref = node["$ref"];
|
|
409
|
+
if (typeof ref !== "string" || !ref.startsWith("#/")) return node;
|
|
410
|
+
let current = doc;
|
|
411
|
+
for (const segment of ref.slice(2).split("/")) {
|
|
412
|
+
if (!isRecord(current)) return void 0;
|
|
413
|
+
current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
414
|
+
}
|
|
415
|
+
return resolveRef(doc, current, depth + 1);
|
|
416
|
+
}
|
|
417
|
+
function matchPath(glob, path) {
|
|
418
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
419
|
+
const trailingDeep = escaped.endsWith("/**");
|
|
420
|
+
const body = trailingDeep ? escaped.slice(0, -3) : escaped;
|
|
421
|
+
const pattern = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
|
|
422
|
+
return new RegExp(`^${pattern}$`).test(path);
|
|
423
|
+
}
|
|
424
|
+
function sanitize(value) {
|
|
425
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 48);
|
|
426
|
+
}
|
|
427
|
+
function slug(path) {
|
|
428
|
+
return path.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
429
|
+
}
|
|
430
|
+
function truncate(value, max) {
|
|
431
|
+
return value.length <= max ? value : `${value.slice(0, max)}
|
|
432
|
+
[truncated]`;
|
|
433
|
+
}
|
|
434
|
+
function asArray(value) {
|
|
435
|
+
return Array.isArray(value) ? value : [];
|
|
436
|
+
}
|
|
437
|
+
function isRecord(value) {
|
|
438
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/agent/mcp.ts
|
|
442
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
443
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
444
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
445
|
+
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
446
|
+
import { describeTool as describeTool2, evaluateToolPolicy as evaluateToolPolicy2 } from "@frankieai/core";
|
|
447
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
448
|
+
async function connectMcpTools(options) {
|
|
449
|
+
const clients = [];
|
|
450
|
+
const tools = [];
|
|
451
|
+
const failed = [];
|
|
452
|
+
const results = await Promise.all(
|
|
453
|
+
options.servers.filter((server) => server.enabled).map(async (server) => {
|
|
454
|
+
try {
|
|
455
|
+
return { server, client: await connectOne(server, options) };
|
|
456
|
+
} catch (error) {
|
|
457
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
458
|
+
options.onWarning?.("mcp_connect_failed", { server: server.name, error: message });
|
|
459
|
+
failed.push({ name: server.name, error: message });
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
for (const result of results) {
|
|
465
|
+
if (!result) continue;
|
|
466
|
+
clients.push(result.client);
|
|
467
|
+
try {
|
|
468
|
+
const listed = await withTimeout(result.client.listTools(), options.connectTimeoutMs ?? 1e4, "MCP tool discovery timed out.");
|
|
469
|
+
for (const tool of listed.tools) {
|
|
470
|
+
if (result.server.selectedTools && !result.server.selectedTools.includes(tool.name)) continue;
|
|
471
|
+
const name = `${MCP_TOOL_PREFIX}${result.server.name}__${tool.name}`;
|
|
472
|
+
if (!evaluateToolPolicy2(name, options.policy).allowed) continue;
|
|
473
|
+
tools.push(adaptTool(name, tool, result.client, options.callTimeoutMs));
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
477
|
+
options.onWarning?.("mcp_list_tools_failed", { server: result.server.name, error: message });
|
|
478
|
+
failed.push({ name: result.server.name, error: message });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
tools,
|
|
483
|
+
failed,
|
|
484
|
+
async close() {
|
|
485
|
+
await Promise.all(clients.map((client) => client.close().catch(() => void 0)));
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function connectOne(server, options) {
|
|
490
|
+
const headers = buildForwardedHeaders(server, options.session);
|
|
491
|
+
const url = new URL(server.url);
|
|
492
|
+
const transport = server.transport === "sse" ? new SSEClientTransport(url, { requestInit: { headers }, fetch: connectorFetch, eventSourceInit: { fetch: connectorFetch } }) : new StreamableHTTPClientTransport(url, { requestInit: { headers }, fetch: connectorFetch });
|
|
493
|
+
const client = new Client({ name: "frankie", version: "0.1.0" }, { capabilities: {} });
|
|
494
|
+
try {
|
|
495
|
+
await withTimeout(
|
|
496
|
+
client.connect(transport),
|
|
497
|
+
options.connectTimeoutMs ?? 1e4,
|
|
498
|
+
`MCP server "${server.name}" did not complete the handshake in time.`
|
|
499
|
+
);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
await client.close().catch(() => void 0);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
return client;
|
|
505
|
+
}
|
|
506
|
+
function adaptTool(name, tool, client, callTimeoutMs = 6e4) {
|
|
507
|
+
return {
|
|
508
|
+
name,
|
|
509
|
+
label: describeTool2(name, {}),
|
|
510
|
+
description: tool.description ?? `The ${tool.name} tool.`,
|
|
511
|
+
// MCP gives us JSON Schema; `Type.Unsafe` passes it through untouched
|
|
512
|
+
// rather than round-tripping it into a typebox equivalent and losing
|
|
513
|
+
// whatever the server actually declared.
|
|
514
|
+
parameters: Type2.Unsafe(normaliseSchema(tool.inputSchema)),
|
|
515
|
+
async execute(_toolCallId, params, signal) {
|
|
516
|
+
const response = await withTimeout(
|
|
517
|
+
client.callTool(
|
|
518
|
+
{ name: tool.name, arguments: params ?? {} },
|
|
519
|
+
void 0,
|
|
520
|
+
signal ? { signal } : void 0
|
|
521
|
+
),
|
|
522
|
+
callTimeoutMs,
|
|
523
|
+
`The ${tool.name} tool did not respond in time.`
|
|
524
|
+
);
|
|
525
|
+
const content = toAgentContent(response.content);
|
|
526
|
+
if (response.isError) {
|
|
527
|
+
throw new Error(textOf(content) || `The ${tool.name} tool reported an error.`);
|
|
528
|
+
}
|
|
529
|
+
return { content, details: response.structuredContent ?? null };
|
|
530
|
+
}
|
|
623
531
|
};
|
|
624
532
|
}
|
|
625
|
-
function
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (param.location !== "path") continue;
|
|
629
|
-
const value = input[param.name];
|
|
630
|
-
resolved = resolved.replace(`{${param.name}}`, encodeURIComponent(String(value ?? "")));
|
|
533
|
+
function normaliseSchema(schema) {
|
|
534
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
535
|
+
return { type: "object", properties: {} };
|
|
631
536
|
}
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
537
|
+
const copy = { ...schema };
|
|
538
|
+
if (copy["type"] !== "object") copy["type"] = "object";
|
|
539
|
+
if (typeof copy["properties"] !== "object" || copy["properties"] === null) copy["properties"] = {};
|
|
540
|
+
return copy;
|
|
541
|
+
}
|
|
542
|
+
function toAgentContent(content) {
|
|
543
|
+
if (!Array.isArray(content)) return [{ type: "text", text: "" }];
|
|
544
|
+
const out = [];
|
|
545
|
+
for (const part of content) {
|
|
546
|
+
if (typeof part !== "object" || part === null) continue;
|
|
547
|
+
const block = part;
|
|
548
|
+
if (block["type"] === "text" && typeof block["text"] === "string") {
|
|
549
|
+
out.push({ type: "text", text: block["text"] });
|
|
550
|
+
} else if (block["type"] === "image" && typeof block["data"] === "string" && typeof block["mimeType"] === "string") {
|
|
551
|
+
out.push({ type: "image", data: block["data"], mimeType: block["mimeType"] });
|
|
552
|
+
} else if (block["type"] === "resource") {
|
|
553
|
+
const resource = block["resource"];
|
|
554
|
+
const text = typeof resource === "object" && resource !== null && typeof resource["text"] === "string" ? String(resource["text"]) : "[resource omitted]";
|
|
555
|
+
out.push({ type: "text", text });
|
|
641
556
|
}
|
|
642
557
|
}
|
|
643
|
-
return
|
|
558
|
+
return out.length > 0 ? out : [{ type: "text", text: "" }];
|
|
644
559
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
const description = typeof operation["description"] === "string" ? operation["description"] : "";
|
|
648
|
-
const text = [summary, description].filter(Boolean).join(" - ");
|
|
649
|
-
return text || `${method.toUpperCase()} ${path}`;
|
|
560
|
+
function textOf(content) {
|
|
561
|
+
return content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n").trim();
|
|
650
562
|
}
|
|
651
|
-
function
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
563
|
+
async function withTimeout(promise, ms, message) {
|
|
564
|
+
let timer;
|
|
565
|
+
try {
|
|
566
|
+
return await Promise.race([
|
|
567
|
+
promise,
|
|
568
|
+
new Promise((_resolve, reject) => {
|
|
569
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
570
|
+
})
|
|
571
|
+
]);
|
|
572
|
+
} finally {
|
|
573
|
+
if (timer) clearTimeout(timer);
|
|
659
574
|
}
|
|
660
|
-
return resolveRef(doc, current, depth + 1);
|
|
661
|
-
}
|
|
662
|
-
function matchPath(glob, path) {
|
|
663
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
664
|
-
const trailingDeep = escaped.endsWith("/**");
|
|
665
|
-
const body = trailingDeep ? escaped.slice(0, -3) : escaped;
|
|
666
|
-
const pattern = body.split("**").map((chunk) => chunk.replace(/\*/g, "[^/]*")).join(".*") + (trailingDeep ? "(?:/.*)?" : "");
|
|
667
|
-
return new RegExp(`^${pattern}$`).test(path);
|
|
668
575
|
}
|
|
669
|
-
|
|
670
|
-
|
|
576
|
+
|
|
577
|
+
// src/auth.ts
|
|
578
|
+
import { createRemoteJWKSet, jwtVerify, SignJWT } from "jose";
|
|
579
|
+
var AuthError = class extends Error {
|
|
580
|
+
constructor(message, code = "unauthorized") {
|
|
581
|
+
super(message);
|
|
582
|
+
this.code = code;
|
|
583
|
+
}
|
|
584
|
+
code;
|
|
585
|
+
};
|
|
586
|
+
var TokenService = class {
|
|
587
|
+
constructor(auth, secrets) {
|
|
588
|
+
this.auth = auth;
|
|
589
|
+
this.sessionKey = new TextEncoder().encode(secrets.sessionSecret);
|
|
590
|
+
this.userKey = secrets.jwtSecret ? new TextEncoder().encode(secrets.jwtSecret) : void 0;
|
|
591
|
+
if (!this.userKey && !auth.jwksUrl && !auth.allowAnonymous) {
|
|
592
|
+
throw new Error("Frankie auth needs either a JWT secret, a JWKS URL, or allowAnonymous.");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
auth;
|
|
596
|
+
sessionKey;
|
|
597
|
+
userKey;
|
|
598
|
+
jwks;
|
|
599
|
+
/** Verify a host-signed user token. Throws `AuthError` on any failure. */
|
|
600
|
+
async verifyUserToken(token) {
|
|
601
|
+
const options = {
|
|
602
|
+
...this.auth.jwtIssuer ? { issuer: this.auth.jwtIssuer } : {},
|
|
603
|
+
...this.auth.jwtAudience ? { audience: this.auth.jwtAudience } : {},
|
|
604
|
+
clockTolerance: 30
|
|
605
|
+
};
|
|
606
|
+
let payload;
|
|
607
|
+
try {
|
|
608
|
+
if (this.auth.jwksUrl) {
|
|
609
|
+
this.jwks ??= createRemoteJWKSet(new URL(this.auth.jwksUrl));
|
|
610
|
+
({ payload } = await jwtVerify(token, this.jwks, options));
|
|
611
|
+
} else if (this.userKey) {
|
|
612
|
+
({ payload } = await jwtVerify(token, this.userKey, { ...options, algorithms: ["HS256"] }));
|
|
613
|
+
} else {
|
|
614
|
+
throw new AuthError("This tenant is not configured to accept user tokens.");
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (error instanceof AuthError) throw error;
|
|
618
|
+
throw new AuthError(`Could not verify the user token: ${error.message}`);
|
|
619
|
+
}
|
|
620
|
+
const subject = typeof payload.sub === "string" ? payload.sub : void 0;
|
|
621
|
+
if (!subject) throw new AuthError("The user token has no `sub` claim.");
|
|
622
|
+
return {
|
|
623
|
+
id: subject,
|
|
624
|
+
displayName: stringClaim(payload, "name") ?? stringClaim(payload, "preferred_username"),
|
|
625
|
+
email: stringClaim(payload, "email"),
|
|
626
|
+
anonymous: false,
|
|
627
|
+
claims: payload
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/** Mint the bearer token the widget uses for the rest of the conversation. */
|
|
631
|
+
async issueSessionToken(input) {
|
|
632
|
+
const expiresAt = new Date(Date.now() + this.auth.sessionTtlSeconds * 1e3);
|
|
633
|
+
const token = await new SignJWT({
|
|
634
|
+
sid: input.sessionId,
|
|
635
|
+
uid: input.user.id,
|
|
636
|
+
anon: input.user.anonymous,
|
|
637
|
+
tid: input.tenantId
|
|
638
|
+
}).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer("frankie").setAudience("frankie-widget").setExpirationTime(Math.floor(expiresAt.getTime() / 1e3)).sign(this.sessionKey);
|
|
639
|
+
return { token, expiresAt };
|
|
640
|
+
}
|
|
641
|
+
async verifySessionToken(token) {
|
|
642
|
+
try {
|
|
643
|
+
const { payload } = await jwtVerify(token, this.sessionKey, {
|
|
644
|
+
algorithms: ["HS256"],
|
|
645
|
+
issuer: "frankie",
|
|
646
|
+
audience: "frankie-widget"
|
|
647
|
+
});
|
|
648
|
+
const sessionId = stringClaim(payload, "sid");
|
|
649
|
+
const userId = stringClaim(payload, "uid");
|
|
650
|
+
const tenantId = stringClaim(payload, "tid");
|
|
651
|
+
if (!sessionId || !userId || !tenantId) throw new AuthError("Malformed session token.");
|
|
652
|
+
return { sessionId, userId, tenantId };
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (error instanceof AuthError) throw error;
|
|
655
|
+
throw new AuthError("Your chat session has expired. Reload the page to start a new one.");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
function stringClaim(payload, key) {
|
|
660
|
+
const value = payload[key];
|
|
661
|
+
return typeof value === "string" ? value : void 0;
|
|
671
662
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
663
|
+
function isOriginAllowed(origin, allowed) {
|
|
664
|
+
if (allowed.length === 0) return origin === void 0;
|
|
665
|
+
if (!origin) return false;
|
|
666
|
+
return allowed.some((entry) => normaliseOrigin(entry) === normaliseOrigin(origin));
|
|
674
667
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
677
|
-
|
|
668
|
+
function normaliseOrigin(value) {
|
|
669
|
+
try {
|
|
670
|
+
const url = new URL(value);
|
|
671
|
+
return url.origin.toLowerCase();
|
|
672
|
+
} catch {
|
|
673
|
+
return value.trim().replace(/\/+$/, "").toLowerCase();
|
|
674
|
+
}
|
|
678
675
|
}
|
|
679
|
-
function
|
|
680
|
-
|
|
676
|
+
function safeEqual(a, b) {
|
|
677
|
+
if (a.length !== b.length) return false;
|
|
678
|
+
let diff = 0;
|
|
679
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
680
|
+
return diff === 0;
|
|
681
681
|
}
|
|
682
|
-
function
|
|
683
|
-
return
|
|
682
|
+
function anonymousUser(seed) {
|
|
683
|
+
return { id: `anon_${seed}`, anonymous: true, claims: {} };
|
|
684
684
|
}
|
|
685
685
|
|
|
686
686
|
// src/control/store.ts
|
|
687
|
-
import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "fs/promises";
|
|
687
|
+
import { mkdir, readFile as readFile2, readdir, rename, rm, writeFile } from "fs/promises";
|
|
688
|
+
import { randomUUID } from "crypto";
|
|
688
689
|
import { join, resolve } from "path";
|
|
689
690
|
import { parseFrankieConfig } from "@frankieai/core";
|
|
690
691
|
var MEMBERS_FILE = "__members.json";
|
|
@@ -692,6 +693,7 @@ function toPublicTenant(record) {
|
|
|
692
693
|
const { config } = record;
|
|
693
694
|
return {
|
|
694
695
|
skillHistory: record.skillHistory ?? [],
|
|
696
|
+
...record.apiSpecSync ? { apiSpecSync: record.apiSpecSync } : {},
|
|
695
697
|
skills: config.skills,
|
|
696
698
|
id: record.id,
|
|
697
699
|
...record.ownerId ? { ownerId: record.ownerId } : {},
|
|
@@ -736,6 +738,40 @@ function toPublicTenant(record) {
|
|
|
736
738
|
};
|
|
737
739
|
}
|
|
738
740
|
var FileTenantStore = class _FileTenantStore {
|
|
741
|
+
async listOrganizations() {
|
|
742
|
+
await this.load();
|
|
743
|
+
return [...new Set([...this.byId.values()].flatMap((record) => record.ownerId ? [record.ownerId] : []).concat([...this.members.values()].map((member) => member.org)))].sort();
|
|
744
|
+
}
|
|
745
|
+
adminPath(ownerId) {
|
|
746
|
+
if (!/^github:[1-9]\d*$/.test(ownerId)) throw new Error("Invalid platform user ID.");
|
|
747
|
+
return join(this.dir, "platform-admins", ownerId.slice(7) + ".json");
|
|
748
|
+
}
|
|
749
|
+
async getPlatformAdmin(ownerId) {
|
|
750
|
+
if (!/^github:[1-9]\d*$/.test(ownerId)) return null;
|
|
751
|
+
try {
|
|
752
|
+
return JSON.parse(await readFile2(this.adminPath(ownerId), "utf8"));
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (error.code === "ENOENT") return null;
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async listPlatformAdmins() {
|
|
759
|
+
const dir = join(this.dir, "platform-admins");
|
|
760
|
+
await mkdir(dir, { recursive: true });
|
|
761
|
+
const files = (await readdir(dir)).filter((name) => /^[1-9]\d*\.json$/.test(name));
|
|
762
|
+
const records = await Promise.all(files.map((file) => this.getPlatformAdmin("github:" + file.slice(0, -5))));
|
|
763
|
+
return records.filter((record) => Boolean(record));
|
|
764
|
+
}
|
|
765
|
+
async putPlatformAdmin(admin) {
|
|
766
|
+
const path = this.adminPath(admin.ownerId);
|
|
767
|
+
await mkdir(join(this.dir, "platform-admins"), { recursive: true });
|
|
768
|
+
const temporary = path + "." + randomUUID() + ".tmp";
|
|
769
|
+
await writeFile(temporary, JSON.stringify(admin), { mode: 384 });
|
|
770
|
+
await rename(temporary, path);
|
|
771
|
+
}
|
|
772
|
+
async deletePlatformAdmin(ownerId) {
|
|
773
|
+
await rm(this.adminPath(ownerId), { force: true });
|
|
774
|
+
}
|
|
739
775
|
dir;
|
|
740
776
|
providerKeyEnv;
|
|
741
777
|
env;
|
|
@@ -878,6 +914,9 @@ var PostgresTenantStore = class {
|
|
|
878
914
|
}
|
|
879
915
|
/** Create the table if it isn't there yet. Safe to call repeatedly. */
|
|
880
916
|
async init() {
|
|
917
|
+
await this.db.query(`CREATE TABLE IF NOT EXISTS frankie_platform_admins (
|
|
918
|
+
owner_id text PRIMARY KEY, login text, granted_by text NOT NULL, created_at text NOT NULL
|
|
919
|
+
)`);
|
|
881
920
|
await this.db.query(
|
|
882
921
|
`CREATE TABLE IF NOT EXISTS frankie_tenants (
|
|
883
922
|
id text PRIMARY KEY,
|
|
@@ -910,6 +949,36 @@ var PostgresTenantStore = class {
|
|
|
910
949
|
createdAt: String(row.created_at)
|
|
911
950
|
};
|
|
912
951
|
}
|
|
952
|
+
adminRow(row) {
|
|
953
|
+
return { ownerId: String(row.owner_id), ...row.login ? { login: String(row.login) } : {}, grantedBy: String(row.granted_by), createdAt: String(row.created_at) };
|
|
954
|
+
}
|
|
955
|
+
async listOrganizations() {
|
|
956
|
+
await this.ensureReady();
|
|
957
|
+
const { rows } = await this.db.query("SELECT owner_id AS org FROM frankie_tenants WHERE owner_id IS NOT NULL UNION SELECT org FROM frankie_org_members");
|
|
958
|
+
return rows.map((row) => String(row.org)).sort();
|
|
959
|
+
}
|
|
960
|
+
async listPlatformAdmins() {
|
|
961
|
+
await this.ensureReady();
|
|
962
|
+
const { rows } = await this.db.query("SELECT * FROM frankie_platform_admins ORDER BY created_at ASC");
|
|
963
|
+
return rows.map((row) => this.adminRow(row));
|
|
964
|
+
}
|
|
965
|
+
async getPlatformAdmin(ownerId) {
|
|
966
|
+
await this.ensureReady();
|
|
967
|
+
const { rows } = await this.db.query("SELECT * FROM frankie_platform_admins WHERE owner_id = $1", [ownerId]);
|
|
968
|
+
return rows[0] ? this.adminRow(rows[0]) : null;
|
|
969
|
+
}
|
|
970
|
+
async putPlatformAdmin(admin) {
|
|
971
|
+
await this.ensureReady();
|
|
972
|
+
await this.db.query(
|
|
973
|
+
`INSERT INTO frankie_platform_admins (owner_id, login, granted_by, created_at)
|
|
974
|
+
VALUES ($1, $2, $3, $4) ON CONFLICT (owner_id) DO NOTHING`,
|
|
975
|
+
[admin.ownerId, admin.login ?? null, admin.grantedBy, admin.createdAt]
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
async deletePlatformAdmin(ownerId) {
|
|
979
|
+
await this.ensureReady();
|
|
980
|
+
await this.db.query("DELETE FROM frankie_platform_admins WHERE owner_id = $1", [ownerId]);
|
|
981
|
+
}
|
|
913
982
|
async listMembers(org) {
|
|
914
983
|
await this.ensureReady();
|
|
915
984
|
const { rows } = await this.db.query(
|
|
@@ -1008,7 +1077,7 @@ var PostgresTenantStore = class {
|
|
|
1008
1077
|
};
|
|
1009
1078
|
|
|
1010
1079
|
// src/sessions.ts
|
|
1011
|
-
import { randomUUID } from "crypto";
|
|
1080
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1012
1081
|
var MemorySessionStore = class {
|
|
1013
1082
|
constructor(ttlMs = 6 * 60 * 60 * 1e3) {
|
|
1014
1083
|
this.ttlMs = ttlMs;
|
|
@@ -1050,7 +1119,7 @@ var MemorySessionStore = class {
|
|
|
1050
1119
|
}
|
|
1051
1120
|
};
|
|
1052
1121
|
function newSessionId() {
|
|
1053
|
-
return
|
|
1122
|
+
return randomUUID2();
|
|
1054
1123
|
}
|
|
1055
1124
|
|
|
1056
1125
|
// src/postgres-sessions.ts
|
|
@@ -1171,20 +1240,20 @@ export {
|
|
|
1171
1240
|
resolveModel,
|
|
1172
1241
|
resolveModelChoice,
|
|
1173
1242
|
modelHasApi,
|
|
1174
|
-
AuthError,
|
|
1175
|
-
TokenService,
|
|
1176
|
-
isOriginAllowed,
|
|
1177
|
-
safeEqual,
|
|
1178
|
-
anonymousUser,
|
|
1179
1243
|
buildForwardedHeaders,
|
|
1180
|
-
MCP_TOOL_PREFIX,
|
|
1181
|
-
connectMcpTools,
|
|
1182
1244
|
API_TOOL_PREFIX,
|
|
1183
1245
|
OpenApiError,
|
|
1184
1246
|
inspectApiCapabilities,
|
|
1185
1247
|
buildApiTools,
|
|
1186
1248
|
loadSpec,
|
|
1187
1249
|
matchPath,
|
|
1250
|
+
MCP_TOOL_PREFIX,
|
|
1251
|
+
connectMcpTools,
|
|
1252
|
+
AuthError,
|
|
1253
|
+
TokenService,
|
|
1254
|
+
isOriginAllowed,
|
|
1255
|
+
safeEqual,
|
|
1256
|
+
anonymousUser,
|
|
1188
1257
|
toPublicTenant,
|
|
1189
1258
|
FileTenantStore,
|
|
1190
1259
|
PostgresTenantStore,
|
|
@@ -1194,4 +1263,4 @@ export {
|
|
|
1194
1263
|
resolveStores,
|
|
1195
1264
|
resolveTenantStore
|
|
1196
1265
|
};
|
|
1197
|
-
//# sourceMappingURL=chunk-
|
|
1266
|
+
//# sourceMappingURL=chunk-WFENEYOV.js.map
|