cache-overflow-mcp 0.3.6 → 0.3.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 (65) hide show
  1. package/README.md +28 -76
  2. package/dist/client.d.ts +5 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +157 -9
  5. package/dist/client.js.map +1 -1
  6. package/dist/config.js +1 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/prompts/index.d.ts +6 -0
  9. package/dist/prompts/index.d.ts.map +1 -1
  10. package/dist/prompts/index.js +55 -1
  11. package/dist/prompts/index.js.map +1 -1
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +18 -10
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/config-service.d.ts +27 -0
  16. package/dist/services/config-service.d.ts.map +1 -0
  17. package/dist/services/config-service.js +100 -0
  18. package/dist/services/config-service.js.map +1 -0
  19. package/dist/testing/mock-server.d.ts.map +1 -1
  20. package/dist/testing/mock-server.js +13 -5
  21. package/dist/testing/mock-server.js.map +1 -1
  22. package/dist/tools/find-solution.d.ts.map +1 -1
  23. package/dist/tools/find-solution.js +74 -3
  24. package/dist/tools/find-solution.js.map +1 -1
  25. package/dist/tools/index.d.ts +6 -0
  26. package/dist/tools/index.d.ts.map +1 -1
  27. package/dist/tools/index.js +53 -1
  28. package/dist/tools/index.js.map +1 -1
  29. package/dist/tools/publish-solution.d.ts.map +1 -1
  30. package/dist/tools/publish-solution.js +68 -3
  31. package/dist/tools/publish-solution.js.map +1 -1
  32. package/dist/tools/submit-feedback.d.ts.map +1 -1
  33. package/dist/tools/submit-feedback.js +62 -2
  34. package/dist/tools/submit-feedback.js.map +1 -1
  35. package/dist/tools/submit-verification.d.ts.map +1 -1
  36. package/dist/tools/submit-verification.js +62 -2
  37. package/dist/tools/submit-verification.js.map +1 -1
  38. package/dist/tools/unlock-solution.d.ts.map +1 -1
  39. package/dist/tools/unlock-solution.js +57 -2
  40. package/dist/tools/unlock-solution.js.map +1 -1
  41. package/dist/types.d.ts +38 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/ui/verification-dialog.d.ts +1 -1
  44. package/dist/ui/verification-dialog.d.ts.map +1 -1
  45. package/dist/ui/verification-dialog.js +78 -4
  46. package/dist/ui/verification-dialog.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/client.ts +177 -9
  49. package/src/config.ts +1 -1
  50. package/src/prompts/index.ts +67 -1
  51. package/src/server.ts +37 -19
  52. package/src/services/config-service.ts +119 -0
  53. package/src/testing/mock-server.ts +15 -7
  54. package/src/tools/find-solution.ts +75 -3
  55. package/src/tools/index.ts +70 -1
  56. package/src/tools/publish-solution.ts +71 -3
  57. package/src/tools/submit-feedback.ts +62 -2
  58. package/src/tools/submit-verification.ts +62 -2
  59. package/src/tools/unlock-solution.ts +56 -2
  60. package/src/types.ts +48 -0
  61. package/src/ui/verification-dialog.ts +84 -4
  62. package/E2E-TESTING.md +0 -195
  63. package/TROUBLESHOOTING.md +0 -219
  64. package/scripts/mock-server.js +0 -37
  65. package/scripts/view-logs.js +0 -125
package/src/client.ts CHANGED
@@ -5,10 +5,32 @@ import { logger } from './logger.js';
5
5
  export class CacheOverflowClient {
6
6
  private apiUrl: string;
7
7
  private authToken: string | undefined;
8
+ private timeout: number;
8
9
 
9
- constructor(apiUrl?: string) {
10
+ constructor(apiUrl?: string, token?: string, timeout?: number) {
10
11
  this.apiUrl = apiUrl ?? config.api.url;
11
- this.authToken = config.auth.token;
12
+ this.authToken = token ?? config.auth.token;
13
+ this.timeout = timeout ?? config.api.timeout;
14
+
15
+ // Validate URL format (#13 - URL Validation)
16
+ try {
17
+ new URL(this.apiUrl);
18
+ } catch (error) {
19
+ logger.error('Invalid CACHE_OVERFLOW_URL format', error as Error, {
20
+ url: this.apiUrl,
21
+ errorType: 'INVALID_API_URL',
22
+ });
23
+ throw new Error(`Invalid API URL: ${this.apiUrl}`);
24
+ }
25
+
26
+ // Validate token format (#12 - Token Validation)
27
+ if (!this.authToken) {
28
+ logger.warn('No CACHE_OVERFLOW_TOKEN provided - all API calls will fail', {});
29
+ } else if (!this.authToken.startsWith('co_')) {
30
+ logger.warn('Invalid token format - tokens should start with "co_"', {
31
+ tokenPrefix: this.authToken.substring(0, 3),
32
+ });
33
+ }
12
34
  }
13
35
 
14
36
  private async request<T>(
@@ -26,11 +48,33 @@ export class CacheOverflowClient {
26
48
 
27
49
  const url = `${this.apiUrl}${path}`;
28
50
 
51
+ // #11 - Add request logging for debugging
52
+ logger.info('API request', {
53
+ method,
54
+ path,
55
+ hasBody: !!body,
56
+ });
57
+
58
+ // #1 - Enforce request timeouts
59
+ const controller = new AbortController();
60
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
61
+
29
62
  try {
30
63
  const response = await fetch(url, {
31
64
  method,
32
65
  headers,
33
66
  body: body ? JSON.stringify(body) : undefined,
67
+ signal: controller.signal,
68
+ });
69
+
70
+ clearTimeout(timeoutId);
71
+
72
+ // #11 - Add response logging
73
+ logger.info('API response', {
74
+ method,
75
+ path,
76
+ status: response.status,
77
+ ok: response.ok,
34
78
  });
35
79
 
36
80
  // Read response as text first, then try to parse as JSON
@@ -60,18 +104,66 @@ export class CacheOverflowClient {
60
104
 
61
105
  if (!response.ok) {
62
106
  const errorMessage = (data.error as string) ?? 'Unknown error';
107
+
108
+ // #7 - Detect and handle rate limiting (429)
109
+ if (response.status === 429) {
110
+ const retryAfter = response.headers.get('Retry-After') || '60';
111
+ logger.warn('Rate limit exceeded', {
112
+ method,
113
+ path,
114
+ retryAfter,
115
+ errorType: 'RATE_LIMIT',
116
+ });
117
+ return {
118
+ success: false,
119
+ error: `Rate limit exceeded. Please wait ${retryAfter} seconds before trying again. Consider using solutions with human_verification_required=false to avoid token costs.`,
120
+ };
121
+ }
122
+
123
+ // #8 - Add status code to error messages with categorization
124
+ let category = '';
125
+ if (response.status >= 500) {
126
+ category = 'Server error - try again later';
127
+ } else if (response.status === 401 || response.status === 403) {
128
+ category = 'Authentication error - check your CACHE_OVERFLOW_TOKEN';
129
+ } else if (response.status >= 400) {
130
+ category = 'Request error - check your input';
131
+ }
132
+
63
133
  logger.error('API request failed', undefined, {
64
134
  method,
65
135
  path,
66
136
  statusCode: response.status,
67
137
  errorMessage,
138
+ category,
68
139
  errorType: 'API_ERROR',
69
140
  });
70
- return { success: false, error: errorMessage };
141
+
142
+ return {
143
+ success: false,
144
+ error: `${errorMessage} (${category})`,
145
+ };
71
146
  }
72
147
 
73
148
  return { success: true, data: data as T };
74
149
  } catch (error) {
150
+ clearTimeout(timeoutId);
151
+
152
+ // #1 - Handle timeout errors specifically
153
+ if ((error as Error).name === 'AbortError') {
154
+ logger.error('Request timed out', error as Error, {
155
+ method,
156
+ path,
157
+ url,
158
+ timeout: this.timeout,
159
+ errorType: 'TIMEOUT',
160
+ });
161
+ return {
162
+ success: false,
163
+ error: `Request timed out after ${this.timeout}ms. The server may be experiencing issues.`,
164
+ };
165
+ }
166
+
75
167
  logger.error('Network or fetch error during API request', error as Error, {
76
168
  method,
77
169
  path,
@@ -79,13 +171,89 @@ export class CacheOverflowClient {
79
171
  errorType: 'NETWORK_ERROR',
80
172
  });
81
173
 
82
- // Re-throw network errors so they can be handled by the caller
174
+ // Re-throw network errors so they can be handled by retry logic
83
175
  throw error;
84
176
  }
85
177
  }
86
178
 
179
+ // #2 - Add retry logic for network failures
180
+ private async requestWithRetry<T>(
181
+ method: string,
182
+ path: string,
183
+ body?: unknown,
184
+ retries = 3
185
+ ): Promise<ApiResponse<T>> {
186
+ let lastError: Error | undefined;
187
+
188
+ for (let attempt = 0; attempt < retries; attempt++) {
189
+ try {
190
+ const result = await this.request<T>(method, path, body);
191
+
192
+ // If successful or last attempt, return the result
193
+ if (result.success || attempt === retries - 1) {
194
+ return result;
195
+ }
196
+
197
+ // Check if we should retry based on the error
198
+ const shouldRetry = this.shouldRetryError(result.error);
199
+ if (shouldRetry) {
200
+ const delay = Math.pow(3, attempt) * 100; // 0, 300, 900ms exponential backoff
201
+ logger.info('Retrying request after delay', {
202
+ method,
203
+ path,
204
+ attempt: attempt + 1,
205
+ maxRetries: retries,
206
+ delay,
207
+ });
208
+ await new Promise(resolve => setTimeout(resolve, delay));
209
+ continue;
210
+ }
211
+
212
+ // Don't retry client errors (4xx)
213
+ return result;
214
+ } catch (error) {
215
+ lastError = error as Error;
216
+
217
+ // Only retry network errors, not client errors
218
+ if (attempt < retries - 1 && this.isNetworkError(error as Error)) {
219
+ const delay = Math.pow(3, attempt) * 100;
220
+ logger.info('Retrying after network error', {
221
+ method,
222
+ path,
223
+ attempt: attempt + 1,
224
+ maxRetries: retries,
225
+ delay,
226
+ error: (error as Error).message,
227
+ });
228
+ await new Promise(resolve => setTimeout(resolve, delay));
229
+ } else {
230
+ throw error;
231
+ }
232
+ }
233
+ }
234
+
235
+ throw lastError;
236
+ }
237
+
238
+ private shouldRetryError(error?: string): boolean {
239
+ if (!error) return false;
240
+
241
+ // Retry on server errors (5xx) or timeout errors
242
+ return error.includes('Server error') ||
243
+ error.includes('timed out') ||
244
+ error.includes('network');
245
+ }
246
+
247
+ private isNetworkError(error: Error): boolean {
248
+ // Network errors typically have these names or messages
249
+ return error.name === 'TypeError' ||
250
+ error.name === 'NetworkError' ||
251
+ error.message.includes('fetch') ||
252
+ error.message.includes('network');
253
+ }
254
+
87
255
  async findSolution(query: string): Promise<ApiResponse<FindSolutionResult[]>> {
88
- const result = await this.request<Array<{ id: string; query_title: string; solution_body?: string; human_verification_required: boolean }>>(
256
+ const result = await this.requestWithRetry<Array<{ id: string; query_title: string; solution_body?: string; human_verification_required: boolean }>>(
89
257
  'GET',
90
258
  `/solutions/search?query=${encodeURIComponent(query)}`
91
259
  );
@@ -105,14 +273,14 @@ export class CacheOverflowClient {
105
273
  }
106
274
 
107
275
  async unlockSolution(solutionId: string): Promise<ApiResponse<Solution>> {
108
- return this.request('POST', `/solutions/${solutionId}/unlock`);
276
+ return this.requestWithRetry('POST', `/solutions/${solutionId}/unlock`);
109
277
  }
110
278
 
111
279
  async publishSolution(
112
280
  queryTitle: string,
113
281
  solutionBody: string
114
282
  ): Promise<ApiResponse<Solution>> {
115
- return this.request('POST', '/solutions', {
283
+ return this.requestWithRetry('POST', '/solutions', {
116
284
  query_title: queryTitle,
117
285
  solution_body: solutionBody,
118
286
  });
@@ -122,7 +290,7 @@ export class CacheOverflowClient {
122
290
  solutionId: string,
123
291
  isSafe: boolean
124
292
  ): Promise<ApiResponse<void>> {
125
- return this.request('POST', `/solutions/${solutionId}/verify`, {
293
+ return this.requestWithRetry('POST', `/solutions/${solutionId}/verify`, {
126
294
  is_safe: isSafe,
127
295
  });
128
296
  }
@@ -131,7 +299,7 @@ export class CacheOverflowClient {
131
299
  solutionId: string,
132
300
  isUseful: boolean
133
301
  ): Promise<ApiResponse<void>> {
134
- return this.request('POST', `/solutions/${solutionId}/feedback`, {
302
+ return this.requestWithRetry('POST', `/solutions/${solutionId}/feedback`, {
135
303
  is_useful: isUseful,
136
304
  });
137
305
  }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export const config = {
2
2
  api: {
3
- url: process.env.CACHE_OVERFLOW_URL ?? 'https://cache-overflow.onrender.com/api',
3
+ url: process.env.CACHE_OVERFLOW_URL ?? 'https://cacheoverflow.dev/api',
4
4
  timeout: parseInt(process.env.CACHE_OVERFLOW_TIMEOUT ?? '30000'),
5
5
  },
6
6
  auth: {
@@ -1,4 +1,7 @@
1
1
  import { Prompt, TextContent } from '@modelcontextprotocol/sdk/types.js';
2
+ import { configService } from '../services/config-service.js';
3
+ import { logger } from '../logger.js';
4
+ import type { RemotePromptDefinition } from '../types.js';
2
5
 
3
6
  export interface PromptDefinition {
4
7
  definition: Prompt;
@@ -165,4 +168,67 @@ Then call \`publish_solution\` to share it with other agents!
165
168
  }),
166
169
  };
167
170
 
168
- export const prompts: PromptDefinition[] = [publishGuidancePrompt, workflowGuidancePrompt];
171
+ // Fallback prompts used when backend is unavailable
172
+ export const FALLBACK_PROMPTS: PromptDefinition[] = [
173
+ publishGuidancePrompt,
174
+ workflowGuidancePrompt,
175
+ ];
176
+
177
+ /**
178
+ * Convert a remote prompt definition to a local Prompt format
179
+ */
180
+ function remoteToLocalPrompt(remote: RemotePromptDefinition): Prompt {
181
+ return {
182
+ name: remote.name,
183
+ description: remote.description,
184
+ arguments: remote.arguments.map((arg) => ({
185
+ name: arg.name,
186
+ description: arg.description,
187
+ required: arg.required,
188
+ })),
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Create a handler for a remote prompt definition.
194
+ * The handler returns the pre-defined messages from the remote config.
195
+ */
196
+ function createRemotePromptHandler(
197
+ remote: RemotePromptDefinition
198
+ ): PromptDefinition['handler'] {
199
+ return async () => ({
200
+ messages: remote.messages.map((msg) => ({
201
+ role: msg.role as 'user' | 'assistant',
202
+ content: {
203
+ type: msg.content.type as 'text',
204
+ text: msg.content.text,
205
+ },
206
+ })),
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Get prompts with definitions from the backend API.
212
+ * Falls back to hardcoded definitions if the backend is unavailable.
213
+ */
214
+ export async function getPrompts(): Promise<PromptDefinition[]> {
215
+ const remoteConfig = await configService.fetchConfig();
216
+
217
+ if (!remoteConfig) {
218
+ logger.info('Using fallback prompt definitions');
219
+ return FALLBACK_PROMPTS;
220
+ }
221
+
222
+ const prompts: PromptDefinition[] = remoteConfig.prompts.map(
223
+ (remotePrompt) => ({
224
+ definition: remoteToLocalPrompt(remotePrompt),
225
+ handler: createRemotePromptHandler(remotePrompt),
226
+ })
227
+ );
228
+
229
+ return prompts;
230
+ }
231
+
232
+ // Keep backward compatibility - export prompts array for existing code
233
+ // Note: This is the fallback, prefer using getPrompts() for dynamic loading
234
+ export const prompts: PromptDefinition[] = FALLBACK_PROMPTS;
package/src/server.ts CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  GetPromptRequestSchema,
8
8
  } from '@modelcontextprotocol/sdk/types.js';
9
9
  import { CacheOverflowClient } from './client.js';
10
- import { tools } from './tools/index.js';
11
- import { prompts } from './prompts/index.js';
10
+ import { getTools } from './tools/index.js';
11
+ import { getPrompts } from './prompts/index.js';
12
12
  import { logger } from './logger.js';
13
13
 
14
14
  export class CacheOverflowServer {
@@ -35,17 +35,21 @@ export class CacheOverflowServer {
35
35
 
36
36
  private setupHandlers(): void {
37
37
  // Tool handlers
38
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
39
- tools: tools.map((t) => t.definition),
40
- }));
38
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
39
+ const tools = await getTools();
40
+ return {
41
+ tools: tools.map((t) => t.definition),
42
+ };
43
+ });
41
44
 
42
45
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
46
+ const tools = await getTools();
43
47
  const tool = tools.find((t) => t.definition.name === request.params.name);
44
48
  if (!tool) {
45
49
  const error = new Error(`Unknown tool: ${request.params.name}`);
46
50
  logger.error('Unknown tool requested', error, {
47
51
  toolName: request.params.name,
48
- availableTools: tools.map(t => t.definition.name),
52
+ availableTools: tools.map((t) => t.definition.name),
49
53
  });
50
54
  throw error;
51
55
  }
@@ -58,26 +62,36 @@ export class CacheOverflowServer {
58
62
  });
59
63
  return await tool.handler(request.params.arguments ?? {}, this.client);
60
64
  } catch (error) {
61
- logger.error(`Tool execution failed: ${request.params.name}`, error as Error, {
62
- toolName: request.params.name,
63
- errorType: 'TOOL_EXECUTION_FAILURE',
64
- });
65
+ logger.error(
66
+ `Tool execution failed: ${request.params.name}`,
67
+ error as Error,
68
+ {
69
+ toolName: request.params.name,
70
+ errorType: 'TOOL_EXECUTION_FAILURE',
71
+ }
72
+ );
65
73
  throw error;
66
74
  }
67
75
  });
68
76
 
69
77
  // Prompt handlers
70
- this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
71
- prompts: prompts.map((p) => p.definition),
72
- }));
78
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
79
+ const prompts = await getPrompts();
80
+ return {
81
+ prompts: prompts.map((p) => p.definition),
82
+ };
83
+ });
73
84
 
74
85
  this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
75
- const prompt = prompts.find((p) => p.definition.name === request.params.name);
86
+ const prompts = await getPrompts();
87
+ const prompt = prompts.find(
88
+ (p) => p.definition.name === request.params.name
89
+ );
76
90
  if (!prompt) {
77
91
  const error = new Error(`Unknown prompt: ${request.params.name}`);
78
92
  logger.error('Unknown prompt requested', error, {
79
93
  promptName: request.params.name,
80
- availablePrompts: prompts.map(p => p.definition.name),
94
+ availablePrompts: prompts.map((p) => p.definition.name),
81
95
  });
82
96
  throw error;
83
97
  }
@@ -85,10 +99,14 @@ export class CacheOverflowServer {
85
99
  try {
86
100
  return await prompt.handler(request.params.arguments ?? {});
87
101
  } catch (error) {
88
- logger.error(`Prompt execution failed: ${request.params.name}`, error as Error, {
89
- promptName: request.params.name,
90
- errorType: 'PROMPT_EXECUTION_FAILURE',
91
- });
102
+ logger.error(
103
+ `Prompt execution failed: ${request.params.name}`,
104
+ error as Error,
105
+ {
106
+ promptName: request.params.name,
107
+ errorType: 'PROMPT_EXECUTION_FAILURE',
108
+ }
109
+ );
92
110
  throw error;
93
111
  }
94
112
  });
@@ -0,0 +1,119 @@
1
+ import { config } from '../config.js';
2
+ import { logger } from '../logger.js';
3
+ import type { McpConfigResponse } from '../types.js';
4
+
5
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
6
+ const FETCH_TIMEOUT_MS = 5000; // 5 seconds
7
+ const SUPPORTED_SCHEMA_VERSION = '1.0.0';
8
+
9
+ interface CachedConfig {
10
+ data: McpConfigResponse;
11
+ fetchedAt: number;
12
+ }
13
+
14
+ class ConfigService {
15
+ private cache: CachedConfig | null = null;
16
+
17
+ /**
18
+ * Fetch MCP configuration from the backend API.
19
+ * Returns cached value if available and not expired.
20
+ * Falls back to null on failure (caller should use fallback).
21
+ */
22
+ async fetchConfig(): Promise<McpConfigResponse | null> {
23
+ // Return cached value if still valid
24
+ if (this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL_MS) {
25
+ return this.cache.data;
26
+ }
27
+
28
+ try {
29
+ const controller = new AbortController();
30
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
31
+
32
+ const response = await fetch(`${config.api.url}/mcp/config`, {
33
+ method: 'GET',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ signal: controller.signal,
38
+ });
39
+
40
+ clearTimeout(timeoutId);
41
+
42
+ if (!response.ok) {
43
+ logger.warn('Failed to fetch MCP config', {
44
+ status: response.status,
45
+ statusText: response.statusText,
46
+ });
47
+ return this.getCachedOrNull();
48
+ }
49
+
50
+ const data = (await response.json()) as McpConfigResponse;
51
+
52
+ // Validate schema version
53
+ if (!this.isSchemaVersionCompatible(data.schema_version)) {
54
+ logger.warn('MCP config schema version mismatch', {
55
+ expected: SUPPORTED_SCHEMA_VERSION,
56
+ received: data.schema_version,
57
+ });
58
+ return this.getCachedOrNull();
59
+ }
60
+
61
+ // Update cache
62
+ this.cache = {
63
+ data,
64
+ fetchedAt: Date.now(),
65
+ };
66
+
67
+ logger.info('Fetched MCP config from backend', {
68
+ schemaVersion: data.schema_version,
69
+ toolCount: data.tools.length,
70
+ promptCount: data.prompts.length,
71
+ });
72
+
73
+ return data;
74
+ } catch (error) {
75
+ if (error instanceof Error) {
76
+ if (error.name === 'AbortError') {
77
+ logger.warn('MCP config fetch timed out');
78
+ } else {
79
+ logger.warn('Failed to fetch MCP config', {
80
+ error: error.message,
81
+ });
82
+ }
83
+ }
84
+ return this.getCachedOrNull();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if the schema version is compatible.
90
+ * For now, we only support exact match.
91
+ * Later can implement semantic version comparison.
92
+ */
93
+ private isSchemaVersionCompatible(version: string): boolean {
94
+ // Extract major version for compatibility check
95
+ const [major] = version.split('.');
96
+ const [supportedMajor] = SUPPORTED_SCHEMA_VERSION.split('.');
97
+ return major === supportedMajor;
98
+ }
99
+
100
+ /**
101
+ * Return cached value if available, otherwise null.
102
+ */
103
+ private getCachedOrNull(): McpConfigResponse | null {
104
+ if (this.cache) {
105
+ return this.cache.data;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Invalidate the cache, forcing a fresh fetch on next request.
112
+ */
113
+ invalidateCache(): void {
114
+ this.cache = null;
115
+ }
116
+ }
117
+
118
+ // Export singleton instance
119
+ export const configService = new ConfigService();
@@ -1,6 +1,6 @@
1
1
  import * as http from 'node:http';
2
2
  import { mockSolutions, mockFindResults, createMockSolution } from './mock-data.js';
3
- import type { Solution, FindSolutionResult } from '../types.js';
3
+ import type { Solution } from '../types.js';
4
4
 
5
5
  interface RouteHandler {
6
6
  (
@@ -31,13 +31,21 @@ export class MockServer {
31
31
  }
32
32
 
33
33
  private setupRoutes(): void {
34
- // POST /solutions/find
35
- this.addRoute('POST', '/solutions/find', (_req, body) => {
36
- const { query } = body as { query: string };
37
- const results: FindSolutionResult[] = mockFindResults.filter((r) =>
38
- r.query_title.toLowerCase().includes((query ?? '').toLowerCase())
34
+ // GET /solutions/search - matches client's findSolution API
35
+ this.addRoute('GET', '/solutions/search', (req) => {
36
+ const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
37
+ const query = url.searchParams.get('query') ?? '';
38
+ const results = mockFindResults.filter((r) =>
39
+ r.query_title.toLowerCase().includes(query.toLowerCase())
39
40
  );
40
- return { status: 200, data: results.length > 0 ? results : mockFindResults };
41
+ // Map solution_id to id to match API response format
42
+ const mappedResults = (results.length > 0 ? results : mockFindResults).map((r) => ({
43
+ id: r.solution_id,
44
+ query_title: r.query_title,
45
+ solution_body: r.solution_body,
46
+ human_verification_required: r.human_verification_required,
47
+ }));
48
+ return { status: 200, data: mappedResults };
41
49
  });
42
50
 
43
51
  // POST /solutions/:id/unlock