@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.
Files changed (90) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +312 -2
  3. package/bin/run.js +7 -0
  4. package/dist/browser/client/react.d.ts +253 -0
  5. package/dist/browser/client/react.d.ts.map +1 -0
  6. package/dist/browser/client/react.js +194 -0
  7. package/dist/browser/client/react.js.map +1 -0
  8. package/dist/browser/client/solid.d.ts +256 -0
  9. package/dist/browser/client/solid.d.ts.map +1 -0
  10. package/dist/browser/client/solid.js +129 -0
  11. package/dist/browser/client/solid.js.map +1 -0
  12. package/dist/browser/client/svelte.d.ts +256 -0
  13. package/dist/browser/client/svelte.d.ts.map +1 -0
  14. package/dist/browser/client/svelte.js +131 -0
  15. package/dist/browser/client/svelte.js.map +1 -0
  16. package/dist/browser/client/vanilla.d.ts +283 -0
  17. package/dist/browser/client/vanilla.d.ts.map +1 -0
  18. package/dist/browser/client/vanilla.js +159 -0
  19. package/dist/browser/client/vanilla.js.map +1 -0
  20. package/dist/browser/client/vue.d.ts +256 -0
  21. package/dist/browser/client/vue.d.ts.map +1 -0
  22. package/dist/browser/client/vue.js +127 -0
  23. package/dist/browser/client/vue.js.map +1 -0
  24. package/dist/browser/client-BCzNDSlW.d.ts +711 -0
  25. package/dist/browser/client-BCzNDSlW.d.ts.map +1 -0
  26. package/dist/browser/index.d.ts +813 -0
  27. package/dist/browser/index.d.ts.map +1 -0
  28. package/dist/browser/index.js +1 -0
  29. package/dist/browser/src-B6dHZXqB.js +2303 -0
  30. package/dist/browser/src-B6dHZXqB.js.map +1 -0
  31. package/dist/cli/cli.d.ts +6 -0
  32. package/dist/cli/cli.d.ts.map +1 -0
  33. package/dist/cli/cli.js +227 -0
  34. package/dist/cli/cli.js.map +1 -0
  35. package/dist/cli/definition.js +330 -0
  36. package/dist/cli/definition.js.map +1 -0
  37. package/dist/cli/diagnostics.js +93 -0
  38. package/dist/cli/diagnostics.js.map +1 -0
  39. package/dist/cli/index.js +13 -0
  40. package/dist/cli/index.js.map +1 -0
  41. package/dist/cli/mcp-api.js +87 -0
  42. package/dist/cli/mcp-api.js.map +1 -0
  43. package/dist/cli/mcp-types.js +100 -0
  44. package/dist/cli/mcp-types.js.map +1 -0
  45. package/dist/cli/oauth-provider.js +113 -0
  46. package/dist/cli/oauth-provider.js.map +1 -0
  47. package/dist/cli/routes.js +743 -0
  48. package/dist/cli/routes.js.map +1 -0
  49. package/dist/cli/schema.js +13 -0
  50. package/dist/cli/schema.js.map +1 -0
  51. package/dist/cli/services.js +330 -0
  52. package/dist/cli/services.js.map +1 -0
  53. package/dist/node/definition.d.ts +260 -0
  54. package/dist/node/definition.d.ts.map +1 -0
  55. package/dist/node/definition.js +330 -0
  56. package/dist/node/definition.js.map +1 -0
  57. package/dist/node/diagnostics.js +93 -0
  58. package/dist/node/diagnostics.js.map +1 -0
  59. package/dist/node/index.d.ts +694 -0
  60. package/dist/node/index.d.ts.map +1 -0
  61. package/dist/node/index.js +26 -0
  62. package/dist/node/index.js.map +1 -0
  63. package/dist/node/mcp-api.d.ts +2 -0
  64. package/dist/node/mcp-api.js +87 -0
  65. package/dist/node/mcp-api.js.map +1 -0
  66. package/dist/node/mcp-types.d.ts +146 -0
  67. package/dist/node/mcp-types.d.ts.map +1 -0
  68. package/dist/node/mcp-types.js +100 -0
  69. package/dist/node/mcp-types.js.map +1 -0
  70. package/dist/node/oauth-provider.d.ts +24 -0
  71. package/dist/node/oauth-provider.d.ts.map +1 -0
  72. package/dist/node/oauth-provider.js +113 -0
  73. package/dist/node/oauth-provider.js.map +1 -0
  74. package/dist/node/routes.d.ts +466 -0
  75. package/dist/node/routes.d.ts.map +1 -0
  76. package/dist/node/routes.js +743 -0
  77. package/dist/node/routes.js.map +1 -0
  78. package/dist/node/schema.d.ts +9 -0
  79. package/dist/node/schema.d.ts.map +1 -0
  80. package/dist/node/schema.js +13 -0
  81. package/dist/node/schema.js.map +1 -0
  82. package/dist/node/services.d.ts +40 -0
  83. package/dist/node/services.d.ts.map +1 -0
  84. package/dist/node/services.js +330 -0
  85. package/dist/node/services.js.map +1 -0
  86. package/dist/node/testing/mcp-test-server.d.ts +42 -0
  87. package/dist/node/testing/mcp-test-server.d.ts.map +1 -0
  88. package/dist/node/testing/mcp-test-server.js +218 -0
  89. package/dist/node/testing/mcp-test-server.js.map +1 -0
  90. package/package.json +130 -3
@@ -0,0 +1,743 @@
1
+ import { assertAllowedEndpoint, callMcpTool, listMcpTools, stringifySecretPayload } from "./mcp-api.js";
2
+ import { mcpSchema } from "./schema.js";
3
+ import { createMcpOperationAuth, isMcpAuthReady, pendingOAuthLink, projectMcpAuthStatus, withoutOAuthConsent, writeOAuthSecretChanges } from "./services.js";
4
+ import { mcpFragmentDefinition, scheduleMcpAuthExpiry } from "./definition.js";
5
+ import { MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER, createServerInputSchema, mcpAuthStatusSchema, mcpOAuthPendingSchema, mcpOAuthRedirectUriSchema, mcpToolCallResultSchema, mcpToolSchema, oauthStartInputSchema, replaceServerConfigurationInputSchema, tokenAuthInputSchema, toolCallInputSchema } from "./mcp-types.js";
6
+ import { buildAuthDiagnostics, errorMessage, refreshOutputSchema } from "./diagnostics.js";
7
+ import { defineRoutes } from "@fragno-dev/core";
8
+ import { z } from "zod";
9
+ //#region src/routes.ts
10
+ const serverConnectionCacheOutputSchema = z.object({
11
+ protocolVersion: z.string().nullable().optional(),
12
+ serverInfo: z.unknown().nullable().optional(),
13
+ capabilities: z.unknown().nullable().optional(),
14
+ tools: z.array(mcpToolSchema).nullable().optional(),
15
+ updatedAt: z.union([z.string(), z.date()]).optional()
16
+ });
17
+ const serverOutputSchema = z.object({
18
+ slug: z.string(),
19
+ name: z.string().nullable().optional(),
20
+ endpointUrl: z.string(),
21
+ authMode: z.string(),
22
+ cache: serverConnectionCacheOutputSchema.nullable().optional()
23
+ });
24
+ function publicServer(server, cache) {
25
+ return {
26
+ slug: server.id.toString(),
27
+ name: server.name,
28
+ endpointUrl: server.endpointUrl,
29
+ authMode: server.authMode,
30
+ ...cache ? { cache: {
31
+ protocolVersion: cache.protocolVersion ?? null,
32
+ serverInfo: cache.serverInfo ?? null,
33
+ capabilities: cache.capabilities ?? null,
34
+ tools: Array.isArray(cache.tools) ? cache.tools : null,
35
+ updatedAt: cache.updatedAt
36
+ } } : {}
37
+ };
38
+ }
39
+ function authMode(auth) {
40
+ return auth.type;
41
+ }
42
+ const mcpRoutesFactory = defineRoutes(mcpFragmentDefinition).create(({ services, defineRoute, config }) => {
43
+ return [
44
+ defineRoute({
45
+ method: "POST",
46
+ path: "/servers",
47
+ inputSchema: createServerInputSchema,
48
+ outputSchema: serverOutputSchema,
49
+ errorCodes: ["SERVER_EXISTS", "ENDPOINT_NOT_ALLOWED"],
50
+ handler: async function({ input }, { json, error }) {
51
+ const body = await input.valid();
52
+ try {
53
+ assertAllowedEndpoint(body.endpointUrl, config);
54
+ } catch (err) {
55
+ return error({
56
+ code: "ENDPOINT_NOT_ALLOWED",
57
+ message: err.message
58
+ }, 400);
59
+ }
60
+ const payload = body.auth.type === "none" ? void 0 : stringifySecretPayload(body.auth);
61
+ if ((await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", body.slug)))).mutate(({ forSchema, retrieveResult: [existing] }) => {
62
+ if (existing) return { exists: true };
63
+ const uow = forSchema(mcpSchema);
64
+ uow.create("server_configuration", {
65
+ id: body.slug,
66
+ name: body.name ?? null,
67
+ endpointUrl: body.endpointUrl,
68
+ authMode: authMode(body.auth)
69
+ });
70
+ if (payload) uow.create("secret", {
71
+ id: `${body.slug}:auth`,
72
+ serverId: body.slug,
73
+ kind: "auth",
74
+ payload,
75
+ expiresAt: null
76
+ });
77
+ if (body.auth.type !== "oauth") uow.triggerHook("internalRefreshServerConfiguration", { serverId: body.slug });
78
+ uow.triggerHook("onServerReadinessChanged", {
79
+ serverId: body.slug,
80
+ ready: isMcpAuthReady(authMode(body.auth), payload === void 0 ? void 0 : {
81
+ payload,
82
+ expiresAt: null
83
+ })
84
+ });
85
+ return { exists: false };
86
+ }).execute()).exists) return error({
87
+ code: "SERVER_EXISTS",
88
+ message: "MCP server already exists"
89
+ }, 409);
90
+ return json(publicServer({
91
+ ...body,
92
+ id: body.slug,
93
+ authMode: authMode(body.auth)
94
+ }), 201);
95
+ }
96
+ }),
97
+ defineRoute({
98
+ method: "GET",
99
+ path: "/servers",
100
+ outputSchema: z.object({ servers: z.array(serverOutputSchema) }),
101
+ handler: async function(_, { json }) {
102
+ const [servers, caches] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).find("server_configuration", (b) => b.whereIndex("primary")).find("server_connection_cache", (b) => b.whereIndex("primary"))).execute();
103
+ const cacheByServerId = new Map(caches.map((cache) => [cache.id.toString(), cache]));
104
+ return json({ servers: servers.map((server) => publicServer(server, cacheByServerId.get(server.id.toString()))) });
105
+ }
106
+ }),
107
+ defineRoute({
108
+ method: "GET",
109
+ path: "/servers/:slug",
110
+ outputSchema: serverOutputSchema,
111
+ errorCodes: ["SERVER_NOT_FOUND"],
112
+ handler: async function({ pathParams }, { json, error }) {
113
+ const [server, cache] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).execute();
114
+ if (!server) return error({
115
+ code: "SERVER_NOT_FOUND",
116
+ message: "MCP server not found"
117
+ }, 404);
118
+ return json(publicServer(server, cache));
119
+ }
120
+ }),
121
+ defineRoute({
122
+ method: "PUT",
123
+ path: "/servers/:slug/configuration",
124
+ inputSchema: replaceServerConfigurationInputSchema,
125
+ outputSchema: serverOutputSchema,
126
+ errorCodes: ["SERVER_NOT_FOUND", "ENDPOINT_NOT_ALLOWED"],
127
+ handler: async function({ input, pathParams }, { json, error }) {
128
+ const body = await input.valid();
129
+ try {
130
+ assertAllowedEndpoint(body.endpointUrl, config);
131
+ } catch (err) {
132
+ return error({
133
+ code: "ENDPOINT_NOT_ALLOWED",
134
+ message: err.message
135
+ }, 400);
136
+ }
137
+ const payload = body.auth.type === "none" ? null : stringifySecretPayload(body.auth);
138
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_server", (eb) => eb("serverId", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [server, secrets, oauthStates, cache] }) => {
139
+ if (!server) return { server: null };
140
+ const uow = forSchema(mcpSchema);
141
+ const replaced = {
142
+ ...server,
143
+ name: body.name ?? null,
144
+ endpointUrl: body.endpointUrl,
145
+ authMode: authMode(body.auth)
146
+ };
147
+ uow.update("server_configuration", server.id, (b) => b.set({
148
+ name: replaced.name,
149
+ endpointUrl: replaced.endpointUrl,
150
+ authMode: replaced.authMode,
151
+ updatedAt: b.now()
152
+ }).check());
153
+ const authSecret = secrets.find((secret) => secret.kind === "auth");
154
+ for (const secret of secrets) if (secret !== authSecret || payload === null) uow.delete("secret", secret.id);
155
+ if (payload !== null) {
156
+ if (authSecret) uow.update("secret", authSecret.id, (b) => b.set({
157
+ payload,
158
+ expiresAt: null,
159
+ updatedAt: b.now()
160
+ }).check());
161
+ else uow.create("secret", {
162
+ id: `${pathParams.slug}:auth`,
163
+ serverId: pathParams.slug,
164
+ kind: "auth",
165
+ payload,
166
+ expiresAt: null
167
+ });
168
+ }
169
+ for (const oauthState of oauthStates) uow.delete("oauthState", oauthState.id);
170
+ if (cache) uow.delete("server_connection_cache", cache.id);
171
+ if (body.auth.type !== "oauth") uow.triggerHook("internalRefreshServerConfiguration", { serverId: pathParams.slug });
172
+ const ready = isMcpAuthReady(replaced.authMode, payload === null ? void 0 : {
173
+ payload,
174
+ expiresAt: null
175
+ });
176
+ if (ready !== isMcpAuthReady(server.authMode, authSecret)) uow.triggerHook("onServerReadinessChanged", {
177
+ serverId: pathParams.slug,
178
+ ready
179
+ });
180
+ return { server: replaced };
181
+ }).execute();
182
+ if (!result.server) return error({
183
+ code: "SERVER_NOT_FOUND",
184
+ message: "MCP server not found"
185
+ }, 404);
186
+ return json(publicServer(result.server));
187
+ }
188
+ }),
189
+ defineRoute({
190
+ method: "DELETE",
191
+ path: "/servers/:slug",
192
+ errorCodes: ["SERVER_NOT_FOUND"],
193
+ handler: async function({ pathParams }, { empty, error }) {
194
+ if (!(await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_server", (eb) => eb("serverId", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [server, secrets, oauthStates, cache] }) => {
195
+ if (!server) return { deleted: false };
196
+ const uow = forSchema(mcpSchema);
197
+ for (const secret of secrets) uow.delete("secret", secret.id);
198
+ for (const oauthState of oauthStates) uow.delete("oauthState", oauthState.id);
199
+ if (cache) uow.delete("server_connection_cache", cache.id);
200
+ uow.delete("server_configuration", server.id);
201
+ uow.triggerHook("onServerConfigurationDeleted", { serverId: pathParams.slug });
202
+ return { deleted: true };
203
+ }).execute()).deleted) return error({
204
+ code: "SERVER_NOT_FOUND",
205
+ message: "MCP server not found"
206
+ }, 404);
207
+ return empty(204);
208
+ }
209
+ }),
210
+ defineRoute({
211
+ method: "GET",
212
+ path: "/servers/:slug/auth/status",
213
+ outputSchema: mcpAuthStatusSchema,
214
+ errorCodes: ["SERVER_NOT_FOUND"],
215
+ handler: async function({ pathParams }, { json, error }) {
216
+ const [server, secret, pendingState] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb.and(eb("serverId", "=", pathParams.slug), eb("kind", "=", "auth")))).findFirst("oauthState", (b) => b.whereIndex("idx_oauth_state_pending", (eb) => eb.and(eb("serverId", "=", pathParams.slug), eb.isNull("consumedAt"), eb("expiresAt", ">", eb.now()))).orderByIndex("idx_oauth_state_pending", "desc"))).execute();
217
+ if (!server) return error({
218
+ code: "SERVER_NOT_FOUND",
219
+ message: "MCP server not found"
220
+ }, 404);
221
+ return json(projectMcpAuthStatus({
222
+ authMode: server.authMode,
223
+ authSecret: secret ?? void 0,
224
+ hasPendingOAuth: pendingOAuthLink(pendingState) !== null
225
+ }));
226
+ }
227
+ }),
228
+ defineRoute({
229
+ method: "GET",
230
+ path: "/servers/:slug/auth/pending",
231
+ outputSchema: mcpOAuthPendingSchema,
232
+ errorCodes: ["SERVER_NOT_FOUND"],
233
+ handler: async function({ pathParams }, { json, error }) {
234
+ const [server, pendingState] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("oauthState", (b) => b.whereIndex("idx_oauth_state_pending", (eb) => eb.and(eb("serverId", "=", pathParams.slug), eb.isNull("consumedAt"), eb("expiresAt", ">", eb.now()))).orderByIndex("idx_oauth_state_pending", "desc"))).execute();
235
+ if (!server) return error({
236
+ code: "SERVER_NOT_FOUND",
237
+ message: "MCP server not found"
238
+ }, 404);
239
+ return json({ pending: pendingOAuthLink(pendingState) });
240
+ }
241
+ }),
242
+ defineRoute({
243
+ method: "POST",
244
+ path: "/servers/:slug/auth/token",
245
+ inputSchema: tokenAuthInputSchema,
246
+ outputSchema: mcpAuthStatusSchema,
247
+ errorCodes: ["SERVER_NOT_FOUND"],
248
+ handler: async function({ input, pathParams }, { json, error }) {
249
+ const { token } = await input.valid();
250
+ const payload = stringifySecretPayload({
251
+ type: "bearer",
252
+ token
253
+ });
254
+ if (!(await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb.and(eb("serverId", "=", pathParams.slug), eb("kind", "=", "auth")))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [server, secret, cache] }) => {
255
+ if (!server) return { found: false };
256
+ const uow = forSchema(mcpSchema);
257
+ uow.update("server_configuration", server.id, (b) => b.set({
258
+ authMode: "bearer",
259
+ updatedAt: b.now()
260
+ }).check());
261
+ if (cache) uow.delete("server_connection_cache", cache.id);
262
+ if (secret) uow.update("secret", secret.id, (b) => b.set({
263
+ payload,
264
+ expiresAt: null,
265
+ updatedAt: b.now()
266
+ }).check());
267
+ else uow.create("secret", {
268
+ id: `${pathParams.slug}:auth`,
269
+ serverId: pathParams.slug,
270
+ kind: "auth",
271
+ payload,
272
+ expiresAt: null
273
+ });
274
+ uow.triggerHook("internalRefreshServerConfiguration", { serverId: pathParams.slug });
275
+ if (!isMcpAuthReady(server.authMode, secret ?? void 0)) uow.triggerHook("onServerReadinessChanged", {
276
+ serverId: pathParams.slug,
277
+ ready: true
278
+ });
279
+ return { found: true };
280
+ }).execute()).found) return error({
281
+ code: "SERVER_NOT_FOUND",
282
+ message: "MCP server not found"
283
+ }, 404);
284
+ return json({
285
+ mode: "bearer",
286
+ credentials: "present"
287
+ });
288
+ }
289
+ }),
290
+ defineRoute({
291
+ method: "POST",
292
+ path: "/servers/:slug/auth/start",
293
+ inputSchema: oauthStartInputSchema,
294
+ outputSchema: z.object({
295
+ authorizationUrl: z.string(),
296
+ state: z.string()
297
+ }),
298
+ queryParameters: [MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER],
299
+ errorCodes: [
300
+ "INVALID_OAUTH_REDIRECT_URI",
301
+ "OAUTH_REDIRECT_URI_NOT_ALLOWED",
302
+ "SERVER_NOT_FOUND",
303
+ "OAUTH_ERROR"
304
+ ],
305
+ handler: async function({ input, pathParams, query }, { json, error }) {
306
+ const body = await input.valid();
307
+ const redirectUri = mcpOAuthRedirectUriSchema.safeParse(query.get(MCP_OAUTH_REDIRECT_URI_QUERY_PARAMETER));
308
+ if (!redirectUri.success) return error({
309
+ code: "INVALID_OAUTH_REDIRECT_URI",
310
+ message: "MCP OAuth redirect URI must be an HTTP or HTTPS URL"
311
+ }, 400);
312
+ const oauthRedirectUri = new URL(redirectUri.data);
313
+ if (!config.allowedOAuthRedirectUris?.(oauthRedirectUri)) return error({
314
+ code: "OAUTH_REDIRECT_URI_NOT_ALLOWED",
315
+ message: "MCP OAuth redirect URI is not allowed by the fragment configuration"
316
+ }, 400);
317
+ const state = `${pathParams.slug}:${crypto.randomUUID()}`;
318
+ try {
319
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.startOAuth({
320
+ serverId: pathParams.slug,
321
+ stateId: state,
322
+ redirectUri: oauthRedirectUri.toString(),
323
+ scope: body.scope,
324
+ clientId: body.clientId,
325
+ clientSecret: body.clientSecret,
326
+ discardTokens: body.discardTokens
327
+ })]).execute();
328
+ if (!result.found) return error({
329
+ code: "SERVER_NOT_FOUND",
330
+ message: "MCP server not found"
331
+ }, 404);
332
+ return json({
333
+ authorizationUrl: result.authorizationUrl,
334
+ state
335
+ });
336
+ } catch (err) {
337
+ return error({
338
+ code: "OAUTH_ERROR",
339
+ message: err.message
340
+ }, 502);
341
+ }
342
+ }
343
+ }),
344
+ defineRoute({
345
+ method: "GET",
346
+ path: "/oauth/callback",
347
+ outputSchema: z.object({
348
+ authenticated: z.boolean(),
349
+ mode: z.string()
350
+ }),
351
+ errorCodes: [
352
+ "SERVER_NOT_FOUND",
353
+ "OAUTH_ERROR",
354
+ "INVALID_OAUTH_STATE"
355
+ ],
356
+ handler: async function({ query }, { json, error }) {
357
+ const code = query.get("code");
358
+ const stateId = query.get("state");
359
+ if (!code || !stateId) return error({
360
+ code: "INVALID_OAUTH_STATE",
361
+ message: "Missing OAuth code or state"
362
+ }, 400);
363
+ try {
364
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.completeOAuthCallback({
365
+ stateId,
366
+ code,
367
+ iss: query.get("iss") ?? void 0
368
+ })]).execute();
369
+ if (!result.found) {
370
+ if (result.reason === "server_not_found") return error({
371
+ code: "SERVER_NOT_FOUND",
372
+ message: "MCP server not found"
373
+ }, 404);
374
+ return error({
375
+ code: "INVALID_OAUTH_STATE",
376
+ message: "Invalid OAuth state"
377
+ }, 400);
378
+ }
379
+ return json({
380
+ authenticated: true,
381
+ mode: "oauth"
382
+ });
383
+ } catch (err) {
384
+ return error({
385
+ code: "OAUTH_ERROR",
386
+ message: err.message
387
+ }, 502);
388
+ }
389
+ }
390
+ }),
391
+ defineRoute({
392
+ method: "DELETE",
393
+ path: "/servers/:slug/auth",
394
+ outputSchema: mcpAuthStatusSchema,
395
+ errorCodes: ["SERVER_NOT_FOUND"],
396
+ handler: async function({ pathParams }, { json, error }) {
397
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_server", (eb) => eb("serverId", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [server, secrets, oauthStates, cache] }) => {
398
+ if (!server) return { found: false };
399
+ const uow = forSchema(mcpSchema);
400
+ const authSecret = secrets.find((secret) => secret.kind === "auth");
401
+ const remainingAuth = server.authMode === "oauth" && authSecret ? {
402
+ payload: withoutOAuthConsent(authSecret.payload),
403
+ expiresAt: null
404
+ } : void 0;
405
+ if (server.authMode === "oauth") {
406
+ if (authSecret && remainingAuth) uow.update("secret", authSecret.id, (b) => b.set({
407
+ payload: remainingAuth.payload,
408
+ expiresAt: null,
409
+ updatedAt: b.now()
410
+ }).check());
411
+ } else for (const secret of secrets) uow.delete("secret", secret.id);
412
+ const ready = isMcpAuthReady(server.authMode, remainingAuth);
413
+ if (ready !== isMcpAuthReady(server.authMode, authSecret)) uow.triggerHook("onServerReadinessChanged", {
414
+ serverId: pathParams.slug,
415
+ ready
416
+ });
417
+ for (const oauthState of oauthStates) uow.delete("oauthState", oauthState.id);
418
+ if (cache) uow.delete("server_connection_cache", cache.id);
419
+ uow.update("server_configuration", server.id, (b) => b.set({ updatedAt: b.now() }).check());
420
+ return {
421
+ found: true,
422
+ status: projectMcpAuthStatus({
423
+ authMode: server.authMode,
424
+ authSecret: void 0,
425
+ hasPendingOAuth: false
426
+ })
427
+ };
428
+ }).execute();
429
+ if (!result.found) return error({
430
+ code: "SERVER_NOT_FOUND",
431
+ message: "MCP server not found"
432
+ }, 404);
433
+ return json(result.status);
434
+ }
435
+ }),
436
+ defineRoute({
437
+ method: "POST",
438
+ path: "/servers/:slug/refresh",
439
+ outputSchema: refreshOutputSchema,
440
+ errorCodes: ["SERVER_NOT_FOUND"],
441
+ handler: async function({ pathParams }, { json, error }) {
442
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
443
+ let prepared;
444
+ let authError;
445
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_server", (eb) => eb("serverId", "=", pathParams.slug)))).afterRetrieve(async (_uow, [server, secrets]) => {
446
+ if (!server) return;
447
+ try {
448
+ const auth = await createMcpOperationAuth({
449
+ config,
450
+ server,
451
+ secrets
452
+ });
453
+ const toolsOperation = await listMcpTools({
454
+ endpointUrl: server.endpointUrl,
455
+ auth,
456
+ fetchImplementation: config.fetch
457
+ });
458
+ prepared = {
459
+ authChanges: toolsOperation.authChanges,
460
+ toolsOperation
461
+ };
462
+ } catch (err) {
463
+ authError = err;
464
+ }
465
+ }).mutate(({ forSchema, retrieveResult: [server, secrets, cache, oauthStates] }) => {
466
+ if (!server) return { found: false };
467
+ const serverId = server.id.toString();
468
+ const previousTools = Array.isArray(cache?.tools) ? cache.tools : null;
469
+ const baseDiagnostics = {
470
+ checkedAt,
471
+ server: publicServer(server),
472
+ cache: {
473
+ presentBeforeCheck: Boolean(cache),
474
+ previousToolCount: previousTools?.length ?? null,
475
+ updatedToolCount: null
476
+ }
477
+ };
478
+ if (authError) return {
479
+ found: true,
480
+ refresh: {
481
+ ...baseDiagnostics,
482
+ ok: false,
483
+ tools: [],
484
+ stage: "auth",
485
+ auth: buildAuthDiagnostics({
486
+ mode: server.authMode,
487
+ secrets,
488
+ oauthStates
489
+ }),
490
+ live: {
491
+ reachable: false,
492
+ listToolsOk: false,
493
+ toolCount: null,
494
+ protocolVersion: null,
495
+ serverInfo: null,
496
+ capabilities: null
497
+ },
498
+ error: {
499
+ code: "AUTH_ERROR",
500
+ message: errorMessage(authError)
501
+ }
502
+ }
503
+ };
504
+ if (!prepared) throw new Error("MCP refresh operation was not prepared");
505
+ const preparedOperation = prepared;
506
+ const uow = forSchema(mcpSchema);
507
+ const upsertSecret = (kind, payload, expiresAt) => {
508
+ const existing = secrets.find((secret) => secret.kind === kind);
509
+ if (existing) {
510
+ uow.update("secret", existing.id, (b) => b.set({
511
+ payload,
512
+ expiresAt,
513
+ updatedAt: b.now()
514
+ }).check());
515
+ return;
516
+ }
517
+ uow.create("secret", {
518
+ id: `${serverId}:${kind}`,
519
+ serverId,
520
+ kind,
521
+ payload,
522
+ expiresAt
523
+ });
524
+ };
525
+ if (preparedOperation.authChanges) writeOAuthSecretChanges(preparedOperation.authChanges, {
526
+ upsert: (kind, payload) => {
527
+ upsertSecret(kind, payload, null);
528
+ },
529
+ remove: (kind) => {
530
+ const existing = secrets.find((secret) => secret.kind === kind);
531
+ if (existing) uow.delete("secret", existing.id);
532
+ }
533
+ });
534
+ if (preparedOperation.authChanges?.authPayload) {
535
+ const after = {
536
+ payload: preparedOperation.authChanges.authPayload,
537
+ expiresAt: preparedOperation.authChanges.authExpiresAt ?? null
538
+ };
539
+ upsertSecret("auth", after.payload, after.expiresAt);
540
+ const before = secrets.find((secret) => secret.kind === "auth");
541
+ const ready = isMcpAuthReady(server.authMode, after);
542
+ if (ready !== isMcpAuthReady(server.authMode, before)) uow.triggerHook("onServerReadinessChanged", {
543
+ serverId,
544
+ ready
545
+ });
546
+ scheduleMcpAuthExpiry(uow, serverId, server.authMode, after);
547
+ }
548
+ const auth = buildAuthDiagnostics({
549
+ mode: server.authMode,
550
+ secrets,
551
+ oauthStates,
552
+ authChanges: preparedOperation.authChanges
553
+ });
554
+ if (!preparedOperation.toolsOperation.ok) return {
555
+ found: true,
556
+ refresh: {
557
+ ...baseDiagnostics,
558
+ ok: false,
559
+ tools: [],
560
+ stage: "list_tools",
561
+ auth,
562
+ live: {
563
+ reachable: false,
564
+ listToolsOk: false,
565
+ toolCount: null,
566
+ protocolVersion: null,
567
+ serverInfo: null,
568
+ capabilities: null
569
+ },
570
+ error: {
571
+ code: "MCP_ERROR",
572
+ message: errorMessage(preparedOperation.toolsOperation.error)
573
+ }
574
+ }
575
+ };
576
+ const tools = Array.isArray(preparedOperation.toolsOperation.result.tools) ? preparedOperation.toolsOperation.result.tools : [];
577
+ if (!previousTools || JSON.stringify(previousTools) !== JSON.stringify(tools)) uow.triggerHook("onServerConfigurationChanged", {
578
+ serverId,
579
+ current: { tools }
580
+ });
581
+ uow.delete("server_connection_cache", serverId);
582
+ uow.create("server_connection_cache", {
583
+ id: serverId,
584
+ serverId,
585
+ protocolVersion: preparedOperation.toolsOperation.metadata.protocolVersion ?? null,
586
+ serverInfo: preparedOperation.toolsOperation.metadata.serverInfo ?? null,
587
+ capabilities: preparedOperation.toolsOperation.metadata.capabilities ?? null,
588
+ tools
589
+ });
590
+ return {
591
+ found: true,
592
+ refresh: {
593
+ ...baseDiagnostics,
594
+ ok: true,
595
+ tools,
596
+ stage: null,
597
+ auth,
598
+ live: {
599
+ reachable: true,
600
+ listToolsOk: true,
601
+ toolCount: tools.length,
602
+ protocolVersion: preparedOperation.toolsOperation.metadata.protocolVersion ?? null,
603
+ serverInfo: preparedOperation.toolsOperation.metadata.serverInfo ?? null,
604
+ capabilities: preparedOperation.toolsOperation.metadata.capabilities ?? null
605
+ },
606
+ cache: {
607
+ ...baseDiagnostics.cache,
608
+ updatedToolCount: tools.length
609
+ },
610
+ error: null
611
+ }
612
+ };
613
+ }).execute();
614
+ if (!result.found) return error({
615
+ code: "SERVER_NOT_FOUND",
616
+ message: "MCP server not found"
617
+ }, 404);
618
+ return json(result.refresh);
619
+ }
620
+ }),
621
+ defineRoute({
622
+ method: "POST",
623
+ path: "/servers/:slug/tools/execute",
624
+ inputSchema: toolCallInputSchema,
625
+ outputSchema: mcpToolCallResultSchema,
626
+ errorCodes: ["SERVER_NOT_FOUND", "MCP_ERROR"],
627
+ handler: async function({ input, pathParams }, { json, error }) {
628
+ const body = await input.valid();
629
+ try {
630
+ let prepared;
631
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(mcpSchema).findFirst("server_configuration", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_server_kind", (eb) => eb("serverId", "=", pathParams.slug))).findFirst("server_connection_cache", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).afterRetrieve(async (_uow, [server, secrets]) => {
632
+ if (!server) return;
633
+ const auth = await createMcpOperationAuth({
634
+ config,
635
+ server,
636
+ secrets
637
+ });
638
+ const toolOperation = await callMcpTool({
639
+ endpointUrl: server.endpointUrl,
640
+ auth,
641
+ name: body.name,
642
+ toolArguments: body.arguments,
643
+ timeoutMs: body.timeoutMs,
644
+ fetchImplementation: config.fetch
645
+ });
646
+ prepared = {
647
+ serverId: server.id.toString(),
648
+ authChanges: toolOperation.authChanges,
649
+ toolOperation
650
+ };
651
+ }).mutate(({ forSchema, retrieveResult: [server, secrets, cache] }) => {
652
+ if (!server) return { found: false };
653
+ if (!prepared) throw new Error("MCP operation was not prepared");
654
+ const preparedOperation = prepared;
655
+ const uow = forSchema(mcpSchema);
656
+ const upsertSecret = (kind, payload, expiresAt) => {
657
+ const existing = secrets.find((secret) => secret.kind === kind);
658
+ if (existing) {
659
+ uow.update("secret", existing.id, (b) => b.set({
660
+ payload,
661
+ expiresAt,
662
+ updatedAt: b.now()
663
+ }).check());
664
+ return;
665
+ }
666
+ uow.create("secret", {
667
+ id: `${preparedOperation.serverId}:${kind}`,
668
+ serverId: preparedOperation.serverId,
669
+ kind,
670
+ payload,
671
+ expiresAt
672
+ });
673
+ };
674
+ if (preparedOperation.authChanges) writeOAuthSecretChanges(preparedOperation.authChanges, {
675
+ upsert: (kind, payload) => {
676
+ upsertSecret(kind, payload, null);
677
+ },
678
+ remove: (kind) => {
679
+ const existing = secrets.find((secret) => secret.kind === kind);
680
+ if (existing) uow.delete("secret", existing.id);
681
+ }
682
+ });
683
+ if (preparedOperation.authChanges?.authPayload) {
684
+ const after = {
685
+ payload: preparedOperation.authChanges.authPayload,
686
+ expiresAt: preparedOperation.authChanges.authExpiresAt ?? null
687
+ };
688
+ upsertSecret("auth", after.payload, after.expiresAt);
689
+ const before = secrets.find((secret) => secret.kind === "auth");
690
+ const ready = isMcpAuthReady(server.authMode, after);
691
+ if (ready !== isMcpAuthReady(server.authMode, before)) uow.triggerHook("onServerReadinessChanged", {
692
+ serverId: preparedOperation.serverId,
693
+ ready
694
+ });
695
+ scheduleMcpAuthExpiry(uow, preparedOperation.serverId, server.authMode, after);
696
+ }
697
+ const toolOperation = preparedOperation.toolOperation;
698
+ if (!toolOperation.ok) return {
699
+ found: true,
700
+ error: toolOperation.error instanceof Error ? toolOperation.error.message : String(toolOperation.error)
701
+ };
702
+ if (cache) uow.update("server_connection_cache", cache.id, (b) => b.set({
703
+ protocolVersion: toolOperation.metadata.protocolVersion ?? null,
704
+ serverInfo: toolOperation.metadata.serverInfo ?? null,
705
+ capabilities: toolOperation.metadata.capabilities ?? null,
706
+ updatedAt: b.now()
707
+ }).check());
708
+ else uow.create("server_connection_cache", {
709
+ id: pathParams.slug,
710
+ serverId: pathParams.slug,
711
+ protocolVersion: toolOperation.metadata.protocolVersion ?? null,
712
+ serverInfo: toolOperation.metadata.serverInfo ?? null,
713
+ capabilities: toolOperation.metadata.capabilities ?? null,
714
+ tools: null
715
+ });
716
+ return {
717
+ found: true,
718
+ result: toolOperation.result
719
+ };
720
+ }).execute();
721
+ if (!result.found) return error({
722
+ code: "SERVER_NOT_FOUND",
723
+ message: "MCP server not found"
724
+ }, 404);
725
+ if ("error" in result) return error({
726
+ code: "MCP_ERROR",
727
+ message: result.error ?? "MCP operation failed"
728
+ }, 502);
729
+ return json(result.result);
730
+ } catch (err) {
731
+ return error({
732
+ code: "MCP_ERROR",
733
+ message: err.message
734
+ }, 502);
735
+ }
736
+ }
737
+ })
738
+ ];
739
+ });
740
+ //#endregion
741
+ export { mcpRoutesFactory };
742
+
743
+ //# sourceMappingURL=routes.js.map