@ductape/mcp 0.2.36 → 0.3.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"action-contract.d.ts","sourceRoot":"","sources":["../src/action-contract.ts"],"names":[],"mappings":"AAiFA,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAYnE"}
1
+ {"version":3,"file":"action-contract.d.ts","sourceRoot":"","sources":["../src/action-contract.ts"],"names":[],"mappings":"AAoFA,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAYnE"}
@@ -61,8 +61,11 @@ function schemaFromLocation(value) {
61
61
  property.description = String(item.description);
62
62
  if (item.sampleValue !== undefined && item.sampleValue !== '')
63
63
  property.example = item.sampleValue;
64
+ const defaultValue = item.defaultValue ?? item.default_value ?? item.default ?? item.value;
65
+ if (defaultValue !== undefined && defaultValue !== '')
66
+ property.default = defaultValue;
64
67
  properties[key] = property;
65
- if (item.required === true)
68
+ if (item.required === true || String(item.required).toLowerCase() === 'true')
66
69
  required.push(key);
67
70
  }
68
71
  for (const row of legacyRows) {
@@ -77,6 +80,7 @@ function schemaFromLocation(value) {
77
80
  type: 'string',
78
81
  ...(description ? { description } : {}),
79
82
  ...(item.value !== undefined ? { example: item.value } : {}),
83
+ ...(item.value !== undefined && item.value !== '' ? { default: item.value } : {}),
80
84
  };
81
85
  if (/\(required\)|^required\b/i.test(description))
82
86
  required.push(key);
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { homedir } from 'os';
16
16
  import { delimiter, join } from 'path';
17
17
  import { z } from 'zod';
18
18
  import { normalizeLiveActionContract } from './action-contract.js';
19
+ import { runtimeInputRecovery } from './runtime-recovery.js';
19
20
  import { executeViaProxy, generateExecutablePayload, getAssetSchemas, } from './proxy-client.js';
20
21
  import { EVENTS_DELIVERY_SEMANTICS, EVENTS_IMPLEMENTATION_WARNING, eventsCapabilityOverview, inspectEventsComponent, searchEventsCapabilities, } from './events-capabilities.js';
21
22
  const MODULES = [
@@ -527,6 +528,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
527
528
  ductape_cli("apps actions list --app <app_tag> --json")
528
529
  ductape_cli("apps actions get --app <app_tag> --action <action_tag> --json")
529
530
  ductape_cli("apps actions delete --app <app_tag> --action <action_tag>")
531
+ Reuse the same canonical full action asset for create and update. An unchanged top-level tag is
532
+ accepted and stripped locally before the immutable action is patched; never create a second
533
+ partial-update file. A tag that differs from --action is rejected before any mutation.
530
534
  NOTE: the flag is --action-file, NOT -f/--file — that flag belongs to the parent "apps" command
531
535
  (used by "apps create"/"apps update"), and Commander resolves a flag shared by an ancestor and a
532
536
  descendant against the ancestor, so -f here would silently never reach this subcommand. Verified
@@ -1352,7 +1356,7 @@ const ductape = new Ductape({
1352
1356
  product: String(payload.product),
1353
1357
  env: String(payload.env),
1354
1358
  redis_url: process.env.DUCTAPE_REDIS_URL,
1355
- runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
1359
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2, warm_connections: true },
1356
1360
  });
1357
1361
 
1358
1362
  async function run() {
@@ -1445,6 +1449,7 @@ const ADMIN_SUBCOMMANDS = [
1445
1449
  'workspaces',
1446
1450
  'link', 'unlink', 'init',
1447
1451
  'products', 'apps', 'marketplace',
1452
+ 'features',
1448
1453
  'resources',
1449
1454
  'notifications',
1450
1455
  'events',
@@ -2560,7 +2565,7 @@ and env, then await ready() before accepting work:
2560
2565
  accessKey: process.env.DUCTAPE_ACCESS_KEY!,
2561
2566
  product: "payments",
2562
2567
  env: "prd",
2563
- runtime_sync: { interval_ms: 30_000, jitter: 0.2, max_backoff_ms: 300_000 },
2568
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2, max_backoff_ms: 300_000, warm_connections: true },
2564
2569
  });
2565
2570
  await ductape.ready();
2566
2571
 
@@ -2575,7 +2580,10 @@ Runtime synchronization is automatic when product + env are supplied:
2575
2580
  assetRevision are narrower change hints. Any databaseRevision change also changes runtimeRevision.
2576
2581
  - A changed snapshot is loaded off-path and atomically replaces cache entries. Deleted assets are removed.
2577
2582
  - Refresh failure retains the last-known-good snapshot and retries with jittered exponential backoff.
2578
- - Use runtimeSnapshotStatus() for revision/error telemetry and refreshRuntime() for an explicit pull.
2583
+ - await ready() before accepting traffic. It now waits for the initial snapshot and database pool
2584
+ warm-up, so first Feature/Database execution does not absorb MongoDB TLS/pool establishment.
2585
+ - runtime_sync.warm_connections defaults to true; disable it only when lazy connections are intentional.
2586
+ - Use runtimeSnapshotStatus() for revision/error/warm-up phase telemetry and refreshRuntime() for an explicit pull.
2579
2587
  - Call close() during process shutdown. Do not create one Ductape instance per request.
2580
2588
 
2581
2589
  For NestJS, use DuctapeModule.forIntegration({ accessKey, product, env, runtimeSync }) instead of
@@ -3124,6 +3132,21 @@ CONNECTION FILE — REQUIRED SHAPE:
3124
3132
  auth and credentials absent when the environment does not require authentication. Never create a
3125
3133
  fake auth_tag merely to store a header.
3126
3134
 
3135
+ OAUTH/TOKEN ACCESS: OAuth grants and shared tokens belong to the product App connection, not to
3136
+ Feature input and not to repeated api.oauth() calls in application code. Inspect the App auth
3137
+ scheme; when it uses oauth2, token_access, credential_access, or fetch_credential_access, direct
3138
+ the user through the provider-consent/login flow in Workbench and preserve the resulting encrypted
3139
+ connection auth for every mapped product environment. Agents must never request, print, or write
3140
+ access/refresh tokens into the repository. Application code initializes Ductape with product+env,
3141
+ awaits ready(), and calls the action; runtime auth injection and token refresh are platform-managed.
3142
+ api.oauth() remains only an explicit process-local override for a connection the user deliberately
3143
+ has not persisted.
3144
+
3145
+ SHARED CONFIG: derive candidate keys from the selected App version's action schemas across headers,
3146
+ query, params, and body. Deduplicate by <location>:<key>, prefer fields reused by several actions,
3147
+ and present the exact candidates to the user. Never ask the user to invent or type a key name that
3148
+ is already declared by an action. Only the value/$Secret reference is user-supplied.
3149
+
3127
3150
  Before writing this file, inspect the exact App version/auth/action schemas. auth.data must have
3128
3151
  headers/query/params/body objects and must match the selected auth schema. Never guess an auth_tag,
3129
3152
  variable key, input location, or required field. Store credentials with secrets.create first and
@@ -3205,6 +3228,22 @@ Action input — flat input format:
3205
3228
  input: { "headers:X-Idempotency-Key": "..." } → request header
3206
3229
  Always use ductape_generate_payload or actions.fetch to know the exact field names — never guess.
3207
3230
 
3231
+ UNIVERSAL HTTP 400 / MISSING-INPUT SELF-HEALING:
3232
+ This applies equally to actions.run, a Feature API step, a Quota option, and every Fallback provider.
3233
+ The outer component does not hide or replace the referenced App Action contract.
3234
+ 1. Do not retry an unchanged payload and never invent the missing value.
3235
+ 2. Read the deepest provider error and identify app, action, field, and body/query/params/headers location.
3236
+ 3. MUST call ductape_schema({ product_tag, env_slug, app_tag, action_tag }) for each failing action.
3237
+ For Feature/Quota/Fallback, fetch the outer component too, follow its step/option mapping, and
3238
+ schema-check the referenced action that actually returned 400.
3239
+ 4. Compare every location separately. Restore schema defaults and required constant query values.
3240
+ Use explicit input keys such as "query:type" when location is ambiguous.
3241
+ 5. If the action definition lost the required/default metadata, repair the canonical
3242
+ ductape/actions/<app-tag>/<action-tag>.action.json and update the App Action via ductape_cli.
3243
+ Do not hardcode a provider-specific workaround into one Feature, Quota, or Fallback.
3244
+ 6. Call ductape_generate_payload, validate it against the looked-up contract, and retry once.
3245
+ If it still returns 400, stop and report the remaining provider/schema mismatch.
3246
+
3208
3247
  Run an action at runtime:
3209
3248
  → CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
3210
3249
  ductape_execute("actions.run", [{ product, env, app, action, input: { "body:field": value } }])
@@ -3314,7 +3353,9 @@ Product structure (IProduct fields):
3314
3353
 
3315
3354
  Bootstrap (single API call returning product context + component config + private key):
3316
3355
  Initialize @ductape/sdk with product + env and await ductape.ready(). It fetches the full
3317
- environment-scoped product runtime snapshot and primes BootstrapCache for connected App actions
3356
+ runtime snapshot and warms its database connection pools before the process is marked ready. Do
3357
+ not start the HTTP server or worker consumer before ready() resolves. The environment-scoped
3358
+ snapshot primes BootstrapCache for connected App actions
3318
3359
  and product components. A service falls back to its targeted bootstrap endpoint only when the
3319
3360
  requested asset is absent. Polling uses ETag/304 and atomically refreshes changed state.
3320
3361
  See ductape_docs({ topic: "runtime-sync" }).
@@ -4654,7 +4695,7 @@ Import (register an EXISTING cloud resource):
4654
4695
  product: "my-product",
4655
4696
  env: "prd",
4656
4697
  redis_url: process.env.DUCTAPE_REDIS_URL, // required for any dispatch(); omit only if never dispatching
4657
- runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
4698
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2, warm_connections: true },
4658
4699
  });
4659
4700
  await ductape.ready();
4660
4701
  await ductape.events.produce({
@@ -6310,7 +6351,15 @@ async function main() {
6310
6351
  'Enable it in Workbench → Tokens → Publishable Key. ' +
6311
6352
  'If write access cannot be granted to the publishable key, perform this operation server-side using a full Ductape SDK instance initialized with an access key.';
6312
6353
  }
6313
- return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
6354
+ const recovery = runtimeInputRecovery(message, {
6355
+ module: args.module,
6356
+ method: args.method,
6357
+ params: args.params,
6358
+ });
6359
+ const text = recovery
6360
+ ? JSON.stringify({ error: message, recovery }, null, 2)
6361
+ : `Error: ${message}`;
6362
+ return { content: [{ type: 'text', text }], isError: true };
6314
6363
  }
6315
6364
  };
6316
6365
  const payloadGenerateHandler = async (args) => {
@@ -0,0 +1,9 @@
1
+ type RuntimeContext = {
2
+ module: string;
3
+ method: string;
4
+ params: unknown[];
5
+ };
6
+ export declare function isRepairableClientInputError(message: string): boolean;
7
+ export declare function runtimeInputRecovery(message: string, context: RuntimeContext): Record<string, unknown> | null;
8
+ export {};
9
+ //# sourceMappingURL=runtime-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-recovery.d.ts","sourceRoot":"","sources":["../src/runtime-recovery.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB,CAAC;AAeF,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAkC7G"}
@@ -0,0 +1,53 @@
1
+ function visit(value, found) {
2
+ if (!value || typeof value !== 'object')
3
+ return;
4
+ if (Array.isArray(value)) {
5
+ value.forEach((item) => visit(item, found));
6
+ return;
7
+ }
8
+ const record = value;
9
+ const app = String(record.app ?? record.provider ?? '').trim();
10
+ const action = String(record.action ?? record.event ?? '').trim();
11
+ if (app && action)
12
+ found.set(`${app}:${action}`, { app, action });
13
+ Object.values(record).forEach((item) => visit(item, found));
14
+ }
15
+ export function isRepairableClientInputError(message) {
16
+ return /(?:status(?: code)?\s*400|http\s*400|err_bad_request|bad request|\bis required\b|required field|missing (?:input|field|parameter|value)|invalid (?:input|payload|parameter|payment type)|validation (?:error|failed))/i.test(message);
17
+ }
18
+ export function runtimeInputRecovery(message, context) {
19
+ if (!isRepairableClientInputError(message))
20
+ return null;
21
+ const actions = new Map();
22
+ visit(context.params, actions);
23
+ const appMatches = [...message.matchAll(/(?:app|provider)["'\s:=]+([\w:.-]+)/gi)].map((match) => match[1]);
24
+ const actionMatches = [...message.matchAll(/(?:action|event)["'\s:=]+([\w.-]+)/gi)].map((match) => match[1]);
25
+ for (const app of appMatches)
26
+ for (const action of actionMatches)
27
+ actions.set(`${app}:${action}`, { app, action });
28
+ return {
29
+ classification: 'repairable_client_input_error',
30
+ failed_operation: `${context.module}.${context.method}`,
31
+ implicated_actions: [...actions.values()],
32
+ rules: [
33
+ 'Do not retry the unchanged payload and do not guess a missing value.',
34
+ 'Treat the deepest provider/API 400 envelope as authoritative, even when wrapped by a Feature, Quota, or Fallback error.',
35
+ 'Redact credentials and never move authentication values into ordinary Feature or runtime input.',
36
+ ],
37
+ repair_steps: [
38
+ 'Read the complete nested error envelope and identify the failing provider, action, HTTP status, field, and input location.',
39
+ 'Call ductape_schema for the exact implicated app action using product_tag, env_slug, app_tag, and action_tag. This lookup is mandatory before changing input.',
40
+ 'For Feature, Quota, or Fallback execution, also fetch that component definition and follow its selected option/step to every referenced app action. Schema-check each failing action, not only the outer component.',
41
+ 'Compare supplied input against body, query, params, and headers separately. Restore schema-declared defaults and required constant query values; preserve explicit location prefixes when a name is ambiguous.',
42
+ 'If the live action contract itself omitted a required/default value, update its canonical ductape/actions/<app-tag>/<action-tag>.action.json and update the action through ductape_cli. Do not patch only generated Feature code.',
43
+ 'Call ductape_generate_payload for the repaired operation, validate the generated payload against the looked-up schema, then retry once.',
44
+ 'If the corrected call still returns 400, stop automatic retries and report the provider response plus the schema/payload mismatch that remains.',
45
+ ],
46
+ scope_notes: {
47
+ actions: 'Repair the direct app-action input.',
48
+ features: 'Repair the failing Feature step mapping or referenced action contract; do not special-case one Feature tag.',
49
+ fallback: 'Repair the input mapping/action contract for the failing provider option while retaining bounded failover.',
50
+ quotas: 'Repair the selected Quota option/action mapping; a Quota wrapper does not make a provider 400 retryable.',
51
+ },
52
+ };
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.36",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs",
18
+ "test": "npm run build && node scripts/check-cli-command-security.mjs && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-paystack-action-schema.mjs && node scripts/check-portable-functions.mjs && node scripts/check-feature-control-flow.mjs && node scripts/check-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-guidance.mjs && node scripts/check-database-action-contract-guidance.mjs && node scripts/check-runtime-sync-guidance.mjs && node scripts/check-runtime-input-recovery.mjs",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },