@ductape/mcp 0.2.7 → 0.2.8

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,13 @@ 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
+
85
92
  The `ductape_cli` MCP tool also exposes public app discovery:
86
93
  `marketplace search <capability>`, `marketplace categories`, and
87
94
  `marketplace get <app_tag>`. Inspect the app before generating or executing an action payload.
package/dist/index.js CHANGED
@@ -1349,13 +1349,20 @@ const docsInputSchema = z.object({
1349
1349
  topic: z.string().describe('Feature topic to look up. Supported: ' +
1350
1350
  'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
1351
1351
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
1352
- 'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
1352
+ 'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue'),
1353
1353
  });
1354
1354
  const eventsDiscoveryInputSchema = z.object({
1355
1355
  query: z.string().optional().describe('Capability or recovery search, including DLQ, dead letter, failed messages, retry, poison message, replay, or consumer failure.'),
1356
1356
  product: z.string().optional().describe('Product tag used for live component inspection.'),
1357
1357
  component: z.string().optional().describe('Events component/broker tag used for live inspection.'),
1358
1358
  });
1359
+ const portableFunctionSetupInputSchema = z.object({
1360
+ framework: z.enum(['express', 'nestjs', 'fastify', 'other']).describe('Application HTTP framework.'),
1361
+ base_url: z.string().url().describe('Externally reachable HTTPS origin. HTTP is accepted only for localhost development.'),
1362
+ namespace: z.string().min(1).describe('Portable function namespace.'),
1363
+ version: z.string().min(1).describe('Contract version.'),
1364
+ operations: z.array(z.string().min(1)).min(1).describe('Operation names that must be registered and exposed.'),
1365
+ });
1359
1366
  const migrationInputSchema = z.object({
1360
1367
  source: z.string().describe('Absolute path to the existing codebase.'),
1361
1368
  e2e_baseline: z.string().describe('Absolute path to a passing migration-e2e baseline manifest created before migration inspection.'),
@@ -1371,6 +1378,65 @@ const migrationInputSchema = z.object({
1371
1378
  write: z.boolean().optional().default(false).describe('Write redacted advisory artifacts only. This never writes application code or executable Ductape assets.'),
1372
1379
  });
1373
1380
  const DOCS = {
1381
+ 'portable-functions': `
1382
+ PORTABLE APPLICATION FUNCTIONS
1383
+
1384
+ Use portable functions when a Feature needs application-owned logic that cannot be expressed with
1385
+ database, action, Event, storage, graph, vector, session, quota, fallback, or transform primitives.
1386
+
1387
+ Non-negotiable rule: arbitrary JavaScript/TypeScript callbacks are not serializable. Never write
1388
+ ctx.step('x', () => applicationService.method()) and assume the method will run elsewhere. The
1389
+ Feature compiler must reject a step that records no portable operation.
1390
+
1391
+ Canonical TypeScript pattern:
1392
+ const AuthFunctions = defineFunctions({
1393
+ namespace: 'statecraft-auth', version: '1',
1394
+ operations: {
1395
+ register: {
1396
+ input: { type: 'object', required: ['email', 'password'], properties: {
1397
+ email: { type: 'string' }, password: { type: 'string', minLength: 12 }
1398
+ }, additionalProperties: false },
1399
+ output: { type: 'object', required: ['token', 'player'], properties: {
1400
+ token: { type: 'string' }, player: { type: 'object' }
1401
+ } },
1402
+ transports: [{ type: 'local' }],
1403
+ handler: (input, context) => authService.register(input.email, input.password)
1404
+ }
1405
+ }
1406
+ });
1407
+ ductape.functions.register(AuthFunctions);
1408
+ await ductape.feature.define({
1409
+ tag: 'register-player', name: 'Register Player',
1410
+ input: { email: { type: 'string', required: true }, password: { type: 'string', required: true } },
1411
+ handler: async ctx => {
1412
+ const auth = ctx.functions.use(AuthFunctions);
1413
+ return ctx.step('register', () => auth.register({ email: ctx.input.email, password: ctx.input.password }));
1414
+ }
1415
+ });
1416
+
1417
+ The compiled step stores namespace, operation, version, input/output JSON Schemas, timeout,
1418
+ idempotency declaration, and permitted transports. It never stores the handler or a sample result.
1419
+
1420
+ Resolution order is local registered handler, then a contract-declared signed HTTP transport.
1421
+ HTTP function calls use Ductape HMAC-SHA256 headers. Raw arbitrary URLs and unsigned invocation are
1422
+ not supported. Application endpoints should use handlePortableFunctionHttpRequest and must receive
1423
+ the raw request body so signature verification covers exactly the bytes sent.
1424
+
1425
+ The feature session is inherited automatically in invocation.context.session, not mixed into the
1426
+ business input. Invocation context also includes product, env, workspace_id, feature_id,
1427
+ feature_tag, step_tag, invocation_id, and deadline_at.
1428
+
1429
+ Failure behavior is strict:
1430
+ FUNCTION_UNAVAILABLE no local handler or configured HTTP transport
1431
+ FUNCTION_SCHEMA_VALIDATION_FAILED input or output violates the contract
1432
+ FUNCTION_SIGNATURE_INVALID HTTP signature is absent, invalid, or expired
1433
+ FUNCTION_CORRELATION_MISMATCH response invocation_id differs from request
1434
+ FUNCTION_TIMEOUT operation exceeded its declared deadline
1435
+
1436
+ Do not invent a function contract. Inspect the application's defineFunctions declarations and use
1437
+ the exact namespace, operation, version, schemas, and transports. If no contract exists, add one in
1438
+ application code and explicitly register its runtime implementation.
1439
+ `.trim(),
1374
1440
  migration: `
1375
1441
  DUCTAPE CODEBASE MIGRATION GUIDE
1376
1442
 
@@ -3284,7 +3350,8 @@ STEP 2 — INVENTORY existing Ductape components
3284
3350
  STEP 3 — PLAN each step
3285
3351
  For every logical step:
3286
3352
  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.
3353
+ Application-owned logic must be a registered portable function called through ctx.functions.
3354
+ A plain callback that calls an application service is not serializable and must fail compilation.
3288
3355
  If a step calls an external service, it MUST go through a registered Ductape App.
3289
3356
  If no App for that service exists in the product → mark it "App to create: <service name>".
3290
3357
  DO NOT plan a raw HTTP call, a direct dispatch to a URL, or any workaround in place of a missing App.
@@ -3292,6 +3359,14 @@ STEP 3 — PLAN each step
3292
3359
  c. Decide if a rollback handler is needed (e.g. charge → refund on later failure).
3293
3360
  Rollback is optional and is not a Feature qualification requirement.
3294
3361
  d. Decide allow_fail: true for non-critical steps (email, analytics, audit logs)
3362
+ e. If the step needs application-owned code, search the repository for an existing
3363
+ defineFunctions contract and runtime registration. If none exists, design the exact contract.
3364
+ Then ALWAYS call ductape_function_setup with the detected framework, namespace, version,
3365
+ operations, and intended external base URL. Proactively inspect deployment configuration to
3366
+ discover that URL and implement both local registration and the signed HTTPS adapter. If no
3367
+ deployable HTTPS origin exists, report that explicit blocker instead of stopping at a local-only
3368
+ setup. Do not mark the function remotely available until the
3369
+ well-known route is reachable and rejects an invalid signature with HTTP 401.
3295
3370
 
3296
3371
  STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating anything
3297
3372
  Show the user:
@@ -3344,7 +3419,7 @@ STEP 8 — SET rollbacks for reversible steps
3344
3419
  async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
3345
3420
  );
3346
3421
 
3347
- Step types: local_domain | action | database | graph | notification | storage | produce | quota |
3422
+ Step types: function | action | database | graph | notification | storage | produce | quota |
3348
3423
  fallback | vector | child_feature | sleep | wait_for_signal | checkpoint
3349
3424
 
3350
3425
  Valid synchronous Feature candidates include generate-world, resolve-nation-turn,
@@ -3450,10 +3525,12 @@ When you call features.define({ handler }), the handler runs TWICE:
3450
3525
  outer handler body. Code in the outer body runs during recording with proxy values and
3451
3526
  may behave unexpectedly (e.g. typeof proxy === 'object' is true but .someField is a proxy).
3452
3527
 
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.
3528
+ A ctx.step callback MUST NOT close over ordinary local domain functions or injected/application
3529
+ services. Closures are not portable and compilation fails closed. Define application-owned logic
3530
+ with defineFunctions, register its local runtime handler, and invoke it through ctx.functions.
3531
+ The MCP workflow must also attempt secure remote availability: detect the framework and deployment
3532
+ origin, install the signed well-known HTTPS adapter, and verify it. Local registration alone is
3533
+ insufficient for a Feature intended to execute outside the application process.
3457
3534
 
3458
3535
  Use an Event when the operation genuinely crosses an asynchronous process/service boundary,
3459
3536
  needs broker delivery semantics, or must be consumed independently. Do not produce an Event merely
@@ -4429,6 +4506,57 @@ const docsHandler = async (args) => {
4429
4506
  : '';
4430
4507
  return { content: [{ type: 'text', text: doc + structured }] };
4431
4508
  };
4509
+ const portableFunctionSetupHandler = async (args) => {
4510
+ const parsed = new URL(args.base_url);
4511
+ const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
4512
+ if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
4513
+ return { content: [{ type: 'text', text: JSON.stringify({
4514
+ ok: false,
4515
+ error: 'FUNCTION_INSECURE_TRANSPORT',
4516
+ message: 'Use HTTPS for portable function endpoints. HTTP is allowed only for localhost development.',
4517
+ }, null, 2) }], isError: true };
4518
+ }
4519
+ const base = args.base_url.replace(/\/$/, '');
4520
+ const routes = args.operations.map(operation => ({
4521
+ operation,
4522
+ url: `${base}/.well-known/ductape/functions/${encodeURIComponent(args.namespace)}/${encodeURIComponent(args.version)}/${encodeURIComponent(operation)}`,
4523
+ }));
4524
+ const adapter = args.framework === 'nestjs'
4525
+ ? `Configure rawBody: true in NestFactory.create, then add a POST controller route at\n` +
4526
+ `/.well-known/ductape/functions/:namespace/:version/:operation. Pass request.rawBody, headers,\n` +
4527
+ `and params to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
4528
+ : args.framework === 'fastify'
4529
+ ? `Register a raw-body plugin for only the well-known function route, then pass rawBody, headers,\nparams to handlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`
4530
+ : args.framework === 'express'
4531
+ ? `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!).`
4532
+ : `Preserve the exact raw JSON request bytes and pass body, headers, and route params to\nhandlePortableFunctionHttpRequest(..., process.env.DUCTAPE_ACCESS_KEY!).`;
4533
+ return { content: [{ type: 'text', text: JSON.stringify({
4534
+ ok: true,
4535
+ requiredActions: [
4536
+ 'Create or locate one defineFunctions contract with exact input/output JSON Schemas.',
4537
+ 'Register handlers with ductape.functions.register during application startup.',
4538
+ `Set DUCTAPE_FUNCTION_BASE_URL=${JSON.stringify(base)} in the application runtime.`,
4539
+ 'Mount the signed well-known HTTP adapter and preserve the exact raw body.',
4540
+ 'Do not add bearer keys, access keys, static headers, or unsigned routes to Feature JSON.',
4541
+ 'Verify invalid signatures receive HTTP 401 and missing handlers receive HTTP 503.',
4542
+ 'Execute each operation and verify actual business state plus Feature/step processor records.',
4543
+ ],
4544
+ routes,
4545
+ adapter,
4546
+ security: {
4547
+ scheme: 'HMAC-SHA256',
4548
+ signedValue: '<timestamp>.<raw-request-body>',
4549
+ headers: ['X-Ductape-Invocation-Id', 'X-Ductape-Timestamp', 'X-Ductape-Signature', 'X-Ductape-Function'],
4550
+ replayToleranceMs: 300000,
4551
+ httpsRequired: !local,
4552
+ },
4553
+ stopConditions: [
4554
+ 'Do not claim remote availability until the route is reachable from the intended executor.',
4555
+ 'Do not compile a closure-only ctx.step; use ctx.functions.',
4556
+ 'Do not fall back to sample output when a runtime is unavailable.',
4557
+ ],
4558
+ }, null, 2) }] };
4559
+ };
4432
4560
  const cliInputSchema = z.object({
4433
4561
  command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
4434
4562
  'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
@@ -4910,7 +5038,7 @@ async function main() {
4910
5038
  'index strategy, operation types) that should be confirmed with the user first.\n\n' +
4911
5039
  'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
4912
5040
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
4913
- 'notifications, resilience, features, events, logs, migration, frontend, frontend-analytics, client, react, vue',
5041
+ 'notifications, resilience, features, portable-functions, events, logs, migration, frontend, frontend-analytics, client, react, vue',
4914
5042
  inputSchema: docsInputSchema,
4915
5043
  }, docsHandler);
4916
5044
  server.registerTool('ductape_events_discover', {
@@ -4921,6 +5049,13 @@ async function main() {
4921
5049
  'applications own transactional outboxes, domain rejection handling, and idempotent consumer mutations.',
4922
5050
  inputSchema: eventsDiscoveryInputSchema,
4923
5051
  }, eventsDiscoveryHandler);
5052
+ server.registerTool('ductape_function_setup', {
5053
+ title: 'Ductape Portable Function Setup',
5054
+ description: 'Generate the mandatory secure local + remote runtime setup for application functions used by Features. ' +
5055
+ 'Use this whenever feature code calls application-owned logic. The result requires a registered local handler, ' +
5056
+ 'a deterministic HTTPS endpoint, raw-body HMAC verification, runtime checks, and fail-closed behavior.',
5057
+ inputSchema: portableFunctionSetupInputSchema,
5058
+ }, portableFunctionSetupHandler);
4924
5059
  server.registerTool('ductape_migration_plan', {
4925
5060
  title: 'Ductape AI Migration Guidance',
4926
5061
  description: 'Inspect a TypeScript, Go, Java, or .NET repository without exposing secret values. ' +
@@ -5046,6 +5181,7 @@ async function main() {
5046
5181
  server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
5047
5182
  server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
5048
5183
  server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
5184
+ server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupHandler);
5049
5185
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
5050
5186
  server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
5051
5187
  }
@@ -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,12 @@
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.
4
10
 
5
11
  ---
6
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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
  },