@pcircle/footprint 1.0.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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/SKILL.md +355 -0
  4. package/dist/index.d.ts +19 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +690 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/lib/crypto/decrypt.d.ts +11 -0
  9. package/dist/lib/crypto/decrypt.d.ts.map +1 -0
  10. package/dist/lib/crypto/decrypt.js +30 -0
  11. package/dist/lib/crypto/decrypt.js.map +1 -0
  12. package/dist/lib/crypto/encrypt.d.ts +13 -0
  13. package/dist/lib/crypto/encrypt.d.ts.map +1 -0
  14. package/dist/lib/crypto/encrypt.js +24 -0
  15. package/dist/lib/crypto/encrypt.js.map +1 -0
  16. package/dist/lib/crypto/index.d.ts +6 -0
  17. package/dist/lib/crypto/index.d.ts.map +1 -0
  18. package/dist/lib/crypto/index.js +5 -0
  19. package/dist/lib/crypto/index.js.map +1 -0
  20. package/dist/lib/crypto/key-derivation.d.ts +30 -0
  21. package/dist/lib/crypto/key-derivation.d.ts.map +1 -0
  22. package/dist/lib/crypto/key-derivation.js +63 -0
  23. package/dist/lib/crypto/key-derivation.js.map +1 -0
  24. package/dist/lib/crypto/types.d.ts +22 -0
  25. package/dist/lib/crypto/types.d.ts.map +1 -0
  26. package/dist/lib/crypto/types.js +10 -0
  27. package/dist/lib/crypto/types.js.map +1 -0
  28. package/dist/lib/storage/database.d.ts +116 -0
  29. package/dist/lib/storage/database.d.ts.map +1 -0
  30. package/dist/lib/storage/database.js +390 -0
  31. package/dist/lib/storage/database.js.map +1 -0
  32. package/dist/lib/storage/export.d.ts +26 -0
  33. package/dist/lib/storage/export.d.ts.map +1 -0
  34. package/dist/lib/storage/export.js +113 -0
  35. package/dist/lib/storage/export.js.map +1 -0
  36. package/dist/lib/storage/git.d.ts +16 -0
  37. package/dist/lib/storage/git.d.ts.map +1 -0
  38. package/dist/lib/storage/git.js +55 -0
  39. package/dist/lib/storage/git.js.map +1 -0
  40. package/dist/lib/storage/index.d.ts +6 -0
  41. package/dist/lib/storage/index.d.ts.map +1 -0
  42. package/dist/lib/storage/index.js +5 -0
  43. package/dist/lib/storage/index.js.map +1 -0
  44. package/dist/lib/storage/schema.d.ts +17 -0
  45. package/dist/lib/storage/schema.d.ts.map +1 -0
  46. package/dist/lib/storage/schema.js +103 -0
  47. package/dist/lib/storage/schema.js.map +1 -0
  48. package/dist/lib/storage/types.d.ts +26 -0
  49. package/dist/lib/storage/types.d.ts.map +1 -0
  50. package/dist/lib/storage/types.js +2 -0
  51. package/dist/lib/storage/types.js.map +1 -0
  52. package/dist/test-helpers.d.ts +33 -0
  53. package/dist/test-helpers.d.ts.map +1 -0
  54. package/dist/test-helpers.js +108 -0
  55. package/dist/test-helpers.js.map +1 -0
  56. package/dist/types.d.ts +29 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +2 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/ui/dashboard.html +965 -0
  61. package/dist/ui/detail.html +348 -0
  62. package/dist/ui/export.html +409 -0
  63. package/dist/ui/register.d.ts +7 -0
  64. package/dist/ui/register.d.ts.map +1 -0
  65. package/dist/ui/register.js +154 -0
  66. package/dist/ui/register.js.map +1 -0
  67. package/dist/ui-tmp/ui/export.html +409 -0
  68. package/package.json +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,690 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { EvidenceDatabase, getCurrentCommit } from './lib/storage/index.js';
5
+ import { deriveKey, encrypt, decrypt } from './lib/crypto/index.js';
6
+ import { registerUIResources } from './ui/register.js';
7
+ import * as z from 'zod';
8
+ import * as crypto from 'crypto';
9
+ function getErrorMessage(error) {
10
+ return error instanceof Error ? error.message : 'Unknown error';
11
+ }
12
+ /**
13
+ * EvidenceMCP Server - Captures LLM conversations as encrypted evidence
14
+ * with Git timestamps and export capabilities.
15
+ */
16
+ export class EvidenceMCPServer {
17
+ server;
18
+ config;
19
+ db;
20
+ derivedKey = null;
21
+ constructor(config) {
22
+ this.config = config;
23
+ try {
24
+ this.db = new EvidenceDatabase(config.dbPath);
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Failed to initialize database: ${getErrorMessage(error)}`);
28
+ }
29
+ this.server = new McpServer({
30
+ name: config.name || 'traceguard-mcp',
31
+ version: config.version || '0.1.0'
32
+ });
33
+ // Register UI resources for MCP Apps
34
+ registerUIResources(this.server);
35
+ this.registerTools();
36
+ this.registerResources();
37
+ }
38
+ async getDerivedKey() {
39
+ if (!this.derivedKey) {
40
+ const result = await deriveKey(this.config.password);
41
+ this.derivedKey = result.key;
42
+ }
43
+ return this.derivedKey;
44
+ }
45
+ registerTools() {
46
+ this.server.registerTool('capture-evidence', {
47
+ title: 'Capture Evidence',
48
+ description: 'Capture and encrypt LLM conversation as evidence',
49
+ inputSchema: {
50
+ conversationId: z.string().describe('Conversation ID'),
51
+ llmProvider: z.string().describe('LLM provider name (e.g., Claude Sonnet 4.5)'),
52
+ content: z.string().describe('Conversation content (messages, prompts, responses)'),
53
+ messageCount: z.number().int().positive().describe('Number of messages'),
54
+ tags: z.string().optional().describe('Optional tags (comma-separated)')
55
+ },
56
+ outputSchema: {
57
+ id: z.string(),
58
+ timestamp: z.string(),
59
+ gitCommitHash: z.string().nullable(),
60
+ success: z.boolean()
61
+ }
62
+ }, async (params) => {
63
+ try {
64
+ if (!params.content || params.content.trim().length === 0) {
65
+ throw new Error('Content cannot be empty');
66
+ }
67
+ if (params.messageCount <= 0) {
68
+ throw new Error('Message count must be positive');
69
+ }
70
+ const key = await this.getDerivedKey();
71
+ const encrypted = await encrypt(params.content, key);
72
+ const gitInfo = await getCurrentCommit();
73
+ const contentHash = crypto.createHash('sha256').update(params.content).digest('hex');
74
+ const id = this.db.create({
75
+ timestamp: new Date().toISOString(),
76
+ conversationId: params.conversationId,
77
+ llmProvider: params.llmProvider,
78
+ encryptedContent: encrypted.ciphertext,
79
+ nonce: encrypted.nonce,
80
+ contentHash,
81
+ messageCount: params.messageCount,
82
+ gitCommitHash: gitInfo?.commitHash || null,
83
+ gitTimestamp: gitInfo?.timestamp || null,
84
+ tags: params.tags || null
85
+ });
86
+ return {
87
+ content: [{ type: 'text', text: `✅ Evidence captured successfully\n- ID: ${id}\n- Timestamp: ${new Date().toISOString()}\n- Git Commit: ${gitInfo?.commitHash || 'N/A'}\n- Message Count: ${params.messageCount}` }],
88
+ structuredContent: {
89
+ id,
90
+ timestamp: new Date().toISOString(),
91
+ gitCommitHash: gitInfo?.commitHash || null,
92
+ success: true
93
+ }
94
+ };
95
+ }
96
+ catch (error) {
97
+ throw new Error(`[Tool: capture-evidence] ${getErrorMessage(error)}. Suggested action: Check content is not empty and messageCount is positive.`);
98
+ }
99
+ });
100
+ this.server.registerTool('list-evidences', {
101
+ title: 'List Evidences',
102
+ description: 'List all captured evidence with pagination',
103
+ inputSchema: {
104
+ limit: z.number().int().positive().optional().describe('Maximum results'),
105
+ offset: z.number().int().min(0).optional().describe('Pagination offset')
106
+ },
107
+ outputSchema: {
108
+ evidences: z.array(z.object({
109
+ id: z.string(),
110
+ timestamp: z.string(),
111
+ conversationId: z.string(),
112
+ llmProvider: z.string(),
113
+ messageCount: z.number(),
114
+ tags: z.string().nullable()
115
+ })),
116
+ total: z.number()
117
+ },
118
+ _meta: {
119
+ ui: {
120
+ resourceUri: "ui://footprint/dashboard.html"
121
+ }
122
+ }
123
+ }, async (params) => {
124
+ try {
125
+ if (params.limit !== undefined && params.limit <= 0) {
126
+ throw new Error('Limit must be positive');
127
+ }
128
+ if (params.offset !== undefined && params.offset < 0) {
129
+ throw new Error('Offset cannot be negative');
130
+ }
131
+ const evidences = this.db.list({ limit: params.limit, offset: params.offset });
132
+ const mappedEvidences = evidences.map(e => ({
133
+ id: e.id,
134
+ timestamp: e.timestamp,
135
+ conversationId: e.conversationId,
136
+ llmProvider: e.llmProvider,
137
+ messageCount: e.messageCount,
138
+ tags: e.tags
139
+ }));
140
+ return {
141
+ content: [{ type: 'text', text: `✅ Evidence list retrieved successfully\n- Count: ${evidences.length} evidence(s)\n- Limit: ${params.limit || 'No limit'}\n- Offset: ${params.offset || 0}` }],
142
+ structuredContent: { evidences: mappedEvidences, total: evidences.length }
143
+ };
144
+ }
145
+ catch (error) {
146
+ throw new Error(`[Tool: list-evidences] ${getErrorMessage(error)}. Suggested action: Ensure limit is positive and offset is non-negative.`);
147
+ }
148
+ });
149
+ this.server.registerTool('export-evidences', {
150
+ title: 'Export Evidences',
151
+ description: 'Export evidences to encrypted ZIP archive',
152
+ inputSchema: {
153
+ evidenceIds: z.array(z.string()).optional().describe('Specific IDs (empty = all)'),
154
+ includeGitInfo: z.boolean().optional().describe('Include Git timestamps')
155
+ },
156
+ outputSchema: {
157
+ filename: z.string(),
158
+ checksum: z.string(),
159
+ evidenceCount: z.number(),
160
+ success: z.boolean()
161
+ },
162
+ _meta: {
163
+ ui: {
164
+ resourceUri: "ui://footprint/export.html"
165
+ }
166
+ }
167
+ }, async (params) => {
168
+ try {
169
+ const { exportEvidences } = await import('./lib/storage/index.js');
170
+ const fs = await import('fs');
171
+ const result = await exportEvidences(this.db, {
172
+ evidenceIds: params.evidenceIds,
173
+ includeGitInfo: params.includeGitInfo ?? false
174
+ });
175
+ fs.writeFileSync(result.filename, result.zipData);
176
+ return {
177
+ content: [{ type: 'text', text: `✅ Export completed successfully\n- Evidence Count: ${result.evidenceCount}\n- Filename: ${result.filename}\n- Checksum: ${result.checksum}\n- Git Info: ${params.includeGitInfo ? 'Included' : 'Excluded'}` }],
178
+ structuredContent: {
179
+ filename: result.filename,
180
+ checksum: result.checksum,
181
+ evidenceCount: result.evidenceCount,
182
+ success: true
183
+ }
184
+ };
185
+ }
186
+ catch (error) {
187
+ throw new Error(`[Tool: export-evidences] ${getErrorMessage(error)}. Suggested action: Check evidenceIds exist and filesystem has write permissions.`);
188
+ }
189
+ });
190
+ this.server.registerTool('get-evidence', {
191
+ title: 'Get Evidence',
192
+ description: 'Retrieve and decrypt specific evidence by ID',
193
+ inputSchema: {
194
+ id: z.string().describe('Evidence ID')
195
+ },
196
+ outputSchema: {
197
+ id: z.string(),
198
+ timestamp: z.string(),
199
+ conversationId: z.string(),
200
+ llmProvider: z.string(),
201
+ content: z.string(),
202
+ messageCount: z.number(),
203
+ gitInfo: z.object({
204
+ commitHash: z.string(),
205
+ timestamp: z.string()
206
+ }).nullable(),
207
+ tags: z.string().nullable()
208
+ },
209
+ _meta: {
210
+ ui: {
211
+ resourceUri: "ui://footprint/detail.html"
212
+ }
213
+ }
214
+ }, async (params) => {
215
+ try {
216
+ const evidence = this.db.findById(params.id);
217
+ if (!evidence) {
218
+ throw new Error(`Evidence not found: ${params.id}`);
219
+ }
220
+ const key = await this.getDerivedKey();
221
+ const decrypted = decrypt(evidence.encryptedContent, evidence.nonce, key);
222
+ const gitInfo = evidence.gitCommitHash
223
+ ? { commitHash: evidence.gitCommitHash, timestamp: evidence.gitTimestamp }
224
+ : null;
225
+ return {
226
+ content: [{ type: 'text', text: `✅ Evidence retrieved successfully\n- ID: ${evidence.id}\n- Timestamp: ${evidence.timestamp}\n- Provider: ${evidence.llmProvider}\n- Message Count: ${evidence.messageCount}\n- Content Preview: ${decrypted.substring(0, 100)}...` }],
227
+ structuredContent: {
228
+ id: evidence.id,
229
+ timestamp: evidence.timestamp,
230
+ conversationId: evidence.conversationId,
231
+ llmProvider: evidence.llmProvider,
232
+ content: decrypted,
233
+ messageCount: evidence.messageCount,
234
+ gitInfo,
235
+ tags: evidence.tags
236
+ }
237
+ };
238
+ }
239
+ catch (error) {
240
+ throw new Error(`[Tool: get-evidence] ${getErrorMessage(error)}. Suggested action: Verify the evidence ID exists and password is correct.`);
241
+ }
242
+ });
243
+ this.server.registerTool('search-evidences', {
244
+ title: 'Search Evidences',
245
+ description: 'Search and filter evidences by query, tags, or date range',
246
+ inputSchema: {
247
+ query: z.string().optional().describe('Search text (matches conversationId, tags)'),
248
+ tags: z.array(z.string()).optional().describe('Filter by tags'),
249
+ dateFrom: z.string().optional().describe('Start date (ISO format)'),
250
+ dateTo: z.string().optional().describe('End date (ISO format)'),
251
+ limit: z.number().int().positive().optional().describe('Maximum results'),
252
+ offset: z.number().int().min(0).optional().describe('Pagination offset')
253
+ },
254
+ outputSchema: {
255
+ evidences: z.array(z.object({
256
+ id: z.string(),
257
+ timestamp: z.string(),
258
+ conversationId: z.string(),
259
+ llmProvider: z.string(),
260
+ messageCount: z.number(),
261
+ tags: z.string().nullable()
262
+ })),
263
+ total: z.number()
264
+ }
265
+ }, async (params) => {
266
+ try {
267
+ // Validate parameters
268
+ if (params.limit !== undefined && params.limit <= 0) {
269
+ throw new Error('Limit must be positive');
270
+ }
271
+ if (params.offset !== undefined && params.offset < 0) {
272
+ throw new Error('Offset cannot be negative');
273
+ }
274
+ // Validate date format if provided
275
+ if (params.dateFrom) {
276
+ const date = new Date(params.dateFrom);
277
+ if (isNaN(date.getTime())) {
278
+ throw new Error('dateFrom must be a valid ISO date string');
279
+ }
280
+ }
281
+ if (params.dateTo) {
282
+ const date = new Date(params.dateTo);
283
+ if (isNaN(date.getTime())) {
284
+ throw new Error('dateTo must be a valid ISO date string');
285
+ }
286
+ }
287
+ const evidences = this.db.search({
288
+ query: params.query,
289
+ tags: params.tags,
290
+ dateFrom: params.dateFrom,
291
+ dateTo: params.dateTo,
292
+ limit: params.limit,
293
+ offset: params.offset
294
+ });
295
+ const mappedEvidences = evidences.map(e => ({
296
+ id: e.id,
297
+ timestamp: e.timestamp,
298
+ conversationId: e.conversationId,
299
+ llmProvider: e.llmProvider,
300
+ messageCount: e.messageCount,
301
+ tags: e.tags
302
+ }));
303
+ return {
304
+ content: [{ type: 'text', text: `✅ Search completed successfully\n- Results: ${evidences.length} evidence(s) found\n- Query: ${params.query || 'None'}\n- Tags: ${params.tags?.join(', ') || 'None'}\n- Date Range: ${params.dateFrom || 'Start'} to ${params.dateTo || 'End'}` }],
305
+ structuredContent: { evidences: mappedEvidences, total: evidences.length }
306
+ };
307
+ }
308
+ catch (error) {
309
+ throw new Error(`[Tool: search-evidences] ${getErrorMessage(error)}. Suggested action: Check date format (ISO 8601), limit > 0, and offset >= 0.`);
310
+ }
311
+ });
312
+ this.server.registerTool('verify-evidence', {
313
+ title: 'Verify Evidence',
314
+ description: 'Verify the integrity and authenticity of captured evidence',
315
+ inputSchema: {
316
+ id: z.string().describe('Evidence ID to verify')
317
+ },
318
+ outputSchema: {
319
+ id: z.string(),
320
+ verified: z.boolean(),
321
+ checks: z.object({
322
+ contentIntegrity: z.object({
323
+ passed: z.boolean(),
324
+ hash: z.string()
325
+ }),
326
+ gitTimestamp: z.object({
327
+ passed: z.boolean(),
328
+ commitHash: z.string().nullable(),
329
+ timestamp: z.string().nullable()
330
+ }),
331
+ encryptionStatus: z.object({
332
+ passed: z.boolean(),
333
+ algorithm: z.string()
334
+ })
335
+ }),
336
+ legalReadiness: z.boolean(),
337
+ verifiedAt: z.string()
338
+ }
339
+ }, async (params) => {
340
+ try {
341
+ // Find the evidence record
342
+ const evidence = this.db.findById(params.id);
343
+ if (!evidence) {
344
+ throw new Error(`Evidence with ID ${params.id} not found`);
345
+ }
346
+ const key = await this.getDerivedKey();
347
+ // Perform verifications
348
+ const checks = {
349
+ contentIntegrity: { passed: false, hash: '' },
350
+ gitTimestamp: { passed: false, commitHash: null, timestamp: null },
351
+ encryptionStatus: { passed: false, algorithm: 'XChaCha20-Poly1305' }
352
+ };
353
+ // 1. Content Integrity: Decrypt content, compute SHA-256 hash, verify it matches stored contentHash
354
+ try {
355
+ const decryptedContent = decrypt(evidence.encryptedContent, evidence.nonce, key);
356
+ const computedHash = crypto.createHash('sha256').update(decryptedContent).digest('hex');
357
+ checks.contentIntegrity.passed = computedHash === evidence.contentHash;
358
+ checks.contentIntegrity.hash = computedHash;
359
+ }
360
+ catch (error) {
361
+ checks.contentIntegrity.passed = false;
362
+ checks.contentIntegrity.hash = '';
363
+ }
364
+ // 2. Git Timestamp: Check if gitCommitHash exists and gitTimestamp is valid
365
+ checks.gitTimestamp.commitHash = evidence.gitCommitHash;
366
+ checks.gitTimestamp.timestamp = evidence.gitTimestamp;
367
+ checks.gitTimestamp.passed = !!(evidence.gitCommitHash && evidence.gitTimestamp);
368
+ // 3. Encryption Status: Verify decryption works (XChaCha20-Poly1305)
369
+ try {
370
+ decrypt(evidence.encryptedContent, evidence.nonce, key);
371
+ checks.encryptionStatus.passed = true;
372
+ }
373
+ catch (error) {
374
+ checks.encryptionStatus.passed = false;
375
+ }
376
+ const verified = checks.contentIntegrity.passed && checks.gitTimestamp.passed && checks.encryptionStatus.passed;
377
+ const legalReadiness = verified;
378
+ const statusSymbols = {
379
+ content: checks.contentIntegrity.passed ? '✓' : '✗',
380
+ git: checks.gitTimestamp.passed ? '✓' : '✗',
381
+ encryption: checks.encryptionStatus.passed ? '✓' : '✗'
382
+ };
383
+ const statusText = verified
384
+ ? `✅ Evidence ${params.id} verified successfully\n- Content: ${statusSymbols.content} Integrity preserved\n- Git: ${statusSymbols.git} Timestamp verified\n- Encryption: ${statusSymbols.encryption} XChaCha20-Poly1305`
385
+ : `❌ Evidence ${params.id} verification failed\n- Content: ${statusSymbols.content} Integrity check\n- Git: ${statusSymbols.git} Timestamp check\n- Encryption: ${statusSymbols.encryption} Decryption check`;
386
+ return {
387
+ content: [{ type: 'text', text: statusText }],
388
+ structuredContent: {
389
+ id: params.id,
390
+ verified,
391
+ checks,
392
+ legalReadiness,
393
+ verifiedAt: new Date().toISOString()
394
+ }
395
+ };
396
+ }
397
+ catch (error) {
398
+ throw new Error(`[Tool: verify-evidence] ${getErrorMessage(error)}. Suggested action: Verify the evidence ID exists and encryption password is correct.`);
399
+ }
400
+ });
401
+ this.server.registerTool('suggest-capture', {
402
+ title: 'Suggest Capture',
403
+ description: 'Analyze conversation content and suggest whether to capture it as evidence',
404
+ inputSchema: {
405
+ summary: z.string().describe('Conversation summary or key content to analyze')
406
+ },
407
+ outputSchema: {
408
+ shouldCapture: z.boolean(),
409
+ reason: z.string(),
410
+ suggestedTags: z.array(z.string()),
411
+ suggestedConversationId: z.string(),
412
+ confidence: z.number()
413
+ }
414
+ }, async (params) => {
415
+ try {
416
+ if (!params.summary || params.summary.trim().length === 0) {
417
+ throw new Error('Summary cannot be empty');
418
+ }
419
+ const summary = params.summary.toLowerCase();
420
+ // Define keyword categories for detection
421
+ const keywordCategories = {
422
+ ip: ['patent', 'intellectual property', 'invention', 'algorithm', 'proprietary', 'innovation', 'design'],
423
+ legal: ['contract', 'agreement', 'legal', 'copyright', 'license', 'terms', 'clause', 'liability'],
424
+ business: ['decision', 'milestone', 'deliverable', 'approval', 'strategy', 'roadmap', 'budget', 'timeline'],
425
+ research: ['hypothesis', 'findings', 'proof', 'research', 'study', 'experiment', 'analysis', 'data'],
426
+ compliance: ['audit', 'compliance', 'evidence', 'documentation', 'regulation', 'policy', 'requirement']
427
+ };
428
+ // Helper: match whole words only (avoid false positives like "logarithm" matching "algorithm")
429
+ const matchesWord = (text, word) => {
430
+ const regex = new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
431
+ return regex.test(text);
432
+ };
433
+ // Detect matching categories and keywords
434
+ const matches = [];
435
+ let totalKeywords = 0;
436
+ for (const [category, keywords] of Object.entries(keywordCategories)) {
437
+ const foundKeywords = keywords.filter(keyword => matchesWord(summary, keyword));
438
+ if (foundKeywords.length > 0) {
439
+ const weight = foundKeywords.length / keywords.length;
440
+ matches.push({ category, keywords: foundKeywords, weight });
441
+ totalKeywords += foundKeywords.length;
442
+ }
443
+ }
444
+ // Determine if should capture
445
+ const shouldCapture = matches.length > 0;
446
+ // Calculate confidence: represents likelihood content is worth capturing
447
+ // High confidence (>0.7) = definitely important
448
+ // Low confidence (<0.5) = probably not important
449
+ let confidence;
450
+ if (shouldCapture) {
451
+ // More keywords and categories = higher importance confidence
452
+ const keywordDensity = totalKeywords / Math.max(summary.split(/\s+/).length, 1);
453
+ const categoryBonus = matches.length * 0.15;
454
+ const keywordBonus = Math.min(totalKeywords * 0.1, 0.4);
455
+ confidence = Math.min(0.95, 0.3 + categoryBonus + keywordBonus + keywordDensity);
456
+ }
457
+ else {
458
+ // No keywords found = low importance confidence
459
+ // Longer text without keywords = slightly higher (might have missed something)
460
+ const wordCount = summary.split(/\s+/).length;
461
+ const lengthFactor = Math.min(wordCount / 200, 0.3); // Max 0.3 for very long text
462
+ confidence = 0.1 + lengthFactor; // Range: 0.1 to 0.4
463
+ }
464
+ // Generate reason
465
+ let reason;
466
+ if (shouldCapture) {
467
+ const categories = matches.map(m => m.category === 'ip' ? 'IP' : m.category);
468
+ if (categories.length === 1) {
469
+ reason = `Contains ${categories[0]} keywords: ${matches[0].keywords.join(', ')}`;
470
+ }
471
+ else {
472
+ reason = `Contains multiple important categories: ${categories.join(', ')}`;
473
+ }
474
+ }
475
+ else {
476
+ reason = 'Appears to be casual conversation with no critical business, legal, or IP content';
477
+ }
478
+ // Generate suggested tags
479
+ const suggestedTags = shouldCapture
480
+ ? [...new Set(matches.flatMap(m => [m.category, ...m.keywords.slice(0, 2)]))]
481
+ : [];
482
+ // Generate conversation ID
483
+ const currentDate = new Date().toISOString().slice(0, 10);
484
+ let conversationId;
485
+ if (shouldCapture && matches.length > 0) {
486
+ // Extract key terms for ID generation
487
+ const primaryKeywords = matches[0].keywords.slice(0, 2);
488
+ const cleanKeywords = primaryKeywords
489
+ .map(k => k.replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''))
490
+ .filter(k => k.length > 0);
491
+ if (cleanKeywords.length > 0) {
492
+ conversationId = `${cleanKeywords.join('-')}-${currentDate}`;
493
+ }
494
+ else {
495
+ conversationId = `${matches[0].category}-discussion-${currentDate}`;
496
+ }
497
+ }
498
+ else {
499
+ conversationId = `conversation-${currentDate}`;
500
+ }
501
+ const resultText = shouldCapture
502
+ ? `💡 Capture suggested (${Math.round(confidence * 100)}% confidence)\n🔍 Reason: ${reason}\n🏷️ Tags: ${suggestedTags.join(', ')}\n📝 ID: ${conversationId}`
503
+ : `🤷 Capture not recommended (${Math.round(confidence * 100)}% confidence)\n💬 ${reason}`;
504
+ return {
505
+ content: [{ type: 'text', text: resultText }],
506
+ structuredContent: {
507
+ shouldCapture,
508
+ reason,
509
+ suggestedTags,
510
+ suggestedConversationId: conversationId,
511
+ confidence: Math.round(confidence * 100) / 100
512
+ }
513
+ };
514
+ }
515
+ catch (error) {
516
+ throw new Error(`[Tool: suggest-capture] ${getErrorMessage(error)}`);
517
+ }
518
+ });
519
+ // Delete evidences tool
520
+ this.server.registerTool('delete-evidences', {
521
+ title: 'Delete Evidences',
522
+ description: 'Permanently delete one or more evidence records',
523
+ inputSchema: {
524
+ evidenceIds: z.array(z.string()).min(1).describe('Array of evidence IDs to delete')
525
+ },
526
+ outputSchema: {
527
+ deletedCount: z.number(),
528
+ success: z.boolean()
529
+ }
530
+ }, async (params) => {
531
+ try {
532
+ if (!params.evidenceIds || params.evidenceIds.length === 0) {
533
+ throw new Error('At least one evidence ID is required');
534
+ }
535
+ const deletedCount = this.db.deleteMany(params.evidenceIds);
536
+ const success = deletedCount > 0;
537
+ const resultText = success
538
+ ? `🗑️ Successfully deleted ${deletedCount} evidence record${deletedCount > 1 ? 's' : ''}`
539
+ : `⚠️ No evidence records found with the provided IDs`;
540
+ return {
541
+ content: [{ type: 'text', text: resultText }],
542
+ structuredContent: { deletedCount, success }
543
+ };
544
+ }
545
+ catch (error) {
546
+ throw new Error(`[Tool: delete-evidences] ${getErrorMessage(error)}. Suggested action: Verify the evidence IDs exist.`);
547
+ }
548
+ });
549
+ // Rename tag tool
550
+ this.server.registerTool('rename-tag', {
551
+ title: 'Rename Tag',
552
+ description: 'Rename a tag across all evidence records',
553
+ inputSchema: {
554
+ oldTag: z.string().min(1).describe('Current tag name to rename'),
555
+ newTag: z.string().min(1).describe('New tag name')
556
+ },
557
+ outputSchema: {
558
+ updatedCount: z.number(),
559
+ success: z.boolean()
560
+ }
561
+ }, async (params) => {
562
+ try {
563
+ if (!params.oldTag || !params.newTag) {
564
+ throw new Error('Both old and new tag names are required');
565
+ }
566
+ if (params.oldTag.trim() === params.newTag.trim()) {
567
+ throw new Error('New tag must be different from old tag');
568
+ }
569
+ const updatedCount = this.db.renameTag(params.oldTag.trim(), params.newTag.trim());
570
+ const success = updatedCount > 0;
571
+ const resultText = success
572
+ ? `🏷️ Renamed tag "${params.oldTag}" to "${params.newTag}" in ${updatedCount} evidence record${updatedCount > 1 ? 's' : ''}`
573
+ : `⚠️ No evidence records found with tag "${params.oldTag}"`;
574
+ return {
575
+ content: [{ type: 'text', text: resultText }],
576
+ structuredContent: { updatedCount, success }
577
+ };
578
+ }
579
+ catch (error) {
580
+ throw new Error(`[Tool: rename-tag] ${getErrorMessage(error)}`);
581
+ }
582
+ });
583
+ // Remove tag tool
584
+ this.server.registerTool('remove-tag', {
585
+ title: 'Remove Tag',
586
+ description: 'Remove a tag from all evidence records',
587
+ inputSchema: {
588
+ tag: z.string().min(1).describe('Tag name to remove')
589
+ },
590
+ outputSchema: {
591
+ updatedCount: z.number(),
592
+ success: z.boolean()
593
+ }
594
+ }, async (params) => {
595
+ try {
596
+ if (!params.tag) {
597
+ throw new Error('Tag name is required');
598
+ }
599
+ const updatedCount = this.db.removeTag(params.tag.trim());
600
+ const success = updatedCount > 0;
601
+ const resultText = success
602
+ ? `🗑️ Removed tag "${params.tag}" from ${updatedCount} evidence record${updatedCount > 1 ? 's' : ''}`
603
+ : `⚠️ No evidence records found with tag "${params.tag}"`;
604
+ return {
605
+ content: [{ type: 'text', text: resultText }],
606
+ structuredContent: { updatedCount, success }
607
+ };
608
+ }
609
+ catch (error) {
610
+ throw new Error(`[Tool: remove-tag] ${getErrorMessage(error)}`);
611
+ }
612
+ });
613
+ // Get tag statistics tool
614
+ this.server.registerTool('get-tag-stats', {
615
+ title: 'Get Tag Statistics',
616
+ description: 'Get all unique tags with their usage counts',
617
+ inputSchema: {},
618
+ outputSchema: {
619
+ tags: z.array(z.object({
620
+ tag: z.string(),
621
+ count: z.number()
622
+ })),
623
+ totalTags: z.number()
624
+ }
625
+ }, async () => {
626
+ try {
627
+ const tagCounts = this.db.getTagCounts();
628
+ const tags = Array.from(tagCounts.entries())
629
+ .map(([tag, count]) => ({ tag, count }))
630
+ .sort((a, b) => b.count - a.count);
631
+ const resultText = tags.length > 0
632
+ ? `📊 Tag Statistics:\n${tags.map(t => ` • ${t.tag}: ${t.count}`).join('\n')}`
633
+ : `📊 No tags found in any evidence records`;
634
+ return {
635
+ content: [{ type: 'text', text: resultText }],
636
+ structuredContent: { tags, totalTags: tags.length }
637
+ };
638
+ }
639
+ catch (error) {
640
+ throw new Error(`[Tool: get-tag-stats] ${getErrorMessage(error)}`);
641
+ }
642
+ });
643
+ }
644
+ registerResources() {
645
+ this.server.registerResource('evidence', new ResourceTemplate('evidence://{id}', { list: undefined }), {
646
+ title: 'Evidence Content',
647
+ description: 'Access encrypted evidence record by ID',
648
+ mimeType: 'text/plain'
649
+ }, async (uri, { id }) => {
650
+ try {
651
+ const evidence = this.db.findById(id);
652
+ if (!evidence) {
653
+ throw new Error(`Evidence with ID ${id} not found`);
654
+ }
655
+ const key = await this.getDerivedKey();
656
+ const decrypted = decrypt(evidence.encryptedContent, evidence.nonce, key);
657
+ return {
658
+ contents: [{ uri: uri.href, mimeType: 'text/plain', text: decrypted }]
659
+ };
660
+ }
661
+ catch (error) {
662
+ throw new Error(`Failed to access evidence resource: ${getErrorMessage(error)}`);
663
+ }
664
+ });
665
+ }
666
+ async start() {
667
+ const transport = new StdioServerTransport();
668
+ await this.server.connect(transport);
669
+ }
670
+ }
671
+ async function main() {
672
+ const config = {
673
+ dbPath: process.env.EVIDENCEMCP_DB_PATH || './evidence.db',
674
+ password: process.env.EVIDENCEMCP_PASSWORD || ''
675
+ };
676
+ if (!config.password) {
677
+ console.error('Error: EVIDENCEMCP_PASSWORD environment variable required');
678
+ process.exit(1);
679
+ }
680
+ const server = new EvidenceMCPServer(config);
681
+ await server.start();
682
+ }
683
+ if (import.meta.url === `file://${process.argv[1]}`) {
684
+ main().catch((error) => {
685
+ console.error('Server error:', error);
686
+ process.exit(1);
687
+ });
688
+ }
689
+ export { EvidenceMCPTestHelpers } from './test-helpers.js';
690
+ //# sourceMappingURL=index.js.map