@decocms/bindings 0.1.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.
- package/README.md +680 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/core/binder.ts +221 -0
- package/src/index.ts +16 -0
- package/src/well-known/collections.ts +403 -0
- package/src/well-known/models.ts +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
# @decocms/bindings
|
|
2
|
+
|
|
3
|
+
Core type definitions and utilities for the bindings system. Bindings define standardized interfaces that integrations (MCPs - Model Context Protocols) can implement, similar to TypeScript interfaces but with runtime validation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the package using npm or bun:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @decocms/bindings
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or with bun:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @decocms/bindings
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Note:** This package requires `zod` as a peer dependency. If you don't already have `zod` installed in your project, you'll need to install it separately.
|
|
20
|
+
|
|
21
|
+
## What are Bindings?
|
|
22
|
+
|
|
23
|
+
Bindings are a core concept for defining and enforcing standardized interfaces that MCPs can implement. They provide a type-safe, declarative way to specify what tools (methods) and schemas an integration must expose to be compatible with certain parts of the system.
|
|
24
|
+
|
|
25
|
+
### Key Features
|
|
26
|
+
|
|
27
|
+
- **Standardization**: Define contracts (schemas and method names) that MCPs must implement
|
|
28
|
+
- **Type Safety**: Leverage Zod schemas and TypeScript types for correct data structures
|
|
29
|
+
- **Runtime Validation**: Check if an integration implements a binding at runtime
|
|
30
|
+
- **Extensibility**: Create new bindings for any use case
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### 1. Defining a Binding
|
|
35
|
+
|
|
36
|
+
A binding is an array of tool definitions, each specifying a name and input/output schemas:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
import type { Binder } from "@decocms/bindings";
|
|
41
|
+
|
|
42
|
+
// Define input/output schemas
|
|
43
|
+
const joinChannelInput = z.object({
|
|
44
|
+
workspace: z.string(),
|
|
45
|
+
discriminator: z.string(),
|
|
46
|
+
agentId: z.string(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const channelOutput = z.object({
|
|
50
|
+
success: z.boolean(),
|
|
51
|
+
channelId: z.string(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Define the binding
|
|
55
|
+
export const CHANNEL_BINDING = [
|
|
56
|
+
{
|
|
57
|
+
name: "DECO_CHAT_CHANNELS_JOIN" as const,
|
|
58
|
+
inputSchema: joinChannelInput,
|
|
59
|
+
outputSchema: channelOutput,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "DECO_CHAT_CHANNELS_LEAVE" as const,
|
|
63
|
+
inputSchema: z.object({ channelId: z.string() }),
|
|
64
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "DECO_CHAT_CHANNELS_LIST" as const,
|
|
68
|
+
inputSchema: z.object({}),
|
|
69
|
+
outputSchema: z.object({
|
|
70
|
+
channels: z.array(z.object({
|
|
71
|
+
label: z.string(),
|
|
72
|
+
value: z.string(),
|
|
73
|
+
})),
|
|
74
|
+
}),
|
|
75
|
+
opt: true, // This tool is optional
|
|
76
|
+
},
|
|
77
|
+
] as const satisfies Binder;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Checking if Tools Implement a Binding
|
|
81
|
+
|
|
82
|
+
Use `createBindingChecker` to verify if a set of tools implements a binding:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createBindingChecker } from "@decocms/bindings";
|
|
86
|
+
|
|
87
|
+
// Create a checker for your binding
|
|
88
|
+
const channelChecker = createBindingChecker(CHANNEL_BINDING);
|
|
89
|
+
|
|
90
|
+
// Check if available tools implement the binding
|
|
91
|
+
const availableTools = [
|
|
92
|
+
{ name: "DECO_CHAT_CHANNELS_JOIN" },
|
|
93
|
+
{ name: "DECO_CHAT_CHANNELS_LEAVE" },
|
|
94
|
+
{ name: "DECO_CHAT_CHANNELS_LIST" },
|
|
95
|
+
{ name: "OTHER_TOOL" },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const isImplemented = channelChecker.isImplementedBy(availableTools);
|
|
99
|
+
console.log(isImplemented); // true - all required tools are present
|
|
100
|
+
|
|
101
|
+
// Optional tools don't need to be present
|
|
102
|
+
const minimalTools = [
|
|
103
|
+
{ name: "DECO_CHAT_CHANNELS_JOIN" },
|
|
104
|
+
{ name: "DECO_CHAT_CHANNELS_LEAVE" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const stillValid = channelChecker.isImplementedBy(minimalTools);
|
|
108
|
+
console.log(stillValid); // true - CHANNELS_LIST is optional
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Using RegExp for Tool Names
|
|
112
|
+
|
|
113
|
+
You can use RegExp patterns for flexible tool matching:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
export const RESOURCE_BINDING = [
|
|
117
|
+
{
|
|
118
|
+
name: /^DECO_RESOURCE_\w+_SEARCH$/ as RegExp,
|
|
119
|
+
inputSchema: z.object({ term: z.string() }),
|
|
120
|
+
outputSchema: z.object({ items: z.array(z.any()) }),
|
|
121
|
+
},
|
|
122
|
+
] as const satisfies Binder;
|
|
123
|
+
|
|
124
|
+
// This will match: DECO_RESOURCE_WORKFLOW_SEARCH, DECO_RESOURCE_USER_SEARCH, etc.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. Type Safety with Bindings
|
|
128
|
+
|
|
129
|
+
TypeScript can infer types from your binding definitions:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import type { ToolBinder } from "@decocms/bindings";
|
|
133
|
+
|
|
134
|
+
// Extract the type of a specific tool
|
|
135
|
+
type JoinChannelTool = typeof CHANNEL_BINDING[0];
|
|
136
|
+
|
|
137
|
+
// Get input type
|
|
138
|
+
type JoinChannelInput = z.infer<JoinChannelTool["inputSchema"]>;
|
|
139
|
+
|
|
140
|
+
// Get output type
|
|
141
|
+
type JoinChannelOutput = z.infer<NonNullable<JoinChannelTool["outputSchema"]>>;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## API Reference
|
|
145
|
+
|
|
146
|
+
### Types
|
|
147
|
+
|
|
148
|
+
#### `ToolBinder<TName, TInput, TReturn>`
|
|
149
|
+
|
|
150
|
+
Defines a single tool within a binding.
|
|
151
|
+
|
|
152
|
+
- `name`: Tool name (string or RegExp)
|
|
153
|
+
- `inputSchema`: Zod schema for input validation
|
|
154
|
+
- `outputSchema?`: Optional Zod schema for output validation
|
|
155
|
+
- `opt?`: If true, tool is optional in the binding
|
|
156
|
+
|
|
157
|
+
#### `Binder<TDefinition>`
|
|
158
|
+
|
|
159
|
+
Represents a collection of tool definitions that form a binding.
|
|
160
|
+
|
|
161
|
+
#### `BindingChecker`
|
|
162
|
+
|
|
163
|
+
Interface with an `isImplementedBy` method for checking binding implementations.
|
|
164
|
+
|
|
165
|
+
### Functions
|
|
166
|
+
|
|
167
|
+
#### `createBindingChecker<TDefinition>(binderTools: TDefinition): BindingChecker`
|
|
168
|
+
|
|
169
|
+
Creates a binding checker that can verify if a set of tools implements the binding.
|
|
170
|
+
|
|
171
|
+
**Parameters:**
|
|
172
|
+
- `binderTools`: The binding definition to check against
|
|
173
|
+
|
|
174
|
+
**Returns:**
|
|
175
|
+
- A `BindingChecker` with an `isImplementedBy` method
|
|
176
|
+
|
|
177
|
+
**Example:**
|
|
178
|
+
```typescript
|
|
179
|
+
const checker = createBindingChecker(MY_BINDING);
|
|
180
|
+
const isValid = checker.isImplementedBy(availableTools);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Common Patterns
|
|
184
|
+
|
|
185
|
+
### Well-Known Bindings
|
|
186
|
+
|
|
187
|
+
The package includes pre-defined bindings for common use cases. Well-known bindings are organized in the `well-known` folder and must be imported directly:
|
|
188
|
+
|
|
189
|
+
- **Collections**: `@decocms/bindings/well-known/collections` - Collection bindings for SQL table-like structures
|
|
190
|
+
- **Models**: `@decocms/bindings/well-known/models` - AI model providers interface
|
|
191
|
+
|
|
192
|
+
See the [Collection Bindings](#collection-bindings) and [Models Bindings](#models-bindings) sections below for detailed usage examples.
|
|
193
|
+
|
|
194
|
+
### Generic Bindings
|
|
195
|
+
|
|
196
|
+
Create factory functions for generic bindings:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
function createResourceBinding(resourceName: string) {
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
name: `DECO_RESOURCE_${resourceName.toUpperCase()}_SEARCH` as const,
|
|
203
|
+
inputSchema: z.object({ term: z.string() }),
|
|
204
|
+
outputSchema: z.object({ items: z.array(z.any()) }),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: `DECO_RESOURCE_${resourceName.toUpperCase()}_READ` as const,
|
|
208
|
+
inputSchema: z.object({ uri: z.string() }),
|
|
209
|
+
outputSchema: z.object({ data: z.any() }),
|
|
210
|
+
},
|
|
211
|
+
] as const satisfies Binder;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const workflowBinding = createResourceBinding("workflow");
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Collection Bindings
|
|
218
|
+
|
|
219
|
+
Collection bindings provide standardized CRUD + Search operations for SQL table-like collections, compatible with TanStack DB query-collection. They are designed for database tables where each entity has a unique URI and audit trail fields.
|
|
220
|
+
|
|
221
|
+
### Key Features
|
|
222
|
+
|
|
223
|
+
- **SQL Table-like Structure**: Represents database tables with standardized operations
|
|
224
|
+
- **URI-based Identification**: Uses collection URIs in format `rsc://{connectionid}/{collection_name}/{item_id}`
|
|
225
|
+
- **Audit Trail**: All entities must include `created_at`, `updated_at`, `created_by`, and `updated_by` fields
|
|
226
|
+
- **TanStack DB Compatible**: Works seamlessly with TanStack DB's query-collection LoadSubsetOptions
|
|
227
|
+
- **Type-Safe**: Full TypeScript support with Zod validation
|
|
228
|
+
|
|
229
|
+
### Base Entity Schema Requirements
|
|
230
|
+
|
|
231
|
+
All collection entity schemas must extend `BaseCollectionEntitySchema`, which requires:
|
|
232
|
+
|
|
233
|
+
- `uri`: Collection URI in format `rsc://{connectionid}/{collection_name}/{item_id}`
|
|
234
|
+
- `created_at`: Creation timestamp (datetime string)
|
|
235
|
+
- `updated_at`: Last update timestamp (datetime string)
|
|
236
|
+
- `created_by`: User who created the entity (optional string)
|
|
237
|
+
- `updated_by`: User who last updated the entity (optional string)
|
|
238
|
+
|
|
239
|
+
### Using Collection Bindings
|
|
240
|
+
|
|
241
|
+
Collection bindings are a well-known binding pattern for SQL table-like structures. Import them from the well-known collections module:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { z } from "zod";
|
|
245
|
+
import { createCollectionBindings } from "@decocms/bindings/well-known/collections";
|
|
246
|
+
import { createBindingChecker } from "@decocms/bindings";
|
|
247
|
+
|
|
248
|
+
// Define your entity schema extending the base schema
|
|
249
|
+
const TodoSchema = z.object({
|
|
250
|
+
uri: z.string(), // Collection URI: rsc://{connectionid}/todos/{item_id}
|
|
251
|
+
created_at: z.string().datetime(),
|
|
252
|
+
updated_at: z.string().datetime(),
|
|
253
|
+
created_by: z.string().optional(),
|
|
254
|
+
updated_by: z.string().optional(),
|
|
255
|
+
// Your custom fields
|
|
256
|
+
title: z.string(),
|
|
257
|
+
completed: z.boolean(),
|
|
258
|
+
userId: z.number(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Create the collection binding (full CRUD)
|
|
262
|
+
const TODO_COLLECTION_BINDING = createCollectionBindings("todos", TodoSchema);
|
|
263
|
+
|
|
264
|
+
// Create a read-only collection binding (only LIST and GET)
|
|
265
|
+
const READONLY_COLLECTION_BINDING = createCollectionBindings("products", ProductSchema, {
|
|
266
|
+
readOnly: true,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Create a checker to verify if tools implement the binding
|
|
270
|
+
const todoChecker = createBindingChecker(TODO_COLLECTION_BINDING);
|
|
271
|
+
|
|
272
|
+
// Check if available tools implement the binding
|
|
273
|
+
const availableTools = [
|
|
274
|
+
{ name: "DECO_COLLECTION_TODOS_LIST" },
|
|
275
|
+
{ name: "DECO_COLLECTION_TODOS_GET" },
|
|
276
|
+
{ name: "DECO_COLLECTION_TODOS_INSERT" },
|
|
277
|
+
{ name: "DECO_COLLECTION_TODOS_UPDATE" },
|
|
278
|
+
{ name: "DECO_COLLECTION_TODOS_DELETE" },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const isImplemented = await todoChecker.isImplementedBy(availableTools);
|
|
282
|
+
console.log(isImplemented); // true if all required tools are present
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Collection Operations
|
|
286
|
+
|
|
287
|
+
The `createCollectionBindings` function generates tool bindings based on the `readOnly` option:
|
|
288
|
+
|
|
289
|
+
**Required operations (always included):**
|
|
290
|
+
|
|
291
|
+
1. **LIST** - `DECO_COLLECTION_{NAME}_LIST`
|
|
292
|
+
- Query/search entities with filtering, sorting, and pagination
|
|
293
|
+
- Input: `where?`, `orderBy?`, `limit?`, `offset?`
|
|
294
|
+
- Output: `items[]`, `totalCount?`, `hasMore?`
|
|
295
|
+
|
|
296
|
+
2. **GET** - `DECO_COLLECTION_{NAME}_GET`
|
|
297
|
+
- Get a single entity by URI
|
|
298
|
+
- Input: `uri`
|
|
299
|
+
- Output: `item | null`
|
|
300
|
+
|
|
301
|
+
**Optional operations (excluded if `readOnly: true`):**
|
|
302
|
+
|
|
303
|
+
3. **INSERT** - `DECO_COLLECTION_{NAME}_INSERT`
|
|
304
|
+
- Create a new entity
|
|
305
|
+
- Input: `data` (without uri, which is auto-generated)
|
|
306
|
+
- Output: `item` (with generated URI)
|
|
307
|
+
|
|
308
|
+
4. **UPDATE** - `DECO_COLLECTION_{NAME}_UPDATE`
|
|
309
|
+
- Update an existing entity
|
|
310
|
+
- Input: `uri`, `data` (partial)
|
|
311
|
+
- Output: `item`
|
|
312
|
+
|
|
313
|
+
5. **DELETE** - `DECO_COLLECTION_{NAME}_DELETE`
|
|
314
|
+
- Delete an entity
|
|
315
|
+
- Input: `uri`
|
|
316
|
+
- Output: `success`, `uri`
|
|
317
|
+
|
|
318
|
+
### Read-Only Collections
|
|
319
|
+
|
|
320
|
+
To create a read-only collection (only LIST and GET operations), pass `{ readOnly: true }` as the third parameter:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Read-only collection - only LIST and GET operations
|
|
324
|
+
const READONLY_COLLECTION_BINDING = createCollectionBindings(
|
|
325
|
+
"products",
|
|
326
|
+
ProductSchema,
|
|
327
|
+
{ readOnly: true }
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This is useful for collections that are managed externally or should not be modified through the MCP interface.
|
|
332
|
+
|
|
333
|
+
## Models Bindings
|
|
334
|
+
|
|
335
|
+
Models bindings provide a well-known interface for AI model providers. They use collection bindings under the hood for LIST and GET operations, and add a streaming endpoint tool.
|
|
336
|
+
|
|
337
|
+
### Using Models Bindings
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { MODELS_BINDING, MODELS_COLLECTION_BINDING } from "@decocms/bindings/well-known/models";
|
|
341
|
+
import { createBindingChecker } from "@decocms/bindings";
|
|
342
|
+
|
|
343
|
+
// Use the pre-defined MODELS_BINDING
|
|
344
|
+
const modelsChecker = createBindingChecker(MODELS_BINDING);
|
|
345
|
+
|
|
346
|
+
// Check if available tools implement the binding
|
|
347
|
+
const availableTools = [
|
|
348
|
+
{ name: "DECO_COLLECTION_MODELS_LIST" },
|
|
349
|
+
{ name: "DECO_COLLECTION_MODELS_GET" },
|
|
350
|
+
{ name: "GET_STREAM_ENDPOINT" },
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const isImplemented = await modelsChecker.isImplementedBy(availableTools);
|
|
354
|
+
console.log(isImplemented); // true if all required tools are present
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Models Binding Tools
|
|
358
|
+
|
|
359
|
+
The `MODELS_BINDING` includes:
|
|
360
|
+
|
|
361
|
+
1. **DECO_COLLECTION_MODELS_LIST** (required)
|
|
362
|
+
- List available AI models with their capabilities
|
|
363
|
+
- Uses collection binding LIST operation
|
|
364
|
+
- Input: `where?`, `orderBy?`, `limit?`, `offset?`
|
|
365
|
+
- Output: `items[]` (array of model entities)
|
|
366
|
+
|
|
367
|
+
2. **DECO_COLLECTION_MODELS_GET** (required)
|
|
368
|
+
- Get a single model by URI
|
|
369
|
+
- Uses collection binding GET operation
|
|
370
|
+
- Input: `uri`
|
|
371
|
+
- Output: `item | null`
|
|
372
|
+
|
|
373
|
+
3. **GET_STREAM_ENDPOINT** (required)
|
|
374
|
+
- Get the streaming endpoint URL for chat completions
|
|
375
|
+
- Input: `{}` (empty object, passthrough)
|
|
376
|
+
- Output: `{ url?: string }` (optional URL)
|
|
377
|
+
|
|
378
|
+
### Model Entity Schema
|
|
379
|
+
|
|
380
|
+
Models follow the collection entity schema with additional model-specific fields:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
{
|
|
384
|
+
uri: string; // Collection URI
|
|
385
|
+
created_at: string; // Creation timestamp
|
|
386
|
+
updated_at: string; // Last update timestamp
|
|
387
|
+
created_by?: string; // User who created
|
|
388
|
+
updated_by?: string; // User who last updated
|
|
389
|
+
id: string; // Model ID
|
|
390
|
+
model: string; // Model identifier
|
|
391
|
+
name: string; // Display name
|
|
392
|
+
logo: string | null; // Logo URL
|
|
393
|
+
capabilities: string[]; // Array of capabilities
|
|
394
|
+
contextWindow: number | null; // Context window size
|
|
395
|
+
inputCost: number | null; // Input cost per token
|
|
396
|
+
outputCost: number | null; // Output cost per token
|
|
397
|
+
outputLimit: number | null; // Output limit
|
|
398
|
+
description: string | null; // Model description
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### MCP Implementation Example
|
|
403
|
+
|
|
404
|
+
Here's how you would implement the models binding in an MCP server:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { MODELS_BINDING } from "@decocms/bindings/well-known/models";
|
|
408
|
+
import { parseCollectionUri } from "@decocms/bindings/well-known/collections";
|
|
409
|
+
import { impl } from "@decocms/sdk/mcp/bindings/binder";
|
|
410
|
+
|
|
411
|
+
const modelTools = impl(MODELS_BINDING, [
|
|
412
|
+
{
|
|
413
|
+
description: "List available AI models",
|
|
414
|
+
handler: async ({ where, orderBy, limit, offset }) => {
|
|
415
|
+
// Query your models database
|
|
416
|
+
const items = await db.models.findMany({
|
|
417
|
+
where: convertWhereToSQL(where),
|
|
418
|
+
orderBy: convertOrderByToSQL(orderBy),
|
|
419
|
+
take: limit,
|
|
420
|
+
skip: offset,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return { items, hasMore: items.length === limit };
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
description: "Get a model by URI",
|
|
428
|
+
handler: async ({ uri }) => {
|
|
429
|
+
const parsed = parseCollectionUri(uri);
|
|
430
|
+
const item = await db.models.findUnique({
|
|
431
|
+
where: { id: parsed.itemId },
|
|
432
|
+
});
|
|
433
|
+
return { item };
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
description: "Get streaming endpoint",
|
|
438
|
+
handler: async () => {
|
|
439
|
+
// Return your streaming endpoint URL
|
|
440
|
+
return { url: "https://api.example.com/v1/chat/completions" };
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Where Expression Structure
|
|
447
|
+
|
|
448
|
+
The `where` parameter supports TanStack DB predicate push-down patterns:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
// Simple comparison
|
|
452
|
+
{
|
|
453
|
+
field: ["category"],
|
|
454
|
+
operator: "eq",
|
|
455
|
+
value: "electronics"
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Logical operators
|
|
459
|
+
{
|
|
460
|
+
operator: "and",
|
|
461
|
+
conditions: [
|
|
462
|
+
{ field: ["category"], operator: "eq", value: "electronics" },
|
|
463
|
+
{ field: ["price"], operator: "lt", value: 100 }
|
|
464
|
+
]
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Supported Operators:**
|
|
469
|
+
- Comparison: `eq`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `contains`
|
|
470
|
+
- Logical: `and`, `or`, `not`
|
|
471
|
+
|
|
472
|
+
### Order By Expression Structure
|
|
473
|
+
|
|
474
|
+
The `orderBy` parameter supports multi-field sorting:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
[
|
|
478
|
+
{
|
|
479
|
+
field: ["price"],
|
|
480
|
+
direction: "asc",
|
|
481
|
+
nulls: "last" // optional: "first" | "last"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
field: ["created_at"],
|
|
485
|
+
direction: "desc"
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Collection URI Helpers
|
|
491
|
+
|
|
492
|
+
The package provides utility functions for working with collection URIs:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import {
|
|
496
|
+
validateCollectionUri,
|
|
497
|
+
parseCollectionUri,
|
|
498
|
+
constructCollectionUri,
|
|
499
|
+
} from "@decocms/bindings/well-known/collections";
|
|
500
|
+
|
|
501
|
+
// Validate a URI
|
|
502
|
+
const isValid = validateCollectionUri("rsc://conn-123/users/user-456");
|
|
503
|
+
// true
|
|
504
|
+
|
|
505
|
+
// Parse a URI into components
|
|
506
|
+
const parsed = parseCollectionUri("rsc://conn-123/users/user-456");
|
|
507
|
+
// { connectionId: "conn-123", collectionName: "users", itemId: "user-456" }
|
|
508
|
+
|
|
509
|
+
// Construct a URI from components
|
|
510
|
+
const uri = constructCollectionUri("conn-123", "users", "user-456");
|
|
511
|
+
// "rsc://conn-123/users/user-456"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### TanStack DB Integration
|
|
515
|
+
|
|
516
|
+
Collection bindings are designed to work with TanStack DB's query-collection. The `where` and `orderBy` expressions are compatible with TanStack DB's `LoadSubsetOptions`, allowing for efficient predicate push-down to your backend.
|
|
517
|
+
|
|
518
|
+
### MCP Implementation Example
|
|
519
|
+
|
|
520
|
+
Here's how you would implement a collection binding in an MCP server:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { z } from "zod";
|
|
524
|
+
import { createCollectionBindings } from "@decocms/bindings/well-known/collections";
|
|
525
|
+
import { parseCollectionUri, constructCollectionUri } from "@decocms/bindings/well-known/collections";
|
|
526
|
+
import { impl } from "@decocms/sdk/mcp/bindings/binder";
|
|
527
|
+
|
|
528
|
+
const TodoSchema = z.object({
|
|
529
|
+
uri: z.string(),
|
|
530
|
+
created_at: z.string().datetime(),
|
|
531
|
+
updated_at: z.string().datetime(),
|
|
532
|
+
created_by: z.string().optional(),
|
|
533
|
+
updated_by: z.string().optional(),
|
|
534
|
+
title: z.string(),
|
|
535
|
+
completed: z.boolean(),
|
|
536
|
+
userId: z.number(),
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const TODO_COLLECTION_BINDING = createCollectionBindings("todos", TodoSchema);
|
|
540
|
+
|
|
541
|
+
// Implement the tools
|
|
542
|
+
const todoTools = impl(TODO_COLLECTION_BINDING, [
|
|
543
|
+
{
|
|
544
|
+
description: "List todos with filtering and sorting",
|
|
545
|
+
handler: async ({ where, orderBy, limit, offset }) => {
|
|
546
|
+
// Convert where/orderBy to SQL and query database
|
|
547
|
+
const items = await db.todos.findMany({
|
|
548
|
+
where: convertWhereToSQL(where),
|
|
549
|
+
orderBy: convertOrderByToSQL(orderBy),
|
|
550
|
+
take: limit,
|
|
551
|
+
skip: offset,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
return { items, hasMore: items.length === limit };
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
description: "Get a todo by URI",
|
|
559
|
+
handler: async ({ uri }) => {
|
|
560
|
+
const parsed = parseCollectionUri(uri);
|
|
561
|
+
const item = await db.todos.findUnique({
|
|
562
|
+
where: { id: parsed.itemId },
|
|
563
|
+
});
|
|
564
|
+
return { item };
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
description: "Create a new todo",
|
|
569
|
+
handler: async ({ data }) => {
|
|
570
|
+
const item = await db.todos.create({ data });
|
|
571
|
+
const uri = constructCollectionUri(
|
|
572
|
+
connectionId,
|
|
573
|
+
"todos",
|
|
574
|
+
item.id
|
|
575
|
+
);
|
|
576
|
+
return { item: { ...item, uri } };
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
description: "Update a todo",
|
|
581
|
+
handler: async ({ uri, data }) => {
|
|
582
|
+
const parsed = parseCollectionUri(uri);
|
|
583
|
+
const item = await db.todos.update({
|
|
584
|
+
where: { id: parsed.itemId },
|
|
585
|
+
data,
|
|
586
|
+
});
|
|
587
|
+
return { item };
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
description: "Delete a todo",
|
|
592
|
+
handler: async ({ uri }) => {
|
|
593
|
+
const parsed = parseCollectionUri(uri);
|
|
594
|
+
await db.todos.delete({ where: { id: parsed.itemId } });
|
|
595
|
+
return { success: true, uri };
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
]);
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Type Safety
|
|
602
|
+
|
|
603
|
+
TypeScript can infer types from your collection binding definitions:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import type {
|
|
607
|
+
CollectionBinding,
|
|
608
|
+
CollectionTools,
|
|
609
|
+
CollectionListInput,
|
|
610
|
+
CollectionGetInput,
|
|
611
|
+
CollectionDeleteInput,
|
|
612
|
+
} from "@decocms/bindings/well-known/collections";
|
|
613
|
+
|
|
614
|
+
// Extract the binding type
|
|
615
|
+
type TodoBinding = typeof TODO_COLLECTION_BINDING;
|
|
616
|
+
|
|
617
|
+
// Extract tool names
|
|
618
|
+
type TodoTools = CollectionTools<typeof TodoSchema>;
|
|
619
|
+
// "DECO_COLLECTION_TODOS_LIST" | "DECO_COLLECTION_TODOS_GET" | ...
|
|
620
|
+
|
|
621
|
+
// Get input types
|
|
622
|
+
type ListInput = CollectionListInput;
|
|
623
|
+
type GetInput = CollectionGetInput;
|
|
624
|
+
type DeleteInput = CollectionDeleteInput;
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Development
|
|
628
|
+
|
|
629
|
+
### Setup
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
# Install dependencies
|
|
633
|
+
bun install
|
|
634
|
+
|
|
635
|
+
# Build the package
|
|
636
|
+
bun run build
|
|
637
|
+
|
|
638
|
+
# Run tests
|
|
639
|
+
bun run test
|
|
640
|
+
|
|
641
|
+
# Watch mode for tests
|
|
642
|
+
bun run test:watch
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Building
|
|
646
|
+
|
|
647
|
+
The package uses [tsup](https://tsup.egoist.dev/) for building:
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
bun run build
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
This will generate ESM output in the `dist/` directory with TypeScript declarations.
|
|
654
|
+
|
|
655
|
+
### Testing
|
|
656
|
+
|
|
657
|
+
Tests are written with [Vitest](https://vitest.dev/):
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
# Run tests once
|
|
661
|
+
bun run test
|
|
662
|
+
|
|
663
|
+
# Watch mode
|
|
664
|
+
bun run test:watch
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Publishing
|
|
668
|
+
|
|
669
|
+
The package is automatically published to npm when a tag matching `bindings-v*` is pushed:
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
# Update version in package.json, then:
|
|
673
|
+
git tag bindings-v0.1.0
|
|
674
|
+
git push origin bindings-v0.1.0
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## License
|
|
678
|
+
|
|
679
|
+
See the root LICENSE.md file in the repository.
|
|
680
|
+
|