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.
- package/README.md +28 -76
- package/dist/client.d.ts +5 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +157 -9
- package/dist/client.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +55 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +18 -10
- package/dist/server.js.map +1 -1
- package/dist/services/config-service.d.ts +27 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +100 -0
- package/dist/services/config-service.js.map +1 -0
- package/dist/testing/mock-server.d.ts.map +1 -1
- package/dist/testing/mock-server.js +13 -5
- package/dist/testing/mock-server.js.map +1 -1
- package/dist/tools/find-solution.d.ts.map +1 -1
- package/dist/tools/find-solution.js +74 -3
- package/dist/tools/find-solution.js.map +1 -1
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +53 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/publish-solution.d.ts.map +1 -1
- package/dist/tools/publish-solution.js +68 -3
- package/dist/tools/publish-solution.js.map +1 -1
- package/dist/tools/submit-feedback.d.ts.map +1 -1
- package/dist/tools/submit-feedback.js +62 -2
- package/dist/tools/submit-feedback.js.map +1 -1
- package/dist/tools/submit-verification.d.ts.map +1 -1
- package/dist/tools/submit-verification.js +62 -2
- package/dist/tools/submit-verification.js.map +1 -1
- package/dist/tools/unlock-solution.d.ts.map +1 -1
- package/dist/tools/unlock-solution.js +57 -2
- package/dist/tools/unlock-solution.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/verification-dialog.d.ts +1 -1
- package/dist/ui/verification-dialog.d.ts.map +1 -1
- package/dist/ui/verification-dialog.js +78 -4
- package/dist/ui/verification-dialog.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +177 -9
- package/src/config.ts +1 -1
- package/src/prompts/index.ts +67 -1
- package/src/server.ts +37 -19
- package/src/services/config-service.ts +119 -0
- package/src/testing/mock-server.ts +15 -7
- package/src/tools/find-solution.ts +75 -3
- package/src/tools/index.ts +70 -1
- package/src/tools/publish-solution.ts +71 -3
- package/src/tools/submit-feedback.ts +62 -2
- package/src/tools/submit-verification.ts +62 -2
- package/src/tools/unlock-solution.ts +56 -2
- package/src/types.ts +48 -0
- package/src/ui/verification-dialog.ts +84 -4
- package/E2E-TESTING.md +0 -195
- package/TROUBLESHOOTING.md +0 -219
- package/scripts/mock-server.js +0 -37
- 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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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://
|
|
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: {
|
package/src/prompts/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
//
|
|
35
|
-
this.addRoute('
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
-
|
|
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
|