@ductape/mcp 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +153 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -857,7 +857,13 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
857
857
  graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
858
858
  graph.fetch [product_tag, graph_tag]
859
859
  graph.list [product_tag?]
860
- graph.update [product_tag, graph_tag, data: { name?: string, description?: string, type?: "neo4j"|"neptune"|"arangodb"|"memgraph", envs?: [{ slug: string, connection_url: string, username?: string, password?: string, database?: string, graphName?: string, region?: string }] }]
860
+ graph.update [product_tag, graph_tag, data: { name?: string, description?: string, type?: "neo4j"|"neptune"|"arangodb"|"memgraph", envs?: [{ slug: string, connection_url?: string, username?: string, password?: string, database?: string, graphName?: string, region?: string }] }]
861
+ Each env entry is a PARTIAL patch merged with that env's existing persisted config — only
862
+ include the fields you're actually changing (e.g. just { slug, password } to rotate a
863
+ password). connection_url is required only when adding a brand-new env slug; omitting it on
864
+ an existing slug leaves the current value untouched. Field name is "password", not
865
+ "masterPassword" — that's a different field used only by cloud resources import-persist-all
866
+ (see the Neo4j Aura import flow section below), not by graph.create/graph.update.
861
867
  graph.delete [graph_tag, product_tag?]
862
868
  graph.connect [{ product, env, graph }]
863
869
  graph.testConnection [config]
@@ -1469,7 +1475,7 @@ const ADMIN_SUBCOMMANDS = [
1469
1475
  'migration-products',
1470
1476
  'migration-secrets',
1471
1477
  ];
1472
- function checkCli() {
1478
+ function checkCli(projectDir) {
1473
1479
  try {
1474
1480
  const out = execFileSync('ductape', ['--version'], {
1475
1481
  shell: false,
@@ -1477,7 +1483,7 @@ function checkCli() {
1477
1483
  timeout: 15000,
1478
1484
  stdio: ['pipe', 'pipe', 'pipe'],
1479
1485
  env: cliEnvironment(),
1480
- cwd: cliCwd(),
1486
+ cwd: cliCwd(projectDir),
1481
1487
  }).trim();
1482
1488
  return { available: true, version: out || 'unknown' };
1483
1489
  }
@@ -1489,7 +1495,7 @@ function checkCli() {
1489
1495
  return { available: !commandMissing };
1490
1496
  }
1491
1497
  }
1492
- function checkLoginState() {
1498
+ function checkLoginState(projectDir) {
1493
1499
  try {
1494
1500
  // `ductape whoami` only reports whether a local credentials file exists. It does not validate
1495
1501
  // the stored token, so an expired token would be cached as authenticated and fail later with 401.
@@ -1500,7 +1506,7 @@ function checkLoginState() {
1500
1506
  timeout: 10000,
1501
1507
  stdio: ['pipe', 'pipe', 'pipe'],
1502
1508
  env: cliEnvironment(),
1503
- cwd: cliCwd(),
1509
+ cwd: cliCwd(projectDir),
1504
1510
  });
1505
1511
  authState = 'ok';
1506
1512
  return 'ok';
@@ -1510,7 +1516,7 @@ function checkLoginState() {
1510
1516
  return 'none';
1511
1517
  }
1512
1518
  }
1513
- function syncWorkspace() {
1519
+ function syncWorkspace(projectDir) {
1514
1520
  const target = process.env.DUCTAPE_WORKSPACE;
1515
1521
  workspaceSynced = true; // mark done regardless so we don't retry on every call
1516
1522
  if (!target)
@@ -1522,14 +1528,14 @@ function syncWorkspace() {
1522
1528
  timeout: 10000,
1523
1529
  stdio: ['pipe', 'pipe', 'pipe'],
1524
1530
  env: cliEnvironment(),
1525
- cwd: cliCwd(),
1531
+ cwd: cliCwd(projectDir),
1526
1532
  });
1527
1533
  }
1528
1534
  catch {
1529
1535
  // best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
1530
1536
  }
1531
1537
  }
1532
- function runCli(command) {
1538
+ function runCli(command, projectDir) {
1533
1539
  let argv;
1534
1540
  try {
1535
1541
  argv = parseCliCommand(command);
@@ -1558,7 +1564,7 @@ function runCli(command) {
1558
1564
  timeout: 90000,
1559
1565
  stdio: ['pipe', 'pipe', 'pipe'],
1560
1566
  env: cliEnvironment(),
1561
- cwd: cliCwd(),
1567
+ cwd: cliCwd(projectDir),
1562
1568
  });
1563
1569
  return { success: true, output: output.trim() };
1564
1570
  }
@@ -1567,7 +1573,7 @@ function runCli(command) {
1567
1573
  if (/\bHTTP 401\b|unauthori[sz]ed|invalid token|token expired/i.test(msg)) {
1568
1574
  // Distinguish a genuinely expired login from a command-specific endpoint/auth bug.
1569
1575
  // A valid workspace read proves the CLI session and selected workspace are authenticated.
1570
- if (checkLoginState() === 'ok') {
1576
+ if (checkLoginState(projectDir) === 'ok') {
1571
1577
  return {
1572
1578
  success: false,
1573
1579
  output: [
@@ -1635,8 +1641,14 @@ function cliEnvironment() {
1635
1641
  * cwd is unrelated to the project). Set DUCTAPE_PROJECT_DIR explicitly in the server's env
1636
1642
  * (e.g. in .mcp.json) to pin it; falls back to this process's own cwd otherwise.
1637
1643
  */
1638
- function cliCwd() {
1639
- return process.env.DUCTAPE_PROJECT_DIR || process.cwd();
1644
+ // `process.cwd()` here is the long-running MCP server process's own working directory — fixed for
1645
+ // its whole lifetime by whatever launched it, not the project the calling agent is currently
1646
+ // working in. A static DUCTAPE_PROJECT_DIR env var has the same problem: it can't track an agent
1647
+ // session that moves between projects. `override` lets a per-call `project_dir` argument (see
1648
+ // cliInputSchema, eventsProjectValidationInputSchema, eventsTopicSetupInputSchema) take precedence
1649
+ // over both, so a single long-lived MCP server can be pointed at the right project per call.
1650
+ function cliCwd(override) {
1651
+ return override || process.env.DUCTAPE_PROJECT_DIR || process.cwd();
1640
1652
  }
1641
1653
  function shellArgument(value) {
1642
1654
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -1676,9 +1688,20 @@ const eventsTopicSetupInputSchema = z.object({
1676
1688
  sample: z.record(z.unknown()).optional(),
1677
1689
  idempotent: z.boolean().optional(),
1678
1690
  queueUrls: z.array(z.object({ env_slug: z.string().min(1), url: z.string().url() }).strict()).optional(),
1691
+ project_dir: z.string().optional().describe('Absolute path to the project this topic asset belongs to. Without this, the returned path ' +
1692
+ 'resolves against the MCP server\'s own working directory or DUCTAPE_PROJECT_DIR, which does ' +
1693
+ 'not track which project the current session is working in — pass it whenever working in a ' +
1694
+ 'project other than wherever the MCP server happened to start.'),
1679
1695
  });
1680
1696
  const eventsProjectValidationInputSchema = z.object({
1681
- dir: z.string().default('ductape/events').describe('Must be ductape/events relative to DUCTAPE_PROJECT_DIR.'),
1697
+ dir: z.string().default('ductape/events').describe('Must be ductape/events relative to the resolved project directory.'),
1698
+ project_dir: z.string().optional().describe('Absolute path to the project to validate. Without this, resolution falls back to the MCP ' +
1699
+ 'server\'s own working directory or DUCTAPE_PROJECT_DIR, which does not track which project ' +
1700
+ 'the current session is working in — pass it whenever working in a project other than ' +
1701
+ 'wherever the MCP server happened to start.'),
1702
+ });
1703
+ const featuresProjectValidationInputSchema = z.object({
1704
+ project_dir: z.string().describe('Absolute path to the linked Ductape project containing ductape/features/. The validator is read-only.'),
1682
1705
  });
1683
1706
  const readOnlyLocalAnnotations = {
1684
1707
  readOnlyHint: true,
@@ -1914,6 +1937,16 @@ SECURITY
1914
1937
  $Secret{tag}; manifests never embed credentials. Keep snd/stg/prd values separate.
1915
1938
  Use ductape_cli("secrets-import-env --env-file <local-file> --source-key <ENV_KEY> --key <secret-tag> --env <slug> --json").
1916
1939
  The command reads the value locally and redacts it from output; never place the value in an MCP argument.
1940
+ Before running that import for any discovered .env-backed secret candidate, ask the user
1941
+ explicitly whether to create a Ductape Secret from it — never decide silently either way (not
1942
+ "migrate everything found" and not "leave .env alone"). Once a value is migrated, update the
1943
+ consuming code to read it through Ductape rather than the environment: NestJS code uses the
1944
+ @Secret() decorator from @ductape/nestjs (see ductape_docs({ topic: "secrets" }) for the DI
1945
+ pattern); every other runtime calls secrets.fetch(key) (ductape_execute("secrets.fetch", [key]))
1946
+ instead of process.env / os.Getenv / System.getenv / IConfiguration for that value. Only remove
1947
+ the .env entry after the new code path is verified working, and never do this for bootstrap
1948
+ config Ductape itself needs to start (DUCTAPE_ACCESS_KEY, DUCTAPE_REDIS_URL, NODE_ENV) — those
1949
+ necessarily stay in the environment.
1917
1950
  secret_references are names-only navigation evidence from Docker Compose, Kubernetes secretKeyRef,
1918
1951
  GitHub/GitLab/Jenkins, Spring, .NET configuration, Terraform, and AWS/GCP/Azure secret managers.
1919
1952
  Their value is always [NOT_READ]. Inspect context before deciding whether a reference is sensitive,
@@ -2022,7 +2055,9 @@ COMPONENT DECISIONS
2022
2055
  represents a reusable product capability.
2023
2056
 
2024
2057
  LANGUAGE RUNTIME SHAPES
2025
- TypeScript: @ductape/sdk; NestJS uses @ductape/nestjs, @Events.Consumer, and request-scoped context.
2058
+ TypeScript: @ductape/sdk; NestJS uses @ductape/nestjs, @Events.Consumer, request-scoped context,
2059
+ and @Secret() (DuctapeSecretsModule.register({ keys })) for any secret value instead of .env/
2060
+ ConfigService — see ductape_docs({ topic: "secrets" }).
2026
2061
  Go: explicit services, context.Context, cancellation, typed errors, and owned worker shutdown.
2027
2062
  Java: DI/Spring integration where present, executor ownership, CompletableFuture boundaries.
2028
2063
  .NET: DI, hosted services, async/await, CancellationToken, configuration binding.
@@ -2904,7 +2939,7 @@ Import an existing resource and register it on the product:
2904
2939
  Use import-persist-all for multi-env products (required):
2905
2940
  ductape_cli("cloud resources import-persist-all -f all-envs.json --json")
2906
2941
  File is a JSON ARRAY — one entry per env, same product + component tag across all entries.
2907
- Each entry: { cloud, service, type, product, component, env, resource, region?, dbName?, masterPassword? }
2942
+ Each entry: { cloud, service, type, product, component, env, resource, region?, dbName?, masterPassword?, username? }
2908
2943
  Supported service identifiers: s3, gcs, blob, rds, postgresql, cloudsql, sqs, pubsub,
2909
2944
  servicebus, neptune, cosmos-gremlin, opensearch, azure-search, atlas-cluster, aura-instance,
2910
2945
  vertex-vector-search, spanner-graph, dynamodb, keyspaces, mysql
@@ -2917,9 +2952,17 @@ Import an existing resource and register it on the product:
2917
2952
  without masterPassword will fail with a clear error at import time; skipping that check and
2918
2953
  importing anyway is not possible — always ask the user for the instance's password before
2919
2954
  calling import-persist-all for an aura-instance entry, the same as you would ask for an RDS
2920
- master password. Aura's username is always "neo4j"never ask the user for it. Atlas
2921
- (atlas-cluster) does NOT need masterPassword — Ductape mints/rotates its own database user via
2922
- the Atlas Admin API.
2955
+ master password. Atlas (atlas-cluster) does NOT need masterPassword Ductape mints/rotates
2956
+ its own database user via the Atlas Admin API.
2957
+
2958
+ username (aura-instance only) defaults to "neo4j" if omitted — that IS the correct value for
2959
+ most Aura instances. But this default is NOT a hard guarantee: at least one real Aura instance
2960
+ has been confirmed to authenticate with a different database username instead (its instance id,
2961
+ in the one case observed) and rejects "neo4j" as unauthorized. If a fresh Aura import connects
2962
+ successfully but then fails at runtime with a Neo4j "unauthorized"/access-denied error despite a
2963
+ correct password, don't assume the password is wrong — try re-importing with username set
2964
+ explicitly (ask the user what database username the instance actually uses) before concluding
2965
+ the password itself is bad.
2923
2966
 
2924
2967
  Provision a brand-new resource and register it:
2925
2968
  ductape_cli("cloud resources provision-persist-all -f all-envs.json --json")
@@ -3074,6 +3117,47 @@ Important:
3074
3117
  - Other services (storage, broker, graph, etc.) resolve $Secret{} references automatically
3075
3118
  using the singleton secrets service — no manual resolution needed in most cases.
3076
3119
  - Never log or return resolved secret values to end users.
3120
+
3121
+ APPLICATION CODE — PREFER DUCTAPE SECRETS OVER .env / process.env:
3122
+ Once a workspace secret exists for a value, application code should read it through Ductape,
3123
+ not through the process environment or a framework config layer backed by .env. This applies
3124
+ to values the code reads for its own runtime use (API keys, DB passwords, webhook signing
3125
+ secrets, etc.) — it does not apply to non-secret bootstrap config Ductape itself needs to start
3126
+ (DUCTAPE_ACCESS_KEY, DUCTAPE_REDIS_URL, NODE_ENV), which necessarily stay in the environment.
3127
+
3128
+ NestJS: inject with the @Secret() decorator from @ductape/nestjs instead of ConfigService.get()
3129
+ or process.env for any value that is (or should be) a Ductape secret:
3130
+ import { DuctapeSecretsModule, Secret, SecretHandle } from '@ductape/nestjs';
3131
+
3132
+ @Module({ imports: [DuctapeSecretsModule.register({ keys: ['STRIPE_API_KEY'] })] })
3133
+ class PaymentsModule {}
3134
+
3135
+ @Injectable()
3136
+ class PaymentsService {
3137
+ constructor(@Secret('STRIPE_API_KEY') private readonly stripeKey: SecretHandle) {}
3138
+ // this.stripeKey.value holds the decrypted secret
3139
+ }
3140
+ Register every key the module needs via DuctapeSecretsModule.register({ keys: [...] }); each
3141
+ registered key becomes an injectable token resolved through DuctapeContextService, not read
3142
+ from process.env at any point.
3143
+
3144
+ Other runtimes (plain Node/Express/Fastify, Go, Java, .NET, etc.): call
3145
+ ductape_execute("secrets.fetch", ["KEY"]) (or the SDK's secrets.fetch(key) directly in
3146
+ application code) rather than reading process.env / os.Getenv / System.getenv / IConfiguration
3147
+ for a value that is a Ductape secret. Resolve $Secret{KEY} references in config the same way —
3148
+ through the secrets service, never by pre-substituting from the environment.
3149
+
3150
+ MIGRATING EXISTING .env SECRETS: when environment/codebase discovery (see ENVIRONMENTS and
3151
+ SECURITY sections of the migrate-codebase guidance) finds .env entries that look like secrets
3152
+ (API keys, passwords, tokens, connection strings with embedded credentials), do not silently
3153
+ decide either way. Ask the user explicitly whether to create Ductape Secrets from those .env
3154
+ entries — do not assume "yes, migrate everything" or "no, leave .env alone". If the user agrees,
3155
+ import each value locally and specifically:
3156
+ ductape_cli("secrets-import-env --env-file <local-file> --source-key <ENV_KEY> --key <secret-tag> --env <slug> --json")
3157
+ then update application code to read the new secret through @Secret()/secrets.fetch() as above,
3158
+ and only remove the .env entry once that code path is verified working. Never read the .env
3159
+ value into MCP context or an MCP argument while doing this: the CLI command above reads the
3160
+ local file directly and redacts the value from its own output.
3077
3161
  `.trim(),
3078
3162
  apps: `
3079
3163
  DUCTAPE APPS
@@ -4289,8 +4373,11 @@ STEP 6 — WRITE the feature into the project codebase
4289
4373
  exiting. Ductape cannot run this step for you generically — a Feature handler is real
4290
4374
  application code that typically depends on the app's own services, unlike a migration file,
4291
4375
  which is declarative data the Ductape backend can apply directly.
4292
- 4. Persist Features by running "ductape features sync" (or ductape_cli("features sync")), which
4293
- finds the linked project and runs its "features:sync" script never automatically on boot.
4376
+ 4. Run ductape_features_validate_project({ project_dir: "<absolute-project-root>" }). This is a
4377
+ read-only AST conformance gate. Stop on any diagnostic; do not sync or mutate remote state.
4378
+ 5. Persist Features by running "ductape features sync" (or ductape_cli("features sync")), which
4379
+ repeats the fail-closed validator, then finds the linked project and runs its "features:sync"
4380
+ script — never automatically on boot.
4294
4381
  Pass an optional filter argument to scope it: "ductape features sync payments".
4295
4382
 
4296
4383
  A NestJS service with both concerns split looks like:
@@ -4322,7 +4409,8 @@ STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
4322
4409
  6. Direct Date.now() and Math.random() are forbidden in handlers. Use ctx.transform.now(),
4323
4410
  ctx.transform.uuid(), and ctx.transform.concat/replace/substring/upper/lower/trim so values
4324
4411
  are generated at execution time. Use a portable Function for domain-specific generators.
4325
- 7. Never use branchOverrides in newly generated code. It exists only to migrate old handlers.
4412
+ 7. Never use branchOverrides or recordScenarios in newly generated code. Existing handlers must
4413
+ set controlFlowMode: "legacy" explicitly while they are being migrated.
4326
4414
 
4327
4415
  Prefer explicit portable branching for step results:
4328
4416
  const result = await ctx.step("find", () => ctx.database.execute(...));
@@ -5924,8 +6012,8 @@ const eventsTopicSetupHandler = async (args) => {
5924
6012
  return {
5925
6013
  content: [{ type: 'text', text: JSON.stringify({
5926
6014
  ok: true,
5927
- project_root: cliCwd(),
5928
- path: join(cliCwd(), relativePath),
6015
+ project_root: cliCwd(args.project_dir),
6016
+ path: join(cliCwd(args.project_dir), relativePath),
5929
6017
  relative_path: relativePath,
5930
6018
  definition,
5931
6019
  create_command: `ductape events topics create -f ${relativePath} --json`,
@@ -5946,7 +6034,14 @@ const eventsProjectValidationHandler = async (args) => {
5946
6034
  isError: true,
5947
6035
  };
5948
6036
  }
5949
- const result = runCli('events topics validate --dir ductape/events --json');
6037
+ const result = runCli('events topics validate --dir ductape/events --json', args.project_dir);
6038
+ return {
6039
+ content: [{ type: 'text', text: result.output || '(no output)' }],
6040
+ ...(result.success ? {} : { isError: true }),
6041
+ };
6042
+ };
6043
+ const featuresProjectValidationHandler = async (args) => {
6044
+ const result = runCli('features validate --json', args.project_dir);
5950
6045
  return {
5951
6046
  content: [{ type: 'text', text: result.output || '(no output)' }],
5952
6047
  ...(result.success ? {} : { isError: true }),
@@ -5965,13 +6060,22 @@ const cliInputSchema = z.object({
5965
6060
  'no linked project required — the product tag is always an explicit argument). Quotas, ' +
5966
6061
  'fallbacks, jobs, and healthchecks use resources commands. App actions and auths are ' +
5967
6062
  'configured in the Workbench UI. Features have no CLI creation command: define them in ' +
5968
- 'application code with features.define under ductape/features/. Persist them with ' +
6063
+ 'application code with features.define under ductape/features/. Always run "features validate" first; ' +
6064
+ 'it performs read-only AST conformance checks and "features sync" repeats the same fail-closed preflight. Persist them with ' +
5969
6065
  '"features sync" (runs the project\'s own "features:sync" npm script) — never call ' +
5970
6066
  'features.define from the app\'s normal startup path, since that blocks every boot on ' +
5971
6067
  'Ductape API reachability. See ductape_docs for the full convention.\n\n' +
5972
6068
  'The CLI uses the user\'s local logged-in session. Prefer browser OAuth in a trusted local terminal: ' +
5973
6069
  'ductape login --browser google (or github). It returns automatically through a validated loopback callback. ' +
5974
6070
  'Never invoke interactive login through MCP or ask the user for credentials.'),
6071
+ project_dir: z.string().optional().describe('Absolute path to the project this command should run against (e.g. "ductape/events" is ' +
6072
+ 'resolved relative to this directory, and product/project linking state is read from it). ' +
6073
+ 'The MCP server is a long-running process — its own working directory (or a static ' +
6074
+ 'DUCTAPE_PROJECT_DIR env var, if set) does NOT follow which project the current conversation ' +
6075
+ 'is actually working in. Pass this whenever the command is project-relative (events topics, ' +
6076
+ 'anything reading ductape/ files, link/unlink) and the session is working in a project other ' +
6077
+ 'than wherever the MCP server happened to start. Omit only for purely workspace-level commands ' +
6078
+ '(e.g. "products list") where no project directory is relevant.'),
5975
6079
  });
5976
6080
  async function loadMcpSdk() {
5977
6081
  try {
@@ -6116,7 +6220,7 @@ async function main() {
6116
6220
  ],
6117
6221
  }));
6118
6222
  const cliHandler = async (args) => {
6119
- const cli = checkCli();
6223
+ const cli = checkCli(args.project_dir);
6120
6224
  if (!cli.available) {
6121
6225
  return {
6122
6226
  content: [{
@@ -6165,7 +6269,7 @@ async function main() {
6165
6269
  // The user may complete `ductape login` in another terminal while this MCP process remains
6166
6270
  // alive; caching "none" would otherwise make the MCP blind to the newly written session.
6167
6271
  if (authState === 'unknown' || authState === 'none') {
6168
- checkLoginState();
6272
+ checkLoginState(args.project_dir);
6169
6273
  }
6170
6274
  if (authState === 'none') {
6171
6275
  const wsFlag = process.env.DUCTAPE_WORKSPACE ? ` --workspace "${process.env.DUCTAPE_WORKSPACE}"` : '';
@@ -6193,10 +6297,10 @@ async function main() {
6193
6297
  }
6194
6298
  // Sync to the configured workspace once per process (best-effort)
6195
6299
  if (!workspaceSynced) {
6196
- syncWorkspace();
6300
+ syncWorkspace(args.project_dir);
6197
6301
  }
6198
6302
  }
6199
- const result = runCli(args.command);
6303
+ const result = runCli(args.command, args.project_dir);
6200
6304
  // Update cached state after auth commands
6201
6305
  if (firstWord === 'login' && result.success) {
6202
6306
  authState = 'ok';
@@ -6786,6 +6890,14 @@ async function main() {
6786
6890
  inputSchema: eventsProjectValidationInputSchema,
6787
6891
  annotations: readOnlyLocalAnnotations,
6788
6892
  }, eventsProjectValidationHandler);
6893
+ server.registerTool('ductape_features_validate_project', {
6894
+ title: 'Validate Ductape Feature Project',
6895
+ description: 'Read-only AST validation of code-first Features under ductape/features/. Detects native branching, loops, ' +
6896
+ 'collection callbacks, host-bound time/random/environment access, and legacy recording options before sync. ' +
6897
+ 'A failed result means features sync must not be run.',
6898
+ inputSchema: featuresProjectValidationInputSchema,
6899
+ annotations: readOnlyLocalAnnotations,
6900
+ }, featuresProjectValidationHandler);
6789
6901
  server.registerTool('ductape_function_setup', {
6790
6902
  title: 'Ductape Portable Function Setup',
6791
6903
  description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
@@ -6971,8 +7083,14 @@ async function main() {
6971
7083
  ' can never be retrieved again via the Aura API — Ductape has no way to source it automatically.\n' +
6972
7084
  ' Always ask the user for the instance password before calling import-persist-all for an\n' +
6973
7085
  ' aura-instance entry; do not attempt the import without it, it will fail with a clear error\n' +
6974
- ' naming exactly this. The username is always "neo4j" for every Aura instance — never ask the\n' +
6975
- ' user for it, and never put it in masterPassword by mistake.\n' +
7086
+ ' naming exactly this.\n' +
7087
+ ' username defaults to "neo4j" if omitted, which is correct for most Aura instances — but this\n' +
7088
+ ' is NOT a hard guarantee. At least one real instance has been confirmed to use a different\n' +
7089
+ ' database username instead (its instance id, in the one case observed) and rejects "neo4j" as\n' +
7090
+ ' unauthorized. If the import succeeds but connect later fails with a Neo4j\n' +
7091
+ ' unauthorized/access-denied error despite a correct password, re-import with username set\n' +
7092
+ ' explicitly (ask the user what database username the instance actually uses) rather than\n' +
7093
+ ' assuming the password is wrong.\n' +
6976
7094
  ' Step 1 — discover the instance:\n' +
6977
7095
  ' ductape_cli("cloud resources list -f /tmp/aura-list.json --json")\n' +
6978
7096
  ' File: {"cloud": "<aura-connection-tag>", "service": "aura-instance"}\n' +
@@ -6983,7 +7101,9 @@ async function main() {
6983
7101
  ' explicitly wants different Aura instances per environment):\n' +
6984
7102
  ' cloud (connection tag), service: "aura-instance", type: "graphs",\n' +
6985
7103
  ' product, component (new or existing graph tag), env, resource (instance id/name from\n' +
6986
- ' Step 1), masterPassword (the instance password — ask the user, do not guess or omit).\n' +
7104
+ ' Step 1), masterPassword (the instance password — ask the user, do not guess or omit),\n' +
7105
+ ' username (optional — omit for the "neo4j" default; only set if the user tells you the\n' +
7106
+ ' instance uses a different one, or if a prior attempt with "neo4j" failed to authenticate).\n' +
6987
7107
  ' Example (two envs, same Aura instance):\n' +
6988
7108
  ' [{"cloud":"aura-tag","service":"aura-instance","type":"graphs",\n' +
6989
7109
  ' "product":"my-product","component":"commerce-network","env":"snd",\n' +
@@ -7027,6 +7147,7 @@ async function main() {
7027
7147
  server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
7028
7148
  server.tool('ductape_events_topic_setup', eventsTopicSetupInputSchema.shape, readOnlyLocalAnnotations, eventsTopicSetupHandler);
7029
7149
  server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
7150
+ server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
7030
7151
  server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
7031
7152
  server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
7032
7153
  server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",