@ductape/mcp 0.2.8 → 0.2.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.9
4
+
5
+ - Security: execute `ductape_cli` with an argument array and no shell, reject shell operators/substitutions, and validate quoted command input before checking the administrative subcommand allowlist.
6
+
3
7
  ## Unreleased
4
8
 
5
9
  - Broadened Feature guidance from durable/event-driven workflows to synchronous or asynchronous named product capabilities.
package/README.md CHANGED
@@ -88,6 +88,7 @@ The server exposes runtime, schema, documentation, CLI, discovery, migration, an
88
88
  - Returns deterministic well-known routes, framework raw-body requirements, HMAC-SHA256 headers,
89
89
  runtime verification steps, and fail-closed conditions.
90
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.
91
92
 
92
93
  The `ductape_cli` MCP tool also exposes public app discovery:
93
94
  `marketplace search <capability>`, `marketplace categories`, and
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parse the user-facing command string into an argv array without invoking a shell.
3
+ * Supports ordinary single/double quoting and backslash escaping for paths/values.
4
+ */
5
+ export declare function parseCliCommand(command: string): string[];
6
+ //# sourceMappingURL=cli-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-command.d.ts","sourceRoot":"","sources":["../src/cli-command.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAmDzD"}
@@ -0,0 +1,66 @@
1
+ /** Characters with shell control/expansion meaning are never valid in ductape_cli input. */
2
+ const SHELL_META = /[;&|`$<>()\r\n\0]/;
3
+ /**
4
+ * Parse the user-facing command string into an argv array without invoking a shell.
5
+ * Supports ordinary single/double quoting and backslash escaping for paths/values.
6
+ */
7
+ export function parseCliCommand(command) {
8
+ if (typeof command !== 'string' || command.trim() === '') {
9
+ throw new Error('A non-empty Ductape CLI command is required.');
10
+ }
11
+ if (SHELL_META.test(command)) {
12
+ throw new Error('Shell operators, substitutions, redirects, and control characters are not allowed.');
13
+ }
14
+ const argv = [];
15
+ let token = '';
16
+ let quote = null;
17
+ let tokenStarted = false;
18
+ for (let index = 0; index < command.length; index += 1) {
19
+ const char = command[index];
20
+ if (quote) {
21
+ if (char === quote) {
22
+ quote = null;
23
+ }
24
+ else if (char === '\\' && quote === '"') {
25
+ index += 1;
26
+ if (index >= command.length)
27
+ throw new Error('Trailing escape in Ductape CLI command.');
28
+ token += command[index];
29
+ }
30
+ else {
31
+ token += char;
32
+ }
33
+ tokenStarted = true;
34
+ continue;
35
+ }
36
+ if (char === "'" || char === '"') {
37
+ quote = char;
38
+ tokenStarted = true;
39
+ }
40
+ else if (char === '\\') {
41
+ index += 1;
42
+ if (index >= command.length)
43
+ throw new Error('Trailing escape in Ductape CLI command.');
44
+ token += command[index];
45
+ tokenStarted = true;
46
+ }
47
+ else if (/\s/.test(char)) {
48
+ if (tokenStarted) {
49
+ argv.push(token);
50
+ token = '';
51
+ tokenStarted = false;
52
+ }
53
+ }
54
+ else {
55
+ token += char;
56
+ tokenStarted = true;
57
+ }
58
+ }
59
+ if (quote)
60
+ throw new Error('Unterminated quote in Ductape CLI command.');
61
+ if (tokenStarted)
62
+ argv.push(token);
63
+ if (argv.length === 0)
64
+ throw new Error('A non-empty Ductape CLI command is required.');
65
+ return argv;
66
+ }
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@
10
10
  * the env var when provided.
11
11
  */
12
12
  import { createRequire } from 'module';
13
- import { execSync } from 'child_process';
13
+ import { execFileSync } from 'child_process';
14
+ import { parseCliCommand } from './cli-command.js';
14
15
  import { homedir } from 'os';
15
16
  import { delimiter, join } from 'path';
16
17
  import { z } from 'zod';
@@ -21,6 +22,60 @@ const MODULES = [
21
22
  'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
22
23
  'actions', 'features', 'jobs', 'logs', 'resilience', 'health', 'fallback', 'secrets',
23
24
  ];
25
+ const redisSetupInputSchema = z.object({
26
+ approved: z.boolean().describe('Must be true only after the user explicitly approves creating/starting a local Docker Redis container.'),
27
+ port: z.number().int().min(1024).max(65535).default(6379),
28
+ container_name: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/).default('ductape-redis'),
29
+ });
30
+ const redisSetupHandler = async (args) => {
31
+ const port = args.port ?? 6379;
32
+ const name = args.container_name ?? 'ductape-redis';
33
+ if (!args.approved) {
34
+ return {
35
+ content: [{ type: 'text', text: [
36
+ 'Redis materially reduces repeated Ductape bootstrap latency and is required for dispatch().',
37
+ 'Ask the user whether they approve pulling redis:7-alpine and creating a local Docker container bound to 127.0.0.1.',
38
+ 'Only call this tool again with approved=true after explicit approval.',
39
+ ].join('\n') }],
40
+ isError: true,
41
+ };
42
+ }
43
+ try {
44
+ execFileSync('docker', ['version', '--format', '{{.Server.Version}}'], { encoding: 'utf8', timeout: 15_000 });
45
+ }
46
+ catch {
47
+ 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 };
48
+ }
49
+ let exists = false;
50
+ let running = false;
51
+ try {
52
+ exists = true;
53
+ running = execFileSync('docker', ['inspect', '-f', '{{.State.Running}}', name], { encoding: 'utf8', timeout: 10_000 }).trim() === 'true';
54
+ }
55
+ catch {
56
+ exists = false;
57
+ }
58
+ if (exists && !running)
59
+ execFileSync('docker', ['start', name], { encoding: 'utf8', timeout: 30_000 });
60
+ if (!exists) {
61
+ execFileSync('docker', [
62
+ 'run', '-d', '--name', name, '--restart', 'unless-stopped',
63
+ '-p', `127.0.0.1:${port}:6379`, 'redis:7-alpine',
64
+ 'redis-server', '--appendonly', 'yes',
65
+ ], { encoding: 'utf8', timeout: 120_000 });
66
+ }
67
+ execFileSync('docker', ['exec', name, 'redis-cli', 'ping'], { encoding: 'utf8', timeout: 10_000 });
68
+ return { content: [{ type: 'text', text: JSON.stringify({
69
+ ready: true,
70
+ container: name,
71
+ redis_url: `redis://127.0.0.1:${port}`,
72
+ next_steps: [
73
+ `Set DUCTAPE_REDIS_URL=redis://127.0.0.1:${port}`,
74
+ 'Pass redisUrl to @ductape/nestjs or redis_url/redis_client to the installed SDK API as supported by its types.',
75
+ 'Add the variable to local .env only; do not commit credentials for managed Redis.',
76
+ ],
77
+ }, null, 2) }] };
78
+ };
24
79
  // ─── Exhaustive SDK Method & Params Reference ────────────────────────────────
25
80
  // Built from a complete read of sdk/ts/src/index.ts (Ductape class public API).
26
81
  // Each entry follows: [module].[method] → params array signature.
@@ -213,6 +268,10 @@ SETUP — register once in AppModule:
213
268
  ║ ║
214
269
  ║ *.run(), events.produce(), and @Events.Consumer do NOT need Redis. ║
215
270
  ║ Only dispatch() does — and it is non-negotiable. ║
271
+ ║ ║
272
+ ║ Redis is STRONGLY RECOMMENDED even without dispatch(): every bootstrap ║
273
+ ║ domain uses memory + optional Redis caching, so shared/multi-process ║
274
+ ║ deployments avoid repeating control-plane reads after warm-up. ║
216
275
  ╚══════════════════════════════════════════════════════════════════════════╝
217
276
 
218
277
  import { DuctapeModule } from '@ductape/nestjs';
@@ -245,6 +304,10 @@ SETUP — register once in AppModule:
245
304
  DUCTAPE_REDIS_URL=redis://localhost:6379 # local dev
246
305
  DUCTAPE_REDIS_URL=rediss://:<password>@host:6380 # managed Redis (TLS)
247
306
 
307
+ If no Redis URL exists, proactively explain the latency/dispatch benefits and ask whether the
308
+ user wants a local Docker Redis dedicated to Ductape. After explicit approval, call
309
+ ductape_redis_setup({ approved: true }). Never pull an image or create/start a container before approval.
310
+
248
311
  INJECTING IN SERVICES AND CONTROLLERS — use @InjectContext():
249
312
 
250
313
  import { InjectContext, DuctapeContext } from '@ductape/nestjs';
@@ -773,11 +836,12 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
773
836
  queries?: { queryName: { handler?: function } },
774
837
  options?: { timeout?: number, retries?: number },
775
838
  envs?: [{ slug: string, active?: boolean }],
776
- recordInput?: object, // sample input for step-recording (run once during define)
839
+ recordInput?: object, // exposed only as ctx.sampleInput while recording
777
840
  recordScenarios?: object[], // multiple recording scenarios for branching
778
841
  branchOverrides?: object, // force step results during recording to reach later branches
779
842
  handler: async (ctx) => {
780
- // ctx.input – typed feature input
843
+ // ctx.input – typed runtime input; always compiles to $Input{} operators
844
+ // ctx.sampleInput – compile-time sample for loop/branch discovery only
781
845
  // ctx.step(tag, fn, rollback?, opts?) – define a durable step
782
846
  // ctx.api.run({ app, event, input }) – call an app action
783
847
  // ctx.database.query/insert/update/delete({ database, event, ... })
@@ -1115,6 +1179,7 @@ const ductape = new Ductape({
1115
1179
  workspace_id: process.env.DUCTAPE_WORKSPACE_ID!,
1116
1180
  user_id: process.env.DUCTAPE_USER_ID!,
1117
1181
  public_key: process.env.DUCTAPE_PUBLIC_KEY!,
1182
+ redis_url: process.env.DUCTAPE_REDIS_URL,
1118
1183
  });
1119
1184
 
1120
1185
  async function run() {
@@ -1186,7 +1251,8 @@ const ADMIN_SUBCOMMANDS = [
1186
1251
  ];
1187
1252
  function checkCli() {
1188
1253
  try {
1189
- const out = execSync('ductape --version', {
1254
+ const out = execFileSync('ductape', ['--version'], {
1255
+ shell: false,
1190
1256
  encoding: 'utf8',
1191
1257
  timeout: 15000,
1192
1258
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1208,7 +1274,8 @@ function checkLoginState() {
1208
1274
  // `ductape whoami` only reports whether a local credentials file exists. It does not validate
1209
1275
  // the stored token, so an expired token would be cached as authenticated and fail later with 401.
1210
1276
  // Use a harmless authenticated read to validate the credential against the API.
1211
- execSync('ductape workspaces list --json', {
1277
+ execFileSync('ductape', ['workspaces', 'list', '--json'], {
1278
+ shell: false,
1212
1279
  encoding: 'utf8',
1213
1280
  timeout: 10000,
1214
1281
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1229,7 +1296,8 @@ function syncWorkspace() {
1229
1296
  if (!target)
1230
1297
  return;
1231
1298
  try {
1232
- execSync(`ductape workspaces use "${target}"`, {
1299
+ execFileSync('ductape', ['workspaces', 'use', target], {
1300
+ shell: false,
1233
1301
  encoding: 'utf8',
1234
1302
  timeout: 10000,
1235
1303
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -1242,7 +1310,14 @@ function syncWorkspace() {
1242
1310
  }
1243
1311
  }
1244
1312
  function runCli(command) {
1245
- const first = command.trim().split(/\s+/)[0];
1313
+ let argv;
1314
+ try {
1315
+ argv = parseCliCommand(command);
1316
+ }
1317
+ catch (error) {
1318
+ return { success: false, output: `Rejected unsafe ductape_cli command: ${error.message}` };
1319
+ }
1320
+ const first = argv[0];
1246
1321
  if (!ADMIN_SUBCOMMANDS.includes(first)) {
1247
1322
  return {
1248
1323
  success: false,
@@ -1250,13 +1325,13 @@ function runCli(command) {
1250
1325
  };
1251
1326
  }
1252
1327
  // Auto-inject --workspace on login if DUCTAPE_WORKSPACE is set and caller hasn't specified one
1253
- let finalCommand = command;
1254
1328
  const ws = process.env.DUCTAPE_WORKSPACE;
1255
- if (first === 'login' && ws && !command.includes('--workspace') && !command.includes('--skip-workspace-select')) {
1256
- finalCommand = `${command} --workspace "${ws}"`;
1329
+ if (first === 'login' && ws && !argv.includes('--workspace') && !argv.includes('--skip-workspace-select')) {
1330
+ argv.push('--workspace', ws);
1257
1331
  }
1258
1332
  try {
1259
- const output = execSync(`ductape ${finalCommand}`, {
1333
+ const output = execFileSync('ductape', argv, {
1334
+ shell: false,
1260
1335
  encoding: 'utf8',
1261
1336
  // Must exceed the proxy's operation timeout so stderr can preserve the structured timeout
1262
1337
  // instead of this wrapper killing the CLI first and reducing it to "(no data)".
@@ -1384,6 +1459,37 @@ PORTABLE APPLICATION FUNCTIONS
1384
1459
  Use portable functions when a Feature needs application-owned logic that cannot be expressed with
1385
1460
  database, action, Event, storage, graph, vector, session, quota, fallback, or transform primitives.
1386
1461
 
1462
+ PRIMITIVES-FIRST DECOMPOSITION — REQUIRED
1463
+ Before creating a Function, inspect the implementation, its callees, side effects, transaction
1464
+ boundary, configuration, and tests. Extract every operation already represented by a Ductape
1465
+ primitive. A Function is the residual application-owned behavior after that extraction; it is not
1466
+ a wrapper around an entire existing service method merely because that method already exists.
1467
+
1468
+ Classify each observed behavior:
1469
+ database read/write/transaction/index → database primitive or database action
1470
+ external provider/API → connected App action
1471
+ publish/consume asynchronous message → Events primitive
1472
+ session creation/validation/revocation → session primitive
1473
+ file/blob operation → storage primitive
1474
+ graph/vector operation → graph/vector primitive
1475
+ email/SMS/push/chat delivery → notification primitive
1476
+ quota/fallback/health/cache → corresponding Ductape primitive
1477
+ multi-step product capability → Feature orchestration
1478
+ reusable domain decision/transformation → portable Function candidate
1479
+ formatting/hash/redaction with one caller→ ordinary utility unless portability is required
1480
+
1481
+ For a mixed legacy method, split the boundary. Keep its irreducible domain decision in a Function
1482
+ and orchestrate extracted primitives as named Feature steps. Preserve atomicity: do not split a
1483
+ database transaction into independently committed steps unless the original contract permits it;
1484
+ use a database action or a Function with an explicitly authorized transactional capability instead.
1485
+
1486
+ Every proposed Function must include evidence for:
1487
+ - why no existing Ductape primitive expresses it;
1488
+ - exact business input/output, errors, side effects, idempotency, and transaction semantics;
1489
+ - whether it is pure and therefore a future signed-WASM candidate;
1490
+ - why it needs local, Events, or HTTPS availability;
1491
+ - which extracted primitives remain separate Feature steps.
1492
+
1387
1493
  Non-negotiable rule: arbitrary JavaScript/TypeScript callbacks are not serializable. Never write
1388
1494
  ctx.step('x', () => applicationService.method()) and assume the method will run elsewhere. The
1389
1495
  Feature compiler must reject a step that records no portable operation.
@@ -1471,6 +1577,39 @@ START
1471
1577
  5. Call again with write=true only after confirming the destination. This writes guidance artifacts only.
1472
1578
  6. Call with ensure_product=true when product inventory confirms the product is absent.
1473
1579
 
1580
+ PRIMITIVES-FIRST CAPABILITY EXTRACTION — REQUIRED FOR EVERY MIGRATION SLICE
1581
+ Do not translate controllers, services, handlers, or exported functions one-for-one into Features
1582
+ or portable Functions. For each candidate capability, trace entry points, callees, state changes,
1583
+ external calls, errors, authorization, session usage, transactions, retries, and tests, then create
1584
+ a decomposition ledger with these classifications:
1585
+
1586
+ DUCTAPE_PRIMITIVE database, App action, Events, session, storage, graph, vector,
1587
+ notification, cache, quota, fallback, healthcheck, secret
1588
+ FEATURE user/product capability coordinating multiple meaningful operations
1589
+ PORTABLE_FUNCTION residual reusable domain logic that primitives cannot express
1590
+ CHILD_FEATURE independently meaningful capability with its own contract/lifecycle
1591
+ UTILITY local implementation detail with no independent product contract
1592
+ INFRASTRUCTURE_ADAPTER framework/provider plumbing replaced by a Ductape primitive
1593
+
1594
+ The required order is:
1595
+ 1. Extract and inventory Ductape primitives from the existing implementation.
1596
+ 2. Establish the capability and transaction boundary from behavior and tests.
1597
+ 3. Design named Feature steps around those primitives.
1598
+ 4. Put only irreducible application-owned logic behind ctx.functions.
1599
+ 5. For each Function, record pure/WASM-candidate versus framework-dependent classification.
1600
+ 6. Call ductape_function_setup and implement verified local plus remote availability.
1601
+
1602
+ Never create a Function that merely hides database, session, Events, storage, notification, graph,
1603
+ vector, quota, fallback, healthcheck, cache, secret, or connected-App work that the Feature can
1604
+ express directly. Never fragment an original atomic transaction just to maximize primitive count.
1605
+ A mixed method may legitimately become several primitive steps plus one small Function, one database
1606
+ action, or one capability-scoped Function when atomicity requires co-location.
1607
+
1608
+ Before implementation, present a decomposition table with: source behavior, evidence location,
1609
+ classification, selected Ductape primitive/function, input/output, side effects, transaction owner,
1610
+ session behavior, failure semantics, and verification test. If the residual Function has no clear
1611
+ reason to exist after primitive extraction, do not create it.
1612
+
1474
1613
  FINAL E2E ACCEPTANCE GATE
1475
1614
  End the migration by running the exact original E2E command against the migrated codebase while retaining
1476
1615
  the original checksum-bound suite. Do not silently edit, delete, skip, quarantine, or weaken baseline tests.
@@ -3404,7 +3543,7 @@ STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
3404
3543
  → Add branchOverrides: { stepTag: { field: value } } so all branches are recorded
3405
3544
  → At runtime the executor evaluates the real result and skips or runs later steps accordingly
3406
3545
  Loop over input array:
3407
- → Add recordInput: { items: [{ id: "1" }, { id: "2" }] } so the loop runs during recording
3546
+ → Add recordInput: { items: [{ id: "1" }, { id: "2" }] } and iterate ctx.sampleInput.items
3408
3547
  → Use unique step tags per iteration (e.g. "process-" + item.id)
3409
3548
  Switch/if-else on feature input values:
3410
3549
  → Add recordScenarios: [{ type: "a" }, { type: "b" }] — handler runs once per scenario
@@ -3514,7 +3653,8 @@ When you call features.define({ handler }), the handler runs TWICE:
3514
3653
  This phase captures the step graph: which steps exist, their types, tags, and declared
3515
3654
  inputs/outputs. No real API calls, DB queries, or side effects occur.
3516
3655
  Arbitrary JS code OUTSIDE ctx.step() ALSO runs during recording — with proxy values.
3517
- For loops: use recordInput so the handler sees sample data and all iterations are recorded.
3656
+ For loops: supply recordInput and iterate ctx.sampleInput so all iterations are recorded.
3657
+ ctx.input is always the runtime operator surface and must never expose recordInput literals.
3518
3658
  For branches: use branchOverrides so each path is captured.
3519
3659
 
3520
3660
  2. EXECUTION PHASE (at runtime) — handler is called with a real ExecutionContext.
@@ -4533,6 +4673,8 @@ const portableFunctionSetupHandler = async (args) => {
4533
4673
  return { content: [{ type: 'text', text: JSON.stringify({
4534
4674
  ok: true,
4535
4675
  requiredActions: [
4676
+ '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.',
4677
+ 'Record whether the residual logic is pure/signed-WASM-capable or framework-dependent, including transaction and side-effect boundaries.',
4536
4678
  'Create or locate one defineFunctions contract with exact input/output JSON Schemas.',
4537
4679
  'Register handlers with ductape.functions.register during application startup.',
4538
4680
  `Set DUCTAPE_FUNCTION_BASE_URL=${JSON.stringify(base)} in the application runtime.`,
@@ -5052,14 +5194,22 @@ async function main() {
5052
5194
  server.registerTool('ductape_function_setup', {
5053
5195
  title: 'Ductape Portable Function Setup',
5054
5196
  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, ' +
5197
+ 'Use this only after a primitives-first decomposition proves residual application-owned logic remains. The result requires a registered local handler, ' +
5056
5198
  'a deterministic HTTPS endpoint, raw-body HMAC verification, runtime checks, and fail-closed behavior.',
5057
5199
  inputSchema: portableFunctionSetupInputSchema,
5058
5200
  }, portableFunctionSetupHandler);
5201
+ server.registerTool('ductape_redis_setup', {
5202
+ title: 'Ductape Local Redis Setup',
5203
+ description: 'Provision or start an isolated local Redis container for Ductape bootstrap caching and dispatch queues. ' +
5204
+ 'First explain the latency benefit and ask for explicit approval. Never pass approved=true before the user agrees. ' +
5205
+ '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.',
5206
+ inputSchema: redisSetupInputSchema,
5207
+ }, redisSetupHandler);
5059
5208
  server.registerTool('ductape_migration_plan', {
5060
5209
  title: 'Ductape AI Migration Guidance',
5061
5210
  description: 'Inspect a TypeScript, Go, Java, or .NET repository without exposing secret values. ' +
5062
5211
  'Builds a relevant-file review queue, secret-name inventory, checksummed migration evidence, and low-confidence navigation hints. ' +
5212
+ 'Migration review must extract Ductape primitives before classifying residual domain logic as portable Functions. ' +
5063
5213
  'The review standards classify capability candidates as FEATURE, FEATURE_STEP, DOMAIN_SERVICE, UTILITY, or INFRASTRUCTURE_ADAPTER; ' +
5064
5214
  'they inspect exports, public methods, entry points, consumers, jobs, repeated orchestration, typed operations, routes, and product terminology. ' +
5065
5215
  'Synchronous local multi-step capabilities may be Features, while related low-level functions must be grouped rather than promoted one-by-one. ' +
@@ -5182,6 +5332,7 @@ async function main() {
5182
5332
  server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
5183
5333
  server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
5184
5334
  server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupHandler);
5335
+ server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
5185
5336
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
5186
5337
  server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
5187
5338
  }
package/docs/TOOLS.md CHANGED
@@ -8,6 +8,12 @@ Produces the mandatory local registry and signed HTTPS exposure plan for portabl
8
8
  functions referenced by Features. The route is
9
9
  `/.well-known/ductape/functions/:namespace/:version/:operation` and requests use HMAC-SHA256.
10
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.
16
+
11
17
  ---
12
18
 
13
19
  ## Marketplace discovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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 && node scripts/check-schema-fallback.mjs && node scripts/check-portable-functions.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-portable-functions.mjs",
19
19
  "start": "node dist/index.js",
20
20
  "dev": "tsx src/index.ts"
21
21
  },