@decocms/runtime 0.0.1-testing-beta.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/config-schema.json +553 -0
- package/dist/admin.d.ts +5 -0
- package/dist/admin.js +21 -0
- package/dist/admin.js.map +1 -0
- package/dist/bindings/deconfig/index.d.ts +9 -0
- package/dist/bindings/deconfig/index.js +9 -0
- package/dist/bindings/deconfig/index.js.map +1 -0
- package/dist/bindings/index.d.ts +1053 -0
- package/dist/bindings/index.js +132 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/chunk-4XSQKJLU.js +105 -0
- package/dist/chunk-4XSQKJLU.js.map +1 -0
- package/dist/chunk-AOFOWQXY.js +27 -0
- package/dist/chunk-AOFOWQXY.js.map +1 -0
- package/dist/chunk-F6XZPFWM.js +127 -0
- package/dist/chunk-F6XZPFWM.js.map +1 -0
- package/dist/chunk-IB3KGSMB.js +150 -0
- package/dist/chunk-IB3KGSMB.js.map +1 -0
- package/dist/chunk-NKUMVYKI.js +128 -0
- package/dist/chunk-NKUMVYKI.js.map +1 -0
- package/dist/chunk-NMXOC7PT.js +763 -0
- package/dist/chunk-NMXOC7PT.js.map +1 -0
- package/dist/chunk-OSSKGDAG.js +395 -0
- package/dist/chunk-OSSKGDAG.js.map +1 -0
- package/dist/chunk-UHR3BLMF.js +92 -0
- package/dist/chunk-UHR3BLMF.js.map +1 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -0
- package/dist/connection-DDtQYrea.d.ts +30 -0
- package/dist/drizzle.d.ts +47 -0
- package/dist/drizzle.js +121 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/index-AKVjfH4b.d.ts +336 -0
- package/dist/index-kMsI0ELb.d.ts +530 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +507 -0
- package/dist/index.js.map +1 -0
- package/dist/mastra.d.ts +8 -0
- package/dist/mastra.js +5 -0
- package/dist/mastra.js.map +1 -0
- package/dist/mcp-Bv7IAgWX.d.ts +109 -0
- package/dist/mcp-client.d.ts +236 -0
- package/dist/mcp-client.js +3 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/proxy.d.ts +10 -0
- package/dist/proxy.js +4 -0
- package/dist/proxy.js.map +1 -0
- package/dist/resources.d.ts +362 -0
- package/dist/resources.js +3 -0
- package/dist/resources.js.map +1 -0
- package/dist/views.d.ts +72 -0
- package/dist/views.js +3 -0
- package/dist/views.js.map +1 -0
- package/package.json +98 -0
- package/src/admin.ts +16 -0
- package/src/auth.ts +233 -0
- package/src/bindings/README.md +132 -0
- package/src/bindings/binder.ts +143 -0
- package/src/bindings/channels.ts +54 -0
- package/src/bindings/deconfig/helpers.ts +107 -0
- package/src/bindings/deconfig/index.ts +1 -0
- package/src/bindings/deconfig/resources.ts +659 -0
- package/src/bindings/deconfig/types.ts +106 -0
- package/src/bindings/index.ts +61 -0
- package/src/bindings/resources/bindings.ts +99 -0
- package/src/bindings/resources/helpers.ts +95 -0
- package/src/bindings/resources/schemas.ts +265 -0
- package/src/bindings/utils.ts +22 -0
- package/src/bindings/views.ts +14 -0
- package/src/bindings.ts +179 -0
- package/src/client.ts +201 -0
- package/src/connection.ts +53 -0
- package/src/drizzle.ts +201 -0
- package/src/http-client-transport.ts +66 -0
- package/src/index.ts +394 -0
- package/src/mastra.ts +666 -0
- package/src/mcp-client.ts +119 -0
- package/src/mcp.ts +171 -0
- package/src/proxy.ts +204 -0
- package/src/resources.ts +168 -0
- package/src/state.ts +44 -0
- package/src/views.ts +26 -0
- package/src/well-known.ts +20 -0
- package/src/wrangler.ts +146 -0
package/src/auth.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { JWK, jwtVerify } from "jose";
|
|
2
|
+
import type { DefaultEnv } from "./index.ts";
|
|
3
|
+
|
|
4
|
+
const DECO_APP_AUTH_COOKIE_NAME = "deco_page_auth";
|
|
5
|
+
const MAX_COOKIE_SIZE = 4000; // Leave some buffer below the 4096 limit
|
|
6
|
+
|
|
7
|
+
export interface State {
|
|
8
|
+
next?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const StateParser = {
|
|
12
|
+
parse: (state: string) => {
|
|
13
|
+
return JSON.parse(decodeURIComponent(atob(state))) as State;
|
|
14
|
+
},
|
|
15
|
+
stringify: (state: State) => {
|
|
16
|
+
return btoa(encodeURIComponent(JSON.stringify(state)));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Helper function to chunk a value into multiple cookies
|
|
21
|
+
const chunkValue = (value: string): string[] => {
|
|
22
|
+
if (value.length <= MAX_COOKIE_SIZE) {
|
|
23
|
+
return [value];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const chunks: string[] = [];
|
|
27
|
+
for (let i = 0; i < value.length; i += MAX_COOKIE_SIZE) {
|
|
28
|
+
chunks.push(value.slice(i, i + MAX_COOKIE_SIZE));
|
|
29
|
+
}
|
|
30
|
+
return chunks;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Helper function to reassemble chunked cookies
|
|
34
|
+
const reassembleChunkedCookies = (
|
|
35
|
+
cookies: Record<string, string>,
|
|
36
|
+
baseName: string,
|
|
37
|
+
): string | undefined => {
|
|
38
|
+
// First try the base cookie (non-chunked)
|
|
39
|
+
if (cookies[baseName]) {
|
|
40
|
+
return cookies[baseName];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try to reassemble from chunks
|
|
44
|
+
const chunks: string[] = [];
|
|
45
|
+
let index = 0;
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
const chunkName = `${baseName}_${index}`;
|
|
49
|
+
if (!cookies[chunkName]) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
chunks.push(cookies[chunkName]);
|
|
53
|
+
index++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return chunks.length > 0 ? chunks.join("") : undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Helper function to parse cookies from request
|
|
60
|
+
const parseCookies = (cookieHeader: string): Record<string, string> => {
|
|
61
|
+
const cookies: Record<string, string> = {};
|
|
62
|
+
if (!cookieHeader) return cookies;
|
|
63
|
+
|
|
64
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
65
|
+
const [name, ...rest] = cookie.trim().split("=");
|
|
66
|
+
if (name && rest.length > 0) {
|
|
67
|
+
cookies[name] = decodeURIComponent(rest.join("="));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return cookies;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const parseJWK = (jwk: string): JWK => JSON.parse(atob(jwk)) as JWK;
|
|
75
|
+
|
|
76
|
+
export const getReqToken = async (req: Request, env: DefaultEnv) => {
|
|
77
|
+
const token = () => {
|
|
78
|
+
// First try to get token from Authorization header
|
|
79
|
+
const authHeader = req.headers.get("Authorization");
|
|
80
|
+
if (authHeader) {
|
|
81
|
+
return authHeader.split(" ")[1];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If not found, try to get from cookie
|
|
85
|
+
const cookieHeader = req.headers.get("Cookie");
|
|
86
|
+
if (cookieHeader) {
|
|
87
|
+
const cookies = parseCookies(cookieHeader);
|
|
88
|
+
return reassembleChunkedCookies(cookies, DECO_APP_AUTH_COOKIE_NAME);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const authToken = token();
|
|
95
|
+
if (!authToken) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
env.DECO_API_JWT_PUBLIC_KEY &&
|
|
100
|
+
(await jwtVerify(authToken, parseJWK(env.DECO_API_JWT_PUBLIC_KEY), {
|
|
101
|
+
issuer: "https://api.decocms.com",
|
|
102
|
+
algorithms: ["RS256"],
|
|
103
|
+
typ: "JWT",
|
|
104
|
+
}).catch((err) => {
|
|
105
|
+
console.error(
|
|
106
|
+
`[auth-token]: error validating: ${err} ${env.DECO_API_JWT_PUBLIC_KEY}`,
|
|
107
|
+
);
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
return authToken;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export interface AuthCallbackOptions {
|
|
114
|
+
apiUrl?: string;
|
|
115
|
+
appName: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const handleAuthCallback = async (
|
|
119
|
+
req: Request,
|
|
120
|
+
options: AuthCallbackOptions,
|
|
121
|
+
): Promise<Response> => {
|
|
122
|
+
const url = new URL(req.url);
|
|
123
|
+
const code = url.searchParams.get("code");
|
|
124
|
+
const state = url.searchParams.get("state");
|
|
125
|
+
|
|
126
|
+
if (!code) {
|
|
127
|
+
return new Response("Missing authorization code", { status: 400 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse state to get the next URL
|
|
131
|
+
let next = "/";
|
|
132
|
+
if (state) {
|
|
133
|
+
try {
|
|
134
|
+
const parsedState = StateParser.parse(state);
|
|
135
|
+
next = parsedState.next || "/";
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore parse errors
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Exchange code for token
|
|
143
|
+
const apiUrl = options.apiUrl ?? "https://api.decocms.com";
|
|
144
|
+
const exchangeResponse = await fetch(`${apiUrl}/apps/code-exchange`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
code,
|
|
151
|
+
client_id: options.appName,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!exchangeResponse.ok) {
|
|
156
|
+
console.error(
|
|
157
|
+
"authentication failed",
|
|
158
|
+
code,
|
|
159
|
+
options.appName,
|
|
160
|
+
await exchangeResponse.text().catch((_) => ""),
|
|
161
|
+
);
|
|
162
|
+
return new Response("Authentication failed", { status: 401 });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { access_token } = (await exchangeResponse.json()) as {
|
|
166
|
+
access_token: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (!access_token) {
|
|
170
|
+
return new Response("No access token received", { status: 401 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Chunk the token if it's too large
|
|
174
|
+
const chunks = chunkValue(access_token);
|
|
175
|
+
const headers = new Headers();
|
|
176
|
+
headers.set("Location", next);
|
|
177
|
+
|
|
178
|
+
// Set cookies for each chunk
|
|
179
|
+
if (chunks.length === 1) {
|
|
180
|
+
// Single cookie for small tokens
|
|
181
|
+
headers.set(
|
|
182
|
+
"Set-Cookie",
|
|
183
|
+
`${DECO_APP_AUTH_COOKIE_NAME}=${access_token}; HttpOnly; SameSite=None; Secure; Path=/`,
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
// Multiple cookies for large tokens
|
|
187
|
+
chunks.forEach((chunk, index) => {
|
|
188
|
+
headers.append(
|
|
189
|
+
"Set-Cookie",
|
|
190
|
+
`${DECO_APP_AUTH_COOKIE_NAME}_${index}=${chunk}; HttpOnly; SameSite=None; Secure; Path=/`,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Response(null, {
|
|
196
|
+
status: 302,
|
|
197
|
+
headers,
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return new Response(`Authentication failed ${err}`, { status: 500 });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const removeAuthCookie = (headers: Headers) => {
|
|
205
|
+
// Clear the base cookie
|
|
206
|
+
headers.append(
|
|
207
|
+
"Set-Cookie",
|
|
208
|
+
`${DECO_APP_AUTH_COOKIE_NAME}=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0`,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Clear all potential chunked cookies
|
|
212
|
+
// We'll try to clear up to 10 chunks (which would support tokens up to 40KB)
|
|
213
|
+
// This is a reasonable upper limit
|
|
214
|
+
for (let i = 0; i < 10; i++) {
|
|
215
|
+
headers.append(
|
|
216
|
+
"Set-Cookie",
|
|
217
|
+
`${DECO_APP_AUTH_COOKIE_NAME}_${i}=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const handleLogout = (req: Request) => {
|
|
223
|
+
const url = new URL(req.url);
|
|
224
|
+
const next = url.searchParams.get("next");
|
|
225
|
+
const redirectTo = new URL("/", url);
|
|
226
|
+
const headers = new Headers();
|
|
227
|
+
removeAuthCookie(headers);
|
|
228
|
+
headers.set("Location", next ?? redirectTo.href);
|
|
229
|
+
return new Response(null, {
|
|
230
|
+
status: 302,
|
|
231
|
+
headers,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Bindings
|
|
2
|
+
|
|
3
|
+
Bindings are a core concept for defining and enforcing standardized interfaces
|
|
4
|
+
that MCPs (Model Context Protocols) can implement. They provide a type-safe,
|
|
5
|
+
declarative way to specify what methods and schemas an integration (MCP) must
|
|
6
|
+
expose to be compatible with certain parts of the system, similar to how
|
|
7
|
+
TypeScript interfaces work.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
- **Standardization:** Bindings define contracts (schemas and method names) that
|
|
12
|
+
MCPs must implement to be considered compatible with a given integration
|
|
13
|
+
point.
|
|
14
|
+
- **Type Safety:** Bindings leverage Zod schemas and TypeScript types to ensure
|
|
15
|
+
correct data structures and method signatures.
|
|
16
|
+
- **Extensibility:** You can define new bindings for any use case, not just the
|
|
17
|
+
built-in triggers.
|
|
18
|
+
|
|
19
|
+
## How Bindings Work
|
|
20
|
+
|
|
21
|
+
1. **Define a Binding:**\
|
|
22
|
+
A binding is a list of required tool definitions (name, input/output schema).
|
|
23
|
+
2. **Implement the Binding:**\
|
|
24
|
+
An MCP "implements" a binding by exposing all required tools with the correct
|
|
25
|
+
names and schemas.
|
|
26
|
+
3. **Check Implementation:**\
|
|
27
|
+
The system can check if an MCP or a set of tools implements a binding using
|
|
28
|
+
helper functions.
|
|
29
|
+
4. **Typed Client:**\
|
|
30
|
+
You can create a type-safe client for interacting with an MCP that implements
|
|
31
|
+
a binding.
|
|
32
|
+
|
|
33
|
+
## Example: Trigger Bindings
|
|
34
|
+
|
|
35
|
+
- **Input Binding (`ON_AGENT_INPUT`):**\
|
|
36
|
+
Used for MCPs that handle incoming events, such as webhooks.
|
|
37
|
+
|
|
38
|
+
These are defined in [`trigger.ts`](./trigger.ts) and exported for use.
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### binder.ts
|
|
43
|
+
|
|
44
|
+
- `bindingClient(binder)`\
|
|
45
|
+
Creates a binding client for a given binding definition.
|
|
46
|
+
- `.implements(connectionOrTools)` — Checks if a connection or tool list
|
|
47
|
+
implements the binding.
|
|
48
|
+
- `.forConnection(mcpConnection)` — Returns a type-safe client for calling the
|
|
49
|
+
bound tools.
|
|
50
|
+
|
|
51
|
+
- `TriggerInputBinding`\
|
|
52
|
+
Predefined binding for agent input triggers.
|
|
53
|
+
|
|
54
|
+
### utils.ts
|
|
55
|
+
|
|
56
|
+
- `Binding(binder)`\
|
|
57
|
+
Utility for checking if a set of tools implements a binding (by name).
|
|
58
|
+
|
|
59
|
+
## Usage Example
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { TriggerInputBinding } from "./bindings/trigger.ts";
|
|
63
|
+
|
|
64
|
+
// Check if a connection implements the input trigger binding
|
|
65
|
+
const isImplemented = await TriggerInputBinding.implements(connection);
|
|
66
|
+
|
|
67
|
+
// Create a client for a connection that implements the binding
|
|
68
|
+
const triggerClient = TriggerInputBinding.forConnection(connection);
|
|
69
|
+
await triggerClient.ON_AGENT_INPUT({ payload: ..., callbacks: ... });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Creating a New Binding
|
|
73
|
+
|
|
74
|
+
To create a new binding:
|
|
75
|
+
|
|
76
|
+
1. **Create a new file** in the `/bindings` folder (e.g., `my-binding.ts`).
|
|
77
|
+
2. **Define your binding** using the `Binder` type and Zod schemas for
|
|
78
|
+
input/output:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { z } from "zod";
|
|
82
|
+
import type { Binder } from "../index.ts";
|
|
83
|
+
|
|
84
|
+
const myInputSchema = z.object({ ... });
|
|
85
|
+
const myOutputSchema = z.object({ ... });
|
|
86
|
+
|
|
87
|
+
export const MY_BINDING_SCHEMA = [{
|
|
88
|
+
name: "MY_BINDING_METHOD" as const,
|
|
89
|
+
inputSchema: myInputSchema,
|
|
90
|
+
outputSchema: myOutputSchema,
|
|
91
|
+
}] as const satisfies Binder;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
3. **Export your binding** in `index.ts`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
export * from "./my-binding.ts";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
4. **Use your binding** in the UI or backend as needed.
|
|
101
|
+
|
|
102
|
+
## Using Bindings in the UI
|
|
103
|
+
|
|
104
|
+
Bindings are integrated into the UI to allow users to select integrations that
|
|
105
|
+
implement a specific binding. For example:
|
|
106
|
+
|
|
107
|
+
- The
|
|
108
|
+
[`BindingSelector`](../../../apps/web/src/components/toolsets/binding-selector.tsx)
|
|
109
|
+
component lets users pick an integration that implements a given binding. It
|
|
110
|
+
uses the `Binding` utility to check if an integration's tools match the
|
|
111
|
+
binding.
|
|
112
|
+
- The
|
|
113
|
+
[`WebhookTriggerForm`](../../../apps/web/src/components/triggers/webhookTriggerForm.tsx)
|
|
114
|
+
uses `BindingSelector` to let users select an integration for the webhook
|
|
115
|
+
trigger, passing the `TRIGGER_INPUT_BINDING_SCHEMA` as the required binding.
|
|
116
|
+
|
|
117
|
+
This pattern allows you to:
|
|
118
|
+
|
|
119
|
+
- Ask the user to select an integration that implements a specific binding
|
|
120
|
+
through the UI
|
|
121
|
+
- Ensure only compatible integrations are selectable
|
|
122
|
+
|
|
123
|
+
## Extending Bindings
|
|
124
|
+
|
|
125
|
+
You can define your own bindings by specifying the required tool names and
|
|
126
|
+
schemas, then use the same pattern to check and interact with MCPs that
|
|
127
|
+
implement them.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
For more details, see the code in this folder and the UI components that consume
|
|
132
|
+
bindings.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/* oxlint-disable no-explicit-any */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { MCPConnection } from "../connection.ts";
|
|
4
|
+
import { createPrivateTool } from "../mastra.ts";
|
|
5
|
+
import {
|
|
6
|
+
createMCPFetchStub,
|
|
7
|
+
type MCPClientFetchStub,
|
|
8
|
+
type ToolBinder,
|
|
9
|
+
} from "../mcp.ts";
|
|
10
|
+
import { CHANNEL_BINDING_SCHEMA } from "./channels.ts";
|
|
11
|
+
import { VIEW_BINDING_SCHEMA } from "./views.ts";
|
|
12
|
+
|
|
13
|
+
// ToolLike is a simplified version of the Tool interface that matches what we need for bindings
|
|
14
|
+
export interface ToolLike<
|
|
15
|
+
TName extends string = string,
|
|
16
|
+
TInput = any,
|
|
17
|
+
TReturn extends object | null | boolean = object,
|
|
18
|
+
> {
|
|
19
|
+
name: TName;
|
|
20
|
+
description: string;
|
|
21
|
+
inputSchema: z.ZodType<TInput>;
|
|
22
|
+
outputSchema?: z.ZodType<TReturn>;
|
|
23
|
+
handler: (props: TInput) => Promise<TReturn> | TReturn;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Binder<
|
|
27
|
+
TDefinition extends readonly ToolBinder[] = readonly ToolBinder[],
|
|
28
|
+
> = TDefinition;
|
|
29
|
+
|
|
30
|
+
export type BinderImplementation<
|
|
31
|
+
TBinder extends Binder<any>,
|
|
32
|
+
TContext = any,
|
|
33
|
+
> = TBinder extends Binder<infer TDefinition>
|
|
34
|
+
? {
|
|
35
|
+
[K in keyof TDefinition]: Omit<
|
|
36
|
+
ToolLike<
|
|
37
|
+
TDefinition[K]["name"],
|
|
38
|
+
z.infer<TDefinition[K]["inputSchema"]>,
|
|
39
|
+
TDefinition[K] extends { outputSchema: infer Schema }
|
|
40
|
+
? Schema extends z.ZodType
|
|
41
|
+
? z.infer<Schema>
|
|
42
|
+
: never
|
|
43
|
+
: never
|
|
44
|
+
>,
|
|
45
|
+
"name" | "inputSchema" | "outputSchema" | "handler"
|
|
46
|
+
> & {
|
|
47
|
+
handler: (
|
|
48
|
+
props: z.infer<TDefinition[K]["inputSchema"]>,
|
|
49
|
+
c?: TContext,
|
|
50
|
+
) => ReturnType<
|
|
51
|
+
ToolLike<
|
|
52
|
+
TDefinition[K]["name"],
|
|
53
|
+
z.infer<TDefinition[K]["inputSchema"]>,
|
|
54
|
+
TDefinition[K] extends { outputSchema: infer Schema }
|
|
55
|
+
? Schema extends z.ZodType
|
|
56
|
+
? z.infer<Schema>
|
|
57
|
+
: never
|
|
58
|
+
: never
|
|
59
|
+
>["handler"]
|
|
60
|
+
>;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
: never;
|
|
64
|
+
|
|
65
|
+
export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
|
|
66
|
+
binder: TDefinition,
|
|
67
|
+
) => {
|
|
68
|
+
return {
|
|
69
|
+
implements: (tools: ToolBinder[]) => {
|
|
70
|
+
return binder.every(
|
|
71
|
+
(tool) =>
|
|
72
|
+
tool.opt === true || (tools ?? []).some((t) => t.name === tool.name),
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
forConnection: (
|
|
76
|
+
mcpConnection: MCPConnection,
|
|
77
|
+
): MCPClientFetchStub<TDefinition> => {
|
|
78
|
+
const stub = createMCPFetchStub<TDefinition>({
|
|
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
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type MCPBindingClient<T extends ReturnType<typeof bindingClient>> =
|
|
104
|
+
ReturnType<T["forConnection"]>;
|
|
105
|
+
|
|
106
|
+
export const ChannelBinding = bindingClient(CHANNEL_BINDING_SCHEMA);
|
|
107
|
+
|
|
108
|
+
export const ViewBinding = bindingClient(VIEW_BINDING_SCHEMA);
|
|
109
|
+
|
|
110
|
+
export type { Callbacks } from "./channels.ts";
|
|
111
|
+
|
|
112
|
+
export const impl = <TBinder extends Binder>(
|
|
113
|
+
schema: TBinder,
|
|
114
|
+
implementation: BinderImplementation<TBinder>,
|
|
115
|
+
createToolFn = createPrivateTool,
|
|
116
|
+
) => {
|
|
117
|
+
const impl: ReturnType<typeof createToolFn>[] = [];
|
|
118
|
+
for (const key in schema) {
|
|
119
|
+
const toolSchema = schema[key];
|
|
120
|
+
const toolImplementation = implementation[key];
|
|
121
|
+
|
|
122
|
+
if (toolSchema.opt && !toolImplementation) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!toolImplementation) {
|
|
127
|
+
throw new Error(`Implementation for ${key} is required`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { name, handler, ...toolLike }: ToolLike = {
|
|
131
|
+
...toolSchema,
|
|
132
|
+
...toolImplementation,
|
|
133
|
+
};
|
|
134
|
+
impl.push(
|
|
135
|
+
createToolFn({
|
|
136
|
+
...toolLike,
|
|
137
|
+
id: name,
|
|
138
|
+
execute: ({ context }) => Promise.resolve(handler(context)),
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return impl;
|
|
143
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolBinder } from "../mcp.ts";
|
|
3
|
+
|
|
4
|
+
const callbacksSchema = z.object({
|
|
5
|
+
stream: z.string(),
|
|
6
|
+
generate: z.string(),
|
|
7
|
+
generateObject: z.string(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const channelIdSchema = z.object({
|
|
11
|
+
workspace: z.string(),
|
|
12
|
+
discriminator: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const channelBindingSchema = channelIdSchema.extend({
|
|
16
|
+
agentId: z.string(),
|
|
17
|
+
agentName: z.string(),
|
|
18
|
+
agentLink: z.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const joinChannelSchema = channelBindingSchema.extend({
|
|
22
|
+
callbacks: callbacksSchema,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const listChannelsSchema = z.object({
|
|
26
|
+
channels: z.array(
|
|
27
|
+
z.object({
|
|
28
|
+
label: z.string(),
|
|
29
|
+
value: z.string(),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type Callbacks = z.infer<typeof callbacksSchema>;
|
|
35
|
+
export type JoinedChannelPayload = z.infer<typeof joinChannelSchema>;
|
|
36
|
+
export type ListChannelsSchema = z.infer<typeof listChannelsSchema>;
|
|
37
|
+
export const CHANNEL_BINDING_SCHEMA = [
|
|
38
|
+
{
|
|
39
|
+
name: "DECO_CHAT_CHANNELS_JOIN" as const,
|
|
40
|
+
inputSchema: joinChannelSchema,
|
|
41
|
+
outputSchema: z.any(),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "DECO_CHAT_CHANNELS_LEAVE" as const,
|
|
45
|
+
inputSchema: channelIdSchema,
|
|
46
|
+
outputSchema: z.any(),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "DECO_CHAT_CHANNELS_LIST" as const,
|
|
50
|
+
inputSchema: z.any(),
|
|
51
|
+
outputSchema: listChannelsSchema,
|
|
52
|
+
opt: true,
|
|
53
|
+
},
|
|
54
|
+
] as const satisfies readonly ToolBinder[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Helper functions for DeconfigResource
|
|
2
|
+
|
|
3
|
+
export const normalizeDirectory = (dir: string) => {
|
|
4
|
+
// Ensure directory starts with / and doesn't end with /
|
|
5
|
+
const normalized = dir.startsWith("/") ? dir : `/${dir}`;
|
|
6
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ResourcePath = {
|
|
10
|
+
build: (directory: string, resourceId: string) => {
|
|
11
|
+
const normalizedDir = normalizeDirectory(directory);
|
|
12
|
+
return `${normalizedDir}/${resourceId}.json`;
|
|
13
|
+
},
|
|
14
|
+
extract: (path: string) => {
|
|
15
|
+
const match = path.match(/^(.+)\/(.+)\.json$/);
|
|
16
|
+
if (!match) {
|
|
17
|
+
throw new Error("Invalid resource path");
|
|
18
|
+
}
|
|
19
|
+
return { directory: match[1], resourceId: match[2] };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const ResourceUri = {
|
|
24
|
+
build: (integrationId: string, resourceName: string, resourceId: string) => {
|
|
25
|
+
return `rsc://${integrationId}/${resourceName}/${resourceId}`;
|
|
26
|
+
},
|
|
27
|
+
unwind: (uri: string) => {
|
|
28
|
+
const match = uri.match(/^rsc:\/\/[^/]+\/([^/]+)\/(.+)$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error("Invalid Resources 2.0 URI format");
|
|
31
|
+
}
|
|
32
|
+
return { resourceName: match[1], resourceId: match[2] };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getMetadataValue(metadata: unknown, key: string): unknown {
|
|
37
|
+
if (!metadata || typeof metadata !== "object") return undefined;
|
|
38
|
+
const metaObj = metadata as Record<string, unknown>;
|
|
39
|
+
if (key in metaObj) return metaObj[key];
|
|
40
|
+
const nested = metaObj.metadata;
|
|
41
|
+
if (nested && typeof nested === "object" && key in nested) {
|
|
42
|
+
return (nested as Record<string, unknown>)[key];
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getMetadataString(
|
|
48
|
+
metadata: unknown,
|
|
49
|
+
key: string,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
const value = getMetadataValue(metadata, key);
|
|
52
|
+
return typeof value === "string" ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const toAsyncIterator = <T>(
|
|
56
|
+
emitter: EventSource,
|
|
57
|
+
eventType: string = "message",
|
|
58
|
+
): AsyncIterable<T> => {
|
|
59
|
+
const queue: T[] = [];
|
|
60
|
+
let done = false;
|
|
61
|
+
let waitPromise: ((data?: T) => void) | null = null;
|
|
62
|
+
|
|
63
|
+
const triggerLoop = () => {
|
|
64
|
+
if (waitPromise) {
|
|
65
|
+
waitPromise();
|
|
66
|
+
waitPromise = null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const messageHandler = (data: MessageEvent) => {
|
|
71
|
+
try {
|
|
72
|
+
queue.push(JSON.parse(data.data));
|
|
73
|
+
} catch {
|
|
74
|
+
// Silently ignore malformed data or optionally log error
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
triggerLoop();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const errorHandler = () => {
|
|
81
|
+
done = true;
|
|
82
|
+
triggerLoop();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
emitter.addEventListener(eventType, messageHandler);
|
|
86
|
+
emitter.addEventListener("error", errorHandler);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
async *[Symbol.asyncIterator]() {
|
|
90
|
+
try {
|
|
91
|
+
while (true) {
|
|
92
|
+
const value = queue.shift();
|
|
93
|
+
if (value) {
|
|
94
|
+
yield value;
|
|
95
|
+
} else {
|
|
96
|
+
if (done) return;
|
|
97
|
+
await new Promise((resolve) => (waitPromise = resolve));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
emitter.removeEventListener(eventType, messageHandler);
|
|
102
|
+
emitter.removeEventListener("error", errorHandler);
|
|
103
|
+
emitter.close();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./resources.ts";
|