@reproapp/mcp 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +184 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/mcp",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Repro MCP server for AI-assisted debugging and privacy policy workflows",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/src/index.js CHANGED
@@ -16,7 +16,6 @@ const SERVER_INSTRUCTIONS = [
16
16
  'For Repro-scoped questions about sessions, requests, traces, privacy policies, apps, or configuration, use the Repro MCP tools only.',
17
17
  'Do not create local scripts, read cached Repro auth files, or call Repro HTTP APIs directly for Repro data or policy workflows.',
18
18
  'For privacy policy writes, inspect first and then use exact statementSid(s) or blockerSetId. Broad natural-language policy update queries are read-only.',
19
- 'When a Repro tool returns valuePreview/valueLink for a large payload, return that preview and link to the user and stop there. Do not automatically follow the link or re-query for the full value unless the user explicitly asks for the full payload or raw value. Exact large-value expansion requires expandLargeValue=true and explicit user intent.',
20
19
  ].join(' ');
21
20
  const DEFAULT_REPRO_AI_BASE_URL = 'https://repro-ai-u8lhw.ondigitalocean.app';
22
21
  const DEFAULT_REPRO_API_BASE_URL = 'https://repro-api-d7288.ondigitalocean.app/api';
@@ -1193,9 +1192,10 @@ async function executeTool(params, config, client, getTools) {
1193
1192
  throw rpcError(-32602, `Unknown tool: ${name}`);
1194
1193
  }
1195
1194
 
1196
- const response = await client.executeTool(name, input);
1195
+ const response = await client.executeTool(name, inputWithExactCapturedPayloads(name, input));
1196
+ const expandedResponse = await expandLinkedCapturedPayloads(name, response, client, input);
1197
1197
 
1198
- const augmented = augmentPaths(response, config.pathMappings);
1198
+ const augmented = augmentPaths(expandedResponse, config.pathMappings);
1199
1199
  const isError =
1200
1200
  augmented &&
1201
1201
  typeof augmented === 'object' &&
@@ -1214,58 +1214,202 @@ async function executeTool(params, config, client, getTools) {
1214
1214
  };
1215
1215
  }
1216
1216
 
1217
- function formatLocalToolContentText(name, value) {
1218
- if (name === 'inspect_privacy_policy' || name === 'list_privacy_policy_blockers') {
1219
- const text = renderPolicyInspectionText(value);
1220
- if (text) return text;
1217
+ function inputWithExactCapturedPayloads(name, input) {
1218
+ if (name !== 'get_kafka_messages') {
1219
+ return input;
1221
1220
  }
1222
- if (name === 'check_privacy_coverage') {
1223
- const text = renderPrivacyCoverageText(value);
1224
- if (text) return text;
1221
+ return {
1222
+ ...input,
1223
+ includeLargeValues: true,
1224
+ expandLargeValue: true,
1225
+ };
1226
+ }
1227
+
1228
+ async function expandLinkedCapturedPayloads(name, response, client, originalInput = {}) {
1229
+ if (name !== 'get_kafka_messages') {
1230
+ return response;
1225
1231
  }
1226
- if (name === 'get_kafka_messages') {
1227
- const text = renderKafkaMessagesText(value);
1228
- if (text) return text;
1232
+
1233
+ const root = cloneJson(response) ?? response;
1234
+ const visited = new Set();
1235
+
1236
+ while (true) {
1237
+ const links = collectCapturedPayloadLinks(root)
1238
+ .filter((entry) => !visited.has(capturedPayloadLinkKey(entry.link)));
1239
+ if (!links.length) {
1240
+ return root;
1241
+ }
1242
+
1243
+ for (const { link } of links) {
1244
+ const key = capturedPayloadLinkKey(link);
1245
+ visited.add(key);
1246
+
1247
+ const toolName = typeof link.tool === 'string' ? link.tool : '';
1248
+ if (toolName !== 'get_kafka_messages') {
1249
+ throw new Error(`Cannot expand captured payload through unsupported MCP tool: ${toolName || 'unknown'}`);
1250
+ }
1251
+
1252
+ const linkArgs = link.arguments && typeof link.arguments === 'object' && !Array.isArray(link.arguments)
1253
+ ? link.arguments
1254
+ : {};
1255
+ const scopedLinkArgs = {
1256
+ ...originalInput,
1257
+ ...linkArgs,
1258
+ };
1259
+ const expanded = await client.executeTool(toolName, inputWithExactCapturedPayloads(toolName, scopedLinkArgs));
1260
+ const expandedLookup = lookupLinkedPayloadValue(expanded, link.path);
1261
+ if (!expandedLookup.found) {
1262
+ throw new Error(`Repro MCP could not expand captured payload at ${link.path || 'unknown path'}`);
1263
+ }
1264
+
1265
+ const applied = replaceLinkedPayloadValue(root, link.path, expandedLookup.value);
1266
+ if (!applied) {
1267
+ throw new Error(`Repro MCP could not replace captured payload at ${link.path || 'unknown path'}`);
1268
+ }
1269
+ }
1229
1270
  }
1230
- return JSON.stringify(value, null, 2);
1231
1271
  }
1232
1272
 
1233
- function renderKafkaMessagesText(value) {
1273
+ function collectCapturedPayloadLinks(value, out = []) {
1234
1274
  if (!value || typeof value !== 'object') {
1235
- return null;
1275
+ return out;
1236
1276
  }
1237
- const data = value.data && typeof value.data === 'object' ? value.data : null;
1238
- const items = Array.isArray(data?.items) ? data.items : [];
1239
- if (!items.length) {
1240
- return null;
1277
+ if (Array.isArray(value)) {
1278
+ for (const item of value) {
1279
+ collectCapturedPayloadLinks(item, out);
1280
+ }
1281
+ return out;
1241
1282
  }
1242
1283
 
1243
- const lines = [`Found ${items.length} Kafka message${items.length === 1 ? '' : 's'}.`];
1244
- let omittedCount = 0;
1284
+ const link = value.valueOmitted === true ? value.valueLink : value.rawValueLink;
1285
+ if (
1286
+ (value.valueOmitted === true || value.rawValueOmitted === true) &&
1287
+ link &&
1288
+ typeof link === 'object' &&
1289
+ link.kind === 'mcp-tool-call' &&
1290
+ typeof link.tool === 'string' &&
1291
+ typeof link.path === 'string'
1292
+ ) {
1293
+ out.push({ holder: value, link });
1294
+ }
1245
1295
 
1246
- for (const item of items.slice(0, 3)) {
1247
- const request = item?.request && typeof item.request === 'object' ? item.request : {};
1248
- const message = item?.message && typeof item.message === 'object' ? item.message : {};
1249
- lines.push(
1250
- [
1251
- typeof request.rid === 'string' ? `RID ${request.rid}` : null,
1252
- typeof message.topic === 'string' ? `topic ${message.topic}` : null,
1253
- typeof message.offset !== 'undefined' ? `offset ${message.offset}` : null,
1254
- typeof message.key !== 'undefined' ? `key ${message.key}` : null,
1255
- ].filter(Boolean).join(' | '),
1256
- );
1257
- if (message.valueOmitted === true || message.rawValueOmitted === true) {
1258
- omittedCount += 1;
1296
+ for (const nested of Object.values(value)) {
1297
+ collectCapturedPayloadLinks(nested, out);
1298
+ }
1299
+ return out;
1300
+ }
1301
+
1302
+ function capturedPayloadLinkKey(link) {
1303
+ return JSON.stringify({
1304
+ tool: link?.tool ?? null,
1305
+ path: link?.path ?? null,
1306
+ arguments: link?.arguments ?? null,
1307
+ });
1308
+ }
1309
+
1310
+ function lookupLinkedPayloadValue(root, pathExpression) {
1311
+ const segments = parseLinkedPayloadPath(pathExpression);
1312
+ if (!segments.length) {
1313
+ return { found: false };
1314
+ }
1315
+ return lookupPathCandidates(root, segments);
1316
+ }
1317
+
1318
+ function replaceLinkedPayloadValue(root, pathExpression, value) {
1319
+ const segments = parseLinkedPayloadPath(pathExpression);
1320
+ if (!segments.length) {
1321
+ return false;
1322
+ }
1323
+ const candidates = pathCandidateSegments(segments);
1324
+ for (const candidate of candidates) {
1325
+ const parentLookup = getAtPath(root, candidate.slice(0, -1));
1326
+ if (!parentLookup.found || !parentLookup.value || typeof parentLookup.value !== 'object') {
1327
+ continue;
1328
+ }
1329
+ const leaf = candidate[candidate.length - 1];
1330
+ parentLookup.value[leaf] = value;
1331
+ if (leaf === 'value') {
1332
+ delete parentLookup.value.valueOmitted;
1333
+ delete parentLookup.value.valuePreview;
1334
+ delete parentLookup.value.valueLink;
1335
+ } else if (leaf === 'rawValue') {
1336
+ delete parentLookup.value.rawValueOmitted;
1337
+ delete parentLookup.value.rawValuePreview;
1338
+ delete parentLookup.value.rawValueLink;
1259
1339
  }
1340
+ return true;
1260
1341
  }
1342
+ return false;
1343
+ }
1261
1344
 
1262
- if (omittedCount > 0) {
1263
- lines.push(
1264
- 'Large Kafka payload preview returned. Do not fetch the full value automatically. Only follow valueLink if the user explicitly asks for the full payload or raw value. Exact expansion requires expandLargeValue=true.'
1265
- );
1345
+ function lookupPathCandidates(root, segments) {
1346
+ for (const candidate of pathCandidateSegments(segments)) {
1347
+ const lookup = getAtPath(root, candidate);
1348
+ if (lookup.found) {
1349
+ return lookup;
1350
+ }
1266
1351
  }
1352
+ return { found: false };
1353
+ }
1354
+
1355
+ function pathCandidateSegments(segments) {
1356
+ return [
1357
+ ['data', ...segments],
1358
+ segments,
1359
+ ];
1360
+ }
1267
1361
 
1268
- return lines.filter(Boolean).join('\n');
1362
+ function parseLinkedPayloadPath(pathExpression) {
1363
+ if (typeof pathExpression !== 'string' || !pathExpression.trim()) {
1364
+ return [];
1365
+ }
1366
+ const segments = [];
1367
+ const pattern = /([A-Za-z_$][\w$-]*)|\[(\d+)\]/g;
1368
+ let match;
1369
+ while ((match = pattern.exec(pathExpression)) !== null) {
1370
+ if (typeof match[1] === 'string') {
1371
+ segments.push(match[1]);
1372
+ } else if (typeof match[2] === 'string') {
1373
+ segments.push(Number.parseInt(match[2], 10));
1374
+ }
1375
+ }
1376
+ return segments;
1377
+ }
1378
+
1379
+ function getAtPath(root, segments) {
1380
+ let current = root;
1381
+ for (const segment of segments) {
1382
+ if (current == null) {
1383
+ return { found: false };
1384
+ }
1385
+ if (typeof segment === 'number') {
1386
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
1387
+ return { found: false };
1388
+ }
1389
+ current = current[segment];
1390
+ continue;
1391
+ }
1392
+ if (typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
1393
+ return { found: false };
1394
+ }
1395
+ current = current[segment];
1396
+ }
1397
+ return { found: true, value: current };
1398
+ }
1399
+
1400
+ function formatLocalToolContentText(name, value) {
1401
+ if (name === 'inspect_privacy_policy' || name === 'list_privacy_policy_blockers') {
1402
+ const text = renderPolicyInspectionText(value);
1403
+ if (text) return text;
1404
+ }
1405
+ if (name === 'check_privacy_coverage') {
1406
+ const text = renderPrivacyCoverageText(value);
1407
+ if (text) return text;
1408
+ }
1409
+ if (name === 'get_kafka_messages') {
1410
+ return JSON.stringify(value, null, 2);
1411
+ }
1412
+ return JSON.stringify(value, null, 2);
1269
1413
  }
1270
1414
 
1271
1415
  function mergeLocalTools(remoteTools) {