@ductape/mcp 0.1.60 → 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
@@ -946,7 +946,7 @@ const payloadGenerateInputSchema = z.object({
946
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; ' +
947
947
  '"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
948
948
  'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'),
949
- 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.'),
950
950
  schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe('"strict" — fail if any required field cannot be resolved. ' +
951
951
  '"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'),
952
952
  input_hint: z.record(z.any()).optional().describe('Optional. Partial input values you already know. These are merged into the generated payload template ' +
@@ -1142,7 +1142,12 @@ const ADMIN_SUBCOMMANDS = [
1142
1142
  ];
1143
1143
  function checkCli() {
1144
1144
  try {
1145
- 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();
1146
1151
  return { available: true, version: out || 'unknown' };
1147
1152
  }
1148
1153
  catch {
@@ -1158,6 +1163,7 @@ function checkLoginState() {
1158
1163
  encoding: 'utf8',
1159
1164
  timeout: 10000,
1160
1165
  stdio: ['pipe', 'pipe', 'pipe'],
1166
+ env: cliEnvironment(),
1161
1167
  });
1162
1168
  authState = 'ok';
1163
1169
  return 'ok';
@@ -1173,7 +1179,12 @@ function syncWorkspace() {
1173
1179
  if (!target)
1174
1180
  return;
1175
1181
  try {
1176
- 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
+ });
1177
1188
  }
1178
1189
  catch {
1179
1190
  // best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
@@ -1200,6 +1211,7 @@ function runCli(command) {
1200
1211
  // instead of this wrapper killing the CLI first and reducing it to "(no data)".
1201
1212
  timeout: 90000,
1202
1213
  stdio: ['pipe', 'pipe', 'pipe'],
1214
+ env: cliEnvironment(),
1203
1215
  });
1204
1216
  return { success: true, output: output.trim() };
1205
1217
  }
@@ -1243,6 +1255,16 @@ function runCli(command) {
1243
1255
  return { success: false, output: msg };
1244
1256
  }
1245
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
+ }
1246
1268
  const docsInputSchema = z.object({
1247
1269
  topic: z.string().describe('Feature topic to look up. Supported: ' +
1248
1270
  'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
@@ -3834,7 +3856,8 @@ async function main() {
3834
3856
  }
3835
3857
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3836
3858
  if (!key) {
3837
- 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.');
3838
3861
  }
3839
3862
  // The TS SDK uses ductape.events.* for broker operations; the backend proxy uses messageBrokers.
3840
3863
  const proxyModule = args.module === 'events' ? 'messageBrokers' : args.module;
@@ -3857,7 +3880,18 @@ async function main() {
3857
3880
  return p;
3858
3881
  });
3859
3882
  }
3860
- 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
+ }
3861
3895
  return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
3862
3896
  }
3863
3897
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.60",
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
@@ -973,8 +973,8 @@ const payloadGenerateInputSchema = z.object({
973
973
  '"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
974
974
  'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'
975
975
  ),
976
- include_cache: z.boolean().optional().default(true).describe(
977
- '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.'
978
978
  ),
979
979
  schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe(
980
980
  '"strict" — fail if any required field cannot be resolved. ' +
@@ -1204,7 +1204,12 @@ const ADMIN_SUBCOMMANDS = [
1204
1204
 
1205
1205
  function checkCli(): { available: boolean; version?: string } {
1206
1206
  try {
1207
- 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();
1208
1213
  return { available: true, version: out || 'unknown' };
1209
1214
  } catch {
1210
1215
  return { available: false };
@@ -1220,6 +1225,7 @@ function checkLoginState(): 'ok' | 'none' {
1220
1225
  encoding: 'utf8',
1221
1226
  timeout: 10000,
1222
1227
  stdio: ['pipe', 'pipe', 'pipe'],
1228
+ env: cliEnvironment(),
1223
1229
  });
1224
1230
  authState = 'ok';
1225
1231
  return 'ok';
@@ -1234,7 +1240,12 @@ function syncWorkspace(): void {
1234
1240
  workspaceSynced = true; // mark done regardless so we don't retry on every call
1235
1241
  if (!target) return;
1236
1242
  try {
1237
- 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
+ });
1238
1249
  } catch {
1239
1250
  // best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
1240
1251
  }
@@ -1261,6 +1272,7 @@ function runCli(command: string): { success: boolean; output: string } {
1261
1272
  // instead of this wrapper killing the CLI first and reducing it to "(no data)".
1262
1273
  timeout: 90000,
1263
1274
  stdio: ['pipe', 'pipe', 'pipe'],
1275
+ env: cliEnvironment(),
1264
1276
  });
1265
1277
  return { success: true, output: output.trim() };
1266
1278
  } catch (err: any) {
@@ -1304,6 +1316,17 @@ function runCli(command: string): { success: boolean; output: string } {
1304
1316
  }
1305
1317
  }
1306
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
+
1307
1330
  const docsInputSchema = z.object({
1308
1331
  topic: z.string().describe(
1309
1332
  'Feature topic to look up. Supported: ' +
@@ -3949,7 +3972,10 @@ async function main() {
3949
3972
  }
3950
3973
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3951
3974
  if (!key) {
3952
- 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
+ );
3953
3979
  }
3954
3980
  // The TS SDK uses ductape.events.* for broker operations; the backend proxy uses messageBrokers.
3955
3981
  const proxyModule: SDKModule = args.module === 'events' ? 'messageBrokers' : args.module;
@@ -3974,7 +4000,19 @@ async function main() {
3974
4000
  });
3975
4001
  }
3976
4002
 
3977
- 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
+ }
3978
4016
  return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
3979
4017
  } catch (err) {
3980
4018
  let message = err instanceof Error ? err.message : String(err);