@decocms/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +84 -167
- package/src/mcp.ts +7 -166
- package/src/proxy.ts +3 -54
- package/src/state.ts +3 -31
- package/src/tools.ts +372 -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
|
@@ -1,56 +1,31 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
|
-
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
3
2
|
import { decodeJwt } from "jose";
|
|
4
3
|
import type { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
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";
|
|
4
|
+
import { createContractBinding, createIntegrationBinding } from "./bindings.ts";
|
|
5
|
+
import { type CORSOptions, handlePreflight, withCORS } from "./cors.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";
|
|
26
13
|
export { proxyConnectionForId } from "./bindings.ts";
|
|
14
|
+
export { type CORSOptions, type CORSOrigin } from "./cors.ts";
|
|
27
15
|
export {
|
|
28
16
|
createMCPFetchStub,
|
|
29
17
|
type CreateStubAPIOptions,
|
|
30
18
|
type ToolBinder,
|
|
31
19
|
} from "./mcp.ts";
|
|
32
|
-
export interface WorkspaceDB {
|
|
33
|
-
query: (params: {
|
|
34
|
-
sql: string;
|
|
35
|
-
params: string[];
|
|
36
|
-
}) => Promise<{ result: QueryResult[] }>;
|
|
37
|
-
}
|
|
38
20
|
|
|
39
21
|
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
|
-
};
|
|
22
|
+
MESH_REQUEST_CONTEXT: RequestContext<TSchema>;
|
|
23
|
+
MESH_BINDINGS: string;
|
|
24
|
+
MESH_APP_DEPLOYMENT_ID: string;
|
|
53
25
|
IS_LOCAL: boolean;
|
|
26
|
+
MESH_URL?: string;
|
|
27
|
+
MESH_RUNTIME_TOKEN?: string;
|
|
28
|
+
MESH_APP_NAME?: string;
|
|
54
29
|
[key: string]: unknown;
|
|
55
30
|
}
|
|
56
31
|
|
|
@@ -58,7 +33,7 @@ export interface BindingsObject {
|
|
|
58
33
|
bindings?: Binding[];
|
|
59
34
|
}
|
|
60
35
|
|
|
61
|
-
export const
|
|
36
|
+
export const MCPBindings = {
|
|
62
37
|
parse: (bindings?: string): Binding[] => {
|
|
63
38
|
if (!bindings) return [];
|
|
64
39
|
try {
|
|
@@ -77,11 +52,12 @@ export interface UserDefaultExport<
|
|
|
77
52
|
TSchema extends z.ZodTypeAny = never,
|
|
78
53
|
TEnv = TUserEnv & DefaultEnv<TSchema>,
|
|
79
54
|
> extends CreateMCPServerOptions<TEnv, TSchema> {
|
|
80
|
-
fetch?: (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
55
|
+
fetch?: (req: Request, env: TEnv, ctx: any) => Promise<Response> | Response;
|
|
56
|
+
/**
|
|
57
|
+
* CORS configuration options.
|
|
58
|
+
* Set to `false` to disable CORS handling entirely.
|
|
59
|
+
*/
|
|
60
|
+
cors?: CORSOptions | false;
|
|
85
61
|
}
|
|
86
62
|
|
|
87
63
|
// 1. Map binding type to its interface
|
|
@@ -104,14 +80,13 @@ export interface User {
|
|
|
104
80
|
|
|
105
81
|
export interface RequestContext<TSchema extends z.ZodTypeAny = any> {
|
|
106
82
|
state: z.infer<TSchema>;
|
|
107
|
-
branch?: string;
|
|
108
83
|
token: string;
|
|
109
|
-
|
|
84
|
+
meshUrl: string;
|
|
110
85
|
ensureAuthenticated: (options?: {
|
|
111
86
|
workspaceHint?: string;
|
|
112
87
|
}) => User | undefined;
|
|
113
88
|
callerApp?: string;
|
|
114
|
-
|
|
89
|
+
connectionId?: string;
|
|
115
90
|
}
|
|
116
91
|
|
|
117
92
|
// 2. Map binding type to its creator function
|
|
@@ -131,26 +106,12 @@ const creatorByType: CreatorByType = {
|
|
|
131
106
|
const withDefaultBindings = ({
|
|
132
107
|
env,
|
|
133
108
|
server,
|
|
134
|
-
ctx,
|
|
135
109
|
url,
|
|
136
110
|
}: {
|
|
137
111
|
env: DefaultEnv;
|
|
138
112
|
server: MCPServer<any, any>;
|
|
139
|
-
ctx: RequestContext;
|
|
140
113
|
url?: string;
|
|
141
114
|
}) => {
|
|
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
115
|
env["SELF"] = new Proxy(
|
|
155
116
|
{},
|
|
156
117
|
{
|
|
@@ -169,15 +130,6 @@ const withDefaultBindings = ({
|
|
|
169
130
|
},
|
|
170
131
|
);
|
|
171
132
|
|
|
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
133
|
env["IS_LOCAL"] =
|
|
182
134
|
(url?.startsWith("http://localhost") ||
|
|
183
135
|
url?.startsWith("http://127.0.0.1")) ??
|
|
@@ -194,13 +146,9 @@ export class UnauthorizedError extends Error {
|
|
|
194
146
|
}
|
|
195
147
|
}
|
|
196
148
|
|
|
197
|
-
const
|
|
198
|
-
const AUTH_START_ENDPOINT = "/oauth/start";
|
|
199
|
-
const AUTH_LOGOUT_ENDPOINT = "/oauth/logout";
|
|
200
|
-
const AUTHENTICATED = (user?: unknown, workspace?: string) => () => {
|
|
149
|
+
const AUTHENTICATED = (user?: unknown) => () => {
|
|
201
150
|
return {
|
|
202
151
|
...((user as User) ?? {}),
|
|
203
|
-
workspace,
|
|
204
152
|
} as User;
|
|
205
153
|
};
|
|
206
154
|
|
|
@@ -208,66 +156,64 @@ export const withBindings = <TEnv>({
|
|
|
208
156
|
env: _env,
|
|
209
157
|
server,
|
|
210
158
|
tokenOrContext,
|
|
211
|
-
origin,
|
|
212
159
|
url,
|
|
213
|
-
|
|
160
|
+
bindings: inlineBindings,
|
|
214
161
|
}: {
|
|
215
162
|
env: TEnv;
|
|
216
163
|
server: MCPServer<TEnv, any>;
|
|
217
164
|
tokenOrContext?: string | RequestContext;
|
|
218
|
-
origin?: string | null;
|
|
219
165
|
url?: string;
|
|
220
|
-
|
|
166
|
+
bindings?: Binding[];
|
|
221
167
|
}): TEnv => {
|
|
222
|
-
branch ??= undefined;
|
|
223
168
|
const env = _env as DefaultEnv<any>;
|
|
224
169
|
|
|
225
|
-
const apiUrl = env.DECO_API_URL ?? "https://api.decocms.com";
|
|
226
170
|
let context;
|
|
227
171
|
if (typeof tokenOrContext === "string") {
|
|
228
172
|
const decoded = decodeJwt(tokenOrContext);
|
|
229
|
-
|
|
173
|
+
// Support both new JWT format (fields directly on payload) and legacy format (nested in metadata)
|
|
174
|
+
const metadata =
|
|
175
|
+
(decoded.metadata as {
|
|
176
|
+
state?: Record<string, unknown>;
|
|
177
|
+
meshUrl?: string;
|
|
178
|
+
connectionId?: string;
|
|
179
|
+
}) ?? {};
|
|
230
180
|
|
|
231
181
|
context = {
|
|
232
|
-
state: decoded.state
|
|
182
|
+
state: decoded.state ?? metadata.state,
|
|
233
183
|
token: tokenOrContext,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ensureAuthenticated: AUTHENTICATED(decoded.user
|
|
237
|
-
branch,
|
|
184
|
+
meshUrl: (decoded.meshUrl as string) ?? metadata.meshUrl,
|
|
185
|
+
connectionId: (decoded.connectionId as string) ?? metadata.connectionId,
|
|
186
|
+
ensureAuthenticated: AUTHENTICATED(decoded.user ?? decoded.sub),
|
|
238
187
|
} as RequestContext<any>;
|
|
239
188
|
} else if (typeof tokenOrContext === "object") {
|
|
240
189
|
context = tokenOrContext;
|
|
241
190
|
const decoded = decodeJwt(tokenOrContext.token);
|
|
242
|
-
|
|
191
|
+
// Support both new JWT format (fields directly on payload) and legacy format (nested in metadata)
|
|
192
|
+
const metadata =
|
|
193
|
+
(decoded.metadata as {
|
|
194
|
+
state?: Record<string, unknown>;
|
|
195
|
+
meshUrl?: string;
|
|
196
|
+
connectionId?: string;
|
|
197
|
+
}) ?? {};
|
|
243
198
|
const appName = decoded.appName as string | undefined;
|
|
244
199
|
context.callerApp = appName;
|
|
245
|
-
context.
|
|
246
|
-
|
|
200
|
+
context.connectionId ??=
|
|
201
|
+
(decoded.connectionId as string) ?? metadata.connectionId;
|
|
202
|
+
context.ensureAuthenticated = AUTHENTICATED(decoded.user ?? decoded.sub);
|
|
247
203
|
} else {
|
|
248
204
|
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);
|
|
205
|
+
state: {},
|
|
206
|
+
token: undefined,
|
|
207
|
+
meshUrl: undefined,
|
|
208
|
+
connectionId: undefined,
|
|
209
|
+
ensureAuthenticated: () => {
|
|
210
|
+
throw new Error("Unauthorized");
|
|
265
211
|
},
|
|
266
|
-
}
|
|
212
|
+
} as unknown as RequestContext<any>;
|
|
267
213
|
}
|
|
268
214
|
|
|
269
|
-
env.
|
|
270
|
-
const bindings =
|
|
215
|
+
env.MESH_REQUEST_CONTEXT = context;
|
|
216
|
+
const bindings = inlineBindings ?? MCPBindings.parse(env.MESH_BINDINGS);
|
|
271
217
|
|
|
272
218
|
for (const binding of bindings) {
|
|
273
219
|
env[binding.name] = creatorByType[binding.type](binding as any, env);
|
|
@@ -276,7 +222,6 @@ export const withBindings = <TEnv>({
|
|
|
276
222
|
withDefaultBindings({
|
|
277
223
|
env,
|
|
278
224
|
server,
|
|
279
|
-
ctx: env.DECO_REQUEST_CONTEXT,
|
|
280
225
|
url,
|
|
281
226
|
});
|
|
282
227
|
|
|
@@ -285,29 +230,16 @@ export const withBindings = <TEnv>({
|
|
|
285
230
|
|
|
286
231
|
export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
287
232
|
userFns: UserDefaultExport<TEnv, TSchema>,
|
|
288
|
-
)
|
|
233
|
+
) => {
|
|
289
234
|
const server = createMCPServer<TEnv, TSchema>(userFns);
|
|
235
|
+
const corsOptions = userFns.cors;
|
|
236
|
+
|
|
290
237
|
const fetcher = async (
|
|
291
238
|
req: Request,
|
|
292
239
|
env: TEnv & DefaultEnv<TSchema>,
|
|
293
|
-
ctx:
|
|
240
|
+
ctx: any,
|
|
294
241
|
) => {
|
|
295
242
|
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
243
|
if (url.pathname === "/mcp") {
|
|
312
244
|
return server.fetch(req, env, ctx);
|
|
313
245
|
}
|
|
@@ -334,55 +266,40 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
|
334
266
|
});
|
|
335
267
|
}
|
|
336
268
|
|
|
337
|
-
if (url.pathname.startsWith(DeconfigResource.WatchPathNameBase)) {
|
|
338
|
-
return DeconfigResource.watchAPI(req, env);
|
|
339
|
-
}
|
|
340
269
|
return (
|
|
341
270
|
userFns.fetch?.(req, env, ctx) ||
|
|
342
271
|
new Response("Not found", { status: 404 })
|
|
343
272
|
);
|
|
344
273
|
};
|
|
274
|
+
|
|
345
275
|
return {
|
|
346
|
-
fetch: async (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const referer = req.headers.get("referer");
|
|
352
|
-
const isFetchRequest = req.headers.has(DECO_MCP_CLIENT_HEADER);
|
|
353
|
-
|
|
354
|
-
try {
|
|
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;
|
|
276
|
+
fetch: async (req: Request, env: TEnv & DefaultEnv<TSchema>, ctx: any) => {
|
|
277
|
+
// Handle CORS preflight (OPTIONS) requests
|
|
278
|
+
if (corsOptions !== false && req.method === "OPTIONS") {
|
|
279
|
+
const options = corsOptions ?? {};
|
|
280
|
+
return handlePreflight(req, options);
|
|
385
281
|
}
|
|
282
|
+
|
|
283
|
+
const bindings = withBindings({
|
|
284
|
+
env,
|
|
285
|
+
server,
|
|
286
|
+
bindings: userFns.bindings,
|
|
287
|
+
tokenOrContext: req.headers.get("x-mesh-token") ?? undefined,
|
|
288
|
+
url: req.url,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const response = await State.run(
|
|
292
|
+
{ req, env: bindings, ctx },
|
|
293
|
+
async () => await fetcher(req, bindings, ctx),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Add CORS headers to response
|
|
297
|
+
if (corsOptions !== false) {
|
|
298
|
+
const options = corsOptions ?? {};
|
|
299
|
+
return withCORS(response, req, options);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return response;
|
|
386
303
|
},
|
|
387
304
|
};
|
|
388
305
|
};
|
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
|
-
};
|