@recapt/mcp 0.0.2-beta → 0.0.4-beta

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.
@@ -8,5 +8,7 @@ interface ApiResponse<T> {
8
8
  error?: string;
9
9
  }
10
10
  export declare function apiGet<T>(path: string, params?: Record<string, string | number | undefined>): Promise<ApiResponse<T>>;
11
- export declare function apiPost<T>(path: string, body: Record<string, unknown>): Promise<ApiResponse<T>>;
11
+ export declare function apiPost<T>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
12
+ export declare function apiPatch<T>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
13
+ export declare function apiDelete<T>(path: string): Promise<ApiResponse<T>>;
12
14
  export {};
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * HTTP client for calling external-api query endpoints.
3
3
  */
4
- const API_URL = process.env.EXTERNAL_API_URL || "http://localhost:4000";
4
+ const API_URL = process.env.EXTERNAL_API_URL || "https://api.recapt.app";
5
5
  const SECRET_KEY = process.env.RECAPT_SECRET_KEY;
6
6
  export function isApiConfigured() {
7
7
  return !!SECRET_KEY;
@@ -40,7 +40,7 @@ export async function apiGet(path, params) {
40
40
  return { error: err instanceof Error ? err.message : String(err) };
41
41
  }
42
42
  }
43
- export async function apiPost(path, body) {
43
+ export async function apiPost(path, body = {}) {
44
44
  if (!SECRET_KEY) {
45
45
  return { error: "RECAPT_SECRET_KEY not configured" };
46
46
  }
@@ -65,3 +65,52 @@ export async function apiPost(path, body) {
65
65
  return { error: err instanceof Error ? err.message : String(err) };
66
66
  }
67
67
  }
68
+ export async function apiPatch(path, body = {}) {
69
+ if (!SECRET_KEY) {
70
+ return { error: "RECAPT_SECRET_KEY not configured" };
71
+ }
72
+ const url = `${API_URL}/v1beta/query${path}`;
73
+ try {
74
+ const res = await fetch(url, {
75
+ method: "PATCH",
76
+ headers: {
77
+ "x-private-key": SECRET_KEY,
78
+ "Content-Type": "application/json",
79
+ },
80
+ body: JSON.stringify(body),
81
+ });
82
+ if (!res.ok) {
83
+ const responseData = (await res.json().catch(() => ({})));
84
+ return { error: responseData.error || `HTTP ${res.status}` };
85
+ }
86
+ const data = (await res.json());
87
+ return { data };
88
+ }
89
+ catch (err) {
90
+ return { error: err instanceof Error ? err.message : String(err) };
91
+ }
92
+ }
93
+ export async function apiDelete(path) {
94
+ if (!SECRET_KEY) {
95
+ return { error: "RECAPT_SECRET_KEY not configured" };
96
+ }
97
+ const url = `${API_URL}/v1beta/query${path}`;
98
+ try {
99
+ const res = await fetch(url, {
100
+ method: "DELETE",
101
+ headers: {
102
+ "x-private-key": SECRET_KEY,
103
+ "Content-Type": "application/json",
104
+ },
105
+ });
106
+ if (!res.ok) {
107
+ const responseData = (await res.json().catch(() => ({})));
108
+ return { error: responseData.error || `HTTP ${res.status}` };
109
+ }
110
+ const data = (await res.json());
111
+ return { data };
112
+ }
113
+ catch (err) {
114
+ return { error: err instanceof Error ? err.message : String(err) };
115
+ }
116
+ }
package/dist/index.js CHANGED
@@ -39,6 +39,10 @@ import { registerPredictOutcomes } from "./tools/predictOutcomes.js";
39
39
  import { registerMemoryTools } from "./tools/memory.js";
40
40
  import { registerListSessions } from "./tools/listSessions.js";
41
41
  import { registerTriageSessions } from "./tools/triageSessions.js";
42
+ import { registerDiagnosticTools } from "./tools/diagnostic.js";
43
+ import { registerTriageTools } from "./tools/triage.js";
44
+ import { registerRemediationTools } from "./tools/remediation.js";
45
+ import { registerKnowledgeTools } from "./tools/knowledge.js";
42
46
  async function main() {
43
47
  console.error("[MCP] Starting recapt behavioral intelligence server...");
44
48
  if (!isApiConfigured()) {
@@ -177,7 +181,83 @@ When triaging, prioritize:
177
181
  3. Sessions with console errors + high frustration (likely bugs)
178
182
 
179
183
  ### Working Memory
180
- - \`memory_save\`, \`memory_recall\`, \`memory_list\`, \`memory_delete\`, \`memory_clear\`: Store and retrieve intermediate results`,
184
+ - \`memory_save\`, \`memory_recall\`, \`memory_list\`, \`memory_delete\`, \`memory_clear\`: Store and retrieve intermediate results
185
+
186
+ ## Self-Healing Workflow
187
+
188
+ When asked to "fix all issues", "heal the site", or "improve my site via recapt":
189
+
190
+ ### 1. Full Diagnostic
191
+ - Call \`run_full_diagnostic\` to get prioritized issue list with severity scores
192
+ - Call \`get_site_knowledge\` to load known false positives and intended behaviors
193
+ - Review the \`recommended_investigation_order\` to prioritize your work
194
+
195
+ ### 2. Triage Each Issue
196
+ For each issue in priority order, determine if it's actionable:
197
+ - Call \`validate_issue\` to check if still occurring (not stale)
198
+ - Check against site knowledge for known false positives
199
+ - If recommendation is "dismiss_stale", consider dismissing
200
+ - If uncertain, call \`investigate_issue\` for deep context
201
+ - **Always ask user to confirm** if behavior is intended when unclear
202
+
203
+ ### 3. Propose Fixes
204
+ For confirmed issues:
205
+ - Analyze root cause from evidence and element context
206
+ - Call \`get_similar_fixes\` to see what worked before for similar issues
207
+ - Call \`get_fix_history\` to see past attempts on this specific issue
208
+ - Call \`propose_fix\` with detailed diagnosis and code suggestion
209
+ - Explain confidence level and reasoning to user
210
+ - **Ask for confirmation when confidence < 0.7**
211
+
212
+ ### 4. Track Deployments
213
+ - Show user all pending fixes with \`list_pending_fixes\`
214
+ - When user confirms "I have deployed this fix", call \`confirm_deployment\`
215
+ - Explain that evaluation will happen after 24-48 hours of data collection
216
+
217
+ ### 5. Evaluate Results
218
+ - After deployment window (24-48h), call \`evaluate_fix\`
219
+ - If outcome is "success": celebrate, the issue will be auto-resolved
220
+ - If outcome is "partial": discuss with user whether to iterate
221
+ - If outcome is "failed": analyze why, propose alternative approach
222
+ - Store learnings with \`add_site_knowledge\` for future reference
223
+
224
+ ### Evaluation Thresholds
225
+ - **Success**: frustration decreased >15% OR health score increased >10 points
226
+ - **Partial**: frustration decreased 5-15% OR health score increased 3-10 points
227
+ - **Failed**: no improvement or regression
228
+
229
+ ### Asking for Confirmation
230
+ Always ask user before:
231
+ - Dismissing an issue as false positive
232
+ - Marking behavior as intended
233
+ - When confidence in fix is <0.7
234
+ - Before evaluating (ensure they've actually deployed)
235
+
236
+ ### Self-Healing Tools
237
+
238
+ #### Diagnostic
239
+ - \`run_full_diagnostic\`: Comprehensive site analysis with prioritized issues
240
+ - \`investigate_issue\`: Deep-dive into specific issue with sessions and errors
241
+ - \`validate_issue\`: Check if issue is still occurring (not stale)
242
+
243
+ #### Triage
244
+ - \`dismiss_issue\`: Mark issue as false positive with reason
245
+ - \`mark_intended_behavior\`: Flag behavior as by design
246
+ - \`get_issue_history\`: View past occurrences and fix attempts
247
+
248
+ #### Remediation
249
+ - \`propose_fix\`: Create remediation record with baseline metrics
250
+ - \`list_pending_fixes\`: Show fixes awaiting deployment
251
+ - \`list_remediations\`: Browse all remediation records
252
+ - \`confirm_deployment\`: Mark fix as deployed, start evaluation timer
253
+ - \`evaluate_fix\`: Compare post-deploy metrics to baseline
254
+ - \`get_fix_history\`: View all fix attempts for an issue
255
+
256
+ #### Knowledge
257
+ - \`get_site_knowledge\`: Retrieve learned patterns and false positives
258
+ - \`add_site_knowledge\`: Store new learning for future reference
259
+ - \`delete_site_knowledge\`: Remove outdated knowledge
260
+ - \`get_similar_fixes\`: Find past fixes for similar issues`,
181
261
  });
182
262
  registerGetDomains(server);
183
263
  registerGetPageMetrics(server);
@@ -207,6 +287,11 @@ When triaging, prioritize:
207
287
  registerMemoryTools(server);
208
288
  registerListSessions(server);
209
289
  registerTriageSessions(server);
290
+ // Self-healing tools
291
+ registerDiagnosticTools(server);
292
+ registerTriageTools(server);
293
+ registerRemediationTools(server);
294
+ registerKnowledgeTools(server);
210
295
  const transport = new StdioServerTransport();
211
296
  await server.connect(transport);
212
297
  console.error("[MCP] Server running on stdio");
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Diagnostic tools for self-healing workflow.
3
+ *
4
+ * Provides comprehensive site diagnostics, issue investigation, and validation.
5
+ */
6
+ export declare function registerDiagnosticTools(server: any): void;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Diagnostic tools for self-healing workflow.
3
+ *
4
+ * Provides comprehensive site diagnostics, issue investigation, and validation.
5
+ */
6
+ import { z } from "zod";
7
+ import { apiGet, apiPost, isApiConfigured } from "../api/client.js";
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export function registerDiagnosticTools(server) {
10
+ server.registerTool("run_full_diagnostic", {
11
+ description: "Run a comprehensive site diagnostic. Returns prioritized issues, problem pages, regressions, and recommended investigation order. Use this as the first step in a self-healing workflow.",
12
+ inputSchema: z.object({
13
+ days: z
14
+ .number()
15
+ .optional()
16
+ .default(7)
17
+ .describe("Number of days to analyze (default: 7)"),
18
+ }),
19
+ }, async ({ days }) => {
20
+ if (!isApiConfigured()) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: JSON.stringify({ error: "API not configured" }),
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
31
+ const { data, error } = await apiGet("/diagnostic/full", { days: days ?? 7 });
32
+ if (error) {
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
35
+ isError: true,
36
+ };
37
+ }
38
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
39
+ });
40
+ server.registerTool("investigate_issue", {
41
+ description: "Deep-dive into a specific issue. Returns affected sessions, element friction data, related console errors, similar issues, and page context. Use after identifying an issue to understand root cause.",
42
+ inputSchema: z.object({
43
+ issue_id: z.string().describe("The ID of the issue to investigate"),
44
+ days: z
45
+ .number()
46
+ .optional()
47
+ .default(7)
48
+ .describe("Number of days to look back (default: 7)"),
49
+ }),
50
+ }, async ({ issue_id, days }) => {
51
+ if (!isApiConfigured()) {
52
+ return {
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: JSON.stringify({ error: "API not configured" }),
57
+ },
58
+ ],
59
+ isError: true,
60
+ };
61
+ }
62
+ const { data, error } = await apiGet(`/issues/${issue_id}/investigate`, { days: days ?? 7 });
63
+ if (error) {
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
66
+ isError: true,
67
+ };
68
+ }
69
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
70
+ });
71
+ server.registerTool("validate_issue", {
72
+ description: "Check if an issue is still actively occurring. Returns staleness info and recommendation (investigate, dismiss_stale, or monitor). Use to filter out stale issues before proposing fixes.",
73
+ inputSchema: z.object({
74
+ issue_id: z.string().describe("The ID of the issue to validate"),
75
+ lookback_days: z
76
+ .number()
77
+ .optional()
78
+ .default(3)
79
+ .describe("Number of days to look back for recent occurrences (default: 3)"),
80
+ }),
81
+ }, async ({ issue_id, lookback_days, }) => {
82
+ if (!isApiConfigured()) {
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: JSON.stringify({ error: "API not configured" }),
88
+ },
89
+ ],
90
+ isError: true,
91
+ };
92
+ }
93
+ const { data, error } = await apiPost(`/issues/${issue_id}/validate`, { lookback_days: lookback_days ?? 3 });
94
+ if (error) {
95
+ return {
96
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
97
+ isError: true,
98
+ };
99
+ }
100
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
101
+ });
102
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Knowledge tools for self-healing workflow.
3
+ *
4
+ * Provides site-specific knowledge management for learning from past fixes.
5
+ */
6
+ export declare function registerKnowledgeTools(server: any): void;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Knowledge tools for self-healing workflow.
3
+ *
4
+ * Provides site-specific knowledge management for learning from past fixes.
5
+ */
6
+ import { z } from "zod";
7
+ import { apiGet, apiPost, apiDelete, isApiConfigured } from "../api/client.js";
8
+ const KNOWLEDGE_CATEGORIES = [
9
+ "false_positive",
10
+ "intended_behavior",
11
+ "fix_pattern",
12
+ "context",
13
+ ];
14
+ const ISSUE_CATEGORIES = [
15
+ "code_error",
16
+ "dead_click",
17
+ "rage_click",
18
+ "ux_friction",
19
+ "performance",
20
+ "form_issue",
21
+ "behavioral_anomaly",
22
+ "structural_issue",
23
+ ];
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ export function registerKnowledgeTools(server) {
26
+ server.registerTool("get_site_knowledge", {
27
+ description: "Retrieve site-specific learnings including known false positives, intended behaviors, and successful fix patterns. Use at the start of a self-healing workflow to avoid re-flagging known issues.",
28
+ inputSchema: z.object({
29
+ category: z
30
+ .enum(KNOWLEDGE_CATEGORIES)
31
+ .optional()
32
+ .describe("Filter by knowledge category"),
33
+ page_path: z.string().optional().describe("Filter by page path"),
34
+ issue_category: z
35
+ .enum(ISSUE_CATEGORIES)
36
+ .optional()
37
+ .describe("Filter by issue category"),
38
+ limit: z
39
+ .number()
40
+ .optional()
41
+ .default(50)
42
+ .describe("Maximum number of entries to return (default: 50)"),
43
+ }),
44
+ }, async ({ category, page_path, issue_category, limit, }) => {
45
+ if (!isApiConfigured()) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: JSON.stringify({ error: "API not configured" }),
51
+ },
52
+ ],
53
+ isError: true,
54
+ };
55
+ }
56
+ const { data, error } = await apiGet("/knowledge", {
57
+ category,
58
+ page_path,
59
+ issue_category,
60
+ limit: limit ?? 50,
61
+ });
62
+ if (error) {
63
+ return {
64
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
65
+ isError: true,
66
+ };
67
+ }
68
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
69
+ });
70
+ server.registerTool("add_site_knowledge", {
71
+ description: "Store a site-specific learning. Use to record false positives, intended behaviors, successful fix patterns, or important context about the site.",
72
+ inputSchema: z.object({
73
+ category: z
74
+ .enum(KNOWLEDGE_CATEGORIES)
75
+ .describe("Category of knowledge: false_positive, intended_behavior, fix_pattern, or context"),
76
+ subject: z
77
+ .string()
78
+ .describe("Unique identifier/title for this knowledge entry"),
79
+ content: z.string().describe("Detailed description of the learning"),
80
+ page_path: z
81
+ .string()
82
+ .optional()
83
+ .describe("Associated page path (if applicable)"),
84
+ element_selector: z
85
+ .string()
86
+ .optional()
87
+ .describe("Associated element selector (if applicable)"),
88
+ issue_category: z
89
+ .enum(ISSUE_CATEGORIES)
90
+ .optional()
91
+ .describe("Associated issue category (if applicable)"),
92
+ }),
93
+ }, async ({ category, subject, content, page_path, element_selector, issue_category, }) => {
94
+ if (!isApiConfigured()) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify({ error: "API not configured" }),
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+ const { data, error } = await apiPost("/knowledge", {
106
+ category,
107
+ subject,
108
+ content,
109
+ page_path,
110
+ element_selector,
111
+ issue_category,
112
+ source: "agent",
113
+ });
114
+ if (error) {
115
+ return {
116
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
117
+ isError: true,
118
+ };
119
+ }
120
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
121
+ });
122
+ server.registerTool("delete_site_knowledge", {
123
+ description: "Delete a site knowledge entry. Use when a learning is no longer valid or was created in error.",
124
+ inputSchema: z.object({
125
+ knowledge_id: z
126
+ .string()
127
+ .describe("The ID of the knowledge entry to delete"),
128
+ }),
129
+ }, async ({ knowledge_id }) => {
130
+ if (!isApiConfigured()) {
131
+ return {
132
+ content: [
133
+ {
134
+ type: "text",
135
+ text: JSON.stringify({ error: "API not configured" }),
136
+ },
137
+ ],
138
+ isError: true,
139
+ };
140
+ }
141
+ const { data, error } = await apiDelete(`/knowledge/${knowledge_id}`);
142
+ if (error) {
143
+ return {
144
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
145
+ isError: true,
146
+ };
147
+ }
148
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
149
+ });
150
+ server.registerTool("get_similar_fixes", {
151
+ description: "Find past fixes for similar issues. Returns successful and failed remediation attempts for issues with the same category, page, or element. Use before proposing a fix to learn from past attempts.",
152
+ inputSchema: z.object({
153
+ issue_id: z
154
+ .string()
155
+ .describe("The ID of the issue to find similar fixes for"),
156
+ limit: z
157
+ .number()
158
+ .optional()
159
+ .default(5)
160
+ .describe("Maximum number of similar fixes to return (default: 5)"),
161
+ }),
162
+ }, async ({ issue_id, limit }) => {
163
+ if (!isApiConfigured()) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify({ error: "API not configured" }),
169
+ },
170
+ ],
171
+ isError: true,
172
+ };
173
+ }
174
+ const { data, error } = await apiGet("/knowledge/similar-fixes", {
175
+ issue_id,
176
+ limit: limit ?? 5,
177
+ });
178
+ if (error) {
179
+ return {
180
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
181
+ isError: true,
182
+ };
183
+ }
184
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
185
+ });
186
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Remediation tools for self-healing workflow.
3
+ *
4
+ * Provides fix proposal, deployment confirmation, evaluation, and history tracking.
5
+ */
6
+ export declare function registerRemediationTools(server: any): void;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Remediation tools for self-healing workflow.
3
+ *
4
+ * Provides fix proposal, deployment confirmation, evaluation, and history tracking.
5
+ */
6
+ import { z } from "zod";
7
+ import { apiGet, apiPost, apiPatch, isApiConfigured } from "../api/client.js";
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export function registerRemediationTools(server) {
10
+ server.registerTool("propose_fix", {
11
+ description: "Propose a fix for an issue. Creates a remediation record with baseline metrics for later evaluation. Include detailed diagnosis and code suggestions when possible.",
12
+ inputSchema: z.object({
13
+ issue_id: z.string().describe("The ID of the issue to fix"),
14
+ diagnosis: z
15
+ .string()
16
+ .describe("Detailed analysis of the root cause of the issue"),
17
+ proposed_fix: z
18
+ .string()
19
+ .describe("Description of the proposed fix and how it addresses the root cause"),
20
+ code_snippet: z
21
+ .string()
22
+ .optional()
23
+ .describe("Suggested code changes (if applicable)"),
24
+ affected_files: z
25
+ .array(z.string())
26
+ .optional()
27
+ .describe("List of files that need to be modified"),
28
+ confidence: z
29
+ .number()
30
+ .min(0)
31
+ .max(1)
32
+ .describe("Confidence level in the fix (0-1). Use <0.7 when uncertain."),
33
+ }),
34
+ }, async ({ issue_id, diagnosis, proposed_fix, code_snippet, affected_files, confidence, }) => {
35
+ if (!isApiConfigured()) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: JSON.stringify({ error: "API not configured" }),
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
46
+ const { data, error } = await apiPost("/remediations", {
47
+ issue_id,
48
+ diagnosis,
49
+ proposed_fix,
50
+ code_snippet,
51
+ affected_files,
52
+ confidence,
53
+ });
54
+ if (error) {
55
+ return {
56
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
57
+ isError: true,
58
+ };
59
+ }
60
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
61
+ });
62
+ server.registerTool("list_pending_fixes", {
63
+ description: "List all proposed fixes awaiting deployment. Use to show the user what fixes are ready to be deployed.",
64
+ inputSchema: z.object({}),
65
+ }, async () => {
66
+ if (!isApiConfigured()) {
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify({ error: "API not configured" }),
72
+ },
73
+ ],
74
+ isError: true,
75
+ };
76
+ }
77
+ const { data, error } = await apiGet("/remediations/pending");
78
+ if (error) {
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
81
+ isError: true,
82
+ };
83
+ }
84
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
85
+ });
86
+ server.registerTool("list_remediations", {
87
+ description: "List remediation records with optional filters. Use to review past and current fix attempts.",
88
+ inputSchema: z.object({
89
+ status: z
90
+ .enum([
91
+ "proposed",
92
+ "deployed",
93
+ "evaluating",
94
+ "succeeded",
95
+ "failed",
96
+ "reverted",
97
+ ])
98
+ .optional()
99
+ .describe("Filter by remediation status"),
100
+ issue_id: z.string().optional().describe("Filter by issue ID"),
101
+ page_path: z.string().optional().describe("Filter by page path"),
102
+ limit: z
103
+ .number()
104
+ .optional()
105
+ .default(20)
106
+ .describe("Maximum number of results (default: 20)"),
107
+ }),
108
+ }, async ({ status, issue_id, page_path, limit, }) => {
109
+ if (!isApiConfigured()) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: JSON.stringify({ error: "API not configured" }),
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+ const { data, error } = await apiGet("/remediations", {
121
+ status,
122
+ issue_id,
123
+ page_path,
124
+ limit: limit ?? 20,
125
+ });
126
+ if (error) {
127
+ return {
128
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
129
+ isError: true,
130
+ };
131
+ }
132
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
133
+ });
134
+ server.registerTool("confirm_deployment", {
135
+ description: "Confirm that a proposed fix has been deployed to production. This starts the evaluation timer. Call this when the user confirms they have deployed the fix.",
136
+ inputSchema: z.object({
137
+ remediation_id: z
138
+ .string()
139
+ .describe("The ID of the remediation to mark as deployed"),
140
+ }),
141
+ }, async ({ remediation_id }) => {
142
+ if (!isApiConfigured()) {
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify({ error: "API not configured" }),
148
+ },
149
+ ],
150
+ isError: true,
151
+ };
152
+ }
153
+ const { data, error } = await apiPatch(`/remediations/${remediation_id}/deploy`, {});
154
+ if (error) {
155
+ return {
156
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
157
+ isError: true,
158
+ };
159
+ }
160
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
161
+ });
162
+ server.registerTool("evaluate_fix", {
163
+ 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.",
164
+ inputSchema: z.object({
165
+ remediation_id: z
166
+ .string()
167
+ .describe("The ID of the remediation to evaluate"),
168
+ min_hours: z
169
+ .number()
170
+ .optional()
171
+ .default(24)
172
+ .describe("Minimum hours since deployment required (default: 24)"),
173
+ }),
174
+ }, async ({ remediation_id, min_hours, }) => {
175
+ if (!isApiConfigured()) {
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: JSON.stringify({ error: "API not configured" }),
181
+ },
182
+ ],
183
+ isError: true,
184
+ };
185
+ }
186
+ const { data, error } = await apiPost(`/remediations/${remediation_id}/evaluate`, { min_hours: min_hours ?? 24 });
187
+ if (error) {
188
+ return {
189
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
190
+ isError: true,
191
+ };
192
+ }
193
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
194
+ });
195
+ server.registerTool("get_fix_history", {
196
+ description: "Get the full remediation history for an issue. Shows all fix attempts, their outcomes, and current status. Use before proposing a new fix to learn from past attempts.",
197
+ inputSchema: z.object({
198
+ issue_id: z
199
+ .string()
200
+ .describe("The ID of the issue to get fix history for"),
201
+ }),
202
+ }, async ({ issue_id }) => {
203
+ if (!isApiConfigured()) {
204
+ return {
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: JSON.stringify({ error: "API not configured" }),
209
+ },
210
+ ],
211
+ isError: true,
212
+ };
213
+ }
214
+ const { data, error } = await apiGet(`/remediations/history/${issue_id}`);
215
+ if (error) {
216
+ return {
217
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
218
+ isError: true,
219
+ };
220
+ }
221
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
222
+ });
223
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Triage tools for self-healing workflow.
3
+ *
4
+ * Provides issue dismissal, marking intended behavior, and issue history.
5
+ */
6
+ export declare function registerTriageTools(server: any): void;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Triage tools for self-healing workflow.
3
+ *
4
+ * Provides issue dismissal, marking intended behavior, and issue history.
5
+ */
6
+ import { z } from "zod";
7
+ import { apiGet, apiPost, isApiConfigured } from "../api/client.js";
8
+ const DISMISS_REASONS = [
9
+ "stale",
10
+ "intended",
11
+ "duplicate",
12
+ "false_positive",
13
+ ];
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ export function registerTriageTools(server) {
16
+ server.registerTool("dismiss_issue", {
17
+ description: "Dismiss an issue as a false positive. Creates site knowledge to prevent re-flagging. Use when an issue is stale, intended behavior, a duplicate, or a false positive. Always confirm with user before dismissing.",
18
+ inputSchema: z.object({
19
+ issue_id: z.string().describe("The ID of the issue to dismiss"),
20
+ reason: z
21
+ .enum(DISMISS_REASONS)
22
+ .describe("Reason for dismissal: stale, intended, duplicate, or false_positive"),
23
+ explanation: z
24
+ .string()
25
+ .describe("Detailed explanation of why this issue is being dismissed"),
26
+ create_knowledge: z
27
+ .boolean()
28
+ .optional()
29
+ .default(true)
30
+ .describe("Whether to create site knowledge entry (default: true)"),
31
+ }),
32
+ }, async ({ issue_id, reason, explanation, create_knowledge, }) => {
33
+ if (!isApiConfigured()) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify({ error: "API not configured" }),
39
+ },
40
+ ],
41
+ isError: true,
42
+ };
43
+ }
44
+ const { data, error } = await apiPost(`/issues/${issue_id}/dismiss`, {
45
+ reason,
46
+ explanation,
47
+ create_knowledge: create_knowledge ?? true,
48
+ });
49
+ if (error) {
50
+ return {
51
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
52
+ isError: true,
53
+ };
54
+ }
55
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
56
+ });
57
+ server.registerTool("mark_intended_behavior", {
58
+ description: "Mark an issue as intended behavior. This is a shortcut for dismissing with reason 'intended' and always creates site knowledge. Use when the detected behavior is by design (e.g., disabled button clicks are expected).",
59
+ inputSchema: z.object({
60
+ issue_id: z
61
+ .string()
62
+ .describe("The ID of the issue to mark as intended"),
63
+ explanation: z
64
+ .string()
65
+ .describe("Explanation of why this behavior is intended"),
66
+ }),
67
+ }, async ({ issue_id, explanation, }) => {
68
+ if (!isApiConfigured()) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: JSON.stringify({ error: "API not configured" }),
74
+ },
75
+ ],
76
+ isError: true,
77
+ };
78
+ }
79
+ const { data, error } = await apiPost(`/issues/${issue_id}/mark-intended`, { explanation });
80
+ if (error) {
81
+ return {
82
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
83
+ isError: true,
84
+ };
85
+ }
86
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
87
+ });
88
+ server.registerTool("get_issue_history", {
89
+ description: "Get the full history of an issue including status changes, remediation attempts, and outcomes. Use to understand past fix attempts before proposing new ones.",
90
+ inputSchema: z.object({
91
+ issue_id: z.string().describe("The ID of the issue to get history for"),
92
+ }),
93
+ }, async ({ issue_id }) => {
94
+ if (!isApiConfigured()) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify({ error: "API not configured" }),
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+ const { data, error } = await apiGet(`/issues/${issue_id}/history`);
106
+ if (error) {
107
+ return {
108
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
109
+ isError: true,
110
+ };
111
+ }
112
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
113
+ });
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recapt/mcp",
3
- "version": "0.0.2-beta",
3
+ "version": "0.0.4-beta",
4
4
  "description": "MCP exposing recapt behavioral intelligence to AI coding agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",