@ductape/mcp 0.2.31 → 0.2.33
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 +398 -16
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -945,6 +945,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
945
945
|
// ctx.input – typed runtime input; always compiles to $Input{} operators
|
|
946
946
|
// ctx.sampleInput – compile-time sample for loop/branch discovery only
|
|
947
947
|
// ctx.step(tag, fn, rollback?, opts?) – define a durable step
|
|
948
|
+
// ctx.when.eq/ne/gt/gte/lt/lte/truthy/falsy(...) – build portable runtime conditions
|
|
949
|
+
// ctx.branch(condition, { then, else? }) – record both paths as conditioned steps
|
|
950
|
+
// ctx.each(ctx.sampleInput.items, async (item, index) => ...) – deterministic compile-time expansion
|
|
948
951
|
// ctx.api.run({ app, action, input }) – call an app action (NOT 'event' -- that field name
|
|
949
952
|
// only applies to ctx.database/ctx.notification/ctx.storage below, never ctx.api/ctx.action)
|
|
950
953
|
// ctx.database.execute({ database, action, input }) for a saved database action
|
|
@@ -1125,7 +1128,8 @@ const payloadGenerateInputSchema = z.object({
|
|
|
1125
1128
|
targets: z.record(z.any()).optional().describe('Identifies the specific operation to generate a payload for. ' +
|
|
1126
1129
|
'For actions: { app: "app_tag", action: "action_tag" }. ' +
|
|
1127
1130
|
'For features: { feature: "feature_tag" }. ' +
|
|
1128
|
-
'For databases: { database: "db_tag", table
|
|
1131
|
+
'For databases: { database: "db_tag", table?: "table_or_collection_name", action?: "action_tag" }. ' +
|
|
1132
|
+
' When generating code for a saved action, action is required and its static outputContract must be inspected. ' +
|
|
1129
1133
|
' Providing table is strongly recommended — the generator scans all actions configured for that table, ' +
|
|
1130
1134
|
' aggregates field definitions (name, type, required, sample value), and returns them in ' +
|
|
1131
1135
|
' meta.schema_context.database.fields. The where/data placeholders in the payload are also ' +
|
|
@@ -1158,11 +1162,16 @@ const snippetGenerateInputSchema = payloadGenerateInputSchema.extend({
|
|
|
1158
1162
|
language: z.enum(['typescript', 'python']).default('typescript'),
|
|
1159
1163
|
});
|
|
1160
1164
|
const schemaInputSchema = z.object({
|
|
1165
|
+
publishable_key: z.string().optional().describe('Optional publishable key for live database schema lookup. Omit when DUCTAPE_PUBLISHABLE_KEY is configured.'),
|
|
1161
1166
|
module: z.enum(['app', 'product']).optional().describe('Optional. Scope the result to one module. With no method, returns only the compact list of available methods.'),
|
|
1162
1167
|
method: z.string().optional().describe('Optional method key such as "databases.create" or "notifications.update". Use with module to return only that method schema.'),
|
|
1163
1168
|
product_tag: z.string().optional().describe('For live app-action discovery, the product the app is linked to. Must be supplied with app_tag and action_tag.'),
|
|
1164
1169
|
app_tag: z.string().optional().describe('For live app-action discovery, the linked app tag. Must be supplied with product_tag and action_tag.'),
|
|
1165
|
-
action_tag: z.string().optional().describe('For live app-action discovery, the exact action tag whose complete contract should be returned.'),
|
|
1170
|
+
action_tag: z.string().optional().describe('For live app-action or database-action discovery, the exact action tag whose complete contract should be returned.'),
|
|
1171
|
+
database_tag: z.string().optional().describe('For live database discovery, the database component tag. Requires product_tag and env_slug.'),
|
|
1172
|
+
env_slug: z.string().optional().describe('For live database discovery, the environment whose synchronized table schema and action contract should be used.'),
|
|
1173
|
+
table: z.string().optional().describe('For live database discovery, the exact table/collection. Required for direct query row contracts; inferred from a saved action when action_tag is supplied.'),
|
|
1174
|
+
database_operation: z.enum(['query', 'insert', 'update', 'delete', 'upsert', 'aggregate', 'raw']).optional().default('query').describe('Direct database primitive whose envelope should be combined with the table schema. Ignored when action_tag identifies a saved action.'),
|
|
1166
1175
|
});
|
|
1167
1176
|
const marketplaceDiscoverInputSchema = z.object({
|
|
1168
1177
|
query: z.string().min(1).describe('Capability or provider search, for example "payments" or "paystack".'),
|
|
@@ -1348,6 +1357,44 @@ function buildSnippet(language, payload, operationFamily, method) {
|
|
|
1348
1357
|
? buildPythonSnippet(payload, operationFamily, method)
|
|
1349
1358
|
: buildTypeScriptSnippet(payload, operationFamily, method);
|
|
1350
1359
|
}
|
|
1360
|
+
function buildDatabaseActionFeatureExample(targets, contract) {
|
|
1361
|
+
const database = targets.database ?? targets.database_tag ?? '<database-tag>';
|
|
1362
|
+
const action = targets.action ?? targets.action_tag ?? '<action-tag>';
|
|
1363
|
+
if (!contract?.normalizedOutput) {
|
|
1364
|
+
return `// Output contract unknown: do not infer a row, array, or envelope.\n` +
|
|
1365
|
+
`// Sync the table schema and fetch the Database Action contract before generating this Feature.`;
|
|
1366
|
+
}
|
|
1367
|
+
const operation = String(contract.operation ?? '').toUpperCase();
|
|
1368
|
+
if (operation === 'QUERY' || operation === 'READ') {
|
|
1369
|
+
return `type DatabaseActionResult = { data: Record<string, unknown>[]; count: number; fields?: string[] };\n\n` +
|
|
1370
|
+
`const queryResult = await ctx.step("${action}", () =>\n` +
|
|
1371
|
+
` ctx.database.execute<DatabaseActionResult>({\n` +
|
|
1372
|
+
` database: ${JSON.stringify(database)},\n` +
|
|
1373
|
+
` action: ${JSON.stringify(action)},\n` +
|
|
1374
|
+
` input: { /* values from ctx.input */ },\n` +
|
|
1375
|
+
` }),\n` +
|
|
1376
|
+
`);\n\n` +
|
|
1377
|
+
`await ctx.branch(ctx.when.eq(ctx.transform.length(queryResult.data), 0), {\n` +
|
|
1378
|
+
` then: async () => { /* handle successful empty query */ },\n` +
|
|
1379
|
+
` else: async () => { const row = queryResult.data[0]; /* consume validated row */ },\n` +
|
|
1380
|
+
`});`;
|
|
1381
|
+
}
|
|
1382
|
+
if ((operation === 'INSERT' || operation === 'CREATE') && contract.createdRecordPath === 'data[0]') {
|
|
1383
|
+
return `const insertResult = await ctx.step("${action}", () =>\n` +
|
|
1384
|
+
` ctx.database.execute<{ data: Record<string, unknown>[]; count: number; insertedIds: unknown[] }>({\n` +
|
|
1385
|
+
` database: ${JSON.stringify(database)}, action: ${JSON.stringify(action)}, input: { /* values */ },\n` +
|
|
1386
|
+
` }),\n` +
|
|
1387
|
+
`);\n` +
|
|
1388
|
+
`const createdRow = insertResult.data[0]; // No redundant follow-up query: contract returns rows.`;
|
|
1389
|
+
}
|
|
1390
|
+
return `// ${operation} returns ${contract.returnedRows ?? 'unknown'} rows.\n` +
|
|
1391
|
+
`const result = await ctx.step("${action}", () =>\n` +
|
|
1392
|
+
` ctx.database.execute({ database: ${JSON.stringify(database)}, action: ${JSON.stringify(action)}, input: { /* values */ } }),\n` +
|
|
1393
|
+
`);\n` +
|
|
1394
|
+
(contract.followUpQueryRequired
|
|
1395
|
+
? `// This contract returns mutation metadata only; add an explicit saved QUERY action before consuming a row.`
|
|
1396
|
+
: `// Consume only fields declared by normalizedOutput.`);
|
|
1397
|
+
}
|
|
1351
1398
|
// ─── CLI helpers ─────────────────────────────────────────────────────────────
|
|
1352
1399
|
// Per-process cache: avoids re-running whoami / workspaces use on every call.
|
|
1353
1400
|
let authState = 'unknown';
|
|
@@ -2472,12 +2519,124 @@ ctx.database.execute inside a Feature step invokes a saved action through its \`
|
|
|
2472
2519
|
Direct ctx.database.query/insert/update/delete calls retain their normal {table, where, data, ...}
|
|
2473
2520
|
shape and do not resolve a saved action.
|
|
2474
2521
|
|
|
2522
|
+
HARDENED DIRECT DATABASE PRIMITIVE CONTRACTS:
|
|
2523
|
+
Direct calls also return typed adapter envelopes; they do not return a raw row, raw array,
|
|
2524
|
+
boolean, or arbitrary T. Derive row fields from the selected table schema and use these exact
|
|
2525
|
+
normalized contracts:
|
|
2526
|
+
ctx.database.query<Row>(...) → { data: Row[], count: number, fields?: string[] }
|
|
2527
|
+
ctx.database.insert<Row>(...) → { data: Row[], count: number, insertedIds: unknown[] }
|
|
2528
|
+
ctx.database.update<Row>(...) → { data: Row[], count: number }
|
|
2529
|
+
ctx.database.delete(...) → { count: number, data?: Row[] }
|
|
2530
|
+
ctx.database.upsert<Row>(...) → { data: Row[], count: number, operation: "inserted"|"updated" }
|
|
2531
|
+
ctx.database.raw<Row>(...) → { data: Row[], count: number, fields?: string[], rowsAffected?: number }
|
|
2532
|
+
ctx.database.aggregate(...) → an alias-keyed aggregate object declared by operations; it is
|
|
2533
|
+
NOT a row envelope unless the selected adapter contract says so.
|
|
2534
|
+
|
|
2535
|
+
Never generate queryResult[0], queryResult.length, a boolean check on deleteResult, or direct
|
|
2536
|
+
field access such as insertResult.id. Query rows live at queryResult.data; delete success/count
|
|
2537
|
+
is deleteResult.count; inserted/updated rows exist in mutationResult.data only when returning is
|
|
2538
|
+
true. INSERT always includes insertedIds, but an inserted ID is not a complete row.
|
|
2539
|
+
|
|
2540
|
+
Direct primitive contracts are derived from the explicit method, adapter/version, table schema,
|
|
2541
|
+
and call options such as returning, returningColumns, select, limit, and aggregation aliases.
|
|
2542
|
+
Never infer row fields from domain expectations. If table schema is missing, call static schema
|
|
2543
|
+
discovery/sync and mark row fields unknown; do not execute the operation merely to learn shape.
|
|
2544
|
+
|
|
2545
|
+
DIRECT FEATURE-SAFE EXAMPLES:
|
|
2546
|
+
const queryResult = await ctx.step("find-customer", () =>
|
|
2547
|
+
ctx.database.query<Customer>({
|
|
2548
|
+
database: "payment-processing", table: "customers",
|
|
2549
|
+
where: { email: ctx.input.customerEmail }, limit: 1,
|
|
2550
|
+
}),
|
|
2551
|
+
);
|
|
2552
|
+
await ctx.branch(ctx.when.eq(ctx.transform.length(queryResult.data), 0), {
|
|
2553
|
+
then: async () => { /* create */ },
|
|
2554
|
+
else: async () => { const customer = queryResult.data[0]; /* consume */ },
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
const insertResult = await ctx.step("create-customer", () =>
|
|
2558
|
+
ctx.database.insert<Customer>({
|
|
2559
|
+
database: "payment-processing", table: "customers", data: { /* fields */ }, returning: true,
|
|
2560
|
+
}),
|
|
2561
|
+
);
|
|
2562
|
+
// returning:true declares created rows at insertResult.data; do not re-query.
|
|
2563
|
+
const createdCustomer = insertResult.data[0];
|
|
2564
|
+
|
|
2565
|
+
Empty { data: [], count: 0 } is successful. Thrown/explicit failure is a failed step. A successful
|
|
2566
|
+
value that violates the primitive's declared envelope is a platform contract violation and must
|
|
2567
|
+
fail at the producing step boundary. Never let a missing required row field flow to an app action.
|
|
2568
|
+
|
|
2475
2569
|
REUSE BEFORE CREATE:
|
|
2476
2570
|
1. ductape_cli("db actions list --database product_tag:database_tag --json")
|
|
2477
2571
|
2. Inspect likely matches with ductape_cli("db actions get product_tag:database_tag:action_tag --json").
|
|
2478
2572
|
3. Reuse the matching tag with database.execute/ctx.database.execute when operation, table,
|
|
2479
2573
|
template, parameters, and result semantics match. Create only when no exact functional match exists.
|
|
2480
2574
|
|
|
2575
|
+
OUTPUT CONTRACTS ARE MANDATORY DISCOVERY METADATA:
|
|
2576
|
+
Every action returned by databases.action.fetch/fetchAll includes outputContract and
|
|
2577
|
+
outputContractsByEnvironment. Read the contract for the target environment before writing a
|
|
2578
|
+
Feature. It contains input, normalizedOutput, rowSchema, cardinality, returnedRows,
|
|
2579
|
+
returnsAffectedRows, createdRecordPath, followUpQueryRequired, adapter, and adapterOutput.
|
|
2580
|
+
|
|
2581
|
+
Never infer a Database Action's output shape from its operation name, generic SDK knowledge,
|
|
2582
|
+
or application-domain expectations. Derive it from the database adapter, action definition,
|
|
2583
|
+
table schema metadata, and action outputContract. If the contract confidence is "unknown" or
|
|
2584
|
+
normalizedOutput is null, state that the output is unknown and request platform metadata; do
|
|
2585
|
+
not invent a raw row, array, envelope, or field. Static discovery must never execute the action.
|
|
2586
|
+
|
|
2587
|
+
MANDATORY DUCTAPE_SCHEMA ROW LOOKUP:
|
|
2588
|
+
The normalized envelope does NOT reveal the fields inside Row. Before generating code that reads
|
|
2589
|
+
result.data[n].field (or evaluates a row field in a Feature branch), call one of these exact forms:
|
|
2590
|
+
Saved Database Action:
|
|
2591
|
+
ductape_schema({ product_tag: "<product_tag>", env_slug: "<env_slug>",
|
|
2592
|
+
database_tag: "<database_tag>", action_tag: "<action_tag>" })
|
|
2593
|
+
Direct database query/primitive:
|
|
2594
|
+
ductape_schema({ product_tag: "<product_tag>", env_slug: "<env_slug>",
|
|
2595
|
+
database_tag: "<database_tag>", table: "<table>", database_operation: "query" })
|
|
2596
|
+
Use output_contract only for the saved action's operation-specific envelope. Use row_schema for
|
|
2597
|
+
the contents of each result.data item for BOTH saved actions and direct calls. Projection/select
|
|
2598
|
+
may narrow those fields further. If row_schema is empty or warnings report missing synchronized
|
|
2599
|
+
metadata, run \`ductape db schema push --db <database_tag> --env <env_slug>\`, retry
|
|
2600
|
+
ductape_schema, and keep row fields unknown until it succeeds. Never execute the query/action to
|
|
2601
|
+
discover its shape, and never substitute a generic product asset schema for this live lookup.
|
|
2602
|
+
|
|
2603
|
+
NORMALIZED ENVELOPES DIFFER BY OPERATION:
|
|
2604
|
+
QUERY → { data: Row[], count: number, fields?: string[] }
|
|
2605
|
+
INSERT → { data: Row[], count: number, insertedIds: unknown[] }
|
|
2606
|
+
UPDATE → { data: Row[], count: number }
|
|
2607
|
+
DELETE → { count: number, data?: Row[] }
|
|
2608
|
+
RAW/EXECUTE → { data: Row[], count: number, fields?: string[], rowsAffected?: number }
|
|
2609
|
+
returning controls whether mutation data contains rows. Do not describe all operations as the
|
|
2610
|
+
same envelope. Adapter-native details belong in adapterOutput; Feature code uses normalizedOutput.
|
|
2611
|
+
|
|
2612
|
+
FEATURE-SAFE QUERY PATTERN:
|
|
2613
|
+
type FindCustomerResult = { data: Customer[]; count: number; fields?: string[] };
|
|
2614
|
+
const queryResult = await ctx.step("find-customer", () =>
|
|
2615
|
+
ctx.database.execute<FindCustomerResult>({
|
|
2616
|
+
database: "payment-processing",
|
|
2617
|
+
action: "find-customer-by-email",
|
|
2618
|
+
input: { email: ctx.input.customerEmail },
|
|
2619
|
+
}),
|
|
2620
|
+
);
|
|
2621
|
+
await ctx.branch(ctx.when.eq(ctx.transform.length(queryResult.data), 0), {
|
|
2622
|
+
then: async () => { /* create */ },
|
|
2623
|
+
else: async () => {
|
|
2624
|
+
const customer = queryResult.data[0];
|
|
2625
|
+
/* consume customer only inside this structurally valid branch */
|
|
2626
|
+
},
|
|
2627
|
+
});
|
|
2628
|
+
Never generate queryResult[0] or ctx.transform.length(queryResult) for an envelope contract.
|
|
2629
|
+
{ data: [], count: 0 } is a successful empty query, not a failed action.
|
|
2630
|
+
|
|
2631
|
+
MUTATION FLOW:
|
|
2632
|
+
Inspect returnedRows, createdRecordPath, and followUpQueryRequired. If INSERT returns rows, use
|
|
2633
|
+
insertResult.data[0] directly and do not add a redundant resolve query. If it returns metadata
|
|
2634
|
+
only, explain that fact and add an explicit follow-up query. Never allow an undefined required
|
|
2635
|
+
id/email to reach a downstream app/provider action: validate it or make that step structurally
|
|
2636
|
+
dependent on queryResult.data[0] or the declared createdRecordPath. A thrown/failed response is
|
|
2637
|
+
a failed step; a successful value that violates normalizedOutput is a platform contract violation
|
|
2638
|
+
and must fail at the producing step boundary.
|
|
2639
|
+
|
|
2481
2640
|
Create/update/delete are ADMINISTRATIVE (access key) — FORBIDDEN through ductape_execute
|
|
2482
2641
|
(publishable key). Use ductape_cli, same as every other admin resource:
|
|
2483
2642
|
ductape_cli("db actions create --action-file ductape/database/actions/<action-tag>.action.json")
|
|
@@ -3373,6 +3532,20 @@ CONFIGURATION BOUNDARY
|
|
|
3373
3532
|
Workbench is also supported. Never route their administrative create/update methods through
|
|
3374
3533
|
ductape_execute: its publishable-key runtime proxy will fail.
|
|
3375
3534
|
|
|
3535
|
+
CLI COMMANDS (aliases are normalized; use these exact shapes):
|
|
3536
|
+
ductape resources feature list --product <product> --json
|
|
3537
|
+
ductape resources feature get -t <feature> --product <product> --json
|
|
3538
|
+
ductape resources quota list --product <product> --json
|
|
3539
|
+
ductape resources quota get -t <quota> --product <product> --json
|
|
3540
|
+
ductape resources fallback list --product <product> --json
|
|
3541
|
+
ductape resources fallback get -t <fallback> --product <product> --json
|
|
3542
|
+
ductape resources health list --product <product> --json
|
|
3543
|
+
ductape resources health get -t <healthcheck> --product <product> --json
|
|
3544
|
+
Create/update use the same resource type plus -f <asset.json>. Singular/plural feature(s),
|
|
3545
|
+
fallback(s), and healthcheck(s) aliases are accepted, but canonical generated commands should
|
|
3546
|
+
use feature, quota, fallback, and health. These commands are administrative catalogue CRUD;
|
|
3547
|
+
runtime run/execute operations belong to the SDK or authenticated execution surface.
|
|
3548
|
+
|
|
3376
3549
|
QUOTAS — weighted/provider-capacity routing pools (NOT request rate limiting):
|
|
3377
3550
|
Workbench definition shape:
|
|
3378
3551
|
{
|
|
@@ -3929,15 +4102,83 @@ STEP 6 — WRITE the feature into the project codebase
|
|
|
3929
4102
|
script does, invoked only through the explicit CLI command.
|
|
3930
4103
|
|
|
3931
4104
|
STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
4105
|
+
MANDATORY CONTROL-FLOW RULES:
|
|
4106
|
+
1. A stored Feature executes its compiled steps. The JavaScript handler is NOT rerun at runtime.
|
|
4107
|
+
2. Never use if/else, switch, ?:, ??, &&, ||, optional chaining, map, filter, find, some,
|
|
4108
|
+
every, reduce, for, for..of, forEach, or while on a ctx.step() result or ctx.input proxy.
|
|
4109
|
+
JavaScript would evaluate the recording proxy, not the future runtime value.
|
|
4110
|
+
3. Use ctx.branch(ctx.when.*, { then, else }) for runtime decisions.
|
|
4111
|
+
4. Use ctx.each only with a concrete ctx.sampleInput array to expand a fixed step graph.
|
|
4112
|
+
5. Put runtime-sized collection algorithms and arbitrary business logic in a registered
|
|
4113
|
+
portable Function, then invoke it through ctx.functions inside a step.
|
|
4114
|
+
6. Direct Date.now() and Math.random() are forbidden in handlers. Use ctx.transform.now(),
|
|
4115
|
+
ctx.transform.uuid(), and ctx.transform.concat/replace/substring/upper/lower/trim so values
|
|
4116
|
+
are generated at execution time. Use a portable Function for domain-specific generators.
|
|
4117
|
+
7. Never use branchOverrides in newly generated code. It exists only to migrate old handlers.
|
|
4118
|
+
|
|
4119
|
+
Prefer explicit portable branching for step results:
|
|
4120
|
+
const result = await ctx.step("find", () => ctx.database.execute(...));
|
|
4121
|
+
await ctx.branch(ctx.when.eq(ctx.transform.length(result.data), 0), {
|
|
4122
|
+
then: () => ctx.step("create", () => ctx.database.execute(...)),
|
|
4123
|
+
else: () => ctx.step("reuse", () => ctx.database.execute(...)),
|
|
4124
|
+
});
|
|
4125
|
+
→ ctx.when also supports ne, gt, gte, lt, lte, truthy, falsy, and/or.
|
|
4126
|
+
→ Do not use ordinary if/else, ternary, or ?? directly on an unresolved step result.
|
|
4127
|
+
→ branchOverrides remains a compatibility escape hatch for older Feature source.
|
|
4128
|
+
→ If both paths must feed a later common step, do not assume ctx.branch returns a selected
|
|
4129
|
+
value. Either record the downstream operation inside each path, persist a shared result that
|
|
4130
|
+
the later step can read, or move the find-or-create algorithm into one portable Function.
|
|
3935
4131
|
Loop over input array:
|
|
3936
|
-
→ Add recordInput: { items: [{ id: "1" }, { id: "2" }] } and
|
|
4132
|
+
→ Add recordInput: { items: [{ id: "1" }, { id: "2" }] } and call
|
|
4133
|
+
ctx.each(ctx.sampleInput.items, async (item, index) => { ... })
|
|
3937
4134
|
→ Use unique step tags per iteration (e.g. "process-" + item.id)
|
|
4135
|
+
→ map/filter/find/indexing on ctx.sampleInput are compile-time graph discovery only.
|
|
4136
|
+
→ Runtime-sized map/filter/find/some/every and arbitrary loops belong in a registered portable
|
|
4137
|
+
Function invoked through ctx.functions; never guess or freeze their results during recording.
|
|
3938
4138
|
Switch/if-else on feature input values:
|
|
3939
|
-
→
|
|
3940
|
-
→
|
|
4139
|
+
→ Prefer ctx.branch(ctx.when.eq(ctx.input.type, "a"), { then, else }).
|
|
4140
|
+
→ recordScenarios is for legacy compile-time graph discovery, not a runtime branch primitive.
|
|
4141
|
+
|
|
4142
|
+
SAFE COPY-READY PATTERNS:
|
|
4143
|
+
Runtime input branch:
|
|
4144
|
+
await ctx.branch(ctx.when.eq(ctx.input.kind, "refund"), {
|
|
4145
|
+
then: () => ctx.step("refund", () => ctx.api.run({ app, action: "refund", input: {...} })),
|
|
4146
|
+
else: () => ctx.step("charge", () => ctx.api.run({ app, action: "charge", input: {...} })),
|
|
4147
|
+
});
|
|
4148
|
+
Nested/indexed result check:
|
|
4149
|
+
await ctx.branch(ctx.when.eq(search.data.length, 0), { then: ..., else: ... });
|
|
4150
|
+
Compound condition:
|
|
4151
|
+
await ctx.branch(ctx.when.and(
|
|
4152
|
+
ctx.when.eq(result.status, "ready"),
|
|
4153
|
+
ctx.when.gt(result.count, 0),
|
|
4154
|
+
), { then: ..., else: ... });
|
|
4155
|
+
Runtime payment/reference string (never frozen at sync time):
|
|
4156
|
+
const reference = ctx.transform.concat(
|
|
4157
|
+
"pay_", ctx.transform.now(), "_", ctx.transform.uuid(),
|
|
4158
|
+
);
|
|
4159
|
+
await ctx.step("charge", () => ctx.fallback.execute({
|
|
4160
|
+
fallback: "charge-payment", input: { reference, ... },
|
|
4161
|
+
}));
|
|
4162
|
+
Fixed graph expansion:
|
|
4163
|
+
recordInput: { regions: ["ng", "gh"] },
|
|
4164
|
+
handler: async (ctx) => ctx.each(ctx.sampleInput.regions, async (region, index) => {
|
|
4165
|
+
await ctx.step("sync-region-" + index, () => ctx.functions.invoke(syncFn, "run", { region }));
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
UNSAFE — NEVER GENERATE:
|
|
4169
|
+
if (result.count === 0) await ctx.step(...);
|
|
4170
|
+
const customer = found[0] ?? await ctx.step(...);
|
|
4171
|
+
ctx.input.items.map(item => ctx.step(...));
|
|
4172
|
+
for (const item of ctx.input.items) await ctx.step(...);
|
|
4173
|
+
const reference = "pay_" + Date.now() + "_" + Math.random();
|
|
4174
|
+
|
|
4175
|
+
BEFORE SYNCING A FEATURE:
|
|
4176
|
+
→ Confirm every runtime decision uses ctx.branch + ctx.when.
|
|
4177
|
+
→ Confirm every ctx.step tag is unique across both paths and every expanded iteration.
|
|
4178
|
+
→ Confirm no runtime proxy is consumed by native JavaScript control flow or array iteration.
|
|
4179
|
+
→ Confirm every ctx.step contains a portable ctx component/function call.
|
|
4180
|
+
→ Compile locally, inspect schema.steps, and verify both branch paths, condition, depends_on,
|
|
4181
|
+
and operator-valued inputs are present before persisting.
|
|
3941
4182
|
|
|
3942
4183
|
STEP 8 — SET rollbacks for reversible steps
|
|
3943
4184
|
Any step that allocates a resource should undo it if a later step fails.
|
|
@@ -4047,16 +4288,18 @@ Feature statuses: pending | running | completed | failed | rolled_back | rolling
|
|
|
4047
4288
|
|
|
4048
4289
|
━━━ FEATURE RECORDING SEMANTICS ━━━
|
|
4049
4290
|
|
|
4050
|
-
When you call features.define({ handler }), the handler
|
|
4291
|
+
When you call features.define({ handler }), there are two distinct phases, but the handler itself
|
|
4292
|
+
runs only during compilation/recording:
|
|
4051
4293
|
|
|
4052
4294
|
1. RECORDING PHASE (at define time) — handler is called with a RecordingContext.
|
|
4053
4295
|
All ctx.step() calls return lightweight proxy objects, not real data.
|
|
4054
4296
|
This phase captures the step graph: which steps exist, their types, tags, and declared
|
|
4055
4297
|
inputs/outputs. No real API calls, DB queries, or side effects occur.
|
|
4056
4298
|
Arbitrary JS code OUTSIDE ctx.step() ALSO runs during recording — with proxy values.
|
|
4057
|
-
For loops: supply recordInput and
|
|
4299
|
+
For loops: supply recordInput and use ctx.each(ctx.sampleInput.items, ...) so all iterations are recorded.
|
|
4058
4300
|
ctx.input is always the runtime operator surface and must never expose recordInput literals.
|
|
4059
|
-
For branches: use
|
|
4301
|
+
For branches: use ctx.branch(ctx.when.*, { then, else }) so both paths are captured. Use
|
|
4302
|
+
branchOverrides only for compatibility with existing Feature handlers.
|
|
4060
4303
|
Never make an authorization, validation, tenancy, or other security decision by branching on
|
|
4061
4304
|
ctx.input during recording. Use ctx.sampleInput only to discover graph shape; enforce security
|
|
4062
4305
|
invariants inside a runtime portable Function or recorded step.
|
|
@@ -4066,11 +4309,15 @@ When you call features.define({ handler }), the handler runs TWICE:
|
|
|
4066
4309
|
integrations and project readiness hooks must not start HTTP listeners, provider probes,
|
|
4067
4310
|
schedulers, or Event consumers. Apply the filter before booting unrelated service modules.
|
|
4068
4311
|
|
|
4069
|
-
2. EXECUTION PHASE (at runtime) —
|
|
4070
|
-
|
|
4071
|
-
|
|
4312
|
+
2. EXECUTION PHASE (at runtime) — the stored schema.steps graph is interpreted by FeatureExecutor.
|
|
4313
|
+
The original JavaScript handler is NOT called. Step inputs and conditions resolve operators
|
|
4314
|
+
such as $Input{}, $Sequence{}, $Step{}, and $Now against runtime state. Only recorded portable
|
|
4315
|
+
component/function operations execute.
|
|
4072
4316
|
|
|
4073
|
-
Implication:
|
|
4317
|
+
Implication: arbitrary JavaScript control flow never becomes runtime behavior merely because it
|
|
4318
|
+
appears in the handler. Express runtime branches through ctx.branch/ctx.when, fixed graph expansion
|
|
4319
|
+
through ctx.each(ctx.sampleInput...), and runtime-sized algorithms through ctx.functions. Put all
|
|
4320
|
+
meaningful business logic INSIDE recorded portable operations, not in the
|
|
4074
4321
|
outer handler body. Code in the outer body runs during recording with proxy values and
|
|
4075
4322
|
may behave unexpectedly (e.g. typeof proxy === 'object' is true but .someField is a proxy).
|
|
4076
4323
|
|
|
@@ -5974,6 +6221,38 @@ async function main() {
|
|
|
5974
6221
|
const generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }), args);
|
|
5975
6222
|
const payload = generated?.payload ?? {};
|
|
5976
6223
|
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
6224
|
+
let databaseActionContract = null;
|
|
6225
|
+
let featureExample;
|
|
6226
|
+
if (args.operation_family === 'database') {
|
|
6227
|
+
const targets = (args.targets ?? {});
|
|
6228
|
+
const action = targets.action ?? targets.action_tag;
|
|
6229
|
+
const database = targets.database ?? targets.database_tag;
|
|
6230
|
+
if (action && database) {
|
|
6231
|
+
const databaseTag = String(database).startsWith(`${args.product_tag}:`)
|
|
6232
|
+
? String(database)
|
|
6233
|
+
: `${args.product_tag}:${database}`;
|
|
6234
|
+
const fullActionTag = String(action).startsWith(`${databaseTag}:`)
|
|
6235
|
+
? String(action)
|
|
6236
|
+
: `${databaseTag}:${action}`;
|
|
6237
|
+
const response = await cliHandler({
|
|
6238
|
+
command: `db actions get ${shellArgument(fullActionTag)} --json`,
|
|
6239
|
+
});
|
|
6240
|
+
if (!response.isError) {
|
|
6241
|
+
const raw = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
6242
|
+
try {
|
|
6243
|
+
const parsed = JSON.parse(raw);
|
|
6244
|
+
const definition = parsed?.data ?? parsed?.result ?? parsed;
|
|
6245
|
+
databaseActionContract = definition?.outputContractsByEnvironment?.[args.env_slug]
|
|
6246
|
+
?? definition?.outputContract
|
|
6247
|
+
?? null;
|
|
6248
|
+
}
|
|
6249
|
+
catch {
|
|
6250
|
+
databaseActionContract = null;
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
featureExample = buildDatabaseActionFeatureExample(targets, databaseActionContract);
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
5977
6256
|
return {
|
|
5978
6257
|
content: [
|
|
5979
6258
|
{
|
|
@@ -5981,6 +6260,8 @@ async function main() {
|
|
|
5981
6260
|
text: JSON.stringify({
|
|
5982
6261
|
payload: generated,
|
|
5983
6262
|
snippet,
|
|
6263
|
+
...(databaseActionContract ? { database_action_contract: databaseActionContract } : {}),
|
|
6264
|
+
...(featureExample ? { feature_example: featureExample } : {}),
|
|
5984
6265
|
}, null, 2),
|
|
5985
6266
|
},
|
|
5986
6267
|
],
|
|
@@ -5993,6 +6274,100 @@ async function main() {
|
|
|
5993
6274
|
};
|
|
5994
6275
|
const schemaHandler = async (args) => {
|
|
5995
6276
|
try {
|
|
6277
|
+
if (args.database_tag) {
|
|
6278
|
+
if (!args.product_tag || !args.database_tag || !args.env_slug) {
|
|
6279
|
+
throw new Error('product_tag, database_tag, and env_slug are required for live database schema discovery');
|
|
6280
|
+
}
|
|
6281
|
+
let action = null;
|
|
6282
|
+
let table = args.table;
|
|
6283
|
+
if (args.action_tag) {
|
|
6284
|
+
const databaseTag = args.database_tag.startsWith(`${args.product_tag}:`)
|
|
6285
|
+
? args.database_tag
|
|
6286
|
+
: `${args.product_tag}:${args.database_tag}`;
|
|
6287
|
+
const fullActionTag = args.action_tag.startsWith(`${databaseTag}:`)
|
|
6288
|
+
? args.action_tag
|
|
6289
|
+
: `${databaseTag}:${args.action_tag}`;
|
|
6290
|
+
const response = await cliHandler({
|
|
6291
|
+
command: `db actions get ${shellArgument(fullActionTag)} --json`,
|
|
6292
|
+
});
|
|
6293
|
+
if (response.isError)
|
|
6294
|
+
return response;
|
|
6295
|
+
const raw = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
6296
|
+
try {
|
|
6297
|
+
const parsed = JSON.parse(raw);
|
|
6298
|
+
action = parsed?.data ?? parsed?.result ?? parsed;
|
|
6299
|
+
table = table ?? action?.tableName;
|
|
6300
|
+
}
|
|
6301
|
+
catch {
|
|
6302
|
+
throw new Error(`Database Action metadata was not valid JSON: ${raw}`);
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
if (!table) {
|
|
6306
|
+
throw new Error('table is required for direct database schema discovery and could not be inferred from the action');
|
|
6307
|
+
}
|
|
6308
|
+
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
6309
|
+
if (!key) {
|
|
6310
|
+
throw new Error('Set DUCTAPE_PUBLISHABLE_KEY or pass publishable_key so ductape_schema can fetch synchronized table metadata');
|
|
6311
|
+
}
|
|
6312
|
+
const generated = await generateExecutablePayload({
|
|
6313
|
+
publishable_key: key,
|
|
6314
|
+
product_tag: args.product_tag,
|
|
6315
|
+
env_slug: args.env_slug,
|
|
6316
|
+
operation_family: 'database',
|
|
6317
|
+
method: args.action_tag
|
|
6318
|
+
? String(action?.operation ?? action?.type ?? 'query').toLowerCase()
|
|
6319
|
+
: (args.database_operation ?? 'query'),
|
|
6320
|
+
targets: {
|
|
6321
|
+
database: args.database_tag,
|
|
6322
|
+
table,
|
|
6323
|
+
...(args.action_tag ? { action: args.action_tag } : {}),
|
|
6324
|
+
},
|
|
6325
|
+
include_session: false,
|
|
6326
|
+
execution_context: 'system',
|
|
6327
|
+
schema_mode: 'best_effort',
|
|
6328
|
+
});
|
|
6329
|
+
const databaseSchema = generated?.meta?.schema_context?.database ?? null;
|
|
6330
|
+
const actionContract = args.action_tag
|
|
6331
|
+
? action?.outputContractsByEnvironment?.[args.env_slug] ?? action?.outputContract ?? null
|
|
6332
|
+
: null;
|
|
6333
|
+
const directOutputContracts = {
|
|
6334
|
+
query: { normalizedOutput: '{ data: Row[], count: number, fields?: string[] }', cardinality: 'many' },
|
|
6335
|
+
insert: { normalizedOutput: '{ data: Row[], count: number, insertedIds: unknown[] }', cardinality: 'many' },
|
|
6336
|
+
update: { normalizedOutput: '{ data: Row[], count: number }', cardinality: 'many' },
|
|
6337
|
+
delete: { normalizedOutput: '{ count: number, data?: Row[] }', cardinality: 'many' },
|
|
6338
|
+
upsert: { normalizedOutput: '{ data: Row[], count: number, operation: "inserted" | "updated" }', cardinality: 'many' },
|
|
6339
|
+
aggregate: { normalizedOutput: 'Record<aggregationAlias, number | unknown>', cardinality: 'one' },
|
|
6340
|
+
raw: { normalizedOutput: '{ data: Row[], count: number, fields?: string[], rowsAffected?: number }', cardinality: 'many' },
|
|
6341
|
+
};
|
|
6342
|
+
const outputContract = actionContract ?? directOutputContracts[args.database_operation ?? 'query'];
|
|
6343
|
+
const rowSchema = actionContract?.rowSchema ?? databaseSchema?.fields ?? {};
|
|
6344
|
+
return {
|
|
6345
|
+
content: [{
|
|
6346
|
+
type: 'text',
|
|
6347
|
+
text: JSON.stringify({
|
|
6348
|
+
scope: args.action_tag ? 'database_action' : 'database_primitive',
|
|
6349
|
+
product_tag: args.product_tag,
|
|
6350
|
+
env_slug: args.env_slug,
|
|
6351
|
+
database_tag: args.database_tag,
|
|
6352
|
+
table,
|
|
6353
|
+
operation: args.action_tag
|
|
6354
|
+
? action?.operation ?? action?.type
|
|
6355
|
+
: args.database_operation ?? 'query',
|
|
6356
|
+
input_contract: args.action_tag ? action?.data ?? [] : generated?.payload?.input,
|
|
6357
|
+
output_contract: outputContract,
|
|
6358
|
+
row_schema: rowSchema,
|
|
6359
|
+
database_schema: databaseSchema,
|
|
6360
|
+
action: args.action_tag ? action : undefined,
|
|
6361
|
+
warnings: generated?.meta?.schema_warnings ?? [],
|
|
6362
|
+
discovery_only: true,
|
|
6363
|
+
executed_action: false,
|
|
6364
|
+
guidance: outputContract?.normalizedOutput
|
|
6365
|
+
? 'Use output_contract for the envelope and row_schema for data item fields.'
|
|
6366
|
+
: 'Do not infer row fields or an action envelope when metadata is absent. Sync schema metadata and retry ductape_schema.',
|
|
6367
|
+
}, null, 2),
|
|
6368
|
+
}],
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
5996
6371
|
const liveActionScope = [args.product_tag, args.app_tag, args.action_tag];
|
|
5997
6372
|
if (liveActionScope.some(Boolean)) {
|
|
5998
6373
|
if (!liveActionScope.every(Boolean)) {
|
|
@@ -6124,11 +6499,18 @@ async function main() {
|
|
|
6124
6499
|
}, snippetGenerateHandler);
|
|
6125
6500
|
server.registerTool('ductape_schema', {
|
|
6126
6501
|
title: 'Ductape Asset Schema',
|
|
6127
|
-
description: 'Returns a compact method index, one targeted asset schema,
|
|
6502
|
+
description: 'Returns a compact method index, one targeted asset schema, one complete live app-action contract, ' +
|
|
6503
|
+
'or a live synchronized Database Action/direct database row contract. ' +
|
|
6128
6504
|
'Call with module="app" or module="product" first to list method keys, then call again with ' +
|
|
6129
6505
|
'module and method (for example method="databases.create") for the complete field schema. ' +
|
|
6130
6506
|
'For one linked app action, call with product_tag, app_tag, and action_tag; do not fetch a full app catalogue. ' +
|
|
6507
|
+
'For a saved Database Action, call with product_tag, env_slug, database_tag, and action_tag. ' +
|
|
6508
|
+
'For a direct database call, call with product_tag, env_slug, database_tag, table, and database_operation. ' +
|
|
6509
|
+
'Database results distinguish output_contract (the operation envelope) from row_schema (fields inside data rows). ' +
|
|
6510
|
+
'This lookup is discovery-only and never executes the database action or primitive. ' +
|
|
6131
6511
|
'Avoid calling without module unless you explicitly need the entire manifest.\n\n' +
|
|
6512
|
+
'ALWAYS call the live database form before generating code that reads fields from a saved Database Action ' +
|
|
6513
|
+
'or direct query result. If row_schema is unavailable, do not guess fields; sync the database schema and retry. ' +
|
|
6132
6514
|
'ALWAYS call this before constructing a file for "resources <type> create" or any cloud ' +
|
|
6133
6515
|
'import/provision operation — field shapes are not guessable from context.\n\n' +
|
|
6134
6516
|
'Conditional fields: some fields are returned as oneOf (an array of variant shapes). ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ductape/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
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-project-link-guidance.mjs && node scripts/check-graph-vector-projection-guidance.mjs && node scripts/check-asset-file-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",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx src/index.ts"
|
|
21
21
|
},
|