@recapt/mcp 0.0.43 → 0.0.44

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
@@ -8,6 +8,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  // ../../libraries/mcp-tools/dist/client.js
9
9
  var DEFAULT_API_URL = "https://api.recapt.app";
10
10
  var REQUEST_TIMEOUT_MS = 12e4;
11
+ var MAX_RETRIES = 3;
12
+ var RETRY_BASE_MS = 500;
13
+ function isTransientError(err) {
14
+ if (!(err instanceof Error))
15
+ return false;
16
+ const msg = err.message.toLowerCase();
17
+ return msg === "fetch failed" || msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("socket hang up") || msg.includes("network") || err.name === "AbortError";
18
+ }
11
19
  var _config = {};
12
20
  function getApiUrl() {
13
21
  return _config.apiUrl || process.env.RECAPT_API_URL || process.env.MCP_API_URL || process.env.EXTERNAL_API_URL || DEFAULT_API_URL;
@@ -50,8 +58,6 @@ async function request(options) {
50
58
  requestBody = { ...requestBody, domain: domainFilter.join(",") };
51
59
  }
52
60
  }
53
- const controller = new AbortController();
54
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
55
61
  const headers = {
56
62
  "x-private-key": secretKey,
57
63
  "Content-Type": "application/json"
@@ -60,27 +66,40 @@ async function request(options) {
60
66
  if (orgId) {
61
67
  headers["x-organization-id"] = orgId;
62
68
  }
63
- try {
64
- const res = await fetch(url.toString(), {
65
- method,
66
- headers,
67
- body: requestBody ? JSON.stringify(requestBody) : void 0,
68
- signal: controller.signal
69
- });
70
- clearTimeout(timeout);
71
- if (!res.ok) {
72
- const errorBody = await res.json().catch(() => ({}));
73
- return { error: errorBody.error || `HTTP ${res.status}` };
74
- }
75
- const data = await res.json();
76
- return { data };
77
- } catch (err) {
78
- clearTimeout(timeout);
79
- if (err instanceof Error && err.name === "AbortError") {
80
- return { error: `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s` };
69
+ const bodyStr = requestBody ? JSON.stringify(requestBody) : void 0;
70
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
73
+ try {
74
+ const res = await fetch(url.toString(), {
75
+ method,
76
+ headers,
77
+ body: bodyStr,
78
+ signal: controller.signal
79
+ });
80
+ clearTimeout(timeout);
81
+ if (!res.ok) {
82
+ const errorBody = await res.json().catch(() => ({}));
83
+ return { error: errorBody.error || `HTTP ${res.status}` };
84
+ }
85
+ const data = await res.json();
86
+ return { data };
87
+ } catch (err) {
88
+ clearTimeout(timeout);
89
+ if (attempt < MAX_RETRIES && isTransientError(err)) {
90
+ const delay = RETRY_BASE_MS * 2 ** attempt;
91
+ await new Promise((r) => setTimeout(r, delay));
92
+ continue;
93
+ }
94
+ if (err instanceof Error && err.name === "AbortError") {
95
+ return {
96
+ error: `Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s`
97
+ };
98
+ }
99
+ return { error: err instanceof Error ? err.message : String(err) };
81
100
  }
82
- return { error: err instanceof Error ? err.message : String(err) };
83
101
  }
102
+ return { error: "Request failed after retries" };
84
103
  }
85
104
  function apiGet(path, params) {
86
105
  return request({ method: "GET", path, params });
@@ -1067,7 +1086,16 @@ var getSessionDetailsTool = {
1067
1086
  description: "Get behavioral timeline for one or more sessions. Shows how frustration, confusion, and confidence evolved over time. Accepts a single session_id or an array of session_ids (max 20).",
1068
1087
  inputSchema: z21.object({
1069
1088
  session_id: z21.string().optional().describe("Single session ID to get details for"),
1070
- session_ids: z21.array(z21.string()).max(20).optional().describe("Array of session IDs to get details for (max 20)")
1089
+ session_ids: z21.preprocess((val) => {
1090
+ if (typeof val === "string") {
1091
+ try {
1092
+ return JSON.parse(val);
1093
+ } catch {
1094
+ return val;
1095
+ }
1096
+ }
1097
+ return val;
1098
+ }, z21.array(z21.string()).max(20).optional()).describe("Array of session IDs to get details for (max 20)")
1071
1099
  }),
1072
1100
  handler: async (args) => {
1073
1101
  const { session_id, session_ids } = args;
@@ -1102,7 +1130,16 @@ var getSessionPagesTool = {
1102
1130
  description: "Get the pages visited within one or more sessions. Returns navigation history with timestamps, source pages, and dwell times. Use this to understand the user's journey through the site during a session. Accepts a single session_id or an array of session_ids (max 20).",
1103
1131
  inputSchema: z22.object({
1104
1132
  session_id: z22.string().optional().describe("Single session ID to get pages for"),
1105
- session_ids: z22.array(z22.string()).max(20).optional().describe("Array of session IDs to get pages for (max 20)")
1133
+ session_ids: z22.preprocess((val) => {
1134
+ if (typeof val === "string") {
1135
+ try {
1136
+ return JSON.parse(val);
1137
+ } catch {
1138
+ return val;
1139
+ }
1140
+ }
1141
+ return val;
1142
+ }, z22.array(z22.string()).max(20).optional()).describe("Array of session IDs to get pages for (max 20)")
1106
1143
  }),
1107
1144
  handler: async (args) => {
1108
1145
  const { session_id, session_ids } = args;
@@ -1326,9 +1363,9 @@ var recordImprovementActionTool = {
1326
1363
  return apiNotConfiguredResult();
1327
1364
  }
1328
1365
  if (remediation_id) {
1329
- const { data: remediation, error: remediationError } = await apiGet(`/remediations/${remediation_id}`);
1330
- if (remediationError || !remediation) {
1331
- return errorResult(`Remediation ${remediation_id} not found`);
1366
+ const { error: remediationError } = await apiGet(`/remediations/${remediation_id}`);
1367
+ if (remediationError) {
1368
+ console.warn(`Preflight remediation check failed for ${remediation_id}: ${remediationError}. Proceeding with POST.`);
1332
1369
  }
1333
1370
  }
1334
1371
  const { data, error } = await apiPost(`/improvement-runs/${run_id}/actions`, {
@@ -1746,14 +1783,18 @@ var evaluateFixTool = {
1746
1783
  description: "Evaluate if a deployed fix improved metrics. Compares post-deployment metrics to baseline. Returns success/partial/failed outcome with detailed verdict. Wait at least 24 hours after deployment for reliable results.",
1747
1784
  inputSchema: z30.object({
1748
1785
  remediation_id: z30.string().describe("The ID of the remediation to evaluate"),
1749
- min_hours: z30.number().optional().default(24).describe("Minimum hours since deployment required (default: 24)")
1786
+ min_hours: z30.number().optional().default(24).describe("Minimum hours since deployment required (default: 24)"),
1787
+ skip_status_update: z30.boolean().optional().default(false).describe("When true, skips writing remediation status (used in phased mode where applyEvaluateUpdates is authoritative)")
1750
1788
  }),
1751
1789
  handler: async (args) => {
1752
- const { remediation_id, min_hours } = args;
1790
+ const { remediation_id, min_hours, skip_status_update } = args;
1753
1791
  if (!isApiConfigured()) {
1754
1792
  return apiNotConfiguredResult();
1755
1793
  }
1756
- const { data, error } = await apiPost(`/remediations/${remediation_id}/evaluate`, { min_hours: min_hours ?? 24 });
1794
+ const { data, error } = await apiPost(`/remediations/${remediation_id}/evaluate`, {
1795
+ min_hours: min_hours ?? 24,
1796
+ skip_status_update: skip_status_update ?? false
1797
+ });
1757
1798
  if (error) {
1758
1799
  return errorResult(error);
1759
1800
  }
@@ -2017,7 +2058,16 @@ var triageSessionsTool = {
2017
2058
  name: "triage_sessions",
2018
2059
  description: "Automatically triage sessions to find compromised user experiences. Analyzes user comments, frustration signals, rage clicks, and console errors. Returns flagged sessions with evidence and replay links. Use this to identify sessions that need attention without manually reviewing every recording. Can triage specific sessions by ID(s) or scan for problematic sessions. NOTE: Session replay URLs require a paid plan (Starter+). Free tier users can see session summaries and triage scores but not watch replays.",
2019
2060
  inputSchema: z33.object({
2020
- session_ids: z33.array(z33.string()).optional().describe("Triage specific sessions by ID. If provided, other filters are ignored."),
2061
+ session_ids: z33.preprocess((val) => {
2062
+ if (typeof val === "string") {
2063
+ try {
2064
+ return JSON.parse(val);
2065
+ } catch {
2066
+ return val;
2067
+ }
2068
+ }
2069
+ return val;
2070
+ }, z33.array(z33.string()).optional()).describe("Triage specific sessions by ID. If provided, other filters are ignored."),
2021
2071
  days: z33.number().optional().default(7).describe("Lookback period in days (default: 7). Ignored if session_ids is provided."),
2022
2072
  page_path: z33.string().optional().describe("Filter to sessions that visited a specific page"),
2023
2073
  min_severity: z33.enum(["critical", "high", "medium", "low"]).optional().describe("Minimum severity to include in results"),
@@ -2808,6 +2858,138 @@ var listRepositoryFilesTool = {
2808
2858
  }
2809
2859
  }
2810
2860
  };
2861
+ var getRepoTreeTool = {
2862
+ name: "get_repo_tree",
2863
+ description: "Get a condensed directory-only tree of the repository structure. Returns a nested object showing the folder hierarchy up to a configurable depth. Much faster than recursive list_repository_files for understanding project layout. Use this FIRST to orient yourself, then list_repository_files on specific directories.",
2864
+ inputSchema: z35.object({
2865
+ max_depth: z35.number().optional().default(4).describe("Maximum directory depth to return (default: 4)"),
2866
+ branch: z35.string().optional().describe("Branch to read from (default: main)")
2867
+ }),
2868
+ handler: async (args) => {
2869
+ try {
2870
+ const ctx = getGitContext();
2871
+ const maxDepth = args.max_depth || 4;
2872
+ const branch = args.branch || "main";
2873
+ if (ctx.provider === "github") {
2874
+ const res = await githubRequest(`/repos/${ctx.repoFullName}/git/trees/${branch}?recursive=1`);
2875
+ if (!res.ok) {
2876
+ return errorResult(`Failed to get repo tree: ${res.statusText}`);
2877
+ }
2878
+ const data = await res.json();
2879
+ const dirs = data.tree.filter((item) => item.type === "tree").map((item) => item.path);
2880
+ const tree = buildNestedTree(dirs, maxDepth);
2881
+ const totalDirs = dirs.length;
2882
+ return successResult({
2883
+ tree,
2884
+ total_directories: totalDirs,
2885
+ depth: maxDepth,
2886
+ provider: "github"
2887
+ });
2888
+ } else {
2889
+ const encodedPath = encodeURIComponent(ctx.repoFullName);
2890
+ const res = await gitlabRequest(`/projects/${encodedPath}/repository/tree?ref=${branch}&recursive=true&per_page=100`);
2891
+ if (!res.ok) {
2892
+ return errorResult(`Failed to get repo tree: ${res.statusText}`);
2893
+ }
2894
+ const data = await res.json();
2895
+ const dirs = data.filter((item) => item.type === "tree").map((item) => item.path);
2896
+ const tree = buildNestedTree(dirs, maxDepth);
2897
+ const totalDirs = dirs.length;
2898
+ return successResult({
2899
+ tree,
2900
+ total_directories: totalDirs,
2901
+ depth: maxDepth,
2902
+ provider: "gitlab"
2903
+ });
2904
+ }
2905
+ } catch (error) {
2906
+ return errorResult(error instanceof Error ? error.message : "Failed to get repo tree");
2907
+ }
2908
+ }
2909
+ };
2910
+ var searchCodeTool = {
2911
+ name: "search_code",
2912
+ description: "Search for code patterns, symbols, or imports across the repository. Use this to find existing conventions, verify import paths, or discover reusable utilities before writing code. Rate-limited (~10 req/min on GitHub) \u2014 use sparingly and with specific queries.",
2913
+ inputSchema: z35.object({
2914
+ query: z35.string().describe('Search terms (e.g., "import { Button }", "useMemo", "className={styles.")'),
2915
+ path_filter: z35.string().optional().describe('Restrict search to a directory (e.g., "client/src/components")'),
2916
+ extension: z35.string().optional().describe('Filter by file extension (e.g., "tsx", "ts", "css")')
2917
+ }),
2918
+ handler: async (args) => {
2919
+ try {
2920
+ const ctx = getGitContext();
2921
+ const query = args.query;
2922
+ const pathFilter = args.path_filter;
2923
+ const extension = args.extension;
2924
+ if (ctx.provider === "github") {
2925
+ let q = `${query} repo:${ctx.repoFullName}`;
2926
+ if (pathFilter)
2927
+ q += ` path:${pathFilter}`;
2928
+ if (extension)
2929
+ q += ` extension:${extension}`;
2930
+ const res = await githubRequest(`/search/code?q=${encodeURIComponent(q)}&per_page=10`, {
2931
+ headers: {
2932
+ Accept: "application/vnd.github.text-match+json"
2933
+ }
2934
+ });
2935
+ if (!res.ok) {
2936
+ const err = await res.json();
2937
+ return errorResult(err.message || `Code search failed: ${res.status}`);
2938
+ }
2939
+ const data = await res.json();
2940
+ const results = data.items.map((item) => ({
2941
+ path: item.path,
2942
+ score: item.score,
2943
+ matched_lines: (item.text_matches ?? []).map((m) => m.fragment),
2944
+ url: item.html_url
2945
+ }));
2946
+ return successResult({
2947
+ total_count: data.total_count,
2948
+ results,
2949
+ provider: "github"
2950
+ });
2951
+ } else {
2952
+ const encodedPath = encodeURIComponent(ctx.repoFullName);
2953
+ const searchQuery = pathFilter ? `${query} filename:${pathFilter}` : query;
2954
+ const res = await gitlabRequest(`/projects/${encodedPath}/search?scope=blobs&search=${encodeURIComponent(searchQuery)}&per_page=10`);
2955
+ if (!res.ok) {
2956
+ const err = await res.json();
2957
+ return errorResult(err.message || `Code search failed: ${res.status}`);
2958
+ }
2959
+ const data = await res.json();
2960
+ const results = data.map((item) => ({
2961
+ path: item.path,
2962
+ matched_lines: [item.data],
2963
+ startline: item.startline
2964
+ }));
2965
+ return successResult({
2966
+ total_count: results.length,
2967
+ results,
2968
+ provider: "gitlab"
2969
+ });
2970
+ }
2971
+ } catch (error) {
2972
+ return errorResult(error instanceof Error ? error.message : "Code search failed");
2973
+ }
2974
+ }
2975
+ };
2976
+ function buildNestedTree(dirs, maxDepth) {
2977
+ const tree = {};
2978
+ for (const dir of dirs) {
2979
+ const parts = dir.split("/");
2980
+ if (parts.length > maxDepth)
2981
+ continue;
2982
+ let current = tree;
2983
+ for (const part of parts) {
2984
+ const key = `${part}/`;
2985
+ if (!current[key]) {
2986
+ current[key] = {};
2987
+ }
2988
+ current = current[key];
2989
+ }
2990
+ }
2991
+ return tree;
2992
+ }
2811
2993
 
2812
2994
  // ../../libraries/mcp-tools/dist/tools/index.js
2813
2995
  var allTools = [
@@ -2890,7 +3072,9 @@ var allTools = [
2890
3072
  createMergeRequestTool,
2891
3073
  checkMrStatusTool,
2892
3074
  listOpenMrsTool,
2893
- listRepositoryFilesTool
3075
+ listRepositoryFilesTool,
3076
+ getRepoTreeTool,
3077
+ searchCodeTool
2894
3078
  ];
2895
3079
  var EXPOSED_TOOL_NAMES = [
2896
3080
  "get_domains",
@@ -595,92 +595,57 @@
595
595
  }
596
596
  },
597
597
  {
598
- "name": "dismiss_issue",
599
- "description": "Dismiss an issue as a false positive. Creates site knowledge to prevent re-flagging.",
598
+ "name": "propose_fix",
599
+ "description": "Propose a fix for a UX issue. REQUIRED: page_path, diagnosis, proposed_fix, confidence. Creates a remediation record with baseline metrics for later evaluation. Returns remediation_id for tracking.",
600
600
  "input_schema": {
601
601
  "type": "object",
602
602
  "properties": {
603
- "issue_id": {
604
- "description": "The ID of the issue to dismiss",
605
- "type": "string"
606
- },
607
- "reason": {
608
- "description": "Reason for dismissal",
609
- "type": "string"
610
- },
611
- "explanation": {
612
- "description": "Detailed explanation",
603
+ "page_path": {
604
+ "description": "The page path where the issue occurs",
613
605
  "type": "string"
614
606
  },
615
- "create_knowledge": {
616
- "description": "Whether to create site knowledge entry (default: true)",
617
- "type": "boolean"
618
- }
619
- },
620
- "required": [
621
- "issue_id",
622
- "reason",
623
- "explanation"
624
- ]
625
- }
626
- },
627
- {
628
- "name": "mark_intended_behavior",
629
- "description": "Mark an issue as intended behavior. Creates site knowledge to prevent re-flagging.",
630
- "input_schema": {
631
- "type": "object",
632
- "properties": {
633
- "issue_id": {
634
- "description": "The ID of the issue to mark as intended",
607
+ "category": {
608
+ "description": "The issue category (code_error, dead_click, rage_click, ux_friction, performance, form_issue, behavioral_anomaly, structural_issue)",
635
609
  "type": "string"
636
610
  },
637
- "explanation": {
638
- "description": "Explanation of why this behavior is intended",
639
- "type": "string"
640
- }
641
- },
642
- "required": [
643
- "issue_id",
644
- "explanation"
645
- ]
646
- }
647
- },
648
- {
649
- "name": "propose_fix",
650
- "description": "Propose a fix for an issue. Creates a remediation record with baseline metrics for later evaluation. Use this to create a new remediation, even without an existing issue ID.",
651
- "input_schema": {
652
- "type": "object",
653
- "properties": {
654
- "issue_id": {
655
- "description": "The ID of the issue to fix",
611
+ "severity": {
612
+ "description": "The issue severity (critical, high, medium, low, info)",
656
613
  "type": "string"
657
614
  },
658
615
  "diagnosis": {
659
- "description": "Detailed analysis of the root cause",
616
+ "description": "Detailed analysis of the root cause of the issue",
660
617
  "type": "string"
661
618
  },
662
619
  "proposed_fix": {
663
- "description": "Description of the proposed fix",
620
+ "description": "Description of the proposed fix and how it addresses the root cause",
664
621
  "type": "string"
665
622
  },
666
623
  "code_snippet": {
667
- "description": "Suggested code changes",
624
+ "description": "Suggested code changes (if applicable)",
668
625
  "type": "string"
669
626
  },
670
627
  "affected_files": {
671
- "description": "List of files to modify",
628
+ "description": "List of files that need to be modified",
672
629
  "type": "array",
673
630
  "items": {
674
631
  "type": "string"
675
632
  }
676
633
  },
677
634
  "confidence": {
678
- "description": "Confidence level (0-1)",
635
+ "description": "Confidence level in the fix (0-1). Use <0.7 when uncertain.",
679
636
  "type": "number"
637
+ },
638
+ "title": {
639
+ "description": "User-friendly title describing the issue from the user's perspective",
640
+ "type": "string"
641
+ },
642
+ "execution_metadata": {
643
+ "description": "AI execution metadata for tracking model selection and performance",
644
+ "type": "object"
680
645
  }
681
646
  },
682
647
  "required": [
683
- "issue_id",
648
+ "page_path",
684
649
  "diagnosis",
685
650
  "proposed_fix",
686
651
  "confidence"
@@ -722,6 +687,29 @@
722
687
  "required": []
723
688
  }
724
689
  },
690
+ {
691
+ "name": "list_remediations_by_status",
692
+ "description": "List remediations filtered by one or more statuses. Use to find all 'waiting' PRs or 'deployed' fixes ready for evaluation.",
693
+ "input_schema": {
694
+ "type": "object",
695
+ "properties": {
696
+ "statuses": {
697
+ "description": "List of statuses to filter by (e.g. ['waiting', 'deployed'])",
698
+ "type": "array",
699
+ "items": {
700
+ "type": "string"
701
+ }
702
+ },
703
+ "include_execution": {
704
+ "description": "Include execution metadata (difficulty class, model used)",
705
+ "type": "boolean"
706
+ }
707
+ },
708
+ "required": [
709
+ "statuses"
710
+ ]
711
+ }
712
+ },
725
713
  {
726
714
  "name": "confirm_deployment",
727
715
  "description": "Confirm that a proposed fix has been deployed to production. Starts the evaluation timer.",
@@ -1318,6 +1306,48 @@
1318
1306
  "required": []
1319
1307
  }
1320
1308
  },
1309
+ {
1310
+ "name": "get_repo_tree",
1311
+ "description": "Get a condensed directory-only tree of the repository. Returns a nested object showing the folder hierarchy up to a configurable depth. Use FIRST to understand project layout before navigating with list_repository_files.",
1312
+ "input_schema": {
1313
+ "type": "object",
1314
+ "properties": {
1315
+ "max_depth": {
1316
+ "description": "Maximum directory depth (default: 4)",
1317
+ "type": "number"
1318
+ },
1319
+ "branch": {
1320
+ "description": "Branch name (default: main)",
1321
+ "type": "string"
1322
+ }
1323
+ },
1324
+ "required": []
1325
+ }
1326
+ },
1327
+ {
1328
+ "name": "search_code",
1329
+ "description": "Search for code patterns, symbols, or imports across the repository. Use to find existing conventions, verify import paths, or discover reusable utilities. Rate-limited (~10 req/min) — use sparingly with specific queries.",
1330
+ "input_schema": {
1331
+ "type": "object",
1332
+ "properties": {
1333
+ "query": {
1334
+ "description": "Search terms (e.g., \"import { Button }\", \"useMemo\", \"className={styles.\")",
1335
+ "type": "string"
1336
+ },
1337
+ "path_filter": {
1338
+ "description": "Restrict search to a directory (e.g., \"client/src/components\")",
1339
+ "type": "string"
1340
+ },
1341
+ "extension": {
1342
+ "description": "Filter by file extension (e.g., \"tsx\", \"ts\", \"css\")",
1343
+ "type": "string"
1344
+ }
1345
+ },
1346
+ "required": [
1347
+ "query"
1348
+ ]
1349
+ }
1350
+ },
1321
1351
  {
1322
1352
  "name": "get_file_content",
1323
1353
  "description": "Read file contents from the repository.",