@claudetools/tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/logger.js ADDED
@@ -0,0 +1,401 @@
1
+ // =============================================================================
2
+ // ClaudeTools Memory MCP Server - Structured Logger
3
+ // =============================================================================
4
+ // File-based logging for real-time monitoring of MCP operations
5
+ // =============================================================================
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ // Emojis for visual scanning
9
+ const CATEGORY_ICONS = {
10
+ TOOL: '🔧',
11
+ API: '🌐',
12
+ MEMORY: '🧠',
13
+ SEARCH: '🔍',
14
+ INJECT: '💉',
15
+ EXTRACT: '📤',
16
+ STORE: '💾',
17
+ QUERY: '❓',
18
+ IMPACT: '💥',
19
+ PATTERN: '🔬',
20
+ CONFIG: '⚙️',
21
+ REGISTRATION: '📝',
22
+ ERROR: '❌',
23
+ };
24
+ const LEVEL_ICONS = {
25
+ DEBUG: '🔹',
26
+ INFO: '🔷',
27
+ WARN: '⚠️',
28
+ ERROR: '❌',
29
+ };
30
+ class MCPLogger {
31
+ logFile;
32
+ logDir;
33
+ enabled;
34
+ constructor() {
35
+ // Log to .logs directory in project root
36
+ this.logDir = path.join(process.cwd(), '.logs');
37
+ this.logFile = path.join(this.logDir, 'mcp-server.log');
38
+ this.enabled = process.env.MCP_LOGGING !== 'false';
39
+ // Ensure log directory exists
40
+ if (this.enabled) {
41
+ try {
42
+ if (!fs.existsSync(this.logDir)) {
43
+ fs.mkdirSync(this.logDir, { recursive: true });
44
+ }
45
+ }
46
+ catch {
47
+ // If we can't create dir, disable logging
48
+ this.enabled = false;
49
+ }
50
+ }
51
+ }
52
+ formatTimestamp() {
53
+ const now = new Date();
54
+ return now.toISOString();
55
+ }
56
+ formatForFile(entry) {
57
+ const icon = CATEGORY_ICONS[entry.category] || '📝';
58
+ const levelIcon = LEVEL_ICONS[entry.level];
59
+ // Format timestamp as readable time only (HH:MM:SS)
60
+ const time = new Date(entry.timestamp).toLocaleTimeString('en-AU', {
61
+ hour12: false,
62
+ hour: '2-digit',
63
+ minute: '2-digit',
64
+ second: '2-digit',
65
+ });
66
+ // Build human-readable message
67
+ let line = `${time} ${levelIcon} ${icon} ${entry.message}`;
68
+ // Add duration as natural language
69
+ if (entry.duration !== undefined) {
70
+ if (entry.duration < 100) {
71
+ line += ` — instant`;
72
+ }
73
+ else if (entry.duration < 500) {
74
+ line += ` — ${entry.duration}ms`;
75
+ }
76
+ else if (entry.duration < 1000) {
77
+ line += ` — took ${entry.duration}ms`;
78
+ }
79
+ else {
80
+ line += ` — took ${(entry.duration / 1000).toFixed(1)}s`;
81
+ }
82
+ }
83
+ // Format data as natural language (no JSON)
84
+ if (entry.data && Object.keys(entry.data).length > 0) {
85
+ const details = this.formatDataAsNaturalLanguage(entry.data);
86
+ if (details) {
87
+ line += `\n ${details}`;
88
+ }
89
+ }
90
+ return line;
91
+ }
92
+ formatDataAsNaturalLanguage(data) {
93
+ const parts = [];
94
+ for (const [key, value] of Object.entries(data)) {
95
+ // Skip tool name (already in message)
96
+ if (key === 'tool')
97
+ continue;
98
+ // Format different types naturally
99
+ if (key === 'args' && typeof value === 'object' && value !== null) {
100
+ const args = value;
101
+ const argParts = [];
102
+ for (const [argKey, argVal] of Object.entries(args)) {
103
+ if (typeof argVal === 'string') {
104
+ // Truncate long strings
105
+ const str = argVal.length > 80 ? argVal.slice(0, 77) + '...' : argVal;
106
+ argParts.push(`${argKey}: "${str}"`);
107
+ }
108
+ else if (typeof argVal === 'number') {
109
+ argParts.push(`${argKey}: ${argVal}`);
110
+ }
111
+ else if (typeof argVal === 'boolean') {
112
+ argParts.push(`${argKey}: ${argVal ? 'yes' : 'no'}`);
113
+ }
114
+ else if (Array.isArray(argVal)) {
115
+ argParts.push(`${argKey}: [${argVal.length} items]`);
116
+ }
117
+ }
118
+ if (argParts.length > 0) {
119
+ parts.push(`with ${argParts.join(', ')}`);
120
+ }
121
+ }
122
+ else if (key === 'method') {
123
+ parts.push(`using ${value}`);
124
+ }
125
+ else if (key === 'queryLength') {
126
+ parts.push(`(${value} chars)`);
127
+ }
128
+ else if (typeof value === 'string' && value.length < 100) {
129
+ parts.push(`${key}: ${value}`);
130
+ }
131
+ else if (typeof value === 'number') {
132
+ parts.push(`${key}: ${value}`);
133
+ }
134
+ }
135
+ return parts.join(' • ');
136
+ }
137
+ write(entry) {
138
+ if (!this.enabled)
139
+ return;
140
+ try {
141
+ const line = this.formatForFile(entry) + '\n';
142
+ fs.appendFileSync(this.logFile, line);
143
+ }
144
+ catch {
145
+ // Silently fail if we can't write
146
+ }
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Core Logging Methods
150
+ // ---------------------------------------------------------------------------
151
+ debug(category, message, data) {
152
+ this.write({
153
+ timestamp: this.formatTimestamp(),
154
+ level: 'DEBUG',
155
+ category,
156
+ message,
157
+ data,
158
+ });
159
+ }
160
+ info(category, message, data) {
161
+ this.write({
162
+ timestamp: this.formatTimestamp(),
163
+ level: 'INFO',
164
+ category,
165
+ message,
166
+ data,
167
+ });
168
+ }
169
+ warn(category, message, data) {
170
+ this.write({
171
+ timestamp: this.formatTimestamp(),
172
+ level: 'WARN',
173
+ category,
174
+ message,
175
+ data,
176
+ });
177
+ }
178
+ error(category, message, error) {
179
+ const errorMessage = error instanceof Error ? error.message : String(error);
180
+ this.write({
181
+ timestamp: this.formatTimestamp(),
182
+ level: 'ERROR',
183
+ category,
184
+ message: `${message}: ${errorMessage}`,
185
+ });
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Structured Operation Logging (Human-Readable)
189
+ // ---------------------------------------------------------------------------
190
+ toolCall(toolName, args) {
191
+ // Format tool name in a friendly way
192
+ const friendlyName = toolName.replace(/_/g, ' ');
193
+ const sanitized = this.sanitizeArgs(args);
194
+ // Build a natural language description
195
+ let description = `Starting ${friendlyName}`;
196
+ // Add context from args
197
+ if (sanitized.query && typeof sanitized.query === 'string') {
198
+ const query = sanitized.query;
199
+ const preview = query.length > 50 ? query.slice(0, 47) + '...' : query;
200
+ description = `Searching for "${preview}"`;
201
+ }
202
+ else if (sanitized.function_name) {
203
+ description = `Analysing function "${sanitized.function_name}"`;
204
+ }
205
+ else if (sanitized.entity1 && sanitized.relationship && sanitized.entity2) {
206
+ description = `Storing: ${sanitized.entity1} → ${sanitized.relationship} → ${sanitized.entity2}`;
207
+ }
208
+ else if (sanitized.code && typeof sanitized.code === 'string') {
209
+ description = `Checking ${(sanitized.code).length} characters of code`;
210
+ }
211
+ this.info('TOOL', description);
212
+ }
213
+ toolResult(toolName, success, duration, resultSummary) {
214
+ const friendlyName = toolName.replace(/_/g, ' ');
215
+ if (success) {
216
+ const message = resultSummary ? resultSummary : `${friendlyName} completed`;
217
+ this.write({
218
+ timestamp: this.formatTimestamp(),
219
+ level: 'INFO',
220
+ category: 'TOOL',
221
+ message,
222
+ duration,
223
+ });
224
+ }
225
+ else {
226
+ this.write({
227
+ timestamp: this.formatTimestamp(),
228
+ level: 'ERROR',
229
+ category: 'TOOL',
230
+ message: `${friendlyName} failed`,
231
+ duration,
232
+ });
233
+ }
234
+ }
235
+ apiRequest(method, endpoint) {
236
+ // Make endpoint human-readable
237
+ const path = endpoint.replace('/api/v1/', '').replace(/\//g, ' → ');
238
+ this.debug('API', `${method} request to ${path}`);
239
+ }
240
+ apiResponse(method, endpoint, status, duration) {
241
+ const level = status >= 400 ? 'ERROR' : 'DEBUG';
242
+ const path = endpoint.replace('/api/v1/', '').replace(/\//g, ' → ');
243
+ const outcome = status >= 400 ? 'failed' : 'succeeded';
244
+ this.write({
245
+ timestamp: this.formatTimestamp(),
246
+ level,
247
+ category: 'API',
248
+ message: `${method} ${path} ${outcome} (${status})`,
249
+ duration,
250
+ });
251
+ }
252
+ searchQuery(query, method) {
253
+ const preview = query.length > 50 ? query.slice(0, 47) + '...' : query;
254
+ this.info('SEARCH', `Looking for "${preview}" using ${method}`);
255
+ }
256
+ searchResults(factsFound, entitiesFound, duration) {
257
+ let message;
258
+ if (factsFound === 0 && entitiesFound === 0) {
259
+ message = 'No results found';
260
+ }
261
+ else if (factsFound === 0) {
262
+ message = `Found ${entitiesFound} ${entitiesFound === 1 ? 'entity' : 'entities'}, no facts`;
263
+ }
264
+ else if (entitiesFound === 0) {
265
+ message = `Found ${factsFound} ${factsFound === 1 ? 'fact' : 'facts'}, no entities`;
266
+ }
267
+ else {
268
+ message = `Found ${factsFound} ${factsFound === 1 ? 'fact' : 'facts'} and ${entitiesFound} ${entitiesFound === 1 ? 'entity' : 'entities'}`;
269
+ }
270
+ this.write({
271
+ timestamp: this.formatTimestamp(),
272
+ level: 'INFO',
273
+ category: 'SEARCH',
274
+ message,
275
+ duration,
276
+ });
277
+ }
278
+ memoryStore(entity1, relation, entity2) {
279
+ // Make the relationship human-readable
280
+ const readableRelation = relation.toLowerCase().replace(/_/g, ' ');
281
+ this.info('STORE', `Remembered: "${entity1}" ${readableRelation} "${entity2}"`);
282
+ }
283
+ queryDependencies(functionName, direction, resultsCount, duration) {
284
+ let message;
285
+ if (direction === 'forward') {
286
+ message = resultsCount === 0
287
+ ? `"${functionName}" doesn't call any other functions`
288
+ : `"${functionName}" calls ${resultsCount} other ${resultsCount === 1 ? 'function' : 'functions'}`;
289
+ }
290
+ else {
291
+ message = resultsCount === 0
292
+ ? `Nothing calls "${functionName}"`
293
+ : `${resultsCount} ${resultsCount === 1 ? 'function calls' : 'functions call'} "${functionName}"`;
294
+ }
295
+ this.write({
296
+ timestamp: this.formatTimestamp(),
297
+ level: 'INFO',
298
+ category: 'QUERY',
299
+ message,
300
+ duration,
301
+ });
302
+ }
303
+ impactAnalysis(functionName, analysisType, riskLevel, totalAffected, duration) {
304
+ const riskEmoji = riskLevel === 'CRITICAL' ? '🚨' : riskLevel === 'HIGH' ? '⚠️' : riskLevel === 'MEDIUM' ? '📋' : '✅';
305
+ let message;
306
+ if (totalAffected === 0) {
307
+ message = `${riskEmoji} Changing "${functionName}" is safe — no dependencies`;
308
+ }
309
+ else {
310
+ message = `${riskEmoji} Changing "${functionName}" affects ${totalAffected} ${totalAffected === 1 ? 'place' : 'places'} — ${riskLevel.toLowerCase()} risk`;
311
+ }
312
+ this.write({
313
+ timestamp: this.formatTimestamp(),
314
+ level: 'INFO',
315
+ category: 'IMPACT',
316
+ message,
317
+ duration,
318
+ });
319
+ }
320
+ patternCheck(codeLength, warningsFound, securityScore, perfScore, duration) {
321
+ let message;
322
+ if (warningsFound === 0) {
323
+ message = `Code looks good — no issues found`;
324
+ }
325
+ else {
326
+ message = `Found ${warningsFound} ${warningsFound === 1 ? 'issue' : 'issues'}`;
327
+ }
328
+ // Add scores as context
329
+ const secLabel = securityScore >= 80 ? 'secure' : securityScore >= 50 ? 'some concerns' : 'needs attention';
330
+ const perfLabel = perfScore >= 80 ? 'efficient' : perfScore >= 50 ? 'acceptable' : 'could be faster';
331
+ message += ` — Security: ${secLabel} (${securityScore}/100), Performance: ${perfLabel} (${perfScore}/100)`;
332
+ this.write({
333
+ timestamp: this.formatTimestamp(),
334
+ level: 'INFO',
335
+ category: 'PATTERN',
336
+ message,
337
+ duration,
338
+ });
339
+ }
340
+ contextInjection(query, factsIncluded, tokenBudget, duration) {
341
+ const preview = query.length > 35 ? query.slice(0, 32) + '...' : query;
342
+ let message;
343
+ if (factsIncluded === 0) {
344
+ message = `No relevant context found for "${preview}"`;
345
+ }
346
+ else {
347
+ message = `Providing ${factsIncluded} ${factsIncluded === 1 ? 'piece' : 'pieces'} of context for "${preview}"`;
348
+ }
349
+ this.write({
350
+ timestamp: this.formatTimestamp(),
351
+ level: 'INFO',
352
+ category: 'INJECT',
353
+ message,
354
+ duration,
355
+ });
356
+ }
357
+ // ---------------------------------------------------------------------------
358
+ // Utility
359
+ // ---------------------------------------------------------------------------
360
+ sanitizeArgs(args) {
361
+ const sanitized = {};
362
+ for (const [key, value] of Object.entries(args)) {
363
+ if (typeof value === 'string' && value.length > 200) {
364
+ sanitized[key] = value.slice(0, 200) + `... (${value.length} chars)`;
365
+ }
366
+ else {
367
+ sanitized[key] = value;
368
+ }
369
+ }
370
+ return sanitized;
371
+ }
372
+ // Start a timing context
373
+ startTimer() {
374
+ const start = Date.now();
375
+ return () => Date.now() - start;
376
+ }
377
+ // Log separator for visual clarity
378
+ separator(label) {
379
+ if (label) {
380
+ this.write({
381
+ timestamp: this.formatTimestamp(),
382
+ level: 'INFO',
383
+ category: 'TOOL',
384
+ message: `${'─'.repeat(20)} ${label} ${'─'.repeat(20)}`,
385
+ });
386
+ }
387
+ }
388
+ // Clear log file
389
+ clear() {
390
+ if (this.enabled) {
391
+ try {
392
+ fs.writeFileSync(this.logFile, '');
393
+ }
394
+ catch {
395
+ // Silently fail
396
+ }
397
+ }
398
+ }
399
+ }
400
+ // Singleton export
401
+ export const mcpLogger = new MCPLogger();
@@ -0,0 +1,2 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ export declare function registerPromptHandlers(server: Server): void;
@@ -0,0 +1,64 @@
1
+ // =============================================================================
2
+ // MCP Prompt Handlers
3
+ // =============================================================================
4
+ import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ export function registerPromptHandlers(server) {
6
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
7
+ prompts: [
8
+ {
9
+ name: 'recall',
10
+ description: 'Recall information from memory about a specific topic',
11
+ arguments: [
12
+ {
13
+ name: 'topic',
14
+ description: 'The topic to recall information about',
15
+ required: true,
16
+ },
17
+ ],
18
+ },
19
+ {
20
+ name: 'remember',
21
+ description: 'Store important information to memory',
22
+ arguments: [
23
+ {
24
+ name: 'information',
25
+ description: 'The information to remember',
26
+ required: true,
27
+ },
28
+ ],
29
+ },
30
+ ],
31
+ }));
32
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
33
+ const { name, arguments: args } = request.params;
34
+ if (name === 'recall') {
35
+ const topic = args?.topic || 'general';
36
+ return {
37
+ messages: [
38
+ {
39
+ role: 'user',
40
+ content: {
41
+ type: 'text',
42
+ text: `Search your memory for information about: ${topic}\n\nUse the memory_search tool to find relevant facts and entities.`,
43
+ },
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ if (name === 'remember') {
49
+ const information = args?.information || '';
50
+ return {
51
+ messages: [
52
+ {
53
+ role: 'user',
54
+ content: {
55
+ type: 'text',
56
+ text: `Store this information to memory:\n\n${information}\n\nUse the memory_store_fact tool to save any specific facts, or memory_add for general conversation context.`,
57
+ },
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ throw new Error(`Unknown prompt: ${name}`);
63
+ });
64
+ }
@@ -0,0 +1,2 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ export declare function registerResourceHandlers(server: Server): void;
@@ -0,0 +1,79 @@
1
+ // =============================================================================
2
+ // MCP Resource Handlers
3
+ // =============================================================================
4
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { getSummary, getEntities, getContext } from './helpers/api-client.js';
6
+ import { formatContextForClaude } from './helpers/formatter.js';
7
+ import { getDefaultProjectId } from './helpers/config.js';
8
+ export function registerResourceHandlers(server) {
9
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
10
+ resources: [
11
+ {
12
+ uri: `memory://summary`,
13
+ name: 'Memory Summary',
14
+ description: 'Current state of the memory system',
15
+ mimeType: 'text/plain',
16
+ },
17
+ {
18
+ uri: `memory://entities`,
19
+ name: 'Entity List',
20
+ description: 'All known entities in the memory system',
21
+ mimeType: 'application/json',
22
+ },
23
+ {
24
+ uri: `memory://context`,
25
+ name: 'Current Context',
26
+ description: 'Recent facts and entities from memory',
27
+ mimeType: 'text/markdown',
28
+ },
29
+ ],
30
+ }));
31
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
32
+ const uri = request.params.uri;
33
+ try {
34
+ // Get project ID (may throw if not configured)
35
+ const projectId = getDefaultProjectId();
36
+ if (uri === 'memory://summary') {
37
+ const summary = await getSummary(projectId);
38
+ return {
39
+ contents: [
40
+ {
41
+ uri,
42
+ mimeType: 'text/plain',
43
+ text: summary,
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ if (uri === 'memory://entities') {
49
+ const entities = await getEntities(projectId);
50
+ return {
51
+ contents: [
52
+ {
53
+ uri,
54
+ mimeType: 'application/json',
55
+ text: JSON.stringify(entities, null, 2),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ if (uri === 'memory://context') {
61
+ const context = await getContext(projectId);
62
+ return {
63
+ contents: [
64
+ {
65
+ uri,
66
+ mimeType: 'text/markdown',
67
+ text: formatContextForClaude(context),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ throw new Error(`Unknown resource: ${uri}`);
73
+ }
74
+ catch (error) {
75
+ const message = error instanceof Error ? error.message : 'Unknown error';
76
+ throw new Error(`Failed to read resource: ${message}`);
77
+ }
78
+ });
79
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;