@decocms/runtime 0.25.0 → 0.26.0

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 (46) hide show
  1. package/dist/bindings/deconfig/index.d.ts +6 -5
  2. package/dist/bindings/deconfig/index.js +4 -4
  3. package/dist/bindings/index.d.ts +1506 -8
  4. package/dist/bindings/index.js +9 -7
  5. package/dist/bindings/index.js.map +1 -1
  6. package/dist/{chunk-F6XZPFWM.js → chunk-3AWMDSOH.js} +20 -51
  7. package/dist/chunk-3AWMDSOH.js.map +1 -0
  8. package/dist/{chunk-QELHWEZH.js → chunk-5EYZ2LVM.js} +8 -5
  9. package/dist/chunk-5EYZ2LVM.js.map +1 -0
  10. package/dist/{chunk-O6IURJAY.js → chunk-GPIGZ6DL.js} +38 -33
  11. package/dist/chunk-GPIGZ6DL.js.map +1 -0
  12. package/dist/{chunk-I2KGAHFY.js → chunk-LCU3FBI3.js} +6 -2
  13. package/dist/chunk-LCU3FBI3.js.map +1 -0
  14. package/dist/drizzle.d.ts +4 -3
  15. package/dist/{index-CStCyFvK.d.ts → index-COMJ3oN7.d.ts} +4 -3
  16. package/dist/{index-DswRTp9B.d.ts → index-DqyElLzZ.d.ts} +4 -4
  17. package/dist/index.d.ts +4 -3
  18. package/dist/index.js +8 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/mastra.d.ts +4 -3
  21. package/dist/mastra.js +1 -1
  22. package/dist/{mcp-yTBduksO.d.ts → mcp-Dbqp-p04.d.ts} +5 -9
  23. package/dist/mcp-client.d.ts +10 -14
  24. package/dist/mcp-client.js +1 -1
  25. package/dist/proxy.d.ts +3 -2
  26. package/dist/proxy.js +2 -2
  27. package/package.json +4 -2
  28. package/src/bindings/binder.ts +37 -34
  29. package/src/bindings/channels.ts +1 -1
  30. package/src/bindings/index.ts +8 -6
  31. package/src/bindings/language-model/ai-sdk.ts +87 -0
  32. package/src/bindings/language-model/index.ts +4 -0
  33. package/src/bindings/language-model/utils.ts +118 -0
  34. package/src/bindings/views.ts +1 -1
  35. package/src/connection.ts +8 -53
  36. package/src/http-client-transport.ts +1 -66
  37. package/src/index.ts +4 -0
  38. package/src/mastra.ts +4 -0
  39. package/src/mcp-client.ts +22 -2
  40. package/src/mcp.ts +16 -17
  41. package/src/proxy.ts +6 -2
  42. package/dist/chunk-F6XZPFWM.js.map +0 -1
  43. package/dist/chunk-I2KGAHFY.js.map +0 -1
  44. package/dist/chunk-O6IURJAY.js.map +0 -1
  45. package/dist/chunk-QELHWEZH.js.map +0 -1
  46. package/dist/connection-DDtQYrea.d.ts +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/runtime",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -9,9 +9,11 @@
9
9
  "dependencies": {
10
10
  "@cloudflare/workers-types": "^4.20250617.0",
11
11
  "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5",
12
+ "@decocms/bindings": "workspace:*",
12
13
  "@mastra/cloudflare-d1": "^0.13.4",
13
14
  "@mastra/core": "^0.20.2",
14
15
  "@modelcontextprotocol/sdk": "^1.19.1",
16
+ "@ai-sdk/provider": "^2.0.0",
15
17
  "bidc": "0.0.3",
16
18
  "drizzle-orm": "^0.44.5",
17
19
  "jose": "^6.0.11",
@@ -101,4 +103,4 @@
101
103
  "publishConfig": {
102
104
  "access": "public"
103
105
  }
104
- }
106
+ }
@@ -1,14 +1,14 @@
1
1
  /* oxlint-disable no-explicit-any */
2
2
  import { z } from "zod";
3
3
  import type { MCPConnection } from "../connection.ts";
4
- import { createPrivateTool } from "../mastra.ts";
4
+ import { createPrivateTool, createStreamableTool } from "../mastra.ts";
5
5
  import {
6
6
  createMCPFetchStub,
7
7
  type MCPClientFetchStub,
8
8
  type ToolBinder,
9
9
  } from "../mcp.ts";
10
- import { CHANNEL_BINDING_SCHEMA } from "./channels.ts";
11
- import { VIEW_BINDING_SCHEMA } from "./views.ts";
10
+ import { CHANNEL_BINDING } from "./channels.ts";
11
+ import { VIEW_BINDING } from "./views.ts";
12
12
 
13
13
  // ToolLike is a simplified version of the Tool interface that matches what we need for bindings
14
14
  export interface ToolLike<
@@ -75,27 +75,16 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
75
75
  forConnection: (
76
76
  mcpConnection: MCPConnection,
77
77
  ): MCPClientFetchStub<TDefinition> => {
78
- const stub = createMCPFetchStub<TDefinition>({
78
+ return createMCPFetchStub<TDefinition>({
79
79
  connection: mcpConnection,
80
- });
81
- return new Proxy<MCPClientFetchStub<TDefinition>>(
82
- {} as MCPClientFetchStub<TDefinition>,
83
- {
84
- get(_, name) {
85
- if (typeof name !== "string") {
86
- throw new Error("Name must be a string");
87
- }
88
-
89
- return (args: Record<string, unknown>) => {
90
- return (
91
- stub[name as keyof MCPClientFetchStub<TDefinition>] as (
92
- args: Record<string, unknown>,
93
- ) => Promise<unknown>
94
- )(args);
95
- };
80
+ streamable: binder.reduce(
81
+ (acc, tool) => {
82
+ acc[tool.name] = tool.streamable === true;
83
+ return acc;
96
84
  },
97
- },
98
- );
85
+ {} as Record<string, boolean>,
86
+ ),
87
+ });
99
88
  },
100
89
  };
101
90
  };
@@ -103,9 +92,8 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
103
92
  export type MCPBindingClient<T extends ReturnType<typeof bindingClient>> =
104
93
  ReturnType<T["forConnection"]>;
105
94
 
106
- export const ChannelBinding = bindingClient(CHANNEL_BINDING_SCHEMA);
107
-
108
- export const ViewBinding = bindingClient(VIEW_BINDING_SCHEMA);
95
+ export const ChannelBinding = bindingClient(CHANNEL_BINDING);
96
+ export const ViewBinding = bindingClient(VIEW_BINDING);
109
97
 
110
98
  export type { Callbacks } from "./channels.ts";
111
99
 
@@ -113,8 +101,12 @@ export const impl = <TBinder extends Binder>(
113
101
  schema: TBinder,
114
102
  implementation: BinderImplementation<TBinder>,
115
103
  createToolFn = createPrivateTool,
104
+ createStreamableToolFn = createStreamableTool,
116
105
  ) => {
117
- const impl: ReturnType<typeof createToolFn>[] = [];
106
+ const impl: (
107
+ | ReturnType<typeof createToolFn>
108
+ | ReturnType<typeof createStreamableToolFn>
109
+ )[] = [];
118
110
  for (const key in schema) {
119
111
  const toolSchema = schema[key];
120
112
  const toolImplementation = implementation[key];
@@ -127,17 +119,28 @@ export const impl = <TBinder extends Binder>(
127
119
  throw new Error(`Implementation for ${key} is required`);
128
120
  }
129
121
 
130
- const { name, handler, ...toolLike }: ToolLike = {
122
+ const { name, handler, streamable, ...toolLike } = {
131
123
  ...toolSchema,
132
124
  ...toolImplementation,
133
125
  };
134
- impl.push(
135
- createToolFn({
136
- ...toolLike,
137
- id: name,
138
- execute: ({ context }) => Promise.resolve(handler(context)),
139
- }),
140
- );
126
+ if (streamable) {
127
+ impl.push(
128
+ createStreamableToolFn({
129
+ ...toolLike,
130
+ streamable,
131
+ id: name,
132
+ execute: ({ context }) => Promise.resolve(handler(context)),
133
+ }),
134
+ );
135
+ } else {
136
+ impl.push(
137
+ createToolFn({
138
+ ...toolLike,
139
+ id: name,
140
+ execute: ({ context }) => Promise.resolve(handler(context)),
141
+ }),
142
+ );
143
+ }
141
144
  }
142
145
  return impl;
143
146
  };
@@ -34,7 +34,7 @@ const listChannelsSchema = z.object({
34
34
  export type Callbacks = z.infer<typeof callbacksSchema>;
35
35
  export type JoinedChannelPayload = z.infer<typeof joinChannelSchema>;
36
36
  export type ListChannelsSchema = z.infer<typeof listChannelsSchema>;
37
- export const CHANNEL_BINDING_SCHEMA = [
37
+ export const CHANNEL_BINDING = [
38
38
  {
39
39
  name: "DECO_CHAT_CHANNELS_JOIN" as const,
40
40
  inputSchema: joinChannelSchema,
@@ -1,7 +1,8 @@
1
- import { CHANNEL_BINDING_SCHEMA } from "./channels.ts";
2
- import { VIEW_BINDING_SCHEMA } from "./views.ts";
1
+ import { CHANNEL_BINDING } from "./channels.ts";
2
+ import { VIEW_BINDING as VIEWS_BINDING } from "./views.ts";
3
3
 
4
4
  // Import new Resources 2.0 bindings function
5
+ import { LANGUAGE_MODEL_BINDING } from "@decocms/bindings/llm";
5
6
  import { createResourceBindings } from "./resources/bindings.ts";
6
7
 
7
8
  // Export types and utilities from binder
@@ -35,11 +36,11 @@ export * from "./resources/schemas.ts";
35
36
 
36
37
  // Export deconfig helpers and types
37
38
  export {
38
- ResourcePath,
39
- ResourceUri,
40
39
  getMetadataString as deconfigGetMetadataString,
41
40
  getMetadataValue as deconfigGetMetadataValue,
42
41
  normalizeDirectory as deconfigNormalizeDirectory,
42
+ ResourcePath,
43
+ ResourceUri,
43
44
  } from "./deconfig/helpers.ts";
44
45
  export { createDeconfigResource } from "./deconfig/index.ts";
45
46
  export type {
@@ -52,8 +53,9 @@ export type {
52
53
  export { deconfigTools } from "./deconfig/types.ts";
53
54
 
54
55
  export const WellKnownBindings = {
55
- Channel: CHANNEL_BINDING_SCHEMA,
56
- View: VIEW_BINDING_SCHEMA,
56
+ Channel: CHANNEL_BINDING,
57
+ View: VIEWS_BINDING,
58
+ LanguageModel: LANGUAGE_MODEL_BINDING,
57
59
  // Note: Resources is not included here since it's a generic function
58
60
  // Use createResourceBindings(dataSchema) directly for Resources 2.0
59
61
  } as const;
@@ -0,0 +1,87 @@
1
+ import type {
2
+ LanguageModelV2,
3
+ LanguageModelV2CallOptions,
4
+ ProviderV2,
5
+ } from "@ai-sdk/provider";
6
+ import { LanguageModelBinding } from "@decocms/bindings/llm";
7
+ import { lazy, responseToStream } from "./utils";
8
+
9
+ const toRegExp = (supportedUrls: Record<string, string[]>) => {
10
+ return Object.fromEntries(
11
+ Object.entries(supportedUrls).map(([key, values]) => [
12
+ key,
13
+ values.map((v) => new RegExp(v)),
14
+ ]),
15
+ );
16
+ };
17
+
18
+ type LLMBindingClient = ReturnType<
19
+ (typeof LanguageModelBinding)["forConnection"]
20
+ >;
21
+
22
+ export interface Provider extends ProviderV2 {
23
+ listModels: LLMBindingClient["COLLECTION_MODELS_LIST"];
24
+ }
25
+
26
+ /**
27
+ * Creates a ai-sdk compatible provider for the given binding
28
+ * @param binding - The binding to create the provider from
29
+ * @returns The provider
30
+ */
31
+ export const createProvider = (binding: LLMBindingClient): Provider => {
32
+ return {
33
+ imageModel: () => {
34
+ throw new Error("Image models are not supported by this provider");
35
+ },
36
+ textEmbeddingModel: () => {
37
+ throw new Error(
38
+ "Text embedding models are not supported by this provider",
39
+ );
40
+ },
41
+ listModels: async () => {
42
+ return await binding.COLLECTION_MODELS_LIST({});
43
+ },
44
+ languageModel: (modelId: string): LanguageModelV2 => {
45
+ const supportedUrls = lazy(() =>
46
+ binding
47
+ .LLM_METADATA({ modelId })
48
+ .then((metadata) => toRegExp(metadata.supportedUrls)),
49
+ );
50
+
51
+ return {
52
+ specificationVersion: "v2" as const,
53
+ provider: "llm-binding",
54
+ modelId,
55
+ supportedUrls,
56
+ doGenerate: async (options: LanguageModelV2CallOptions) => {
57
+ const response = await binding.LLM_DO_GENERATE({
58
+ callOptions: options,
59
+ modelId,
60
+ });
61
+ // Ensure usage fields are always present as required by LanguageModelV2
62
+ return {
63
+ ...response,
64
+ usage: {
65
+ inputTokens: response.usage.inputTokens ?? undefined,
66
+ outputTokens: response.usage.outputTokens ?? undefined,
67
+ totalTokens: response.usage.totalTokens ?? undefined,
68
+ reasoningTokens: response.usage.reasoningTokens ?? undefined,
69
+ },
70
+ };
71
+ },
72
+ doStream: async (options: LanguageModelV2CallOptions) => {
73
+ const response = await binding.LLM_DO_STREAM({
74
+ callOptions: options,
75
+ modelId,
76
+ });
77
+ return {
78
+ stream: responseToStream(response),
79
+ response: {
80
+ headers: Object.fromEntries(response.headers?.entries() ?? []),
81
+ },
82
+ };
83
+ },
84
+ };
85
+ },
86
+ };
87
+ };
@@ -0,0 +1,4 @@
1
+ export * from "@decocms/bindings";
2
+ export { LanguageModelBinding } from "@decocms/bindings/llm";
3
+ export * from "./ai-sdk.ts";
4
+ export * from "./utils.ts";
@@ -0,0 +1,118 @@
1
+ import { LanguageModelV2StreamPart } from "@ai-sdk/provider";
2
+
3
+ // Helper to convert the AI SDK stream to a Response
4
+ export function streamToResponse(
5
+ stream: ReadableStream<LanguageModelV2StreamPart>,
6
+ headers?: Record<string, string>,
7
+ ): Response {
8
+ // Transform LanguageModelV2StreamPart objects to newline-delimited JSON
9
+ const encodedStream = stream.pipeThrough(
10
+ new TransformStream<LanguageModelV2StreamPart, Uint8Array>({
11
+ transform(chunk, controller) {
12
+ // Serialize each chunk as JSON with newline delimiter
13
+ const line = JSON.stringify(chunk) + "\n";
14
+ controller.enqueue(new TextEncoder().encode(line));
15
+ },
16
+ }),
17
+ );
18
+
19
+ return new Response(encodedStream, {
20
+ headers: {
21
+ "Content-Type": "application/x-ndjson", // newline-delimited JSON
22
+ "Cache-Control": "no-cache",
23
+ Connection: "keep-alive",
24
+ ...headers,
25
+ },
26
+ });
27
+ }
28
+
29
+ export function responseToStream(
30
+ response: Response,
31
+ ): ReadableStream<LanguageModelV2StreamPart> {
32
+ if (!response.body) {
33
+ throw new Error("Response body is null");
34
+ }
35
+
36
+ return response.body.pipeThrough(new TextDecoderStream()).pipeThrough(
37
+ new TransformStream<string, LanguageModelV2StreamPart>({
38
+ transform(chunk, controller) {
39
+ // Split by newlines and parse each line
40
+ const lines = chunk.split("\n");
41
+
42
+ for (const line of lines) {
43
+ if (line.trim()) {
44
+ try {
45
+ const parsed = JSON.parse(line) as LanguageModelV2StreamPart;
46
+ controller.enqueue(parsed);
47
+ } catch (error) {
48
+ console.error("Failed to parse stream chunk:", error);
49
+ }
50
+ }
51
+ }
52
+ },
53
+ }),
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Lazy promise wrapper that defers execution until the promise is awaited.
59
+ * The factory function is only called when .then() is invoked for the first time.
60
+ */
61
+ class Lazy<T> implements PromiseLike<T> {
62
+ private promise: Promise<T> | null = null;
63
+
64
+ constructor(private factory: () => Promise<T>) {}
65
+
66
+ private getOrCreatePromise(): Promise<T> {
67
+ if (!this.promise) {
68
+ this.promise = this.factory();
69
+ }
70
+ return this.promise;
71
+ }
72
+
73
+ // eslint-disable-next-line no-thenable
74
+ then<TResult1 = T, TResult2 = never>(
75
+ onfulfilled?:
76
+ | ((value: T) => TResult1 | PromiseLike<TResult1>)
77
+ | null
78
+ | undefined,
79
+ onrejected?:
80
+ | ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
81
+ | null
82
+ | undefined,
83
+ ): PromiseLike<TResult1 | TResult2> {
84
+ return this.getOrCreatePromise().then(onfulfilled, onrejected);
85
+ }
86
+
87
+ catch<TResult = never>(
88
+ onrejected?:
89
+ | ((reason: unknown) => TResult | PromiseLike<TResult>)
90
+ | null
91
+ | undefined,
92
+ ): Promise<T | TResult> {
93
+ return this.getOrCreatePromise().catch(onrejected);
94
+ }
95
+
96
+ finally(onfinally?: (() => void) | null | undefined): Promise<T> {
97
+ return this.getOrCreatePromise().finally(onfinally);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Creates a lazy promise that only executes when awaited.
103
+ *
104
+ * @param factory - A function that returns a Promise<T>
105
+ * @returns A Promise-like object that defers execution until .then() is called
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const lazyData = lazy(() => fetchExpensiveData());
110
+ * // fetchExpensiveData() is NOT called yet
111
+ *
112
+ * const result = await lazyData;
113
+ * // fetchExpensiveData() is called NOW
114
+ * ```
115
+ */
116
+ export function lazy<T>(factory: () => Promise<T>): Promise<T> {
117
+ return new Lazy(factory) as unknown as Promise<T>;
118
+ }
@@ -2,7 +2,7 @@ import { ViewsListOutputSchema } from "../views.ts";
2
2
  import { z } from "zod";
3
3
  import type { ToolBinder } from "../mcp.ts";
4
4
 
5
- export const VIEW_BINDING_SCHEMA = [
5
+ export const VIEW_BINDING = [
6
6
  {
7
7
  name: "DECO_CHAT_VIEWS_LIST" as const,
8
8
  inputSchema: z.any(),
package/src/connection.ts CHANGED
@@ -1,53 +1,8 @@
1
- export type SSEConnection = {
2
- type: "SSE";
3
- url: string;
4
- token?: string;
5
- headers?: Record<string, string>;
6
- };
7
-
8
- export type WebsocketConnection = {
9
- type: "Websocket";
10
- url: string;
11
- token?: string;
12
- };
13
-
14
- export type DecoConnection = {
15
- type: "Deco";
16
- tenant: string;
17
- token?: string;
18
- };
19
-
20
- export type InnateConnection = {
21
- type: "INNATE";
22
- name: string;
23
- workspace?: string;
24
- };
25
-
26
- export type HTTPConnection = {
27
- type: "HTTP";
28
- url: string;
29
- headers?: Record<string, string>;
30
- token?: string;
31
- };
32
-
33
- export type MCPConnection =
34
- | SSEConnection
35
- | WebsocketConnection
36
- | InnateConnection
37
- | DecoConnection
38
- | HTTPConnection;
39
-
40
- export type Integration = {
41
- /** Unique identifier for the MCP */
42
- id: string;
43
- /** Human-readable name of the integration */
44
- name: string;
45
- /** Brief description of the integration's functionality */
46
- description?: string;
47
- /** URL to the integration's icon */
48
- icon?: string;
49
- /** Access level of the integration */
50
- access?: string | null;
51
- /** Connection configuration */
52
- connection: MCPConnection;
53
- };
1
+ export type {
2
+ DecoConnection,
3
+ HTTPConnection,
4
+ InnateConnection,
5
+ MCPConnection,
6
+ SSEConnection,
7
+ WebsocketConnection,
8
+ } from "@decocms/bindings/connection";
@@ -1,66 +1 @@
1
- import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2
- import {
3
- StreamableHTTPClientTransport,
4
- type StreamableHTTPClientTransportOptions,
5
- } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
-
7
- export class HTTPClientTransport extends StreamableHTTPClientTransport {
8
- constructor(url: URL, opts?: StreamableHTTPClientTransportOptions) {
9
- super(url, opts);
10
- }
11
-
12
- override send(
13
- message: JSONRPCMessage,
14
- options?: {
15
- resumptionToken?: string;
16
- onresumptiontoken?: (token: string) => void;
17
- },
18
- ): Promise<void> {
19
- const mockAction = getMockActionFor(message);
20
- if (mockAction?.type === "emit") {
21
- this.onmessage?.(mockAction.message);
22
- return Promise.resolve();
23
- }
24
- if (mockAction?.type === "suppress") {
25
- return Promise.resolve();
26
- }
27
- return super.send(message, options);
28
- }
29
- }
30
-
31
- type MockAction =
32
- | { type: "emit"; message: JSONRPCMessage }
33
- | { type: "suppress" };
34
-
35
- function getMockActionFor(message: JSONRPCMessage): MockAction | null {
36
- const m = message;
37
- if (!m || typeof m !== "object" || !("method" in m)) return null;
38
-
39
- switch (m.method) {
40
- case "initialize": {
41
- const protocolVersion = m?.params?.protocolVersion;
42
- if (!protocolVersion) return null;
43
- return {
44
- type: "emit",
45
- message: {
46
- result: {
47
- protocolVersion,
48
- capabilities: { tools: {} },
49
- serverInfo: { name: "deco-chat-server", version: "1.0.0" },
50
- },
51
- jsonrpc: m.jsonrpc ?? "2.0",
52
- // @ts-expect-error - id is not typed
53
- id: m.id,
54
- } as JSONRPCMessage,
55
- };
56
- }
57
- case "notifications/roots/list_changed":
58
- case "notifications/initialized":
59
- case "notifications/cancelled":
60
- case "notifications/progress": {
61
- return { type: "suppress" };
62
- }
63
- default:
64
- return null;
65
- }
66
- }
1
+ export { HTTPClientTransport } from "@decocms/bindings/client";
package/src/index.ts CHANGED
@@ -337,6 +337,10 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
337
337
  toolCallInput,
338
338
  });
339
339
 
340
+ if (result instanceof Response) {
341
+ return result;
342
+ }
343
+
340
344
  return new Response(JSON.stringify(result), {
341
345
  headers: {
342
346
  "Content-Type": "application/json",
package/src/mastra.ts CHANGED
@@ -128,6 +128,10 @@ export function createStreamableTool<
128
128
  return {
129
129
  ...streamableTool,
130
130
  execute: (input: ToolExecutionContext<TSchemaIn>) => {
131
+ const env = input.runtimeContext.get("env") as DefaultEnv;
132
+ if (env) {
133
+ env.DECO_REQUEST_CONTEXT.ensureAuthenticated();
134
+ }
131
135
  return streamableTool.execute({
132
136
  ...input,
133
137
  runtimeContext: createRuntimeContext(input.runtimeContext),
package/src/mcp-client.ts CHANGED
@@ -42,11 +42,15 @@ class Client extends BaseClient {
42
42
  }
43
43
  }
44
44
 
45
+ export interface ServerClient {
46
+ client: Client;
47
+ callStreamableTool: (tool: string, args: unknown) => Promise<Response>;
48
+ }
45
49
  export const createServerClient = async (
46
50
  mcpServer: { connection: MCPConnection; name?: string },
47
51
  signal?: AbortSignal,
48
52
  extraHeaders?: Record<string, string>,
49
- ): Promise<Client> => {
53
+ ): Promise<ServerClient> => {
50
54
  const transport = createTransport(mcpServer.connection, signal, extraHeaders);
51
55
 
52
56
  if (!transport) {
@@ -60,7 +64,23 @@ export const createServerClient = async (
60
64
 
61
65
  await client.connect(transport);
62
66
 
63
- return client;
67
+ return {
68
+ client,
69
+ callStreamableTool: (tool, args) => {
70
+ if (mcpServer.connection.type !== "HTTP") {
71
+ throw new Error("HTTP connection required");
72
+ }
73
+ return fetch(mcpServer.connection.url + `/call-tool/${tool}`, {
74
+ method: "POST",
75
+ redirect: "manual",
76
+ body: JSON.stringify(args),
77
+ headers: {
78
+ ...extraHeaders,
79
+ Authorization: `Bearer ${mcpServer.connection.token}`,
80
+ },
81
+ });
82
+ },
83
+ };
64
84
  };
65
85
 
66
86
  export const createTransport = (
package/src/mcp.ts CHANGED
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import type { MCPConnection } from "./connection.ts";
5
5
  import type { DefaultEnv } from "./index.ts";
6
6
  import { createMCPClientProxy } from "./proxy.ts";
7
+ import type { ToolBinder } from "@decocms/bindings";
7
8
 
8
9
  export interface FetchOptions extends RequestInit {
9
10
  path?: string;
@@ -101,16 +102,13 @@ export const MCPClient = new Proxy(
101
102
  },
102
103
  );
103
104
 
104
- export interface ToolBinder<
105
- TName extends string = string,
106
- TInput = any,
107
- TReturn extends object | null | boolean = object,
108
- > {
109
- name: TName;
110
- inputSchema: z.ZodType<TInput>;
111
- outputSchema?: z.ZodType<TReturn>;
112
- opt?: true;
113
- }
105
+ export type { ToolBinder };
106
+
107
+ export const isStreamableToolBinder = (
108
+ toolBinder: ToolBinder,
109
+ ): toolBinder is ToolBinder<string, any, any, true> => {
110
+ return toolBinder.streamable === true;
111
+ };
114
112
  export type MCPClientStub<TDefinition extends readonly ToolBinder[]> = {
115
113
  [K in TDefinition[number] as K["name"]]: K extends ToolBinder<
116
114
  string,
@@ -122,13 +120,13 @@ export type MCPClientStub<TDefinition extends readonly ToolBinder[]> = {
122
120
  };
123
121
 
124
122
  export type MCPClientFetchStub<TDefinition extends readonly ToolBinder[]> = {
125
- [K in TDefinition[number] as K["name"]]: K extends ToolBinder<
126
- string,
127
- infer TInput,
128
- infer TReturn
129
- >
130
- ? (params: TInput, init?: RequestInit) => Promise<Awaited<TReturn>>
131
- : never;
123
+ [K in TDefinition[number] as K["name"]]: K["streamable"] extends true
124
+ ? K extends ToolBinder<string, infer TInput, any, true>
125
+ ? (params: TInput, init?: RequestInit) => Promise<Response>
126
+ : never
127
+ : K extends ToolBinder<string, infer TInput, infer TReturn, any>
128
+ ? (params: TInput, init?: RequestInit) => Promise<Awaited<TReturn>>
129
+ : never;
132
130
  };
133
131
 
134
132
  export type MCPConnectionProvider = MCPConnection;
@@ -151,6 +149,7 @@ export interface CreateStubAPIOptions {
151
149
  workspace?: string;
152
150
  token?: string;
153
151
  connection?: MCPConnectionProvider;
152
+ streamable?: Record<string, boolean>;
154
153
  debugId?: () => string;
155
154
  getErrorByStatusCode?: (
156
155
  statusCode: number,