@ductape/mcp 0.2.37 → 0.3.1
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":"
|
|
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"}
|
package/dist/action-contract.js
CHANGED
|
@@ -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 = [
|
|
@@ -1355,7 +1356,7 @@ const ductape = new Ductape({
|
|
|
1355
1356
|
product: String(payload.product),
|
|
1356
1357
|
env: String(payload.env),
|
|
1357
1358
|
redis_url: process.env.DUCTAPE_REDIS_URL,
|
|
1358
|
-
runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
|
|
1359
|
+
runtime_sync: { interval_ms: 30_000, jitter: 0.2, warm_connections: true },
|
|
1359
1360
|
});
|
|
1360
1361
|
|
|
1361
1362
|
async function run() {
|
|
@@ -2564,7 +2565,7 @@ and env, then await ready() before accepting work:
|
|
|
2564
2565
|
accessKey: process.env.DUCTAPE_ACCESS_KEY!,
|
|
2565
2566
|
product: "payments",
|
|
2566
2567
|
env: "prd",
|
|
2567
|
-
|
|
2568
|
+
runtime_sync: { interval_ms: 30_000, jitter: 0.2, max_backoff_ms: 300_000, warm_connections: true },
|
|
2568
2569
|
});
|
|
2569
2570
|
await ductape.ready();
|
|
2570
2571
|
|
|
@@ -2579,7 +2580,10 @@ Runtime synchronization is automatic when product + env are supplied:
|
|
|
2579
2580
|
assetRevision are narrower change hints. Any databaseRevision change also changes runtimeRevision.
|
|
2580
2581
|
- A changed snapshot is loaded off-path and atomically replaces cache entries. Deleted assets are removed.
|
|
2581
2582
|
- Refresh failure retains the last-known-good snapshot and retries with jittered exponential backoff.
|
|
2582
|
-
-
|
|
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.
|
|
2583
2587
|
- Call close() during process shutdown. Do not create one Ductape instance per request.
|
|
2584
2588
|
|
|
2585
2589
|
For NestJS, use DuctapeModule.forIntegration({ accessKey, product, env, runtimeSync }) instead of
|
|
@@ -3224,6 +3228,22 @@ Action input — flat input format:
|
|
|
3224
3228
|
input: { "headers:X-Idempotency-Key": "..." } → request header
|
|
3225
3229
|
Always use ductape_generate_payload or actions.fetch to know the exact field names — never guess.
|
|
3226
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
|
+
|
|
3227
3247
|
Run an action at runtime:
|
|
3228
3248
|
→ CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
|
|
3229
3249
|
ductape_execute("actions.run", [{ product, env, app, action, input: { "body:field": value } }])
|
|
@@ -3333,7 +3353,9 @@ Product structure (IProduct fields):
|
|
|
3333
3353
|
|
|
3334
3354
|
Bootstrap (single API call returning product context + component config + private key):
|
|
3335
3355
|
Initialize @ductape/sdk with product + env and await ductape.ready(). It fetches the full
|
|
3336
|
-
|
|
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
|
|
3337
3359
|
and product components. A service falls back to its targeted bootstrap endpoint only when the
|
|
3338
3360
|
requested asset is absent. Polling uses ETag/304 and atomically refreshes changed state.
|
|
3339
3361
|
See ductape_docs({ topic: "runtime-sync" }).
|
|
@@ -3687,7 +3709,7 @@ QUOTAS — weighted/provider-capacity routing pools (NOT request rate limiting):
|
|
|
3687
3709
|
input: { to: { type: "string", required: true }, message: { type: "string" } },
|
|
3688
3710
|
options: [
|
|
3689
3711
|
{ provider: "twilio", app: "twilio-app", type: "action", event: "send-sms",
|
|
3690
|
-
quota: 1000, uses: 0, retries: 2,
|
|
3712
|
+
quota: 1000, uses: 0, retries: 2, health: "twilio-health",
|
|
3691
3713
|
input: { "body:to": "$Input{to}", "body:message": "$Input{message}" },
|
|
3692
3714
|
output: {} },
|
|
3693
3715
|
{ provider: "nexmo", app: "nexmo-app", type: "action", event: "send-sms",
|
|
@@ -3709,13 +3731,54 @@ FALLBACKS — automatic provider switching on failure:
|
|
|
3709
3731
|
fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
3710
3732
|
fallback.dispatch [{ product, env, tag, input, schedule? }]
|
|
3711
3733
|
|
|
3734
|
+
HEALTH-GATED PROVIDERS — REQUIRED BEHAVIOR:
|
|
3735
|
+
Provider options may declare health: "<healthcheck-tag>". healthcheck is a deprecated input alias;
|
|
3736
|
+
generate health in new JSON and code. Before creating a quota/fallback, list the product's existing
|
|
3737
|
+
healthchecks and reuse one whose probe targets the same app/resource and operation. Create a new
|
|
3738
|
+
healthcheck only when no equivalent check exists. Verify the referenced tag exists in the same
|
|
3739
|
+
product and covers every environment used by the quota/fallback.
|
|
3740
|
+
|
|
3741
|
+
This is an active prerequisite, not optional advice. When creating or updating a quota/fallback:
|
|
3742
|
+
1. List existing healthchecks and connected product providers.
|
|
3743
|
+
2. Match each provider to an existing check by probe type + provider asset/app + safe probe event.
|
|
3744
|
+
3. For every unmatched provider, inspect its live action/resource schema, choose a repeat-safe probe,
|
|
3745
|
+
write ductape/healthchecks/<tag>.json, and run: ductape resources health create --tag <product>
|
|
3746
|
+
-f ductape/healthchecks/<tag>.json --json.
|
|
3747
|
+
4. Fetch and explicitly run each newly-created check in every target environment. Do not create the
|
|
3748
|
+
quota/fallback until the probe schema and status lookup both succeed.
|
|
3749
|
+
5. Put health: "<verified-healthcheck-tag>" on every provider option, then create/update and refetch
|
|
3750
|
+
the quota/fallback. A provider without a verified health tag is an incomplete resilience asset.
|
|
3751
|
+
If no safe repeatable probe exists, stop and explain the blocker; never invent an endpoint or omit
|
|
3752
|
+
health silently.
|
|
3753
|
+
|
|
3754
|
+
Runtime contract:
|
|
3755
|
+
- Read the linked health status before provider selection.
|
|
3756
|
+
- Never select a provider whose linked status is unavailable, even when every provider is down.
|
|
3757
|
+
Return NO_HEALTHY_PROVIDERS/NO_PROVIDERS_AVAILABLE instead of forcing traffic through it.
|
|
3758
|
+
- When a selected provider exhausts its bounded retries, immediately mark the linked healthcheck
|
|
3759
|
+
unavailable. The current fallback or quota invocation must immediately continue/reselect from
|
|
3760
|
+
the remaining healthy options; subsequent invocations must also skip it. Never wait for the
|
|
3761
|
+
health poll before performing local failover.
|
|
3762
|
+
- Runtime success does not close the circuit. Only a successful scheduled or explicit health probe
|
|
3763
|
+
restores available, preventing one opportunistic request from bypassing recovery validation.
|
|
3764
|
+
- One global Tickets-hosted scheduler is the fallback runner when no fresh local SDK monitor result
|
|
3765
|
+
exists. Do not require application code to call monitor() for platform healthchecks to run.
|
|
3766
|
+
- Remote and local runtimes share health through the product's centrally persisted health state,
|
|
3767
|
+
not through a common Redis instance. SDK runtime-manifest polling compares healthRevision and
|
|
3768
|
+
atomically refreshes an in-process health snapshot in the background. Quota/fallback routing uses
|
|
3769
|
+
synchronous snapshot reads; never add a bootstrap, Redis, or backend lookup to the invocation path.
|
|
3770
|
+
A local provider failure trips the in-process state immediately and persists it asynchronously so
|
|
3771
|
+
other instances receive it on their next lightweight poll.
|
|
3772
|
+
- A transition from available to unavailable sends one alert to accepted workspace participants;
|
|
3773
|
+
repeated unhealthy probe results must not generate duplicate transition emails.
|
|
3774
|
+
|
|
3712
3775
|
HEALTHCHECKS — continuous probe with failure notifications:
|
|
3713
|
-
Scheduled probes have a platform-owned fallback runner in the Ductape
|
|
3714
|
-
may also run them: while it consistently persists a fresh lastChecked record, the
|
|
3715
|
-
that product/env/check. The
|
|
3776
|
+
Scheduled probes have a platform-owned global fallback runner in the Ductape Tickets service. A local SDK monitor
|
|
3777
|
+
may also run them: while it consistently persists a fresh lastChecked record, the global worker defers
|
|
3778
|
+
that product/env/check. The server freshness window accounts for the SDK's batched backend status
|
|
3716
3779
|
flush (currently five minutes), not only the shorter probe interval. If records stop arriving and
|
|
3717
|
-
the freshness window expires, the
|
|
3718
|
-
disable the
|
|
3780
|
+
the freshness window expires, the global worker automatically takes over. Do not instruct users to
|
|
3781
|
+
disable the server fallback when using a local monitor.
|
|
3719
3782
|
|
|
3720
3783
|
Workbench definition shape:
|
|
3721
3784
|
{
|
|
@@ -4673,7 +4736,7 @@ Import (register an EXISTING cloud resource):
|
|
|
4673
4736
|
product: "my-product",
|
|
4674
4737
|
env: "prd",
|
|
4675
4738
|
redis_url: process.env.DUCTAPE_REDIS_URL, // required for any dispatch(); omit only if never dispatching
|
|
4676
|
-
runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
|
|
4739
|
+
runtime_sync: { interval_ms: 30_000, jitter: 0.2, warm_connections: true },
|
|
4677
4740
|
});
|
|
4678
4741
|
await ductape.ready();
|
|
4679
4742
|
await ductape.events.produce({
|
|
@@ -6329,7 +6392,15 @@ async function main() {
|
|
|
6329
6392
|
'Enable it in Workbench → Tokens → Publishable Key. ' +
|
|
6330
6393
|
'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.';
|
|
6331
6394
|
}
|
|
6332
|
-
|
|
6395
|
+
const recovery = runtimeInputRecovery(message, {
|
|
6396
|
+
module: args.module,
|
|
6397
|
+
method: args.method,
|
|
6398
|
+
params: args.params,
|
|
6399
|
+
});
|
|
6400
|
+
const text = recovery
|
|
6401
|
+
? JSON.stringify({ error: message, recovery }, null, 2)
|
|
6402
|
+
: `Error: ${message}`;
|
|
6403
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
6333
6404
|
}
|
|
6334
6405
|
};
|
|
6335
6406
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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 && node scripts/check-resilience-health-guidance.mjs",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|