memory-journal-mcp 4.3.0 → 4.4.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/.dockerignore +131 -122
- package/.gitattributes +29 -0
- package/.github/workflows/docker-publish.yml +1 -1
- package/.github/workflows/lint-and-test.yml +1 -2
- package/.github/workflows/secrets-scanning.yml +0 -1
- package/.github/workflows/security-update.yml +6 -6
- package/.vscode/settings.json +17 -15
- package/CHANGELOG.md +1065 -11
- package/DOCKER_README.md +51 -33
- package/Dockerfile +14 -12
- package/README.md +68 -33
- package/SECURITY.md +225 -220
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +70 -26
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -0
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +6 -0
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +51 -10
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +143 -43
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +7 -1
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts +74 -2
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +508 -7
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/index.js +1 -0
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +257 -13
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +595 -8
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/server/McpServer.d.ts +2 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +69 -26
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/progress-utils.d.ts +18 -3
- package/dist/utils/progress-utils.d.ts.map +1 -1
- package/dist/utils/progress-utils.js.map +1 -1
- package/dist/utils/security-utils.d.ts +91 -0
- package/dist/utils/security-utils.d.ts.map +1 -0
- package/dist/utils/security-utils.js +184 -0
- package/dist/utils/security-utils.js.map +1 -0
- package/dist/vector/VectorSearchManager.d.ts +2 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +100 -34
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +46 -37
- package/mcp-config-example.json +0 -2
- package/package.json +21 -14
- package/releases/v4.3.1.md +69 -0
- package/releases/v4.4.0.md +120 -0
- package/server.json +3 -3
- package/src/cli.ts +11 -0
- package/src/constants/ServerInstructions.ts +70 -26
- package/src/constants/icons.ts +7 -0
- package/src/database/SqliteAdapter.ts +165 -44
- package/src/filtering/ToolFilter.ts +7 -1
- package/src/github/GitHubIntegration.ts +588 -8
- package/src/handlers/prompts/index.ts +1 -0
- package/src/handlers/resources/index.ts +318 -12
- package/src/handlers/tools/index.ts +686 -13
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/progress-utils.ts +17 -6
- package/src/utils/security-utils.ts +205 -0
- package/src/vector/VectorSearchManager.ts +110 -39
- package/tests/constants/icons.test.ts +102 -0
- package/tests/constants/server-instructions.test.ts +549 -0
- package/tests/database/sqlite-adapter.bench.ts +63 -0
- package/tests/database/sqlite-adapter.test.ts +555 -0
- package/tests/filtering/tool-filter.test.ts +266 -0
- package/tests/github/github-integration.test.ts +1024 -0
- package/tests/handlers/github-resource-handlers.test.ts +473 -0
- package/tests/handlers/github-tool-handlers.test.ts +556 -0
- package/tests/handlers/prompt-handlers.test.ts +91 -0
- package/tests/handlers/resource-handlers.test.ts +339 -0
- package/tests/handlers/tool-handlers.test.ts +497 -0
- package/tests/handlers/vector-tool-handlers.test.ts +238 -0
- package/tests/security/sql-injection.test.ts +347 -0
- package/tests/server/mcp-server.bench.ts +55 -0
- package/tests/server/mcp-server.test.ts +675 -0
- package/tests/utils/logger.test.ts +180 -0
- package/tests/utils/mcp-logger.test.ts +212 -0
- package/tests/utils/progress-utils.test.ts +156 -0
- package/tests/utils/security-utils.test.ts +82 -0
- package/tests/vector/vector-search-manager.test.ts +335 -0
- package/tests/vector/vector-search.bench.ts +53 -0
- package/vitest.config.ts +15 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- package/.github/workflows/dependabot-auto-merge.yml +0 -42
package/src/server/McpServer.ts
CHANGED
|
@@ -31,14 +31,22 @@ import { getPrompts, getPrompt } from '../handlers/prompts/index.js'
|
|
|
31
31
|
import { generateInstructions } from '../constants/ServerInstructions.js'
|
|
32
32
|
import pkg from '../../package.json' with { type: 'json' }
|
|
33
33
|
|
|
34
|
+
/** Session timeout for stateful HTTP mode (30 minutes) */
|
|
35
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000
|
|
36
|
+
|
|
37
|
+
/** Session timeout sweep interval (5 minutes) */
|
|
38
|
+
const SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1000
|
|
39
|
+
|
|
34
40
|
export interface ServerOptions {
|
|
35
41
|
transport: 'stdio' | 'http'
|
|
36
42
|
port?: number
|
|
43
|
+
host?: string
|
|
37
44
|
dbPath: string
|
|
38
45
|
toolFilter?: string
|
|
39
46
|
defaultProjectNumber?: number
|
|
40
47
|
autoRebuildIndex?: boolean
|
|
41
48
|
statelessHttp?: boolean
|
|
49
|
+
corsOrigin?: string
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/**
|
|
@@ -107,14 +115,13 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
107
115
|
}
|
|
108
116
|
: undefined
|
|
109
117
|
|
|
118
|
+
// Get all tools once (unfiltered) for both instruction generation and registration
|
|
119
|
+
const allTools = getTools(db, null, vectorManager, github, { defaultProjectNumber })
|
|
120
|
+
const allToolNames = new Set(allTools.map((t) => (t as { name: string }).name))
|
|
121
|
+
|
|
110
122
|
// Generate dynamic instructions based on enabled tools, resources, prompts, and latest entry
|
|
111
123
|
const instructions = generateInstructions(
|
|
112
|
-
filterConfig?.enabledTools ??
|
|
113
|
-
new Set(
|
|
114
|
-
getTools(db, null, vectorManager, github, { defaultProjectNumber }).map(
|
|
115
|
-
(t) => (t as { name: string }).name
|
|
116
|
-
)
|
|
117
|
-
),
|
|
124
|
+
filterConfig?.enabledTools ?? allToolNames,
|
|
118
125
|
resources.map((r) => {
|
|
119
126
|
const res = r as { uri: string; name: string; description?: string }
|
|
120
127
|
return { uri: res.uri, name: res.name, description: res.description }
|
|
@@ -140,8 +147,10 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
140
147
|
}
|
|
141
148
|
)
|
|
142
149
|
|
|
143
|
-
//
|
|
144
|
-
const tools =
|
|
150
|
+
// Apply filter to get the set of tools to register
|
|
151
|
+
const tools = filterConfig
|
|
152
|
+
? getTools(db, filterConfig, vectorManager, github, { defaultProjectNumber })
|
|
153
|
+
: allTools
|
|
145
154
|
for (const tool of tools) {
|
|
146
155
|
const toolDef = tool as {
|
|
147
156
|
name: string
|
|
@@ -394,12 +403,21 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
394
403
|
} else {
|
|
395
404
|
// HTTP transport with SSE support
|
|
396
405
|
const port = options.port ?? 3000
|
|
406
|
+
const host = options.host ?? 'localhost'
|
|
407
|
+
const corsOrigin = options.corsOrigin ?? process.env['MCP_CORS_ORIGIN'] ?? '*'
|
|
397
408
|
const app: Express = express()
|
|
398
409
|
|
|
399
|
-
//
|
|
410
|
+
// Security headers middleware
|
|
411
|
+
app.use((_req: Request, res: Response, next: () => void) => {
|
|
412
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
413
|
+
res.setHeader('X-Frame-Options', 'DENY')
|
|
414
|
+
next()
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// CORS middleware for browser-based clients (e.g., MCP Inspector)
|
|
418
|
+
// Origin is configurable via --cors-origin flag or MCP_CORS_ORIGIN env var
|
|
400
419
|
app.use((req: Request, res: Response, next: () => void) => {
|
|
401
|
-
|
|
402
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
420
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin)
|
|
403
421
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
404
422
|
res.setHeader(
|
|
405
423
|
'Access-Control-Allow-Headers',
|
|
@@ -416,22 +434,14 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
416
434
|
next()
|
|
417
435
|
})
|
|
418
436
|
|
|
419
|
-
// JSON body parser
|
|
420
|
-
app.use(express.json())
|
|
437
|
+
// JSON body parser with size limit to prevent memory exhaustion (DoS)
|
|
438
|
+
app.use(express.json({ limit: '1mb' }))
|
|
421
439
|
|
|
422
440
|
// Explicit OPTIONS handler for /mcp - MUST be before other /mcp routes
|
|
423
441
|
// Using app.all to intercept before Express 5's auto-OPTIONS
|
|
424
442
|
app.all('/mcp', (req: Request, res: Response, next: () => void) => {
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
428
|
-
res.setHeader(
|
|
429
|
-
'Access-Control-Allow-Headers',
|
|
430
|
-
'Content-Type, Accept, mcp-session-id, Last-Event-ID, mcp-protocol-version'
|
|
431
|
-
)
|
|
432
|
-
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id')
|
|
433
|
-
|
|
434
|
-
// For OPTIONS, respond immediately with CORS headers
|
|
443
|
+
// CORS headers are already set by the middleware above.
|
|
444
|
+
// For OPTIONS, respond immediately.
|
|
435
445
|
if (req.method === 'OPTIONS') {
|
|
436
446
|
res.status(204).end()
|
|
437
447
|
return
|
|
@@ -479,11 +489,12 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
479
489
|
})
|
|
480
490
|
|
|
481
491
|
// Start HTTP server
|
|
482
|
-
const httpServer = app.listen(port, () => {
|
|
492
|
+
const httpServer = app.listen(port, host, () => {
|
|
483
493
|
logger.info('MCP server started on HTTP (stateless)', {
|
|
484
494
|
module: 'McpServer',
|
|
485
495
|
port,
|
|
486
|
-
|
|
496
|
+
host,
|
|
497
|
+
endpoint: `http://${host}:${port}/mcp`,
|
|
487
498
|
})
|
|
488
499
|
})
|
|
489
500
|
|
|
@@ -521,8 +532,37 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
521
532
|
// === STATEFUL MODE ===
|
|
522
533
|
// Session-based transport with SSE support for notifications
|
|
523
534
|
|
|
524
|
-
// Session transport storage
|
|
535
|
+
// Session transport storage with last-activity timestamps
|
|
525
536
|
const transports = new Map<string, StreamableHTTPServerTransport>()
|
|
537
|
+
const sessionLastActivity = new Map<string, number>()
|
|
538
|
+
|
|
539
|
+
/** Update the last-activity timestamp for a session */
|
|
540
|
+
const touchSession = (sid: string): void => {
|
|
541
|
+
sessionLastActivity.set(sid, Date.now())
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Sweep expired sessions (called periodically) */
|
|
545
|
+
const sweepExpiredSessions = (): void => {
|
|
546
|
+
const now = Date.now()
|
|
547
|
+
for (const [sid, lastActivity] of sessionLastActivity) {
|
|
548
|
+
if (now - lastActivity > SESSION_TIMEOUT_MS && transports.has(sid)) {
|
|
549
|
+
logger.info('Expiring idle HTTP session', {
|
|
550
|
+
module: 'McpServer',
|
|
551
|
+
sessionId: sid,
|
|
552
|
+
idleMinutes: Math.round((now - lastActivity) / 60_000),
|
|
553
|
+
})
|
|
554
|
+
const t = transports.get(sid)
|
|
555
|
+
if (t) {
|
|
556
|
+
void t.close()
|
|
557
|
+
}
|
|
558
|
+
transports.delete(sid)
|
|
559
|
+
sessionLastActivity.delete(sid)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Start session timeout sweep (runs every 5 minutes)
|
|
565
|
+
const sessionSweepTimer = setInterval(sweepExpiredSessions, SESSION_SWEEP_INTERVAL_MS)
|
|
526
566
|
|
|
527
567
|
// POST /mcp - Handle JSON-RPC requests
|
|
528
568
|
app.post('/mcp', (req: Request, res: Response): void => {
|
|
@@ -533,7 +573,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
533
573
|
let httpTransport: StreamableHTTPServerTransport | undefined
|
|
534
574
|
|
|
535
575
|
if (sessionId && transports.has(sessionId)) {
|
|
536
|
-
// Reuse existing transport
|
|
576
|
+
// Reuse existing transport and refresh session activity
|
|
577
|
+
touchSession(sessionId)
|
|
537
578
|
httpTransport = transports.get(sessionId)
|
|
538
579
|
} else if (sessionId === undefined && isInitializeRequest(req.body)) {
|
|
539
580
|
// New initialization request - create transport
|
|
@@ -545,6 +586,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
545
586
|
sessionId: sid,
|
|
546
587
|
})
|
|
547
588
|
transports.set(sid, newTransport)
|
|
589
|
+
touchSession(sid)
|
|
548
590
|
},
|
|
549
591
|
})
|
|
550
592
|
|
|
@@ -557,6 +599,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
557
599
|
sessionId: sid,
|
|
558
600
|
})
|
|
559
601
|
transports.delete(sid)
|
|
602
|
+
sessionLastActivity.delete(sid)
|
|
560
603
|
}
|
|
561
604
|
}
|
|
562
605
|
|
|
@@ -614,6 +657,9 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
614
657
|
return
|
|
615
658
|
}
|
|
616
659
|
|
|
660
|
+
// Refresh session activity on SSE reconnect
|
|
661
|
+
touchSession(sessionId)
|
|
662
|
+
|
|
617
663
|
const lastEventId = req.headers['last-event-id']
|
|
618
664
|
if (lastEventId !== undefined) {
|
|
619
665
|
logger.debug('Client reconnecting with Last-Event-ID', {
|
|
@@ -656,11 +702,12 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
656
702
|
})
|
|
657
703
|
|
|
658
704
|
// Start HTTP server
|
|
659
|
-
const httpServer = app.listen(port, () => {
|
|
705
|
+
const httpServer = app.listen(port, host, () => {
|
|
660
706
|
logger.info('MCP server started on HTTP (stateful)', {
|
|
661
707
|
module: 'McpServer',
|
|
662
708
|
port,
|
|
663
|
-
|
|
709
|
+
host,
|
|
710
|
+
endpoint: `http://${host}:${port}/mcp`,
|
|
664
711
|
})
|
|
665
712
|
})
|
|
666
713
|
|
|
@@ -689,6 +736,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
689
736
|
}
|
|
690
737
|
}
|
|
691
738
|
transports.clear()
|
|
739
|
+
sessionLastActivity.clear()
|
|
740
|
+
clearInterval(sessionSweepTimer)
|
|
692
741
|
|
|
693
742
|
httpServer.close()
|
|
694
743
|
db.close()
|
|
@@ -697,14 +746,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
697
746
|
})()
|
|
698
747
|
})
|
|
699
748
|
|
|
700
|
-
//
|
|
701
|
-
// setInterval keeps the event loop active and prevents exit
|
|
702
|
-
setInterval(
|
|
703
|
-
() => {
|
|
704
|
-
// Heartbeat - keeps event loop active
|
|
705
|
-
},
|
|
706
|
-
1000 * 60 * 60
|
|
707
|
-
) // 1 hour interval (just needs to exist)
|
|
749
|
+
// sessionSweepTimer keeps the event loop active (no additional heartbeat needed)
|
|
708
750
|
}
|
|
709
751
|
}
|
|
710
752
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -266,6 +266,17 @@ export interface JournalEntry {
|
|
|
266
266
|
autoContext: string | null
|
|
267
267
|
deletedAt: string | null
|
|
268
268
|
tags: string[]
|
|
269
|
+
// GitHub integration fields
|
|
270
|
+
projectNumber?: number | null
|
|
271
|
+
projectOwner?: string | null
|
|
272
|
+
issueNumber?: number | null
|
|
273
|
+
issueUrl?: string | null
|
|
274
|
+
prNumber?: number | null
|
|
275
|
+
prUrl?: string | null
|
|
276
|
+
prStatus?: string | null
|
|
277
|
+
workflowRunId?: number | null
|
|
278
|
+
workflowName?: string | null
|
|
279
|
+
workflowStatus?: string | null
|
|
269
280
|
}
|
|
270
281
|
|
|
271
282
|
/**
|
|
@@ -298,6 +309,30 @@ export interface Embedding {
|
|
|
298
309
|
modelName: string
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Importance scoring breakdown showing weighted component contributions
|
|
314
|
+
*/
|
|
315
|
+
export interface ImportanceBreakdown {
|
|
316
|
+
/** Significance type contribution (weight: 0.30) */
|
|
317
|
+
significance: number
|
|
318
|
+
/** Relationship count contribution (weight: 0.35) */
|
|
319
|
+
relationships: number
|
|
320
|
+
/** Causal relationship contribution (weight: 0.20) */
|
|
321
|
+
causal: number
|
|
322
|
+
/** Recency decay contribution (weight: 0.15) */
|
|
323
|
+
recency: number
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Importance calculation result with total score and component breakdown
|
|
328
|
+
*/
|
|
329
|
+
export interface ImportanceResult {
|
|
330
|
+
/** Total importance score (0.0-1.0) */
|
|
331
|
+
score: number
|
|
332
|
+
/** Weighted component contributions */
|
|
333
|
+
breakdown: ImportanceBreakdown
|
|
334
|
+
}
|
|
335
|
+
|
|
301
336
|
// ============================================================================
|
|
302
337
|
// GitHub Integration Types
|
|
303
338
|
// ============================================================================
|
|
@@ -320,6 +355,7 @@ export interface GitHubIssue {
|
|
|
320
355
|
title: string
|
|
321
356
|
url: string
|
|
322
357
|
state: 'OPEN' | 'CLOSED'
|
|
358
|
+
milestone?: { number: number; title: string } | null
|
|
323
359
|
}
|
|
324
360
|
|
|
325
361
|
/**
|
|
@@ -332,6 +368,23 @@ export interface GitHubPullRequest {
|
|
|
332
368
|
state: 'OPEN' | 'CLOSED' | 'MERGED'
|
|
333
369
|
}
|
|
334
370
|
|
|
371
|
+
/**
|
|
372
|
+
* GitHub milestone information
|
|
373
|
+
*/
|
|
374
|
+
export interface GitHubMilestone {
|
|
375
|
+
number: number
|
|
376
|
+
title: string
|
|
377
|
+
description: string | null
|
|
378
|
+
state: 'open' | 'closed'
|
|
379
|
+
url: string
|
|
380
|
+
dueOn: string | null
|
|
381
|
+
openIssues: number
|
|
382
|
+
closedIssues: number
|
|
383
|
+
createdAt: string
|
|
384
|
+
updatedAt: string
|
|
385
|
+
creator: string | null
|
|
386
|
+
}
|
|
387
|
+
|
|
335
388
|
/**
|
|
336
389
|
* GitHub workflow run information
|
|
337
390
|
*/
|
|
@@ -359,6 +412,51 @@ export interface ProjectContext {
|
|
|
359
412
|
issues: GitHubIssue[]
|
|
360
413
|
pullRequests: GitHubPullRequest[]
|
|
361
414
|
workflowRuns: GitHubWorkflowRun[]
|
|
415
|
+
milestones: GitHubMilestone[]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Repository Insights/Traffic Types
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Repository statistics (stars, forks, watchers)
|
|
424
|
+
*/
|
|
425
|
+
export interface RepoStats {
|
|
426
|
+
stars: number
|
|
427
|
+
forks: number
|
|
428
|
+
watchers: number
|
|
429
|
+
openIssues: number
|
|
430
|
+
size: number // KB
|
|
431
|
+
defaultBranch: string
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Aggregated traffic data (14-day rolling)
|
|
436
|
+
*/
|
|
437
|
+
export interface TrafficData {
|
|
438
|
+
clones: { total: number; unique: number; dailyAvg: number }
|
|
439
|
+
views: { total: number; unique: number; dailyAvg: number }
|
|
440
|
+
period: string // e.g. "14 days"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Traffic referrer source
|
|
445
|
+
*/
|
|
446
|
+
export interface TrafficReferrer {
|
|
447
|
+
referrer: string
|
|
448
|
+
count: number
|
|
449
|
+
uniques: number
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Popular repository path
|
|
454
|
+
*/
|
|
455
|
+
export interface PopularPath {
|
|
456
|
+
path: string
|
|
457
|
+
title: string
|
|
458
|
+
count: number
|
|
459
|
+
uniques: number
|
|
362
460
|
}
|
|
363
461
|
|
|
364
462
|
// ============================================================================
|
package/src/utils/logger.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Centralized logging to stderr only (stdout reserved for MCP protocol).
|
|
5
5
|
* Follows RFC 5424 severity levels.
|
|
6
|
+
* Automatically sanitizes error fields to prevent token leakage.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
import { sanitizeErrorForLogging } from './security-utils.js'
|
|
10
|
+
|
|
8
11
|
type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical'
|
|
9
12
|
|
|
10
13
|
interface LogContext {
|
|
@@ -57,7 +60,13 @@ class Logger {
|
|
|
57
60
|
private log(level: LogLevel, message: string, context?: LogContext): void {
|
|
58
61
|
if (!this.shouldLog(level)) return
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
// Sanitize error fields to prevent token leakage in logs
|
|
64
|
+
let sanitizedContext = context
|
|
65
|
+
if (context?.['error'] != null && typeof context['error'] === 'string') {
|
|
66
|
+
sanitizedContext = { ...context, error: sanitizeErrorForLogging(context['error']) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const formatted = this.formatMessage(level, message, sanitizedContext)
|
|
61
70
|
|
|
62
71
|
// Always write to stderr (stdout is reserved for MCP protocol)
|
|
63
72
|
console.error(formatted)
|
|
@@ -5,18 +5,29 @@
|
|
|
5
5
|
* Follows MCP 2025-11-25 specification for notifications/progress.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Minimal interface for sending MCP notifications.
|
|
10
|
+
* Uses structural typing to avoid importing the deprecated Server class.
|
|
11
|
+
*/
|
|
12
|
+
interface NotificationSender {
|
|
13
|
+
notification(notification: {
|
|
14
|
+
method: 'notifications/progress'
|
|
15
|
+
params: {
|
|
16
|
+
progressToken: string | number
|
|
17
|
+
progress: number
|
|
18
|
+
total?: number
|
|
19
|
+
message?: string
|
|
20
|
+
}
|
|
21
|
+
}): Promise<void>
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
/** Progress token from client request _meta */
|
|
13
25
|
export type ProgressToken = string | number
|
|
14
26
|
|
|
15
27
|
/** Context required to send progress notifications */
|
|
16
28
|
export interface ProgressContext {
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
server: Server
|
|
29
|
+
/** Object with notification method for sending progress updates */
|
|
30
|
+
server: NotificationSender
|
|
20
31
|
/** Progress token from request _meta (if client requested progress) */
|
|
21
32
|
progressToken?: ProgressToken
|
|
22
33
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Utilities for Memory Journal MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Centralized security validation following MCP Security Patterns.
|
|
5
|
+
* Uses typed errors for consistent error handling across the codebase.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Typed Security Errors
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for security-related errors
|
|
14
|
+
*/
|
|
15
|
+
export class SecurityError extends Error {
|
|
16
|
+
readonly code: string
|
|
17
|
+
|
|
18
|
+
constructor(message: string, code: string) {
|
|
19
|
+
super(message)
|
|
20
|
+
this.name = 'SecurityError'
|
|
21
|
+
this.code = code
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when an invalid date format pattern is detected
|
|
27
|
+
*/
|
|
28
|
+
export class InvalidDateFormatError extends SecurityError {
|
|
29
|
+
constructor(value: string) {
|
|
30
|
+
super(`Invalid date format pattern: '${value}'`, 'INVALID_DATE_FORMAT')
|
|
31
|
+
this.name = 'InvalidDateFormatError'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when SQL injection patterns are detected in input
|
|
37
|
+
*/
|
|
38
|
+
export class SqlInjectionError extends SecurityError {
|
|
39
|
+
constructor(pattern: string) {
|
|
40
|
+
super(`Potential SQL injection detected: '${pattern}'`, 'SQL_INJECTION')
|
|
41
|
+
this.name = 'SqlInjectionError'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when path traversal is detected in input
|
|
47
|
+
*/
|
|
48
|
+
export class PathTraversalError extends SecurityError {
|
|
49
|
+
constructor(path: string) {
|
|
50
|
+
super(`Path traversal detected: '${path}'`, 'PATH_TRAVERSAL')
|
|
51
|
+
this.name = 'PathTraversalError'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Date Format Validation
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whitelist of allowed strftime format patterns for SQLite.
|
|
61
|
+
* These are the only patterns allowed to be interpolated into SQL.
|
|
62
|
+
*/
|
|
63
|
+
const ALLOWED_DATE_FORMATS: Record<string, string> = {
|
|
64
|
+
day: '%Y-%m-%d',
|
|
65
|
+
week: '%Y-W%W',
|
|
66
|
+
month: '%Y-%m',
|
|
67
|
+
} as const
|
|
68
|
+
|
|
69
|
+
export type DateGroupBy = 'day' | 'week' | 'month'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validates and returns a safe strftime format pattern.
|
|
73
|
+
*
|
|
74
|
+
* @param groupBy - The grouping period ('day', 'week', or 'month')
|
|
75
|
+
* @returns The validated strftime format pattern
|
|
76
|
+
* @throws InvalidDateFormatError if the groupBy value is invalid
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const format = validateDateFormatPattern('day') // Returns '%Y-%m-%d'
|
|
81
|
+
* const format = validateDateFormatPattern('invalid') // Throws InvalidDateFormatError
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function validateDateFormatPattern(groupBy: string): string {
|
|
85
|
+
const format = ALLOWED_DATE_FORMATS[groupBy]
|
|
86
|
+
if (!format) {
|
|
87
|
+
throw new InvalidDateFormatError(groupBy)
|
|
88
|
+
}
|
|
89
|
+
return format
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Search Query Sanitization
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Escapes special characters in LIKE patterns to prevent injection.
|
|
98
|
+
* SQLite LIKE uses % and _ as wildcards.
|
|
99
|
+
*
|
|
100
|
+
* @param query - The user-provided search query
|
|
101
|
+
* @returns Escaped query safe for use in LIKE patterns
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* sanitizeSearchQuery('100%') // Returns '100\\%'
|
|
106
|
+
* sanitizeSearchQuery('test_value') // Returns 'test\\_value'
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function sanitizeSearchQuery(query: string): string {
|
|
110
|
+
// Escape backslashes first, then LIKE wildcards
|
|
111
|
+
return query.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// SQL Injection Detection
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Patterns that indicate SQL injection attempts.
|
|
120
|
+
* Used for validation in edge cases where parameterized queries aren't possible.
|
|
121
|
+
*/
|
|
122
|
+
const SQL_INJECTION_PATTERNS = [
|
|
123
|
+
/;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|TRUNCATE)/i,
|
|
124
|
+
/--\s*/,
|
|
125
|
+
/\/\*[\s\S]*?\*\//,
|
|
126
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
127
|
+
/'\s*OR\s+['"]?1['"]?\s*=\s*['"]?1/i,
|
|
128
|
+
/ATTACH\s+DATABASE/i,
|
|
129
|
+
/DETACH\s+DATABASE/i,
|
|
130
|
+
/load_extension\s*\(/i,
|
|
131
|
+
] as const
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks if a string contains potential SQL injection patterns.
|
|
135
|
+
* This is a secondary defense layer; parameterized queries are the primary defense.
|
|
136
|
+
*
|
|
137
|
+
* @param input - The string to check
|
|
138
|
+
* @returns true if injection patterns are detected, false otherwise
|
|
139
|
+
*/
|
|
140
|
+
export function containsSqlInjection(input: string): boolean {
|
|
141
|
+
return SQL_INJECTION_PATTERNS.some((pattern) => pattern.test(input))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validates that input does not contain SQL injection patterns.
|
|
146
|
+
*
|
|
147
|
+
* @param input - The string to validate
|
|
148
|
+
* @throws SqlInjectionError if injection patterns are detected
|
|
149
|
+
*/
|
|
150
|
+
export function assertNoSqlInjection(input: string): void {
|
|
151
|
+
if (containsSqlInjection(input)) {
|
|
152
|
+
throw new SqlInjectionError(input.substring(0, 50))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Path Validation
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validates that a filename does not contain path traversal characters.
|
|
162
|
+
*
|
|
163
|
+
* @param filename - The filename to validate
|
|
164
|
+
* @throws PathTraversalError if path traversal is detected
|
|
165
|
+
*/
|
|
166
|
+
export function assertNoPathTraversal(filename: string): void {
|
|
167
|
+
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
|
168
|
+
throw new PathTraversalError(filename)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Error Message Sanitization
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Patterns that may contain sensitive tokens in error messages.
|
|
178
|
+
* Used to scrub error output before logging.
|
|
179
|
+
*/
|
|
180
|
+
const TOKEN_PATTERNS = [
|
|
181
|
+
// GitHub personal access tokens (classic and fine-grained)
|
|
182
|
+
/ghp_[A-Za-z0-9_]{36,}/g,
|
|
183
|
+
/github_pat_[A-Za-z0-9_]{82,}/g,
|
|
184
|
+
// Authorization headers in error dumps
|
|
185
|
+
/Authorization:\s*(?:token|Bearer)\s+\S+/gi,
|
|
186
|
+
// Generic Bearer tokens
|
|
187
|
+
/Bearer\s+[A-Za-z0-9._\-~+/]+=*/gi,
|
|
188
|
+
] as const
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sanitizes an error message by replacing any detected tokens with '[REDACTED]'.
|
|
192
|
+
* This is a defense-in-depth measure for error logging paths.
|
|
193
|
+
*
|
|
194
|
+
* @param message - The error message to sanitize
|
|
195
|
+
* @returns The sanitized message with tokens replaced
|
|
196
|
+
*/
|
|
197
|
+
export function sanitizeErrorForLogging(message: string): string {
|
|
198
|
+
let sanitized = message
|
|
199
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
200
|
+
// Reset lastIndex for global regex patterns
|
|
201
|
+
pattern.lastIndex = 0
|
|
202
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]')
|
|
203
|
+
}
|
|
204
|
+
return sanitized
|
|
205
|
+
}
|