@ductape/mcp 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/src/index.ts ADDED
@@ -0,0 +1,813 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ductape MCP Server
4
+ *
5
+ * Exposes Ductape SDK operations as MCP tools by calling the backend proxy.
6
+ *
7
+ * Authentication:
8
+ * Requires passing a `publishable_key` with every `ductape_execute` payload.
9
+ */
10
+
11
+ import { z } from 'zod';
12
+ import {
13
+ executeViaProxy,
14
+ generateExecutablePayload,
15
+ type SDKModule,
16
+ } from './proxy-client.js';
17
+
18
+ const MODULES: SDKModule[] = [
19
+ 'product', 'app', 'databases', 'graph', 'webhooks', 'notifications',
20
+ 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
21
+ 'actions', 'features', 'jobs', 'logs', 'resilience', 'health', 'fallback', 'secrets',
22
+ ];
23
+
24
+
25
+
26
+ // ─── Exhaustive SDK Method & Params Reference ────────────────────────────────
27
+ // Built from a complete read of sdk/ts/src/index.ts (Ductape class public API).
28
+ // Each entry follows: [module].[method] → params array signature.
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ const METHOD_DOCS = `
32
+ ALL params are passed as a JSON array in positional order matching the SDK signature.
33
+
34
+ ━━━ MODULE: product ━━━
35
+ product.create [data: { name, description, tag?, envs?: [{slug, name}] }]
36
+ product.fetch [product_tag: string]
37
+ product.update [product_tag: string, data: { name?: string, description?: string }]
38
+ product.init [product_tag: string]
39
+ product.environments.create [product_tag, data: { slug: string, env_name: string, description: string, active?: boolean }]
40
+ product.environments.update [product_tag, slug: string, data: { env_name?: string, description?: string, active?: boolean }]
41
+ product.environments.list [product_tag]
42
+ product.environments.fetch [product_tag, slug]
43
+ product.apps.connect [product_tag, app_tag]
44
+ product.apps.add [product_tag, app: { access_tag: string, envs: [{ app_env_slug: string, product_env_slug: string, variables?: [{key: string, value: string}], auth?: { auth_tag: string, data: string|object, expiry?: number } }] }]
45
+ product.apps.list [product_tag]
46
+ product.apps.fetch [product_tag, access_tag]
47
+ product.apps.update [product_tag, access_tag: string, data: { version?: string, envs?: [{ app_env_slug: string, product_env_slug: string, variables?: [{key: string, value: string}], auth?: { auth_tag: string, data: string|object, expiry?: number } }] }]
48
+
49
+ ━━━ MODULE: app ━━━
50
+ app.create [data: { app_name: string, description: string, unique?: boolean }]
51
+ app.fetch [app_tag]
52
+ app.update [app_tag, data: { app_name?: string, description?: string, logo?: string, colors?: { primary?: string, secondary?: string, accent?: string, background?: string }, require_whitelist?: boolean, aboutText?: string, aboutHTML?: string, request_type?: string }]
53
+ app.init [app_tag]
54
+
55
+ ━━━ MODULE: actions (app actions) ━━━
56
+ actions.create [app_tag, data: { tag: string, name: string, resource: string, method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", description?: string, body?: object, params?: object, query?: object, headers?: object, response?: { name?: string, status_code: number, success: boolean, body: object, response_format: "json"|"xml"|"form" } }]
57
+ actions.update [app_tag, action_tag, data: { resource?: string, method?: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", description?: string, request_type?: "json"|"xml"|"form", body?: object, query?: object, params?: object, headers?: object, response?: { name?: string, success: boolean, body: object, response_format: "json"|"xml"|"form", status_code: number } }]
58
+ actions.fetch [app_tag, action_tag]
59
+ actions.list [app_tag]
60
+ actions.run [{ product, env, app, action, input: { "body:fieldName": value, "headers:Authorization": "Bearer token", "params:id": "123", "query:limit": 10 } }]
61
+ actions.dispatch [{ product, env, app, action, input, retries?, session?, cache?, schedule?: { start_at?, cron?, every?, limit?, tz? } }]
62
+
63
+ ━━━ MODULE: auths ━━━
64
+ auths.create [app_tag, data: { tag: string, name: string, setup_type: "header"|"bearer"|"basic"|"oauth2"|"apikey", expiry: number, period: "seconds"|"minutes"|"hours"|"days", description: string, action_tag?: string }]
65
+ auths.update [app_tag, auth_tag, data: { name?: string, setup_type?: "header"|"bearer"|"basic"|"oauth2"|"apikey", expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", description?: string, action_tag?: string }]
66
+ auths.fetch [app_tag, auth_tag]
67
+ auths.list [app_tag]
68
+
69
+ ━━━ MODULE: webhooks ━━━
70
+ webhooks.create [app_tag, data: { tag: string, name: string, description: string, envs?: [{ slug: string, registration_url?: string, method?: "GET"|"POST"|"PUT"|"PATCH", sample?: object }] }]
71
+ webhooks.update [app_tag, webhook_tag, data: { name?: string, description?: string, envs?: [{ slug: string, registration_url?: string, method?: "GET"|"POST"|"PUT"|"PATCH", sample?: object }] }]
72
+ webhooks.fetch [app_tag, webhook_tag]
73
+ webhooks.list [app_tag]
74
+ webhooks.events.create [app_tag, data: { tag: string, name: string, selector: string, description: string, sample: object }]
75
+ webhooks.events.update [app_tag, event_tag, data: { name?: string, selector?: string, selectorValue?: string, description?: string, sample?: object }]
76
+ webhooks.events.fetch [app_tag, event_tag]
77
+ webhooks.events.list [app_tag, webhook_tag]
78
+
79
+ ━━━ MODULE: sessions ━━━
80
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
81
+ sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
82
+ sessions.fetch [product_tag, session_tag]
83
+ sessions.list [product_tag]
84
+ sessions.delete [product_tag, session_tag]
85
+ sessions.start [{ product, env, tag, data: { key: value } }]
86
+ sessions.verify [{ product, env, tag, token }]
87
+ sessions.refresh [{ product, env, tag, refreshToken }]
88
+ sessions.revoke [{ product, env, tag, sessionId?, identifier? }]
89
+ sessions.listActive [{ product, env, tag, identifier?, page?, limit? }]
90
+ sessions.fetchUsers [{ product, session, env?, page?, limit? }]
91
+ sessions.fetchUserDetails [{ product, session, identifier, env? }]
92
+ sessions.fetchDashboard [{ product, session, env? }]
93
+
94
+ ━━━ MODULE: quotas ━━━
95
+ quotas.create [product_tag, data: {
96
+ tag: string, // unique identifier
97
+ name?: string, // display name
98
+ description?: string,
99
+ input: { // declare the input fields this quota expects
100
+ fieldName: {
101
+ type: "string"|"number"|"boolean"|"object"|"array",
102
+ required?: boolean,
103
+ description?: string
104
+ }
105
+ },
106
+ options: [ // list of providers tried in order
107
+ {
108
+ provider?: string, // friendly name for this provider slot
109
+ app: string, // app tag whose action to call
110
+ type: "action",
111
+ event: string, // action tag on that app
112
+ quota: number, // max allowed uses for this provider
113
+ uses?: number, // current use count (usually 0 at creation)
114
+ input: { "body:field": "$Input{fieldName}" }, // maps quota input → action input
115
+ output: {}, // expected output shape (can be {})
116
+ retries: number,
117
+ healthcheck?: string, // optional healthcheck tag to gate this provider
118
+ check_interval?: number // ms between health polls
119
+ }
120
+ ]
121
+ }]
122
+ quotas.update [product_tag, quota_tag, data: { name?: string, description?: string, options?: array }]
123
+ quotas.fetch [product_tag, quota_tag]
124
+ quotas.list [product_tag]
125
+ quotas.delete [product_tag, quota_tag]
126
+ quotas.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }]
127
+ quotas.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }]
128
+
129
+ ━━━ MODULE: fallback ━━━
130
+ fallback.create [product_tag, data: {
131
+ tag: string,
132
+ name?: string,
133
+ description?: string,
134
+ input: { // input fields this fallback expects
135
+ fieldName: {
136
+ type: "string"|"number"|"boolean"|"object"|"array",
137
+ required?: boolean,
138
+ description?: string
139
+ }
140
+ },
141
+ options: [ // ordered list: primary first, then fallback(s)
142
+ {
143
+ provider?: string, // friendly name e.g. "primary", "backup"
144
+ app: string, // app tag
145
+ type: "action",
146
+ event: string, // action tag
147
+ input: { "body:field": "$Input{fieldName}" },
148
+ output: {},
149
+ retries: number,
150
+ healthcheck?: string,
151
+ check_interval?: number
152
+ }
153
+ ]
154
+ }]
155
+ fallback.update [product_tag, fallback_tag, data: { name?: string, description?: string, options?: array }]
156
+ fallback.fetch [product_tag, fallback_tag]
157
+ fallback.list [product_tag]
158
+ fallback.delete [product_tag, fallback_tag]
159
+ fallback.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }]
160
+ fallback.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }]
161
+
162
+ ━━━ MODULE: health ━━━
163
+ health.create [product_tag, data: { tag: string, name: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"workflow"|"feature", app?: string, event?: string, input?: object }, interval: number, retries: number, envs: [{ slug: string, input?: object }], onFailure?: { notifications?: [{ notification: string, message: string, channels: { email?: { recipients: string[] }, push?: { recipients?: string[] }, sms?: { recipients: string[] } } }], webhooks?: [{ url: string, method?: "GET"|"POST", headers?: object, body?: object }] } }]
164
+ health.update [product_tag, health_tag, data: { name?: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"workflow"|"feature", app?: string, event?: string, input?: object }, interval?: number, retries?: number, envs?: [{ slug: string, input?: object }], onFailure?: object }]
165
+ health.fetch [product_tag, health_tag]
166
+ health.list [product_tag]
167
+ health.delete [product_tag, health_tag]
168
+ health.status [{ product, env, tag }]
169
+ health.run [{ product, env, tag }]
170
+ health.check [{ product, env, tag }]
171
+
172
+ ━━━ MODULE: notifications ━━━
173
+ notifications.create [product_tag, data: { tag: string, name: string, type?: "email"|"push"|"sms"|"callback" }]
174
+ notifications.update [product_tag, notif_tag, data: { name?: string, type?: "email"|"push"|"sms"|"callback" }]
175
+ notifications.fetch [product_tag, notif_tag]
176
+ notifications.list [product_tag]
177
+ notifications.delete [product_tag, notif_tag]
178
+ notifications.messages.create [product_tag, data: { tag: string, notification: string, subject?: { template: string, data: object }, body?: { template: string, data: object } }]
179
+ notifications.messages.update [product_tag, msg_tag, data: { subject?: { template: string, data: object }, body?: { template: string, data: object } }]
180
+ notifications.messages.fetch [product_tag, msg_tag]
181
+ notifications.messages.list [product_tag, notification_tag]
182
+ notifications.send [{ product, env, event, input: { email?: { recipients, subject, template }, push_notification?: { device_tokens, title, body, data }, sms?: { recipients, body }, callback?: { query, params, body, headers } } }]
183
+ notifications.email.send [{ product, env, notification, input: { recipients: string[], subject: object, template: object }, session?, cache? }]
184
+ notifications.push.send [{ product, env, notification, input: { device_tokens: string[], title: object, body: object, data: object }, session?, cache? }]
185
+ notifications.sms.send [{ product, env, notification, input: { recipients: string[], body: object }, session?, cache? }]
186
+ notifications.callback.send [{ product, env, notification, input: { query, params, body, headers }, session?, cache? }]
187
+ notifications.dispatch [{ product, env, notification, event, input, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }]
188
+ notifications.getMessages [{ product_tag?, env?, notification_tag?, status?, type?, start_date?, end_date?, page?, limit? }]
189
+
190
+ ━━━ MODULE: messageBrokers ━━━
191
+ messageBrokers.create [{ product: string, tag: string, name: string, description?: string, type: "kafka"|"rabbitmq"|"redis"|"sqs", envs: [{ slug: string, connection_url: string }] }]
192
+ messageBrokers.update [product_tag, broker_tag, data: { name?: string, description?: string, type?: "kafka"|"rabbitmq"|"redis"|"sqs", envs?: [{ slug: string, connection_url: string }] }]
193
+ messageBrokers.fetch [product_tag, broker_tag]
194
+ messageBrokers.list [product_tag]
195
+ messageBrokers.delete [product_tag, broker_tag]
196
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
197
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
198
+ messageBrokers.topics.fetch [product_tag, topic_tag]
199
+ messageBrokers.topics.list [product_tag, broker_tag]
200
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }]
201
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag", callback: "function_ref" }]
202
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message }, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }]
203
+ messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, producerTag?, consumerTag?, status?, startDate?, endDate?, page?, limit? }]
204
+ messageBrokers.messages.getProducers [{ product, env, brokerTag, topicTag?, page?, limit? }]
205
+ messageBrokers.messages.getConsumers [{ product, env, brokerTag, topicTag?, page?, limit? }]
206
+ messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, consumerTag?, startDate?, endDate?, page?, limit? }]
207
+ messageBrokers.messages.getStats [{ product, env, brokerTag }]
208
+ messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
209
+
210
+ ━━━ MODULE: storage ━━━
211
+ storage.create [{ product: string, tag: string, name: string, description?: string, envs: [{ slug: string, type: "aws"|"azure"|"gcp", config: { bucket?: string, region?: string, accessKeyId?: string, secretAccessKey?: string, containerName?: string, connectionString?: string, projectId?: string, keyFilename?: string } }] }]
212
+ storage.update [product_tag, storage_tag, data: { name?: string, description?: string, envs?: [{ slug: string, type: "aws"|"azure"|"gcp", config: { bucket?: string, region?: string, accessKeyId?: string, secretAccessKey?: string } }] }]
213
+ storage.fetch [product_tag, storage_tag]
214
+ storage.list [product_tag]
215
+ storage.delete [product_tag, storage_tag]
216
+ storage.upload [{ product, env, storage, fileName, buffer: string|Buffer, mimeType? }]
217
+ storage.download [{ product, env, storage, fileName }]
218
+ storage.remove [{ product, env, storage, fileName }]
219
+ storage.listFiles [{ product, env, storage, prefix?, limit?, continuationToken? }]
220
+ storage.getSignedUrl [{ product, env, storage, fileName, expiresIn?: number, action?: "read"|"write" }]
221
+ storage.stats [{ product, env, storage, prefix?, session?, cache? }]
222
+ storage.testConnection [{ product, env, storage }]
223
+ storage.files.upload [{ product, env, storage, fileName, buffer, mimeType? }]
224
+ storage.files.download [{ product, env, storage, fileName }]
225
+ storage.files.delete [{ product, env, storage, fileName }]
226
+ storage.files.list [{ product, env, storage, prefix?, limit?, continuationToken? }]
227
+ storage.files.getSignedUrl [{ product, env, storage, fileName, expiresIn?, action? }]
228
+ storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }]
229
+
230
+ ━━━ MODULE: databases ━━━
231
+ databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite", envs: [{slug, connection_url}] }]
232
+ databases.register [product_tag, data: IProductDatabase]
233
+ databases.list [product_tag]
234
+ databases.fetch [product_tag, database_tag]
235
+ databases.updateDatabase [product_tag, database_tag, data: { name?: string, description?: string, type?: "postgresql"|"mongodb"|"mysql", envs?: [{ slug: string, connection_url: string, description?: string }] }]
236
+ databases.connect [{ product, env, database }]
237
+ databases.testConnection [{ product, env, database }]
238
+ databases.disconnect []
239
+ databases.query [{ product, env, database, entity, where?: {field: {$eq|$gt|$lt|$in: value}}, select?: string[], orderBy?: [{field, order:"ASC"|"DESC"}], limit?, offset? }]
240
+ databases.insert [{ product, env, database, entity, data: {key: value}|{key:value}[], returning? }]
241
+ databases.update [{ product, env, database, entity, data: {key:value}, where: {key: {$eq: value}}, returning? }]
242
+ databases.delete [{ product, env, database, entity, where: {key: {$eq: value}}, returning? }]
243
+ databases.upsert [{ product, env, database, entity, data: {key:value}, conflictKeys: string[], returning? }]
244
+ databases.count [{ product, env, database, entity, where? }]
245
+ databases.sum [{ product, env, database, entity, field, where? }]
246
+ databases.avg [{ product, env, database, entity, field, where? }]
247
+ databases.min [{ product, env, database, entity, field, where? }]
248
+ databases.max [{ product, env, database, entity, field, where? }]
249
+ databases.aggregate [{ product, env, database, entity, aggregations: [{type:"count"|"sum"|"avg"|"min"|"max", field?, alias}], where?, groupBy? }]
250
+ databases.schema.create [collection_name, definition: { fieldName: "string"|"number"|"boolean"|"date"|{type, required?, unique?, default?} }, options?]
251
+ databases.schema.drop [collection_name, options?]
252
+ databases.schema.addField [collection, fieldName, definition]
253
+ databases.schema.dropField [collection, fieldName]
254
+ databases.schema.renameField [collection, oldName, newName]
255
+ databases.schema.modifyField [collection, fieldName, changes: Partial<{type, required, unique, default}>]
256
+ databases.schema.createIndex [collection, fields: string[]|[{field, order}], options?: {unique?, name?}]
257
+ databases.schema.dropIndex [collection, indexName]
258
+ databases.schema.list [schemaName?]
259
+ databases.schema.describe [collection_name]
260
+ databases.schema.indexes [collection_name]
261
+ databases.migration.create [{ product, database, data: { name, tag, description?, value: { up: string[], down: string[] } } }]
262
+ databases.migration.update [{ product, tag, data: { name?, description?, value? } }]
263
+ databases.migration.fetch [{ product, tag }]
264
+ databases.migration.list [{ product, database }]
265
+ databases.migration.delete [{ product, tag }]
266
+ databases.migration.run [migrations, options?]
267
+ databases.migration.rollback [migrations, count?]
268
+ databases.migration.history []
269
+ databases.migration.status [migrations]
270
+ databases.action.create [{ product, database, data: { tag, name, description?, type:"sql"|"nosql", query } }]
271
+ databases.action.update [{ product: string, tag: string, data: { name?: string, description?: string, query?: string } }]
272
+ databases.action.fetch [action_tag]
273
+ databases.action.list [database_tag]
274
+ databases.action.delete [action_tag]
275
+ databases.action.dispatch [{ product, env, database, action, input, schedule? }]
276
+ databases.dispatch [{ product, env, database, action, input, schedule? }]
277
+
278
+ ━━━ MODULE: graph ━━━
279
+ graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
280
+ graph.fetch [product_tag, graph_tag]
281
+ graph.list [product_tag?]
282
+ graph.update [product_tag, graph_tag, data: { name?: string, description?: string, type?: "neo4j"|"neptune"|"arangodb"|"memgraph", envs?: [{ slug: string, connection_url: string, username?: string, password?: string, database?: string, graphName?: string, region?: string }] }]
283
+ graph.delete [graph_tag, product_tag?]
284
+ graph.connect [{ product, env, graph }]
285
+ graph.testConnection [config]
286
+ graph.createNode [{ labels: string[], properties: { key: value } }, transaction?]
287
+ graph.findNodes [{ labels?: string[], where?: { key: value }, limit?, skip? }, transaction?]
288
+ graph.findNodeById [id: string|number, transaction?]
289
+ graph.updateNode [{ id, properties: { key: value } }, transaction?]
290
+ graph.deleteNode [{ id, detach?: boolean }, transaction?]
291
+ graph.mergeNode [{ labels: string[], matchProps: { key: value }, setProps?: { key: value } }, transaction?]
292
+ graph.addLabels [{ id, labels: string[] }, transaction?]
293
+ graph.removeLabels [{ id, labels: string[] }, transaction?]
294
+ graph.setLabels [{ id, labels: string[] }, transaction?]
295
+ graph.createRelationship [{ fromId, toId, type: string, properties?: { key: value } }, transaction?]
296
+ graph.findRelationships [{ type?: string, where?, limit? }, transaction?]
297
+ graph.findRelationshipById [id, transaction?]
298
+ graph.updateRelationship [{ id, properties: { key: value } }, transaction?]
299
+ graph.deleteRelationship [{ id }, transaction?]
300
+ graph.mergeRelationship [{ fromId, toId, type, matchProps?, setProps? }, transaction?]
301
+ graph.traverse [{ startId, direction?: "in"|"out"|"both", relationshipTypes?: string[], maxDepth?, where? }, transaction?]
302
+ graph.shortestPath [{ fromId, toId, relationshipType?, maxDepth? }, transaction?]
303
+ graph.allPaths [{ fromId, toId, relationshipType?, maxDepth? }, transaction?]
304
+ graph.getNeighborhood [{ id, depth?, relationshipTypes? }, transaction?]
305
+ graph.findConnectedComponents [{ labels? }, transaction?]
306
+ graph.query [cypher_query: string, params?: { key: value }, transaction?]
307
+ graph.getStatistics [transaction?]
308
+ graph.countNodes [labels?: string[], where?, transaction?]
309
+ graph.countRelationships [types?: string[], where?, transaction?]
310
+ graph.fullTextSearch [{ index, query, limit? }, transaction?]
311
+ graph.vectorSearch [{ index, vector: number[], topK? }, transaction?]
312
+ graph.createNodeIndex [{ label, field, type?: "btree"|"fulltext"|"vector" }]
313
+ graph.createNodeConstraint [{ label, field, type: "unique"|"exists" }]
314
+ graph.createRelationshipIndex [{ type, field }]
315
+ graph.listIndexes []
316
+ graph.listConstraints []
317
+ graph.dropIndex [name: string]
318
+ graph.dropConstraint [name: string]
319
+ graph.listLabels []
320
+ graph.listRelationshipTypes []
321
+ graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
322
+ graph.listActions [graphTag?, productTag?]
323
+ graph.getAction [actionTag, graphTag?, productTag?]
324
+ graph.updateAction [actionTag, updates, graphTag?, productTag?]
325
+ graph.deleteAction [actionTag, graphTag?, productTag?]
326
+ graph.beginTransaction [options?]
327
+ graph.commitTransaction [transaction]
328
+ graph.rollbackTransaction [transaction]
329
+ graph.dispatch [data]
330
+
331
+ ━━━ MODULE: vector ━━━
332
+ vector.create [{ product, tag, name, description?, provider: "pinecone"|"qdrant"|"weaviate", dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct", envs: [{slug, api_key, environment?}] }]
333
+ vector.update [{ product: string, tag: string, data: { name?: string, description?: string, type?: "pinecone"|"qdrant"|"weaviate"|"chroma"|"milvus"|"pgvector"|"memory", dimensions?: number, metric?: "cosine"|"euclidean"|"dotproduct", envs?: [{ slug: string, endpoint?: string, apiKey?: string, region?: string, index?: string, namespace?: string }] } }]
334
+ vector.fetch [{ product, tag }]
335
+ vector.list [{ product }]
336
+ vector.delete [{ product, tag }]
337
+ vector.connect [{ product, env, vector }]
338
+ vector.disconnect [{ product, env, vector }]
339
+ vector.query [{ product, env, tag, vector: number[], topK?: number, filter?, namespace?, includeValues?, includeMetadata? }]
340
+ vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
341
+ vector.fetchVectors [{ product, env, vector, ids: string[], namespace? }]
342
+ vector.deleteVectors [{ product, env, tag, ids: string[], namespace?, deleteAll? }]
343
+ vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace?, includeValues?, includeMetadata? }]
344
+ vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace?, session? }]
345
+ vector.fetchOne [{ product, env, vector, id, namespace? }]
346
+ vector.updateVector [{ product, env, vector, id, values?, setMetadata?, mergeMetadata?, namespace? }]
347
+ vector.updateMetadata [{ product, env, vector, id, metadata: { key: value }, merge?, namespace? }]
348
+ vector.deleteByIds [{ product, env, vector, ids: string[], namespace? }]
349
+ vector.deleteAll [{ product, env, vector, namespace? }]
350
+ vector.listVectors [{ product, env, vector, namespace?, prefix?, limit?, cursor? }]
351
+ vector.listAllVectors [{ product, env, vector, namespace?, prefix? }]
352
+ vector.listNamespaces [{ product, env, vector }]
353
+ vector.deleteNamespace [{ product, env, vector, namespace }]
354
+ vector.describeIndex [{ product, env, vector }]
355
+ vector.getStats [{ product, env, vector }]
356
+ vector.createIndex [{ product, env, vector, name, dimensions, metric?, replicas?, shards? }]
357
+ vector.deleteIndex [{ product, env, vector, name }]
358
+ vector.listIndexes [{ product, env, vector }]
359
+ vector.count [{ product, env, vector, namespace? }]
360
+
361
+ ━━━ MODULE: workflow ━━━
362
+ workflow.create [product_tag, data: {
363
+ tag: string,
364
+ name: string,
365
+ description?: string,
366
+ input?: { fieldName: { type: string, required?: boolean } },
367
+ output?: object,
368
+ envs?: [{ slug: string, active?: boolean }],
369
+ steps: [
370
+ {
371
+ tag: string, // unique step id
372
+ name?: string,
373
+ type: "action"|"database"|"graph"|"notification"|"storage"|"publish"|"workflow"|"sleep"|"wait_signal",
374
+ app?: string, // for type=action
375
+ event?: string, // action/event tag
376
+ database?: string, // for type=database
377
+ graph?: string, // for type=graph
378
+ notification?: string, // for type=notification
379
+ storage?: string, // for type=storage
380
+ broker?: string, // for type=publish
381
+ workflow?: string, // for type=workflow (child)
382
+ input?: { "body:field": "$Input{fieldName}" | "$Step{stepTag}{field}" | literal },
383
+ condition?: string, // e.g. "$Step{validate}{valid} == true"
384
+ dependsOn?: string[],
385
+ options?: { retries?: number, timeout?: number, allow_fail?: boolean, critical?: boolean }
386
+ }
387
+ ]
388
+ }]
389
+ workflow.update [product_tag, workflow_tag, data: { name?: string, description?: string, steps?: array, envs?: array }]
390
+ workflow.fetch [product_tag, workflow_tag]
391
+ workflow.fetchAll [product_tag]
392
+ workflow.delete [product_tag, workflow_tag]
393
+
394
+ workflow.define [{
395
+ product?: string,
396
+ tag: string,
397
+ name: string,
398
+ description?: string,
399
+ input?: { fieldName: { type: string, required?: boolean } },
400
+ output?: object,
401
+ signals?: { signalName: { input?: object } },
402
+ queries?: { queryName: { handler?: function } },
403
+ options?: { timeout?: number, retries?: number },
404
+ envs?: [{ slug: string, active?: boolean }],
405
+ recordInput?: object, // sample input for step-recording (run once during define)
406
+ recordScenarios?: object[], // multiple recording scenarios for branching
407
+ branchOverrides?: object, // force step results during recording to reach later branches
408
+ handler: async (ctx) => {
409
+ // ctx.input – typed workflow input
410
+ // ctx.step(tag, fn, rollback?, opts?) – define a durable step
411
+ // ctx.action.run({ app, event, input }) – call an app action
412
+ // ctx.database.query/insert/update/delete({ database, event, ... })
413
+ // ctx.graph.execute({ graph, action, input })
414
+ // ctx.notification.send/email/push/sms({ notification, event, ... })
415
+ // ctx.storage.upload/download({ storage, event, input })
416
+ // ctx.messaging.produce({ event: "broker:topic", message: {} })
417
+ // ctx.quota.execute({ quota, input })
418
+ // ctx.fallback.execute({ fallback, input })
419
+ // ctx.healthcheck.getStatus(tag)
420
+ // ctx.sleep(ms|"1h30m")
421
+ // ctx.waitForSignal("signal-name", { timeout? })
422
+ // ctx.setState(key, value) / ctx.getState(key)
423
+ // ctx.workflow(childId, childTag, childInput) – child workflow
424
+ }
425
+ }]
426
+
427
+ workflow.execute [{
428
+ product: string,
429
+ env: string,
430
+ tag: string,
431
+ input: { fieldName: value },
432
+ session?: string, // "session_tag:jwt_token"
433
+ idempotency_key?: string,
434
+ cache?: string,
435
+ retries?: number,
436
+ timeout?: number
437
+ }]
438
+
439
+ workflow.dispatch [{
440
+ product: string,
441
+ env: string,
442
+ workflow: string,
443
+ input: { fieldName: value },
444
+ schedule?: { start_at?: number|string, cron?: string, every?: number, limit?: number, endDate?: number|string, tz?: string },
445
+ session?: string,
446
+ cache?: string,
447
+ retries?: number
448
+ }]
449
+
450
+ workflow.signal [{ product, env, workflow_id, signal: string, payload?: object }]
451
+ workflow.query [{ product, env, workflow_id, query: string, params?: object }]
452
+ workflow.status [executionId: string]
453
+ workflow.cancel [executionId: string, reason?: string]
454
+ workflow.replay [executionId: string, options?: object]
455
+ workflow.restart [executionId: string]
456
+ workflow.resume [executionId: string]
457
+ workflow.replayFromStep [executionId: string, stepTag: string]
458
+ workflow.history [executionId: string]
459
+ workflow.stepDetail [executionId: string, stepTag: string]
460
+ workflow.relatedExecutions [executionId: string]
461
+ workflow.compare [executionId1: string, executionId2: string]
462
+
463
+ ━━━ MODULE: caches ━━━
464
+ caches.create [{ product, tag, name, description?, type: "redis"|"memcached"|"in-memory", envs: [{slug, connection_url}] }]
465
+ caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
466
+ caches.fetch [product_tag, cache_tag]
467
+ caches.list [product_tag]
468
+ caches.delete [product_tag, cache_tag]
469
+ caches.get [{ key: string }]
470
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
471
+ caches.clear [{ key: string }]
472
+ caches.clearAll [{ product, cache, env? }]
473
+ caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
474
+ caches.fetchDashboard [{ product, cache, env? }]
475
+
476
+ ━━━ MODULE: jobs ━━━
477
+ jobs.create [product_tag, data: { tag, name, description?, type: "action"|"notification"|"storage"|"database"|"publish", app?, event?, executions?, intervals?, start_at? }]
478
+ jobs.update [product_tag, job_tag, data: { name?: string, description?: string, type?: "action"|"notification"|"storage"|"database"|"publish", app?: string, event?: string, executions?: number, intervals?: number, start_at?: number }]
479
+ jobs.fetch [product_tag, job_tag]
480
+ jobs.list [product_tag]
481
+ jobs.delete [product_tag, job_tag]
482
+ jobs.get [jobId: string]
483
+ jobs.listJobs [{ product?, status?, limit?, page? }]
484
+ jobs.cancel [jobId, { reason? }?]
485
+ jobs.cancelMany [{ product?, status? }]
486
+ jobs.pause [jobId]
487
+ jobs.pauseMany [{ product?, recurring? }]
488
+ jobs.resume [jobId]
489
+ jobs.resumeMany [{ product?, status? }]
490
+ jobs.retry [jobId, { delay? }?]
491
+ jobs.retryMany [{ status?, from? }]
492
+ jobs.reschedule [jobId, { start_at?, cron?, every? }]
493
+ jobs.getHistory [jobId, { limit? }?]
494
+ jobs.getStats [{ product?, env?, from?, to? }?]
495
+
496
+ ━━━ MODULE: secrets ━━━
497
+ secrets.create [{ key, value, description?, token_type?: "api"|"password"|"certificate", scope?: string[], envs?: string[], expires_at?: number }]
498
+ secrets.update [key, { value?, description?, scope?, envs?, expires_at? }]
499
+ secrets.fetch [key]
500
+ secrets.list []
501
+ secrets.delete [key]
502
+ secrets.revoke [key]
503
+ secrets.exists [key]
504
+ secrets.resolve [value, { env?, app? }?]
505
+ secrets.validate [value]
506
+
507
+ ━━━ MODULE: logs ━━━
508
+ logs.init [product_tag?, app_tag?] (at least one required)
509
+ logs.fetch [{ product_id?, app_id?, env?, level?: "info"|"warn"|"error"|"debug", start_date?, end_date?, page?, limit? }]
510
+ `;
511
+
512
+ const executeInputSchema = z.object({
513
+ publishable_key: z.string().describe('The publishable key for your workspace. Required for authentication.'),
514
+ module: z.enum(MODULES as [string, ...string[]]).describe(
515
+ 'SDK module to target. Options: ' + MODULES.join(', ') + '.'
516
+ ),
517
+ method: z.string().describe(
518
+ 'The SDK method to call. Use dot-notation for nested methods (e.g. "environments.create", "schema.addField", "messages.query").\n' +
519
+ 'Full reference: see the METHOD_DOCS embedded in the params description.'
520
+ ),
521
+ params: z.array(z.any()).default([]).describe(METHOD_DOCS),
522
+ });
523
+
524
+ const payloadGenerateInputSchema = z.object({
525
+ workspace_id: z.string(),
526
+ user_id: z.string(),
527
+ public_key: z.string(),
528
+ product_tag: z.string(),
529
+ env_slug: z.string(),
530
+ operation_family: z.string(),
531
+ method: z.string(),
532
+ targets: z.record(z.any()).optional(),
533
+ include_session: z.boolean().optional().default(true),
534
+ include_cache: z.boolean().optional().default(true),
535
+ schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort'),
536
+ input_hint: z.record(z.any()).optional(),
537
+ });
538
+
539
+ const snippetGenerateInputSchema = payloadGenerateInputSchema.extend({
540
+ language: z.enum(['typescript', 'python']).default('typescript'),
541
+ });
542
+
543
+ function toPrettyJson(value: unknown): string {
544
+ return JSON.stringify(value ?? {}, null, 2);
545
+ }
546
+
547
+ const ALLOWED_SNIPPET_METHODS: Record<string, string[]> = {
548
+ action: ['run', 'dispatch', 'execute'],
549
+ workflow: ['run', 'dispatch', 'execute'],
550
+ database: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'upsert', 'count', 'aggregate'],
551
+ graph: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'execute'],
552
+ vector: ['query', 'find', 'findSimilar', 'upsert', 'insert', 'delete', 'dispatch'],
553
+ storage: ['dispatch', 'upload', 'download', 'remove', 'listFiles', 'getSignedUrl', 'stats'],
554
+ notification: ['dispatch', 'send', 'email.send', 'push.send', 'sms.send', 'callback.send'],
555
+ messaging: ['dispatch', 'send', 'publish', 'produce', 'consume'],
556
+ broker: ['dispatch', 'send', 'publish', 'produce', 'consume'],
557
+ quota: ['run', 'dispatch', 'check', 'consume'],
558
+ fallback: ['run', 'dispatch'],
559
+ healthcheck: ['run', 'check', 'status'],
560
+ health: ['run', 'check', 'status'],
561
+ session: ['start', 'verify', 'refresh', 'revoke', 'listActive'],
562
+ cache: ['get', 'set', 'clear', 'clearAll', 'dispatch', 'fetchValues'],
563
+ };
564
+
565
+ function ensureSupportedSnippetOperation(operationFamily: string, method: string): void {
566
+ const family = String(operationFamily || '').toLowerCase();
567
+ const m = String(method || '');
568
+ const allowed = ALLOWED_SNIPPET_METHODS[family] || [];
569
+ if (!allowed.includes(m)) {
570
+ const supportedFamilies = Object.keys(ALLOWED_SNIPPET_METHODS).sort().join(', ');
571
+ throw new Error(
572
+ `Unsupported snippet operation "${family}.${m}". Allowed methods for "${family}": ${allowed.join(', ') || '(none)'}.\n` +
573
+ `Supported families: ${supportedFamilies}`,
574
+ );
575
+ }
576
+ }
577
+
578
+ function resolveSdkCallPath(operationFamily: string, method: string): string {
579
+ const family = String(operationFamily || '').toLowerCase();
580
+ const m = String(method || '').toLowerCase();
581
+
582
+ if (family === 'action') return `actions.${m}`;
583
+ if (family === 'workflow') return `workflow.${m}`;
584
+ if (family === 'database') return m === 'dispatch' ? 'databases.dispatch' : `databases.${m}`;
585
+ if (family === 'graph') return m === 'dispatch' ? 'graph.dispatch' : `graph.${m}`;
586
+ if (family === 'vector') return `vector.${m}`;
587
+ if (family === 'storage') return m === 'dispatch' ? 'storage.dispatch' : `storage.${m}`;
588
+ if (family === 'notification') return m === 'dispatch' ? 'notifications.dispatch' : `notifications.${m}`;
589
+ if (family === 'messaging' || family === 'broker') {
590
+ if (m === 'dispatch') return 'messageBrokers.dispatch';
591
+ if (m === 'send' || m === 'publish' || m === 'produce') return 'messageBrokers.produce';
592
+ return `messageBrokers.${m}`;
593
+ }
594
+ if (family === 'quota') return `quotas.${m}`;
595
+ if (family === 'fallback') return `fallback.${m}`;
596
+ if (family === 'healthcheck' || family === 'health') return `health.${m}`;
597
+ if (family === 'session') return `sessions.${m}`;
598
+ if (family === 'cache') return m === 'dispatch' ? 'caches.dispatch' : `caches.${m}`;
599
+ return `${family}.${m}`;
600
+ }
601
+
602
+ function buildSdkInvocationArgs(payload: Record<string, unknown>): Record<string, unknown> {
603
+ const input = (payload?.input as Record<string, unknown>) || {};
604
+ return {
605
+ product: payload?.product_tag,
606
+ env: payload?.env,
607
+ ...input,
608
+ ...(payload?.session ? { session: payload.session } : {}),
609
+ ...(payload?.cache ? { cache: payload.cache } : {}),
610
+ };
611
+ }
612
+
613
+ function buildTypeScriptSnippet(
614
+ payload: Record<string, unknown>,
615
+ operationFamily: string,
616
+ method: string,
617
+ ): string {
618
+ const callPath = resolveSdkCallPath(operationFamily, method);
619
+ const invocationArgs = buildSdkInvocationArgs(payload);
620
+ return `import Ductape from "@ductape/sdk";
621
+
622
+ const ductape = new Ductape({
623
+ workspace_id: process.env.DUCTAPE_WORKSPACE_ID!,
624
+ user_id: process.env.DUCTAPE_USER_ID!,
625
+ public_key: process.env.DUCTAPE_PUBLIC_KEY!,
626
+ });
627
+
628
+ async function run() {
629
+ const payload = ${toPrettyJson(payload)};
630
+ const args = ${toPrettyJson(invocationArgs)};
631
+ const result = await ductape.${callPath}(args);
632
+ return { payload, result };
633
+ }
634
+
635
+ run().catch(console.error);
636
+ `;
637
+ }
638
+
639
+ function buildPythonSnippet(
640
+ payload: Record<string, unknown>,
641
+ operationFamily: string,
642
+ method: string,
643
+ ): string {
644
+ const callPath = resolveSdkCallPath(operationFamily, method);
645
+ const invocationArgs = buildSdkInvocationArgs(payload);
646
+ return `from ductape import Ductape
647
+ import os
648
+ import json
649
+
650
+ ductape = Ductape(
651
+ workspace_id=os.environ.get("DUCTAPE_WORKSPACE_ID"),
652
+ user_id=os.environ.get("DUCTAPE_USER_ID"),
653
+ public_key=os.environ.get("DUCTAPE_PUBLIC_KEY"),
654
+ )
655
+
656
+ def run():
657
+ payload = ${toPrettyJson(payload)}
658
+ args = ${toPrettyJson(invocationArgs)}
659
+ result = ductape.${callPath}(args)
660
+ return {"payload": payload, "result": result}
661
+
662
+ if __name__ == "__main__":
663
+ print(json.dumps(run(), indent=2))
664
+ `;
665
+ }
666
+
667
+ function buildSnippet(
668
+ language: 'typescript' | 'python',
669
+ payload: Record<string, unknown>,
670
+ operationFamily: string,
671
+ method: string,
672
+ ): string {
673
+ return language === 'python'
674
+ ? buildPythonSnippet(payload, operationFamily, method)
675
+ : buildTypeScriptSnippet(payload, operationFamily, method);
676
+ }
677
+
678
+
679
+ async function loadMcpSdk(): Promise<{
680
+ McpServer: new (info: { name: string; version: string }) => any;
681
+ StdioServerTransport: new () => any;
682
+ }> {
683
+ try {
684
+ const [{ McpServer }, { StdioServerTransport }] = await Promise.all([
685
+ import('@modelcontextprotocol/sdk/server/mcp.js'),
686
+ import('@modelcontextprotocol/sdk/server/stdio.js'),
687
+ ]);
688
+ if (McpServer && StdioServerTransport) {
689
+ return { McpServer, StdioServerTransport };
690
+ }
691
+ } catch {
692
+ // fall through to v2 alpha package
693
+ }
694
+
695
+ try {
696
+ const sdk = await import('@modelcontextprotocol/server');
697
+ if (sdk.McpServer && sdk.StdioServerTransport) {
698
+ return { McpServer: sdk.McpServer, StdioServerTransport: sdk.StdioServerTransport };
699
+ }
700
+ } catch {
701
+ // fall through
702
+ }
703
+
704
+ console.error(
705
+ 'Failed to load MCP SDK. Install: npm install @modelcontextprotocol/sdk zod\n' +
706
+ 'Or v2 alpha: npm install @modelcontextprotocol/server zod @cfworker/json-schema',
707
+ );
708
+ process.exit(1);
709
+ }
710
+
711
+ async function main() {
712
+ const { McpServer, StdioServerTransport } = await loadMcpSdk();
713
+ const server = new McpServer({ name: 'ductape-mcp', version: '0.1.0' });
714
+ const transport = new StdioServerTransport();
715
+
716
+ const executeHandler = async (args: { publishable_key: string; module: SDKModule; method: string; params: unknown[] }) => {
717
+ try {
718
+ if (!args.publishable_key) {
719
+ throw new Error('Not authenticated. Please provide the `publishable_key` in your tool invocation arguments.');
720
+ }
721
+ const result = await executeViaProxy(args.publishable_key, args.module, args.method, args.params);
722
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
723
+ } catch (err) {
724
+ const message = err instanceof Error ? err.message : String(err);
725
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
726
+ }
727
+ };
728
+
729
+ const payloadGenerateHandler = async (
730
+ args: z.infer<typeof payloadGenerateInputSchema>,
731
+ ) => {
732
+ try {
733
+ const result = await generateExecutablePayload(args);
734
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
735
+ } catch (err) {
736
+ const message = err instanceof Error ? err.message : String(err);
737
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
738
+ }
739
+ };
740
+
741
+ const snippetGenerateHandler = async (
742
+ args: z.infer<typeof snippetGenerateInputSchema>,
743
+ ) => {
744
+ try {
745
+ ensureSupportedSnippetOperation(args.operation_family, args.method);
746
+ const generated = await generateExecutablePayload(args);
747
+ const payload = (generated as any)?.payload ?? {};
748
+ const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
749
+ return {
750
+ content: [
751
+ {
752
+ type: 'text',
753
+ text: JSON.stringify(
754
+ {
755
+ payload: generated,
756
+ snippet,
757
+ },
758
+ null,
759
+ 2,
760
+ ),
761
+ },
762
+ ],
763
+ };
764
+ } catch (err) {
765
+ const message = err instanceof Error ? err.message : String(err);
766
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
767
+ }
768
+ };
769
+
770
+ if (typeof server.registerTool === 'function') {
771
+ server.registerTool(
772
+ 'ductape_execute',
773
+ {
774
+ title: 'Ductape SDK Execute',
775
+ description: 'Execute a Ductape SDK operation via the backend proxy (databases, graph, storage, etc.)',
776
+ inputSchema: executeInputSchema,
777
+ },
778
+ executeHandler,
779
+ );
780
+ server.registerTool(
781
+ 'ductape_generate_payload',
782
+ {
783
+ title: 'Ductape Payload Generator',
784
+ description: 'Generate canonical executable payload templates and schema metadata for SDK code snippet generation',
785
+ inputSchema: payloadGenerateInputSchema,
786
+ },
787
+ payloadGenerateHandler,
788
+ );
789
+ server.registerTool(
790
+ 'ductape_generate_snippet',
791
+ {
792
+ title: 'Ductape Snippet Generator',
793
+ description: 'Generate canonical payload and ready TypeScript/Python snippet for engineers',
794
+ inputSchema: snippetGenerateInputSchema,
795
+ },
796
+ snippetGenerateHandler,
797
+ );
798
+ } else if (typeof server.tool === 'function') {
799
+ server.tool('ductape_execute', executeInputSchema.shape, executeHandler);
800
+ server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
801
+ server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
802
+ } else {
803
+ console.error('MCP server does not expose .registerTool() or .tool()');
804
+ process.exit(1);
805
+ }
806
+
807
+ await server.connect(transport);
808
+ }
809
+
810
+ main().catch((err) => {
811
+ console.error(err);
812
+ process.exit(1);
813
+ });