@reproapp/mcp 0.0.7 → 0.0.9

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 +221 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/mcp",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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,8 @@ 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.',
19
+ 'For factual debugging questions, use exact evidence tools first and answer from their structured output. Use investigate only after exact evidence is gathered, or when the user explicitly asks for synthesis/root cause.',
20
+ 'Never answer that captured client data is absent because code appears to omit it. Verify with request, DB, trace, function-call, and message payload tools, then say exactly what Repro captured or did not capture.',
20
21
  ].join(' ');
21
22
  const DEFAULT_REPRO_AI_BASE_URL = 'https://repro-ai-u8lhw.ondigitalocean.app';
22
23
  const DEFAULT_REPRO_API_BASE_URL = 'https://repro-api-d7288.ondigitalocean.app/api';
@@ -194,6 +195,31 @@ const LOCAL_TOOL_DEFINITIONS = [
194
195
  },
195
196
  ];
196
197
 
198
+ const TOOL_DESCRIPTION_OVERRIDES = {
199
+ investigate:
200
+ 'Synthesize a root-cause hypothesis from Repro evidence after exact telemetry tools have been checked. Do not use this as the first or only tool for factual inventory questions about requests, DB changes, traces, function args/returns, emails, Kafka/RabbitMQ/messages, or payload contents; call the exact evidence tool first and cite that output.',
201
+ list_requests:
202
+ 'Exact evidence tool: list captured HTTP and backend transport requests in scope. Use this before investigate whenever the user asks what request happened, which endpoint ran, request timing/status, or which request RID anchors later DB/trace/message evidence.',
203
+ get_request:
204
+ 'Exact evidence tool: fetch one captured request by RID, including request/response bodies and captured value refs when present. Use this before answering factual questions about a specific request payload, response, endpoint, or transport message wrapper.',
205
+ get_requests_relative_to_anchor:
206
+ 'Exact evidence tool: list captured requests before or after an action/request/DB anchor. Use for ordering questions and localizing the request sequence around a UI action or backend event.',
207
+ get_db_changes:
208
+ 'Exact evidence tool: fetch captured DB/Redis changes. Use this before answering what collections, documents, fields, queries, mutations, or result metadata changed. Set includeQueryDetails when the user needs query/update/body details.',
209
+ get_trace:
210
+ 'Exact evidence tool: fetch captured trace batches for a request RID. Use this before answering which functions ran, trace timing, thrown errors, or trace event details for a request.',
211
+ get_action_path:
212
+ 'Exact evidence tool: fetch the correlated action path with requests, DB changes, and emails/messages when available. Use this to connect a UI action to backend evidence before giving a narrative answer.',
213
+ get_function_candidates:
214
+ 'Exact evidence tool: find likely captured functions for an action/request/function search. Use this before get_function_calls when the exact function anchor is unclear.',
215
+ get_function_calls:
216
+ 'Exact evidence tool: fetch concrete captured function calls, arguments, return values, errors, and value refs. Use this before answering what function handled something or what args/returns were captured.',
217
+ get_kafka_messages:
218
+ 'Exact evidence tool: fetch captured Kafka publish/consume payloads. Use this for messaging questions and payload contents; for exact single-message lookups, the MCP server expands captured large values automatically.',
219
+ get_captured_value:
220
+ 'Exact evidence tool: fetch the full masked value behind a captured valueId. Use this whenever another tool reports a captured value reference or large_value_available before claiming the payload is unavailable.',
221
+ };
222
+
197
223
  function loadConfig(env = process.env) {
198
224
  const baseUrl = normalizeBaseUrl(env.REPRO_AI_BASE_URL || DEFAULT_REPRO_AI_BASE_URL);
199
225
  const apiBaseUrl = normalizeOptionalBaseUrl(env.REPRO_API_BASE_URL, DEFAULT_REPRO_API_BASE_URL);
@@ -1142,11 +1168,20 @@ async function handleRpcMessage(message, config, client, getTools) {
1142
1168
  function publicToolDefinition(tool) {
1143
1169
  return {
1144
1170
  name: tool.name,
1145
- description: tool.description,
1171
+ description: publicToolDescription(tool),
1146
1172
  inputSchema: tool.inputSchema,
1147
1173
  };
1148
1174
  }
1149
1175
 
1176
+ function publicToolDescription(tool) {
1177
+ const base = typeof tool?.description === 'string' ? tool.description.trim() : '';
1178
+ const override = TOOL_DESCRIPTION_OVERRIDES[tool?.name];
1179
+ if (!override) {
1180
+ return base;
1181
+ }
1182
+ return base ? `${override} ${base}` : override;
1183
+ }
1184
+
1150
1185
  function normalizeToolCatalog(catalog) {
1151
1186
  if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.tools)) {
1152
1187
  throw new Error('repro-ai returned an invalid tool catalog');
@@ -1193,9 +1228,10 @@ async function executeTool(params, config, client, getTools) {
1193
1228
  throw rpcError(-32602, `Unknown tool: ${name}`);
1194
1229
  }
1195
1230
 
1196
- const response = await client.executeTool(name, input);
1231
+ const response = await client.executeTool(name, inputWithExactCapturedPayloads(name, input));
1232
+ const expandedResponse = await expandLinkedCapturedPayloads(name, response, client, input);
1197
1233
 
1198
- const augmented = augmentPaths(response, config.pathMappings);
1234
+ const augmented = augmentPaths(expandedResponse, config.pathMappings);
1199
1235
  const isError =
1200
1236
  augmented &&
1201
1237
  typeof augmented === 'object' &&
@@ -1214,58 +1250,202 @@ async function executeTool(params, config, client, getTools) {
1214
1250
  };
1215
1251
  }
1216
1252
 
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;
1253
+ function inputWithExactCapturedPayloads(name, input) {
1254
+ if (name !== 'get_kafka_messages') {
1255
+ return input;
1221
1256
  }
1222
- if (name === 'check_privacy_coverage') {
1223
- const text = renderPrivacyCoverageText(value);
1224
- if (text) return text;
1257
+ return {
1258
+ ...input,
1259
+ includeLargeValues: true,
1260
+ expandLargeValue: true,
1261
+ };
1262
+ }
1263
+
1264
+ async function expandLinkedCapturedPayloads(name, response, client, originalInput = {}) {
1265
+ if (name !== 'get_kafka_messages') {
1266
+ return response;
1225
1267
  }
1226
- if (name === 'get_kafka_messages') {
1227
- const text = renderKafkaMessagesText(value);
1228
- if (text) return text;
1268
+
1269
+ const root = cloneJson(response) ?? response;
1270
+ const visited = new Set();
1271
+
1272
+ while (true) {
1273
+ const links = collectCapturedPayloadLinks(root)
1274
+ .filter((entry) => !visited.has(capturedPayloadLinkKey(entry.link)));
1275
+ if (!links.length) {
1276
+ return root;
1277
+ }
1278
+
1279
+ for (const { link } of links) {
1280
+ const key = capturedPayloadLinkKey(link);
1281
+ visited.add(key);
1282
+
1283
+ const toolName = typeof link.tool === 'string' ? link.tool : '';
1284
+ if (toolName !== 'get_kafka_messages') {
1285
+ throw new Error(`Cannot expand captured payload through unsupported MCP tool: ${toolName || 'unknown'}`);
1286
+ }
1287
+
1288
+ const linkArgs = link.arguments && typeof link.arguments === 'object' && !Array.isArray(link.arguments)
1289
+ ? link.arguments
1290
+ : {};
1291
+ const scopedLinkArgs = {
1292
+ ...originalInput,
1293
+ ...linkArgs,
1294
+ };
1295
+ const expanded = await client.executeTool(toolName, inputWithExactCapturedPayloads(toolName, scopedLinkArgs));
1296
+ const expandedLookup = lookupLinkedPayloadValue(expanded, link.path);
1297
+ if (!expandedLookup.found) {
1298
+ throw new Error(`Repro MCP could not expand captured payload at ${link.path || 'unknown path'}`);
1299
+ }
1300
+
1301
+ const applied = replaceLinkedPayloadValue(root, link.path, expandedLookup.value);
1302
+ if (!applied) {
1303
+ throw new Error(`Repro MCP could not replace captured payload at ${link.path || 'unknown path'}`);
1304
+ }
1305
+ }
1229
1306
  }
1230
- return JSON.stringify(value, null, 2);
1231
1307
  }
1232
1308
 
1233
- function renderKafkaMessagesText(value) {
1309
+ function collectCapturedPayloadLinks(value, out = []) {
1234
1310
  if (!value || typeof value !== 'object') {
1235
- return null;
1311
+ return out;
1236
1312
  }
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;
1313
+ if (Array.isArray(value)) {
1314
+ for (const item of value) {
1315
+ collectCapturedPayloadLinks(item, out);
1316
+ }
1317
+ return out;
1241
1318
  }
1242
1319
 
1243
- const lines = [`Found ${items.length} Kafka message${items.length === 1 ? '' : 's'}.`];
1244
- let omittedCount = 0;
1320
+ const link = value.valueOmitted === true ? value.valueLink : value.rawValueLink;
1321
+ if (
1322
+ (value.valueOmitted === true || value.rawValueOmitted === true) &&
1323
+ link &&
1324
+ typeof link === 'object' &&
1325
+ link.kind === 'mcp-tool-call' &&
1326
+ typeof link.tool === 'string' &&
1327
+ typeof link.path === 'string'
1328
+ ) {
1329
+ out.push({ holder: value, link });
1330
+ }
1245
1331
 
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;
1332
+ for (const nested of Object.values(value)) {
1333
+ collectCapturedPayloadLinks(nested, out);
1334
+ }
1335
+ return out;
1336
+ }
1337
+
1338
+ function capturedPayloadLinkKey(link) {
1339
+ return JSON.stringify({
1340
+ tool: link?.tool ?? null,
1341
+ path: link?.path ?? null,
1342
+ arguments: link?.arguments ?? null,
1343
+ });
1344
+ }
1345
+
1346
+ function lookupLinkedPayloadValue(root, pathExpression) {
1347
+ const segments = parseLinkedPayloadPath(pathExpression);
1348
+ if (!segments.length) {
1349
+ return { found: false };
1350
+ }
1351
+ return lookupPathCandidates(root, segments);
1352
+ }
1353
+
1354
+ function replaceLinkedPayloadValue(root, pathExpression, value) {
1355
+ const segments = parseLinkedPayloadPath(pathExpression);
1356
+ if (!segments.length) {
1357
+ return false;
1358
+ }
1359
+ const candidates = pathCandidateSegments(segments);
1360
+ for (const candidate of candidates) {
1361
+ const parentLookup = getAtPath(root, candidate.slice(0, -1));
1362
+ if (!parentLookup.found || !parentLookup.value || typeof parentLookup.value !== 'object') {
1363
+ continue;
1259
1364
  }
1365
+ const leaf = candidate[candidate.length - 1];
1366
+ parentLookup.value[leaf] = value;
1367
+ if (leaf === 'value') {
1368
+ delete parentLookup.value.valueOmitted;
1369
+ delete parentLookup.value.valuePreview;
1370
+ delete parentLookup.value.valueLink;
1371
+ } else if (leaf === 'rawValue') {
1372
+ delete parentLookup.value.rawValueOmitted;
1373
+ delete parentLookup.value.rawValuePreview;
1374
+ delete parentLookup.value.rawValueLink;
1375
+ }
1376
+ return true;
1260
1377
  }
1378
+ return false;
1379
+ }
1261
1380
 
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
- );
1381
+ function lookupPathCandidates(root, segments) {
1382
+ for (const candidate of pathCandidateSegments(segments)) {
1383
+ const lookup = getAtPath(root, candidate);
1384
+ if (lookup.found) {
1385
+ return lookup;
1386
+ }
1266
1387
  }
1388
+ return { found: false };
1389
+ }
1390
+
1391
+ function pathCandidateSegments(segments) {
1392
+ return [
1393
+ ['data', ...segments],
1394
+ segments,
1395
+ ];
1396
+ }
1267
1397
 
1268
- return lines.filter(Boolean).join('\n');
1398
+ function parseLinkedPayloadPath(pathExpression) {
1399
+ if (typeof pathExpression !== 'string' || !pathExpression.trim()) {
1400
+ return [];
1401
+ }
1402
+ const segments = [];
1403
+ const pattern = /([A-Za-z_$][\w$-]*)|\[(\d+)\]/g;
1404
+ let match;
1405
+ while ((match = pattern.exec(pathExpression)) !== null) {
1406
+ if (typeof match[1] === 'string') {
1407
+ segments.push(match[1]);
1408
+ } else if (typeof match[2] === 'string') {
1409
+ segments.push(Number.parseInt(match[2], 10));
1410
+ }
1411
+ }
1412
+ return segments;
1413
+ }
1414
+
1415
+ function getAtPath(root, segments) {
1416
+ let current = root;
1417
+ for (const segment of segments) {
1418
+ if (current == null) {
1419
+ return { found: false };
1420
+ }
1421
+ if (typeof segment === 'number') {
1422
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
1423
+ return { found: false };
1424
+ }
1425
+ current = current[segment];
1426
+ continue;
1427
+ }
1428
+ if (typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, segment)) {
1429
+ return { found: false };
1430
+ }
1431
+ current = current[segment];
1432
+ }
1433
+ return { found: true, value: current };
1434
+ }
1435
+
1436
+ function formatLocalToolContentText(name, value) {
1437
+ if (name === 'inspect_privacy_policy' || name === 'list_privacy_policy_blockers') {
1438
+ const text = renderPolicyInspectionText(value);
1439
+ if (text) return text;
1440
+ }
1441
+ if (name === 'check_privacy_coverage') {
1442
+ const text = renderPrivacyCoverageText(value);
1443
+ if (text) return text;
1444
+ }
1445
+ if (name === 'get_kafka_messages') {
1446
+ return JSON.stringify(value, null, 2);
1447
+ }
1448
+ return JSON.stringify(value, null, 2);
1269
1449
  }
1270
1450
 
1271
1451
  function mergeLocalTools(remoteTools) {