@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 +354 -26
- package/docs/TOOLS.md +36 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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,
|
|
1845
|
-
|
|
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
|
-
|
|
2716
|
-
|
|
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
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
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:
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
|
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
|
-
'
|
|
5101
|
-
'
|
|
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
|
-
|
|
5385
|
-
'
|
|
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
|