@fragno-dev/mcp-fragment 0.0.0-stage → 0.0.3
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.md +16 -0
- package/README.md +312 -2
- package/bin/run.js +7 -0
- package/dist/browser/client/react.d.ts +253 -0
- package/dist/browser/client/react.d.ts.map +1 -0
- package/dist/browser/client/react.js +194 -0
- package/dist/browser/client/react.js.map +1 -0
- package/dist/browser/client/solid.d.ts +256 -0
- package/dist/browser/client/solid.d.ts.map +1 -0
- package/dist/browser/client/solid.js +129 -0
- package/dist/browser/client/solid.js.map +1 -0
- package/dist/browser/client/svelte.d.ts +256 -0
- package/dist/browser/client/svelte.d.ts.map +1 -0
- package/dist/browser/client/svelte.js +131 -0
- package/dist/browser/client/svelte.js.map +1 -0
- package/dist/browser/client/vanilla.d.ts +283 -0
- package/dist/browser/client/vanilla.d.ts.map +1 -0
- package/dist/browser/client/vanilla.js +159 -0
- package/dist/browser/client/vanilla.js.map +1 -0
- package/dist/browser/client/vue.d.ts +256 -0
- package/dist/browser/client/vue.d.ts.map +1 -0
- package/dist/browser/client/vue.js +127 -0
- package/dist/browser/client/vue.js.map +1 -0
- package/dist/browser/client-BCzNDSlW.d.ts +711 -0
- package/dist/browser/client-BCzNDSlW.d.ts.map +1 -0
- package/dist/browser/index.d.ts +813 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +1 -0
- package/dist/browser/src-B6dHZXqB.js +2303 -0
- package/dist/browser/src-B6dHZXqB.js.map +1 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +227 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/definition.js +330 -0
- package/dist/cli/definition.js.map +1 -0
- package/dist/cli/diagnostics.js +93 -0
- package/dist/cli/diagnostics.js.map +1 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mcp-api.js +87 -0
- package/dist/cli/mcp-api.js.map +1 -0
- package/dist/cli/mcp-types.js +100 -0
- package/dist/cli/mcp-types.js.map +1 -0
- package/dist/cli/oauth-provider.js +113 -0
- package/dist/cli/oauth-provider.js.map +1 -0
- package/dist/cli/routes.js +743 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/schema.js +13 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/cli/services.js +330 -0
- package/dist/cli/services.js.map +1 -0
- package/dist/node/definition.d.ts +260 -0
- package/dist/node/definition.d.ts.map +1 -0
- package/dist/node/definition.js +330 -0
- package/dist/node/definition.js.map +1 -0
- package/dist/node/diagnostics.js +93 -0
- package/dist/node/diagnostics.js.map +1 -0
- package/dist/node/index.d.ts +694 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +26 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/mcp-api.d.ts +2 -0
- package/dist/node/mcp-api.js +87 -0
- package/dist/node/mcp-api.js.map +1 -0
- package/dist/node/mcp-types.d.ts +146 -0
- package/dist/node/mcp-types.d.ts.map +1 -0
- package/dist/node/mcp-types.js +100 -0
- package/dist/node/mcp-types.js.map +1 -0
- package/dist/node/oauth-provider.d.ts +24 -0
- package/dist/node/oauth-provider.d.ts.map +1 -0
- package/dist/node/oauth-provider.js +113 -0
- package/dist/node/oauth-provider.js.map +1 -0
- package/dist/node/routes.d.ts +466 -0
- package/dist/node/routes.d.ts.map +1 -0
- package/dist/node/routes.js +743 -0
- package/dist/node/routes.js.map +1 -0
- package/dist/node/schema.d.ts +9 -0
- package/dist/node/schema.d.ts.map +1 -0
- package/dist/node/schema.js +13 -0
- package/dist/node/schema.js.map +1 -0
- package/dist/node/services.d.ts +40 -0
- package/dist/node/services.d.ts.map +1 -0
- package/dist/node/services.js +330 -0
- package/dist/node/services.js.map +1 -0
- package/dist/node/testing/mcp-test-server.d.ts +42 -0
- package/dist/node/testing/mcp-test-server.d.ts.map +1 -0
- package/dist/node/testing/mcp-test-server.js +218 -0
- package/dist/node/testing/mcp-test-server.js.map +1 -0
- package/package.json +130 -3
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER } from "./mcp-types.js";
|
|
3
|
+
import { createMcpFragment } from "./index.js";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { migrate } from "@fragno-dev/db";
|
|
10
|
+
import { toNodeHandler } from "@fragno-dev/node";
|
|
11
|
+
//#region src/cli/cli.ts
|
|
12
|
+
const DEFAULT_PORT = 3927;
|
|
13
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
14
|
+
const DEFAULT_MOUNT_ROUTE = "/api/mcp-fragment";
|
|
15
|
+
const USAGE = `fragno-mcp <command> [options]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
serve Run the local MCP fragment server
|
|
19
|
+
add <slug> <endpoint-url> Register a remote MCP server
|
|
20
|
+
oauth <slug> Start OAuth and open the authorization URL
|
|
21
|
+
status <slug> Show auth status
|
|
22
|
+
tools <slug> List remote MCP tools
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--base-url <url> Local fragment base URL (default: http://127.0.0.1:3927)
|
|
26
|
+
--port <port> Server port for serve/oauth (default: 3927)
|
|
27
|
+
--host <host> Server host for serve (default: 127.0.0.1)
|
|
28
|
+
--data-dir <path> SQLite data directory (default: ~/.fragno/mcp-fragment)
|
|
29
|
+
--mount-route <path> Fragment mount route (default: /api/mcp-fragment)
|
|
30
|
+
--scope <scope> OAuth scope
|
|
31
|
+
--client-id <id> OAuth client id
|
|
32
|
+
--client-secret <secret> OAuth client secret
|
|
33
|
+
--json Print JSON
|
|
34
|
+
--no-open Do not open the browser for oauth
|
|
35
|
+
-h, --help Show help`;
|
|
36
|
+
function parse(argv) {
|
|
37
|
+
const args = [];
|
|
38
|
+
const options = {};
|
|
39
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
40
|
+
const arg = argv[i];
|
|
41
|
+
if (arg === "--") {
|
|
42
|
+
args.push(...argv.slice(i + 1));
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (arg === "--help" || arg === "-h") {
|
|
46
|
+
options["help"] = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg.startsWith("--")) {
|
|
50
|
+
const [rawKey, rawValue] = arg.split("=", 2);
|
|
51
|
+
const key = rawKey.slice(2);
|
|
52
|
+
if (key === "json" || key === "no-open") {
|
|
53
|
+
options[key] = rawValue === void 0 ? true : rawValue !== "false";
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = rawValue ?? argv[i + 1];
|
|
57
|
+
if (rawValue === void 0) i += 1;
|
|
58
|
+
options[key] = value ?? "";
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
args.push(arg);
|
|
62
|
+
}
|
|
63
|
+
const [command, ...rest] = args;
|
|
64
|
+
return {
|
|
65
|
+
command,
|
|
66
|
+
args: rest,
|
|
67
|
+
options
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function optionString(options, key) {
|
|
71
|
+
const value = options[key];
|
|
72
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
73
|
+
}
|
|
74
|
+
function optionBoolean(options, key) {
|
|
75
|
+
return options[key] === true;
|
|
76
|
+
}
|
|
77
|
+
function localBaseUrl(options) {
|
|
78
|
+
return optionString(options, "base-url") ?? `http://${DEFAULT_HOST}:${optionString(options, "port") ?? DEFAULT_PORT}`;
|
|
79
|
+
}
|
|
80
|
+
function mountRoute(options) {
|
|
81
|
+
return optionString(options, "mount-route") ?? DEFAULT_MOUNT_ROUTE;
|
|
82
|
+
}
|
|
83
|
+
function dataDir(options) {
|
|
84
|
+
return optionString(options, "data-dir") ?? path.join(homedir(), ".fragno", "mcp-fragment");
|
|
85
|
+
}
|
|
86
|
+
function fragmentUrl(options, route) {
|
|
87
|
+
return `${localBaseUrl(options).replace(/\/$/, "")}${mountRoute(options)}${route}`;
|
|
88
|
+
}
|
|
89
|
+
async function requestJson(method, url, body) {
|
|
90
|
+
const response = await fetch(url, {
|
|
91
|
+
method,
|
|
92
|
+
headers: body === void 0 ? void 0 : { "content-type": "application/json" },
|
|
93
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
94
|
+
});
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
const payload = text ? JSON.parse(text) : void 0;
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const message = payload && typeof payload === "object" && "message" in payload ? String(payload.message) : text || `HTTP ${response.status}`;
|
|
99
|
+
throw new Error(message);
|
|
100
|
+
}
|
|
101
|
+
return payload;
|
|
102
|
+
}
|
|
103
|
+
async function startServer(options, logger) {
|
|
104
|
+
const dir = dataDir(options);
|
|
105
|
+
await mkdir(dir, { recursive: true });
|
|
106
|
+
process.env["FRAGNO_DATA_DIR"] = dir;
|
|
107
|
+
const baseUrl = localBaseUrl(options).replace(/\/$/, "");
|
|
108
|
+
const fragmentMountRoute = mountRoute(options);
|
|
109
|
+
const oauthRedirectUri = `${baseUrl}${fragmentMountRoute}/oauth/callback`;
|
|
110
|
+
const fragment = createMcpFragment({ allowedOAuthRedirectUris: (url) => url.toString() === oauthRedirectUri }, {
|
|
111
|
+
mountRoute: fragmentMountRoute,
|
|
112
|
+
databaseNamespace: "mcp-fragment"
|
|
113
|
+
});
|
|
114
|
+
logger.log("migrating sqlite database if needed...");
|
|
115
|
+
await migrate(fragment);
|
|
116
|
+
const handler = toNodeHandler(fragment.handler.bind(fragment));
|
|
117
|
+
const server = createServer((req, res) => {
|
|
118
|
+
if (req.url?.startsWith(fragment.mountRoute)) {
|
|
119
|
+
handler(req, res);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("Not found");
|
|
123
|
+
});
|
|
124
|
+
const port = Number(optionString(options, "port") ?? DEFAULT_PORT);
|
|
125
|
+
const host = optionString(options, "host") ?? DEFAULT_HOST;
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
server.once("error", reject);
|
|
128
|
+
server.listen(port, host, () => {
|
|
129
|
+
server.off("error", reject);
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
logger.log(`fragno-mcp listening at ${baseUrl}${fragment.mountRoute}`);
|
|
134
|
+
logger.log(`sqlite data dir: ${dir}`);
|
|
135
|
+
return server;
|
|
136
|
+
}
|
|
137
|
+
function openBrowser(url) {
|
|
138
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
139
|
+
const args = process.platform === "win32" ? [
|
|
140
|
+
"/c",
|
|
141
|
+
"start",
|
|
142
|
+
"",
|
|
143
|
+
url
|
|
144
|
+
] : [url];
|
|
145
|
+
spawn(command, args, {
|
|
146
|
+
stdio: "ignore",
|
|
147
|
+
detached: true
|
|
148
|
+
}).unref();
|
|
149
|
+
}
|
|
150
|
+
function print(value, json, logger) {
|
|
151
|
+
if (json) {
|
|
152
|
+
logger.log(JSON.stringify(value, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (value && typeof value === "object" && "tools" in value && Array.isArray(value.tools)) {
|
|
156
|
+
for (const tool of value.tools) {
|
|
157
|
+
const record = tool && typeof tool === "object" ? tool : {};
|
|
158
|
+
logger.log(`${String(record["name"] ?? "(unnamed)")}\t${String(record["description"] ?? "")}`);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
logger.log(JSON.stringify(value, null, 2));
|
|
163
|
+
}
|
|
164
|
+
async function run(argv, logger = console) {
|
|
165
|
+
const parsed = parse(argv.slice(2));
|
|
166
|
+
if (optionBoolean(parsed.options, "help") || !parsed.command) {
|
|
167
|
+
logger.log(USAGE);
|
|
168
|
+
return parsed.command ? 0 : 1;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
switch (parsed.command) {
|
|
172
|
+
case "serve":
|
|
173
|
+
await startServer(parsed.options, logger);
|
|
174
|
+
await new Promise(() => void 0);
|
|
175
|
+
return 0;
|
|
176
|
+
case "add": {
|
|
177
|
+
const [slug, endpointUrl] = parsed.args;
|
|
178
|
+
if (!slug || !endpointUrl) throw new Error("Usage: fragno-mcp add <slug> <endpoint-url>");
|
|
179
|
+
print(await requestJson("POST", fragmentUrl(parsed.options, "/servers"), {
|
|
180
|
+
slug,
|
|
181
|
+
endpointUrl,
|
|
182
|
+
auth: { type: "none" }
|
|
183
|
+
}), optionBoolean(parsed.options, "json"), logger);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
case "oauth": {
|
|
187
|
+
const [slug] = parsed.args;
|
|
188
|
+
if (!slug) throw new Error("Usage: fragno-mcp oauth <slug>");
|
|
189
|
+
const startUrl = new URL(fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/auth/start`));
|
|
190
|
+
startUrl.searchParams.set(MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER, fragmentUrl(parsed.options, "/oauth/callback"));
|
|
191
|
+
const result = await requestJson("POST", startUrl.toString(), {
|
|
192
|
+
scope: optionString(parsed.options, "scope"),
|
|
193
|
+
clientId: optionString(parsed.options, "client-id"),
|
|
194
|
+
clientSecret: optionString(parsed.options, "client-secret")
|
|
195
|
+
});
|
|
196
|
+
logger.log(`Open this URL to authorize:\n${result.authorizationUrl}`);
|
|
197
|
+
if (!optionBoolean(parsed.options, "no-open")) openBrowser(result.authorizationUrl);
|
|
198
|
+
logger.log("Keep `fragno-mcp serve` running until the browser redirects back to the local callback.");
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
case "status": {
|
|
202
|
+
const [slug] = parsed.args;
|
|
203
|
+
if (!slug) throw new Error("Usage: fragno-mcp status <slug>");
|
|
204
|
+
print(await requestJson("GET", fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/auth/status`)), optionBoolean(parsed.options, "json"), logger);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
case "tools": {
|
|
208
|
+
const [slug] = parsed.args;
|
|
209
|
+
if (!slug) throw new Error("Usage: fragno-mcp tools <slug>");
|
|
210
|
+
print(await requestJson("GET", fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/tools`)), optionBoolean(parsed.options, "json"), logger);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
default: throw new Error(`Unknown command: ${parsed.command}`);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error(`${error instanceof Error ? error.message : String(error)}\n\n${USAGE}`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (process.argv[1] ? import.meta.url === new URL(process.argv[1], "file:").href : false) {
|
|
221
|
+
const exitCode = await run(process.argv);
|
|
222
|
+
if (typeof exitCode === "number") process.exitCode = exitCode;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { run };
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../../src/cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn } from \"node:child_process\";\nimport { mkdir } from \"node:fs/promises\";\nimport { createServer, type Server } from \"node:http\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport { migrate } from \"@fragno-dev/db\";\nimport { toNodeHandler } from \"@fragno-dev/node\";\n\nimport { createMcpFragment } from \"../index\";\nimport { MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER } from \"../mcp-types\";\n\nconst DEFAULT_PORT = 3927;\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_MOUNT_ROUTE = \"/api/mcp-fragment\";\n\nconst USAGE = `fragno-mcp <command> [options]\n\nCommands:\n serve Run the local MCP fragment server\n add <slug> <endpoint-url> Register a remote MCP server\n oauth <slug> Start OAuth and open the authorization URL\n status <slug> Show auth status\n tools <slug> List remote MCP tools\n\nOptions:\n --base-url <url> Local fragment base URL (default: http://127.0.0.1:3927)\n --port <port> Server port for serve/oauth (default: 3927)\n --host <host> Server host for serve (default: 127.0.0.1)\n --data-dir <path> SQLite data directory (default: ~/.fragno/mcp-fragment)\n --mount-route <path> Fragment mount route (default: /api/mcp-fragment)\n --scope <scope> OAuth scope\n --client-id <id> OAuth client id\n --client-secret <secret> OAuth client secret\n --json Print JSON\n --no-open Do not open the browser for oauth\n -h, --help Show help`;\n\ntype Logger = Pick<Console, \"log\" | \"error\">;\n\ntype Parsed = {\n command?: string;\n args: string[];\n options: Record<string, string | boolean>;\n};\n\nfunction parse(argv: string[]): Parsed {\n const args: string[] = [];\n const options: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === \"--\") {\n args.push(...argv.slice(i + 1));\n break;\n }\n if (arg === \"--help\" || arg === \"-h\") {\n options[\"help\"] = true;\n continue;\n }\n if (arg.startsWith(\"--\")) {\n const [rawKey, rawValue] = arg.split(\"=\", 2);\n const key = rawKey.slice(2);\n if (key === \"json\" || key === \"no-open\") {\n options[key] = rawValue === undefined ? true : rawValue !== \"false\";\n continue;\n }\n const value = rawValue ?? argv[i + 1];\n if (rawValue === undefined) {\n i += 1;\n }\n options[key] = value ?? \"\";\n continue;\n }\n args.push(arg);\n }\n const [command, ...rest] = args;\n return { command, args: rest, options };\n}\n\nfunction optionString(options: Parsed[\"options\"], key: string): string | undefined {\n const value = options[key];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction optionBoolean(options: Parsed[\"options\"], key: string): boolean {\n return options[key] === true;\n}\n\nfunction localBaseUrl(options: Parsed[\"options\"]): string {\n return (\n optionString(options, \"base-url\") ??\n `http://${DEFAULT_HOST}:${optionString(options, \"port\") ?? DEFAULT_PORT}`\n );\n}\n\nfunction mountRoute(options: Parsed[\"options\"]): string {\n return optionString(options, \"mount-route\") ?? DEFAULT_MOUNT_ROUTE;\n}\n\nfunction dataDir(options: Parsed[\"options\"]): string {\n return optionString(options, \"data-dir\") ?? path.join(homedir(), \".fragno\", \"mcp-fragment\");\n}\n\nfunction fragmentUrl(options: Parsed[\"options\"], route: string): string {\n return `${localBaseUrl(options).replace(/\\/$/, \"\")}${mountRoute(options)}${route}`;\n}\n\nasync function requestJson(method: string, url: string, body?: unknown): Promise<unknown> {\n const response = await fetch(url, {\n method,\n headers: body === undefined ? undefined : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await response.text();\n const payload = text ? JSON.parse(text) : undefined;\n if (!response.ok) {\n const message =\n payload && typeof payload === \"object\" && \"message\" in payload\n ? String((payload as { message: unknown }).message)\n : text || `HTTP ${response.status}`;\n throw new Error(message);\n }\n return payload;\n}\n\nasync function startServer(options: Parsed[\"options\"], logger: Logger): Promise<Server> {\n const dir = dataDir(options);\n await mkdir(dir, { recursive: true });\n process.env[\"FRAGNO_DATA_DIR\"] = dir;\n\n const baseUrl = localBaseUrl(options).replace(/\\/$/, \"\");\n const fragmentMountRoute = mountRoute(options);\n const oauthRedirectUri = `${baseUrl}${fragmentMountRoute}/oauth/callback`;\n const fragment = createMcpFragment(\n {\n allowedOAuthRedirectUris: (url) => url.toString() === oauthRedirectUri,\n },\n { mountRoute: fragmentMountRoute, databaseNamespace: \"mcp-fragment\" },\n );\n logger.log(\"migrating sqlite database if needed...\");\n await migrate(fragment);\n const handler = toNodeHandler(fragment.handler.bind(fragment));\n const server = createServer((req, res) => {\n if (req.url?.startsWith(fragment.mountRoute)) {\n handler(req, res);\n return;\n }\n res.writeHead(404, { \"content-type\": \"text/plain\" }).end(\"Not found\");\n });\n const port = Number(optionString(options, \"port\") ?? DEFAULT_PORT);\n const host = optionString(options, \"host\") ?? DEFAULT_HOST;\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(port, host, () => {\n server.off(\"error\", reject);\n resolve();\n });\n });\n logger.log(`fragno-mcp listening at ${baseUrl}${fragment.mountRoute}`);\n logger.log(`sqlite data dir: ${dir}`);\n return server;\n}\n\nfunction openBrowser(url: string) {\n const command =\n process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n spawn(command, args, { stdio: \"ignore\", detached: true }).unref();\n}\n\nfunction print(value: unknown, json: boolean, logger: Logger) {\n if (json) {\n logger.log(JSON.stringify(value, null, 2));\n return;\n }\n if (\n value &&\n typeof value === \"object\" &&\n \"tools\" in value &&\n Array.isArray((value as { tools: unknown[] }).tools)\n ) {\n for (const tool of (value as { tools: unknown[] }).tools) {\n const record = tool && typeof tool === \"object\" ? (tool as Record<string, unknown>) : {};\n logger.log(\n `${String(record[\"name\"] ?? \"(unnamed)\")}\\t${String(record[\"description\"] ?? \"\")}`,\n );\n }\n return;\n }\n logger.log(JSON.stringify(value, null, 2));\n}\n\nexport async function run(argv: string[], logger: Logger = console): Promise<number> {\n const parsed = parse(argv.slice(2));\n if (optionBoolean(parsed.options, \"help\") || !parsed.command) {\n logger.log(USAGE);\n return parsed.command ? 0 : 1;\n }\n\n try {\n switch (parsed.command) {\n case \"serve\": {\n await startServer(parsed.options, logger);\n await new Promise(() => undefined);\n return 0;\n }\n case \"add\": {\n const [slug, endpointUrl] = parsed.args;\n if (!slug || !endpointUrl) {\n throw new Error(\"Usage: fragno-mcp add <slug> <endpoint-url>\");\n }\n const result = await requestJson(\"POST\", fragmentUrl(parsed.options, \"/servers\"), {\n slug,\n endpointUrl,\n auth: { type: \"none\" },\n });\n print(result, optionBoolean(parsed.options, \"json\"), logger);\n return 0;\n }\n case \"oauth\": {\n const [slug] = parsed.args;\n if (!slug) {\n throw new Error(\"Usage: fragno-mcp oauth <slug>\");\n }\n const startUrl = new URL(\n fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/auth/start`),\n );\n startUrl.searchParams.set(\n MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER,\n fragmentUrl(parsed.options, \"/oauth/callback\"),\n );\n const result = (await requestJson(\"POST\", startUrl.toString(), {\n scope: optionString(parsed.options, \"scope\"),\n clientId: optionString(parsed.options, \"client-id\"),\n clientSecret: optionString(parsed.options, \"client-secret\"),\n })) as { authorizationUrl: string };\n logger.log(`Open this URL to authorize:\\n${result.authorizationUrl}`);\n if (!optionBoolean(parsed.options, \"no-open\")) {\n openBrowser(result.authorizationUrl);\n }\n logger.log(\n \"Keep `fragno-mcp serve` running until the browser redirects back to the local callback.\",\n );\n return 0;\n }\n case \"status\": {\n const [slug] = parsed.args;\n if (!slug) {\n throw new Error(\"Usage: fragno-mcp status <slug>\");\n }\n print(\n await requestJson(\n \"GET\",\n fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/auth/status`),\n ),\n optionBoolean(parsed.options, \"json\"),\n logger,\n );\n return 0;\n }\n case \"tools\": {\n const [slug] = parsed.args;\n if (!slug) {\n throw new Error(\"Usage: fragno-mcp tools <slug>\");\n }\n print(\n await requestJson(\n \"GET\",\n fragmentUrl(parsed.options, `/servers/${encodeURIComponent(slug)}/tools`),\n ),\n optionBoolean(parsed.options, \"json\"),\n logger,\n );\n return 0;\n }\n default:\n throw new Error(`Unknown command: ${parsed.command}`);\n }\n } catch (error) {\n logger.error(`${error instanceof Error ? error.message : String(error)}\\n\\n${USAGE}`);\n return 1;\n }\n}\n\nconst isDirectCliRun = process.argv[1]\n ? import.meta.url === new URL(process.argv[1], \"file:\").href\n : false;\n\nif (isDirectCliRun) {\n const exitCode = await run(process.argv);\n if (typeof exitCode === \"number\") {\n process.exitCode = exitCode;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { listMcpTools } from "./mcp-api.js";
|
|
2
|
+
import { mcpSchema } from "./schema.js";
|
|
3
|
+
import { createMcpOperationAuth, createOAuthCallbackSnapshot, createOAuthStartSnapshot, isMcpAuthReady, mcpAuthReadyUntil, resolveMcpOperationAuth, resolveOAuthStartClient, withoutOAuthConsent, writeOAuthSecretChanges } from "./services.js";
|
|
4
|
+
import { withDatabase } from "@fragno-dev/db";
|
|
5
|
+
import { defineFragment } from "@fragno-dev/core";
|
|
6
|
+
//#region src/definition.ts
|
|
7
|
+
/** Expiry changes readiness without a write, so a write that starts the countdown schedules it. */
|
|
8
|
+
function scheduleMcpAuthExpiry(uow, serverId, authMode, after) {
|
|
9
|
+
const readyUntil = mcpAuthReadyUntil(authMode, after);
|
|
10
|
+
if (readyUntil && after.expiresAt && isMcpAuthReady(authMode, after)) uow.triggerHook("internalAuthExpired", {
|
|
11
|
+
serverId,
|
|
12
|
+
expiresAt: after.expiresAt.toISOString()
|
|
13
|
+
}, { processAt: readyUntil });
|
|
14
|
+
}
|
|
15
|
+
const mcpFragmentDefinition = defineFragment("mcp-fragment").extend(withDatabase(mcpSchema)).provideHooks(({ defineHook, config }) => ({
|
|
16
|
+
internalRefreshServerConfiguration: defineHook(async function({ serverId }) {
|
|
17
|
+
let prepared;
|
|
18
|
+
const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", serverId))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", serverId))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", serverId)))).afterRetrieve(async (_uow, [server, secrets]) => {
|
|
19
|
+
if (!server) return;
|
|
20
|
+
const auth = await createMcpOperationAuth({
|
|
21
|
+
config,
|
|
22
|
+
server,
|
|
23
|
+
secrets
|
|
24
|
+
});
|
|
25
|
+
const toolsOperation = await listMcpTools({
|
|
26
|
+
endpointUrl: server.endpointUrl,
|
|
27
|
+
auth,
|
|
28
|
+
fetchImplementation: config.fetch
|
|
29
|
+
});
|
|
30
|
+
prepared = {
|
|
31
|
+
authChanges: toolsOperation.authChanges,
|
|
32
|
+
toolsOperation
|
|
33
|
+
};
|
|
34
|
+
}).mutate(({ forSchema, retrieveResult: [server, secrets, cache] }) => {
|
|
35
|
+
if (!server) return { action: "missing" };
|
|
36
|
+
if (!prepared) throw new Error("MCP refresh operation was not prepared");
|
|
37
|
+
const preparedOperation = prepared;
|
|
38
|
+
const uow = forSchema(mcpSchema);
|
|
39
|
+
const upsertSecret = (kind, payload, expiresAt) => {
|
|
40
|
+
const existing = secrets.find((secret) => secret.kind === kind);
|
|
41
|
+
if (existing) {
|
|
42
|
+
uow.update("secret", existing.id, (b) => b.set({
|
|
43
|
+
payload,
|
|
44
|
+
expiresAt,
|
|
45
|
+
updatedAt: b.now()
|
|
46
|
+
}).check());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
uow.create("secret", {
|
|
50
|
+
id: `${serverId}:${kind}`,
|
|
51
|
+
serverId,
|
|
52
|
+
kind,
|
|
53
|
+
payload,
|
|
54
|
+
expiresAt
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
if (preparedOperation.authChanges) writeOAuthSecretChanges(preparedOperation.authChanges, {
|
|
58
|
+
upsert: (kind, payload) => {
|
|
59
|
+
upsertSecret(kind, payload, null);
|
|
60
|
+
},
|
|
61
|
+
remove: (kind) => {
|
|
62
|
+
const existing = secrets.find((secret) => secret.kind === kind);
|
|
63
|
+
if (existing) uow.delete("secret", existing.id);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
if (preparedOperation.authChanges?.authPayload) {
|
|
67
|
+
const after = {
|
|
68
|
+
payload: preparedOperation.authChanges.authPayload,
|
|
69
|
+
expiresAt: preparedOperation.authChanges.authExpiresAt ?? null
|
|
70
|
+
};
|
|
71
|
+
upsertSecret("auth", after.payload, after.expiresAt);
|
|
72
|
+
const before = secrets.find((secret) => secret.kind === "auth");
|
|
73
|
+
const ready = isMcpAuthReady(server.authMode, after);
|
|
74
|
+
if (ready !== isMcpAuthReady(server.authMode, before)) uow.triggerHook("onServerReadinessChanged", {
|
|
75
|
+
serverId,
|
|
76
|
+
ready
|
|
77
|
+
});
|
|
78
|
+
scheduleMcpAuthExpiry(uow, serverId, server.authMode, after);
|
|
79
|
+
}
|
|
80
|
+
if (!preparedOperation.toolsOperation.ok) return {
|
|
81
|
+
action: "error",
|
|
82
|
+
error: preparedOperation.toolsOperation.error instanceof Error ? preparedOperation.toolsOperation.error.message : String(preparedOperation.toolsOperation.error)
|
|
83
|
+
};
|
|
84
|
+
const tools = Array.isArray(preparedOperation.toolsOperation.result.tools) ? preparedOperation.toolsOperation.result.tools : [];
|
|
85
|
+
const previousTools = Array.isArray(cache?.tools) ? cache.tools : null;
|
|
86
|
+
if (!previousTools || JSON.stringify(previousTools) !== JSON.stringify(tools)) uow.triggerHook("onServerConfigurationChanged", {
|
|
87
|
+
serverId,
|
|
88
|
+
current: { tools }
|
|
89
|
+
});
|
|
90
|
+
uow.delete("server_connection_cache", serverId);
|
|
91
|
+
uow.create("server_connection_cache", {
|
|
92
|
+
id: serverId,
|
|
93
|
+
serverId,
|
|
94
|
+
protocolVersion: preparedOperation.toolsOperation.metadata.protocolVersion ?? null,
|
|
95
|
+
serverInfo: preparedOperation.toolsOperation.metadata.serverInfo ?? null,
|
|
96
|
+
capabilities: preparedOperation.toolsOperation.metadata.capabilities ?? null,
|
|
97
|
+
tools
|
|
98
|
+
});
|
|
99
|
+
return { action: "refreshed" };
|
|
100
|
+
}).execute();
|
|
101
|
+
if (result.action === "error") throw new Error(result.error);
|
|
102
|
+
}),
|
|
103
|
+
onServerConfigurationChanged: defineHook(async function(payload) {
|
|
104
|
+
await config.onServerConfigurationChanged?.(payload, this);
|
|
105
|
+
}),
|
|
106
|
+
onServerConfigurationDeleted: defineHook(async function(payload) {
|
|
107
|
+
await config.onServerConfigurationDeleted?.(payload, this);
|
|
108
|
+
}),
|
|
109
|
+
onServerReadinessChanged: defineHook(async function(payload) {
|
|
110
|
+
const server = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", payload.serverId)))).transformRetrieve(([server]) => server && {
|
|
111
|
+
name: server.name,
|
|
112
|
+
authMode: server.authMode
|
|
113
|
+
}).execute();
|
|
114
|
+
await config.onServerReadinessChanged?.({
|
|
115
|
+
...payload,
|
|
116
|
+
server
|
|
117
|
+
}, this);
|
|
118
|
+
}),
|
|
119
|
+
internalAuthExpired: defineHook(async function({ serverId, expiresAt }) {
|
|
120
|
+
const expired = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", serverId))).findFirst("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb.and(eb("serverId", "=", serverId), eb("kind", "=", "auth"))))).transformRetrieve(([server, secret]) => server !== null && secret !== null && secret.expiresAt?.getTime() === Date.parse(expiresAt) && mcpAuthReadyUntil(server.authMode, secret) !== null ? {
|
|
121
|
+
name: server.name,
|
|
122
|
+
authMode: server.authMode
|
|
123
|
+
} : null).execute();
|
|
124
|
+
if (expired) await config.onServerReadinessChanged?.({
|
|
125
|
+
serverId,
|
|
126
|
+
ready: false,
|
|
127
|
+
server: expired
|
|
128
|
+
}, this);
|
|
129
|
+
})
|
|
130
|
+
})).providesBaseService(({ defineService, config }) => defineService({
|
|
131
|
+
startOAuth: function(input) {
|
|
132
|
+
return this.serviceTx(mcpSchema).retrieve((uow) => uow.findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", input.serverId))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", input.serverId)))).transformRetrieve(async ([server, secrets]) => {
|
|
133
|
+
if (!server) return { found: false };
|
|
134
|
+
const { authorizationUrl, changes } = await createOAuthStartSnapshot({
|
|
135
|
+
config,
|
|
136
|
+
server,
|
|
137
|
+
secrets,
|
|
138
|
+
stateId: input.stateId,
|
|
139
|
+
redirectUri: input.redirectUri,
|
|
140
|
+
...resolveOAuthStartClient(secrets, input)
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
found: true,
|
|
144
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
145
|
+
serverId: server.id.toString(),
|
|
146
|
+
authMode: server.authMode,
|
|
147
|
+
secrets,
|
|
148
|
+
changes
|
|
149
|
+
};
|
|
150
|
+
}).mutate(({ uow, retrieveResult }) => {
|
|
151
|
+
if (!retrieveResult.found) return { found: false };
|
|
152
|
+
const upsertSecret = (kind, payload, expiresAt) => {
|
|
153
|
+
const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);
|
|
154
|
+
if (existing) {
|
|
155
|
+
uow.update("secret", existing.id, (b) => b.set({
|
|
156
|
+
payload,
|
|
157
|
+
expiresAt,
|
|
158
|
+
updatedAt: b.now()
|
|
159
|
+
}).check());
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
uow.create("secret", {
|
|
163
|
+
id: `${retrieveResult.serverId}:${kind}`,
|
|
164
|
+
serverId: retrieveResult.serverId,
|
|
165
|
+
kind,
|
|
166
|
+
payload,
|
|
167
|
+
expiresAt
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
writeOAuthSecretChanges(retrieveResult.changes, {
|
|
171
|
+
upsert: (kind, payload) => {
|
|
172
|
+
upsertSecret(kind, payload, null);
|
|
173
|
+
},
|
|
174
|
+
remove: (kind) => {
|
|
175
|
+
const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);
|
|
176
|
+
if (existing) uow.delete("secret", existing.id);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const authSecret = retrieveResult.secrets.find((secret) => secret.kind === "auth");
|
|
180
|
+
if (input.discardTokens && authSecret) {
|
|
181
|
+
const payload = withoutOAuthConsent(authSecret.payload);
|
|
182
|
+
uow.update("secret", authSecret.id, (b) => b.set({
|
|
183
|
+
payload,
|
|
184
|
+
expiresAt: null,
|
|
185
|
+
updatedAt: b.now()
|
|
186
|
+
}).check());
|
|
187
|
+
const { authMode } = retrieveResult;
|
|
188
|
+
const ready = isMcpAuthReady(authMode, {
|
|
189
|
+
payload,
|
|
190
|
+
expiresAt: null
|
|
191
|
+
});
|
|
192
|
+
if (ready !== isMcpAuthReady(authMode, authSecret)) uow.triggerHook("onServerReadinessChanged", {
|
|
193
|
+
serverId: retrieveResult.serverId,
|
|
194
|
+
ready
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (retrieveResult.changes.oauthState) uow.create("oauthState", {
|
|
198
|
+
id: retrieveResult.changes.oauthState.id,
|
|
199
|
+
serverId: retrieveResult.serverId,
|
|
200
|
+
authorizationUrl: retrieveResult.authorizationUrl,
|
|
201
|
+
codeVerifier: retrieveResult.changes.oauthState.codeVerifier,
|
|
202
|
+
redirectUri: retrieveResult.changes.oauthState.redirectUri,
|
|
203
|
+
scope: retrieveResult.changes.oauthState.scope ?? null,
|
|
204
|
+
expiresAt: retrieveResult.changes.oauthState.expiresAt,
|
|
205
|
+
consumedAt: null
|
|
206
|
+
});
|
|
207
|
+
return { found: true };
|
|
208
|
+
}).transform(({ retrieveResult }) => retrieveResult.found ? {
|
|
209
|
+
found: true,
|
|
210
|
+
authorizationUrl: retrieveResult.authorizationUrl
|
|
211
|
+
} : { found: false }).build();
|
|
212
|
+
},
|
|
213
|
+
completeOAuthCallback: function(input) {
|
|
214
|
+
const serverId = input.stateId.split(":")[0] ?? "";
|
|
215
|
+
return this.serviceTx(mcpSchema).retrieve((uow) => uow.findFirst("oauthState", (b) => b.whereIndex("primary", (eb) => eb("id", "=", input.stateId))).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", serverId))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", serverId))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", serverId)))).transformRetrieve(async ([state, server, secrets, cache]) => {
|
|
216
|
+
if (!state || state.consumedAt || new Date(state.expiresAt).getTime() < Date.now()) return {
|
|
217
|
+
found: false,
|
|
218
|
+
reason: "invalid_state"
|
|
219
|
+
};
|
|
220
|
+
if (!server) return {
|
|
221
|
+
found: false,
|
|
222
|
+
reason: "server_not_found"
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
found: true,
|
|
226
|
+
server,
|
|
227
|
+
secrets,
|
|
228
|
+
cache,
|
|
229
|
+
changes: await createOAuthCallbackSnapshot({
|
|
230
|
+
config,
|
|
231
|
+
stateId: input.stateId,
|
|
232
|
+
code: input.code,
|
|
233
|
+
iss: input.iss,
|
|
234
|
+
state,
|
|
235
|
+
server,
|
|
236
|
+
secrets
|
|
237
|
+
})
|
|
238
|
+
};
|
|
239
|
+
}).mutate(({ uow, retrieveResult }) => {
|
|
240
|
+
if (!retrieveResult.found) return {
|
|
241
|
+
found: false,
|
|
242
|
+
reason: retrieveResult.reason
|
|
243
|
+
};
|
|
244
|
+
const serverId = retrieveResult.server.id.toString();
|
|
245
|
+
const upsertSecret = (kind, payload, expiresAt) => {
|
|
246
|
+
const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);
|
|
247
|
+
if (existing) {
|
|
248
|
+
uow.update("secret", existing.id, (b) => b.set({
|
|
249
|
+
payload,
|
|
250
|
+
expiresAt,
|
|
251
|
+
updatedAt: b.now()
|
|
252
|
+
}).check());
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
uow.create("secret", {
|
|
256
|
+
id: `${serverId}:${kind}`,
|
|
257
|
+
serverId,
|
|
258
|
+
kind,
|
|
259
|
+
payload,
|
|
260
|
+
expiresAt
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
writeOAuthSecretChanges(retrieveResult.changes, {
|
|
264
|
+
upsert: (kind, payload) => {
|
|
265
|
+
upsertSecret(kind, payload, null);
|
|
266
|
+
},
|
|
267
|
+
remove: (kind) => {
|
|
268
|
+
const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);
|
|
269
|
+
if (existing) uow.delete("secret", existing.id);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
if (retrieveResult.changes.authPayload) {
|
|
273
|
+
const after = {
|
|
274
|
+
payload: retrieveResult.changes.authPayload,
|
|
275
|
+
expiresAt: retrieveResult.changes.authExpiresAt ?? null
|
|
276
|
+
};
|
|
277
|
+
upsertSecret("auth", after.payload, after.expiresAt);
|
|
278
|
+
uow.update("server_configuration", retrieveResult.server.id, (b) => b.set({
|
|
279
|
+
authMode: "oauth",
|
|
280
|
+
updatedAt: b.now()
|
|
281
|
+
}).check());
|
|
282
|
+
const before = retrieveResult.secrets.find((secret) => secret.kind === "auth");
|
|
283
|
+
const ready = isMcpAuthReady("oauth", after);
|
|
284
|
+
if (ready !== isMcpAuthReady(retrieveResult.server.authMode, before)) uow.triggerHook("onServerReadinessChanged", {
|
|
285
|
+
serverId,
|
|
286
|
+
ready
|
|
287
|
+
});
|
|
288
|
+
scheduleMcpAuthExpiry(uow, serverId, "oauth", after);
|
|
289
|
+
if (retrieveResult.cache) uow.delete("server_connection_cache", retrieveResult.cache.id);
|
|
290
|
+
uow.triggerHook("internalRefreshServerConfiguration", { serverId });
|
|
291
|
+
}
|
|
292
|
+
if (retrieveResult.changes.consumeStateId) uow.update("oauthState", input.stateId, (b) => b.set({ consumedAt: b.now() }));
|
|
293
|
+
return { found: true };
|
|
294
|
+
}).transform(({ retrieveResult }) => retrieveResult.found ? { found: true } : {
|
|
295
|
+
found: false,
|
|
296
|
+
reason: retrieveResult.reason
|
|
297
|
+
}).build();
|
|
298
|
+
},
|
|
299
|
+
prepareMcpOperation: function(input) {
|
|
300
|
+
return this.serviceTx(mcpSchema).retrieve((uow) => uow.findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", input.serverId))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", input.serverId))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", input.serverId)))).transformRetrieve(async ([server, secrets, cache]) => {
|
|
301
|
+
if (!server) return { found: false };
|
|
302
|
+
const resolved = await resolveMcpOperationAuth({
|
|
303
|
+
config,
|
|
304
|
+
server,
|
|
305
|
+
secrets
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
found: true,
|
|
309
|
+
serverId: server.id.toString(),
|
|
310
|
+
endpointUrl: server.endpointUrl,
|
|
311
|
+
token: resolved.token,
|
|
312
|
+
secrets,
|
|
313
|
+
cache,
|
|
314
|
+
authChanges: resolved.authChanges
|
|
315
|
+
};
|
|
316
|
+
}).transform(({ retrieveResult }) => retrieveResult.found ? {
|
|
317
|
+
found: true,
|
|
318
|
+
endpointUrl: retrieveResult.endpointUrl,
|
|
319
|
+
token: retrieveResult.token,
|
|
320
|
+
secrets: retrieveResult.secrets,
|
|
321
|
+
cache: retrieveResult.cache,
|
|
322
|
+
authChanges: retrieveResult.authChanges,
|
|
323
|
+
serverId: retrieveResult.serverId
|
|
324
|
+
} : { found: false }).build();
|
|
325
|
+
}
|
|
326
|
+
})).build();
|
|
327
|
+
//#endregion
|
|
328
|
+
export { mcpFragmentDefinition, scheduleMcpAuthExpiry };
|
|
329
|
+
|
|
330
|
+
//# sourceMappingURL=definition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definition.js","names":[],"sources":["../../src/definition.ts"],"sourcesContent":["import { defineFragment } from \"@fragno-dev/core\";\nimport { withDatabase, type HookContext, type HookFn } from \"@fragno-dev/db\";\n\nimport { listMcpTools } from \"./mcp-api\";\nimport type { McpFragmentConfig as BaseMcpFragmentConfig } from \"./mcp-types\";\nimport { mcpSchema } from \"./schema\";\nimport {\n createOAuthCallbackSnapshot,\n createMcpOperationAuth,\n createOAuthStartSnapshot,\n isMcpAuthReady,\n mcpAuthReadyUntil,\n resolveMcpOperationAuth,\n resolveOAuthStartClient,\n withoutOAuthConsent,\n writeOAuthSecretChanges,\n type AuthPersistenceChanges,\n} from \"./services\";\n\nexport interface McpServerConfigurationChangedPayload {\n serverId: string;\n current: {\n tools: unknown[];\n };\n}\n\nexport interface McpServerConfigurationDeletedPayload {\n serverId: string;\n}\n\nexport interface McpServerReadinessChangedPayload {\n serverId: string;\n /** Whether stored auth can authorize operations; the server can still reject it. */\n ready: boolean;\n}\n\n/**\n * A readiness change as delivered. The server is read at delivery rather than stored with the hook,\n * so it is the current configuration, or null after it was deleted.\n */\nexport interface McpServerReadinessChange extends McpServerReadinessChangedPayload {\n server: { name: string | null; authMode: string } | null;\n}\n\nexport interface McpFragmentHooksConfig {\n onServerConfigurationChanged?: (\n payload: McpServerConfigurationChangedPayload,\n context: HookContext,\n ) => Promise<void> | void;\n onServerConfigurationDeleted?: (\n payload: McpServerConfigurationDeletedPayload,\n context: HookContext,\n ) => Promise<void> | void;\n /**\n * Fires when a write changes whether the server can authorize operations, including creation\n * and credentials the server invalidates during an operation, and when an access token without a\n * refresh token expires.\n */\n onServerReadinessChanged?: (\n payload: McpServerReadinessChange,\n context: HookContext,\n ) => Promise<void> | void;\n}\n\nexport type McpFragmentConfig = BaseMcpFragmentConfig & McpFragmentHooksConfig;\n\nexport interface McpInternalRefreshServerConfigurationPayload {\n serverId: string;\n}\n\n/** The token stored when expiry was scheduled; a later write replaces it and reports itself. */\ninterface McpInternalAuthExpiredPayload {\n serverId: string;\n expiresAt: string;\n}\n\nexport type McpHooksMap = {\n internalRefreshServerConfiguration: HookFn<McpInternalRefreshServerConfigurationPayload>;\n internalAuthExpired: HookFn<McpInternalAuthExpiredPayload>;\n onServerConfigurationChanged: HookFn<McpServerConfigurationChangedPayload>;\n onServerConfigurationDeleted: HookFn<McpServerConfigurationDeletedPayload>;\n onServerReadinessChanged: HookFn<McpServerReadinessChangedPayload>;\n};\n\n/** Expiry changes readiness without a write, so a write that starts the countdown schedules it. */\nexport function scheduleMcpAuthExpiry(\n // Hook transactions type their hooks loosely, so only the one trigger is required.\n uow: {\n triggerHook(\n name: \"internalAuthExpired\",\n payload: McpInternalAuthExpiredPayload,\n options: { processAt: Date },\n ): void;\n },\n serverId: string,\n authMode: string,\n after: { payload: string; expiresAt: Date | null },\n) {\n const readyUntil = mcpAuthReadyUntil(authMode, after);\n if (readyUntil && after.expiresAt && isMcpAuthReady(authMode, after)) {\n uow.triggerHook(\n \"internalAuthExpired\",\n { serverId, expiresAt: after.expiresAt.toISOString() },\n { processAt: readyUntil },\n );\n }\n}\n\nexport const mcpFragmentDefinition = defineFragment<McpFragmentConfig>(\"mcp-fragment\")\n .extend(withDatabase(mcpSchema))\n .provideHooks<McpHooksMap>(({ defineHook, config }) => ({\n internalRefreshServerConfiguration: defineHook(async function ({ serverId }) {\n let prepared:\n | {\n authChanges?: AuthPersistenceChanges;\n toolsOperation: Awaited<ReturnType<typeof listMcpTools<AuthPersistenceChanges>>>;\n }\n | undefined;\n\n const result = await this.handlerTx()\n .retrieve(({ forSchema }) =>\n forSchema(mcpSchema)\n .findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", serverId)),\n )\n .find(\"secret\", (b) =>\n b.whereIndex(\"idx_secret_server_kind\", (eb) => eb(\"serverId\", \"=\", serverId)),\n )\n .findFirst(\"server_connection_cache\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", serverId)),\n ),\n )\n .afterRetrieve(async (_uow, [server, secrets]) => {\n if (!server) {\n return;\n }\n\n const auth = await createMcpOperationAuth({ config, server, secrets });\n const toolsOperation = await listMcpTools({\n endpointUrl: server.endpointUrl,\n auth,\n fetchImplementation: config.fetch,\n });\n prepared = {\n authChanges: toolsOperation.authChanges,\n toolsOperation,\n };\n })\n .mutate(({ forSchema, retrieveResult: [server, secrets, cache] }) => {\n if (!server) {\n return { action: \"missing\" as const };\n }\n if (!prepared) {\n throw new Error(\"MCP refresh operation was not prepared\");\n }\n\n const preparedOperation = prepared;\n const uow = forSchema(mcpSchema);\n const upsertSecret = (kind: string, payload: string, expiresAt: Date | null) => {\n const existing = secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.update(\"secret\", existing.id, (b) =>\n b.set({ payload, expiresAt, updatedAt: b.now() }).check(),\n );\n return;\n }\n uow.create(\"secret\", {\n id: `${serverId}:${kind}`,\n serverId,\n kind,\n payload,\n expiresAt,\n });\n };\n\n if (preparedOperation.authChanges) {\n writeOAuthSecretChanges(preparedOperation.authChanges, {\n upsert: (kind, payload) => {\n upsertSecret(kind, payload, null);\n },\n remove: (kind) => {\n const existing = secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.delete(\"secret\", existing.id);\n }\n },\n });\n }\n if (preparedOperation.authChanges?.authPayload) {\n const after = {\n payload: preparedOperation.authChanges.authPayload,\n expiresAt: preparedOperation.authChanges.authExpiresAt ?? null,\n };\n upsertSecret(\"auth\", after.payload, after.expiresAt);\n const before = secrets.find((secret) => secret.kind === \"auth\");\n const ready = isMcpAuthReady(server.authMode, after);\n if (ready !== isMcpAuthReady(server.authMode, before)) {\n uow.triggerHook(\"onServerReadinessChanged\", { serverId, ready });\n }\n scheduleMcpAuthExpiry(uow, serverId, server.authMode, after);\n }\n\n if (!preparedOperation.toolsOperation.ok) {\n return {\n action: \"error\" as const,\n error:\n preparedOperation.toolsOperation.error instanceof Error\n ? preparedOperation.toolsOperation.error.message\n : String(preparedOperation.toolsOperation.error),\n };\n }\n\n const tools = Array.isArray(preparedOperation.toolsOperation.result.tools)\n ? preparedOperation.toolsOperation.result.tools\n : [];\n const previousTools = Array.isArray(cache?.tools) ? cache.tools : null;\n if (!previousTools || JSON.stringify(previousTools) !== JSON.stringify(tools)) {\n uow.triggerHook(\"onServerConfigurationChanged\", {\n serverId,\n current: { tools },\n });\n }\n\n uow.delete(\"server_connection_cache\", serverId);\n uow.create(\"server_connection_cache\", {\n id: serverId,\n serverId,\n protocolVersion: preparedOperation.toolsOperation.metadata.protocolVersion ?? null,\n serverInfo: preparedOperation.toolsOperation.metadata.serverInfo ?? null,\n capabilities: preparedOperation.toolsOperation.metadata.capabilities ?? null,\n tools,\n });\n\n return { action: \"refreshed\" as const };\n })\n .execute();\n\n if (result.action === \"error\") {\n throw new Error(result.error);\n }\n }),\n onServerConfigurationChanged: defineHook(async function (payload) {\n await config.onServerConfigurationChanged?.(payload, this);\n }),\n onServerConfigurationDeleted: defineHook(async function (payload) {\n await config.onServerConfigurationDeleted?.(payload, this);\n }),\n onServerReadinessChanged: defineHook(async function (payload) {\n const server = await this.handlerTx()\n .retrieve(({ forSchema }) =>\n forSchema(mcpSchema).findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", payload.serverId)),\n ),\n )\n .transformRetrieve(([server]) => server && { name: server.name, authMode: server.authMode })\n .execute();\n await config.onServerReadinessChanged?.({ ...payload, server }, this);\n }),\n internalAuthExpired: defineHook(async function ({ serverId, expiresAt }) {\n const expired = await this.handlerTx()\n .retrieve(({ forSchema }) =>\n forSchema(mcpSchema)\n .findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", serverId)),\n )\n .findFirst(\"secret\", (b) =>\n b.whereIndex(\"idx_secret_server_kind\", (eb) =>\n eb.and(eb(\"serverId\", \"=\", serverId), eb(\"kind\", \"=\", \"auth\")),\n ),\n ),\n )\n .transformRetrieve(([server, secret]) =>\n // The hook's due time already passed, so an untouched token is the expired one.\n server !== null &&\n secret !== null &&\n secret.expiresAt?.getTime() === Date.parse(expiresAt) &&\n mcpAuthReadyUntil(server.authMode, secret) !== null\n ? { name: server.name, authMode: server.authMode }\n : null,\n )\n .execute();\n if (expired) {\n await config.onServerReadinessChanged?.({ serverId, ready: false, server: expired }, this);\n }\n }),\n }))\n .providesBaseService(({ defineService, config }) =>\n defineService({\n startOAuth: function (input: {\n serverId: string;\n stateId: string;\n redirectUri: string;\n scope?: string;\n clientId?: string;\n clientSecret?: string;\n discardTokens: boolean;\n }) {\n return this.serviceTx(mcpSchema)\n .retrieve((uow) =>\n uow\n .findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", input.serverId)),\n )\n .find(\"secret\", (b) =>\n b.whereIndex(\"idx_secret_server_kind\", (eb) => eb(\"serverId\", \"=\", input.serverId)),\n ),\n )\n .transformRetrieve(async ([server, secrets]) => {\n if (!server) {\n return { found: false as const };\n }\n const { authorizationUrl, changes } = await createOAuthStartSnapshot({\n config,\n server,\n secrets,\n stateId: input.stateId,\n redirectUri: input.redirectUri,\n ...resolveOAuthStartClient(secrets, input),\n });\n return {\n found: true as const,\n authorizationUrl: authorizationUrl.toString(),\n serverId: server.id.toString(),\n authMode: server.authMode,\n secrets,\n changes,\n };\n })\n .mutate(({ uow, retrieveResult }) => {\n if (!retrieveResult.found) {\n return { found: false as const };\n }\n\n const upsertSecret = (kind: string, payload: string, expiresAt: Date | null) => {\n const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.update(\"secret\", existing.id, (b) =>\n b.set({ payload, expiresAt, updatedAt: b.now() }).check(),\n );\n return;\n }\n uow.create(\"secret\", {\n id: `${retrieveResult.serverId}:${kind}`,\n serverId: retrieveResult.serverId,\n kind,\n payload,\n expiresAt,\n });\n };\n\n writeOAuthSecretChanges(retrieveResult.changes, {\n upsert: (kind, payload) => {\n upsertSecret(kind, payload, null);\n },\n remove: (kind) => {\n const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.delete(\"secret\", existing.id);\n }\n },\n });\n const authSecret = retrieveResult.secrets.find((secret) => secret.kind === \"auth\");\n if (input.discardTokens && authSecret) {\n // Keeps the client configuration; only consent evidence is discarded.\n const payload = withoutOAuthConsent(authSecret.payload);\n uow.update(\"secret\", authSecret.id, (b) =>\n b.set({ payload, expiresAt: null, updatedAt: b.now() }).check(),\n );\n const { authMode } = retrieveResult;\n const ready = isMcpAuthReady(authMode, { payload, expiresAt: null });\n if (ready !== isMcpAuthReady(authMode, authSecret)) {\n uow.triggerHook(\"onServerReadinessChanged\", {\n serverId: retrieveResult.serverId,\n ready,\n });\n }\n }\n if (retrieveResult.changes.oauthState) {\n uow.create(\"oauthState\", {\n id: retrieveResult.changes.oauthState.id,\n serverId: retrieveResult.serverId,\n // The link carries only the PKCE challenge, never the verifier.\n authorizationUrl: retrieveResult.authorizationUrl,\n codeVerifier: retrieveResult.changes.oauthState.codeVerifier,\n redirectUri: retrieveResult.changes.oauthState.redirectUri,\n scope: retrieveResult.changes.oauthState.scope ?? null,\n expiresAt: retrieveResult.changes.oauthState.expiresAt,\n consumedAt: null,\n });\n }\n\n return { found: true as const };\n })\n .transform(({ retrieveResult }) =>\n retrieveResult.found\n ? {\n found: true as const,\n authorizationUrl: retrieveResult.authorizationUrl,\n }\n : { found: false as const },\n )\n .build();\n },\n\n completeOAuthCallback: function (input: {\n stateId: string;\n code: string;\n iss: string | undefined;\n }) {\n const serverId = input.stateId.split(\":\")[0] ?? \"\";\n return this.serviceTx(mcpSchema)\n .retrieve((uow) =>\n uow\n .findFirst(\"oauthState\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", input.stateId)),\n )\n .findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", serverId)),\n )\n .find(\"secret\", (b) =>\n b.whereIndex(\"idx_secret_server_kind\", (eb) => eb(\"serverId\", \"=\", serverId)),\n )\n .findFirst(\"server_connection_cache\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", serverId)),\n ),\n )\n .transformRetrieve(async ([state, server, secrets, cache]) => {\n if (!state || state.consumedAt || new Date(state.expiresAt).getTime() < Date.now()) {\n return { found: false as const, reason: \"invalid_state\" as const };\n }\n if (!server) {\n return { found: false as const, reason: \"server_not_found\" as const };\n }\n const changes = await createOAuthCallbackSnapshot({\n config,\n stateId: input.stateId,\n code: input.code,\n iss: input.iss,\n state,\n server,\n secrets,\n });\n return { found: true as const, server, secrets, cache, changes };\n })\n .mutate(({ uow, retrieveResult }) => {\n if (!retrieveResult.found) {\n return { found: false as const, reason: retrieveResult.reason };\n }\n\n const serverId = retrieveResult.server.id.toString();\n const upsertSecret = (kind: string, payload: string, expiresAt: Date | null) => {\n const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.update(\"secret\", existing.id, (b) =>\n b.set({ payload, expiresAt, updatedAt: b.now() }).check(),\n );\n return;\n }\n uow.create(\"secret\", {\n id: `${serverId}:${kind}`,\n serverId,\n kind,\n payload,\n expiresAt,\n });\n };\n\n writeOAuthSecretChanges(retrieveResult.changes, {\n upsert: (kind, payload) => {\n upsertSecret(kind, payload, null);\n },\n remove: (kind) => {\n const existing = retrieveResult.secrets.find((secret) => secret.kind === kind);\n if (existing) {\n uow.delete(\"secret\", existing.id);\n }\n },\n });\n if (retrieveResult.changes.authPayload) {\n const after = {\n payload: retrieveResult.changes.authPayload,\n expiresAt: retrieveResult.changes.authExpiresAt ?? null,\n };\n upsertSecret(\"auth\", after.payload, after.expiresAt);\n uow.update(\"server_configuration\", retrieveResult.server.id, (b) =>\n b.set({ authMode: \"oauth\", updatedAt: b.now() }).check(),\n );\n const before = retrieveResult.secrets.find((secret) => secret.kind === \"auth\");\n const ready = isMcpAuthReady(\"oauth\", after);\n if (ready !== isMcpAuthReady(retrieveResult.server.authMode, before)) {\n uow.triggerHook(\"onServerReadinessChanged\", { serverId, ready });\n }\n scheduleMcpAuthExpiry(uow, serverId, \"oauth\", after);\n if (retrieveResult.cache) {\n uow.delete(\"server_connection_cache\", retrieveResult.cache.id);\n }\n uow.triggerHook(\"internalRefreshServerConfiguration\", { serverId });\n }\n if (retrieveResult.changes.consumeStateId) {\n uow.update(\"oauthState\", input.stateId, (b) => b.set({ consumedAt: b.now() }));\n }\n\n return { found: true as const };\n })\n .transform(({ retrieveResult }) =>\n retrieveResult.found\n ? { found: true as const }\n : { found: false as const, reason: retrieveResult.reason },\n )\n .build();\n },\n\n prepareMcpOperation: function (input: { serverId: string }) {\n return this.serviceTx(mcpSchema)\n .retrieve((uow) =>\n uow\n .findFirst(\"server_configuration\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", input.serverId)),\n )\n .find(\"secret\", (b) =>\n b.whereIndex(\"idx_secret_server_kind\", (eb) => eb(\"serverId\", \"=\", input.serverId)),\n )\n .findFirst(\"server_connection_cache\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", input.serverId)),\n ),\n )\n .transformRetrieve(async ([server, secrets, cache]) => {\n if (!server) {\n return { found: false as const };\n }\n const resolved = await resolveMcpOperationAuth({ config, server, secrets });\n return {\n found: true as const,\n serverId: server.id.toString(),\n endpointUrl: server.endpointUrl,\n token: resolved.token,\n secrets,\n cache,\n authChanges: resolved.authChanges,\n };\n })\n .transform(({ retrieveResult }) =>\n retrieveResult.found\n ? {\n found: true as const,\n endpointUrl: retrieveResult.endpointUrl,\n token: retrieveResult.token,\n secrets: retrieveResult.secrets,\n cache: retrieveResult.cache,\n authChanges: retrieveResult.authChanges,\n serverId: retrieveResult.serverId,\n }\n : { found: false as const },\n )\n .build();\n },\n }),\n )\n .build();\n"],"mappings":"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"}
|