@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/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
- * Requires passing a `publishable_key` with every `ductape_execute` payload.
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, "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? } }]
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
- input: { // declare the input fields this quota expects
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
- input: { "body:field": "$Input{fieldName}" }, // maps quota input → action input
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
- input: { // input fields this fallback expects
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"|"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 }]
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: { 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? } }]
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: workflow ━━━
362
- workflow.create [product_tag, data: {
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"|"workflow"|"sleep"|"wait_signal",
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
- workflow?: string, // for type=workflow (child)
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
- 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]
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
- workflow.define [{
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 workflow input
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.workflow(childId, childTag, childInput) – child workflow
482
+ // ctx.feature(childId, childTag, childInput) – child feature
424
483
  }
425
484
  }]
426
485
 
427
- workflow.execute [{
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
- workflow.dispatch [{
498
+ features.dispatch [{ ← CALL ductape_generate_payload FIRST (operation_family="features", method="dispatch", targets={feature})
440
499
  product: string,
441
500
  env: string,
442
- workflow: string,
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
- 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]
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('The publishable key for your workspace. Required for authentication.'),
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
- 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(),
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
- workflow: ['run', 'dispatch', 'execute'],
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 === 'workflow') return `workflow.${m}`;
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: string; module: SDKModule; method: string; params: unknown[] }) => {
887
+ const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
717
888
  try {
718
- if (!args.publishable_key) {
719
- throw new Error('Not authenticated. Please provide the `publishable_key` in your tool invocation arguments.');
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(args.publishable_key, args.module, args.method, args.params);
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 result = await generateExecutablePayload(args);
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: 'Execute a Ductape SDK operation via the backend proxy (databases, graph, storage, etc.)',
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: 'Generate canonical executable payload templates and schema metadata for SDK code snippet generation',
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
- main().catch((err) => {
811
- console.error(err);
812
- process.exit(1);
813
- });
1027
+ if (!handleCliFlags()) {
1028
+ main().catch((err) => {
1029
+ console.error(err);
1030
+ process.exit(1);
1031
+ });
1032
+ }