@ductape/mcp 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -55,7 +55,7 @@ Use an absolute path for `args[0]`.
55
55
 
56
56
  ## Tools
57
57
 
58
- The server exposes **three tools**:
58
+ The server exposes runtime, schema, documentation, CLI, discovery, migration, and setup tools. Core tools include:
59
59
 
60
60
  1. **`ductape_execute`**:
61
61
  - It runs any allowed SDK module method via the backend proxy.
@@ -82,6 +82,14 @@ The server exposes **three tools**:
82
82
  - ready-to-copy SDK snippet in `typescript` or `python`
83
83
  - Intended for engineers and copilots that need executable examples quickly.
84
84
 
85
+ 4. **`ductape_function_setup`**:
86
+ - Produces the secure local and remote setup for application functions referenced by portable Features.
87
+ - Requires an externally reachable HTTPS base URL (HTTP only for localhost development).
88
+ - Returns deterministic well-known routes, framework raw-body requirements, HMAC-SHA256 headers,
89
+ runtime verification steps, and fail-closed conditions.
90
+ - Agents must implement and verify the route; they must not claim remote availability from local registration alone.
91
+ - Agents must first extract native Ductape primitives from migrated code and reserve Functions for irreducible residual domain logic while preserving original transaction boundaries.
92
+
85
93
  The `ductape_cli` MCP tool also exposes public app discovery:
86
94
  `marketplace search <capability>`, `marketplace categories`, and
87
95
  `marketplace get <app_tag>`. Inspect the app before generating or executing an action payload.
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * the env var when provided.
11
11
  */
12
12
  import { createRequire } from 'module';
13
- import { execSync } from 'child_process';
13
+ import { execFileSync, execSync } from 'child_process';
14
14
  import { homedir } from 'os';
15
15
  import { delimiter, join } from 'path';
16
16
  import { z } from 'zod';
@@ -21,6 +21,60 @@ const MODULES = [
21
21
  'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
22
22
  'actions', 'features', 'jobs', 'logs', 'resilience', 'health', 'fallback', 'secrets',
23
23
  ];
24
+ const redisSetupInputSchema = z.object({
25
+ approved: z.boolean().describe('Must be true only after the user explicitly approves creating/starting a local Docker Redis container.'),
26
+ port: z.number().int().min(1024).max(65535).default(6379),
27
+ container_name: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/).default('ductape-redis'),
28
+ });
29
+ const redisSetupHandler = async (args) => {
30
+ const port = args.port ?? 6379;
31
+ const name = args.container_name ?? 'ductape-redis';
32
+ if (!args.approved) {
33
+ return {
34
+ content: [{ type: 'text', text: [
35
+ 'Redis materially reduces repeated Ductape bootstrap latency and is required for dispatch().',
36
+ 'Ask the user whether they approve pulling redis:7-alpine and creating a local Docker container bound to 127.0.0.1.',
37
+ 'Only call this tool again with approved=true after explicit approval.',
38
+ ].join('\n') }],
39
+ isError: true,
40
+ };
41
+ }
42
+ try {
43
+ execFileSync('docker', ['version', '--format', '{{.Server.Version}}'], { encoding: 'utf8', timeout: 15_000 });
44
+ }
45
+ catch {
46
+ return { content: [{ type: 'text', text: 'Docker is not installed or its daemon is unavailable. Install/start Docker, or supply a managed DUCTAPE_REDIS_URL.' }], isError: true };
47
+ }
48
+ let exists = false;
49
+ let running = false;
50
+ try {
51
+ exists = true;
52
+ running = execFileSync('docker', ['inspect', '-f', '{{.State.Running}}', name], { encoding: 'utf8', timeout: 10_000 }).trim() === 'true';
53
+ }
54
+ catch {
55
+ exists = false;
56
+ }
57
+ if (exists && !running)
58
+ execFileSync('docker', ['start', name], { encoding: 'utf8', timeout: 30_000 });
59
+ if (!exists) {
60
+ execFileSync('docker', [
61
+ 'run', '-d', '--name', name, '--restart', 'unless-stopped',
62
+ '-p', `127.0.0.1:${port}:6379`, 'redis:7-alpine',
63
+ 'redis-server', '--appendonly', 'yes',
64
+ ], { encoding: 'utf8', timeout: 120_000 });
65
+ }
66
+ execFileSync('docker', ['exec', name, 'redis-cli', 'ping'], { encoding: 'utf8', timeout: 10_000 });
67
+ return { content: [{ type: 'text', text: JSON.stringify({
68
+ ready: true,
69
+ container: name,
70
+ redis_url: `redis://127.0.0.1:${port}`,
71
+ next_steps: [
72
+ `Set DUCTAPE_REDIS_URL=redis://127.0.0.1:${port}`,
73
+ 'Pass redisUrl to @ductape/nestjs or redis_url/redis_client to the installed SDK API as supported by its types.',
74
+ 'Add the variable to local .env only; do not commit credentials for managed Redis.',
75
+ ],
76
+ }, null, 2) }] };
77
+ };
24
78
  // ─── Exhaustive SDK Method & Params Reference ────────────────────────────────
25
79
  // Built from a complete read of sdk/ts/src/index.ts (Ductape class public API).
26
80
  // Each entry follows: [module].[method] → params array signature.
@@ -213,6 +267,10 @@ SETUP — register once in AppModule:
213
267
  ║ ║
214
268
  ║ *.run(), events.produce(), and @Events.Consumer do NOT need Redis. ║
215
269
  ║ Only dispatch() does — and it is non-negotiable. ║
270
+ ║ ║
271
+ ║ Redis is STRONGLY RECOMMENDED even without dispatch(): every bootstrap ║
272
+ ║ domain uses memory + optional Redis caching, so shared/multi-process ║
273
+ ║ deployments avoid repeating control-plane reads after warm-up. ║
216
274
  ╚══════════════════════════════════════════════════════════════════════════╝
217
275
 
218
276
  import { DuctapeModule } from '@ductape/nestjs';
@@ -245,6 +303,10 @@ SETUP — register once in AppModule:
245
303
  DUCTAPE_REDIS_URL=redis://localhost:6379 # local dev
246
304
  DUCTAPE_REDIS_URL=rediss://:<password>@host:6380 # managed Redis (TLS)
247
305
 
306
+ If no Redis URL exists, proactively explain the latency/dispatch benefits and ask whether the
307
+ user wants a local Docker Redis dedicated to Ductape. After explicit approval, call
308
+ ductape_redis_setup({ approved: true }). Never pull an image or create/start a container before approval.
309
+
248
310
  INJECTING IN SERVICES AND CONTROLLERS — use @InjectContext():
249
311
 
250
312
  import { InjectContext, DuctapeContext } from '@ductape/nestjs';
@@ -773,11 +835,12 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
773
835
  queries?: { queryName: { handler?: function } },
774
836
  options?: { timeout?: number, retries?: number },
775
837
  envs?: [{ slug: string, active?: boolean }],
776
- recordInput?: object, // sample input for step-recording (run once during define)
838
+ recordInput?: object, // exposed only as ctx.sampleInput while recording
777
839
  recordScenarios?: object[], // multiple recording scenarios for branching
778
840
  branchOverrides?: object, // force step results during recording to reach later branches
779
841
  handler: async (ctx) => {
780
- // ctx.input – typed feature input
842
+ // ctx.input – typed runtime input; always compiles to $Input{} operators
843
+ // ctx.sampleInput – compile-time sample for loop/branch discovery only
781
844
  // ctx.step(tag, fn, rollback?, opts?) – define a durable step
782
845
  // ctx.api.run({ app, event, input }) – call an app action
783
846
  // ctx.database.query/insert/update/delete({ database, event, ... })
@@ -1115,6 +1178,7 @@ const ductape = new Ductape({
1115
1178
  workspace_id: process.env.DUCTAPE_WORKSPACE_ID!,
1116
1179
  user_id: process.env.DUCTAPE_USER_ID!,
1117
1180
  public_key: process.env.DUCTAPE_PUBLIC_KEY!,
1181
+ redis_url: process.env.DUCTAPE_REDIS_URL,
1118
1182
  });
1119
1183
 
1120
1184
  async function run() {
@@ -1349,13 +1413,20 @@ const docsInputSchema = z.object({
1349
1413
  topic: z.string().describe('Feature topic to look up. Supported: ' +
1350
1414
  'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
1351
1415
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
1352
- 'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
1416
+ 'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
1353
1417
  });
1354
1418
  const eventsDiscoveryInputSchema = z.object({
1355
1419
  query: z.string().optional().describe('Capability or recovery search, including DLQ, dead letter, failed messages, retry, poison message, replay, or consumer failure.'),
1356
1420
  product: z.string().optional().describe('Product tag used for live component inspection.'),
1357
1421
  component: z.string().optional().describe('Events component/broker tag used for live inspection.'),
1358
1422
  });
1423
+ const portableFunctionSetupInputSchema = z.object({
1424
+ framework: z.enum(['express', 'nestjs', 'fastify', 'other']).describe('Application HTTP framework.'),
1425
+ base_url: z.string().url().describe('Externally reachable HTTPS origin. HTTP is accepted only for localhost development.'),
1426
+ namespace: z.string().min(1).describe('Portable function namespace.'),
1427
+ version: z.string().min(1).describe('Contract version.'),
1428
+ operations: z.array(z.string().min(1)).min(1).describe('Operation names that must be registered and exposed.'),
1429
+ });
1359
1430
  const migrationInputSchema = z.object({
1360
1431
  source: z.string().describe('Absolute path to the existing codebase.'),
1361
1432
  e2e_baseline: z.string().describe('Absolute path to a passing migration-e2e baseline manifest created before migration inspection.'),
@@ -1371,6 +1442,96 @@ const migrationInputSchema = z.object({
1371
1442
  write: z.boolean().optional().default(false).describe('Write redacted advisory artifacts only. This never writes application code or executable Ductape assets.'),
1372
1443
  });
1373
1444
  const DOCS = {
1445
+ 'portable-functions': `
1446
+ PORTABLE APPLICATION FUNCTIONS
1447
+
1448
+ Use portable functions when a Feature needs application-owned logic that cannot be expressed with
1449
+ database, action, Event, storage, graph, vector, session, quota, fallback, or transform primitives.
1450
+
1451
+ PRIMITIVES-FIRST DECOMPOSITION — REQUIRED
1452
+ Before creating a Function, inspect the implementation, its callees, side effects, transaction
1453
+ boundary, configuration, and tests. Extract every operation already represented by a Ductape
1454
+ primitive. A Function is the residual application-owned behavior after that extraction; it is not
1455
+ a wrapper around an entire existing service method merely because that method already exists.
1456
+
1457
+ Classify each observed behavior:
1458
+ database read/write/transaction/index → database primitive or database action
1459
+ external provider/API → connected App action
1460
+ publish/consume asynchronous message → Events primitive
1461
+ session creation/validation/revocation → session primitive
1462
+ file/blob operation → storage primitive
1463
+ graph/vector operation → graph/vector primitive
1464
+ email/SMS/push/chat delivery → notification primitive
1465
+ quota/fallback/health/cache → corresponding Ductape primitive
1466
+ multi-step product capability → Feature orchestration
1467
+ reusable domain decision/transformation → portable Function candidate
1468
+ formatting/hash/redaction with one caller→ ordinary utility unless portability is required
1469
+
1470
+ For a mixed legacy method, split the boundary. Keep its irreducible domain decision in a Function
1471
+ and orchestrate extracted primitives as named Feature steps. Preserve atomicity: do not split a
1472
+ database transaction into independently committed steps unless the original contract permits it;
1473
+ use a database action or a Function with an explicitly authorized transactional capability instead.
1474
+
1475
+ Every proposed Function must include evidence for:
1476
+ - why no existing Ductape primitive expresses it;
1477
+ - exact business input/output, errors, side effects, idempotency, and transaction semantics;
1478
+ - whether it is pure and therefore a future signed-WASM candidate;
1479
+ - why it needs local, Events, or HTTPS availability;
1480
+ - which extracted primitives remain separate Feature steps.
1481
+
1482
+ Non-negotiable rule: arbitrary JavaScript/TypeScript callbacks are not serializable. Never write
1483
+ ctx.step('x', () => applicationService.method()) and assume the method will run elsewhere. The
1484
+ Feature compiler must reject a step that records no portable operation.
1485
+
1486
+ Canonical TypeScript pattern:
1487
+ const AuthFunctions = defineFunctions({
1488
+ namespace: 'statecraft-auth', version: '1',
1489
+ operations: {
1490
+ register: {
1491
+ input: { type: 'object', required: ['email', 'password'], properties: {
1492
+ email: { type: 'string' }, password: { type: 'string', minLength: 12 }
1493
+ }, additionalProperties: false },
1494
+ output: { type: 'object', required: ['token', 'player'], properties: {
1495
+ token: { type: 'string' }, player: { type: 'object' }
1496
+ } },
1497
+ transports: [{ type: 'local' }],
1498
+ handler: (input, context) => authService.register(input.email, input.password)
1499
+ }
1500
+ }
1501
+ });
1502
+ ductape.functions.register(AuthFunctions);
1503
+ await ductape.feature.define({
1504
+ tag: 'register-player', name: 'Register Player',
1505
+ input: { email: { type: 'string', required: true }, password: { type: 'string', required: true } },
1506
+ handler: async ctx => {
1507
+ const auth = ctx.functions.use(AuthFunctions);
1508
+ return ctx.step('register', () => auth.register({ email: ctx.input.email, password: ctx.input.password }));
1509
+ }
1510
+ });
1511
+
1512
+ The compiled step stores namespace, operation, version, input/output JSON Schemas, timeout,
1513
+ idempotency declaration, and permitted transports. It never stores the handler or a sample result.
1514
+
1515
+ Resolution order is local registered handler, then a contract-declared signed HTTP transport.
1516
+ HTTP function calls use Ductape HMAC-SHA256 headers. Raw arbitrary URLs and unsigned invocation are
1517
+ not supported. Application endpoints should use handlePortableFunctionHttpRequest and must receive
1518
+ the raw request body so signature verification covers exactly the bytes sent.
1519
+
1520
+ The feature session is inherited automatically in invocation.context.session, not mixed into the
1521
+ business input. Invocation context also includes product, env, workspace_id, feature_id,
1522
+ feature_tag, step_tag, invocation_id, and deadline_at.
1523
+
1524
+ Failure behavior is strict:
1525
+ FUNCTION_UNAVAILABLE no local handler or configured HTTP transport
1526
+ FUNCTION_SCHEMA_VALIDATION_FAILED input or output violates the contract
1527
+ FUNCTION_SIGNATURE_INVALID HTTP signature is absent, invalid, or expired
1528
+ FUNCTION_CORRELATION_MISMATCH response invocation_id differs from request
1529
+ FUNCTION_TIMEOUT operation exceeded its declared deadline
1530
+
1531
+ Do not invent a function contract. Inspect the application's defineFunctions declarations and use
1532
+ the exact namespace, operation, version, schemas, and transports. If no contract exists, add one in
1533
+ application code and explicitly register its runtime implementation.
1534
+ `.trim(),
1374
1535
  migration: `
1375
1536
  DUCTAPE CODEBASE MIGRATION GUIDE
1376
1537
 
@@ -1405,6 +1566,39 @@ START
1405
1566
  5. Call again with write=true only after confirming the destination. This writes guidance artifacts only.
1406
1567
  6. Call with ensure_product=true when product inventory confirms the product is absent.
1407
1568
 
1569
+ PRIMITIVES-FIRST CAPABILITY EXTRACTION — REQUIRED FOR EVERY MIGRATION SLICE
1570
+ Do not translate controllers, services, handlers, or exported functions one-for-one into Features
1571
+ or portable Functions. For each candidate capability, trace entry points, callees, state changes,
1572
+ external calls, errors, authorization, session usage, transactions, retries, and tests, then create
1573
+ a decomposition ledger with these classifications:
1574
+
1575
+ DUCTAPE_PRIMITIVE database, App action, Events, session, storage, graph, vector,
1576
+ notification, cache, quota, fallback, healthcheck, secret
1577
+ FEATURE user/product capability coordinating multiple meaningful operations
1578
+ PORTABLE_FUNCTION residual reusable domain logic that primitives cannot express
1579
+ CHILD_FEATURE independently meaningful capability with its own contract/lifecycle
1580
+ UTILITY local implementation detail with no independent product contract
1581
+ INFRASTRUCTURE_ADAPTER framework/provider plumbing replaced by a Ductape primitive
1582
+
1583
+ The required order is:
1584
+ 1. Extract and inventory Ductape primitives from the existing implementation.
1585
+ 2. Establish the capability and transaction boundary from behavior and tests.
1586
+ 3. Design named Feature steps around those primitives.
1587
+ 4. Put only irreducible application-owned logic behind ctx.functions.
1588
+ 5. For each Function, record pure/WASM-candidate versus framework-dependent classification.
1589
+ 6. Call ductape_function_setup and implement verified local plus remote availability.
1590
+
1591
+ Never create a Function that merely hides database, session, Events, storage, notification, graph,
1592
+ vector, quota, fallback, healthcheck, cache, secret, or connected-App work that the Feature can
1593
+ express directly. Never fragment an original atomic transaction just to maximize primitive count.
1594
+ A mixed method may legitimately become several primitive steps plus one small Function, one database
1595
+ action, or one capability-scoped Function when atomicity requires co-location.
1596
+
1597
+ Before implementation, present a decomposition table with: source behavior, evidence location,
1598
+ classification, selected Ductape primitive/function, input/output, side effects, transaction owner,
1599
+ session behavior, failure semantics, and verification test. If the residual Function has no clear
1600
+ reason to exist after primitive extraction, do not create it.
1601
+
1408
1602
  FINAL E2E ACCEPTANCE GATE
1409
1603
  End the migration by running the exact original E2E command against the migrated codebase while retaining
1410
1604
  the original checksum-bound suite. Do not silently edit, delete, skip, quarantine, or weaken baseline tests.
@@ -3284,7 +3478,8 @@ STEP 2 — INVENTORY existing Ductape components
3284
3478
  STEP 3 — PLAN each step
3285
3479
  For every logical step:
3286
3480
  a. Identify whether it is local domain logic, a child Feature, or an existing Ductape component.
3287
- Local typed domain logic may run inside ctx.step; it does not require an Event or App.
3481
+ Application-owned logic must be a registered portable function called through ctx.functions.
3482
+ A plain callback that calls an application service is not serializable and must fail compilation.
3288
3483
  If a step calls an external service, it MUST go through a registered Ductape App.
3289
3484
  If no App for that service exists in the product → mark it "App to create: <service name>".
3290
3485
  DO NOT plan a raw HTTP call, a direct dispatch to a URL, or any workaround in place of a missing App.
@@ -3292,6 +3487,14 @@ STEP 3 — PLAN each step
3292
3487
  c. Decide if a rollback handler is needed (e.g. charge → refund on later failure).
3293
3488
  Rollback is optional and is not a Feature qualification requirement.
3294
3489
  d. Decide allow_fail: true for non-critical steps (email, analytics, audit logs)
3490
+ e. If the step needs application-owned code, search the repository for an existing
3491
+ defineFunctions contract and runtime registration. If none exists, design the exact contract.
3492
+ Then ALWAYS call ductape_function_setup with the detected framework, namespace, version,
3493
+ operations, and intended external base URL. Proactively inspect deployment configuration to
3494
+ discover that URL and implement both local registration and the signed HTTPS adapter. If no
3495
+ deployable HTTPS origin exists, report that explicit blocker instead of stopping at a local-only
3496
+ setup. Do not mark the function remotely available until the
3497
+ well-known route is reachable and rejects an invalid signature with HTTP 401.
3295
3498
 
3296
3499
  STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating anything
3297
3500
  Show the user:
@@ -3329,7 +3532,7 @@ STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
3329
3532
  → Add branchOverrides: { stepTag: { field: value } } so all branches are recorded
3330
3533
  → At runtime the executor evaluates the real result and skips or runs later steps accordingly
3331
3534
  Loop over input array:
3332
- → Add recordInput: { items: [{ id: "1" }, { id: "2" }] } so the loop runs during recording
3535
+ → Add recordInput: { items: [{ id: "1" }, { id: "2" }] } and iterate ctx.sampleInput.items
3333
3536
  → Use unique step tags per iteration (e.g. "process-" + item.id)
3334
3537
  Switch/if-else on feature input values:
3335
3538
  → Add recordScenarios: [{ type: "a" }, { type: "b" }] — handler runs once per scenario
@@ -3344,7 +3547,7 @@ STEP 8 — SET rollbacks for reversible steps
3344
3547
  async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
3345
3548
  );
3346
3549
 
3347
- Step types: local_domain | action | database | graph | notification | storage | produce | quota |
3550
+ Step types: function | action | database | graph | notification | storage | produce | quota |
3348
3551
  fallback | vector | child_feature | sleep | wait_for_signal | checkpoint
3349
3552
 
3350
3553
  Valid synchronous Feature candidates include generate-world, resolve-nation-turn,
@@ -3439,7 +3642,8 @@ When you call features.define({ handler }), the handler runs TWICE:
3439
3642
  This phase captures the step graph: which steps exist, their types, tags, and declared
3440
3643
  inputs/outputs. No real API calls, DB queries, or side effects occur.
3441
3644
  Arbitrary JS code OUTSIDE ctx.step() ALSO runs during recording — with proxy values.
3442
- For loops: use recordInput so the handler sees sample data and all iterations are recorded.
3645
+ For loops: supply recordInput and iterate ctx.sampleInput so all iterations are recorded.
3646
+ ctx.input is always the runtime operator surface and must never expose recordInput literals.
3443
3647
  For branches: use branchOverrides so each path is captured.
3444
3648
 
3445
3649
  2. EXECUTION PHASE (at runtime) — handler is called with a real ExecutionContext.
@@ -3450,10 +3654,12 @@ When you call features.define({ handler }), the handler runs TWICE:
3450
3654
  outer handler body. Code in the outer body runs during recording with proxy values and
3451
3655
  may behave unexpectedly (e.g. typeof proxy === 'object' is true but .someField is a proxy).
3452
3656
 
3453
- A ctx.step callback may call ordinary local domain functions and injected/application services
3454
- available to the registration scope. This is the normal shape for a synchronous capability such
3455
- as pricing, entitlement evaluation, route-capacity calculation, or turn resolution. Keep the
3456
- meaningful work inside ctx.step callbacks so recording does not execute it.
3657
+ A ctx.step callback MUST NOT close over ordinary local domain functions or injected/application
3658
+ services. Closures are not portable and compilation fails closed. Define application-owned logic
3659
+ with defineFunctions, register its local runtime handler, and invoke it through ctx.functions.
3660
+ The MCP workflow must also attempt secure remote availability: detect the framework and deployment
3661
+ origin, install the signed well-known HTTPS adapter, and verify it. Local registration alone is
3662
+ insufficient for a Feature intended to execute outside the application process.
3457
3663
 
3458
3664
  Use an Event when the operation genuinely crosses an asynchronous process/service boundary,
3459
3665
  needs broker delivery semantics, or must be consumed independently. Do not produce an Event merely
@@ -4429,6 +4635,59 @@ const docsHandler = async (args) => {
4429
4635
  : '';
4430
4636
  return { content: [{ type: 'text', text: doc + structured }] };
4431
4637
  };
4638
+ const portableFunctionSetupHandler = async (args) => {
4639
+ const parsed = new URL(args.base_url);
4640
+ const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
4641
+ if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
4642
+ return { content: [{ type: 'text', text: JSON.stringify({
4643
+ ok: false,
4644
+ error: 'FUNCTION_INSECURE_TRANSPORT',
4645
+ message: 'Use HTTPS for portable function endpoints. HTTP is allowed only for localhost development.',
4646
+ }, null, 2) }], isError: true };
4647
+ }
4648
+ const base = args.base_url.replace(/\/$/, '');
4649
+ const routes = args.operations.map(operation => ({
4650
+ operation,
4651
+ url: `${base}/.well-known/ductape/functions/${encodeURIComponent(args.namespace)}/${encodeURIComponent(args.version)}/${encodeURIComponent(operation)}`,
4652
+ }));
4653
+ const adapter = args.framework === 'nestjs'
4654
+ ? `Configure rawBody: true in NestFactory.create, then add a POST controller route at\n` +
4655
+ `/.well-known/ductape/functions/:namespace/:version/:operation. Pass request.rawBody, headers,\n` +
4656
+ `and params to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
4657
+ : args.framework === 'fastify'
4658
+ ? `Register a raw-body plugin for only the well-known function route, then pass rawBody, headers,\nparams to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
4659
+ : args.framework === 'express'
4660
+ ? `Mount express.raw({ type: 'application/json' }) before JSON parsing on the well-known function route,\nthen pass request.body, headers, and params to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
4661
+ : `Preserve the exact raw JSON request bytes and pass body, headers, and route params to\nhandlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`;
4662
+ return { content: [{ type: 'text', text: JSON.stringify({
4663
+ ok: true,
4664
+ requiredActions: [
4665
+ 'Prove that database, App, Events, session, storage, graph, vector, notification, cache, quota, fallback, healthcheck, and secret primitives were extracted before defining this residual application Function.',
4666
+ 'Record whether the residual logic is pure/signed-WASM-capable or framework-dependent, including transaction and side-effect boundaries.',
4667
+ 'Create or locate one defineFunctions contract with exact input/output JSON Schemas.',
4668
+ 'Register handlers with ductape.functions.register during application startup.',
4669
+ `Set DUCTAPE_FUNCTION_BASE_URL=${JSON.stringify(base)} in the application runtime.`,
4670
+ 'Mount the signed well-known HTTP adapter and preserve the exact raw body.',
4671
+ 'Do not add bearer keys, access keys, static headers, or unsigned routes to Feature JSON.',
4672
+ 'Verify invalid signatures receive HTTP 401 and missing handlers receive HTTP 503.',
4673
+ 'Execute each operation and verify actual business state plus Feature/step processor records.',
4674
+ ],
4675
+ routes,
4676
+ adapter,
4677
+ security: {
4678
+ scheme: 'HMAC-SHA256',
4679
+ signedValue: '<timestamp>.<raw-request-body>',
4680
+ headers: ['X-Ductape-Invocation-Id', 'X-Ductape-Timestamp', 'X-Ductape-Signature', 'X-Ductape-Function'],
4681
+ replayToleranceMs: 300000,
4682
+ httpsRequired: !local,
4683
+ },
4684
+ stopConditions: [
4685
+ 'Do not claim remote availability until the route is reachable from the intended executor.',
4686
+ 'Do not compile a closure-only ctx.step; use ctx.functions.',
4687
+ 'Do not fall back to sample output when a runtime is unavailable.',
4688
+ ],
4689
+ }, null, 2) }] };
4690
+ };
4432
4691
  const cliInputSchema = z.object({
4433
4692
  command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
4434
4693
  'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
@@ -4910,7 +5169,7 @@ async function main() {
4910
5169
  'index strategy, operation types) that should be confirmed with the user first.\n\n' +
4911
5170
  'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
4912
5171
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
4913
- 'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue',
5172
+ 'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue',
4914
5173
  inputSchema: docsInputSchema,
4915
5174
  }, docsHandler);
4916
5175
  server.registerTool('ductape_events_discover', {
@@ -4921,10 +5180,25 @@ async function main() {
4921
5180
  'applications own transactional outboxes, domain rejection handling, and idempotent consumer mutations.',
4922
5181
  inputSchema: eventsDiscoveryInputSchema,
4923
5182
  }, eventsDiscoveryHandler);
5183
+ server.registerTool('ductape_function_setup', {
5184
+ title: 'Ductape Portable Function Setup',
5185
+ description: 'Generate the mandatory secure local + remote runtime setup for application functions used by Features. ' +
5186
+ 'Use this only after a primitives-first decomposition proves residual application-owned logic remains. The result requires a registered local handler, ' +
5187
+ 'a deterministic HTTPS endpoint, raw-body HMAC verification, runtime checks, and fail-closed behavior.',
5188
+ inputSchema: portableFunctionSetupInputSchema,
5189
+ }, portableFunctionSetupHandler);
5190
+ server.registerTool('ductape_redis_setup', {
5191
+ title: 'Ductape Local Redis Setup',
5192
+ description: 'Provision or start an isolated local Redis container for Ductape bootstrap caching and dispatch queues. ' +
5193
+ 'First explain the latency benefit and ask for explicit approval. Never pass approved=true before the user agrees. ' +
5194
+ 'Uses redis:7-alpine, binds only to 127.0.0.1, enables append-only persistence, and returns the redis URL to wire into Ductape initialization.',
5195
+ inputSchema: redisSetupInputSchema,
5196
+ }, redisSetupHandler);
4924
5197
  server.registerTool('ductape_migration_plan', {
4925
5198
  title: 'Ductape AI Migration Guidance',
4926
5199
  description: 'Inspect a TypeScript, Go, Java, or .NET repository without exposing secret values. ' +
4927
5200
  'Builds a relevant-file review queue, secret-name inventory, checksummed migration evidence, and low-confidence navigation hints. ' +
5201
+ 'Migration review must extract Ductape primitives before classifying residual domain logic as portable Functions. ' +
4928
5202
  'The review standards classify capability candidates as FEATURE, FEATURE_STEP, DOMAIN_SERVICE, UTILITY, or INFRASTRUCTURE_ADAPTER; ' +
4929
5203
  'they inspect exports, public methods, entry points, consumers, jobs, repeated orchestration, typed operations, routes, and product terminology. ' +
4930
5204
  'Synchronous local multi-step capabilities may be Features, while related low-level functions must be grouped rather than promoted one-by-one. ' +
@@ -5046,6 +5320,8 @@ async function main() {
5046
5320
  server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
5047
5321
  server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
5048
5322
  server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
5323
+ server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupHandler);
5324
+ server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
5049
5325
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
5050
5326
  server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
5051
5327
  }
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AASD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAuBxF;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAiCZ"}
1
+ {"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA4BD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAoGxF;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAiCZ"}
@@ -28,28 +28,113 @@ export async function executeViaProxy(publishable_key, module, method, params =
28
28
  }
29
29
  return body.data?.data;
30
30
  }
31
+ async function readJsonResponse(res, label) {
32
+ const contentType = res.headers.get('content-type')?.toLowerCase() ?? '';
33
+ if (!contentType.includes('json')) {
34
+ const responseText = await res.text();
35
+ const preview = responseText.replace(/\s+/g, ' ').trim().slice(0, 160);
36
+ throw new Error(`${label}: HTTP ${res.status} returned ${contentType || 'an unknown content type'}` +
37
+ (preview ? ` (${preview})` : ''));
38
+ }
39
+ try {
40
+ return await res.json();
41
+ }
42
+ catch (error) {
43
+ const detail = error instanceof Error ? error.message : String(error);
44
+ throw new Error(`${label}: HTTP ${res.status} returned invalid JSON (${detail})`);
45
+ }
46
+ }
31
47
  export async function getAssetSchemas(module, method) {
32
48
  const path = module
33
49
  ? `/proxy/v1/schema/${encodeURIComponent(module)}${method ? `/${encodeURIComponent(method)}` : ''}`
34
50
  : '/proxy/v1/schema';
35
51
  const url = `${API_BASE_URL.replace(/\/$/, '')}${path}`;
36
52
  const res = await fetch(url);
37
- const body = (await res.json());
38
53
  if (res.status === 404 && module && method) {
39
54
  const fallbackUrl = `${API_BASE_URL.replace(/\/$/, '')}/proxy/v1/schema/${encodeURIComponent(module)}`;
40
55
  const fallbackRes = await fetch(fallbackUrl);
41
- const fallbackBody = (await fallbackRes.json());
42
- const methodSchema = fallbackBody.data?.methods?.[method];
43
- if (fallbackRes.ok && methodSchema) {
44
- return { module, method, schema: methodSchema };
56
+ const fallbackBody = await readJsonResponse(fallbackRes, `Module schema fallback for ${module}.${method}`);
57
+ if (!fallbackRes.ok) {
58
+ throw new Error(fallbackBody.message ?? `Schema fallback request failed: ${fallbackRes.status}`);
59
+ }
60
+ if (typeof fallbackBody.status === 'boolean' && !fallbackBody.status) {
61
+ throw new Error(fallbackBody.message ?? 'Schema fallback fetch failed');
45
62
  }
63
+ const methods = fallbackBody.data?.method_schemas ?? fallbackBody.data?.methods;
64
+ const hasExactMethod = methods !== null && typeof methods === 'object' &&
65
+ !Array.isArray(methods) &&
66
+ Object.prototype.hasOwnProperty.call(methods, method);
67
+ const methodSchema = hasExactMethod ? methods[method] : undefined;
68
+ if (methodSchema !== null && typeof methodSchema === 'object') {
69
+ return {
70
+ module,
71
+ method,
72
+ schema: methodSchema,
73
+ resolution: {
74
+ exact: true,
75
+ source: fallbackBody.data?.method_schemas ? 'module.method_schemas' : 'module.methods',
76
+ targeted_route_status: res.status,
77
+ lookup_path: fallbackBody.data?.method_schemas
78
+ ? `data.method_schemas[${JSON.stringify(method)}]`
79
+ : `data.methods[${JSON.stringify(method)}]`,
80
+ },
81
+ };
82
+ }
83
+ if (Array.isArray(fallbackBody.data?.methods)) {
84
+ const rootUrl = `${API_BASE_URL.replace(/\/$/, '')}/proxy/v1/schema`;
85
+ const rootRes = await fetch(rootUrl);
86
+ const rootBody = await readJsonResponse(rootRes, `Root schema fallback for ${module}.${method}`);
87
+ if (!rootRes.ok) {
88
+ throw new Error(rootBody.message ?? `Root schema fallback request failed: ${rootRes.status}`);
89
+ }
90
+ if (typeof rootBody.status === 'boolean' && !rootBody.status) {
91
+ throw new Error(rootBody.message ?? 'Root schema fallback fetch failed');
92
+ }
93
+ const moduleSchemas = rootBody.data?.modules?.[module];
94
+ const hasRootMethod = moduleSchemas !== null && typeof moduleSchemas === 'object' &&
95
+ !Array.isArray(moduleSchemas) &&
96
+ Object.prototype.hasOwnProperty.call(moduleSchemas, method);
97
+ const rootMethodSchema = hasRootMethod ? moduleSchemas[method] : undefined;
98
+ if (rootMethodSchema !== null && typeof rootMethodSchema === 'object') {
99
+ return {
100
+ module,
101
+ method,
102
+ schema: rootMethodSchema,
103
+ resolution: {
104
+ exact: true,
105
+ source: 'root.modules',
106
+ targeted_route_status: res.status,
107
+ lookup_path: `data.modules[${JSON.stringify(module)}][${JSON.stringify(method)}]`,
108
+ },
109
+ };
110
+ }
111
+ }
112
+ throw new Error(hasExactMethod
113
+ ? `Schema method "${method}" in the ${module} module schema is malformed`
114
+ : `Schema method "${method}" was not found in the ${module} module schema`);
46
115
  }
116
+ const body = await readJsonResponse(res, 'Schema request');
47
117
  if (!res.ok) {
48
118
  throw new Error(body.message ?? `Schema request failed: ${res.status}`);
49
119
  }
50
120
  if (typeof body.status === 'boolean' && !body.status) {
51
121
  throw new Error(body.message ?? 'Schema fetch failed');
52
122
  }
123
+ if (module && method) {
124
+ if (body.data === null || typeof body.data !== 'object') {
125
+ throw new Error(`Targeted schema response for ${module}.${method} is malformed`);
126
+ }
127
+ return {
128
+ module,
129
+ method,
130
+ schema: body.data,
131
+ resolution: {
132
+ exact: true,
133
+ source: 'targeted-route',
134
+ targeted_route_status: res.status,
135
+ },
136
+ };
137
+ }
53
138
  return body.data;
54
139
  }
55
140
  function normalizeTargets(targets) {
package/docs/TOOLS.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # MCP Tools Reference
2
2
 
3
- The Ductape MCP server exposes **one tool**. All operations go through the backend proxy; the list of allowed modules and methods is enforced by the proxy
3
+ The Ductape MCP server exposes proxy, CLI, discovery, documentation, migration, and portable-function setup tools.
4
+
5
+ ## Tool: `ductape_function_setup`
6
+
7
+ Produces the mandatory local registry and signed HTTPS exposure plan for portable application
8
+ functions referenced by Features. The route is
9
+ `/.well-known/ductape/functions/:namespace/:version/:operation` and requests use HMAC-SHA256.
10
+
11
+ Call it only after a primitives-first migration review. Extract database, App, Events, session,
12
+ storage, graph, vector, notification, cache, quota, fallback, healthcheck, and secret behavior into
13
+ native Ductape primitives. A Function contains only the irreducible application-owned logic unless
14
+ the original atomic transaction requires co-location. Classify that residual logic as pure and
15
+ future-WASM-capable or framework-dependent.
4
16
 
5
17
  ---
6
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs",
18
+ "test": "npm run build && node scripts/check-frontend-analytics-guidance.mjs && node scripts/check-events-discovery.mjs && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.mjs",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },