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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the stderr Logger class: level filtering, formatting, sanitization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import { logger } from '../../src/utils/logger.js'
|
|
9
|
+
|
|
10
|
+
describe('Logger', () => {
|
|
11
|
+
let errorSpy: ReturnType<typeof vi.spyOn>
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
errorSpy.mockRestore()
|
|
19
|
+
// Reset to default level
|
|
20
|
+
logger.setLevel('info')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// ========================================================================
|
|
24
|
+
// Level filtering
|
|
25
|
+
// ========================================================================
|
|
26
|
+
|
|
27
|
+
describe('level filtering', () => {
|
|
28
|
+
it('should log messages at or below the configured level', () => {
|
|
29
|
+
logger.setLevel('info')
|
|
30
|
+
logger.info('info message')
|
|
31
|
+
logger.error('error message')
|
|
32
|
+
|
|
33
|
+
expect(errorSpy).toHaveBeenCalledTimes(2)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should suppress messages above the configured level', () => {
|
|
37
|
+
logger.setLevel('error')
|
|
38
|
+
logger.debug('debug message')
|
|
39
|
+
logger.info('info message')
|
|
40
|
+
logger.notice('notice message')
|
|
41
|
+
logger.warning('warning message')
|
|
42
|
+
|
|
43
|
+
expect(errorSpy).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should allow debug messages at debug level', () => {
|
|
47
|
+
logger.setLevel('debug')
|
|
48
|
+
logger.debug('debug message')
|
|
49
|
+
|
|
50
|
+
expect(errorSpy).toHaveBeenCalledTimes(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should log critical messages at any level', () => {
|
|
54
|
+
logger.setLevel('critical')
|
|
55
|
+
logger.critical('critical failure')
|
|
56
|
+
|
|
57
|
+
expect(errorSpy).toHaveBeenCalledTimes(1)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ========================================================================
|
|
62
|
+
// Formatting
|
|
63
|
+
// ========================================================================
|
|
64
|
+
|
|
65
|
+
describe('formatting', () => {
|
|
66
|
+
it('should include timestamp, level, and message', () => {
|
|
67
|
+
logger.info('test message')
|
|
68
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
69
|
+
|
|
70
|
+
// [timestamp] [LEVEL ] message
|
|
71
|
+
expect(output).toMatch(/^\[.+\] \[INFO\s+\]/)
|
|
72
|
+
expect(output).toContain('test message')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should include module when provided', () => {
|
|
76
|
+
logger.info('test', { module: 'TestModule' })
|
|
77
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
78
|
+
|
|
79
|
+
expect(output).toContain('[TestModule]')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should include operation when provided', () => {
|
|
83
|
+
logger.info('test', { operation: 'doSomething' })
|
|
84
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
85
|
+
|
|
86
|
+
expect(output).toContain('[doSomething]')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should include extra context fields as JSON', () => {
|
|
90
|
+
logger.info('test', { module: 'M', entityId: 42 })
|
|
91
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
92
|
+
|
|
93
|
+
expect(output).toContain('"entityId":42')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should omit extras JSON when no extra fields', () => {
|
|
97
|
+
logger.info('plain message')
|
|
98
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
99
|
+
|
|
100
|
+
// Should not contain JSON braces (from extras)
|
|
101
|
+
expect(output).not.toContain('{')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ========================================================================
|
|
106
|
+
// Sanitization
|
|
107
|
+
// ========================================================================
|
|
108
|
+
|
|
109
|
+
describe('sanitization', () => {
|
|
110
|
+
it('should sanitize error field containing tokens', () => {
|
|
111
|
+
const token = 'ghp_' + 'X'.repeat(36)
|
|
112
|
+
logger.error('Request failed', {
|
|
113
|
+
module: 'GitHub',
|
|
114
|
+
error: `Token ${token} expired`,
|
|
115
|
+
})
|
|
116
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
117
|
+
|
|
118
|
+
expect(output).not.toContain(token)
|
|
119
|
+
expect(output).toContain('[REDACTED]')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should not sanitize non-string error fields', () => {
|
|
123
|
+
logger.error('Error occurred', {
|
|
124
|
+
module: 'DB',
|
|
125
|
+
error: 500,
|
|
126
|
+
})
|
|
127
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
128
|
+
|
|
129
|
+
expect(output).toContain('"error":500')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// ========================================================================
|
|
134
|
+
// setLevel
|
|
135
|
+
// ========================================================================
|
|
136
|
+
|
|
137
|
+
describe('setLevel', () => {
|
|
138
|
+
it('should change the minimum log level', () => {
|
|
139
|
+
logger.setLevel('critical')
|
|
140
|
+
logger.error('should not appear')
|
|
141
|
+
expect(errorSpy).not.toHaveBeenCalled()
|
|
142
|
+
|
|
143
|
+
logger.setLevel('error')
|
|
144
|
+
logger.error('should appear')
|
|
145
|
+
expect(errorSpy).toHaveBeenCalledTimes(1)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ========================================================================
|
|
150
|
+
// Convenience methods
|
|
151
|
+
// ========================================================================
|
|
152
|
+
|
|
153
|
+
describe('convenience methods', () => {
|
|
154
|
+
it('should log via debug()', () => {
|
|
155
|
+
logger.setLevel('debug')
|
|
156
|
+
logger.debug('debug msg')
|
|
157
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
158
|
+
expect(output).toContain('DEBUG')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should log via notice()', () => {
|
|
162
|
+
logger.notice('notice msg')
|
|
163
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
164
|
+
expect(output).toContain('NOTICE')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should log via warning()', () => {
|
|
168
|
+
logger.warning('warning msg')
|
|
169
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
170
|
+
expect(output).toContain('WARNING')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should log via critical()', () => {
|
|
174
|
+
logger.setLevel('critical')
|
|
175
|
+
logger.critical('critical msg')
|
|
176
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
177
|
+
expect(output).toContain('CRITICAL')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpLogger Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the MCP Protocol Logger: level filtering, stderr fallback, MCP send.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import { McpLogger, mcpLogger } from '../../src/utils/McpLogger.js'
|
|
9
|
+
|
|
10
|
+
describe('McpLogger', () => {
|
|
11
|
+
let mcpLog: McpLogger
|
|
12
|
+
let errorSpy: ReturnType<typeof vi.spyOn>
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mcpLog = new McpLogger()
|
|
16
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
errorSpy.mockRestore()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// ========================================================================
|
|
24
|
+
// Level management
|
|
25
|
+
// ========================================================================
|
|
26
|
+
|
|
27
|
+
describe('setLevel / getLevel', () => {
|
|
28
|
+
it('should default to info level', () => {
|
|
29
|
+
expect(mcpLog.getLevel()).toBe('info')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should set and get level correctly', () => {
|
|
33
|
+
mcpLog.setLevel('debug')
|
|
34
|
+
expect(mcpLog.getLevel()).toBe('debug')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should accept all valid MCP log levels', () => {
|
|
38
|
+
const levels = [
|
|
39
|
+
'debug',
|
|
40
|
+
'info',
|
|
41
|
+
'notice',
|
|
42
|
+
'warning',
|
|
43
|
+
'error',
|
|
44
|
+
'critical',
|
|
45
|
+
'alert',
|
|
46
|
+
'emergency',
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
for (const level of levels) {
|
|
50
|
+
mcpLog.setLevel(level)
|
|
51
|
+
expect(mcpLog.getLevel()).toBe(level)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ========================================================================
|
|
57
|
+
// Level filtering
|
|
58
|
+
// ========================================================================
|
|
59
|
+
|
|
60
|
+
describe('level filtering', () => {
|
|
61
|
+
it('should log messages at or below configured severity', () => {
|
|
62
|
+
mcpLog.setLevel('info')
|
|
63
|
+
mcpLog.log('info', 'test', { message: 'info msg' })
|
|
64
|
+
mcpLog.log('error', 'test', { message: 'error msg' })
|
|
65
|
+
|
|
66
|
+
expect(errorSpy).toHaveBeenCalledTimes(2)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should suppress messages above configured severity', () => {
|
|
70
|
+
mcpLog.setLevel('error')
|
|
71
|
+
mcpLog.log('info', 'test', { message: 'should be suppressed' })
|
|
72
|
+
mcpLog.log('debug', 'test', { message: 'also suppressed' })
|
|
73
|
+
|
|
74
|
+
expect(errorSpy).not.toHaveBeenCalled()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should log emergency at any level', () => {
|
|
78
|
+
mcpLog.setLevel('emergency')
|
|
79
|
+
mcpLog.log('emergency', 'test', { message: 'critical system failure' })
|
|
80
|
+
|
|
81
|
+
expect(errorSpy).toHaveBeenCalledTimes(1)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ========================================================================
|
|
86
|
+
// stderr fallback (no server)
|
|
87
|
+
// ========================================================================
|
|
88
|
+
|
|
89
|
+
describe('stderr fallback', () => {
|
|
90
|
+
it('should format output with timestamp, level, and logger name', () => {
|
|
91
|
+
mcpLog.log('info', 'MyModule', { message: 'hello world' })
|
|
92
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
93
|
+
|
|
94
|
+
expect(output).toMatch(/^\[.+\] \[INFO\s+\]/)
|
|
95
|
+
expect(output).toContain('[MyModule]')
|
|
96
|
+
expect(output).toContain('hello world')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should include extra context in stderr output', () => {
|
|
100
|
+
mcpLog.log('warning', 'DB', {
|
|
101
|
+
message: 'Slow query',
|
|
102
|
+
operation: 'SELECT',
|
|
103
|
+
duration: 2500,
|
|
104
|
+
})
|
|
105
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
106
|
+
|
|
107
|
+
expect(output).toContain('"operation":"SELECT"')
|
|
108
|
+
expect(output).toContain('"duration":2500')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ========================================================================
|
|
113
|
+
// MCP server integration
|
|
114
|
+
// ========================================================================
|
|
115
|
+
|
|
116
|
+
describe('MCP server send', () => {
|
|
117
|
+
it('should send via MCP protocol when server is set', () => {
|
|
118
|
+
const mockServer = {
|
|
119
|
+
sendLoggingMessage: vi.fn(),
|
|
120
|
+
}
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
mcpLog.setServer(mockServer as any)
|
|
123
|
+
|
|
124
|
+
mcpLog.log('info', 'test', { message: 'via MCP' })
|
|
125
|
+
|
|
126
|
+
expect(mockServer.sendLoggingMessage).toHaveBeenCalledWith({
|
|
127
|
+
level: 'info',
|
|
128
|
+
logger: 'test',
|
|
129
|
+
data: { message: 'via MCP' },
|
|
130
|
+
})
|
|
131
|
+
// Should also log to stderr
|
|
132
|
+
expect(errorSpy).toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should fall back to stderr if MCP send throws', () => {
|
|
136
|
+
const mockServer = {
|
|
137
|
+
sendLoggingMessage: vi.fn(() => {
|
|
138
|
+
throw new Error('Transport error')
|
|
139
|
+
}),
|
|
140
|
+
}
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
mcpLog.setServer(mockServer as any)
|
|
143
|
+
|
|
144
|
+
// Should not throw
|
|
145
|
+
mcpLog.log('error', 'test', { message: 'fallback test' })
|
|
146
|
+
|
|
147
|
+
// Should still log to stderr (fallback + always-log)
|
|
148
|
+
expect(errorSpy).toHaveBeenCalled()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// ========================================================================
|
|
153
|
+
// Convenience methods
|
|
154
|
+
// ========================================================================
|
|
155
|
+
|
|
156
|
+
describe('convenience methods', () => {
|
|
157
|
+
it('should log via debug()', () => {
|
|
158
|
+
mcpLog.setLevel('debug')
|
|
159
|
+
mcpLog.debug('mod', 'debug message')
|
|
160
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
161
|
+
expect(output).toContain('DEBUG')
|
|
162
|
+
expect(output).toContain('debug message')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should log via info()', () => {
|
|
166
|
+
mcpLog.info('mod', 'info message')
|
|
167
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
168
|
+
expect(output).toContain('INFO')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should log via notice()', () => {
|
|
172
|
+
mcpLog.notice('mod', 'notice message')
|
|
173
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
174
|
+
expect(output).toContain('NOTICE')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should log via warning()', () => {
|
|
178
|
+
mcpLog.warning('mod', 'warning message')
|
|
179
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
180
|
+
expect(output).toContain('WARNING')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should log via error()', () => {
|
|
184
|
+
mcpLog.error('mod', 'error message')
|
|
185
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
186
|
+
expect(output).toContain('ERROR')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should log via critical()', () => {
|
|
190
|
+
mcpLog.setLevel('critical')
|
|
191
|
+
mcpLog.critical('mod', 'critical message')
|
|
192
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
193
|
+
expect(output).toContain('CRITICAL')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should pass context through convenience methods', () => {
|
|
197
|
+
mcpLog.info('mod', 'msg', { extra: 'data' })
|
|
198
|
+
const output = errorSpy.mock.calls[0]?.[0] as string
|
|
199
|
+
expect(output).toContain('"extra":"data"')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// ========================================================================
|
|
204
|
+
// Singleton
|
|
205
|
+
// ========================================================================
|
|
206
|
+
|
|
207
|
+
describe('singleton', () => {
|
|
208
|
+
it('should export mcpLogger as a McpLogger instance', () => {
|
|
209
|
+
expect(mcpLogger).toBeInstanceOf(McpLogger)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Utilities Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests sendProgress and createBatchProgressReporter with mock NotificationSender.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
8
|
+
import { sendProgress, createBatchProgressReporter } from '../../src/utils/progress-utils.js'
|
|
9
|
+
import type { ProgressContext } from '../../src/utils/progress-utils.js'
|
|
10
|
+
|
|
11
|
+
/** Create a mock ProgressContext */
|
|
12
|
+
function createMockContext(token?: string | number): {
|
|
13
|
+
ctx: ProgressContext
|
|
14
|
+
notifications: { progress: number; total?: number; message?: string }[]
|
|
15
|
+
} {
|
|
16
|
+
const notifications: { progress: number; total?: number; message?: string }[] = []
|
|
17
|
+
const ctx: ProgressContext = {
|
|
18
|
+
server: {
|
|
19
|
+
notification: vi.fn(async (n) => {
|
|
20
|
+
notifications.push(n.params)
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
progressToken: token,
|
|
24
|
+
}
|
|
25
|
+
return { ctx, notifications }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// sendProgress
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
describe('sendProgress', () => {
|
|
33
|
+
it('should no-op when context is undefined', async () => {
|
|
34
|
+
// Should not throw
|
|
35
|
+
await sendProgress(undefined, 5, 10, 'working')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should no-op when progressToken is undefined', async () => {
|
|
39
|
+
const { ctx } = createMockContext(undefined)
|
|
40
|
+
await sendProgress(ctx, 5, 10, 'working')
|
|
41
|
+
expect(ctx.server.notification).not.toHaveBeenCalled()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should send notification with correct shape', async () => {
|
|
45
|
+
const { ctx, notifications } = createMockContext('token-1')
|
|
46
|
+
await sendProgress(ctx, 3, 10, 'Processing')
|
|
47
|
+
|
|
48
|
+
expect(notifications).toHaveLength(1)
|
|
49
|
+
expect(notifications[0]).toEqual({
|
|
50
|
+
progressToken: 'token-1',
|
|
51
|
+
progress: 3,
|
|
52
|
+
total: 10,
|
|
53
|
+
message: 'Processing',
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should omit total when undefined', async () => {
|
|
58
|
+
const { ctx, notifications } = createMockContext('t2')
|
|
59
|
+
await sendProgress(ctx, 5, undefined, 'step')
|
|
60
|
+
|
|
61
|
+
expect(notifications[0]).not.toHaveProperty('total')
|
|
62
|
+
expect(notifications[0]?.progress).toBe(5)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should omit message when undefined', async () => {
|
|
66
|
+
const { ctx, notifications } = createMockContext('t3')
|
|
67
|
+
await sendProgress(ctx, 7, 10)
|
|
68
|
+
|
|
69
|
+
expect(notifications[0]).not.toHaveProperty('message')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should omit message when empty string', async () => {
|
|
73
|
+
const { ctx, notifications } = createMockContext('t4')
|
|
74
|
+
await sendProgress(ctx, 1, 5, '')
|
|
75
|
+
|
|
76
|
+
expect(notifications[0]).not.toHaveProperty('message')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should accept numeric progressToken', async () => {
|
|
80
|
+
const { ctx, notifications } = createMockContext(42)
|
|
81
|
+
await sendProgress(ctx, 1, 1, 'done')
|
|
82
|
+
|
|
83
|
+
expect(notifications[0]?.progressToken).toBe(42)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should silently handle notification errors', async () => {
|
|
87
|
+
const ctx: ProgressContext = {
|
|
88
|
+
server: {
|
|
89
|
+
notification: vi.fn(async () => {
|
|
90
|
+
throw new Error('Transport closed')
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
progressToken: 'token',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Should not throw
|
|
97
|
+
await sendProgress(ctx, 1, 10, 'test')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// createBatchProgressReporter
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
describe('createBatchProgressReporter', () => {
|
|
106
|
+
it('should report at throttle intervals', async () => {
|
|
107
|
+
const { ctx, notifications } = createMockContext('batch-1')
|
|
108
|
+
const report = createBatchProgressReporter(ctx, 100, 10)
|
|
109
|
+
|
|
110
|
+
// Report items 1 through 30
|
|
111
|
+
for (let i = 1; i <= 30; i++) {
|
|
112
|
+
await report(i, `Item ${i}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Should have reported at 10, 20, 30
|
|
116
|
+
expect(notifications).toHaveLength(3)
|
|
117
|
+
expect(notifications[0]?.progress).toBe(10)
|
|
118
|
+
expect(notifications[1]?.progress).toBe(20)
|
|
119
|
+
expect(notifications[2]?.progress).toBe(30)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should always report on completion', async () => {
|
|
123
|
+
const { ctx, notifications } = createMockContext('batch-2')
|
|
124
|
+
const total = 15
|
|
125
|
+
const report = createBatchProgressReporter(ctx, total, 10)
|
|
126
|
+
|
|
127
|
+
// Report items 1 through 15 (total)
|
|
128
|
+
for (let i = 1; i <= total; i++) {
|
|
129
|
+
await report(i)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Should report at 10 and 15 (completion)
|
|
133
|
+
expect(notifications).toHaveLength(2)
|
|
134
|
+
expect(notifications[1]?.progress).toBe(15)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should skip intermediate items below throttle', async () => {
|
|
138
|
+
const { ctx, notifications } = createMockContext('batch-3')
|
|
139
|
+
const report = createBatchProgressReporter(ctx, 100, 20)
|
|
140
|
+
|
|
141
|
+
await report(5)
|
|
142
|
+
await report(10)
|
|
143
|
+
await report(15)
|
|
144
|
+
|
|
145
|
+
expect(notifications).toHaveLength(0)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should work with undefined context', async () => {
|
|
149
|
+
const report = createBatchProgressReporter(undefined, 50, 10)
|
|
150
|
+
|
|
151
|
+
// Should not throw
|
|
152
|
+
for (let i = 1; i <= 50; i++) {
|
|
153
|
+
await report(i)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Utilities Tests - sanitizeErrorForLogging
|
|
3
|
+
*
|
|
4
|
+
* Covers the token scrubbing function not tested by sql-injection.test.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
import { sanitizeErrorForLogging } from '../../src/utils/security-utils.js'
|
|
9
|
+
|
|
10
|
+
describe('sanitizeErrorForLogging', () => {
|
|
11
|
+
it('should redact GitHub classic PATs (ghp_)', () => {
|
|
12
|
+
const token = 'ghp_' + 'A'.repeat(36)
|
|
13
|
+
const message = `Request failed: token ${token} is invalid`
|
|
14
|
+
const result = sanitizeErrorForLogging(message)
|
|
15
|
+
|
|
16
|
+
expect(result).not.toContain(token)
|
|
17
|
+
expect(result).toContain('[REDACTED]')
|
|
18
|
+
expect(result).toBe('Request failed: token [REDACTED] is invalid')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should redact GitHub fine-grained PATs (github_pat_)', () => {
|
|
22
|
+
const token = 'github_pat_' + 'B'.repeat(82)
|
|
23
|
+
const message = `Auth error with ${token}`
|
|
24
|
+
const result = sanitizeErrorForLogging(message)
|
|
25
|
+
|
|
26
|
+
expect(result).not.toContain(token)
|
|
27
|
+
expect(result).toContain('[REDACTED]')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should redact Authorization headers with token', () => {
|
|
31
|
+
const message =
|
|
32
|
+
'Failed request with Authorization: token ghp_secret123456789012345678901234567890'
|
|
33
|
+
const result = sanitizeErrorForLogging(message)
|
|
34
|
+
|
|
35
|
+
expect(result).toContain('[REDACTED]')
|
|
36
|
+
expect(result).not.toContain('ghp_secret')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should redact Authorization headers with Bearer', () => {
|
|
40
|
+
const message = 'Error: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig'
|
|
41
|
+
const result = sanitizeErrorForLogging(message)
|
|
42
|
+
|
|
43
|
+
expect(result).toContain('[REDACTED]')
|
|
44
|
+
expect(result).not.toContain('eyJhbGciOiJIUzI1NiJ9')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should redact generic Bearer tokens', () => {
|
|
48
|
+
const message = 'Token expired: Bearer abc123.def456.ghi789+/=='
|
|
49
|
+
const result = sanitizeErrorForLogging(message)
|
|
50
|
+
|
|
51
|
+
expect(result).toContain('[REDACTED]')
|
|
52
|
+
expect(result).not.toContain('abc123.def456')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should pass through clean messages unchanged', () => {
|
|
56
|
+
const message = 'Database connection failed at localhost:5432'
|
|
57
|
+
expect(sanitizeErrorForLogging(message)).toBe(message)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should pass through empty string', () => {
|
|
61
|
+
expect(sanitizeErrorForLogging('')).toBe('')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should handle multiple tokens in one message', () => {
|
|
65
|
+
const token1 = 'ghp_' + 'C'.repeat(36)
|
|
66
|
+
const token2 = 'ghp_' + 'D'.repeat(36)
|
|
67
|
+
const message = `Token1: ${token1}, Token2: ${token2}`
|
|
68
|
+
const result = sanitizeErrorForLogging(message)
|
|
69
|
+
|
|
70
|
+
expect(result).not.toContain(token1)
|
|
71
|
+
expect(result).not.toContain(token2)
|
|
72
|
+
expect(result).toBe('Token1: [REDACTED], Token2: [REDACTED]')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should be idempotent - calling twice yields same result', () => {
|
|
76
|
+
const token = 'ghp_' + 'E'.repeat(36)
|
|
77
|
+
const message = `Error with ${token}`
|
|
78
|
+
const first = sanitizeErrorForLogging(message)
|
|
79
|
+
const second = sanitizeErrorForLogging(first)
|
|
80
|
+
expect(first).toBe(second)
|
|
81
|
+
})
|
|
82
|
+
})
|