@ductape/mcp 0.2.21 → 0.2.23

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.js CHANGED
@@ -90,12 +90,15 @@ There are THREE categories of operations. Use the right tool for each:
90
90
  These require an access key and CANNOT be done via ductape_execute (publishable key only).
91
91
  → Use ductape_cli instead. Examples:
92
92
  ductape_cli("products list")
93
- ductape_cli("products create --name \\"My Product\\" --tag my-product")
93
+ ductape_cli("products create -f product.json") # body: { name, tag?, envs?: [...] } — no --name/--tag flags exist, only -f <file.json> or interactive TTY
94
94
  ductape_cli("cloud connections list")
95
95
  ductape_cli("link --product my-product --env dev")
96
96
  If the CLI is not installed, ductape_cli will return install instructions automatically.
97
- NOTE: App actions are configured in the Workbench UI — there is no CLI command for them.
98
97
  Environments DO have CLI commands: ductape_cli("products environments list/get/create/update ...").
98
+ App actions (endpoints) also have CLI commands: ductape_cli("apps actions list/get/create/update/delete
99
+ --app <app_tag> ..."). When the user wants to integrate a new endpoint, call
100
+ ductape_integrate_endpoint first — it returns the full ordered flow (choose/create the app,
101
+ define the action, connect the app to the product) before any code should be written.
99
102
 
100
103
  RESOLVING "No linked project" ERRORS:
101
104
  Some commands (declarative sync below, db migrate/schema, products environments *) need a
@@ -194,16 +197,27 @@ There are THREE categories of operations. Use the right tool for each:
194
197
  1. ASSET CREATION / UPDATE (create, update, add, register… for ANY asset type)
195
198
  ALL creation and update operations require an access key and CANNOT go through ductape_execute.
196
199
  → Use ductape_cli for every create/update operation. Examples:
197
- ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
200
+ ductape_cli("apps create -f app.json") # no --name/--description flags exist, only -f <file.json> or interactive TTY. body: { app_name, description, tag, envs?: [{ env_name, slug, description, base_url, active }] } — tag is REQUIRED.
198
201
  ductape_cli("apps list")
199
202
  ductape_cli("resources storage list")
200
203
  ductape_cli("resources database create -f db-config.json")
201
204
  This applies to: products, apps, and resources (databases, storage, caches, etc.),
202
205
  cloud connections, and secrets. Environments have their own CLI commands (see below);
203
- App actions and auths are configured in the Workbench UI. Quotas, fallbacks, jobs, and
206
+ app actions have their own CLI commands too ductape_cli("apps actions create --app <tag>
207
+ --action-file action.json") (NOT -f — that flag is claimed by the parent "apps" command, so
208
+ Commander resolves it against the ancestor and this subcommand never sees it; same fix as
209
+ "products environments create/update" uses --env-file for the identical reason). Body: {
210
+ tag, name, resource, method, request_type, description?, params?, query?, headers?, body?,
211
+ response? }, where resource is a path RELATIVE to each of the app's environment base_urls
212
+ (e.g. "/v1/users/{id}"), not a full URL, and request_type is REQUIRED — one of
213
+ "application/json" | "application/x-www-form-urlencoded" | "multipart/form-data" | "SOAP" |
214
+ "html" (not the short names "json"/"xml"/"form"). Auths are still configured
215
+ in the Workbench UI. Quotas, fallbacks, jobs, and
204
216
  healthchecks are administrative resources managed with ductape_cli("resources <type> ...").
205
217
  Features have no CLI create command because their definitions are code-first through
206
- features.define.
218
+ features.define. They DO have a CLI persistence command — ductape_cli("features sync") —
219
+ which runs the project's own "features:sync" npm script to register ductape/features/
220
+ definitions against the live product; see the DEPLOYING features.define CALLS section below.
207
221
 
208
222
  ⚠ MULTI-ENV REQUIREMENT — applies to ALL product assets (storage, database, cache,
209
223
  messageBroker, graph, vector, and any other resource with an envs array):
@@ -468,8 +482,26 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
468
482
  app.init [app_tag]
469
483
 
470
484
  ━━━ MODULE: actions (app actions) ━━━
471
- 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" } }]
472
- 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 } }]
485
+ These are admin ops go through ductape_cli, not ductape_execute:
486
+ ductape_cli("apps actions create --app <app_tag> --action-file action.json")
487
+ ductape_cli("apps actions update --app <app_tag> --action <action_tag> --action-file action.json")
488
+ ductape_cli("apps actions list --app <app_tag> --json")
489
+ ductape_cli("apps actions get --app <app_tag> --action <action_tag> --json")
490
+ ductape_cli("apps actions delete --app <app_tag> --action <action_tag>")
491
+ NOTE: the flag is --action-file, NOT -f/--file — that flag belongs to the parent "apps" command
492
+ (used by "apps create"/"apps update"), and Commander resolves a flag shared by an ancestor and a
493
+ descendant against the ancestor, so -f here would silently never reach this subcommand. Verified
494
+ live: passing -f prints "required option '-f, --file <path>' not specified" even though it was
495
+ supplied; --action-file is the fix (same pattern already used by "products environments
496
+ create/update" with --env-file for the identical collision).
497
+ resource is a path RELATIVE to each of the app's environment base_urls (e.g. "/v1/users/{id}"),
498
+ never a full URL — Ductape joins base_url + resource per environment at call time.
499
+ request_type is REQUIRED (verified live) — one of "application/json" |
500
+ "application/x-www-form-urlencoded" | "multipart/form-data" | "SOAP" | "html". response.status_code
501
+ is a STRING enum of standard HTTP status codes (e.g. "200", not the number 200) and
502
+ response.response_format uses the SAME enum as request_type above (not "json"/"xml"/"form").
503
+ actions.create [app_tag, data: { tag: string, name: string, resource: string, method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", request_type: "application/json"|"application/x-www-form-urlencoded"|"multipart/form-data"|"SOAP"|"html", description?: string, body?: object, params?: object, query?: object, headers?: object, response?: { name?: string, status_code: string, success: boolean, body: object, response_format: "application/json"|"application/x-www-form-urlencoded"|"multipart/form-data"|"SOAP"|"html" } }]
504
+ actions.update [app_tag, action_tag, data: { resource?: string, method?: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", description?: string, request_type?: "application/json"|"application/x-www-form-urlencoded"|"multipart/form-data"|"SOAP"|"html", body?: object, query?: object, params?: object, headers?: object, response?: { name?: string, success: boolean, body: object, response_format: "application/json"|"application/x-www-form-urlencoded"|"multipart/form-data"|"SOAP"|"html", status_code: string } }]
473
505
  actions.fetch [app_tag, action_tag]
474
506
  actions.list [app_tag]
475
507
  actions.run [{ product, env, app, action, input: { "body:fieldName": value, ... } }] ← CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
@@ -1110,6 +1142,11 @@ const marketplaceConnectInputSchema = z.object({
1110
1142
  const marketplaceInspectInputSchema = z.object({
1111
1143
  app_tag: z.string().min(1).describe('Exact public app tag returned by ductape_marketplace_discover.'),
1112
1144
  });
1145
+ const integrateEndpointInputSchema = z.object({
1146
+ product_tag: z.string().min(1).optional().describe('Target product tag, if already known. Omit if the user has not said which product this is for yet.'),
1147
+ app_tag: z.string().min(1).optional().describe('Existing app tag, if the user already said which app this endpoint belongs to. Omit when creating ' +
1148
+ 'a new app, or when the app has not been chosen yet — the returned flow starts by asking.'),
1149
+ });
1113
1150
  function toPrettyJson(value) {
1114
1151
  return JSON.stringify(value ?? {}, null, 2);
1115
1152
  }
@@ -1679,6 +1716,15 @@ PRIMITIVES-FIRST CAPABILITY EXTRACTION — REQUIRED FOR EVERY MIGRATION SLICE
1679
1716
  5. For each Function, record pure/WASM-candidate versus framework-dependent classification.
1680
1717
  6. Call ductape_function_setup and implement verified local plus remote availability.
1681
1718
 
1719
+ Persist migrated Features the same way as newly designed ones: define every Feature under
1720
+ ductape/features/ (e.g. ductape/features/src/my-feature.ts), never inline in the app's normal
1721
+ startup path. Register only local function/operation handlers (ductape.sdk.functions.register(...))
1722
+ at boot — no network call. Persist Feature definitions to the live product with
1723
+ "ductape features sync" (ductape_cli("features sync")), which runs the project's own
1724
+ "features:sync" npm script — never automatically on app boot, the same way a migrated database
1725
+ schema is applied via "ductape db migrate" rather than at startup. See "DEPLOYING features.define
1726
+ CALLS" below for the full convention and required "features:sync" script shape.
1727
+
1682
1728
  Never create a Function that merely hides database, session, Events, storage, notification, graph,
1683
1729
  vector, quota, fallback, healthcheck, cache, secret, or connected-App work that the Feature can
1684
1730
  express directly. Never fragment an original atomic transaction just to maximize primitive count.
@@ -1742,6 +1788,17 @@ ENVIRONMENTS
1742
1788
  Report matched, missing, extra, and ambiguous normalized aliases. Never guess an ambiguous mapping.
1743
1789
  Environment mutation is administrative CLI work and must never be routed through ductape_execute.
1744
1790
 
1791
+ INACTIVE ENVIRONMENTS (e.g. an auto-created "snd"): whether an inactive environment needs a real
1792
+ physical provider resource behind it, or can stay a placeholder with no live infrastructure, is
1793
+ not settled platform behavior — do not assume either way. Resource-create payloads that require
1794
+ complete environment coverage (see above) still need an entry for every environment returned by
1795
+ "products environments list", active or not; whether that entry must point at a real backing
1796
+ resource for an inactive env is a product-owner decision to surface explicitly, not infer.
1797
+ Separately: "products environments update <product> <slug>" with {"active":false} may report
1798
+ "updated": true without the change actually persisting — confirmed via independent read-back,
1799
+ not just replication lag. If deactivating an environment is important, verify with a follow-up
1800
+ "products environments get" rather than trusting the update response.
1801
+
1745
1802
  PRODUCT AND ASSET BOOTSTRAP
1746
1803
  Product creation is idempotent: fetch by tag, create only when absent, then link the destination.
1747
1804
  Use ductape_cli for products, apps, resources, cloud connections, secrets, apply, and migrations.
@@ -1798,6 +1855,8 @@ COMPONENT DECISIONS
1798
1855
  Pinecone/Qdrant/Weaviate/OpenSearch → Vector; JWT/session middleware → Sessions.
1799
1856
  Named reusable product capabilities with a stable managed-execution boundary → code-first Features,
1800
1857
  including synchronous multi-step capabilities and durable/scheduled/signal-driven orchestration.
1858
+ Define Features under ductape/features/ and persist them via "ductape features sync", never at
1859
+ app boot — see PRIMITIVES-FIRST CAPABILITY EXTRACTION above.
1801
1860
  Retries, health checks, fallbacks, quotas, circuit breakers → Resilience.
1802
1861
  Keep low-level deterministic rules as ordinary domain functions when they do not form a useful
1803
1862
  independent capability boundary. Wrap or compose them into Features when the combined operation
@@ -1841,8 +1900,10 @@ AI EDITING STANDARD
1841
1900
  rules in code, while allowing a coherent synchronous capability composed from those rules to be a Feature.
1842
1901
  Immediate work propagates the full session; durable work uses approved actor metadata or system context.
1843
1902
  Consumers are idempotent, retry ownership is singular and bounded, and external effects are not duplicated.
1844
- After each slice: format, build, test, rescan, reconcile assets through ductape_cli, smoke-test in snd,
1845
- and report unresolved findings. A clean scanner result alone never proves a correct migration.
1903
+ After each slice: format, build, test, rescan, persist any migrated Feature definitions via
1904
+ ductape_cli("features sync") (never automatically on app boot), reconcile assets through
1905
+ ductape_cli, smoke-test in snd, and report unresolved findings. A clean scanner result alone
1906
+ never proves a correct migration.
1846
1907
 
1847
1908
  CONTEXTUAL FILE REVIEW PROTOCOL
1848
1909
  Use review_queue as a coverage aid. Review relevant files individually, starting with repository
@@ -2695,6 +2756,18 @@ CONNECT A DISCOVERED APP THROUGH MCP:
2695
2756
  Never guess environment mappings or action tags. Connecting mutates product configuration;
2696
2757
  obtain user approval when the user has not already requested the connection.
2697
2758
 
2759
+ CONFIRMED LIMITATION (2026-08-17): both ductape_marketplace_connect and the equivalent
2760
+ ductape_cli("products apps connect --product <tag> --app <app-tag> --env-map ...") reject any
2761
+ app that is not marked public in the marketplace, with error
2762
+ App "<tag>" is not public in the marketplace. This includes apps a user creates themselves under
2763
+ their own tag (e.g. a private "ductape:paystack") — private/workspace-owned apps CANNOT be
2764
+ connected to a product through MCP or CLI at all, only through Workbench, even though
2765
+ ductape_marketplace_inspect can still read a private app's full action catalogue (inspect and
2766
+ connect have different visibility rules). If you hit this, do not keep retrying — tell the user
2767
+ the app must be connected via Workbench, and once they confirm it's connected, verify with
2768
+ ductape_cli("products apps list --product <product_tag> --json") rather than attempting the
2769
+ connect call again.
2770
+
2698
2771
  ONLY after all five steps can any code call:
2699
2772
  ctx.api.run({ app: '<app_tag>', action: '<action_tag>', input: { ... } }) ← in a feature handler
2700
2773
  actions.run([{ product, env, app: '<app_tag>', action: '<action_tag>', input }]) ← at runtime
@@ -2712,8 +2785,12 @@ If a feature step needs to call an external service and no App is registered for
2712
2785
  An app is a versioned API integration definition. It contains environments (base URLs), actions
2713
2786
  (individual endpoint specs), auth schemes, webhooks, variables, and constants.
2714
2787
 
2715
- Create an app (admin ductape_cli):
2716
- ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
2788
+ Call ductape_integrate_endpoint for the full ordered flow (choose/create the app, define the
2789
+ action, connect it to the product) before writing the ctx.api.run call below.
2790
+
2791
+ Create an app (admin — ductape_cli; no --name/--description flags exist, only -f <file.json> or
2792
+ interactive TTY):
2793
+ ductape_cli("apps create -f app.json") # body: { app_name, description, tag, envs?: [...] } — tag is REQUIRED
2717
2794
  ductape_cli("app.init", ["app_tag"]) → loads app into builder state
2718
2795
 
2719
2796
  Import from a file:
@@ -2794,15 +2871,27 @@ Connecting an app to a product (after creation):
2794
2871
  NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
2795
2872
  Use ductape_cli for all product-level operations.
2796
2873
 
2797
- FULL FLOW to make an app callable from a product:
2798
- 1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
2799
- 2. Add environments: Workbench
2800
- 3. Configure auth: Workbench
2801
- 4. Define actions: Workbench
2874
+ FULL FLOW to make an app callable from a product — call ductape_integrate_endpoint for the
2875
+ ordered version of this with exact ask-the-user points; summary (all verified live):
2876
+ 1. Create the app: ductape_cli("apps create -f app.json")
2877
+ body: { app_name, description, tag, envs: [{ env_name, slug, description,
2878
+ base_url, active }, ...] } — tag is REQUIRED; every active env needs a base_url.
2879
+ No --name/--description flags exist, only -f <file.json> or interactive TTY.
2880
+ 2. Add environments: Part of step 1's envs array, or later via ductape_cli("apps update --tag
2881
+ <app_tag> --proxy -f app.json") with an updated envs array.
2882
+ 3. Configure auth: Workbench (no CLI command).
2883
+ 4. Define actions: ductape_cli("apps actions create --app <app_tag> --action-file action.json")
2884
+ body: { tag, name, resource, method, request_type, description?, params?,
2885
+ query?, headers?, body?, response? } — request_type is REQUIRED, one of
2886
+ "application/json" | "application/x-www-form-urlencoded" |
2887
+ "multipart/form-data" | "SOAP" | "html" (not "json"/"xml"/"form"); resource
2888
+ is relative to each env's base_url, never a full URL.
2802
2889
  OR import: ductape_cli("apps import <file.json> -t postman|openapi")
2803
- 5. Connect to product: Requires the product_id.
2804
- There is no CLI command for this step — the SDK product.apps.add method requires
2805
- an access key which only the backend can provide. Connect via Workbench.
2890
+ 5. Connect to product: ductape_cli("products apps connect --product <product_tag> --app <app_tag>
2891
+ --env-map <app_env>:<product_env> [repeat --env-map per environment] --json")
2892
+ Requires the app to be public in the marketplace OR owned by the same
2893
+ workspace as the product. Does the full connect + environment-mapping
2894
+ sequence in one call.
2806
2895
  6. Verify: ductape_cli("products apps list --product <product_tag> --json")
2807
2896
  ductape_cli("products apps actions list --product <product_tag> --app <app_tag> --json")
2808
2897
  `.trim(),
@@ -3079,6 +3168,23 @@ TAG AND SMTP RULES
3079
3168
  emails.smtp.secure is a boolean and cannot be a $Secret{...} string.
3080
3169
  Credential strings such as auth.user and auth.pass may use $Secret{...}.
3081
3170
 
3171
+ notifications messages create/update/get had NO --product override before CLI 0.3.17 — it
3172
+ silently resolved the product from whatever is linked in the current working directory, with
3173
+ no way to override. Confirmed root cause of a real multi-hour misdiagnosis: it kept resolving
3174
+ to a different product than intended, and the resulting "Notification X not found" error never
3175
+ named which product it actually checked. If seeing this on an older CLI, verify which project
3176
+ is linked in cwd before assuming the notification component itself is missing.
3177
+
3178
+ SendGrid field names (confirmed against the live Joi validator, not just this doc — cross-check
3179
+ the SDK source when in doubt): { provider: "sendgrid", sendgrid: { apiKey: "$Secret{...}",
3180
+ sender_email: "$Secret{...}" } }. The field is sender_email, not sender — "sender" is silently
3181
+ rejected with a required-field error naming sender_email specifically, not a helpful diff.
3182
+
3183
+ Nexmo/Vonage SMS field names: { provider: "nexmo", apiKey: "$Secret{...}",
3184
+ apiSecret: "$Secret{...}", sender: "YourSenderId" }. The credential fields are apiKey/apiSecret
3185
+ — NOT accountSid (that is a Twilio-only field name and is rejected with "not allowed" if used
3186
+ for nexmo).
3187
+
3082
3188
  Send at runtime (one channel at a time):
3083
3189
  → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
3084
3190
  notifications.email.send [{ product, env, notification, input: { recipients, subject?, template? } }]
@@ -3130,6 +3236,16 @@ FIREBASE THROUGH A GCP CLOUD CONNECTION
3130
3236
  intent; creating a notification does not prove Google-side FCM permission. Runtime delivery
3131
3237
  requires fcm.googleapis.com and roles/firebasecloudmessaging.admin, so validate the connection
3132
3238
  and perform a delivery test.
3239
+
3240
+ CONFIRMED CONTRADICTION (2026-08-17): despite the "never put the private key in the file"
3241
+ guidance above, resources notifications create with authMode: "cloud_connection" currently
3242
+ rejects the payload progressively demanding envs[].push_notifications.credentials.project_id,
3243
+ then .private_key_id, then .client_email, then finally the raw .private_key itself — the full
3244
+ manual service-account shape, defeating the documented purpose of cloud_connection mode. Do NOT
3245
+ fabricate a placeholder private_key to satisfy this — that field is genuinely sensitive and a
3246
+ fake value risks being mistaken for real credential material later. If you hit this, drop the
3247
+ push_notifications block and register email/SMS only until this is fixed, and tell the user
3248
+ explicitly rather than working around it with fake data.
3133
3249
  Notification tag and message tag are ALWAYS passed together as "notification_tag:message_tag".
3134
3250
  `.trim(),
3135
3251
  resilience: `
@@ -3660,6 +3776,55 @@ STEP 6 — WRITE the feature into the project codebase
3660
3776
  - Write rollback handlers inline as the third argument to ctx.step()
3661
3777
  - Return a plain object as the feature's output
3662
3778
 
3779
+ ━━━ DEPLOYING features.define CALLS — DO NOT run them on every app boot ━━━
3780
+
3781
+ features.define() has a network-bound RECORDING PHASE (see FEATURE RECORDING SEMANTICS below) that
3782
+ calls the live Ductape API to compile and register the Feature's step graph. This is administrative
3783
+ configuration work, not a request-serving concern — treat it exactly like a database migration:
3784
+ defined in source, but APPLIED via an explicit, separate command, never automatically on every
3785
+ process start.
3786
+
3787
+ Calling features.define() from inside a framework's normal startup lifecycle hook (NestJS
3788
+ onModuleInit, Express app bootstrap, a Lambda cold-start path, etc.) makes ordinary app startup
3789
+ depend on Ductape API reachability and latency for every single boot/restart/replica — including
3790
+ local dev restarts, autoscaling events, and health-check-triggered restarts. A slow or unreachable
3791
+ Ductape API then blocks the app from ever starting to serve requests, even for routes that have
3792
+ nothing to do with the affected Feature.
3793
+
3794
+ REQUIRED CONVENTION — this is what "ductape features sync" (CLI) expects:
3795
+ 1. Put every Feature definition under ductape/features/ (e.g. ductape/features/src/my-feature.ts),
3796
+ each calling ductape.feature.define({ ... }) from a registerXFeature(ductape) export. This
3797
+ mirrors ductape/database/migrations/ — a fixed, discoverable location for Ductape-managed
3798
+ source, regardless of the surrounding project structure.
3799
+ 2. In the app's normal startup path, register ONLY local function/operation handlers
3800
+ (ductape.sdk.functions.register(...)) — no network call, safe on every boot. Never call
3801
+ features.define(...) here.
3802
+ 3. Add a "features:sync" script to package.json. It owns booting just enough of the app to
3803
+ construct real dependencies (in NestJS, use NestFactory.createApplicationContext(module) —
3804
+ same DI container, no HTTP listener) and calling every registerXFeature(...) once, then
3805
+ exiting. Ductape cannot run this step for you generically — a Feature handler is real
3806
+ application code that typically depends on the app's own services, unlike a migration file,
3807
+ which is declarative data the Ductape backend can apply directly.
3808
+ 4. Persist Features by running "ductape features sync" (or ductape_cli("features sync")), which
3809
+ finds the linked project and runs its "features:sync" script — never automatically on boot.
3810
+ Pass an optional filter argument to scope it: "ductape features sync payments".
3811
+
3812
+ A NestJS service with both concerns split looks like:
3813
+ async onModuleInit() {
3814
+ this.ductape.sdk.functions.register({ ...myFunctions, operations: { ... /* local handlers */ } });
3815
+ }
3816
+ async syncFeatures(): Promise<void> {
3817
+ await registerMyFeature(this.ductape.sdk); // the features.define(...) call
3818
+ }
3819
+ The project's own "features:sync" script resolves each service with a syncFeatures() method via a
3820
+ headless application context and calls it — mirroring how a migration runner applies pending
3821
+ migrations explicitly. "ductape features sync" is a thin, framework-agnostic front door onto that
3822
+ script; it does not itself execute application code.
3823
+
3824
+ This applies to any framework, not just NestJS — the only requirement is that whatever construct
3825
+ the app boots FOR REQUESTS never itself calls features.define(); the project's own "features:sync"
3826
+ script does, invoked only through the explicit CLI command.
3827
+
3663
3828
  STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
3664
3829
  Branch on step result (early return in handler):
3665
3830
  → Add branchOverrides: { stepTag: { field: value } } so all branches are recorded
@@ -4888,7 +5053,14 @@ EMBEDDING EXECUTION — CURRENT CODE-FIRST CONTRACT:
4888
5053
  generated.embeddings.some(v => v.length !== EXPECTED_VECTOR_DIMENSIONS)) {
4889
5054
  throw new Error('EMBEDDING_DIMENSION_MISMATCH');
4890
5055
  }
4891
- Register a real local handler at application bootstrap or configure a signed HTTPS/events transport.
5056
+ Register a real local handler at application bootstrap or configure gRPC-mTLS, Events, or signed HTTPS.
5057
+ Remote resolution order is gRPC-mTLS, Events, then signed HTTPS. A gRPC transport is
5058
+ { type:'grpc', endpoint:'host:443', service:'ductape.functions.v1.PortableFunctions',
5059
+ method:'Invoke', authentication:'mtls', tls:{ ca_env, cert_env, key_env } }.
5060
+ Never write PEM, certificates, private keys, tokens, or access keys into function JSON. Set
5061
+ DUCTAPE_FUNCTION_GRPC_ENDPOINT and referenced TLS variables in the application runtime. The gRPC
5062
+ adapter must pool channels. Only UNAVAILABLE/deadline failures may fall back; auth, correlation,
5063
+ schema, and application failures must not execute again over another transport.
4892
5064
  Input and output JSON schemas are validated at runtime. Missing implementation fails with
4893
5065
  FUNCTION_UNAVAILABLE; schema mismatch fails with FUNCTION_SCHEMA_VALIDATION_FAILED; timeout fails
4894
5066
  with FUNCTION_TIMEOUT. Never substitute sample/random/recording-time embeddings. The Feature's
@@ -5042,6 +5214,113 @@ const portableFunctionSetupHandler = async (args) => {
5042
5214
  ],
5043
5215
  }, null, 2) }] };
5044
5216
  };
5217
+ // This tool only derives a step-by-step plan from its arguments and read-only lookups the calling
5218
+ // agent performs itself via other tools. It runs no CLI command and writes nothing.
5219
+ const integrateEndpointHandler = async (args) => {
5220
+ const hasApp = Boolean(args.app_tag);
5221
+ const hasProduct = Boolean(args.product_tag);
5222
+ const stages = [];
5223
+ if (!hasApp) {
5224
+ stages.push({
5225
+ stage: 0,
5226
+ title: 'Choose the app',
5227
+ ask_user: 'Should this endpoint be added to an existing app, or do you want to create a new app for it?',
5228
+ if_existing_app: [
5229
+ 'Get the exact app tag. If unsure, call ductape_cli("apps list --json"), or ' +
5230
+ 'ductape_marketplace_discover/ductape_marketplace_inspect if it might be a public marketplace app.',
5231
+ 'Re-call this tool with that app_tag once known — the returned flow will skip straight to stage 2.',
5232
+ ],
5233
+ if_new_app: 'Continue to stage 1.',
5234
+ });
5235
+ }
5236
+ if (!hasApp) {
5237
+ stages.push({
5238
+ stage: 1,
5239
+ title: 'Create the app (new app path only — skip if using an existing app)',
5240
+ ask_user: [
5241
+ 'App name and a short description.',
5242
+ 'What environments does this app need (e.g. dev, staging, production), and what is the base ' +
5243
+ 'URL for each? At least one environment is required. Every ACTIVE environment must have a ' +
5244
+ 'base_url before the action in stage 2 can be called against it.',
5245
+ ],
5246
+ then: [
5247
+ 'Write a JSON body: { app_name, description, tag, envs: [{ env_name, slug, description, ' +
5248
+ 'base_url, active: true }, ...] }. tag is REQUIRED — derive a slug from app_name if the ' +
5249
+ 'user has not given one explicitly.',
5250
+ 'Run: ductape_cli("apps create -f <path-to-body.json> --json")',
5251
+ 'The app tag is what you supplied, not something the response generates — use it as app_tag ' +
5252
+ 'in every later stage.',
5253
+ ],
5254
+ });
5255
+ }
5256
+ stages.push({
5257
+ stage: 2,
5258
+ title: 'Define the action (the endpoint itself)',
5259
+ app_tag: args.app_tag ?? '<app tag from stage 1, or the existing app tag>',
5260
+ ask_user: [
5261
+ 'The HTTP method (GET/POST/PUT/PATCH/DELETE).',
5262
+ 'The endpoint PATH relative to the app\'s base_url — e.g. "/v1/users/{id}", never a full URL. ' +
5263
+ 'Ductape joins base_url + resource per environment at call time, so the same action works ' +
5264
+ 'across every environment automatically.',
5265
+ 'Only what is relevant to this specific endpoint: query params, headers, request body shape ' +
5266
+ '(for POST/PUT/PATCH), and the expected response shape (status_code, success flag, a body ' +
5267
+ 'sample). Do not invent fields the user has not described or that are not in the API docs.',
5268
+ ],
5269
+ then: [
5270
+ 'If the exact body shape is uncertain, call ductape_schema first.',
5271
+ 'Write a JSON body: { tag, name, resource, method, request_type, description?, params?, ' +
5272
+ 'query?, headers?, body?, response? }. request_type is REQUIRED — one of "application/json" ' +
5273
+ '| "application/x-www-form-urlencoded" | "multipart/form-data" | "SOAP" | "html" (not ' +
5274
+ '"json"/"xml"/"form"). If response is included, response.status_code and ' +
5275
+ 'response.response_format use that same set of values, and status_code is a STRING (e.g. ' +
5276
+ '"200", not the number 200).',
5277
+ 'Run: ductape_cli("apps actions create --app <app_tag> --action-file <path-to-action.json> ' +
5278
+ '--json") — the flag is --action-file, not -f (that flag belongs to the parent "apps" ' +
5279
+ 'command and Commander resolves it there, so -f would silently be ignored here).',
5280
+ ],
5281
+ });
5282
+ stages.push({
5283
+ stage: 3,
5284
+ title: 'Connect the app to the product',
5285
+ skip_if: 'The app is already connected to the target product — check with ' +
5286
+ 'ductape_cli("products apps list --product <product_tag> --json") before asking the user anything here.',
5287
+ product_tag: args.product_tag ?? '<ask the user, if not already established from context>',
5288
+ then: [
5289
+ hasProduct
5290
+ ? `Product already known: ${args.product_tag}.`
5291
+ : 'Ask the user which product this belongs to, unless already obvious from context.',
5292
+ 'Get the product\'s environments: ductape_cli("products environments list <product_tag> --json")',
5293
+ 'Map each product environment to one of the app\'s environments (usually 1:1 by name) — ask the ' +
5294
+ 'user rather than guessing if it is not obvious.',
5295
+ 'Run: ductape_cli("products apps connect --product <product_tag> --app <app_tag> --env-map ' +
5296
+ '<app_env>:<product_env> [repeat --env-map per environment] --json"). If the app is a public ' +
5297
+ 'marketplace app, ductape_marketplace_connect does the same thing in one call.',
5298
+ ],
5299
+ });
5300
+ stages.push({
5301
+ stage: 4,
5302
+ title: 'Only now write integration code',
5303
+ instructions: [
5304
+ 'Do not write any code that calls this endpoint before stages 0-3 are complete — the action and ' +
5305
+ 'the product connection must exist first, or the generated code will reference a tag that ' +
5306
+ 'does not exist yet.',
5307
+ 'Call ductape_generate_payload (operation_family="action", method="run" or "dispatch", ' +
5308
+ 'targets={app, action}) first, then ductape_generate_snippet or ductape_execute to call it.',
5309
+ ],
5310
+ });
5311
+ return {
5312
+ content: [{
5313
+ type: 'text',
5314
+ text: JSON.stringify({
5315
+ ok: true,
5316
+ flow: 'integrate_endpoint',
5317
+ note: 'Follow these stages in order. Each stage tells you what to ask the user and which ' +
5318
+ 'ductape_cli command to run. Stages already satisfied by the arguments you passed are omitted.',
5319
+ stages,
5320
+ }, null, 2),
5321
+ }],
5322
+ };
5323
+ };
5045
5324
  const eventsTopicSetupHandler = async (args) => {
5046
5325
  const tag = `${args.broker_tag}:${args.topic_tag}`;
5047
5326
  const relativePath = `ductape/events/${args.topic_tag}.topic.json`;
@@ -5096,9 +5375,11 @@ const cliInputSchema = z.object({
5096
5375
  'Note: environments have their own CLI commands (products environments list/get/create/update, ' +
5097
5376
  'no linked project required — the product tag is always an explicit argument). Quotas, ' +
5098
5377
  'fallbacks, jobs, and healthchecks use resources commands. App actions and auths are ' +
5099
- 'configured in the Workbench UI. Features have no CLI ' +
5100
- 'creation command: define them in application code with features.define so application ' +
5101
- 'boot/runtime registration makes them available.\n\n' +
5378
+ 'configured in the Workbench UI. Features have no CLI creation command: define them in ' +
5379
+ 'application code with features.define under ductape/features/. Persist them with ' +
5380
+ '"features sync" (runs the project\'s own "features:sync" npm script) — never call ' +
5381
+ 'features.define from the app\'s normal startup path, since that blocks every boot on ' +
5382
+ 'Ductape API reachability. See ductape_docs for the full convention.\n\n' +
5102
5383
  'The CLI uses the user\'s local logged-in session (ductape login) — no key is required.'),
5103
5384
  });
5104
5385
  async function loadMcpSdk() {
@@ -5374,6 +5655,26 @@ async function main() {
5374
5655
  return p;
5375
5656
  });
5376
5657
  }
5658
+ // Most non-setup publishable-key runtime calls require params[0] to carry a "session" field
5659
+ // (confirmed 2026-08-18 against live backend logs: the proxy's own error is "When using
5660
+ // publishable key, params must include a session (session token from backend)." — the HTTP
5661
+ // response to the client only ever says the much vaguer "Authentication failed", so this
5662
+ // failure mode is otherwise very hard to diagnose from the client side alone). Fail fast
5663
+ // locally with a specific message instead of making a doomed network call.
5664
+ const sessionRequiredModules = new Set([
5665
+ 'actions', 'features', 'databases', 'graph', 'vector', 'storage',
5666
+ 'notifications', 'messageBrokers', 'events', 'quotas', 'fallback', 'health',
5667
+ ]);
5668
+ const sessionExemptMethods = new Set(['consume', 'status', 'check', 'fetch', 'list']);
5669
+ const requiresSession = sessionRequiredModules.has(proxyModule) && !sessionExemptMethods.has(args.method.toLowerCase());
5670
+ const paramsHaveSession = params.some((p) => p && typeof p === 'object' && !Array.isArray(p) && 'session' in p);
5671
+ if (requiresSession && !paramsHaveSession) {
5672
+ throw new Error(`"${args.module}.${args.method}" is a runtime operation and requires a "session" field in ` +
5673
+ 'params[0] — none was provided. ductape_execute cannot supply a session on your behalf. ' +
5674
+ 'Call ductape_generate_payload for this operation to see the exact param shape, obtain a ' +
5675
+ 'real session token (e.g. via sessions.start against this product/env — not a placeholder ' +
5676
+ 'string), and include it as params[0].session before retrying.');
5677
+ }
5377
5678
  let result;
5378
5679
  try {
5379
5680
  result = await executeViaProxy(key, proxyModule, args.method, params);
@@ -5381,8 +5682,17 @@ async function main() {
5381
5682
  catch (error) {
5382
5683
  const message = error instanceof Error ? error.message : String(error);
5383
5684
  if (/authentication failed|unauthorized|invalid.*key/i.test(message)) {
5384
- throw new Error('The DUCTAPE_PUBLISHABLE_KEY was rejected by the runtime proxy. ' +
5385
- 'Use a publishable key for the same workspace/product. The MCP server never accepts or forwards access keys.');
5685
+ const sessionHint = !paramsHaveSession
5686
+ ? ' No "session" field was present in params most non-setup publishable-key runtime ' +
5687
+ 'calls require one (a real token from sessions.start, not a placeholder). This is the ' +
5688
+ 'most common cause of an opaque "Authentication failed" response; call ' +
5689
+ 'ductape_generate_payload first to see whether this operation expects a session field, ' +
5690
+ 'then obtain a real session token before retrying.'
5691
+ : ' A "session" field was present, so a missing session is likely not the cause here — ' +
5692
+ 'check that the publishable key itself belongs to the same workspace/product, and that ' +
5693
+ 'any module/action restrictions on the key (Workbench → Tokens → Publishable Key) ' +
5694
+ 'permit this operation.';
5695
+ throw new Error(`Runtime authentication was rejected (backend said: "${message}").` + sessionHint);
5386
5696
  }
5387
5697
  throw error;
5388
5698
  }
@@ -5559,7 +5869,15 @@ async function main() {
5559
5869
  'Two-step rule for runtime operations:\n' +
5560
5870
  ' 1. Call ductape_generate_payload first to get the canonical payload template.\n' +
5561
5871
  ' This reveals the exact "input" field keys — they are product/env/operation-specific.\n' +
5562
- ' 2. Fill in the values from the template, then call ductape_execute.',
5872
+ ' 2. Fill in the values from the template, then call ductape_execute.\n\n' +
5873
+ 'Session requirement: most non-setup operations on actions, features, databases, graph, ' +
5874
+ 'vector, storage, notifications, message brokers/events, quotas, fallback, and health ' +
5875
+ 'require a real session token in params[0].session (from sessions.start against the same ' +
5876
+ 'product/env — never a placeholder string). This tool checks for that locally and fails ' +
5877
+ 'fast with a specific error before calling the backend if it is missing for an operation ' +
5878
+ 'that needs one. The backend itself only reports a generic "Authentication failed" for ' +
5879
+ 'this case, so a missing session and a genuinely bad key look identical unless you read ' +
5880
+ 'this local check first.',
5563
5881
  inputSchema: executeInputSchema,
5564
5882
  }, executeHandler);
5565
5883
  server.registerTool('ductape_generate_payload', {
@@ -5687,6 +6005,15 @@ async function main() {
5687
6005
  command: `products apps connect --product ${shellArgument(args.product_tag)} --app ${shellArgument(args.app_tag)}${mappings} --json`,
5688
6006
  });
5689
6007
  });
6008
+ server.registerTool('ductape_integrate_endpoint', {
6009
+ title: 'Integrate a New Endpoint',
6010
+ description: 'Call this FIRST whenever the user wants to integrate a new API endpoint — before writing any ' +
6011
+ 'code. Returns the ordered flow: choose an existing app or create a new one, define the ' +
6012
+ 'action (endpoint) on it, connect the app to the target product, and only then write code. ' +
6013
+ 'Read-only — it runs no CLI command itself and performs the steps only when you follow up with ' +
6014
+ 'the ductape_cli calls it names.',
6015
+ inputSchema: integrateEndpointInputSchema,
6016
+ }, integrateEndpointHandler);
5690
6017
  server.registerTool('ductape_cli', {
5691
6018
  title: 'Ductape CLI',
5692
6019
  description: 'Run a Ductape CLI command for administrative operations.\n\n' +
@@ -5805,6 +6132,7 @@ async function main() {
5805
6132
  server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
5806
6133
  server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
5807
6134
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
6135
+ server.tool('ductape_integrate_endpoint', integrateEndpointInputSchema.shape, integrateEndpointHandler);
5808
6136
  server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
5809
6137
  }
5810
6138
  else {
package/docs/TOOLS.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  The Ductape MCP server exposes proxy, CLI, discovery, documentation, migration, and portable-function setup tools.
4
4
 
5
+ ## Tool: `ductape_integrate_endpoint`
6
+
7
+ Call this first whenever the user wants to integrate a new API endpoint, before writing any code.
8
+ It is read-only guidance — it runs no CLI command itself and returns the ordered flow to follow:
9
+
10
+ 1. **Choose the app** — ask first: existing app, or create a new one? Do not assume "new" just
11
+ because the API seems unfamiliar — check `ductape_cli("apps list --json")` and let the user
12
+ decide. If new: `ductape_cli("apps create -f app.json")`, body `{ app_name, description, tag,
13
+ envs: [...] }` (`tag` is REQUIRED — no `--name`/`--description` flags exist, only `-f
14
+ <file.json>` or interactive TTY), collecting an environment + base URL for each environment the
15
+ app needs; every active environment needs a `base_url` before its action can be called.
16
+ 2. **Define the action** — the HTTP method and the endpoint's path *relative to the app's base_url*
17
+ (e.g. `/v1/users/{id}`, never a full URL — Ductape joins `base_url + resource` per environment at
18
+ call time), plus only the params/headers/body/response fields relevant to that endpoint. Then
19
+ `ductape_cli("apps actions create --app <tag> --action-file action.json")` — the flag is
20
+ `--action-file`, **not** `-f`/`--file` (that flag belongs to the parent `apps` command; Commander
21
+ resolves a flag shared by an ancestor and a descendant against the ancestor, so `-f` here is
22
+ silently ignored — same fix `products environments create/update` uses `--env-file` for).
23
+ `request_type` is REQUIRED on the body — one of `"application/json"` |
24
+ `"application/x-www-form-urlencoded"` | `"multipart/form-data"` | `"SOAP"` | `"html"` (not the
25
+ short names `"json"`/`"xml"`/`"form"`); if `response` is included, `response.status_code` is a
26
+ **string** (e.g. `"200"`) and `response.response_format` uses that same value set.
27
+ 3. **Connect the app to the product** — skip if already connected. Otherwise map each product
28
+ environment to an app environment and run
29
+ `ductape_cli("products apps connect --product <tag> --app <tag> --env-map <app_env>:<product_env> ...")`.
30
+ 4. **Only then write integration code** — `ductape_generate_payload` → `ductape_generate_snippet` /
31
+ `ductape_execute`, calling `actions.run` or `actions.dispatch`.
32
+
33
+ Optional arguments `product_tag` and `app_tag` let the tool skip stages already answered (e.g. pass
34
+ `app_tag` once the user has said which existing app this belongs to, and the returned flow starts at
35
+ stage 2 instead of asking again) — but never infer `app_tag` on the tool's behalf without the user
36
+ having actually said so; when in doubt, omit it and let stage 1 ask.
37
+
38
+ All of the above was verified against a live workspace: created a `jsonplaceholder` app with `prd`/
39
+ `snd` environments, added a `GET /todos/1` action, and connected it to a real product end to end.
40
+
5
41
  ## Tools: `ductape_events_topic_setup` and `ductape_events_validate_project`
6
42
 
7
43
  `ductape_events_topic_setup` is a read-only generator for one canonical topic asset. It returns the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",