@recapt/mcp 0.0.42 → 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 });
@@ -145,8 +164,6 @@ function registerAllToolHandlers() {
145
164
  toolHandlers.set("analyze_funnel", createApiHandler("POST", "/flows/funnel"));
146
165
  toolHandlers.set("get_flow_friction", createApiHandler("GET", "/flow-friction"));
147
166
  toolHandlers.set("get_journey_patterns", createApiHandler("GET", "/flows/patterns"));
148
- toolHandlers.set("get_issues", createApiHandler("GET", "/issues"));
149
- toolHandlers.set("get_actionable_issues", createApiHandler("GET", "/actionable-issues"));
150
167
  toolHandlers.set("get_anomalies", createApiHandler("GET", "/anomalies"));
151
168
  toolHandlers.set("detect_regressions", createApiHandler("GET", "/regressions"));
152
169
  toolHandlers.set("detect_drift", createApiHandler("GET", "/drift"));
@@ -1069,7 +1086,16 @@ var getSessionDetailsTool = {
1069
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).",
1070
1087
  inputSchema: z21.object({
1071
1088
  session_id: z21.string().optional().describe("Single session ID to get details for"),
1072
- 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)")
1073
1099
  }),
1074
1100
  handler: async (args) => {
1075
1101
  const { session_id, session_ids } = args;
@@ -1104,7 +1130,16 @@ var getSessionPagesTool = {
1104
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).",
1105
1131
  inputSchema: z22.object({
1106
1132
  session_id: z22.string().optional().describe("Single session ID to get pages for"),
1107
- 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)")
1108
1143
  }),
1109
1144
  handler: async (args) => {
1110
1145
  const { session_id, session_ids } = args;
@@ -1328,9 +1363,9 @@ var recordImprovementActionTool = {
1328
1363
  return apiNotConfiguredResult();
1329
1364
  }
1330
1365
  if (remediation_id) {
1331
- const { data: remediation, error: remediationError } = await apiGet(`/remediations/${remediation_id}`);
1332
- if (remediationError || !remediation) {
1333
- 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.`);
1334
1369
  }
1335
1370
  }
1336
1371
  const { data, error } = await apiPost(`/improvement-runs/${run_id}/actions`, {
@@ -1748,14 +1783,18 @@ var evaluateFixTool = {
1748
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.",
1749
1784
  inputSchema: z30.object({
1750
1785
  remediation_id: z30.string().describe("The ID of the remediation to evaluate"),
1751
- 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)")
1752
1788
  }),
1753
1789
  handler: async (args) => {
1754
- const { remediation_id, min_hours } = args;
1790
+ const { remediation_id, min_hours, skip_status_update } = args;
1755
1791
  if (!isApiConfigured()) {
1756
1792
  return apiNotConfiguredResult();
1757
1793
  }
1758
- 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
+ });
1759
1798
  if (error) {
1760
1799
  return errorResult(error);
1761
1800
  }
@@ -2019,7 +2058,16 @@ var triageSessionsTool = {
2019
2058
  name: "triage_sessions",
2020
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.",
2021
2060
  inputSchema: z33.object({
2022
- 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."),
2023
2071
  days: z33.number().optional().default(7).describe("Lookback period in days (default: 7). Ignored if session_ids is provided."),
2024
2072
  page_path: z33.string().optional().describe("Filter to sessions that visited a specific page"),
2025
2073
  min_severity: z33.enum(["critical", "high", "medium", "low"]).optional().describe("Minimum severity to include in results"),
@@ -2810,6 +2858,138 @@ var listRepositoryFilesTool = {
2810
2858
  }
2811
2859
  }
2812
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
+ }
2813
2993
 
2814
2994
  // ../../libraries/mcp-tools/dist/tools/index.js
2815
2995
  var allTools = [
@@ -2892,7 +3072,9 @@ var allTools = [
2892
3072
  createMergeRequestTool,
2893
3073
  checkMrStatusTool,
2894
3074
  listOpenMrsTool,
2895
- listRepositoryFilesTool
3075
+ listRepositoryFilesTool,
3076
+ getRepoTreeTool,
3077
+ searchCodeTool
2896
3078
  ];
2897
3079
  var EXPOSED_TOOL_NAMES = [
2898
3080
  "get_domains",