cache-overflow-mcp 0.3.3 → 0.3.5

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 (43) hide show
  1. package/.env.example +3 -3
  2. package/AGENTS.md +24 -3
  3. package/README.md +59 -0
  4. package/TROUBLESHOOTING.md +219 -0
  5. package/dist/cli.js +13 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +54 -10
  9. package/dist/client.js.map +1 -1
  10. package/dist/config.d.ts +3 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +5 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/logger.d.ts +16 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/logger.js +127 -0
  17. package/dist/logger.js.map +1 -0
  18. package/dist/server.d.ts.map +1 -1
  19. package/dist/server.js +50 -7
  20. package/dist/server.js.map +1 -1
  21. package/dist/testing/mock-data.js +40 -40
  22. package/dist/ui/verification-dialog.d.ts.map +1 -1
  23. package/dist/ui/verification-dialog.js +307 -268
  24. package/dist/ui/verification-dialog.js.map +1 -1
  25. package/package.json +3 -2
  26. package/scripts/view-logs.js +125 -0
  27. package/src/cli.ts +23 -10
  28. package/src/client.test.ts +116 -116
  29. package/src/client.ts +122 -76
  30. package/src/config.ts +14 -9
  31. package/src/index.ts +3 -3
  32. package/src/logger.ts +150 -0
  33. package/src/server.ts +49 -7
  34. package/src/testing/mock-data.ts +142 -142
  35. package/src/testing/mock-server.ts +176 -176
  36. package/src/tools/index.ts +23 -23
  37. package/src/types.ts +39 -39
  38. package/src/ui/verification-dialog.ts +382 -342
  39. package/tsconfig.json +20 -20
  40. package/dist/tools/get-balance.d.ts +0 -3
  41. package/dist/tools/get-balance.d.ts.map +0 -1
  42. package/dist/tools/get-balance.js +0 -34
  43. package/dist/tools/get-balance.js.map +0 -1
package/src/logger.ts ADDED
@@ -0,0 +1,150 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ interface LogEntry {
6
+ timestamp: string;
7
+ level: 'ERROR' | 'WARN' | 'INFO';
8
+ message: string;
9
+ error?: {
10
+ name: string;
11
+ message: string;
12
+ stack?: string;
13
+ };
14
+ context?: Record<string, unknown>;
15
+ }
16
+
17
+ class Logger {
18
+ private logFilePath: string;
19
+ private maxLogSizeBytes = 5 * 1024 * 1024; // 5MB max log file size
20
+
21
+ constructor() {
22
+ // Determine log file location
23
+ const logDir = process.env.CACHE_OVERFLOW_LOG_DIR ?? path.join(os.homedir(), '.cache-overflow');
24
+
25
+ // Ensure log directory exists
26
+ if (!fs.existsSync(logDir)) {
27
+ try {
28
+ fs.mkdirSync(logDir, { recursive: true });
29
+ } catch (err) {
30
+ // Fallback to temp directory if home directory is not writable
31
+ const tempLogDir = path.join(os.tmpdir(), 'cache-overflow');
32
+ if (!fs.existsSync(tempLogDir)) {
33
+ fs.mkdirSync(tempLogDir, { recursive: true });
34
+ }
35
+ this.logFilePath = path.join(tempLogDir, 'cache-overflow-mcp.log');
36
+ return;
37
+ }
38
+ }
39
+
40
+ this.logFilePath = path.join(logDir, 'cache-overflow-mcp.log');
41
+ }
42
+
43
+ private rotateLogIfNeeded(): void {
44
+ try {
45
+ if (fs.existsSync(this.logFilePath)) {
46
+ const stats = fs.statSync(this.logFilePath);
47
+ if (stats.size >= this.maxLogSizeBytes) {
48
+ // Rotate: keep the last 1MB of the file
49
+ const content = fs.readFileSync(this.logFilePath, 'utf-8');
50
+ const lines = content.split('\n');
51
+ const keptLines = lines.slice(-1000); // Keep last 1000 lines
52
+ fs.writeFileSync(this.logFilePath, keptLines.join('\n') + '\n');
53
+ }
54
+ }
55
+ } catch (err) {
56
+ // Ignore rotation errors - don't want logging to break the app
57
+ }
58
+ }
59
+
60
+ private sanitizeContext(context?: Record<string, unknown>): Record<string, unknown> | undefined {
61
+ if (!context) return undefined;
62
+
63
+ const sanitized: Record<string, unknown> = {};
64
+ for (const [key, value] of Object.entries(context)) {
65
+ // Sanitize sensitive fields
66
+ if (key.toLowerCase().includes('token') ||
67
+ key.toLowerCase().includes('password') ||
68
+ key.toLowerCase().includes('secret') ||
69
+ key.toLowerCase().includes('auth')) {
70
+ sanitized[key] = '[REDACTED]';
71
+ } else if (typeof value === 'object' && value !== null) {
72
+ sanitized[key] = this.sanitizeContext(value as Record<string, unknown>);
73
+ } else {
74
+ sanitized[key] = value;
75
+ }
76
+ }
77
+ return sanitized;
78
+ }
79
+
80
+ private writeLog(entry: LogEntry): void {
81
+ try {
82
+ this.rotateLogIfNeeded();
83
+ const logLine = JSON.stringify(entry) + '\n';
84
+ fs.appendFileSync(this.logFilePath, logLine);
85
+ } catch (err) {
86
+ // If we can't write to log file, write to stderr as fallback
87
+ console.error('Failed to write to log file:', err);
88
+ console.error('Original log entry:', entry);
89
+ }
90
+ }
91
+
92
+ error(message: string, error?: Error, context?: Record<string, unknown>): void {
93
+ const entry: LogEntry = {
94
+ timestamp: new Date().toISOString(),
95
+ level: 'ERROR',
96
+ message,
97
+ context: this.sanitizeContext(context),
98
+ };
99
+
100
+ if (error) {
101
+ entry.error = {
102
+ name: error.name,
103
+ message: error.message,
104
+ stack: error.stack,
105
+ };
106
+ }
107
+
108
+ this.writeLog(entry);
109
+ }
110
+
111
+ warn(message: string, context?: Record<string, unknown>): void {
112
+ const entry: LogEntry = {
113
+ timestamp: new Date().toISOString(),
114
+ level: 'WARN',
115
+ message,
116
+ context: this.sanitizeContext(context),
117
+ };
118
+
119
+ this.writeLog(entry);
120
+ }
121
+
122
+ info(message: string, context?: Record<string, unknown>): void {
123
+ const entry: LogEntry = {
124
+ timestamp: new Date().toISOString(),
125
+ level: 'INFO',
126
+ message,
127
+ context: this.sanitizeContext(context),
128
+ };
129
+
130
+ this.writeLog(entry);
131
+ }
132
+
133
+ getLogFilePath(): string {
134
+ return this.logFilePath;
135
+ }
136
+
137
+ logStartup(): void {
138
+ this.info('MCP Server starting', {
139
+ version: '0.3.5',
140
+ nodeVersion: process.version,
141
+ platform: process.platform,
142
+ arch: process.arch,
143
+ apiUrl: process.env.CACHE_OVERFLOW_URL ?? 'https://cache-overflow.onrender.com/api',
144
+ hasAuthToken: !!process.env.CACHE_OVERFLOW_TOKEN,
145
+ });
146
+ }
147
+ }
148
+
149
+ // Singleton instance
150
+ export const logger = new Logger();
package/src/server.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { CacheOverflowClient } from './client.js';
10
10
  import { tools } from './tools/index.js';
11
11
  import { prompts } from './prompts/index.js';
12
+ import { logger } from './logger.js';
12
13
 
13
14
  export class CacheOverflowServer {
14
15
  private server: Server;
@@ -18,7 +19,7 @@ export class CacheOverflowServer {
18
19
  this.server = new Server(
19
20
  {
20
21
  name: 'cache-overflow',
21
- version: '0.3.0',
22
+ version: '0.3.5',
22
23
  },
23
24
  {
24
25
  capabilities: {
@@ -41,9 +42,28 @@ export class CacheOverflowServer {
41
42
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
42
43
  const tool = tools.find((t) => t.definition.name === request.params.name);
43
44
  if (!tool) {
44
- throw new Error(`Unknown tool: ${request.params.name}`);
45
+ const error = new Error(`Unknown tool: ${request.params.name}`);
46
+ logger.error('Unknown tool requested', error, {
47
+ toolName: request.params.name,
48
+ availableTools: tools.map(t => t.definition.name),
49
+ });
50
+ throw error;
51
+ }
52
+
53
+ try {
54
+ logger.info(`Executing tool: ${request.params.name}`, {
55
+ toolName: request.params.name,
56
+ // Don't log full arguments as they might contain sensitive data
57
+ hasArguments: Object.keys(request.params.arguments ?? {}).length > 0,
58
+ });
59
+ return await tool.handler(request.params.arguments ?? {}, this.client);
60
+ } 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
+ throw error;
45
66
  }
46
- return tool.handler(request.params.arguments ?? {}, this.client);
47
67
  });
48
68
 
49
69
  // Prompt handlers
@@ -54,14 +74,36 @@ export class CacheOverflowServer {
54
74
  this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
55
75
  const prompt = prompts.find((p) => p.definition.name === request.params.name);
56
76
  if (!prompt) {
57
- throw new Error(`Unknown prompt: ${request.params.name}`);
77
+ const error = new Error(`Unknown prompt: ${request.params.name}`);
78
+ logger.error('Unknown prompt requested', error, {
79
+ promptName: request.params.name,
80
+ availablePrompts: prompts.map(p => p.definition.name),
81
+ });
82
+ throw error;
83
+ }
84
+
85
+ try {
86
+ return await prompt.handler(request.params.arguments ?? {});
87
+ } 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
+ });
92
+ throw error;
58
93
  }
59
- return prompt.handler(request.params.arguments ?? {});
60
94
  });
61
95
  }
62
96
 
63
97
  async start(): Promise<void> {
64
- const transport = new StdioServerTransport();
65
- await this.server.connect(transport);
98
+ try {
99
+ const transport = new StdioServerTransport();
100
+ await this.server.connect(transport);
101
+ logger.info('MCP server connected successfully via stdio transport');
102
+ } catch (error) {
103
+ logger.error('Failed to connect MCP server', error as Error, {
104
+ errorType: 'CONNECTION_FAILURE',
105
+ });
106
+ throw error;
107
+ }
66
108
  }
67
109
  }
@@ -1,142 +1,142 @@
1
- import type { Solution, FindSolutionResult, Balance } from '../types.js';
2
-
3
- export const mockSolutions: Solution[] = [
4
- {
5
- id: 'sol_001',
6
- author_id: 'user_123',
7
- query_title: 'How to implement binary search in TypeScript',
8
- solution_body: `function binarySearch<T>(arr: T[], target: T): number {
9
- let left = 0;
10
- let right = arr.length - 1;
11
- while (left <= right) {
12
- const mid = Math.floor((left + right) / 2);
13
- if (arr[mid] === target) return mid;
14
- if (arr[mid] < target) left = mid + 1;
15
- else right = mid - 1;
16
- }
17
- return -1;
18
- }`,
19
- price_current: 50,
20
- verification_state: 'VERIFIED',
21
- access_count: 127,
22
- upvotes: 45,
23
- downvotes: 2,
24
- },
25
- {
26
- id: 'sol_002',
27
- author_id: 'user_456',
28
- query_title: 'Fix memory leak in Node.js event listeners',
29
- solution_body: `// Always remove event listeners when done
30
- const handler = () => { /* ... */ };
31
- emitter.on('event', handler);
32
- // Later:
33
- emitter.off('event', handler);
34
-
35
- // Or use once() for one-time listeners
36
- emitter.once('event', () => { /* ... */ });`,
37
- price_current: 75,
38
- verification_state: 'VERIFIED',
39
- access_count: 89,
40
- upvotes: 32,
41
- downvotes: 1,
42
- },
43
- {
44
- id: 'sol_003',
45
- author_id: 'user_789',
46
- query_title: 'Optimize React re-renders with useMemo',
47
- solution_body: `import { useMemo } from 'react';
48
-
49
- function ExpensiveComponent({ data }) {
50
- const processed = useMemo(() => {
51
- return data.map(item => heavyComputation(item));
52
- }, [data]);
53
-
54
- return <div>{processed}</div>;
55
- }`,
56
- price_current: 60,
57
- verification_state: 'PENDING',
58
- access_count: 15,
59
- upvotes: 8,
60
- downvotes: 0,
61
- },
62
- ];
63
-
64
- export const mockFindResults: FindSolutionResult[] = [
65
- {
66
- solution_id: 'sol_001',
67
- query_title: 'How to implement binary search in TypeScript',
68
- human_verification_required: false,
69
- },
70
- {
71
- solution_id: 'sol_002',
72
- query_title: 'Fix memory leak in Node.js event listeners',
73
- solution_body: `// Always remove event listeners when done
74
- const handler = () => { /* ... */ };
75
- emitter.on('event', handler);
76
- // Later:
77
- emitter.off('event', handler);
78
-
79
- // Or use once() for one-time listeners
80
- emitter.once('event', () => { /* ... */ });`,
81
- human_verification_required: true,
82
- },
83
- {
84
- solution_id: 'sol_003',
85
- query_title: 'Optimize React re-renders with useMemo',
86
- solution_body: `import { useMemo } from 'react';
87
-
88
- function ExpensiveComponent({ data }) {
89
- const processed = useMemo(() => {
90
- return data.map(item => heavyComputation(item));
91
- }, [data]);
92
-
93
- return <div>{processed}</div>;
94
- }`,
95
- human_verification_required: false,
96
- },
97
- ];
98
-
99
- export const mockBalance: Balance = {
100
- available: 1500,
101
- pending_debits: 75,
102
- pending_credits: 200,
103
- total_earned: 3500,
104
- total_spent: 1800,
105
- };
106
-
107
- export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
108
- return {
109
- id: `sol_${Date.now()}`,
110
- author_id: 'user_mock',
111
- query_title: 'Mock solution title',
112
- solution_body: 'Mock solution body content',
113
- price_current: 50,
114
- verification_state: 'PENDING',
115
- access_count: 0,
116
- upvotes: 0,
117
- downvotes: 0,
118
- ...overrides,
119
- };
120
- }
121
-
122
- export function createMockFindResult(
123
- overrides: Partial<FindSolutionResult> = {}
124
- ): FindSolutionResult {
125
- return {
126
- solution_id: `sol_${Date.now()}`,
127
- query_title: 'Mock query title',
128
- human_verification_required: false,
129
- ...overrides,
130
- };
131
- }
132
-
133
- export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
134
- return {
135
- available: 1000,
136
- pending_debits: 0,
137
- pending_credits: 0,
138
- total_earned: 1000,
139
- total_spent: 0,
140
- ...overrides,
141
- };
142
- }
1
+ import type { Solution, FindSolutionResult, Balance } from '../types.js';
2
+
3
+ export const mockSolutions: Solution[] = [
4
+ {
5
+ id: 'sol_001',
6
+ author_id: 'user_123',
7
+ query_title: 'How to implement binary search in TypeScript',
8
+ solution_body: `function binarySearch<T>(arr: T[], target: T): number {
9
+ let left = 0;
10
+ let right = arr.length - 1;
11
+ while (left <= right) {
12
+ const mid = Math.floor((left + right) / 2);
13
+ if (arr[mid] === target) return mid;
14
+ if (arr[mid] < target) left = mid + 1;
15
+ else right = mid - 1;
16
+ }
17
+ return -1;
18
+ }`,
19
+ price_current: 50,
20
+ verification_state: 'VERIFIED',
21
+ access_count: 127,
22
+ upvotes: 45,
23
+ downvotes: 2,
24
+ },
25
+ {
26
+ id: 'sol_002',
27
+ author_id: 'user_456',
28
+ query_title: 'Fix memory leak in Node.js event listeners',
29
+ solution_body: `// Always remove event listeners when done
30
+ const handler = () => { /* ... */ };
31
+ emitter.on('event', handler);
32
+ // Later:
33
+ emitter.off('event', handler);
34
+
35
+ // Or use once() for one-time listeners
36
+ emitter.once('event', () => { /* ... */ });`,
37
+ price_current: 75,
38
+ verification_state: 'VERIFIED',
39
+ access_count: 89,
40
+ upvotes: 32,
41
+ downvotes: 1,
42
+ },
43
+ {
44
+ id: 'sol_003',
45
+ author_id: 'user_789',
46
+ query_title: 'Optimize React re-renders with useMemo',
47
+ solution_body: `import { useMemo } from 'react';
48
+
49
+ function ExpensiveComponent({ data }) {
50
+ const processed = useMemo(() => {
51
+ return data.map(item => heavyComputation(item));
52
+ }, [data]);
53
+
54
+ return <div>{processed}</div>;
55
+ }`,
56
+ price_current: 60,
57
+ verification_state: 'PENDING',
58
+ access_count: 15,
59
+ upvotes: 8,
60
+ downvotes: 0,
61
+ },
62
+ ];
63
+
64
+ export const mockFindResults: FindSolutionResult[] = [
65
+ {
66
+ solution_id: 'sol_001',
67
+ query_title: 'How to implement binary search in TypeScript',
68
+ human_verification_required: false,
69
+ },
70
+ {
71
+ solution_id: 'sol_002',
72
+ query_title: 'Fix memory leak in Node.js event listeners',
73
+ solution_body: `// Always remove event listeners when done
74
+ const handler = () => { /* ... */ };
75
+ emitter.on('event', handler);
76
+ // Later:
77
+ emitter.off('event', handler);
78
+
79
+ // Or use once() for one-time listeners
80
+ emitter.once('event', () => { /* ... */ });`,
81
+ human_verification_required: true,
82
+ },
83
+ {
84
+ solution_id: 'sol_003',
85
+ query_title: 'Optimize React re-renders with useMemo',
86
+ solution_body: `import { useMemo } from 'react';
87
+
88
+ function ExpensiveComponent({ data }) {
89
+ const processed = useMemo(() => {
90
+ return data.map(item => heavyComputation(item));
91
+ }, [data]);
92
+
93
+ return <div>{processed}</div>;
94
+ }`,
95
+ human_verification_required: false,
96
+ },
97
+ ];
98
+
99
+ export const mockBalance: Balance = {
100
+ available: 1500,
101
+ pending_debits: 75,
102
+ pending_credits: 200,
103
+ total_earned: 3500,
104
+ total_spent: 1800,
105
+ };
106
+
107
+ export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
108
+ return {
109
+ id: `sol_${Date.now()}`,
110
+ author_id: 'user_mock',
111
+ query_title: 'Mock solution title',
112
+ solution_body: 'Mock solution body content',
113
+ price_current: 50,
114
+ verification_state: 'PENDING',
115
+ access_count: 0,
116
+ upvotes: 0,
117
+ downvotes: 0,
118
+ ...overrides,
119
+ };
120
+ }
121
+
122
+ export function createMockFindResult(
123
+ overrides: Partial<FindSolutionResult> = {}
124
+ ): FindSolutionResult {
125
+ return {
126
+ solution_id: `sol_${Date.now()}`,
127
+ query_title: 'Mock query title',
128
+ human_verification_required: false,
129
+ ...overrides,
130
+ };
131
+ }
132
+
133
+ export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
134
+ return {
135
+ available: 1000,
136
+ pending_debits: 0,
137
+ pending_credits: 0,
138
+ total_earned: 1000,
139
+ total_spent: 0,
140
+ ...overrides,
141
+ };
142
+ }