@ductape/mcp 0.2.33 → 0.2.35

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.
Files changed (2) hide show
  1. package/dist/index.js +163 -20
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -311,6 +311,31 @@ When the target application is a NestJS service or controller, always use @ducta
311
311
  instead of instantiating @ductape/sdk directly. It provides NestJS DI integration,
312
312
  global interceptors, decorators, and type-safe resource handles.
313
313
 
314
+ PRODUCT RUNTIME SNAPSHOT — REQUIRED FOR SERVER INITIALIZATION:
315
+ Always provide both product and env at module initialization. Nest awaits the SDK's initial
316
+ product runtime snapshot during onModuleInit, before the application accepts traffic. The
317
+ snapshot preloads connected App versions/actions plus database actions/schema metadata, graphs,
318
+ vectors, storage, brokers/topics, sessions, notifications, resilience assets, agents, functions,
319
+ and caches into the execution bootstrap cache. It also hydrates the selected Workbench App
320
+ connection for product+env (variables and token_access credentials). Do not emit api.config()
321
+ or api.oauth() boilerplate when that connection is already configured on the product. Explicit
322
+ api.config() is only a process-local override and has higher precedence. credential_access login
323
+ material remains isolated to the auth action and its encrypted refresh path. External provider/
324
+ database connections remain lazy and pooled.
325
+
326
+ Features, Quotas, Fallbacks, health checks, and direct api.run calls all reuse this same hydrated
327
+ connection through the action processor. Never create a second connection or copy secrets into a
328
+ Feature/Quota/Fallback definition. Reference the existing connected App access_tag and action tag.
329
+
330
+ Runtime synchronization is enabled by default when product + env are present. Do not set
331
+ runtimeSync:false merely to simplify generated code. Configure it only when the user has an
332
+ explicit operational reason:
333
+ runtimeSync: { intervalMs: 30_000, jitter: 0.2, maxBackoffMs: 300_000 }
334
+
335
+ Polling uses runtimeRevision + HTTP ETag/304. databaseRevision is a refresh hint included in the
336
+ umbrella runtimeRevision, so schema and Database Action changes are detected. Failed refreshes
337
+ retain the last-known-good snapshot. Nest stops polling through onModuleDestroy automatically.
338
+
314
339
  SETUP — register once in AppModule:
315
340
 
316
341
  ╔══════════════════════════════════════════════════════════════════════════╗
@@ -338,6 +363,7 @@ SETUP — register once in AppModule:
338
363
  product: 'my-product',
339
364
  env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
340
365
  redisUrl: process.env.DUCTAPE_REDIS_URL, // required — no dispatch() works without this
366
+ runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
341
367
  }),
342
368
  ],
343
369
  })
@@ -352,6 +378,7 @@ SETUP — register once in AppModule:
352
378
  product: cfg.get('DUCTAPE_PRODUCT'),
353
379
  env: cfg.get('DUCTAPE_ENV'),
354
380
  redisUrl: cfg.get('DUCTAPE_REDIS_URL'), // required — no dispatch() works without this
381
+ runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
355
382
  }),
356
383
  })
357
384
 
@@ -473,6 +500,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
473
500
  ductape_cli("products apps list --product <id_or_tag> --json") ← compact linked apps only
474
501
  ductape_cli("products apps actions list --product <id_or_tag> --app <app_tag> --json")
475
502
  ductape_cli("products apps actions get --product <id_or_tag> --app <app_tag> --action <action_tag> --json")
503
+ ductape_cli("products apps configure --product <id_or_tag> --app <app_or_access_tag> --connection-file ductape/apps/<app_tag>/connection.json --json")
476
504
 
477
505
  SDK method signatures (for reference, admin key only):
478
506
  product.create [data: { name, description, tag?, envs?: [{slug, name}] }]
@@ -484,6 +512,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
484
512
  product.apps.add [product_tag, app: { access_tag, envs: [{ app_env_slug, product_env_slug, variables?, auth? }] }]
485
513
  product.apps.list [product_tag]
486
514
  product.apps.fetch [product_tag, access_tag]
515
+ product.apps.update [product_tag, access_tag, { envs }] ← prefer CLI configure; it preserves omitted auth/variables
487
516
 
488
517
  ━━━ MODULE: app ━━━
489
518
  app.create [data: { app_name: string, description: string, unique?: boolean }]
@@ -1186,6 +1215,11 @@ const marketplaceConnectInputSchema = z.object({
1186
1215
  product_env_slug: z.string().min(1),
1187
1216
  })).min(1).describe('Explicit app-to-product environment mappings. Include every product environment that will use the app.'),
1188
1217
  });
1218
+ const appConnectionConfigureInputSchema = z.object({
1219
+ product_tag: z.string().min(1).describe('Product containing the existing App connection.'),
1220
+ app_tag: z.string().min(1).describe('Connected App tag or exact product access tag returned by products apps list.'),
1221
+ file: z.string().min(1).describe('Canonical JSON file under ductape/apps/<app-tag>/connection.json containing { envs: [...] }.'),
1222
+ });
1189
1223
  const marketplaceInspectInputSchema = z.object({
1190
1224
  app_tag: z.string().min(1).describe('Exact public app tag returned by ductape_marketplace_discover.'),
1191
1225
  });
@@ -1312,18 +1346,24 @@ function buildTypeScriptSnippet(payload, operationFamily, method) {
1312
1346
  const invocationArgs = buildSdkInvocationArgs(payload);
1313
1347
  return `import Ductape from "@ductape/sdk";
1314
1348
 
1349
+ const payload = ${toPrettyJson(payload)};
1315
1350
  const ductape = new Ductape({
1316
- workspace_id: process.env.DUCTAPE_WORKSPACE_ID!,
1317
- user_id: process.env.DUCTAPE_USER_ID!,
1318
- public_key: process.env.DUCTAPE_PUBLIC_KEY!,
1351
+ accessKey: process.env.DUCTAPE_ACCESS_KEY!,
1352
+ product: String(payload.product),
1353
+ env: String(payload.env),
1319
1354
  redis_url: process.env.DUCTAPE_REDIS_URL,
1355
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
1320
1356
  });
1321
1357
 
1322
1358
  async function run() {
1323
- const payload = ${toPrettyJson(payload)};
1359
+ await ductape.ready();
1324
1360
  const args = ${toPrettyJson(invocationArgs)};
1325
- const result = await ductape.${callPath}(args);
1326
- return { payload, result };
1361
+ try {
1362
+ const result = await ductape.${callPath}(args);
1363
+ return { payload, result };
1364
+ } finally {
1365
+ await ductape.close();
1366
+ }
1327
1367
  }
1328
1368
 
1329
1369
  run().catch(console.error);
@@ -1598,7 +1638,7 @@ function shellArgument(value) {
1598
1638
  }
1599
1639
  const docsInputSchema = z.object({
1600
1640
  topic: z.string().describe('Feature topic to look up. Supported: ' +
1601
- 'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
1641
+ 'transactions, presave, triggers, aggregations, migrations, indexes, performance, runtime-sync, actions, ' +
1602
1642
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
1603
1643
  'notifications, resilience, features, portable-functions, events, logs, migration, cli-authentication, frontend, frontend-analytics, client, react, vue'),
1604
1644
  });
@@ -2509,6 +2549,39 @@ DUCTAPE DATABASE PERFORMANCE GUIDANCE
2509
2549
  5. Caching
2510
2550
  - For read-heavy, rarely-changing data use caches.get before databases.query.
2511
2551
  - Invalidate cache keys in an afterWrite trigger (see ductape_docs({ topic: "triggers" })).
2552
+ `.trim(),
2553
+ 'runtime-sync': `
2554
+ DUCTAPE PRODUCT RUNTIME SNAPSHOTS
2555
+
2556
+ For every long-lived TypeScript/Node backend, initialize @ductape/sdk with accessKey, product,
2557
+ and env, then await ready() before accepting work:
2558
+
2559
+ const ductape = new Ductape({
2560
+ accessKey: process.env.DUCTAPE_ACCESS_KEY!,
2561
+ product: "payments",
2562
+ env: "prd",
2563
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2, max_backoff_ms: 300_000 },
2564
+ });
2565
+ await ductape.ready();
2566
+
2567
+ This fetches one environment-scoped product snapshot and primes the existing bootstrap cache with
2568
+ connected App versions/actions, databases/actions/table schemas, graphs, vectors, storage,
2569
+ brokers/topics, sessions, notifications, resilience assets, agents, functions, and caches.
2570
+ Connections remain lazy and pooled; snapshot bootstrap does not call external providers.
2571
+
2572
+ Runtime synchronization is automatic when product + env are supplied:
2573
+ - Conditional polling sends If-None-Match with runtimeRevision; unchanged state returns HTTP 304.
2574
+ - runtimeRevision is authoritative. databaseRevision, connectionRevision, secretRevision, and
2575
+ assetRevision are narrower change hints. Any databaseRevision change also changes runtimeRevision.
2576
+ - A changed snapshot is loaded off-path and atomically replaces cache entries. Deleted assets are removed.
2577
+ - 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.
2579
+ - Call close() during process shutdown. Do not create one Ductape instance per request.
2580
+
2581
+ For NestJS, use DuctapeModule.forIntegration({ accessKey, product, env, runtimeSync }) instead of
2582
+ new Ductape(). @ductape/nestjs awaits ready() in onModuleInit and closes the poller in
2583
+ onModuleDestroy. Never omit product/env when the service has stable defaults: doing so disables
2584
+ product snapshot bootstrap and returns execution to lazy per-asset bootstrap misses.
2512
2585
  `.trim(),
2513
2586
  actions: `
2514
2587
  DUCTAPE DATABASE ACTIONS
@@ -3015,20 +3088,49 @@ CONNECT A DISCOVERED APP THROUGH MCP:
3015
3088
  4. Match each product environment explicitly to one environment exposed by the app.
3016
3089
  5. Call ductape_marketplace_connect({ product_tag, app_tag, environments }).
3017
3090
  6. Verify with ductape_cli("products apps list --product <product_tag> --json").
3091
+ 7. If the App requires variables or auth, create the canonical file at
3092
+ ductape/apps/<app_tag>/connection.json and call ductape_app_connection_configure. A bare
3093
+ connect only establishes access + environment mapping; it does not invent configuration.
3018
3094
  Never guess environment mappings or action tags. Connecting mutates product configuration;
3019
3095
  obtain user approval when the user has not already requested the connection.
3020
3096
 
3021
- CONFIRMED LIMITATION (2026-08-17): both ductape_marketplace_connect and the equivalent
3022
- ductape_cli("products apps connect --product <tag> --app <app-tag> --env-map ...") reject any
3023
- app that is not marked public in the marketplace, with error
3024
- App "<tag>" is not public in the marketplace. This includes apps a user creates themselves under
3025
- their own tag (e.g. a private "ductape:paystack") — private/workspace-owned apps CANNOT be
3026
- connected to a product through MCP or CLI at all, only through Workbench, even though
3027
- ductape_marketplace_inspect can still read a private app's full action catalogue (inspect and
3028
- connect have different visibility rules). If you hit this, do not keep retrying — tell the user
3029
- the app must be connected via Workbench, and once they confirm it's connected, verify with
3030
- ductape_cli("products apps list --product <product_tag> --json") rather than attempting the
3031
- connect call again.
3097
+ Visibility: public marketplace Apps and private Apps owned by the current workspace can be
3098
+ connected. A private App owned by another workspace must be rejected; do not retry around that
3099
+ ownership boundary.
3100
+
3101
+ CONNECTION FILE REQUIRED SHAPE:
3102
+ File: ductape/apps/<app_tag>/connection.json
3103
+ {
3104
+ "envs": [{
3105
+ "product_env_slug": "prd",
3106
+ "app_env_slug": "production",
3107
+ "variables": [{ "key": "region", "value": "ng" }],
3108
+ "auth": {
3109
+ "auth_tag": "secret-key",
3110
+ "data": {
3111
+ "headers": { "Authorization": "$Secret{PAYSTACK_SECRET_KEY}" },
3112
+ "query": {}, "params": {}, "body": {}
3113
+ }
3114
+ }
3115
+ }]
3116
+ }
3117
+
3118
+ Before writing this file, inspect the exact App version/auth/action schemas. auth.data must have
3119
+ headers/query/params/body objects and must match the selected auth schema. Never guess an auth_tag,
3120
+ variable key, input location, or required field. Store credentials with secrets.create first and
3121
+ write $Secret{KEY}, never plaintext, into the connection file.
3122
+
3123
+ CREATE/REPAIR/ROTATE:
3124
+ ductape_cli("products apps configure --product <product_tag> --app <app_or_access_tag> \
3125
+ --connection-file ductape/apps/<app_tag>/connection.json --json")
3126
+ The command is merge-safe: an omitted auth or variables field preserves the existing saved field.
3127
+ Providing auth or variables explicitly replaces that field for the named product environment.
3128
+ It never returns saved credential values. Include only environments intentionally being changed;
3129
+ other existing mappings are retained. To repair an old connection with no mapping, add its row.
3130
+
3131
+ After configuration, verify the non-secret summary with products apps list, then inspect the exact
3132
+ action contract. Do not test a payment or other side-effecting action merely to verify connection
3133
+ persistence unless the user explicitly requests a safe test and supplies appropriate test input.
3032
3134
 
3033
3135
  ONLY after all five steps can any code call:
3034
3136
  ctx.api.run({ app: '<app_tag>', action: '<action_tag>', input: { ... } }) ← in a feature handler
@@ -3202,8 +3304,11 @@ Product structure (IProduct fields):
3202
3304
  workflows[] (features), models[], agents[], jobs[]
3203
3305
 
3204
3306
  Bootstrap (single API call returning product context + component config + private key):
3205
- Each service makes a single bootstrap call at first use; results are cached in BootstrapCache
3206
- (Redis when available). This avoids repeated round-trips in high-frequency paths.
3307
+ Initialize @ductape/sdk with product + env and await ductape.ready(). It fetches the full
3308
+ environment-scoped product runtime snapshot and primes BootstrapCache for connected App actions
3309
+ and product components. A service falls back to its targeted bootstrap endpoint only when the
3310
+ requested asset is absent. Polling uses ETag/304 and atomically refreshes changed state.
3311
+ See ductape_docs({ topic: "runtime-sync" }).
3207
3312
  `.trim(),
3208
3313
  sessions: `
3209
3314
  DUCTAPE SESSIONS
@@ -4537,8 +4642,12 @@ Import (register an EXISTING cloud resource):
4537
4642
  // dispatch() requires redis_url in the Ductape initialization options — it throws at runtime without it.
4538
4643
  const ductape = new Ductape({
4539
4644
  accessKey: process.env.DUCTAPE_ACCESS_KEY,
4645
+ product: "my-product",
4646
+ env: "prd",
4540
4647
  redis_url: process.env.DUCTAPE_REDIS_URL, // required for any dispatch(); omit only if never dispatching
4648
+ runtime_sync: { interval_ms: 30_000, jitter: 0.2 },
4541
4649
  });
4650
+ await ductape.ready();
4542
4651
  await ductape.events.produce({
4543
4652
  product: "my-product",
4544
4653
  env: "prd",
@@ -4561,6 +4670,7 @@ Import (register an EXISTING cloud resource):
4561
4670
  product: 'my-product',
4562
4671
  env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
4563
4672
  redisUrl: process.env.DUCTAPE_REDIS_URL, // required — dispatch() throws without this
4673
+ runtimeSync: { intervalMs: 30_000, jitter: 0.2 },
4564
4674
  }),
4565
4675
  });
4566
4676
  // Environment: DUCTAPE_REDIS_URL=redis://localhost:6379 (local) or rediss://:<pw>@host:6380 (managed)
@@ -5665,6 +5775,12 @@ const integrateEndpointHandler = async (args) => {
5665
5775
  'Run: ductape_cli("products apps connect --product <product_tag> --app <app_tag> --env-map ' +
5666
5776
  '<app_env>:<product_env> [repeat --env-map per environment] --json"). If the app is a public ' +
5667
5777
  'marketplace app, ductape_marketplace_connect does the same thing in one call.',
5778
+ 'If the App declares variables or auth, connecting is not the end: call ductape_schema for the ' +
5779
+ 'exact app/auth contract, store credential values with secrets.create, write ' +
5780
+ 'ductape/apps/<app_tag>/connection.json using $Secret{KEY} references, and call ' +
5781
+ 'ductape_app_connection_configure. Never put plaintext secrets in the asset file.',
5782
+ 'For an already-connected App, skip connect and use configure. Omitted auth/variables preserve ' +
5783
+ 'the current encrypted values, so mapping-only repairs must not force credential rotation.',
5668
5784
  ],
5669
5785
  });
5670
5786
  stages.push({
@@ -5867,6 +5983,11 @@ const DUCTAPE_FIRST_DOCTRINE = 'Follow this checklist before defaulting to plain
5867
5983
  '(ductape.functions.register/use) — register portable business logic once and reuse it across ' +
5868
5984
  'every Feature that needs it. Call ductape_schema before constructing any new resource file, ' +
5869
5985
  'so you match the exact required shape rather than guessing.\n\n' +
5986
+ '7. A Workbench App connection is perpetual product+environment runtime configuration. Once its ' +
5987
+ 'environment mapping, variables, and auth are saved, generated server code must initialize the ' +
5988
+ 'SDK with product+env and await ready(); do not repeat api.config/api.oauth in every codebase. ' +
5989
+ 'Direct actions, Features, Quotas, Fallbacks, and health checks reuse that hydrated connection. ' +
5990
+ 'Use api.config only when the user explicitly asks for a local override.\n\n' +
5870
5991
  'Default assumption for any backend built through this MCP server: orchestration goes through ' +
5871
5992
  'Features, provider risk goes through Quotas/Fallback, credentials go through Secrets, shared ' +
5872
5993
  'capability goes through reuse, and the ductape/ folder is what ships — not a database table ' +
@@ -6613,6 +6734,28 @@ async function main() {
6613
6734
  command: `products apps connect --product ${shellArgument(args.product_tag)} --app ${shellArgument(args.app_tag)}${mappings} --json`,
6614
6735
  });
6615
6736
  });
6737
+ server.registerTool('ductape_app_connection_configure', {
6738
+ title: 'Configure a Connected Ductape App',
6739
+ description: 'Configure, repair, or rotate an existing product App connection from its canonical JSON asset. ' +
6740
+ 'Inspect the App/auth schema first, store secrets separately, use $Secret{KEY} references, and ' +
6741
+ 'keep the file under ductape/apps/<app-tag>/connection.json. Omitted auth or variables preserve ' +
6742
+ 'the saved encrypted field; the tool never reads credential values back.',
6743
+ inputSchema: appConnectionConfigureInputSchema,
6744
+ }, async (args) => {
6745
+ const normalized = args.file.replace(/\\/g, '/');
6746
+ if (!/(^|\/)ductape\/apps\/[^/]+\/connection\.json$/.test(normalized)) {
6747
+ return {
6748
+ content: [{ type: 'text', text: JSON.stringify({
6749
+ error: 'INVALID_ASSET_PATH',
6750
+ message: 'Connection assets must be permanent JSON files at ductape/apps/<app-tag>/connection.json.',
6751
+ }, null, 2) }],
6752
+ isError: true,
6753
+ };
6754
+ }
6755
+ return cliHandler({
6756
+ command: `products apps configure --product ${shellArgument(args.product_tag)} --app ${shellArgument(args.app_tag)} --connection-file ${shellArgument(args.file)} --json`,
6757
+ });
6758
+ });
6616
6759
  server.registerTool('ductape_integrate_endpoint', {
6617
6760
  title: 'Integrate a New Endpoint',
6618
6761
  description: 'Call this FIRST whenever the user wants to integrate a new API endpoint — before writing any ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
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",
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",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },