@decocms/runtime 1.1.3 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/asset-server/index.ts +4 -2
- package/src/index.ts +6 -0
- package/src/tools.ts +215 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/runtime",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@cloudflare/workers-types": "^4.20250617.0",
|
|
11
11
|
"@decocms/bindings": "^1.0.7",
|
|
12
|
-
"@modelcontextprotocol/sdk": "1.25.
|
|
12
|
+
"@modelcontextprotocol/sdk": "1.25.2",
|
|
13
13
|
"@ai-sdk/provider": "^3.0.0",
|
|
14
14
|
"hono": "^4.10.7",
|
|
15
15
|
"jose": "^6.0.11",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { devServerProxy } from "./dev-server-proxy";
|
|
2
|
-
import { resolve, dirname } from "path";
|
|
2
|
+
import { resolve, dirname, join } from "path";
|
|
3
3
|
|
|
4
4
|
export interface AssetServerConfig {
|
|
5
5
|
/**
|
|
@@ -209,9 +209,11 @@ export function createAssetHandler(config: AssetServerConfig = {}) {
|
|
|
209
209
|
return null; // Path traversal attempt blocked
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// Try to serve the index.html file relative to the requested file
|
|
213
|
+
const indexRelativeToFilePath = join(filePath, "index.html");
|
|
212
214
|
// Try to serve the requested file, fall back to index.html for SPA routing
|
|
213
215
|
const indexPath = resolve(clientDir, "index.html");
|
|
214
|
-
for (const pathToTry of [filePath, indexPath]) {
|
|
216
|
+
for (const pathToTry of [filePath, indexRelativeToFilePath, indexPath]) {
|
|
215
217
|
try {
|
|
216
218
|
const file = Bun.file(pathToTry);
|
|
217
219
|
if (await file.exists()) {
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,12 @@ export {
|
|
|
22
22
|
type PromptExecutionContext,
|
|
23
23
|
type CreatedPrompt,
|
|
24
24
|
type GetPromptResult,
|
|
25
|
+
createResource,
|
|
26
|
+
createPublicResource,
|
|
27
|
+
type Resource,
|
|
28
|
+
type ResourceExecutionContext,
|
|
29
|
+
type ResourceContents,
|
|
30
|
+
type CreatedResource,
|
|
25
31
|
} from "./tools.ts";
|
|
26
32
|
import type { Binding } from "./wrangler.ts";
|
|
27
33
|
export { proxyConnectionForId, BindingOf } from "./bindings.ts";
|
package/src/tools.ts
CHANGED
|
@@ -133,6 +133,67 @@ export type CreatedPrompt = {
|
|
|
133
133
|
}): Promise<GetPromptResult> | GetPromptResult;
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Resource Types
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Context passed to resource read functions.
|
|
142
|
+
*/
|
|
143
|
+
export interface ResourceExecutionContext {
|
|
144
|
+
uri: URL;
|
|
145
|
+
runtimeContext: AppContext;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resource contents returned from read operations.
|
|
150
|
+
* Per MCP spec, resources return either text or blob content.
|
|
151
|
+
*/
|
|
152
|
+
export interface ResourceContents {
|
|
153
|
+
/** The URI of the resource */
|
|
154
|
+
uri: string;
|
|
155
|
+
/** MIME type of the content */
|
|
156
|
+
mimeType?: string;
|
|
157
|
+
/** Text content (for text-based resources) */
|
|
158
|
+
text?: string;
|
|
159
|
+
/** Base64-encoded binary content (for binary resources) */
|
|
160
|
+
blob?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resource interface for defining MCP resources.
|
|
165
|
+
* Resources are read-only, addressable entities that expose data like config, docs, or context.
|
|
166
|
+
*/
|
|
167
|
+
export interface Resource {
|
|
168
|
+
/** Resource URI (static) or URI template (e.g., "config://app" or "file://{path}") */
|
|
169
|
+
uri: string;
|
|
170
|
+
/** Human-readable name for the resource */
|
|
171
|
+
name: string;
|
|
172
|
+
/** Description of what the resource contains */
|
|
173
|
+
description?: string;
|
|
174
|
+
/** MIME type of the resource content */
|
|
175
|
+
mimeType?: string;
|
|
176
|
+
/** Handler function to read the resource content */
|
|
177
|
+
read(
|
|
178
|
+
context: ResourceExecutionContext,
|
|
179
|
+
): Promise<ResourceContents> | ResourceContents;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* CreatedResource is a permissive type that any Resource can be assigned to.
|
|
184
|
+
* Uses a structural type with relaxed read signature to allow resources with any context.
|
|
185
|
+
*/
|
|
186
|
+
export type CreatedResource = {
|
|
187
|
+
uri: string;
|
|
188
|
+
name: string;
|
|
189
|
+
description?: string;
|
|
190
|
+
mimeType?: string;
|
|
191
|
+
read(context: {
|
|
192
|
+
uri: URL;
|
|
193
|
+
runtimeContext: AppContext;
|
|
194
|
+
}): Promise<ResourceContents> | ResourceContents;
|
|
195
|
+
};
|
|
196
|
+
|
|
136
197
|
/**
|
|
137
198
|
* creates a private tool that always ensure for athentication before being executed
|
|
138
199
|
*/
|
|
@@ -223,6 +284,39 @@ export function createPrompt<TArgs extends PromptArgsRawShape>(
|
|
|
223
284
|
});
|
|
224
285
|
}
|
|
225
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Creates a public resource that does not require authentication.
|
|
289
|
+
*/
|
|
290
|
+
export function createPublicResource(opts: Resource): Resource {
|
|
291
|
+
return {
|
|
292
|
+
...opts,
|
|
293
|
+
read: (input: ResourceExecutionContext) => {
|
|
294
|
+
return opts.read({
|
|
295
|
+
...input,
|
|
296
|
+
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Creates a resource that always ensures authentication before being read.
|
|
304
|
+
* This is the default and recommended way to create resources.
|
|
305
|
+
*/
|
|
306
|
+
export function createResource(opts: Resource): Resource {
|
|
307
|
+
const read = opts.read;
|
|
308
|
+
return createPublicResource({
|
|
309
|
+
...opts,
|
|
310
|
+
read: (input: ResourceExecutionContext) => {
|
|
311
|
+
const env = input.runtimeContext.env;
|
|
312
|
+
if (env) {
|
|
313
|
+
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
314
|
+
}
|
|
315
|
+
return read(input);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
226
320
|
export interface ViewExport {
|
|
227
321
|
title: string;
|
|
228
322
|
icon: string;
|
|
@@ -360,6 +454,17 @@ export interface CreateMCPServerOptions<
|
|
|
360
454
|
| Promise<CreatedPrompt[]>
|
|
361
455
|
>
|
|
362
456
|
| ((env: TEnv) => CreatedPrompt[] | Promise<CreatedPrompt[]>);
|
|
457
|
+
resources?:
|
|
458
|
+
| Array<
|
|
459
|
+
(
|
|
460
|
+
env: TEnv,
|
|
461
|
+
) =>
|
|
462
|
+
| Promise<CreatedResource>
|
|
463
|
+
| CreatedResource
|
|
464
|
+
| CreatedResource[]
|
|
465
|
+
| Promise<CreatedResource[]>
|
|
466
|
+
>
|
|
467
|
+
| ((env: TEnv) => CreatedResource[] | Promise<CreatedResource[]>);
|
|
363
468
|
}
|
|
364
469
|
|
|
365
470
|
export type Fetch<TEnv = unknown> = (
|
|
@@ -530,7 +635,7 @@ export const createMCPServer = <
|
|
|
530
635
|
|
|
531
636
|
const server = new McpServer(
|
|
532
637
|
{ name: "@deco/mcp-api", version: "1.0.0" },
|
|
533
|
-
{ capabilities: { tools: {}, prompts: {} } },
|
|
638
|
+
{ capabilities: { tools: {}, prompts: {}, resources: {} } },
|
|
534
639
|
);
|
|
535
640
|
|
|
536
641
|
const toolsFn =
|
|
@@ -576,13 +681,28 @@ export const createMCPServer = <
|
|
|
576
681
|
: z.object({}).shape,
|
|
577
682
|
},
|
|
578
683
|
async (args) => {
|
|
579
|
-
|
|
684
|
+
const result = await tool.execute({
|
|
580
685
|
context: args,
|
|
581
686
|
runtimeContext: createRuntimeContext(),
|
|
582
687
|
});
|
|
583
688
|
|
|
689
|
+
// For streamable tools, the Response is handled at the transport layer
|
|
690
|
+
// Do NOT call result.bytes() - it buffers the entire response in memory
|
|
691
|
+
// causing massive memory leaks (2GB+ Uint8Array accumulation)
|
|
584
692
|
if (isStreamableTool(tool) && result instanceof Response) {
|
|
585
|
-
|
|
693
|
+
return {
|
|
694
|
+
structuredContent: {
|
|
695
|
+
streamable: true,
|
|
696
|
+
status: result.status,
|
|
697
|
+
statusText: result.statusText,
|
|
698
|
+
},
|
|
699
|
+
content: [
|
|
700
|
+
{
|
|
701
|
+
type: "text",
|
|
702
|
+
text: `Streaming response: ${result.status} ${result.statusText}`,
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
};
|
|
586
706
|
}
|
|
587
707
|
return {
|
|
588
708
|
structuredContent: result as Record<string, unknown>,
|
|
@@ -637,7 +757,86 @@ export const createMCPServer = <
|
|
|
637
757
|
);
|
|
638
758
|
}
|
|
639
759
|
|
|
640
|
-
|
|
760
|
+
// Resolve and register resources
|
|
761
|
+
const resourcesFn =
|
|
762
|
+
typeof options.resources === "function"
|
|
763
|
+
? options.resources
|
|
764
|
+
: async (bindings: TEnv) => {
|
|
765
|
+
if (typeof options.resources === "function") {
|
|
766
|
+
return await options.resources(bindings);
|
|
767
|
+
}
|
|
768
|
+
return await Promise.all(
|
|
769
|
+
options.resources?.flatMap(async (resource) => {
|
|
770
|
+
const resourceResult = resource(bindings);
|
|
771
|
+
const awaited = await resourceResult;
|
|
772
|
+
if (Array.isArray(awaited)) {
|
|
773
|
+
return awaited;
|
|
774
|
+
}
|
|
775
|
+
return [awaited];
|
|
776
|
+
}) ?? [],
|
|
777
|
+
).then((r) => r.flat());
|
|
778
|
+
};
|
|
779
|
+
const resources = await resourcesFn(bindings);
|
|
780
|
+
|
|
781
|
+
for (const resource of resources) {
|
|
782
|
+
server.resource(
|
|
783
|
+
resource.name,
|
|
784
|
+
resource.uri,
|
|
785
|
+
{
|
|
786
|
+
description: resource.description,
|
|
787
|
+
mimeType: resource.mimeType,
|
|
788
|
+
},
|
|
789
|
+
async (uri) => {
|
|
790
|
+
const result = await resource.read({
|
|
791
|
+
uri,
|
|
792
|
+
runtimeContext: createRuntimeContext(),
|
|
793
|
+
});
|
|
794
|
+
// Build content object based on what's provided (text or blob, not both)
|
|
795
|
+
const content: {
|
|
796
|
+
uri: string;
|
|
797
|
+
mimeType?: string;
|
|
798
|
+
text?: string;
|
|
799
|
+
blob?: string;
|
|
800
|
+
} = { uri: result.uri };
|
|
801
|
+
|
|
802
|
+
if (result.mimeType) {
|
|
803
|
+
content.mimeType = result.mimeType;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// MCP SDK expects either text or blob content, not both
|
|
807
|
+
if (result.text !== undefined) {
|
|
808
|
+
return {
|
|
809
|
+
contents: [
|
|
810
|
+
{
|
|
811
|
+
uri: result.uri,
|
|
812
|
+
mimeType: result.mimeType,
|
|
813
|
+
text: result.text,
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
};
|
|
817
|
+
} else if (result.blob !== undefined) {
|
|
818
|
+
return {
|
|
819
|
+
contents: [
|
|
820
|
+
{
|
|
821
|
+
uri: result.uri,
|
|
822
|
+
mimeType: result.mimeType,
|
|
823
|
+
blob: result.blob,
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Fallback to empty text if neither provided
|
|
830
|
+
return {
|
|
831
|
+
contents: [
|
|
832
|
+
{ uri: result.uri, mimeType: result.mimeType, text: "" },
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
},
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return { server, tools, prompts, resources };
|
|
641
840
|
};
|
|
642
841
|
|
|
643
842
|
const fetch = async (req: Request, env: TEnv) => {
|
|
@@ -646,7 +845,18 @@ export const createMCPServer = <
|
|
|
646
845
|
|
|
647
846
|
await server.connect(transport);
|
|
648
847
|
|
|
649
|
-
|
|
848
|
+
try {
|
|
849
|
+
return await transport.handleRequest(req);
|
|
850
|
+
} finally {
|
|
851
|
+
// CRITICAL: Close transport to prevent memory leaks
|
|
852
|
+
// Without this, ReadableStream/WritableStream controllers accumulate
|
|
853
|
+
// causing thousands of stream objects to be retained in memory
|
|
854
|
+
try {
|
|
855
|
+
await transport.close?.();
|
|
856
|
+
} catch {
|
|
857
|
+
// Ignore close errors - transport may already be closed
|
|
858
|
+
}
|
|
859
|
+
}
|
|
650
860
|
};
|
|
651
861
|
|
|
652
862
|
const callTool: CallTool = async ({ toolCallId, toolCallInput }) => {
|