@ductape/mcp 0.3.1 → 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.
- package/dist/index.js +185 -26
- 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
|
|
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
|
-
|
|
1639
|
-
|
|
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
|
|
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,
|
|
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,11 +2939,31 @@ 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? }
|
|
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
|
|
2911
2946
|
|
|
2947
|
+
masterPassword is required for self-hosted-style imports where Ductape cannot mint or fetch
|
|
2948
|
+
credentials on its own: rds, postgresql, cloudsql, mysql (unless the instance already has
|
|
2949
|
+
stored Ductape credentials from a prior link), and — critically — aura-instance. Neo4j Aura
|
|
2950
|
+
shows the auto-generated database password only ONCE, at instance creation, in the Aura
|
|
2951
|
+
console, and it can never be retrieved again via the Aura API. Importing an Aura instance
|
|
2952
|
+
without masterPassword will fail with a clear error at import time; skipping that check and
|
|
2953
|
+
importing anyway is not possible — always ask the user for the instance's password before
|
|
2954
|
+
calling import-persist-all for an aura-instance entry, the same as you would ask for an RDS
|
|
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.
|
|
2966
|
+
|
|
2912
2967
|
Provision a brand-new resource and register it:
|
|
2913
2968
|
ductape_cli("cloud resources provision-persist-all -f all-envs.json --json")
|
|
2914
2969
|
Additional per-entry fields: tier, region/location, waitForReady.
|
|
@@ -3062,6 +3117,47 @@ Important:
|
|
|
3062
3117
|
- Other services (storage, broker, graph, etc.) resolve $Secret{} references automatically
|
|
3063
3118
|
using the singleton secrets service — no manual resolution needed in most cases.
|
|
3064
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.
|
|
3065
3161
|
`.trim(),
|
|
3066
3162
|
apps: `
|
|
3067
3163
|
DUCTAPE APPS
|
|
@@ -4277,8 +4373,11 @@ STEP 6 — WRITE the feature into the project codebase
|
|
|
4277
4373
|
exiting. Ductape cannot run this step for you generically — a Feature handler is real
|
|
4278
4374
|
application code that typically depends on the app's own services, unlike a migration file,
|
|
4279
4375
|
which is declarative data the Ductape backend can apply directly.
|
|
4280
|
-
4.
|
|
4281
|
-
|
|
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.
|
|
4282
4381
|
Pass an optional filter argument to scope it: "ductape features sync payments".
|
|
4283
4382
|
|
|
4284
4383
|
A NestJS service with both concerns split looks like:
|
|
@@ -4310,7 +4409,8 @@ STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
|
|
|
4310
4409
|
6. Direct Date.now() and Math.random() are forbidden in handlers. Use ctx.transform.now(),
|
|
4311
4410
|
ctx.transform.uuid(), and ctx.transform.concat/replace/substring/upper/lower/trim so values
|
|
4312
4411
|
are generated at execution time. Use a portable Function for domain-specific generators.
|
|
4313
|
-
7. Never use branchOverrides in newly generated code.
|
|
4412
|
+
7. Never use branchOverrides or recordScenarios in newly generated code. Existing handlers must
|
|
4413
|
+
set controlFlowMode: "legacy" explicitly while they are being migrated.
|
|
4314
4414
|
|
|
4315
4415
|
Prefer explicit portable branching for step results:
|
|
4316
4416
|
const result = await ctx.step("find", () => ctx.database.execute(...));
|
|
@@ -5912,8 +6012,8 @@ const eventsTopicSetupHandler = async (args) => {
|
|
|
5912
6012
|
return {
|
|
5913
6013
|
content: [{ type: 'text', text: JSON.stringify({
|
|
5914
6014
|
ok: true,
|
|
5915
|
-
project_root: cliCwd(),
|
|
5916
|
-
path: join(cliCwd(), relativePath),
|
|
6015
|
+
project_root: cliCwd(args.project_dir),
|
|
6016
|
+
path: join(cliCwd(args.project_dir), relativePath),
|
|
5917
6017
|
relative_path: relativePath,
|
|
5918
6018
|
definition,
|
|
5919
6019
|
create_command: `ductape events topics create -f ${relativePath} --json`,
|
|
@@ -5934,7 +6034,14 @@ const eventsProjectValidationHandler = async (args) => {
|
|
|
5934
6034
|
isError: true,
|
|
5935
6035
|
};
|
|
5936
6036
|
}
|
|
5937
|
-
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);
|
|
5938
6045
|
return {
|
|
5939
6046
|
content: [{ type: 'text', text: result.output || '(no output)' }],
|
|
5940
6047
|
...(result.success ? {} : { isError: true }),
|
|
@@ -5953,13 +6060,22 @@ const cliInputSchema = z.object({
|
|
|
5953
6060
|
'no linked project required — the product tag is always an explicit argument). Quotas, ' +
|
|
5954
6061
|
'fallbacks, jobs, and healthchecks use resources commands. App actions and auths are ' +
|
|
5955
6062
|
'configured in the Workbench UI. Features have no CLI creation command: define them in ' +
|
|
5956
|
-
'application code with features.define under ductape/features/.
|
|
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 ' +
|
|
5957
6065
|
'"features sync" (runs the project\'s own "features:sync" npm script) — never call ' +
|
|
5958
6066
|
'features.define from the app\'s normal startup path, since that blocks every boot on ' +
|
|
5959
6067
|
'Ductape API reachability. See ductape_docs for the full convention.\n\n' +
|
|
5960
6068
|
'The CLI uses the user\'s local logged-in session. Prefer browser OAuth in a trusted local terminal: ' +
|
|
5961
6069
|
'ductape login --browser google (or github). It returns automatically through a validated loopback callback. ' +
|
|
5962
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.'),
|
|
5963
6079
|
});
|
|
5964
6080
|
async function loadMcpSdk() {
|
|
5965
6081
|
try {
|
|
@@ -6104,7 +6220,7 @@ async function main() {
|
|
|
6104
6220
|
],
|
|
6105
6221
|
}));
|
|
6106
6222
|
const cliHandler = async (args) => {
|
|
6107
|
-
const cli = checkCli();
|
|
6223
|
+
const cli = checkCli(args.project_dir);
|
|
6108
6224
|
if (!cli.available) {
|
|
6109
6225
|
return {
|
|
6110
6226
|
content: [{
|
|
@@ -6153,7 +6269,7 @@ async function main() {
|
|
|
6153
6269
|
// The user may complete `ductape login` in another terminal while this MCP process remains
|
|
6154
6270
|
// alive; caching "none" would otherwise make the MCP blind to the newly written session.
|
|
6155
6271
|
if (authState === 'unknown' || authState === 'none') {
|
|
6156
|
-
checkLoginState();
|
|
6272
|
+
checkLoginState(args.project_dir);
|
|
6157
6273
|
}
|
|
6158
6274
|
if (authState === 'none') {
|
|
6159
6275
|
const wsFlag = process.env.DUCTAPE_WORKSPACE ? ` --workspace "${process.env.DUCTAPE_WORKSPACE}"` : '';
|
|
@@ -6181,10 +6297,10 @@ async function main() {
|
|
|
6181
6297
|
}
|
|
6182
6298
|
// Sync to the configured workspace once per process (best-effort)
|
|
6183
6299
|
if (!workspaceSynced) {
|
|
6184
|
-
syncWorkspace();
|
|
6300
|
+
syncWorkspace(args.project_dir);
|
|
6185
6301
|
}
|
|
6186
6302
|
}
|
|
6187
|
-
const result = runCli(args.command);
|
|
6303
|
+
const result = runCli(args.command, args.project_dir);
|
|
6188
6304
|
// Update cached state after auth commands
|
|
6189
6305
|
if (firstWord === 'login' && result.success) {
|
|
6190
6306
|
authState = 'ok';
|
|
@@ -6774,6 +6890,14 @@ async function main() {
|
|
|
6774
6890
|
inputSchema: eventsProjectValidationInputSchema,
|
|
6775
6891
|
annotations: readOnlyLocalAnnotations,
|
|
6776
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);
|
|
6777
6901
|
server.registerTool('ductape_function_setup', {
|
|
6778
6902
|
title: 'Ductape Portable Function Setup',
|
|
6779
6903
|
description: 'Read-only: generate (without applying) the mandatory secure local + remote runtime setup for application functions used by Features. ' +
|
|
@@ -6953,6 +7077,40 @@ async function main() {
|
|
|
6953
7077
|
' "product":"my-product","component":"billing-db","env":"snd","resource":"Cluster0","dbName":"billing_snd"},\n' +
|
|
6954
7078
|
' {"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
|
|
6955
7079
|
' "product":"my-product","component":"billing-db","env":"prd","resource":"Cluster0","dbName":"billing_prd"}]\n' +
|
|
7080
|
+
' - Neo4j Aura (graph) import flow — service identifier is "aura-instance":\n' +
|
|
7081
|
+
' CRITICAL: masterPassword is REQUIRED in every import entry for aura-instance. Aura shows the\n' +
|
|
7082
|
+
' auto-generated database password only ONCE, at instance creation, in the Aura console, and it\n' +
|
|
7083
|
+
' can never be retrieved again via the Aura API — Ductape has no way to source it automatically.\n' +
|
|
7084
|
+
' Always ask the user for the instance password before calling import-persist-all for an\n' +
|
|
7085
|
+
' aura-instance entry; do not attempt the import without it, it will fail with a clear error\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' +
|
|
7094
|
+
' Step 1 — discover the instance:\n' +
|
|
7095
|
+
' ductape_cli("cloud resources list -f /tmp/aura-list.json --json")\n' +
|
|
7096
|
+
' File: {"cloud": "<aura-connection-tag>", "service": "aura-instance"}\n' +
|
|
7097
|
+
' Returns a list of Aura instances; note the "id" or "name" field (this is your resource\n' +
|
|
7098
|
+
' identifier). Aura instances cannot be created through Ductape — provision-persist-all is\n' +
|
|
7099
|
+
' not supported for this provider; only an already-existing instance can be linked.\n' +
|
|
7100
|
+
' Step 2 — import (one entry per product env, same instance across all envs unless the user\n' +
|
|
7101
|
+
' explicitly wants different Aura instances per environment):\n' +
|
|
7102
|
+
' cloud (connection tag), service: "aura-instance", type: "graphs",\n' +
|
|
7103
|
+
' product, component (new or existing graph tag), env, resource (instance id/name from\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' +
|
|
7107
|
+
' Example (two envs, same Aura instance):\n' +
|
|
7108
|
+
' [{"cloud":"aura-tag","service":"aura-instance","type":"graphs",\n' +
|
|
7109
|
+
' "product":"my-product","component":"commerce-network","env":"snd",\n' +
|
|
7110
|
+
' "resource":"1d7fc731","masterPassword":"<user-supplied>"},\n' +
|
|
7111
|
+
' {"cloud":"aura-tag","service":"aura-instance","type":"graphs",\n' +
|
|
7112
|
+
' "product":"my-product","component":"commerce-network","env":"prd",\n' +
|
|
7113
|
+
' "resource":"1d7fc731","masterPassword":"<user-supplied>"}]\n' +
|
|
6956
7114
|
' - Message broker / event broker import:\n' +
|
|
6957
7115
|
' CLI accepts these aliases for the messageBrokers module: events, event, broker, brokers, message-brokers.\n' +
|
|
6958
7116
|
' List existing brokers: ductape_cli("resources events list --json")\n' +
|
|
@@ -6989,6 +7147,7 @@ async function main() {
|
|
|
6989
7147
|
server.tool('ductape_events_discover', eventsDiscoveryInputSchema.shape, eventsDiscoveryHandler);
|
|
6990
7148
|
server.tool('ductape_events_topic_setup', eventsTopicSetupInputSchema.shape, readOnlyLocalAnnotations, eventsTopicSetupHandler);
|
|
6991
7149
|
server.tool('ductape_events_validate_project', eventsProjectValidationInputSchema.shape, readOnlyLocalAnnotations, eventsProjectValidationHandler);
|
|
7150
|
+
server.tool('ductape_features_validate_project', featuresProjectValidationInputSchema.shape, readOnlyLocalAnnotations, featuresProjectValidationHandler);
|
|
6992
7151
|
server.tool('ductape_function_setup', portableFunctionSetupInputSchema.shape, portableFunctionSetupAnnotations, portableFunctionSetupHandler);
|
|
6993
7152
|
server.tool('ductape_redis_setup', redisSetupInputSchema.shape, redisSetupHandler);
|
|
6994
7153
|
server.tool('ductape_migration_plan', migrationInputSchema.shape, migrationHandler);
|