@ductape/mcp 0.1.0 → 0.1.2
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/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -79
- package/dist/proxy-client.d.ts +2 -3
- package/dist/proxy-client.d.ts.map +1 -1
- package/dist/proxy-client.js +14 -1
- package/package.json +1 -1
- package/src/index.ts +297 -78
- package/src/proxy-client.ts +16 -4
package/src/index.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* Exposes Ductape SDK operations as MCP tools by calling the backend proxy.
|
|
6
6
|
*
|
|
7
7
|
* Authentication:
|
|
8
|
-
*
|
|
8
|
+
* Set DUCTAPE_PUBLISHABLE_KEY in the MCP server's env config to avoid passing
|
|
9
|
+
* publishable_key on every tool call. Per-call publishable_key still overrides
|
|
10
|
+
* the env var when provided.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { z } from 'zod';
|
|
12
14
|
import {
|
|
13
15
|
executeViaProxy,
|
|
14
16
|
generateExecutablePayload,
|
|
17
|
+
getAssetSchemas,
|
|
15
18
|
type SDKModule,
|
|
16
19
|
} from './proxy-client.js';
|
|
17
20
|
|
|
@@ -29,6 +32,32 @@ const MODULES: SDKModule[] = [
|
|
|
29
32
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
33
|
|
|
31
34
|
const METHOD_DOCS = `
|
|
35
|
+
━━━ TOOL SELECTION GUIDE ━━━
|
|
36
|
+
|
|
37
|
+
There are two categories of SDK operations. Use the right tool for each:
|
|
38
|
+
|
|
39
|
+
1. ASSET CREATION / UPDATE (create, update, add, register…)
|
|
40
|
+
Input shape is fixed by the SDK's Joi validators.
|
|
41
|
+
→ Call ductape_schema first to discover required fields and enum values.
|
|
42
|
+
→ Then call ductape_execute with the filled params.
|
|
43
|
+
EXCEPTION — features.create / quotas.create / fallback.create have TWO distinct input fields:
|
|
44
|
+
a) Top-level "input": a SCHEMA DECLARATION — defines what fields the asset accepts at runtime.
|
|
45
|
+
Construct this yourself based on what the caller will provide. Do NOT use ductape_generate_payload for this.
|
|
46
|
+
b) options[].input (quotas/fallbacks) or steps[].input (features): maps declared inputs → the underlying action's fields.
|
|
47
|
+
← CALL ductape_generate_payload for the target action to discover what fields it accepts,
|
|
48
|
+
then wire them using "$Input{fieldName}" or "$Step{stepTag}{field}" references.
|
|
49
|
+
|
|
50
|
+
2. RUNTIME OPERATIONS (run, dispatch, execute, start, send, produce, query, insert, update, delete…)
|
|
51
|
+
The "input" field shape is product- and operation-specific — it is NOT derivable from Joi validators.
|
|
52
|
+
It is defined by how the product's action/feature/session/quota/etc. was configured in Ductape.
|
|
53
|
+
→ ALWAYS call ductape_generate_payload first to get the canonical payload template.
|
|
54
|
+
→ The template shows you exactly which input keys are expected and their types/defaults.
|
|
55
|
+
→ Then fill in the values and pass the completed payload to ductape_execute.
|
|
56
|
+
|
|
57
|
+
Skipping ductape_generate_payload for runtime operations will produce incorrect or empty input payloads.
|
|
58
|
+
|
|
59
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
60
|
+
|
|
32
61
|
ALL params are passed as a JSON array in positional order matching the SDK signature.
|
|
33
62
|
|
|
34
63
|
━━━ MODULE: product ━━━
|
|
@@ -57,8 +86,8 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
57
86
|
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
87
|
actions.fetch [app_tag, action_tag]
|
|
59
88
|
actions.list [app_tag]
|
|
60
|
-
actions.run [{ product, env, app, action, input: { "body:fieldName": value,
|
|
61
|
-
actions.dispatch [{ product, env, app, action, input, retries?, session?, cache?, schedule?: { start_at?, cron?, every?, limit?, tz? } }]
|
|
89
|
+
actions.run [{ product, env, app, action, input: { "body:fieldName": value, ... } }] ← CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
|
|
90
|
+
actions.dispatch [{ product, env, app, action, input, retries?, session?, cache?, schedule?: { start_at?, cron?, every?, limit?, tz? } }] ← CALL ductape_generate_payload FIRST (operation_family="action", method="dispatch")
|
|
62
91
|
|
|
63
92
|
━━━ MODULE: auths ━━━
|
|
64
93
|
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 }]
|
|
@@ -82,7 +111,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
82
111
|
sessions.fetch [product_tag, session_tag]
|
|
83
112
|
sessions.list [product_tag]
|
|
84
113
|
sessions.delete [product_tag, session_tag]
|
|
85
|
-
sessions.start [{ product, env, tag, data: { key: value } }]
|
|
114
|
+
sessions.start [{ product, env, tag, data: { key: value } }] ← CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag}) to discover the data field shape
|
|
86
115
|
sessions.verify [{ product, env, tag, token }]
|
|
87
116
|
sessions.refresh [{ product, env, tag, refreshToken }]
|
|
88
117
|
sessions.revoke [{ product, env, tag, sessionId?, identifier? }]
|
|
@@ -96,13 +125,18 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
96
125
|
tag: string, // unique identifier
|
|
97
126
|
name?: string, // display name
|
|
98
127
|
description?: string,
|
|
99
|
-
|
|
128
|
+
|
|
129
|
+
// INPUT SCHEMA (top-level): declares what fields the quota accepts when called at runtime.
|
|
130
|
+
// These are the fields callers will pass to quotas.run / quotas.dispatch.
|
|
131
|
+
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
132
|
+
input: {
|
|
100
133
|
fieldName: {
|
|
101
134
|
type: "string"|"number"|"boolean"|"object"|"array",
|
|
102
135
|
required?: boolean,
|
|
103
136
|
description?: string
|
|
104
137
|
}
|
|
105
138
|
},
|
|
139
|
+
|
|
106
140
|
options: [ // list of providers tried in order
|
|
107
141
|
{
|
|
108
142
|
provider?: string, // friendly name for this provider slot
|
|
@@ -111,7 +145,12 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
111
145
|
event: string, // action tag on that app
|
|
112
146
|
quota: number, // max allowed uses for this provider
|
|
113
147
|
uses?: number, // current use count (usually 0 at creation)
|
|
114
|
-
|
|
148
|
+
|
|
149
|
+
// OPTIONS INPUT (per provider): maps the quota's declared input fields → the underlying action's fields.
|
|
150
|
+
// ← CALL ductape_generate_payload (operation_family="action", method="run", targets={app, event})
|
|
151
|
+
// to discover what fields the action accepts, then wire them with "$Input{fieldName}" references.
|
|
152
|
+
input: { "body:field": "$Input{fieldName}" },
|
|
153
|
+
|
|
115
154
|
output: {}, // expected output shape (can be {})
|
|
116
155
|
retries: number,
|
|
117
156
|
healthcheck?: string, // optional healthcheck tag to gate this provider
|
|
@@ -123,28 +162,38 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
123
162
|
quotas.fetch [product_tag, quota_tag]
|
|
124
163
|
quotas.list [product_tag]
|
|
125
164
|
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 } }]
|
|
165
|
+
quotas.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }] ← CALL ductape_generate_payload FIRST (operation_family="quota", method="run", targets={tag})
|
|
166
|
+
quotas.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }] ← CALL ductape_generate_payload FIRST (operation_family="quota", method="dispatch")
|
|
128
167
|
|
|
129
168
|
━━━ MODULE: fallback ━━━
|
|
130
169
|
fallback.create [product_tag, data: {
|
|
131
170
|
tag: string,
|
|
132
171
|
name?: string,
|
|
133
172
|
description?: string,
|
|
134
|
-
|
|
173
|
+
|
|
174
|
+
// INPUT SCHEMA (top-level): declares what fields the fallback accepts when called at runtime.
|
|
175
|
+
// These are the fields callers will pass to fallback.run / fallback.dispatch.
|
|
176
|
+
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
177
|
+
input: {
|
|
135
178
|
fieldName: {
|
|
136
179
|
type: "string"|"number"|"boolean"|"object"|"array",
|
|
137
180
|
required?: boolean,
|
|
138
181
|
description?: string
|
|
139
182
|
}
|
|
140
183
|
},
|
|
184
|
+
|
|
141
185
|
options: [ // ordered list: primary first, then fallback(s)
|
|
142
186
|
{
|
|
143
187
|
provider?: string, // friendly name e.g. "primary", "backup"
|
|
144
188
|
app: string, // app tag
|
|
145
189
|
type: "action",
|
|
146
190
|
event: string, // action tag
|
|
191
|
+
|
|
192
|
+
// OPTIONS INPUT (per provider): maps the fallback's declared input fields → the underlying action's fields.
|
|
193
|
+
// ← CALL ductape_generate_payload (operation_family="action", method="run", targets={app, event})
|
|
194
|
+
// to discover what fields the action accepts, then wire them with "$Input{fieldName}" references.
|
|
147
195
|
input: { "body:field": "$Input{fieldName}" },
|
|
196
|
+
|
|
148
197
|
output: {},
|
|
149
198
|
retries: number,
|
|
150
199
|
healthcheck?: string,
|
|
@@ -156,12 +205,12 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
156
205
|
fallback.fetch [product_tag, fallback_tag]
|
|
157
206
|
fallback.list [product_tag]
|
|
158
207
|
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 } }]
|
|
208
|
+
fallback.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }] ← CALL ductape_generate_payload FIRST (operation_family="fallback", method="run", targets={tag})
|
|
209
|
+
fallback.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }] ← CALL ductape_generate_payload FIRST (operation_family="fallback", method="dispatch")
|
|
161
210
|
|
|
162
211
|
━━━ MODULE: health ━━━
|
|
163
|
-
health.create [product_tag, data: { tag: string, name: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"
|
|
164
|
-
health.update [product_tag, health_tag, data: { name?: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"
|
|
212
|
+
health.create [product_tag, data: { tag: string, name: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"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 }] } }]
|
|
213
|
+
health.update [product_tag, health_tag, data: { name?: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"feature", app?: string, event?: string, input?: object }, interval?: number, retries?: number, envs?: [{ slug: string, input?: object }], onFailure?: object }]
|
|
165
214
|
health.fetch [product_tag, health_tag]
|
|
166
215
|
health.list [product_tag]
|
|
167
216
|
health.delete [product_tag, health_tag]
|
|
@@ -179,12 +228,12 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
179
228
|
notifications.messages.update [product_tag, msg_tag, data: { subject?: { template: string, data: object }, body?: { template: string, data: object } }]
|
|
180
229
|
notifications.messages.fetch [product_tag, msg_tag]
|
|
181
230
|
notifications.messages.list [product_tag, notification_tag]
|
|
182
|
-
notifications.send [{ product, env, event, input: {
|
|
183
|
-
notifications.email.send [{ product, env, notification, input: {
|
|
184
|
-
notifications.push.send [{ product, env, notification, input: {
|
|
185
|
-
notifications.sms.send [{ product, env, notification, input: {
|
|
186
|
-
notifications.callback.send [{ product, env, notification, input: {
|
|
187
|
-
notifications.dispatch [{ product, env, notification, event, input, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }]
|
|
231
|
+
notifications.send [{ product, env, event, input: { ... } }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="send", targets={notification})
|
|
232
|
+
notifications.email.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
233
|
+
notifications.push.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="push.send")
|
|
234
|
+
notifications.sms.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="sms.send")
|
|
235
|
+
notifications.callback.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="callback.send")
|
|
236
|
+
notifications.dispatch [{ product, env, notification, event, input, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="dispatch")
|
|
188
237
|
notifications.getMessages [{ product_tag?, env?, notification_tag?, status?, type?, start_date?, end_date?, page?, limit? }]
|
|
189
238
|
|
|
190
239
|
━━━ MODULE: messageBrokers ━━━
|
|
@@ -197,9 +246,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
197
246
|
messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
|
|
198
247
|
messageBrokers.topics.fetch [product_tag, topic_tag]
|
|
199
248
|
messageBrokers.topics.list [product_tag, broker_tag]
|
|
200
|
-
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }]
|
|
249
|
+
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
|
|
201
250
|
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? } }]
|
|
251
|
+
messageBrokers.dispatch [{ product, env, broker, event, input: { message }, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
|
|
203
252
|
messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, producerTag?, consumerTag?, status?, startDate?, endDate?, page?, limit? }]
|
|
204
253
|
messageBrokers.messages.getProducers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
205
254
|
messageBrokers.messages.getConsumers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
@@ -225,7 +274,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
225
274
|
storage.files.delete [{ product, env, storage, fileName }]
|
|
226
275
|
storage.files.list [{ product, env, storage, prefix?, limit?, continuationToken? }]
|
|
227
276
|
storage.files.getSignedUrl [{ product, env, storage, fileName, expiresIn?, action? }]
|
|
228
|
-
storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }]
|
|
277
|
+
storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch", targets={storage})
|
|
229
278
|
|
|
230
279
|
━━━ MODULE: databases ━━━
|
|
231
280
|
databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite", envs: [{slug, connection_url}] }]
|
|
@@ -272,8 +321,8 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
272
321
|
databases.action.fetch [action_tag]
|
|
273
322
|
databases.action.list [database_tag]
|
|
274
323
|
databases.action.delete [action_tag]
|
|
275
|
-
databases.action.dispatch [{ product, env, database, action, input, schedule? }]
|
|
276
|
-
databases.dispatch [{ product, env, database, action, input, schedule? }]
|
|
324
|
+
databases.action.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch", targets={database, action})
|
|
325
|
+
databases.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch")
|
|
277
326
|
|
|
278
327
|
━━━ MODULE: graph ━━━
|
|
279
328
|
graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
|
|
@@ -326,7 +375,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
326
375
|
graph.beginTransaction [options?]
|
|
327
376
|
graph.commitTransaction [transaction]
|
|
328
377
|
graph.rollbackTransaction [transaction]
|
|
329
|
-
graph.dispatch [data]
|
|
378
|
+
graph.dispatch [data] ← CALL ductape_generate_payload FIRST (operation_family="graph", method="dispatch", targets={graph})
|
|
330
379
|
|
|
331
380
|
━━━ MODULE: vector ━━━
|
|
332
381
|
vector.create [{ product, tag, name, description?, provider: "pinecone"|"qdrant"|"weaviate", dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct", envs: [{slug, api_key, environment?}] }]
|
|
@@ -358,19 +407,24 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
358
407
|
vector.listIndexes [{ product, env, vector }]
|
|
359
408
|
vector.count [{ product, env, vector, namespace? }]
|
|
360
409
|
|
|
361
|
-
━━━ MODULE:
|
|
362
|
-
|
|
410
|
+
━━━ MODULE: features ━━━
|
|
411
|
+
features.create [product_tag, data: {
|
|
363
412
|
tag: string,
|
|
364
413
|
name: string,
|
|
365
414
|
description?: string,
|
|
415
|
+
|
|
416
|
+
// INPUT SCHEMA (top-level): declares what fields this feature accepts when executed at runtime.
|
|
417
|
+
// These are the fields callers will pass to features.execute / features.dispatch.
|
|
418
|
+
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
366
419
|
input?: { fieldName: { type: string, required?: boolean } },
|
|
420
|
+
|
|
367
421
|
output?: object,
|
|
368
422
|
envs?: [{ slug: string, active?: boolean }],
|
|
369
423
|
steps: [
|
|
370
424
|
{
|
|
371
425
|
tag: string, // unique step id
|
|
372
426
|
name?: string,
|
|
373
|
-
type: "action"|"database"|"graph"|"notification"|"storage"|"publish"|"
|
|
427
|
+
type: "action"|"database"|"graph"|"notification"|"storage"|"publish"|"feature"|"sleep"|"wait_signal",
|
|
374
428
|
app?: string, // for type=action
|
|
375
429
|
event?: string, // action/event tag
|
|
376
430
|
database?: string, // for type=database
|
|
@@ -378,20 +432,25 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
378
432
|
notification?: string, // for type=notification
|
|
379
433
|
storage?: string, // for type=storage
|
|
380
434
|
broker?: string, // for type=publish
|
|
381
|
-
|
|
435
|
+
feature?: string, // for type=feature (child feature)
|
|
436
|
+
|
|
437
|
+
// STEP INPUT: maps this feature's declared input fields (or prior step outputs) → the step's underlying action/event fields.
|
|
438
|
+
// ← CALL ductape_generate_payload (operation_family matching step type, method="run", targets={app/event/database/etc.})
|
|
439
|
+
// to discover what fields the target accepts, then wire them with "$Input{fieldName}" or "$Step{stepTag}{field}" references.
|
|
382
440
|
input?: { "body:field": "$Input{fieldName}" | "$Step{stepTag}{field}" | literal },
|
|
441
|
+
|
|
383
442
|
condition?: string, // e.g. "$Step{validate}{valid} == true"
|
|
384
443
|
dependsOn?: string[],
|
|
385
444
|
options?: { retries?: number, timeout?: number, allow_fail?: boolean, critical?: boolean }
|
|
386
445
|
}
|
|
387
446
|
]
|
|
388
447
|
}]
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
448
|
+
features.update [product_tag, feature_tag, data: { name?: string, description?: string, steps?: array, envs?: array }]
|
|
449
|
+
features.fetch [product_tag, feature_tag]
|
|
450
|
+
features.fetchAll [product_tag]
|
|
451
|
+
features.delete [product_tag, feature_tag]
|
|
393
452
|
|
|
394
|
-
|
|
453
|
+
features.define [{
|
|
395
454
|
product?: string,
|
|
396
455
|
tag: string,
|
|
397
456
|
name: string,
|
|
@@ -406,7 +465,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
406
465
|
recordScenarios?: object[], // multiple recording scenarios for branching
|
|
407
466
|
branchOverrides?: object, // force step results during recording to reach later branches
|
|
408
467
|
handler: async (ctx) => {
|
|
409
|
-
// ctx.input – typed
|
|
468
|
+
// ctx.input – typed feature input
|
|
410
469
|
// ctx.step(tag, fn, rollback?, opts?) – define a durable step
|
|
411
470
|
// ctx.action.run({ app, event, input }) – call an app action
|
|
412
471
|
// ctx.database.query/insert/update/delete({ database, event, ... })
|
|
@@ -420,11 +479,11 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
420
479
|
// ctx.sleep(ms|"1h30m")
|
|
421
480
|
// ctx.waitForSignal("signal-name", { timeout? })
|
|
422
481
|
// ctx.setState(key, value) / ctx.getState(key)
|
|
423
|
-
// ctx.
|
|
482
|
+
// ctx.feature(childId, childTag, childInput) – child feature
|
|
424
483
|
}
|
|
425
484
|
}]
|
|
426
485
|
|
|
427
|
-
|
|
486
|
+
features.execute [{ ← CALL ductape_generate_payload FIRST (operation_family="features", method="execute", targets={tag}) to discover the input field shape
|
|
428
487
|
product: string,
|
|
429
488
|
env: string,
|
|
430
489
|
tag: string,
|
|
@@ -436,10 +495,10 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
436
495
|
timeout?: number
|
|
437
496
|
}]
|
|
438
497
|
|
|
439
|
-
|
|
498
|
+
features.dispatch [{ ← CALL ductape_generate_payload FIRST (operation_family="features", method="dispatch", targets={feature})
|
|
440
499
|
product: string,
|
|
441
500
|
env: string,
|
|
442
|
-
|
|
501
|
+
feature: string,
|
|
443
502
|
input: { fieldName: value },
|
|
444
503
|
schedule?: { start_at?: number|string, cron?: string, every?: number, limit?: number, endDate?: number|string, tz?: string },
|
|
445
504
|
session?: string,
|
|
@@ -447,18 +506,18 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
447
506
|
retries?: number
|
|
448
507
|
}]
|
|
449
508
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
509
|
+
features.signal [{ product, env, feature_id, signal: string, payload?: object }]
|
|
510
|
+
features.query [{ product, env, feature_id, query: string, params?: object }]
|
|
511
|
+
features.status [executionId: string]
|
|
512
|
+
features.cancel [executionId: string, reason?: string]
|
|
513
|
+
features.replay [executionId: string, options?: object]
|
|
514
|
+
features.restart [executionId: string]
|
|
515
|
+
features.resume [executionId: string]
|
|
516
|
+
features.replayFromStep [executionId: string, stepTag: string]
|
|
517
|
+
features.history [executionId: string]
|
|
518
|
+
features.stepDetail [executionId: string, stepTag: string]
|
|
519
|
+
features.relatedExecutions [executionId: string]
|
|
520
|
+
features.compare [executionId1: string, executionId2: string]
|
|
462
521
|
|
|
463
522
|
━━━ MODULE: caches ━━━
|
|
464
523
|
caches.create [{ product, tag, name, description?, type: "redis"|"memcached"|"in-memory", envs: [{slug, connection_url}] }]
|
|
@@ -507,10 +566,68 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
507
566
|
━━━ MODULE: logs ━━━
|
|
508
567
|
logs.init [product_tag?, app_tag?] (at least one required)
|
|
509
568
|
logs.fetch [{ product_id?, app_id?, env?, level?: "info"|"warn"|"error"|"debug", start_date?, end_date?, page?, limit? }]
|
|
569
|
+
|
|
570
|
+
━━━ TOOL: ductape_schema ━━━
|
|
571
|
+
Use this tool to dynamically discover the full field manifest for Ductape asset creation and update operations,
|
|
572
|
+
derived live from the SDK's Joi validators. No arguments are required to get everything.
|
|
573
|
+
|
|
574
|
+
Arguments:
|
|
575
|
+
module?: "app" | "product" (optional — omit to get both modules + all enums)
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
When module is omitted:
|
|
579
|
+
{
|
|
580
|
+
modules: {
|
|
581
|
+
app: { [method: string]: { fields: { [field]: FieldDef } } },
|
|
582
|
+
product: { [method: string]: { fields: { [field]: FieldDef } } }
|
|
583
|
+
},
|
|
584
|
+
enums: {
|
|
585
|
+
HttpMethods, AuthTypes, DataFormats, DataTypes, StatusCodes, TokenPeriods,
|
|
586
|
+
DatabaseTypes, AppComponents, ProductComponents, Categories, DefaultTypes,
|
|
587
|
+
EventTypes, InputsTypes, PublicStates
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
When module="app" or module="product":
|
|
592
|
+
{ module: string, methods: { [method]: { fields: { [field]: FieldDef } } }, enums: { ... } }
|
|
593
|
+
|
|
594
|
+
FieldDef shape:
|
|
595
|
+
{
|
|
596
|
+
type: string, // "string" | "number" | "boolean" | "object" | "array" | "alternatives" | "any"
|
|
597
|
+
required: boolean,
|
|
598
|
+
enum?: (string | number | boolean)[], // present when field is restricted to specific values
|
|
599
|
+
fields?: { [key]: FieldDef }, // present for nested objects
|
|
600
|
+
items?: FieldDef, // present for arrays (describes each element)
|
|
601
|
+
oneOf?: FieldDef[], // present for alternatives (.try(...))
|
|
602
|
+
minLength?: number, maxLength?: number, // string length constraints
|
|
603
|
+
min?: number, max?: number, // number range constraints
|
|
604
|
+
pattern?: string // string pattern regex
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
Example method keys for app module:
|
|
608
|
+
"create", "update", "environments.create", "environments.update",
|
|
609
|
+
"actions.create", "actions.update", "auths.create", "auths.update",
|
|
610
|
+
"variables.create", "variables.update", "constants.create", "constants.update",
|
|
611
|
+
"webhooks.create", "webhooks.update", "webhooks.events.create", "webhooks.events.update"
|
|
612
|
+
|
|
613
|
+
Example method keys for product module:
|
|
614
|
+
"create", "environments.create", "environments.update", "apps.add", "apps.update",
|
|
615
|
+
"databases.create", "databases.update", "notifications.create", "notifications.update",
|
|
616
|
+
"caches.create", "caches.update", "jobs.create", "jobs.update",
|
|
617
|
+
"messageBrokers.create", "messageBrokers.update", "fallbacks.create", "fallbacks.update",
|
|
618
|
+
"graphs.create", "graphs.update", "vectors.create", "vectors.update",
|
|
619
|
+
"sessions.create", "sessions.update", "healthchecks.create",
|
|
620
|
+
"quotas.create", "quotas.update", "functions.create", "functions.update",
|
|
621
|
+
"agents.create", "agents.update", "models.create", "models.update"
|
|
622
|
+
|
|
623
|
+
When to use this tool:
|
|
624
|
+
- Before calling ductape_execute to create or update any asset — use this to discover required fields
|
|
625
|
+
- To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
|
|
626
|
+
- To understand nested object shapes without reading SDK docs
|
|
510
627
|
`;
|
|
511
628
|
|
|
512
629
|
const executeInputSchema = z.object({
|
|
513
|
-
publishable_key: z.string().describe('
|
|
630
|
+
publishable_key: z.string().optional().describe('Your workspace publishable key. Omit if DUCTAPE_PUBLISHABLE_KEY is set in the MCP server env config.'),
|
|
514
631
|
module: z.enum(MODULES as [string, ...string[]]).describe(
|
|
515
632
|
'SDK module to target. Options: ' + MODULES.join(', ') + '.'
|
|
516
633
|
),
|
|
@@ -522,31 +639,57 @@ const executeInputSchema = z.object({
|
|
|
522
639
|
});
|
|
523
640
|
|
|
524
641
|
const payloadGenerateInputSchema = z.object({
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
642
|
+
publishable_key: z.string().optional().describe('Your workspace publishable key. Omit if DUCTAPE_PUBLISHABLE_KEY is set in the MCP server env config.'),
|
|
643
|
+
product_tag: z.string().describe('The product tag (e.g. "my-product"). Identifies which product configuration to read.'),
|
|
644
|
+
env_slug: z.string().describe('The environment slug (e.g. "dev", "prod"). Determines which env-specific values are used.'),
|
|
645
|
+
operation_family: z.string().describe(
|
|
646
|
+
'The operation category. One of: "action", "features", "database", "graph", "vector", "storage", ' +
|
|
647
|
+
'"notification", "messaging", "quota", "fallback", "healthcheck", "session", "cache".'
|
|
648
|
+
),
|
|
649
|
+
method: z.string().describe(
|
|
650
|
+
'The method within the operation family. E.g. "run", "dispatch", "execute", "start", "send", "produce", ' +
|
|
651
|
+
'"query", "insert", "update", "delete". Must match an allowed method for the given operation_family.'
|
|
652
|
+
),
|
|
653
|
+
targets: z.record(z.any()).optional().describe(
|
|
654
|
+
'Identifies the specific operation to generate a payload for. ' +
|
|
655
|
+
'For actions: { app: "app_tag", action: "action_tag" }. ' +
|
|
656
|
+
'For features: { feature: "feature_tag" }. ' +
|
|
657
|
+
'For databases: { database: "db_tag", action?: "action_tag" }. ' +
|
|
658
|
+
'For sessions: { session: "session_tag" }. ' +
|
|
659
|
+
'For notifications: { notification: "notif_tag" }. ' +
|
|
660
|
+
'For quotas/fallbacks: { tag: "resource_tag" }. ' +
|
|
661
|
+
'For storage: { storage: "storage_tag" }. ' +
|
|
662
|
+
'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'
|
|
663
|
+
),
|
|
664
|
+
include_session: z.boolean().optional().default(true).describe('Include session field in the generated payload template.'),
|
|
665
|
+
include_cache: z.boolean().optional().default(true).describe('Include cache field in the generated payload template.'),
|
|
666
|
+
schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe(
|
|
667
|
+
'"strict" — fail if any required field cannot be resolved. ' +
|
|
668
|
+
'"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'
|
|
669
|
+
),
|
|
670
|
+
input_hint: z.record(z.any()).optional().describe(
|
|
671
|
+
'Optional. Partial input values you already know. These are merged into the generated payload template ' +
|
|
672
|
+
'so the result is pre-filled. Use this to get a payload with known values already substituted.'
|
|
673
|
+
),
|
|
537
674
|
});
|
|
538
675
|
|
|
539
676
|
const snippetGenerateInputSchema = payloadGenerateInputSchema.extend({
|
|
540
677
|
language: z.enum(['typescript', 'python']).default('typescript'),
|
|
541
678
|
});
|
|
542
679
|
|
|
680
|
+
const schemaInputSchema = z.object({
|
|
681
|
+
module: z.enum(['app', 'product']).optional().describe(
|
|
682
|
+
'Optional. Scope the result to a single module ("app" or "product"). Omit to get the full manifest including enums.'
|
|
683
|
+
),
|
|
684
|
+
});
|
|
685
|
+
|
|
543
686
|
function toPrettyJson(value: unknown): string {
|
|
544
687
|
return JSON.stringify(value ?? {}, null, 2);
|
|
545
688
|
}
|
|
546
689
|
|
|
547
690
|
const ALLOWED_SNIPPET_METHODS: Record<string, string[]> = {
|
|
548
691
|
action: ['run', 'dispatch', 'execute'],
|
|
549
|
-
|
|
692
|
+
features: ['run', 'dispatch', 'execute'],
|
|
550
693
|
database: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'upsert', 'count', 'aggregate'],
|
|
551
694
|
graph: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'execute'],
|
|
552
695
|
vector: ['query', 'find', 'findSimilar', 'upsert', 'insert', 'delete', 'dispatch'],
|
|
@@ -580,7 +723,7 @@ function resolveSdkCallPath(operationFamily: string, method: string): string {
|
|
|
580
723
|
const m = String(method || '').toLowerCase();
|
|
581
724
|
|
|
582
725
|
if (family === 'action') return `actions.${m}`;
|
|
583
|
-
if (family === '
|
|
726
|
+
if (family === 'features' || family === 'feature') return `features.${m}`;
|
|
584
727
|
if (family === 'database') return m === 'dispatch' ? 'databases.dispatch' : `databases.${m}`;
|
|
585
728
|
if (family === 'graph') return m === 'dispatch' ? 'graph.dispatch' : `graph.${m}`;
|
|
586
729
|
if (family === 'vector') return `vector.${m}`;
|
|
@@ -708,17 +851,46 @@ async function loadMcpSdk(): Promise<{
|
|
|
708
851
|
process.exit(1);
|
|
709
852
|
}
|
|
710
853
|
|
|
854
|
+
function handleCliFlags(): boolean {
|
|
855
|
+
const arg = process.argv[2];
|
|
856
|
+
if (arg === '--version' || arg === '-v') {
|
|
857
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
858
|
+
const { version } = require('../package.json') as { version: string };
|
|
859
|
+
process.stdout.write(version + '\n');
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
if (arg === '--help' || arg === '-h') {
|
|
863
|
+
process.stdout.write(
|
|
864
|
+
'ductape-mcp — Ductape MCP server\n\n' +
|
|
865
|
+
'Usage: ductape-mcp\n\n' +
|
|
866
|
+
'The server communicates over stdio and is meant to be spawned by an MCP\n' +
|
|
867
|
+
'client (Cursor, Claude Desktop, Claude Code). Run it directly only to\n' +
|
|
868
|
+
'verify the binary is working.\n\n' +
|
|
869
|
+
'Options:\n' +
|
|
870
|
+
' --version, -v Print version and exit\n' +
|
|
871
|
+
' --help, -h Print this message and exit\n\n' +
|
|
872
|
+
'Environment:\n' +
|
|
873
|
+
' DUCTAPE_PUBLISHABLE_KEY Your workspace publishable key. Set this in\n' +
|
|
874
|
+
' the MCP client env config to avoid passing\n' +
|
|
875
|
+
' publishable_key on every tool call.\n',
|
|
876
|
+
);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
711
882
|
async function main() {
|
|
712
883
|
const { McpServer, StdioServerTransport } = await loadMcpSdk();
|
|
713
884
|
const server = new McpServer({ name: 'ductape-mcp', version: '0.1.0' });
|
|
714
885
|
const transport = new StdioServerTransport();
|
|
715
886
|
|
|
716
|
-
const executeHandler = async (args: { publishable_key
|
|
887
|
+
const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
|
|
717
888
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
889
|
+
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
890
|
+
if (!key) {
|
|
891
|
+
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
720
892
|
}
|
|
721
|
-
const result = await executeViaProxy(
|
|
893
|
+
const result = await executeViaProxy(key, args.module, args.method, args.params);
|
|
722
894
|
return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
|
|
723
895
|
} catch (err) {
|
|
724
896
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -730,7 +902,11 @@ async function main() {
|
|
|
730
902
|
args: z.infer<typeof payloadGenerateInputSchema>,
|
|
731
903
|
) => {
|
|
732
904
|
try {
|
|
733
|
-
const
|
|
905
|
+
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
906
|
+
if (!key) {
|
|
907
|
+
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
908
|
+
}
|
|
909
|
+
const result = await generateExecutablePayload({ ...args, publishable_key: key });
|
|
734
910
|
return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
|
|
735
911
|
} catch (err) {
|
|
736
912
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -742,8 +918,12 @@ async function main() {
|
|
|
742
918
|
args: z.infer<typeof snippetGenerateInputSchema>,
|
|
743
919
|
) => {
|
|
744
920
|
try {
|
|
921
|
+
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
922
|
+
if (!key) {
|
|
923
|
+
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
924
|
+
}
|
|
745
925
|
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
746
|
-
const generated = await generateExecutablePayload(args);
|
|
926
|
+
const generated = await generateExecutablePayload({ ...args, publishable_key: key });
|
|
747
927
|
const payload = (generated as any)?.payload ?? {};
|
|
748
928
|
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
749
929
|
return {
|
|
@@ -767,12 +947,28 @@ async function main() {
|
|
|
767
947
|
}
|
|
768
948
|
};
|
|
769
949
|
|
|
950
|
+
const schemaHandler = async (args: { module?: 'app' | 'product' }) => {
|
|
951
|
+
try {
|
|
952
|
+
const data = await getAssetSchemas(args.module);
|
|
953
|
+
return { content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }] };
|
|
954
|
+
} catch (err) {
|
|
955
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
956
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
770
960
|
if (typeof server.registerTool === 'function') {
|
|
771
961
|
server.registerTool(
|
|
772
962
|
'ductape_execute',
|
|
773
963
|
{
|
|
774
964
|
title: 'Ductape SDK Execute',
|
|
775
|
-
description:
|
|
965
|
+
description:
|
|
966
|
+
'Execute a Ductape SDK operation via the backend proxy.\n\n' +
|
|
967
|
+
'IMPORTANT — two-step rule for runtime operations (run, dispatch, execute, start, send, produce, query, insert, update, delete):\n' +
|
|
968
|
+
' 1. Call ductape_generate_payload first to get the canonical payload template for the specific operation.\n' +
|
|
969
|
+
' This reveals the exact "input" field keys and their types — they are product/env/operation-specific.\n' +
|
|
970
|
+
' 2. Fill in the values from the template, then call ductape_execute.\n\n' +
|
|
971
|
+
'For asset creation/update operations (create, update, add), use ductape_schema to discover required fields instead.',
|
|
776
972
|
inputSchema: executeInputSchema,
|
|
777
973
|
},
|
|
778
974
|
executeHandler,
|
|
@@ -781,7 +977,14 @@ async function main() {
|
|
|
781
977
|
'ductape_generate_payload',
|
|
782
978
|
{
|
|
783
979
|
title: 'Ductape Payload Generator',
|
|
784
|
-
description:
|
|
980
|
+
description:
|
|
981
|
+
'Generate the canonical input payload template for a runtime SDK operation.\n\n' +
|
|
982
|
+
'CALL THIS BEFORE ductape_execute when you need to run, dispatch, execute, start, send, produce, ' +
|
|
983
|
+
'or otherwise trigger any Ductape operation that takes an "input" field.\n\n' +
|
|
984
|
+
'The "input" field shape is defined by how each action/feature/session/quota/etc. was configured ' +
|
|
985
|
+
'in the product — it cannot be inferred from the SDK schema alone. This tool returns the exact ' +
|
|
986
|
+
'field names, types, and placeholder values for that specific operation in that specific environment.\n\n' +
|
|
987
|
+
'Returns: { payload: { product, env, input: { fieldName: placeholder, ... }, session?, cache? }, meta: { ... } }',
|
|
785
988
|
inputSchema: payloadGenerateInputSchema,
|
|
786
989
|
},
|
|
787
990
|
payloadGenerateHandler,
|
|
@@ -795,10 +998,24 @@ async function main() {
|
|
|
795
998
|
},
|
|
796
999
|
snippetGenerateHandler,
|
|
797
1000
|
);
|
|
1001
|
+
server.registerTool(
|
|
1002
|
+
'ductape_schema',
|
|
1003
|
+
{
|
|
1004
|
+
title: 'Ductape Asset Schema',
|
|
1005
|
+
description:
|
|
1006
|
+
'Returns the full field manifest for Ductape asset creation/update methods, ' +
|
|
1007
|
+
'derived live from the SDK Joi validators. Includes field types, required flags, ' +
|
|
1008
|
+
'enum values, nested structures, and all enum constants. ' +
|
|
1009
|
+
'Pass module="app" or module="product" to scope the result.',
|
|
1010
|
+
inputSchema: schemaInputSchema,
|
|
1011
|
+
},
|
|
1012
|
+
schemaHandler,
|
|
1013
|
+
);
|
|
798
1014
|
} else if (typeof server.tool === 'function') {
|
|
799
1015
|
server.tool('ductape_execute', executeInputSchema.shape, executeHandler);
|
|
800
1016
|
server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
|
|
801
1017
|
server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
|
|
1018
|
+
server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
|
|
802
1019
|
} else {
|
|
803
1020
|
console.error('MCP server does not expose .registerTool() or .tool()');
|
|
804
1021
|
process.exit(1);
|
|
@@ -807,7 +1024,9 @@ async function main() {
|
|
|
807
1024
|
await server.connect(transport);
|
|
808
1025
|
}
|
|
809
1026
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1027
|
+
if (!handleCliFlags()) {
|
|
1028
|
+
main().catch((err) => {
|
|
1029
|
+
console.error(err);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
});
|
|
1032
|
+
}
|