@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 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.create [product_tag, data: {
727
- tag: string,
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().default(true).describe('Include a session placeholder inside the generated input object. ' +
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().default(true).describe('Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'),
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 !== false;
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', { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
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}"`, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
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('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
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
- const result = await executeViaProxy(key, proxyModule, args.method, params);
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 result = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
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 generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
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,CA8BZ"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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.create [product_tag, data: {
738
- tag: string,
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().default(true).describe(
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().default(true).describe(
1013
- 'Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'
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 !== false;
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', { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
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}"`, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
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('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
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
- const result = await executeViaProxy(key, proxyModule, args.method, params);
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 ?? {};
@@ -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,