@ductape/mcp 0.2.21 → 0.2.22
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 +162 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -203,7 +203,9 @@ There are THREE categories of operations. Use the right tool for each:
|
|
|
203
203
|
App actions and auths are configured in the Workbench UI. Quotas, fallbacks, jobs, and
|
|
204
204
|
healthchecks are administrative resources managed with ductape_cli("resources <type> ...").
|
|
205
205
|
Features have no CLI create command because their definitions are code-first through
|
|
206
|
-
features.define.
|
|
206
|
+
features.define. They DO have a CLI persistence command — ductape_cli("features sync") —
|
|
207
|
+
which runs the project's own "features:sync" npm script to register ductape/features/
|
|
208
|
+
definitions against the live product; see the DEPLOYING features.define CALLS section below.
|
|
207
209
|
|
|
208
210
|
⚠ MULTI-ENV REQUIREMENT — applies to ALL product assets (storage, database, cache,
|
|
209
211
|
messageBroker, graph, vector, and any other resource with an envs array):
|
|
@@ -1679,6 +1681,15 @@ PRIMITIVES-FIRST CAPABILITY EXTRACTION — REQUIRED FOR EVERY MIGRATION SLICE
|
|
|
1679
1681
|
5. For each Function, record pure/WASM-candidate versus framework-dependent classification.
|
|
1680
1682
|
6. Call ductape_function_setup and implement verified local plus remote availability.
|
|
1681
1683
|
|
|
1684
|
+
Persist migrated Features the same way as newly designed ones: define every Feature under
|
|
1685
|
+
ductape/features/ (e.g. ductape/features/src/my-feature.ts), never inline in the app's normal
|
|
1686
|
+
startup path. Register only local function/operation handlers (ductape.sdk.functions.register(...))
|
|
1687
|
+
at boot — no network call. Persist Feature definitions to the live product with
|
|
1688
|
+
"ductape features sync" (ductape_cli("features sync")), which runs the project's own
|
|
1689
|
+
"features:sync" npm script — never automatically on app boot, the same way a migrated database
|
|
1690
|
+
schema is applied via "ductape db migrate" rather than at startup. See "DEPLOYING features.define
|
|
1691
|
+
CALLS" below for the full convention and required "features:sync" script shape.
|
|
1692
|
+
|
|
1682
1693
|
Never create a Function that merely hides database, session, Events, storage, notification, graph,
|
|
1683
1694
|
vector, quota, fallback, healthcheck, cache, secret, or connected-App work that the Feature can
|
|
1684
1695
|
express directly. Never fragment an original atomic transaction just to maximize primitive count.
|
|
@@ -1742,6 +1753,17 @@ ENVIRONMENTS
|
|
|
1742
1753
|
Report matched, missing, extra, and ambiguous normalized aliases. Never guess an ambiguous mapping.
|
|
1743
1754
|
Environment mutation is administrative CLI work and must never be routed through ductape_execute.
|
|
1744
1755
|
|
|
1756
|
+
INACTIVE ENVIRONMENTS (e.g. an auto-created "snd"): whether an inactive environment needs a real
|
|
1757
|
+
physical provider resource behind it, or can stay a placeholder with no live infrastructure, is
|
|
1758
|
+
not settled platform behavior — do not assume either way. Resource-create payloads that require
|
|
1759
|
+
complete environment coverage (see above) still need an entry for every environment returned by
|
|
1760
|
+
"products environments list", active or not; whether that entry must point at a real backing
|
|
1761
|
+
resource for an inactive env is a product-owner decision to surface explicitly, not infer.
|
|
1762
|
+
Separately: "products environments update <product> <slug>" with {"active":false} may report
|
|
1763
|
+
"updated": true without the change actually persisting — confirmed via independent read-back,
|
|
1764
|
+
not just replication lag. If deactivating an environment is important, verify with a follow-up
|
|
1765
|
+
"products environments get" rather than trusting the update response.
|
|
1766
|
+
|
|
1745
1767
|
PRODUCT AND ASSET BOOTSTRAP
|
|
1746
1768
|
Product creation is idempotent: fetch by tag, create only when absent, then link the destination.
|
|
1747
1769
|
Use ductape_cli for products, apps, resources, cloud connections, secrets, apply, and migrations.
|
|
@@ -1798,6 +1820,8 @@ COMPONENT DECISIONS
|
|
|
1798
1820
|
Pinecone/Qdrant/Weaviate/OpenSearch → Vector; JWT/session middleware → Sessions.
|
|
1799
1821
|
Named reusable product capabilities with a stable managed-execution boundary → code-first Features,
|
|
1800
1822
|
including synchronous multi-step capabilities and durable/scheduled/signal-driven orchestration.
|
|
1823
|
+
Define Features under ductape/features/ and persist them via "ductape features sync", never at
|
|
1824
|
+
app boot — see PRIMITIVES-FIRST CAPABILITY EXTRACTION above.
|
|
1801
1825
|
Retries, health checks, fallbacks, quotas, circuit breakers → Resilience.
|
|
1802
1826
|
Keep low-level deterministic rules as ordinary domain functions when they do not form a useful
|
|
1803
1827
|
independent capability boundary. Wrap or compose them into Features when the combined operation
|
|
@@ -1841,8 +1865,10 @@ AI EDITING STANDARD
|
|
|
1841
1865
|
rules in code, while allowing a coherent synchronous capability composed from those rules to be a Feature.
|
|
1842
1866
|
Immediate work propagates the full session; durable work uses approved actor metadata or system context.
|
|
1843
1867
|
Consumers are idempotent, retry ownership is singular and bounded, and external effects are not duplicated.
|
|
1844
|
-
After each slice: format, build, test, rescan,
|
|
1845
|
-
|
|
1868
|
+
After each slice: format, build, test, rescan, persist any migrated Feature definitions via
|
|
1869
|
+
ductape_cli("features sync") (never automatically on app boot), reconcile assets through
|
|
1870
|
+
ductape_cli, smoke-test in snd, and report unresolved findings. A clean scanner result alone
|
|
1871
|
+
never proves a correct migration.
|
|
1846
1872
|
|
|
1847
1873
|
CONTEXTUAL FILE REVIEW PROTOCOL
|
|
1848
1874
|
Use review_queue as a coverage aid. Review relevant files individually, starting with repository
|
|
@@ -2695,6 +2721,18 @@ CONNECT A DISCOVERED APP THROUGH MCP:
|
|
|
2695
2721
|
Never guess environment mappings or action tags. Connecting mutates product configuration;
|
|
2696
2722
|
obtain user approval when the user has not already requested the connection.
|
|
2697
2723
|
|
|
2724
|
+
CONFIRMED LIMITATION (2026-08-17): both ductape_marketplace_connect and the equivalent
|
|
2725
|
+
ductape_cli("products apps connect --product <tag> --app <app-tag> --env-map ...") reject any
|
|
2726
|
+
app that is not marked public in the marketplace, with error
|
|
2727
|
+
App "<tag>" is not public in the marketplace. This includes apps a user creates themselves under
|
|
2728
|
+
their own tag (e.g. a private "ductape:paystack") — private/workspace-owned apps CANNOT be
|
|
2729
|
+
connected to a product through MCP or CLI at all, only through Workbench, even though
|
|
2730
|
+
ductape_marketplace_inspect can still read a private app's full action catalogue (inspect and
|
|
2731
|
+
connect have different visibility rules). If you hit this, do not keep retrying — tell the user
|
|
2732
|
+
the app must be connected via Workbench, and once they confirm it's connected, verify with
|
|
2733
|
+
ductape_cli("products apps list --product <product_tag> --json") rather than attempting the
|
|
2734
|
+
connect call again.
|
|
2735
|
+
|
|
2698
2736
|
ONLY after all five steps can any code call:
|
|
2699
2737
|
ctx.api.run({ app: '<app_tag>', action: '<action_tag>', input: { ... } }) ← in a feature handler
|
|
2700
2738
|
actions.run([{ product, env, app: '<app_tag>', action: '<action_tag>', input }]) ← at runtime
|
|
@@ -3079,6 +3117,23 @@ TAG AND SMTP RULES
|
|
|
3079
3117
|
emails.smtp.secure is a boolean and cannot be a $Secret{...} string.
|
|
3080
3118
|
Credential strings such as auth.user and auth.pass may use $Secret{...}.
|
|
3081
3119
|
|
|
3120
|
+
notifications messages create/update/get had NO --product override before CLI 0.3.17 — it
|
|
3121
|
+
silently resolved the product from whatever is linked in the current working directory, with
|
|
3122
|
+
no way to override. Confirmed root cause of a real multi-hour misdiagnosis: it kept resolving
|
|
3123
|
+
to a different product than intended, and the resulting "Notification X not found" error never
|
|
3124
|
+
named which product it actually checked. If seeing this on an older CLI, verify which project
|
|
3125
|
+
is linked in cwd before assuming the notification component itself is missing.
|
|
3126
|
+
|
|
3127
|
+
SendGrid field names (confirmed against the live Joi validator, not just this doc — cross-check
|
|
3128
|
+
the SDK source when in doubt): { provider: "sendgrid", sendgrid: { apiKey: "$Secret{...}",
|
|
3129
|
+
sender_email: "$Secret{...}" } }. The field is sender_email, not sender — "sender" is silently
|
|
3130
|
+
rejected with a required-field error naming sender_email specifically, not a helpful diff.
|
|
3131
|
+
|
|
3132
|
+
Nexmo/Vonage SMS field names: { provider: "nexmo", apiKey: "$Secret{...}",
|
|
3133
|
+
apiSecret: "$Secret{...}", sender: "YourSenderId" }. The credential fields are apiKey/apiSecret
|
|
3134
|
+
— NOT accountSid (that is a Twilio-only field name and is rejected with "not allowed" if used
|
|
3135
|
+
for nexmo).
|
|
3136
|
+
|
|
3082
3137
|
Send at runtime (one channel at a time):
|
|
3083
3138
|
→ CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
3084
3139
|
notifications.email.send [{ product, env, notification, input: { recipients, subject?, template? } }]
|
|
@@ -3130,6 +3185,16 @@ FIREBASE THROUGH A GCP CLOUD CONNECTION
|
|
|
3130
3185
|
intent; creating a notification does not prove Google-side FCM permission. Runtime delivery
|
|
3131
3186
|
requires fcm.googleapis.com and roles/firebasecloudmessaging.admin, so validate the connection
|
|
3132
3187
|
and perform a delivery test.
|
|
3188
|
+
|
|
3189
|
+
CONFIRMED CONTRADICTION (2026-08-17): despite the "never put the private key in the file"
|
|
3190
|
+
guidance above, resources notifications create with authMode: "cloud_connection" currently
|
|
3191
|
+
rejects the payload progressively demanding envs[].push_notifications.credentials.project_id,
|
|
3192
|
+
then .private_key_id, then .client_email, then finally the raw .private_key itself — the full
|
|
3193
|
+
manual service-account shape, defeating the documented purpose of cloud_connection mode. Do NOT
|
|
3194
|
+
fabricate a placeholder private_key to satisfy this — that field is genuinely sensitive and a
|
|
3195
|
+
fake value risks being mistaken for real credential material later. If you hit this, drop the
|
|
3196
|
+
push_notifications block and register email/SMS only until this is fixed, and tell the user
|
|
3197
|
+
explicitly rather than working around it with fake data.
|
|
3133
3198
|
Notification tag and message tag are ALWAYS passed together as "notification_tag:message_tag".
|
|
3134
3199
|
`.trim(),
|
|
3135
3200
|
resilience: `
|
|
@@ -3660,6 +3725,55 @@ STEP 6 — WRITE the feature into the project codebase
|
|
|
3660
3725
|
- Write rollback handlers inline as the third argument to ctx.step()
|
|
3661
3726
|
- Return a plain object as the feature's output
|
|
3662
3727
|
|
|
3728
|
+
━━━ DEPLOYING features.define CALLS — DO NOT run them on every app boot ━━━
|
|
3729
|
+
|
|
3730
|
+
features.define() has a network-bound RECORDING PHASE (see FEATURE RECORDING SEMANTICS below) that
|
|
3731
|
+
calls the live Ductape API to compile and register the Feature's step graph. This is administrative
|
|
3732
|
+
configuration work, not a request-serving concern — treat it exactly like a database migration:
|
|
3733
|
+
defined in source, but APPLIED via an explicit, separate command, never automatically on every
|
|
3734
|
+
process start.
|
|
3735
|
+
|
|
3736
|
+
Calling features.define() from inside a framework's normal startup lifecycle hook (NestJS
|
|
3737
|
+
onModuleInit, Express app bootstrap, a Lambda cold-start path, etc.) makes ordinary app startup
|
|
3738
|
+
depend on Ductape API reachability and latency for every single boot/restart/replica — including
|
|
3739
|
+
local dev restarts, autoscaling events, and health-check-triggered restarts. A slow or unreachable
|
|
3740
|
+
Ductape API then blocks the app from ever starting to serve requests, even for routes that have
|
|
3741
|
+
nothing to do with the affected Feature.
|
|
3742
|
+
|
|
3743
|
+
REQUIRED CONVENTION — this is what "ductape features sync" (CLI) expects:
|
|
3744
|
+
1. Put every Feature definition under ductape/features/ (e.g. ductape/features/src/my-feature.ts),
|
|
3745
|
+
each calling ductape.feature.define({ ... }) from a registerXFeature(ductape) export. This
|
|
3746
|
+
mirrors ductape/database/migrations/ — a fixed, discoverable location for Ductape-managed
|
|
3747
|
+
source, regardless of the surrounding project structure.
|
|
3748
|
+
2. In the app's normal startup path, register ONLY local function/operation handlers
|
|
3749
|
+
(ductape.sdk.functions.register(...)) — no network call, safe on every boot. Never call
|
|
3750
|
+
features.define(...) here.
|
|
3751
|
+
3. Add a "features:sync" script to package.json. It owns booting just enough of the app to
|
|
3752
|
+
construct real dependencies (in NestJS, use NestFactory.createApplicationContext(module) —
|
|
3753
|
+
same DI container, no HTTP listener) and calling every registerXFeature(...) once, then
|
|
3754
|
+
exiting. Ductape cannot run this step for you generically — a Feature handler is real
|
|
3755
|
+
application code that typically depends on the app's own services, unlike a migration file,
|
|
3756
|
+
which is declarative data the Ductape backend can apply directly.
|
|
3757
|
+
4. Persist Features by running "ductape features sync" (or ductape_cli("features sync")), which
|
|
3758
|
+
finds the linked project and runs its "features:sync" script — never automatically on boot.
|
|
3759
|
+
Pass an optional filter argument to scope it: "ductape features sync payments".
|
|
3760
|
+
|
|
3761
|
+
A NestJS service with both concerns split looks like:
|
|
3762
|
+
async onModuleInit() {
|
|
3763
|
+
this.ductape.sdk.functions.register({ ...myFunctions, operations: { ... /* local handlers */ } });
|
|
3764
|
+
}
|
|
3765
|
+
async syncFeatures(): Promise<void> {
|
|
3766
|
+
await registerMyFeature(this.ductape.sdk); // the features.define(...) call
|
|
3767
|
+
}
|
|
3768
|
+
The project's own "features:sync" script resolves each service with a syncFeatures() method via a
|
|
3769
|
+
headless application context and calls it — mirroring how a migration runner applies pending
|
|
3770
|
+
migrations explicitly. "ductape features sync" is a thin, framework-agnostic front door onto that
|
|
3771
|
+
script; it does not itself execute application code.
|
|
3772
|
+
|
|
3773
|
+
This applies to any framework, not just NestJS — the only requirement is that whatever construct
|
|
3774
|
+
the app boots FOR REQUESTS never itself calls features.define(); the project's own "features:sync"
|
|
3775
|
+
script does, invoked only through the explicit CLI command.
|
|
3776
|
+
|
|
3663
3777
|
STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
|
|
3664
3778
|
Branch on step result (early return in handler):
|
|
3665
3779
|
→ Add branchOverrides: { stepTag: { field: value } } so all branches are recorded
|
|
@@ -5096,9 +5210,11 @@ const cliInputSchema = z.object({
|
|
|
5096
5210
|
'Note: environments have their own CLI commands (products environments list/get/create/update, ' +
|
|
5097
5211
|
'no linked project required — the product tag is always an explicit argument). Quotas, ' +
|
|
5098
5212
|
'fallbacks, jobs, and healthchecks use resources commands. App actions and auths are ' +
|
|
5099
|
-
'configured in the Workbench UI. Features have no CLI ' +
|
|
5100
|
-
'
|
|
5101
|
-
'
|
|
5213
|
+
'configured in the Workbench UI. Features have no CLI creation command: define them in ' +
|
|
5214
|
+
'application code with features.define under ductape/features/. Persist them with ' +
|
|
5215
|
+
'"features sync" (runs the project\'s own "features:sync" npm script) — never call ' +
|
|
5216
|
+
'features.define from the app\'s normal startup path, since that blocks every boot on ' +
|
|
5217
|
+
'Ductape API reachability. See ductape_docs for the full convention.\n\n' +
|
|
5102
5218
|
'The CLI uses the user\'s local logged-in session (ductape login) — no key is required.'),
|
|
5103
5219
|
});
|
|
5104
5220
|
async function loadMcpSdk() {
|
|
@@ -5374,6 +5490,26 @@ async function main() {
|
|
|
5374
5490
|
return p;
|
|
5375
5491
|
});
|
|
5376
5492
|
}
|
|
5493
|
+
// Most non-setup publishable-key runtime calls require params[0] to carry a "session" field
|
|
5494
|
+
// (confirmed 2026-08-18 against live backend logs: the proxy's own error is "When using
|
|
5495
|
+
// publishable key, params must include a session (session token from backend)." — the HTTP
|
|
5496
|
+
// response to the client only ever says the much vaguer "Authentication failed", so this
|
|
5497
|
+
// failure mode is otherwise very hard to diagnose from the client side alone). Fail fast
|
|
5498
|
+
// locally with a specific message instead of making a doomed network call.
|
|
5499
|
+
const sessionRequiredModules = new Set([
|
|
5500
|
+
'actions', 'features', 'databases', 'graph', 'vector', 'storage',
|
|
5501
|
+
'notifications', 'messageBrokers', 'events', 'quotas', 'fallback', 'health',
|
|
5502
|
+
]);
|
|
5503
|
+
const sessionExemptMethods = new Set(['consume', 'status', 'check', 'fetch', 'list']);
|
|
5504
|
+
const requiresSession = sessionRequiredModules.has(proxyModule) && !sessionExemptMethods.has(args.method.toLowerCase());
|
|
5505
|
+
const paramsHaveSession = params.some((p) => p && typeof p === 'object' && !Array.isArray(p) && 'session' in p);
|
|
5506
|
+
if (requiresSession && !paramsHaveSession) {
|
|
5507
|
+
throw new Error(`"${args.module}.${args.method}" is a runtime operation and requires a "session" field in ` +
|
|
5508
|
+
'params[0] — none was provided. ductape_execute cannot supply a session on your behalf. ' +
|
|
5509
|
+
'Call ductape_generate_payload for this operation to see the exact param shape, obtain a ' +
|
|
5510
|
+
'real session token (e.g. via sessions.start against this product/env — not a placeholder ' +
|
|
5511
|
+
'string), and include it as params[0].session before retrying.');
|
|
5512
|
+
}
|
|
5377
5513
|
let result;
|
|
5378
5514
|
try {
|
|
5379
5515
|
result = await executeViaProxy(key, proxyModule, args.method, params);
|
|
@@ -5381,8 +5517,17 @@ async function main() {
|
|
|
5381
5517
|
catch (error) {
|
|
5382
5518
|
const message = error instanceof Error ? error.message : String(error);
|
|
5383
5519
|
if (/authentication failed|unauthorized|invalid.*key/i.test(message)) {
|
|
5384
|
-
|
|
5385
|
-
'
|
|
5520
|
+
const sessionHint = !paramsHaveSession
|
|
5521
|
+
? ' No "session" field was present in params — most non-setup publishable-key runtime ' +
|
|
5522
|
+
'calls require one (a real token from sessions.start, not a placeholder). This is the ' +
|
|
5523
|
+
'most common cause of an opaque "Authentication failed" response; call ' +
|
|
5524
|
+
'ductape_generate_payload first to see whether this operation expects a session field, ' +
|
|
5525
|
+
'then obtain a real session token before retrying.'
|
|
5526
|
+
: ' A "session" field was present, so a missing session is likely not the cause here — ' +
|
|
5527
|
+
'check that the publishable key itself belongs to the same workspace/product, and that ' +
|
|
5528
|
+
'any module/action restrictions on the key (Workbench → Tokens → Publishable Key) ' +
|
|
5529
|
+
'permit this operation.';
|
|
5530
|
+
throw new Error(`Runtime authentication was rejected (backend said: "${message}").` + sessionHint);
|
|
5386
5531
|
}
|
|
5387
5532
|
throw error;
|
|
5388
5533
|
}
|
|
@@ -5559,7 +5704,15 @@ async function main() {
|
|
|
5559
5704
|
'Two-step rule for runtime operations:\n' +
|
|
5560
5705
|
' 1. Call ductape_generate_payload first to get the canonical payload template.\n' +
|
|
5561
5706
|
' 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
|
|
5707
|
+
' 2. Fill in the values from the template, then call ductape_execute.\n\n' +
|
|
5708
|
+
'Session requirement: most non-setup operations on actions, features, databases, graph, ' +
|
|
5709
|
+
'vector, storage, notifications, message brokers/events, quotas, fallback, and health ' +
|
|
5710
|
+
'require a real session token in params[0].session (from sessions.start against the same ' +
|
|
5711
|
+
'product/env — never a placeholder string). This tool checks for that locally and fails ' +
|
|
5712
|
+
'fast with a specific error before calling the backend if it is missing for an operation ' +
|
|
5713
|
+
'that needs one. The backend itself only reports a generic "Authentication failed" for ' +
|
|
5714
|
+
'this case, so a missing session and a genuinely bad key look identical unless you read ' +
|
|
5715
|
+
'this local check first.',
|
|
5563
5716
|
inputSchema: executeInputSchema,
|
|
5564
5717
|
}, executeHandler);
|
|
5565
5718
|
server.registerTool('ductape_generate_payload', {
|