@decocms/runtime 1.0.0-alpha.2 → 1.0.0-alpha.20
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/config-schema.json +553 -0
- package/package.json +5 -14
- package/src/bindings/binder.ts +1 -4
- package/src/bindings/index.ts +0 -33
- package/src/bindings/language-model/utils.ts +0 -91
- package/src/bindings.ts +31 -110
- package/src/client.ts +1 -145
- package/src/cors.ts +140 -0
- package/src/index.ts +80 -154
- package/src/mcp.ts +7 -166
- package/src/proxy.ts +3 -54
- package/src/state.ts +3 -31
- package/src/tools.ts +376 -0
- package/src/wrangler.ts +5 -5
- package/tsconfig.json +1 -1
- package/src/admin.ts +0 -16
- package/src/auth.ts +0 -233
- package/src/bindings/deconfig/helpers.ts +0 -107
- package/src/bindings/deconfig/index.ts +0 -1
- package/src/bindings/deconfig/resources.ts +0 -689
- package/src/bindings/deconfig/types.ts +0 -106
- package/src/bindings/language-model/ai-sdk.ts +0 -90
- package/src/bindings/language-model/index.ts +0 -4
- package/src/bindings/resources/bindings.ts +0 -99
- package/src/bindings/resources/helpers.ts +0 -95
- package/src/bindings/resources/schemas.ts +0 -265
- package/src/bindings/views.ts +0 -14
- package/src/drizzle.ts +0 -201
- package/src/mastra.ts +0 -670
- package/src/resources.ts +0 -168
- package/src/views.ts +0 -26
- package/src/well-known.ts +0 -20
package/src/index.ts
CHANGED
|
@@ -2,55 +2,31 @@
|
|
|
2
2
|
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
3
3
|
import { decodeJwt } from "jose";
|
|
4
4
|
import type { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
handleAuthCallback,
|
|
8
|
-
handleLogout,
|
|
9
|
-
StateParser,
|
|
10
|
-
} from "./auth.ts";
|
|
11
|
-
import {
|
|
12
|
-
createContractBinding,
|
|
13
|
-
createIntegrationBinding,
|
|
14
|
-
workspaceClient,
|
|
15
|
-
} from "./bindings.ts";
|
|
16
|
-
import { DeconfigResource } from "./bindings/deconfig/index.ts";
|
|
17
|
-
import { DECO_MCP_CLIENT_HEADER } from "./client.ts";
|
|
5
|
+
import { createContractBinding, createIntegrationBinding } from "./bindings.ts";
|
|
6
|
+
import { State } from "./state.ts";
|
|
18
7
|
import {
|
|
19
8
|
createMCPServer,
|
|
20
9
|
type CreateMCPServerOptions,
|
|
21
10
|
MCPServer,
|
|
22
|
-
} from "./
|
|
23
|
-
import { MCPClient, type QueryResult } from "./mcp.ts";
|
|
24
|
-
import { State } from "./state.ts";
|
|
11
|
+
} from "./tools.ts";
|
|
25
12
|
import type { Binding, ContractBinding, MCPBinding } from "./wrangler.ts";
|
|
13
|
+
import { type CORSOptions, handlePreflight, withCORS } from "./cors.ts";
|
|
26
14
|
export { proxyConnectionForId } from "./bindings.ts";
|
|
27
15
|
export {
|
|
28
16
|
createMCPFetchStub,
|
|
29
17
|
type CreateStubAPIOptions,
|
|
30
18
|
type ToolBinder,
|
|
31
19
|
} from "./mcp.ts";
|
|
32
|
-
export
|
|
33
|
-
query: (params: {
|
|
34
|
-
sql: string;
|
|
35
|
-
params: string[];
|
|
36
|
-
}) => Promise<{ result: QueryResult[] }>;
|
|
37
|
-
}
|
|
20
|
+
export { type CORSOptions, type CORSOrigin } from "./cors.ts";
|
|
38
21
|
|
|
39
22
|
export interface DefaultEnv<TSchema extends z.ZodTypeAny = any> {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
DECO_APP_ENTRYPOINT: string;
|
|
44
|
-
DECO_API_URL?: string;
|
|
45
|
-
DECO_WORKSPACE: string;
|
|
46
|
-
DECO_API_JWT_PUBLIC_KEY: string;
|
|
47
|
-
DECO_APP_DEPLOYMENT_ID: string;
|
|
48
|
-
DECO_BINDINGS: string;
|
|
49
|
-
DECO_API_TOKEN: string;
|
|
50
|
-
DECO_WORKSPACE_DB: WorkspaceDB & {
|
|
51
|
-
forContext: (ctx: RequestContext) => WorkspaceDB;
|
|
52
|
-
};
|
|
23
|
+
MESH_REQUEST_CONTEXT: RequestContext<TSchema>;
|
|
24
|
+
MESH_BINDINGS: string;
|
|
25
|
+
MESH_APP_DEPLOYMENT_ID: string;
|
|
53
26
|
IS_LOCAL: boolean;
|
|
27
|
+
MESH_URL?: string;
|
|
28
|
+
MESH_RUNTIME_TOKEN?: string;
|
|
29
|
+
MESH_APP_NAME?: string;
|
|
54
30
|
[key: string]: unknown;
|
|
55
31
|
}
|
|
56
32
|
|
|
@@ -58,7 +34,7 @@ export interface BindingsObject {
|
|
|
58
34
|
bindings?: Binding[];
|
|
59
35
|
}
|
|
60
36
|
|
|
61
|
-
export const
|
|
37
|
+
export const MCPBindings = {
|
|
62
38
|
parse: (bindings?: string): Binding[] => {
|
|
63
39
|
if (!bindings) return [];
|
|
64
40
|
try {
|
|
@@ -82,6 +58,11 @@ export interface UserDefaultExport<
|
|
|
82
58
|
env: TEnv,
|
|
83
59
|
ctx: ExecutionContext,
|
|
84
60
|
) => Promise<Response> | Response;
|
|
61
|
+
/**
|
|
62
|
+
* CORS configuration options.
|
|
63
|
+
* Set to `false` to disable CORS handling entirely.
|
|
64
|
+
*/
|
|
65
|
+
cors?: CORSOptions | false;
|
|
85
66
|
}
|
|
86
67
|
|
|
87
68
|
// 1. Map binding type to its interface
|
|
@@ -104,14 +85,13 @@ export interface User {
|
|
|
104
85
|
|
|
105
86
|
export interface RequestContext<TSchema extends z.ZodTypeAny = any> {
|
|
106
87
|
state: z.infer<TSchema>;
|
|
107
|
-
branch?: string;
|
|
108
88
|
token: string;
|
|
109
|
-
|
|
89
|
+
meshUrl: string;
|
|
110
90
|
ensureAuthenticated: (options?: {
|
|
111
91
|
workspaceHint?: string;
|
|
112
92
|
}) => User | undefined;
|
|
113
93
|
callerApp?: string;
|
|
114
|
-
|
|
94
|
+
connectionId?: string;
|
|
115
95
|
}
|
|
116
96
|
|
|
117
97
|
// 2. Map binding type to its creator function
|
|
@@ -131,26 +111,12 @@ const creatorByType: CreatorByType = {
|
|
|
131
111
|
const withDefaultBindings = ({
|
|
132
112
|
env,
|
|
133
113
|
server,
|
|
134
|
-
ctx,
|
|
135
114
|
url,
|
|
136
115
|
}: {
|
|
137
116
|
env: DefaultEnv;
|
|
138
117
|
server: MCPServer<any, any>;
|
|
139
|
-
ctx: RequestContext;
|
|
140
118
|
url?: string;
|
|
141
119
|
}) => {
|
|
142
|
-
const client = workspaceClient(ctx, env.DECO_API_URL);
|
|
143
|
-
const createWorkspaceDB = (ctx: RequestContext): WorkspaceDB => {
|
|
144
|
-
const client = workspaceClient(ctx, env.DECO_API_URL);
|
|
145
|
-
return {
|
|
146
|
-
query: ({ sql, params }) => {
|
|
147
|
-
return client.DATABASES_RUN_SQL({
|
|
148
|
-
sql,
|
|
149
|
-
params,
|
|
150
|
-
});
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
120
|
env["SELF"] = new Proxy(
|
|
155
121
|
{},
|
|
156
122
|
{
|
|
@@ -169,15 +135,6 @@ const withDefaultBindings = ({
|
|
|
169
135
|
},
|
|
170
136
|
);
|
|
171
137
|
|
|
172
|
-
const workspaceDbBinding = {
|
|
173
|
-
...createWorkspaceDB(ctx),
|
|
174
|
-
forContext: createWorkspaceDB,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
env["DECO_API"] = MCPClient;
|
|
178
|
-
env["DECO_WORKSPACE_API"] = client;
|
|
179
|
-
env["DECO_WORKSPACE_DB"] = workspaceDbBinding;
|
|
180
|
-
|
|
181
138
|
env["IS_LOCAL"] =
|
|
182
139
|
(url?.startsWith("http://localhost") ||
|
|
183
140
|
url?.startsWith("http://127.0.0.1")) ??
|
|
@@ -194,13 +151,9 @@ export class UnauthorizedError extends Error {
|
|
|
194
151
|
}
|
|
195
152
|
}
|
|
196
153
|
|
|
197
|
-
const
|
|
198
|
-
const AUTH_START_ENDPOINT = "/oauth/start";
|
|
199
|
-
const AUTH_LOGOUT_ENDPOINT = "/oauth/logout";
|
|
200
|
-
const AUTHENTICATED = (user?: unknown, workspace?: string) => () => {
|
|
154
|
+
const AUTHENTICATED = (user?: unknown) => () => {
|
|
201
155
|
return {
|
|
202
156
|
...((user as User) ?? {}),
|
|
203
|
-
workspace,
|
|
204
157
|
} as User;
|
|
205
158
|
};
|
|
206
159
|
|
|
@@ -208,66 +161,64 @@ export const withBindings = <TEnv>({
|
|
|
208
161
|
env: _env,
|
|
209
162
|
server,
|
|
210
163
|
tokenOrContext,
|
|
211
|
-
origin,
|
|
212
164
|
url,
|
|
213
|
-
|
|
165
|
+
bindings: inlineBindings,
|
|
214
166
|
}: {
|
|
215
167
|
env: TEnv;
|
|
216
168
|
server: MCPServer<TEnv, any>;
|
|
217
169
|
tokenOrContext?: string | RequestContext;
|
|
218
|
-
origin?: string | null;
|
|
219
170
|
url?: string;
|
|
220
|
-
|
|
171
|
+
bindings?: Binding[];
|
|
221
172
|
}): TEnv => {
|
|
222
|
-
branch ??= undefined;
|
|
223
173
|
const env = _env as DefaultEnv<any>;
|
|
224
174
|
|
|
225
|
-
const apiUrl = env.DECO_API_URL ?? "https://api.decocms.com";
|
|
226
175
|
let context;
|
|
227
176
|
if (typeof tokenOrContext === "string") {
|
|
228
177
|
const decoded = decodeJwt(tokenOrContext);
|
|
229
|
-
|
|
178
|
+
// Support both new JWT format (fields directly on payload) and legacy format (nested in metadata)
|
|
179
|
+
const metadata =
|
|
180
|
+
(decoded.metadata as {
|
|
181
|
+
state?: Record<string, unknown>;
|
|
182
|
+
meshUrl?: string;
|
|
183
|
+
connectionId?: string;
|
|
184
|
+
}) ?? {};
|
|
230
185
|
|
|
231
186
|
context = {
|
|
232
|
-
state: decoded.state
|
|
187
|
+
state: decoded.state ?? metadata.state,
|
|
233
188
|
token: tokenOrContext,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ensureAuthenticated: AUTHENTICATED(decoded.user
|
|
237
|
-
branch,
|
|
189
|
+
meshUrl: (decoded.meshUrl as string) ?? metadata.meshUrl,
|
|
190
|
+
connectionId: (decoded.connectionId as string) ?? metadata.connectionId,
|
|
191
|
+
ensureAuthenticated: AUTHENTICATED(decoded.user ?? decoded.sub),
|
|
238
192
|
} as RequestContext<any>;
|
|
239
193
|
} else if (typeof tokenOrContext === "object") {
|
|
240
194
|
context = tokenOrContext;
|
|
241
195
|
const decoded = decodeJwt(tokenOrContext.token);
|
|
242
|
-
|
|
196
|
+
// Support both new JWT format (fields directly on payload) and legacy format (nested in metadata)
|
|
197
|
+
const metadata =
|
|
198
|
+
(decoded.metadata as {
|
|
199
|
+
state?: Record<string, unknown>;
|
|
200
|
+
meshUrl?: string;
|
|
201
|
+
connectionId?: string;
|
|
202
|
+
}) ?? {};
|
|
243
203
|
const appName = decoded.appName as string | undefined;
|
|
244
204
|
context.callerApp = appName;
|
|
245
|
-
context.
|
|
246
|
-
|
|
205
|
+
context.connectionId ??=
|
|
206
|
+
(decoded.connectionId as string) ?? metadata.connectionId;
|
|
207
|
+
context.ensureAuthenticated = AUTHENTICATED(decoded.user ?? decoded.sub);
|
|
247
208
|
} else {
|
|
248
209
|
context = {
|
|
249
|
-
state:
|
|
250
|
-
token:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
ensureAuthenticated: (
|
|
254
|
-
|
|
255
|
-
const authUri = new URL("/apps/oauth", apiUrl);
|
|
256
|
-
authUri.searchParams.set("client_id", env.DECO_APP_NAME);
|
|
257
|
-
authUri.searchParams.set(
|
|
258
|
-
"redirect_uri",
|
|
259
|
-
new URL(AUTH_CALLBACK_ENDPOINT, origin ?? env.DECO_APP_ENTRYPOINT)
|
|
260
|
-
.href,
|
|
261
|
-
);
|
|
262
|
-
workspaceHint &&
|
|
263
|
-
authUri.searchParams.set("workspace_hint", workspaceHint);
|
|
264
|
-
throw new UnauthorizedError("Unauthorized", authUri);
|
|
210
|
+
state: {},
|
|
211
|
+
token: undefined,
|
|
212
|
+
meshUrl: undefined,
|
|
213
|
+
connectionId: undefined,
|
|
214
|
+
ensureAuthenticated: () => {
|
|
215
|
+
throw new Error("Unauthorized");
|
|
265
216
|
},
|
|
266
|
-
}
|
|
217
|
+
} as unknown as RequestContext<any>;
|
|
267
218
|
}
|
|
268
219
|
|
|
269
|
-
env.
|
|
270
|
-
const bindings =
|
|
220
|
+
env.MESH_REQUEST_CONTEXT = context;
|
|
221
|
+
const bindings = inlineBindings ?? MCPBindings.parse(env.MESH_BINDINGS);
|
|
271
222
|
|
|
272
223
|
for (const binding of bindings) {
|
|
273
224
|
env[binding.name] = creatorByType[binding.type](binding as any, env);
|
|
@@ -276,7 +227,6 @@ export const withBindings = <TEnv>({
|
|
|
276
227
|
withDefaultBindings({
|
|
277
228
|
env,
|
|
278
229
|
server,
|
|
279
|
-
ctx: env.DECO_REQUEST_CONTEXT,
|
|
280
230
|
url,
|
|
281
231
|
});
|
|
282
232
|
|
|
@@ -287,27 +237,14 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
287
237
|
userFns: UserDefaultExport<TEnv, TSchema>,
|
|
288
238
|
): ExportedHandler<TEnv & DefaultEnv<TSchema>> => {
|
|
289
239
|
const server = createMCPServer<TEnv, TSchema>(userFns);
|
|
240
|
+
const corsOptions = userFns.cors;
|
|
241
|
+
|
|
290
242
|
const fetcher = async (
|
|
291
243
|
req: Request,
|
|
292
244
|
env: TEnv & DefaultEnv<TSchema>,
|
|
293
245
|
ctx: ExecutionContext,
|
|
294
246
|
) => {
|
|
295
247
|
const url = new URL(req.url);
|
|
296
|
-
if (url.pathname === AUTH_CALLBACK_ENDPOINT) {
|
|
297
|
-
return handleAuthCallback(req, {
|
|
298
|
-
apiUrl: env.DECO_API_URL,
|
|
299
|
-
appName: env.DECO_APP_NAME,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
if (url.pathname === AUTH_START_ENDPOINT) {
|
|
303
|
-
env.DECO_REQUEST_CONTEXT.ensureAuthenticated();
|
|
304
|
-
const redirectTo = new URL("/", url);
|
|
305
|
-
const next = url.searchParams.get("next");
|
|
306
|
-
return Response.redirect(next ?? redirectTo, 302);
|
|
307
|
-
}
|
|
308
|
-
if (url.pathname === AUTH_LOGOUT_ENDPOINT) {
|
|
309
|
-
return handleLogout(req);
|
|
310
|
-
}
|
|
311
248
|
if (url.pathname === "/mcp") {
|
|
312
249
|
return server.fetch(req, env, ctx);
|
|
313
250
|
}
|
|
@@ -334,55 +271,44 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
334
271
|
});
|
|
335
272
|
}
|
|
336
273
|
|
|
337
|
-
if (url.pathname.startsWith(DeconfigResource.WatchPathNameBase)) {
|
|
338
|
-
return DeconfigResource.watchAPI(req, env);
|
|
339
|
-
}
|
|
340
274
|
return (
|
|
341
275
|
userFns.fetch?.(req, env, ctx) ||
|
|
342
276
|
new Response("Not found", { status: 404 })
|
|
343
277
|
);
|
|
344
278
|
};
|
|
279
|
+
|
|
345
280
|
return {
|
|
346
281
|
fetch: async (
|
|
347
282
|
req: Request,
|
|
348
283
|
env: TEnv & DefaultEnv<TSchema>,
|
|
349
284
|
ctx: ExecutionContext,
|
|
350
285
|
) => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const bindings = withBindings({
|
|
356
|
-
env,
|
|
357
|
-
server,
|
|
358
|
-
branch:
|
|
359
|
-
req.headers.get("x-deco-branch") ??
|
|
360
|
-
new URL(req.url).searchParams.get("__b"),
|
|
361
|
-
tokenOrContext: await getReqToken(req, env),
|
|
362
|
-
origin:
|
|
363
|
-
referer ?? req.headers.get("origin") ?? new URL(req.url).origin,
|
|
364
|
-
url: req.url,
|
|
365
|
-
});
|
|
366
|
-
return await State.run(
|
|
367
|
-
{ req, env: bindings, ctx },
|
|
368
|
-
async () => await fetcher(req, bindings, ctx),
|
|
369
|
-
);
|
|
370
|
-
} catch (error) {
|
|
371
|
-
if (error instanceof UnauthorizedError) {
|
|
372
|
-
if (!isFetchRequest) {
|
|
373
|
-
const url = new URL(req.url);
|
|
374
|
-
error.redirectTo.searchParams.set(
|
|
375
|
-
"state",
|
|
376
|
-
StateParser.stringify({
|
|
377
|
-
next: url.searchParams.get("next") ?? referer ?? req.url,
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
return Response.redirect(error.redirectTo, 302);
|
|
381
|
-
}
|
|
382
|
-
return new Response(null, { status: 401 });
|
|
383
|
-
}
|
|
384
|
-
throw error;
|
|
286
|
+
// Handle CORS preflight (OPTIONS) requests
|
|
287
|
+
if (corsOptions !== false && req.method === "OPTIONS") {
|
|
288
|
+
const options = corsOptions ?? {};
|
|
289
|
+
return handlePreflight(req, options);
|
|
385
290
|
}
|
|
291
|
+
|
|
292
|
+
const bindings = withBindings({
|
|
293
|
+
env,
|
|
294
|
+
server,
|
|
295
|
+
bindings: userFns.bindings,
|
|
296
|
+
tokenOrContext: req.headers.get("x-mesh-token") ?? undefined,
|
|
297
|
+
url: req.url,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const response = await State.run(
|
|
301
|
+
{ req, env: bindings, ctx },
|
|
302
|
+
async () => await fetcher(req, bindings, ctx),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Add CORS headers to response
|
|
306
|
+
if (corsOptions !== false) {
|
|
307
|
+
const options = corsOptions ?? {};
|
|
308
|
+
return withCORS(response, req, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return response;
|
|
386
312
|
},
|
|
387
313
|
};
|
|
388
314
|
};
|
package/src/mcp.ts
CHANGED
|
@@ -1,170 +1,11 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type { MCPConnection } from "./connection.ts";
|
|
4
|
-
import { createMCPClientProxy } from "./proxy.ts";
|
|
5
2
|
import type { ToolBinder } from "@decocms/bindings";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sql_duration_ms: z.number().optional(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const Meta = z.object({
|
|
17
|
-
changed_db: z.boolean().optional(),
|
|
18
|
-
changes: z.number().optional(),
|
|
19
|
-
duration: z.number().optional(),
|
|
20
|
-
last_row_id: z.number().optional(),
|
|
21
|
-
rows_read: z.number().optional(),
|
|
22
|
-
rows_written: z.number().optional(),
|
|
23
|
-
served_by_primary: z.boolean().optional(),
|
|
24
|
-
served_by_region: z
|
|
25
|
-
.enum(["WNAM", "ENAM", "WEUR", "EEUR", "APAC", "OC"])
|
|
26
|
-
.optional(),
|
|
27
|
-
size_after: z.number().optional(),
|
|
28
|
-
timings: Timings.optional(),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const QueryResult = z.object({
|
|
32
|
-
meta: Meta.optional(),
|
|
33
|
-
results: z.array(z.unknown()).optional(),
|
|
34
|
-
success: z.boolean().optional(),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export type QueryResult = z.infer<typeof QueryResult>;
|
|
38
|
-
|
|
39
|
-
const workspaceTools = [
|
|
40
|
-
{
|
|
41
|
-
name: "INTEGRATIONS_GET" as const,
|
|
42
|
-
inputSchema: z.object({
|
|
43
|
-
id: z.string(),
|
|
44
|
-
}),
|
|
45
|
-
outputSchema: z.object({
|
|
46
|
-
connection: z.object({}),
|
|
47
|
-
}),
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: "DATABASES_RUN_SQL" as const,
|
|
51
|
-
inputSchema: z.object({
|
|
52
|
-
sql: z.string().describe("The SQL query to run"),
|
|
53
|
-
params: z
|
|
54
|
-
.array(z.string())
|
|
55
|
-
.describe("The parameters to pass to the SQL query"),
|
|
56
|
-
}),
|
|
57
|
-
outputSchema: z.object({
|
|
58
|
-
result: z.array(QueryResult),
|
|
59
|
-
}),
|
|
60
|
-
},
|
|
61
|
-
] satisfies ToolBinder<string, unknown, object>[];
|
|
62
|
-
|
|
63
|
-
// Default fetcher instance with API_SERVER_URL and API_HEADERS
|
|
64
|
-
const global = createMCPFetchStub<[]>({});
|
|
65
|
-
export const MCPClient = new Proxy(
|
|
66
|
-
{} as typeof global & {
|
|
67
|
-
forWorkspace: (
|
|
68
|
-
workspace: string,
|
|
69
|
-
token?: string,
|
|
70
|
-
decoCmsApiUrl?: string,
|
|
71
|
-
) => MCPClientFetchStub<typeof workspaceTools>;
|
|
72
|
-
forConnection: <TDefinition extends readonly ToolBinder[]>(
|
|
73
|
-
connection: MCPConnectionProvider,
|
|
74
|
-
decoCmsApiUrl?: string,
|
|
75
|
-
) => MCPClientFetchStub<TDefinition>;
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
get(_, name) {
|
|
79
|
-
if (name === "toJSON") {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (name === "forWorkspace") {
|
|
84
|
-
return (workspace: string, token?: string, decoCmsApiUrl?: string) =>
|
|
85
|
-
createMCPFetchStub<[]>({
|
|
86
|
-
workspace,
|
|
87
|
-
token,
|
|
88
|
-
decoCmsApiUrl,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (name === "forConnection") {
|
|
92
|
-
return <TDefinition extends readonly ToolBinder[]>(
|
|
93
|
-
connection: MCPConnectionProvider,
|
|
94
|
-
decoCmsApiUrl?: string,
|
|
95
|
-
) =>
|
|
96
|
-
createMCPFetchStub<TDefinition>({
|
|
97
|
-
connection,
|
|
98
|
-
decoCmsApiUrl,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return global[name as keyof typeof global];
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
);
|
|
3
|
+
export {
|
|
4
|
+
createMCPFetchStub,
|
|
5
|
+
MCPClient,
|
|
6
|
+
type CreateStubAPIOptions,
|
|
7
|
+
type MCPClientFetchStub,
|
|
8
|
+
type MCPClientStub,
|
|
9
|
+
} from "@decocms/bindings/client"; // Default fetcher instance with API_SERVER_URL and API_HEADERS
|
|
105
10
|
|
|
106
11
|
export type { ToolBinder };
|
|
107
|
-
|
|
108
|
-
export const isStreamableToolBinder = (
|
|
109
|
-
toolBinder: ToolBinder,
|
|
110
|
-
): toolBinder is ToolBinder<string, any, any, true> => {
|
|
111
|
-
return toolBinder.streamable === true;
|
|
112
|
-
};
|
|
113
|
-
export type MCPClientStub<TDefinition extends readonly ToolBinder[]> = {
|
|
114
|
-
[K in TDefinition[number] as K["name"]]: K extends ToolBinder<
|
|
115
|
-
string,
|
|
116
|
-
infer TInput,
|
|
117
|
-
infer TReturn
|
|
118
|
-
>
|
|
119
|
-
? (params: TInput, init?: RequestInit) => Promise<TReturn>
|
|
120
|
-
: never;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export type MCPClientFetchStub<TDefinition extends readonly ToolBinder[]> = {
|
|
124
|
-
[K in TDefinition[number] as K["name"]]: K["streamable"] extends true
|
|
125
|
-
? K extends ToolBinder<string, infer TInput, any, true>
|
|
126
|
-
? (params: TInput, init?: RequestInit) => Promise<Response>
|
|
127
|
-
: never
|
|
128
|
-
: K extends ToolBinder<string, infer TInput, infer TReturn, any>
|
|
129
|
-
? (params: TInput, init?: RequestInit) => Promise<Awaited<TReturn>>
|
|
130
|
-
: never;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
export type MCPConnectionProvider = MCPConnection;
|
|
134
|
-
|
|
135
|
-
export interface MCPClientRaw {
|
|
136
|
-
callTool: (tool: string, args: unknown) => Promise<unknown>;
|
|
137
|
-
listTools: () => Promise<
|
|
138
|
-
{
|
|
139
|
-
name: string;
|
|
140
|
-
inputSchema: any;
|
|
141
|
-
outputSchema?: any;
|
|
142
|
-
description: string;
|
|
143
|
-
}[]
|
|
144
|
-
>;
|
|
145
|
-
}
|
|
146
|
-
export type JSONSchemaToZodConverter = (jsonSchema: any) => z.ZodTypeAny;
|
|
147
|
-
export interface CreateStubAPIOptions {
|
|
148
|
-
mcpPath?: string;
|
|
149
|
-
decoCmsApiUrl?: string;
|
|
150
|
-
workspace?: string;
|
|
151
|
-
token?: string;
|
|
152
|
-
connection?: MCPConnectionProvider;
|
|
153
|
-
streamable?: Record<string, boolean>;
|
|
154
|
-
debugId?: () => string;
|
|
155
|
-
getErrorByStatusCode?: (
|
|
156
|
-
statusCode: number,
|
|
157
|
-
message?: string,
|
|
158
|
-
traceId?: string,
|
|
159
|
-
errorObject?: unknown,
|
|
160
|
-
) => Error;
|
|
161
|
-
supportsToolName?: boolean;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function createMCPFetchStub<TDefinition extends readonly ToolBinder[]>(
|
|
165
|
-
options?: CreateStubAPIOptions,
|
|
166
|
-
): MCPClientFetchStub<TDefinition> {
|
|
167
|
-
return createMCPClientProxy<MCPClientFetchStub<TDefinition>>({
|
|
168
|
-
...(options ?? {}),
|
|
169
|
-
});
|
|
170
|
-
}
|
package/src/proxy.ts
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
|
-
import type { ToolExecutionContext as _ToolExecutionContext } from "@mastra/core";
|
|
3
2
|
import { convertJsonSchemaToZod } from "zod-from-json-schema";
|
|
4
3
|
import { MCPConnection } from "./connection.ts";
|
|
5
4
|
import { createServerClient } from "./mcp-client.ts";
|
|
6
5
|
import type { CreateStubAPIOptions } from "./mcp.ts";
|
|
7
|
-
import { WELL_KNOWN_API_HOSTNAMES } from "./well-known.ts";
|
|
8
|
-
|
|
9
|
-
const getWorkspace = (workspace?: string) => {
|
|
10
|
-
if (workspace && workspace.length > 0 && !workspace.includes("/")) {
|
|
11
|
-
return `/shared/${workspace}`;
|
|
12
|
-
}
|
|
13
|
-
return workspace ?? "";
|
|
14
|
-
};
|
|
15
6
|
|
|
16
7
|
const safeParse = (content: string) => {
|
|
17
8
|
try {
|
|
@@ -33,36 +24,13 @@ const toolsMap = new Map<
|
|
|
33
24
|
>
|
|
34
25
|
>();
|
|
35
26
|
|
|
36
|
-
/**
|
|
37
|
-
* Determines if a given URL supports tool names in the path.
|
|
38
|
-
* Our APIs (api.decocms.com, api.deco.chat, localhost) support /tool/${toolName} routing.
|
|
39
|
-
* Third-party APIs typically don't support this pattern.
|
|
40
|
-
*/
|
|
41
|
-
function supportsToolNameInPath(url: string): boolean {
|
|
42
|
-
try {
|
|
43
|
-
// Our main APIs that support /tool/${toolName} routing
|
|
44
|
-
return WELL_KNOWN_API_HOSTNAMES.includes(new URL(url).hostname);
|
|
45
|
-
} catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
27
|
/**
|
|
51
28
|
* The base fetcher used to fetch the MCP from API.
|
|
52
29
|
*/
|
|
53
30
|
export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
54
|
-
options
|
|
31
|
+
options: CreateStubAPIOptions,
|
|
55
32
|
): T {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const connection: MCPConnection = options?.connection || {
|
|
59
|
-
type: "HTTP",
|
|
60
|
-
token: options?.token,
|
|
61
|
-
url: new URL(
|
|
62
|
-
`${getWorkspace(options?.workspace)}${mcpPath}`,
|
|
63
|
-
options?.decoCmsApiUrl ?? `https://api.decocms.com`,
|
|
64
|
-
).href,
|
|
65
|
-
};
|
|
33
|
+
const connection: MCPConnection = options.connection;
|
|
66
34
|
|
|
67
35
|
return new Proxy<T>({} as T, {
|
|
68
36
|
get(_, name) {
|
|
@@ -81,28 +49,9 @@ export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
|
81
49
|
// Create a connection with the tool name in the URL path for better logging
|
|
82
50
|
// Only modify connections that have a URL property (HTTP, SSE, Websocket)
|
|
83
51
|
// Use automatic detection based on URL, with optional override
|
|
84
|
-
let toolConnection = connection;
|
|
85
|
-
const shouldAddToolName =
|
|
86
|
-
options?.supportsToolName ??
|
|
87
|
-
("url" in connection &&
|
|
88
|
-
typeof connection.url === "string" &&
|
|
89
|
-
supportsToolNameInPath(connection.url));
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
shouldAddToolName &&
|
|
93
|
-
"url" in connection &&
|
|
94
|
-
typeof connection.url === "string"
|
|
95
|
-
) {
|
|
96
|
-
toolConnection = {
|
|
97
|
-
...connection,
|
|
98
|
-
url: connection.url.endsWith("/")
|
|
99
|
-
? `${connection.url}tool/${String(name)}`
|
|
100
|
-
: `${connection.url}/tool/${String(name)}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
52
|
|
|
104
53
|
const { client, callStreamableTool } = await createServerClient(
|
|
105
|
-
{ connection
|
|
54
|
+
{ connection },
|
|
106
55
|
undefined,
|
|
107
56
|
extraHeaders,
|
|
108
57
|
);
|
package/src/state.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
3
|
-
import type { AppContext } from "./
|
|
4
|
-
import { createTool } from "./mastra.ts";
|
|
2
|
+
import { DefaultEnv } from "./index.ts";
|
|
3
|
+
import type { AppContext } from "./tools.ts";
|
|
5
4
|
|
|
6
5
|
const asyncLocalStorage = new AsyncLocalStorage<AppContext | undefined>();
|
|
7
6
|
|
|
@@ -9,36 +8,9 @@ export const State = {
|
|
|
9
8
|
getStore: () => {
|
|
10
9
|
return asyncLocalStorage.getStore();
|
|
11
10
|
},
|
|
12
|
-
run: <TEnv, R, TArgs extends unknown[]>(
|
|
11
|
+
run: <TEnv extends DefaultEnv, R, TArgs extends unknown[]>(
|
|
13
12
|
ctx: AppContext<TEnv>,
|
|
14
13
|
f: (...args: TArgs) => R,
|
|
15
14
|
...args: TArgs
|
|
16
15
|
): R => asyncLocalStorage.run(ctx, f, ...args),
|
|
17
16
|
};
|
|
18
|
-
|
|
19
|
-
export interface ValidationPayload {
|
|
20
|
-
state: unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const createStateValidationTool = (stateSchema?: z.ZodTypeAny) => {
|
|
24
|
-
return createTool({
|
|
25
|
-
id: "DECO_CHAT_STATE_VALIDATION",
|
|
26
|
-
description: "Validate the state of the OAuth flow",
|
|
27
|
-
inputSchema: z.object({
|
|
28
|
-
state: z.unknown(),
|
|
29
|
-
}),
|
|
30
|
-
outputSchema: z.object({
|
|
31
|
-
valid: z.boolean(),
|
|
32
|
-
}),
|
|
33
|
-
execute: (ctx) => {
|
|
34
|
-
if (!stateSchema) {
|
|
35
|
-
return Promise.resolve({ valid: true });
|
|
36
|
-
}
|
|
37
|
-
const parsed = stateSchema.safeParse(ctx.context.state);
|
|
38
|
-
return Promise.resolve({
|
|
39
|
-
valid: parsed.success,
|
|
40
|
-
reason: parsed.error?.message,
|
|
41
|
-
});
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
};
|