@ductape/mcp 0.1.59 → 0.1.61
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 +48 -48
- package/dist/proxy-client.d.ts.map +1 -1
- package/dist/proxy-client.js +4 -1
- package/package.json +1 -1
- package/scripts/check-frontend-analytics-guidance.mjs +11 -0
- package/src/index.ts +53 -49
- package/src/proxy-client.ts +4 -1
package/dist/index.js
CHANGED
|
@@ -723,47 +723,10 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
723
723
|
vector.count [{ product, env, vector, namespace? }]
|
|
724
724
|
|
|
725
725
|
━━━ MODULE: features ━━━
|
|
726
|
-
features.
|
|
727
|
-
|
|
728
|
-
name: string,
|
|
729
|
-
description?: string,
|
|
730
|
-
|
|
731
|
-
// INPUT SCHEMA (top-level): declares what fields this feature accepts when executed at runtime.
|
|
732
|
-
// These are the fields callers will pass to features.execute / features.dispatch.
|
|
733
|
-
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
734
|
-
input?: { fieldName: { type: string, required?: boolean } },
|
|
735
|
-
|
|
736
|
-
output?: object,
|
|
737
|
-
envs?: [{ slug: string, active?: boolean }],
|
|
738
|
-
steps: [
|
|
739
|
-
{
|
|
740
|
-
tag: string, // unique step id
|
|
741
|
-
name?: string,
|
|
742
|
-
type: "action"|"database"|"graph"|"notification"|"storage"|"publish"|"feature"|"sleep"|"wait_signal",
|
|
743
|
-
app?: string, // for type=action
|
|
744
|
-
event?: string, // action/event tag
|
|
745
|
-
database?: string, // for type=database
|
|
746
|
-
graph?: string, // for type=graph
|
|
747
|
-
notification?: string, // for type=notification
|
|
748
|
-
storage?: string, // for type=storage
|
|
749
|
-
broker?: string, // for type=publish
|
|
750
|
-
feature?: string, // for type=feature (child feature)
|
|
751
|
-
|
|
752
|
-
// STEP INPUT: maps this feature's declared input fields (or prior step outputs) → the step's underlying action/event fields.
|
|
753
|
-
// ← CALL ductape_generate_payload (operation_family matching step type, method="run", targets={app/event/database/etc.})
|
|
754
|
-
// to discover what fields the target accepts, then wire them with "$Input{fieldName}" or "$Step{stepTag}{field}" references.
|
|
755
|
-
input?: { "body:field": "$Input{fieldName}" | "$Step{stepTag}{field}" | literal },
|
|
756
|
-
|
|
757
|
-
condition?: string, // e.g. "$Step{validate}{valid} == true"
|
|
758
|
-
dependsOn?: string[],
|
|
759
|
-
options?: { retries?: number, timeout?: number, allow_fail?: boolean, critical?: boolean }
|
|
760
|
-
}
|
|
761
|
-
]
|
|
762
|
-
}]
|
|
763
|
-
features.update [product_tag, feature_tag, data: { name?: string, description?: string, steps?: array, envs?: array }]
|
|
726
|
+
Feature definitions are code-first. Use features.define in application source; do not call
|
|
727
|
+
administrative create/update/delete methods through ductape_execute.
|
|
764
728
|
features.fetch [product_tag, feature_tag]
|
|
765
729
|
features.fetchAll [product_tag]
|
|
766
|
-
features.delete [product_tag, feature_tag]
|
|
767
730
|
|
|
768
731
|
features.define [{
|
|
769
732
|
product?: string,
|
|
@@ -977,12 +940,13 @@ const payloadGenerateInputSchema = z.object({
|
|
|
977
940
|
'For quotas/fallbacks: { tag: "resource_tag" }. ' +
|
|
978
941
|
'For storage: { storage: "storage_tag" }. ' +
|
|
979
942
|
'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'),
|
|
980
|
-
include_session: z.boolean().optional().
|
|
943
|
+
include_session: z.boolean().optional().describe('Include a session placeholder inside the generated input object. ' +
|
|
944
|
+
'Defaults to false for execution_context="system" and true otherwise. ' +
|
|
981
945
|
'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'),
|
|
982
946
|
execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe('Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
|
|
983
947
|
'"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
|
|
984
948
|
'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'),
|
|
985
|
-
include_cache: z.boolean().optional().
|
|
949
|
+
include_cache: z.boolean().optional().describe('Include a cache tag inside the generated payload. Defaults to false; set true only when the runtime call should use cache explicitly.'),
|
|
986
950
|
schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe('"strict" — fail if any required field cannot be resolved. ' +
|
|
987
951
|
'"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'),
|
|
988
952
|
input_hint: z.record(z.any()).optional().describe('Optional. Partial input values you already know. These are merged into the generated payload template ' +
|
|
@@ -1084,7 +1048,7 @@ function addSessionAwarenessMetadata(generated, args) {
|
|
|
1084
1048
|
const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
|
|
1085
1049
|
const executionContext = args.execution_context ?? 'user';
|
|
1086
1050
|
const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
|
|
1087
|
-
const sessionRequested = args.include_session !==
|
|
1051
|
+
const sessionRequested = args.include_session ?? executionContext !== 'system';
|
|
1088
1052
|
const warnings = [];
|
|
1089
1053
|
if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
|
|
1090
1054
|
warnings.push(`Session attribution is missing for a ${executionContext}-context operation. ` +
|
|
@@ -1178,7 +1142,12 @@ const ADMIN_SUBCOMMANDS = [
|
|
|
1178
1142
|
];
|
|
1179
1143
|
function checkCli() {
|
|
1180
1144
|
try {
|
|
1181
|
-
const out = execSync('ductape --version', {
|
|
1145
|
+
const out = execSync('ductape --version', {
|
|
1146
|
+
encoding: 'utf8',
|
|
1147
|
+
timeout: 5000,
|
|
1148
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1149
|
+
env: cliEnvironment(),
|
|
1150
|
+
}).trim();
|
|
1182
1151
|
return { available: true, version: out || 'unknown' };
|
|
1183
1152
|
}
|
|
1184
1153
|
catch {
|
|
@@ -1194,6 +1163,7 @@ function checkLoginState() {
|
|
|
1194
1163
|
encoding: 'utf8',
|
|
1195
1164
|
timeout: 10000,
|
|
1196
1165
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1166
|
+
env: cliEnvironment(),
|
|
1197
1167
|
});
|
|
1198
1168
|
authState = 'ok';
|
|
1199
1169
|
return 'ok';
|
|
@@ -1209,7 +1179,12 @@ function syncWorkspace() {
|
|
|
1209
1179
|
if (!target)
|
|
1210
1180
|
return;
|
|
1211
1181
|
try {
|
|
1212
|
-
execSync(`ductape workspaces use "${target}"`, {
|
|
1182
|
+
execSync(`ductape workspaces use "${target}"`, {
|
|
1183
|
+
encoding: 'utf8',
|
|
1184
|
+
timeout: 10000,
|
|
1185
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1186
|
+
env: cliEnvironment(),
|
|
1187
|
+
});
|
|
1213
1188
|
}
|
|
1214
1189
|
catch {
|
|
1215
1190
|
// best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
|
|
@@ -1236,6 +1211,7 @@ function runCli(command) {
|
|
|
1236
1211
|
// instead of this wrapper killing the CLI first and reducing it to "(no data)".
|
|
1237
1212
|
timeout: 90000,
|
|
1238
1213
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1214
|
+
env: cliEnvironment(),
|
|
1239
1215
|
});
|
|
1240
1216
|
return { success: true, output: output.trim() };
|
|
1241
1217
|
}
|
|
@@ -1279,6 +1255,16 @@ function runCli(command) {
|
|
|
1279
1255
|
return { success: false, output: msg };
|
|
1280
1256
|
}
|
|
1281
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* MCP is publishable-key-only. The standalone CLI may manage its own access-key
|
|
1260
|
+
* credentials, but an access key present in the MCP host environment is never
|
|
1261
|
+
* forwarded into CLI subprocesses.
|
|
1262
|
+
*/
|
|
1263
|
+
function cliEnvironment() {
|
|
1264
|
+
const environment = { ...process.env };
|
|
1265
|
+
delete environment.DUCTAPE_ACCESS_KEY;
|
|
1266
|
+
return environment;
|
|
1267
|
+
}
|
|
1282
1268
|
const docsInputSchema = z.object({
|
|
1283
1269
|
topic: z.string().describe('Feature topic to look up. Supported: ' +
|
|
1284
1270
|
'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
@@ -3870,7 +3856,8 @@ async function main() {
|
|
|
3870
3856
|
}
|
|
3871
3857
|
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
3872
3858
|
if (!key) {
|
|
3873
|
-
throw new Error('
|
|
3859
|
+
throw new Error('Runtime authentication is missing. Set DUCTAPE_PUBLISHABLE_KEY in the MCP server environment ' +
|
|
3860
|
+
'or pass publishable_key to ductape_execute. The MCP server never accepts access keys.');
|
|
3874
3861
|
}
|
|
3875
3862
|
// The TS SDK uses ductape.events.* for broker operations; the backend proxy uses messageBrokers.
|
|
3876
3863
|
const proxyModule = args.module === 'events' ? 'messageBrokers' : args.module;
|
|
@@ -3893,7 +3880,18 @@ async function main() {
|
|
|
3893
3880
|
return p;
|
|
3894
3881
|
});
|
|
3895
3882
|
}
|
|
3896
|
-
|
|
3883
|
+
let result;
|
|
3884
|
+
try {
|
|
3885
|
+
result = await executeViaProxy(key, proxyModule, args.method, params);
|
|
3886
|
+
}
|
|
3887
|
+
catch (error) {
|
|
3888
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3889
|
+
if (/authentication failed|unauthorized|invalid.*key/i.test(message)) {
|
|
3890
|
+
throw new Error('The DUCTAPE_PUBLISHABLE_KEY was rejected by the runtime proxy. ' +
|
|
3891
|
+
'Use a publishable key for the same workspace/product. The MCP server never accepts or forwards access keys.');
|
|
3892
|
+
}
|
|
3893
|
+
throw error;
|
|
3894
|
+
}
|
|
3897
3895
|
return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
|
|
3898
3896
|
}
|
|
3899
3897
|
catch (err) {
|
|
@@ -3914,7 +3912,8 @@ async function main() {
|
|
|
3914
3912
|
if (!key) {
|
|
3915
3913
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3916
3914
|
}
|
|
3917
|
-
const
|
|
3915
|
+
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
3916
|
+
const result = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }), args);
|
|
3918
3917
|
let text = JSON.stringify(result ?? null, null, 2);
|
|
3919
3918
|
if (args.operation_family === 'database') {
|
|
3920
3919
|
const meta = result?.meta ?? {};
|
|
@@ -3941,7 +3940,8 @@ async function main() {
|
|
|
3941
3940
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3942
3941
|
}
|
|
3943
3942
|
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
3944
|
-
const
|
|
3943
|
+
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
3944
|
+
const generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }), args);
|
|
3945
3945
|
const payload = generated?.payload ?? {};
|
|
3946
3946
|
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
3947
3947
|
return {
|
|
@@ -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,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,
|
|
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,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAiCZ"}
|
package/dist/proxy-client.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function executeViaProxy(publishable_key, module, method, params =
|
|
|
14
14
|
},
|
|
15
15
|
body: JSON.stringify({
|
|
16
16
|
publishable_key,
|
|
17
|
-
module,
|
|
17
|
+
module: module === 'features' ? 'feature' : module,
|
|
18
18
|
method,
|
|
19
19
|
params,
|
|
20
20
|
}),
|
|
@@ -73,6 +73,9 @@ export async function generateExecutablePayload(request) {
|
|
|
73
73
|
const { execution_context: _executionContext, ...backendRequest } = request;
|
|
74
74
|
const normalizedRequest = {
|
|
75
75
|
...backendRequest,
|
|
76
|
+
operation_family: backendRequest.operation_family.toLowerCase() === 'features'
|
|
77
|
+
? 'feature'
|
|
78
|
+
: backendRequest.operation_family,
|
|
76
79
|
targets: backendRequest.targets
|
|
77
80
|
? normalizeTargets(backendRequest.targets)
|
|
78
81
|
: backendRequest.targets,
|
package/package.json
CHANGED
|
@@ -6,6 +6,17 @@ import { dirname, resolve } from 'node:path';
|
|
|
6
6
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const source = readFileSync(resolve(here, '../src/index.ts'), 'utf8');
|
|
8
8
|
|
|
9
|
+
assert.doesNotMatch(
|
|
10
|
+
source,
|
|
11
|
+
/(?:const|let|var)\s+\w+\s*=\s*process\.env\.DUCTAPE_ACCESS_KEY/,
|
|
12
|
+
'MCP server must never read DUCTAPE_ACCESS_KEY',
|
|
13
|
+
);
|
|
14
|
+
assert.match(
|
|
15
|
+
source,
|
|
16
|
+
/delete environment\.DUCTAPE_ACCESS_KEY/,
|
|
17
|
+
'MCP CLI subprocess environment must strip DUCTAPE_ACCESS_KEY',
|
|
18
|
+
);
|
|
19
|
+
|
|
9
20
|
const checks = [
|
|
10
21
|
['dedicated docs topic', /'frontend-analytics': `/],
|
|
11
22
|
['anonymous analytics without an invented session', /Before login:[\s\S]*do not invent a session/i],
|
package/src/index.ts
CHANGED
|
@@ -734,47 +734,10 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
734
734
|
vector.count [{ product, env, vector, namespace? }]
|
|
735
735
|
|
|
736
736
|
━━━ MODULE: features ━━━
|
|
737
|
-
features.
|
|
738
|
-
|
|
739
|
-
name: string,
|
|
740
|
-
description?: string,
|
|
741
|
-
|
|
742
|
-
// INPUT SCHEMA (top-level): declares what fields this feature accepts when executed at runtime.
|
|
743
|
-
// These are the fields callers will pass to features.execute / features.dispatch.
|
|
744
|
-
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
745
|
-
input?: { fieldName: { type: string, required?: boolean } },
|
|
746
|
-
|
|
747
|
-
output?: object,
|
|
748
|
-
envs?: [{ slug: string, active?: boolean }],
|
|
749
|
-
steps: [
|
|
750
|
-
{
|
|
751
|
-
tag: string, // unique step id
|
|
752
|
-
name?: string,
|
|
753
|
-
type: "action"|"database"|"graph"|"notification"|"storage"|"publish"|"feature"|"sleep"|"wait_signal",
|
|
754
|
-
app?: string, // for type=action
|
|
755
|
-
event?: string, // action/event tag
|
|
756
|
-
database?: string, // for type=database
|
|
757
|
-
graph?: string, // for type=graph
|
|
758
|
-
notification?: string, // for type=notification
|
|
759
|
-
storage?: string, // for type=storage
|
|
760
|
-
broker?: string, // for type=publish
|
|
761
|
-
feature?: string, // for type=feature (child feature)
|
|
762
|
-
|
|
763
|
-
// STEP INPUT: maps this feature's declared input fields (or prior step outputs) → the step's underlying action/event fields.
|
|
764
|
-
// ← CALL ductape_generate_payload (operation_family matching step type, method="run", targets={app/event/database/etc.})
|
|
765
|
-
// to discover what fields the target accepts, then wire them with "$Input{fieldName}" or "$Step{stepTag}{field}" references.
|
|
766
|
-
input?: { "body:field": "$Input{fieldName}" | "$Step{stepTag}{field}" | literal },
|
|
767
|
-
|
|
768
|
-
condition?: string, // e.g. "$Step{validate}{valid} == true"
|
|
769
|
-
dependsOn?: string[],
|
|
770
|
-
options?: { retries?: number, timeout?: number, allow_fail?: boolean, critical?: boolean }
|
|
771
|
-
}
|
|
772
|
-
]
|
|
773
|
-
}]
|
|
774
|
-
features.update [product_tag, feature_tag, data: { name?: string, description?: string, steps?: array, envs?: array }]
|
|
737
|
+
Feature definitions are code-first. Use features.define in application source; do not call
|
|
738
|
+
administrative create/update/delete methods through ductape_execute.
|
|
775
739
|
features.fetch [product_tag, feature_tag]
|
|
776
740
|
features.fetchAll [product_tag]
|
|
777
|
-
features.delete [product_tag, feature_tag]
|
|
778
741
|
|
|
779
742
|
features.define [{
|
|
780
743
|
product?: string,
|
|
@@ -1000,8 +963,9 @@ const payloadGenerateInputSchema = z.object({
|
|
|
1000
963
|
'For storage: { storage: "storage_tag" }. ' +
|
|
1001
964
|
'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'
|
|
1002
965
|
),
|
|
1003
|
-
include_session: z.boolean().optional().
|
|
966
|
+
include_session: z.boolean().optional().describe(
|
|
1004
967
|
'Include a session placeholder inside the generated input object. ' +
|
|
968
|
+
'Defaults to false for execution_context="system" and true otherwise. ' +
|
|
1005
969
|
'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'
|
|
1006
970
|
),
|
|
1007
971
|
execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe(
|
|
@@ -1009,8 +973,8 @@ const payloadGenerateInputSchema = z.object({
|
|
|
1009
973
|
'"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
|
|
1010
974
|
'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'
|
|
1011
975
|
),
|
|
1012
|
-
include_cache: z.boolean().optional().
|
|
1013
|
-
'Include
|
|
976
|
+
include_cache: z.boolean().optional().describe(
|
|
977
|
+
'Include a cache tag inside the generated payload. Defaults to false; set true only when the runtime call should use cache explicitly.'
|
|
1014
978
|
),
|
|
1015
979
|
schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe(
|
|
1016
980
|
'"strict" — fail if any required field cannot be resolved. ' +
|
|
@@ -1119,7 +1083,7 @@ function addSessionAwarenessMetadata(
|
|
|
1119
1083
|
const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
|
|
1120
1084
|
const executionContext = args.execution_context ?? 'user';
|
|
1121
1085
|
const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
|
|
1122
|
-
const sessionRequested = args.include_session !==
|
|
1086
|
+
const sessionRequested = args.include_session ?? executionContext !== 'system';
|
|
1123
1087
|
const warnings: string[] = [];
|
|
1124
1088
|
|
|
1125
1089
|
if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
|
|
@@ -1240,7 +1204,12 @@ const ADMIN_SUBCOMMANDS = [
|
|
|
1240
1204
|
|
|
1241
1205
|
function checkCli(): { available: boolean; version?: string } {
|
|
1242
1206
|
try {
|
|
1243
|
-
const out = execSync('ductape --version', {
|
|
1207
|
+
const out = execSync('ductape --version', {
|
|
1208
|
+
encoding: 'utf8',
|
|
1209
|
+
timeout: 5000,
|
|
1210
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1211
|
+
env: cliEnvironment(),
|
|
1212
|
+
}).trim();
|
|
1244
1213
|
return { available: true, version: out || 'unknown' };
|
|
1245
1214
|
} catch {
|
|
1246
1215
|
return { available: false };
|
|
@@ -1256,6 +1225,7 @@ function checkLoginState(): 'ok' | 'none' {
|
|
|
1256
1225
|
encoding: 'utf8',
|
|
1257
1226
|
timeout: 10000,
|
|
1258
1227
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1228
|
+
env: cliEnvironment(),
|
|
1259
1229
|
});
|
|
1260
1230
|
authState = 'ok';
|
|
1261
1231
|
return 'ok';
|
|
@@ -1270,7 +1240,12 @@ function syncWorkspace(): void {
|
|
|
1270
1240
|
workspaceSynced = true; // mark done regardless so we don't retry on every call
|
|
1271
1241
|
if (!target) return;
|
|
1272
1242
|
try {
|
|
1273
|
-
execSync(`ductape workspaces use "${target}"`, {
|
|
1243
|
+
execSync(`ductape workspaces use "${target}"`, {
|
|
1244
|
+
encoding: 'utf8',
|
|
1245
|
+
timeout: 10000,
|
|
1246
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1247
|
+
env: cliEnvironment(),
|
|
1248
|
+
});
|
|
1274
1249
|
} catch {
|
|
1275
1250
|
// best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
|
|
1276
1251
|
}
|
|
@@ -1297,6 +1272,7 @@ function runCli(command: string): { success: boolean; output: string } {
|
|
|
1297
1272
|
// instead of this wrapper killing the CLI first and reducing it to "(no data)".
|
|
1298
1273
|
timeout: 90000,
|
|
1299
1274
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1275
|
+
env: cliEnvironment(),
|
|
1300
1276
|
});
|
|
1301
1277
|
return { success: true, output: output.trim() };
|
|
1302
1278
|
} catch (err: any) {
|
|
@@ -1340,6 +1316,17 @@ function runCli(command: string): { success: boolean; output: string } {
|
|
|
1340
1316
|
}
|
|
1341
1317
|
}
|
|
1342
1318
|
|
|
1319
|
+
/**
|
|
1320
|
+
* MCP is publishable-key-only. The standalone CLI may manage its own access-key
|
|
1321
|
+
* credentials, but an access key present in the MCP host environment is never
|
|
1322
|
+
* forwarded into CLI subprocesses.
|
|
1323
|
+
*/
|
|
1324
|
+
function cliEnvironment(): NodeJS.ProcessEnv {
|
|
1325
|
+
const environment = { ...process.env };
|
|
1326
|
+
delete environment.DUCTAPE_ACCESS_KEY;
|
|
1327
|
+
return environment;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1343
1330
|
const docsInputSchema = z.object({
|
|
1344
1331
|
topic: z.string().describe(
|
|
1345
1332
|
'Feature topic to look up. Supported: ' +
|
|
@@ -3985,7 +3972,10 @@ async function main() {
|
|
|
3985
3972
|
}
|
|
3986
3973
|
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
3987
3974
|
if (!key) {
|
|
3988
|
-
throw new Error(
|
|
3975
|
+
throw new Error(
|
|
3976
|
+
'Runtime authentication is missing. Set DUCTAPE_PUBLISHABLE_KEY in the MCP server environment ' +
|
|
3977
|
+
'or pass publishable_key to ductape_execute. The MCP server never accepts access keys.',
|
|
3978
|
+
);
|
|
3989
3979
|
}
|
|
3990
3980
|
// The TS SDK uses ductape.events.* for broker operations; the backend proxy uses messageBrokers.
|
|
3991
3981
|
const proxyModule: SDKModule = args.module === 'events' ? 'messageBrokers' : args.module;
|
|
@@ -4010,7 +4000,19 @@ async function main() {
|
|
|
4010
4000
|
});
|
|
4011
4001
|
}
|
|
4012
4002
|
|
|
4013
|
-
|
|
4003
|
+
let result: unknown;
|
|
4004
|
+
try {
|
|
4005
|
+
result = await executeViaProxy(key, proxyModule, args.method, params);
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4008
|
+
if (/authentication failed|unauthorized|invalid.*key/i.test(message)) {
|
|
4009
|
+
throw new Error(
|
|
4010
|
+
'The DUCTAPE_PUBLISHABLE_KEY was rejected by the runtime proxy. ' +
|
|
4011
|
+
'Use a publishable key for the same workspace/product. The MCP server never accepts or forwards access keys.',
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
throw error;
|
|
4015
|
+
}
|
|
4014
4016
|
return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
|
|
4015
4017
|
} catch (err) {
|
|
4016
4018
|
let message = err instanceof Error ? err.message : String(err);
|
|
@@ -4033,8 +4035,9 @@ async function main() {
|
|
|
4033
4035
|
if (!key) {
|
|
4034
4036
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
4035
4037
|
}
|
|
4038
|
+
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
4036
4039
|
const result = addSessionAwarenessMetadata(
|
|
4037
|
-
await generateExecutablePayload({ ...args, publishable_key: key }),
|
|
4040
|
+
await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }),
|
|
4038
4041
|
args,
|
|
4039
4042
|
);
|
|
4040
4043
|
|
|
@@ -4068,8 +4071,9 @@ async function main() {
|
|
|
4068
4071
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
4069
4072
|
}
|
|
4070
4073
|
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
4074
|
+
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
4071
4075
|
const generated = addSessionAwarenessMetadata(
|
|
4072
|
-
await generateExecutablePayload({ ...args, publishable_key: key }),
|
|
4076
|
+
await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }),
|
|
4073
4077
|
args,
|
|
4074
4078
|
);
|
|
4075
4079
|
const payload = (generated as any)?.payload ?? {};
|
package/src/proxy-client.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function executeViaProxy<T = unknown>(
|
|
|
52
52
|
},
|
|
53
53
|
body: JSON.stringify({
|
|
54
54
|
publishable_key,
|
|
55
|
-
module,
|
|
55
|
+
module: module === 'features' ? 'feature' : module,
|
|
56
56
|
method,
|
|
57
57
|
params,
|
|
58
58
|
}),
|
|
@@ -143,6 +143,9 @@ export async function generateExecutablePayload<T = IGenerateExecutablePayloadRe
|
|
|
143
143
|
const { execution_context: _executionContext, ...backendRequest } = request;
|
|
144
144
|
const normalizedRequest = {
|
|
145
145
|
...backendRequest,
|
|
146
|
+
operation_family: backendRequest.operation_family.toLowerCase() === 'features'
|
|
147
|
+
? 'feature'
|
|
148
|
+
: backendRequest.operation_family,
|
|
146
149
|
targets: backendRequest.targets
|
|
147
150
|
? normalizeTargets(backendRequest.targets as Record<string, unknown>)
|
|
148
151
|
: backendRequest.targets,
|