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,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Handler Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests getResources listing and readResource for non-GitHub resources.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
8
|
+
import { getResources, readResource } from '../../src/handlers/resources/index.js'
|
|
9
|
+
import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
|
|
10
|
+
|
|
11
|
+
describe('Resource Handlers', () => {
|
|
12
|
+
let db: SqliteAdapter
|
|
13
|
+
const testDbPath = './test-resources.db'
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
db = new SqliteAdapter(testDbPath)
|
|
17
|
+
await db.initialize()
|
|
18
|
+
|
|
19
|
+
// Seed test data
|
|
20
|
+
const e1 = db.createEntry({
|
|
21
|
+
content: 'Resource test entry alpha',
|
|
22
|
+
tags: ['res-tag'],
|
|
23
|
+
significanceType: 'milestone',
|
|
24
|
+
})
|
|
25
|
+
const e2 = db.createEntry({
|
|
26
|
+
content: 'Resource test entry beta',
|
|
27
|
+
isPersonal: false,
|
|
28
|
+
projectNumber: 42,
|
|
29
|
+
issueNumber: 7,
|
|
30
|
+
})
|
|
31
|
+
const e3 = db.createEntry({
|
|
32
|
+
content: 'Resource test entry gamma',
|
|
33
|
+
prNumber: 15,
|
|
34
|
+
})
|
|
35
|
+
db.linkEntries(e1.id, e2.id, 'references', 'Related work')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
db.close()
|
|
40
|
+
try {
|
|
41
|
+
const fs = require('node:fs')
|
|
42
|
+
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore cleanup errors
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ========================================================================
|
|
49
|
+
// getResources
|
|
50
|
+
// ========================================================================
|
|
51
|
+
|
|
52
|
+
describe('getResources', () => {
|
|
53
|
+
it('should return non-empty array of resource definitions', () => {
|
|
54
|
+
const resources = getResources()
|
|
55
|
+
expect(Array.isArray(resources)).toBe(true)
|
|
56
|
+
expect(resources.length).toBeGreaterThan(10)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should have uri, name, and description on each resource', () => {
|
|
60
|
+
const resources = getResources()
|
|
61
|
+
for (const r of resources) {
|
|
62
|
+
const res = r as { uri: string; name: string; description: string }
|
|
63
|
+
expect(typeof res.uri).toBe('string')
|
|
64
|
+
expect(res.uri).toMatch(/^memory:\/\//)
|
|
65
|
+
expect(typeof res.name).toBe('string')
|
|
66
|
+
expect(typeof res.description).toBe('string')
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should include known resource URIs', () => {
|
|
71
|
+
const resources = getResources()
|
|
72
|
+
const uris = resources.map((r) => (r as { uri: string }).uri)
|
|
73
|
+
|
|
74
|
+
expect(uris).toContain('memory://briefing')
|
|
75
|
+
expect(uris).toContain('memory://health')
|
|
76
|
+
expect(uris).toContain('memory://recent')
|
|
77
|
+
expect(uris).toContain('memory://significant')
|
|
78
|
+
expect(uris).toContain('memory://tags')
|
|
79
|
+
expect(uris).toContain('memory://team/recent')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should include template resources', () => {
|
|
83
|
+
const resources = getResources()
|
|
84
|
+
const uris = resources.map((r) => (r as { uri: string }).uri)
|
|
85
|
+
|
|
86
|
+
expect(uris).toContain('memory://projects/{number}/timeline')
|
|
87
|
+
expect(uris).toContain('memory://issues/{issue_number}/entries')
|
|
88
|
+
expect(uris).toContain('memory://prs/{pr_number}/entries')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// ========================================================================
|
|
93
|
+
// readResource - static resources
|
|
94
|
+
// ========================================================================
|
|
95
|
+
|
|
96
|
+
describe('readResource - static resources', () => {
|
|
97
|
+
it('should read memory://briefing', async () => {
|
|
98
|
+
const result = await readResource('memory://briefing', db)
|
|
99
|
+
|
|
100
|
+
expect(result.data).toBeDefined()
|
|
101
|
+
const data = result.data as {
|
|
102
|
+
version: string
|
|
103
|
+
journal: { totalEntries: number }
|
|
104
|
+
userMessage: string
|
|
105
|
+
}
|
|
106
|
+
expect(data.version).toBeDefined()
|
|
107
|
+
expect(data.journal.totalEntries).toBeGreaterThan(0)
|
|
108
|
+
expect(data.userMessage).toContain('Session Context')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should read memory://instructions', async () => {
|
|
112
|
+
const result = await readResource('memory://instructions', db)
|
|
113
|
+
|
|
114
|
+
expect(result.data).toBeDefined()
|
|
115
|
+
expect(typeof result.data).toBe('string')
|
|
116
|
+
expect(result.data as string).toContain('Session Start')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should show all tools in memory://instructions when no filter is set', async () => {
|
|
120
|
+
// Bug fix: when filterConfig is null (all tools enabled), memory://instructions
|
|
121
|
+
// must show Active Tools (39), not Active Tools (3) from the old hardcoded fallback
|
|
122
|
+
const result = await readResource('memory://instructions', db, undefined, null)
|
|
123
|
+
|
|
124
|
+
const text = result.data as string
|
|
125
|
+
expect(text).toContain('Active Tools (39)')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should read memory://recent', async () => {
|
|
129
|
+
const result = await readResource('memory://recent', db)
|
|
130
|
+
|
|
131
|
+
const data = result.data as { entries: unknown[]; count: number }
|
|
132
|
+
expect(data.entries).toBeDefined()
|
|
133
|
+
expect(data.count).toBeGreaterThan(0)
|
|
134
|
+
expect(result.annotations?.lastModified).toBeDefined()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should read memory://significant', async () => {
|
|
138
|
+
const result = await readResource('memory://significant', db)
|
|
139
|
+
|
|
140
|
+
const data = result.data as { entries: unknown[]; count: number }
|
|
141
|
+
expect(data.entries).toBeDefined()
|
|
142
|
+
// We created an entry with significanceType='milestone'
|
|
143
|
+
expect(data.count).toBeGreaterThan(0)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should sort memory://significant by importance descending, not by timestamp', async () => {
|
|
147
|
+
// Create 3 significant entries: e2 gets a relationship (higher importance), e3 gets 2 (highest)
|
|
148
|
+
const eBase = db.createEntry({ content: 'Sort test: base entry (no relationships)' })
|
|
149
|
+
const e1 = db.createEntry({
|
|
150
|
+
content: 'Sort test: sig entry 1 - no relationships',
|
|
151
|
+
significanceType: 'milestone',
|
|
152
|
+
})
|
|
153
|
+
const e2 = db.createEntry({
|
|
154
|
+
content: 'Sort test: sig entry 2 - one relationship',
|
|
155
|
+
significanceType: 'blocker_resolved',
|
|
156
|
+
})
|
|
157
|
+
const e3 = db.createEntry({
|
|
158
|
+
content: 'Sort test: sig entry 3 - two relationships',
|
|
159
|
+
significanceType: 'milestone',
|
|
160
|
+
})
|
|
161
|
+
// Give e2 one relationship, e3 two relationships → e3 should score highest
|
|
162
|
+
db.linkEntries(e2.id, eBase.id, 'references', 'One link')
|
|
163
|
+
db.linkEntries(e3.id, eBase.id, 'references', 'Link A')
|
|
164
|
+
db.linkEntries(e3.id, e1.id, 'references', 'Link B')
|
|
165
|
+
|
|
166
|
+
const result = await readResource('memory://significant', db)
|
|
167
|
+
const data = result.data as {
|
|
168
|
+
entries: { id: number; importance: number }[]
|
|
169
|
+
count: number
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 1: importance field present on all entries
|
|
173
|
+
for (const entry of data.entries) {
|
|
174
|
+
expect(typeof entry.importance).toBe('number')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2: entries sorted descending by importance (no entry should be less important than its successor)
|
|
178
|
+
for (let i = 1; i < data.entries.length; i++) {
|
|
179
|
+
const prev = data.entries[i - 1]!
|
|
180
|
+
const curr = data.entries[i]!
|
|
181
|
+
expect(prev.importance).toBeGreaterThanOrEqual(curr.importance)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should read memory://tags', async () => {
|
|
186
|
+
const result = await readResource('memory://tags', db)
|
|
187
|
+
|
|
188
|
+
const data = result.data as { tags: unknown[]; count: number }
|
|
189
|
+
expect(data.tags).toBeDefined()
|
|
190
|
+
expect(data.count).toBeGreaterThan(0)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should read memory://health', async () => {
|
|
194
|
+
const result = await readResource('memory://health', db)
|
|
195
|
+
|
|
196
|
+
const data = result.data as {
|
|
197
|
+
database: { entryCount: number }
|
|
198
|
+
toolFilter: { totalCount: number }
|
|
199
|
+
timestamp: string
|
|
200
|
+
}
|
|
201
|
+
expect(data.database.entryCount).toBeGreaterThan(0)
|
|
202
|
+
expect(data.toolFilter.totalCount).toBeGreaterThan(0)
|
|
203
|
+
expect(data.timestamp).toBeDefined()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should read memory://team/recent', async () => {
|
|
207
|
+
const result = await readResource('memory://team/recent', db)
|
|
208
|
+
|
|
209
|
+
const data = result.data as { entries: unknown[]; count: number }
|
|
210
|
+
expect(data.entries).toBeDefined()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should read memory://graph/recent', async () => {
|
|
214
|
+
const result = await readResource('memory://graph/recent', db)
|
|
215
|
+
|
|
216
|
+
const data = result.data as {
|
|
217
|
+
format: string
|
|
218
|
+
diagram: string
|
|
219
|
+
relationshipCount?: number
|
|
220
|
+
}
|
|
221
|
+
expect(data.format).toBe('mermaid')
|
|
222
|
+
expect(data.diagram).toContain('graph TD')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should read memory://statistics', async () => {
|
|
226
|
+
const result = await readResource('memory://statistics', db)
|
|
227
|
+
|
|
228
|
+
const data = result.data as { totalEntries: number; entriesByType: object }
|
|
229
|
+
expect(data.totalEntries).toBeGreaterThan(0)
|
|
230
|
+
expect(data.entriesByType).toBeDefined()
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ========================================================================
|
|
235
|
+
// readResource - template resources
|
|
236
|
+
// ========================================================================
|
|
237
|
+
|
|
238
|
+
describe('readResource - template resources', () => {
|
|
239
|
+
it('should read memory://projects/{number}/timeline', async () => {
|
|
240
|
+
const result = await readResource('memory://projects/42/timeline', db)
|
|
241
|
+
|
|
242
|
+
const data = result.data as { projectNumber: number; entries: unknown[]; count: number }
|
|
243
|
+
expect(data.projectNumber).toBe(42)
|
|
244
|
+
expect(data.count).toBeGreaterThan(0)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should read memory://issues/{issue_number}/entries', async () => {
|
|
248
|
+
const result = await readResource('memory://issues/7/entries', db)
|
|
249
|
+
|
|
250
|
+
const data = result.data as { issueNumber: number; entries: unknown[]; count: number }
|
|
251
|
+
expect(data.issueNumber).toBe(7)
|
|
252
|
+
expect(data.count).toBeGreaterThan(0)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should read memory://prs/{pr_number}/entries', async () => {
|
|
256
|
+
const result = await readResource('memory://prs/15/entries', db)
|
|
257
|
+
|
|
258
|
+
const data = result.data as { prNumber: number; entries: unknown[]; count: number }
|
|
259
|
+
expect(data.prNumber).toBe(15)
|
|
260
|
+
expect(data.count).toBeGreaterThan(0)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should return empty for non-matching project', async () => {
|
|
264
|
+
const result = await readResource('memory://projects/99999/timeline', db)
|
|
265
|
+
|
|
266
|
+
const data = result.data as { count: number }
|
|
267
|
+
expect(data.count).toBe(0)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should handle empty PR entries with hint', async () => {
|
|
271
|
+
const result = await readResource('memory://prs/99999/entries', db)
|
|
272
|
+
|
|
273
|
+
const data = result.data as { count: number; hint?: string }
|
|
274
|
+
expect(data.count).toBe(0)
|
|
275
|
+
expect(data.hint).toContain('No journal entries')
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// ========================================================================
|
|
280
|
+
// readResource - error cases
|
|
281
|
+
// ========================================================================
|
|
282
|
+
|
|
283
|
+
describe('readResource - error cases', () => {
|
|
284
|
+
it('should throw for unknown resource', async () => {
|
|
285
|
+
await expect(readResource('memory://nonexistent', db)).rejects.toThrow(
|
|
286
|
+
'Unknown resource'
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ========================================================================
|
|
292
|
+
// readResource - branch coverage for query params & variants
|
|
293
|
+
// ========================================================================
|
|
294
|
+
|
|
295
|
+
describe('readResource - additional branch coverage', () => {
|
|
296
|
+
it('should read memory://graph/actions', async () => {
|
|
297
|
+
const result = await readResource('memory://graph/actions', db)
|
|
298
|
+
|
|
299
|
+
const data = result.data as { format: string; diagram: string }
|
|
300
|
+
expect(data.format).toBe('mermaid')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('should read memory://actions/recent', async () => {
|
|
304
|
+
const result = await readResource('memory://actions/recent', db)
|
|
305
|
+
|
|
306
|
+
const data = result.data as { entries: unknown[]; count: number }
|
|
307
|
+
expect(data.entries).toBeDefined()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('should return briefing with expected structure', async () => {
|
|
311
|
+
const result = await readResource('memory://briefing', db)
|
|
312
|
+
const data = result.data as {
|
|
313
|
+
version: string
|
|
314
|
+
journal: { totalEntries: number }
|
|
315
|
+
behaviors: { create: string }
|
|
316
|
+
userMessage: string
|
|
317
|
+
templateResources: string[]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
expect(data.behaviors.create).toContain('implementations')
|
|
321
|
+
expect(data.templateResources).toContain('memory://projects/{number}/timeline')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should return annotations on recent entries', async () => {
|
|
325
|
+
const result = await readResource('memory://recent', db)
|
|
326
|
+
|
|
327
|
+
expect(result.annotations).toBeDefined()
|
|
328
|
+
expect(result.annotations?.lastModified).toBeDefined()
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should handle PR timeline template', async () => {
|
|
332
|
+
// PR 15 was seeded with an entry
|
|
333
|
+
const result = await readResource('memory://prs/15/timeline', db)
|
|
334
|
+
|
|
335
|
+
const data = result.data as { prNumber: number }
|
|
336
|
+
expect(data.prNumber).toBe(15)
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
})
|