memory-journal-mcp 4.4.2 → 4.5.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/.github/workflows/lint-and-test.yml +1 -1
- package/.github/workflows/security-update.yml +1 -1
- package/CHANGELOG.md +81 -1
- package/DOCKER_README.md +57 -7
- package/Dockerfile +17 -17
- package/README.md +65 -6
- package/SECURITY.md +27 -35
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +5 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +137 -83
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +2 -1
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +15 -8
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/handlers/resources/index.d.ts +3 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +5 -2
- 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 +63 -16
- 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 +43 -2
- package/dist/server/McpServer.js.map +1 -1
- package/dist/server/Scheduler.d.ts +91 -0
- package/dist/server/Scheduler.d.ts.map +1 -0
- package/dist/server/Scheduler.js +201 -0
- package/dist/server/Scheduler.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +6 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security-utils.d.ts +0 -21
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +0 -47
- package/dist/utils/security-utils.js.map +1 -1
- package/hooks/README.md +107 -0
- package/hooks/cursor/hooks.json +10 -0
- package/hooks/cursor/memory-journal.mdc +22 -0
- package/hooks/cursor/session-end.sh +19 -0
- package/hooks/kilo-code/session-end-mode.json +11 -0
- package/hooks/kiro/session-end.md +13 -0
- package/package.json +8 -8
- package/releases/v4.5.0.md +116 -0
- package/scripts/generate-server-instructions.ts +176 -0
- package/scripts/server-instructions-function-body.ts +77 -0
- package/server.json +3 -3
- package/src/cli.ts +26 -0
- package/src/constants/ServerInstructions.ts +137 -83
- package/src/constants/server-instructions.md +262 -0
- package/src/database/SqliteAdapter.ts +22 -8
- package/src/handlers/resources/index.ts +8 -2
- package/src/handlers/tools/index.ts +70 -20
- package/src/server/McpServer.ts +60 -2
- package/src/server/Scheduler.ts +278 -0
- package/src/utils/logger.ts +6 -3
- package/src/utils/security-utils.ts +0 -52
- package/tests/constants/server-instructions.test.ts +26 -0
- package/tests/database/sqlite-adapter.test.ts +84 -0
- package/tests/filtering/tool-filter.test.ts +46 -0
- package/tests/handlers/github-resource-handlers.test.ts +453 -0
- package/tests/handlers/github-tool-handlers.test.ts +899 -0
- package/tests/handlers/prompt-handlers.test.ts +40 -0
- package/tests/handlers/resource-handlers.test.ts +32 -0
- package/tests/handlers/tool-handlers.test.ts +13 -2
- package/tests/security/sql-injection.test.ts +3 -54
- package/tests/server/mcp-server.test.ts +491 -5
- package/tests/server/scheduler.test.ts +400 -0
- package/tests/vector/vector-search-manager.test.ts +60 -0
- package/.vscode/settings.json +0 -84
|
@@ -9,7 +9,11 @@ import initSqlJs, { type Database } from 'sql.js'
|
|
|
9
9
|
import * as fs from 'node:fs'
|
|
10
10
|
import * as path from 'node:path'
|
|
11
11
|
import { logger } from '../utils/logger.js'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
validateDateFormatPattern,
|
|
14
|
+
sanitizeSearchQuery,
|
|
15
|
+
assertNoPathTraversal,
|
|
16
|
+
} from '../utils/security-utils.js'
|
|
13
17
|
import type {
|
|
14
18
|
JournalEntry,
|
|
15
19
|
Tag,
|
|
@@ -168,6 +172,11 @@ export class SqliteAdapter {
|
|
|
168
172
|
|
|
169
173
|
// Initialize schema
|
|
170
174
|
this.db.run(SCHEMA_SQL)
|
|
175
|
+
|
|
176
|
+
// Enable foreign key enforcement (SQLite disables by default)
|
|
177
|
+
// Required for ON DELETE CASCADE in entry_tags, relationships, embeddings
|
|
178
|
+
this.db.run('PRAGMA foreign_keys = ON')
|
|
179
|
+
|
|
171
180
|
this.initialized = true
|
|
172
181
|
|
|
173
182
|
logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath })
|
|
@@ -582,9 +591,9 @@ export class SqliteAdapter {
|
|
|
582
591
|
|
|
583
592
|
let sql = `
|
|
584
593
|
SELECT * FROM memory_journal
|
|
585
|
-
WHERE deleted_at IS NULL AND content LIKE ?
|
|
594
|
+
WHERE deleted_at IS NULL AND content LIKE ? ESCAPE '\\'
|
|
586
595
|
`
|
|
587
|
-
const params: unknown[] = [`%${query}%`]
|
|
596
|
+
const params: unknown[] = [`%${sanitizeSearchQuery(query)}%`]
|
|
588
597
|
|
|
589
598
|
if (isPersonal !== undefined) {
|
|
590
599
|
sql += ` AND is_personal = ?`
|
|
@@ -1037,6 +1046,11 @@ export class SqliteAdapter {
|
|
|
1037
1046
|
const db = this.ensureDb()
|
|
1038
1047
|
const backupsDir = this.getBackupsDir()
|
|
1039
1048
|
|
|
1049
|
+
// Validate backup name against path traversal before sanitization
|
|
1050
|
+
if (backupName) {
|
|
1051
|
+
assertNoPathTraversal(backupName)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1040
1054
|
// Ensure backups directory exists
|
|
1041
1055
|
if (!fs.existsSync(backupsDir)) {
|
|
1042
1056
|
fs.mkdirSync(backupsDir, { recursive: true })
|
|
@@ -1118,7 +1132,7 @@ export class SqliteAdapter {
|
|
|
1118
1132
|
deleteOldBackups(keepCount: number): { deleted: string[]; kept: number } {
|
|
1119
1133
|
const backups = this.listBackups() // Already sorted newest-first
|
|
1120
1134
|
|
|
1121
|
-
if (keepCount < 1) {
|
|
1135
|
+
if (keepCount < 1 || Number.isNaN(keepCount)) {
|
|
1122
1136
|
throw new Error('keepCount must be at least 1')
|
|
1123
1137
|
}
|
|
1124
1138
|
|
|
@@ -1155,9 +1169,7 @@ export class SqliteAdapter {
|
|
|
1155
1169
|
newEntryCount: number
|
|
1156
1170
|
}> {
|
|
1157
1171
|
// Validate filename (prevent path traversal)
|
|
1158
|
-
|
|
1159
|
-
throw new Error('Invalid backup filename: path separators not allowed')
|
|
1160
|
-
}
|
|
1172
|
+
assertNoPathTraversal(filename)
|
|
1161
1173
|
|
|
1162
1174
|
const backupsDir = this.getBackupsDir()
|
|
1163
1175
|
const backupPath = path.join(backupsDir, filename)
|
|
@@ -1187,6 +1199,7 @@ export class SqliteAdapter {
|
|
|
1187
1199
|
// Initialize new database from backup
|
|
1188
1200
|
const SQL = await import('sql.js').then((m) => m.default())
|
|
1189
1201
|
this.db = new SQL.Database(backupBuffer)
|
|
1202
|
+
this.db.run('PRAGMA foreign_keys = ON')
|
|
1190
1203
|
this.initialized = true
|
|
1191
1204
|
|
|
1192
1205
|
// Get new entry count
|
|
@@ -1329,7 +1342,8 @@ export class SqliteAdapter {
|
|
|
1329
1342
|
}
|
|
1330
1343
|
|
|
1331
1344
|
/**
|
|
1332
|
-
* Get raw
|
|
1345
|
+
* Get raw sql.js Database handle for advanced queries.
|
|
1346
|
+
* @internal Callers MUST use parameterized queries — never concatenate user input into SQL.
|
|
1333
1347
|
*/
|
|
1334
1348
|
getRawDb(): Database {
|
|
1335
1349
|
return this.ensureDb()
|
|
@@ -10,6 +10,7 @@ import type { ToolFilterConfig } from '../../filtering/ToolFilter.js'
|
|
|
10
10
|
import { getAllToolNames } from '../../filtering/ToolFilter.js'
|
|
11
11
|
import type { Tag, McpIcon } from '../../types/index.js'
|
|
12
12
|
import type { GitHubIntegration } from '../../github/GitHubIntegration.js'
|
|
13
|
+
import type { Scheduler } from '../../server/Scheduler.js'
|
|
13
14
|
import { generateInstructions, type InstructionLevel } from '../../constants/ServerInstructions.js'
|
|
14
15
|
import { getPrompts } from '../prompts/index.js'
|
|
15
16
|
import {
|
|
@@ -36,6 +37,7 @@ export interface ResourceContext {
|
|
|
36
37
|
vectorManager?: VectorSearchManager
|
|
37
38
|
filterConfig?: ToolFilterConfig | null
|
|
38
39
|
github?: GitHubIntegration | null
|
|
40
|
+
scheduler?: Scheduler | null
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -131,10 +133,11 @@ export async function readResource(
|
|
|
131
133
|
db: SqliteAdapter,
|
|
132
134
|
vectorManager?: VectorSearchManager,
|
|
133
135
|
filterConfig?: ToolFilterConfig | null,
|
|
134
|
-
github?: GitHubIntegration | null
|
|
136
|
+
github?: GitHubIntegration | null,
|
|
137
|
+
scheduler?: Scheduler | null
|
|
135
138
|
): Promise<{ data: unknown; annotations?: { lastModified?: string } }> {
|
|
136
139
|
const resources = getAllResourceDefinitions()
|
|
137
|
-
const context: ResourceContext = { db, vectorManager, filterConfig, github }
|
|
140
|
+
const context: ResourceContext = { db, vectorManager, filterConfig, github, scheduler }
|
|
138
141
|
|
|
139
142
|
// Strip query parameters for matching, but pass full URI to handler
|
|
140
143
|
const baseUri = getBaseUri(uri)
|
|
@@ -1172,6 +1175,9 @@ I have project memory access and will create entries for significant work.`,
|
|
|
1172
1175
|
...dbHealth,
|
|
1173
1176
|
vectorIndex,
|
|
1174
1177
|
toolFilter,
|
|
1178
|
+
scheduler: context.scheduler
|
|
1179
|
+
? context.scheduler.getStatus()
|
|
1180
|
+
: { active: false, jobs: [] },
|
|
1175
1181
|
timestamp: lastModified,
|
|
1176
1182
|
},
|
|
1177
1183
|
annotations: { lastModified },
|
|
@@ -37,12 +37,48 @@ export interface ToolContext {
|
|
|
37
37
|
// Zod Schemas for Input Validation
|
|
38
38
|
// ============================================================================
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Valid entry types (matches EntryType union in types/index.ts)
|
|
42
|
+
*/
|
|
43
|
+
const ENTRY_TYPES = [
|
|
44
|
+
'personal_reflection',
|
|
45
|
+
'project_decision',
|
|
46
|
+
'technical_achievement',
|
|
47
|
+
'bug_fix',
|
|
48
|
+
'feature_implementation',
|
|
49
|
+
'code_review',
|
|
50
|
+
'meeting_notes',
|
|
51
|
+
'learning',
|
|
52
|
+
'research',
|
|
53
|
+
'planning',
|
|
54
|
+
'retrospective',
|
|
55
|
+
'standup',
|
|
56
|
+
'other',
|
|
57
|
+
] as const
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Valid significance types (matches SignificanceType union in types/index.ts)
|
|
61
|
+
*/
|
|
62
|
+
const SIGNIFICANCE_TYPES = [
|
|
63
|
+
'milestone',
|
|
64
|
+
'breakthrough',
|
|
65
|
+
'technical_breakthrough',
|
|
66
|
+
'decision',
|
|
67
|
+
'lesson_learned',
|
|
68
|
+
'blocker_resolved',
|
|
69
|
+
'release',
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
/** YYYY-MM-DD date format regex */
|
|
73
|
+
const DATE_FORMAT_REGEX = /^\d{4}-\d{2}-\d{2}$/
|
|
74
|
+
const DATE_FORMAT_MESSAGE = 'Date must be YYYY-MM-DD format'
|
|
75
|
+
|
|
40
76
|
const CreateEntrySchema = z.object({
|
|
41
77
|
content: z.string().min(1).max(50000),
|
|
42
|
-
entry_type: z.
|
|
78
|
+
entry_type: z.enum(ENTRY_TYPES).optional().default('personal_reflection'),
|
|
43
79
|
tags: z.array(z.string()).optional().default([]),
|
|
44
80
|
is_personal: z.boolean().optional().default(true),
|
|
45
|
-
significance_type: z.
|
|
81
|
+
significance_type: z.enum(SIGNIFICANCE_TYPES).optional(),
|
|
46
82
|
auto_context: z.boolean().optional().default(true),
|
|
47
83
|
project_number: z.number().optional(),
|
|
48
84
|
project_owner: z.string().optional(),
|
|
@@ -87,9 +123,9 @@ const SearchEntriesSchema = z.object({
|
|
|
87
123
|
})
|
|
88
124
|
|
|
89
125
|
const SearchByDateRangeSchema = z.object({
|
|
90
|
-
start_date: z.string(),
|
|
91
|
-
end_date: z.string(),
|
|
92
|
-
entry_type: z.
|
|
126
|
+
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE),
|
|
127
|
+
end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE),
|
|
128
|
+
entry_type: z.enum(ENTRY_TYPES).optional(),
|
|
93
129
|
tags: z.array(z.string()).optional(),
|
|
94
130
|
is_personal: z.boolean().optional(),
|
|
95
131
|
project_number: z.number().optional(),
|
|
@@ -112,8 +148,8 @@ const SemanticSearchSchema = z.object({
|
|
|
112
148
|
|
|
113
149
|
const GetStatisticsSchema = z.object({
|
|
114
150
|
group_by: z.enum(['day', 'week', 'month']).optional().default('week'),
|
|
115
|
-
start_date: z.string().optional(),
|
|
116
|
-
end_date: z.string().optional(),
|
|
151
|
+
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
|
|
152
|
+
end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
|
|
117
153
|
project_breakdown: z.boolean().optional().default(false),
|
|
118
154
|
})
|
|
119
155
|
|
|
@@ -138,9 +174,9 @@ const LinkEntriesSchema = z.object({
|
|
|
138
174
|
|
|
139
175
|
const ExportEntriesSchema = z.object({
|
|
140
176
|
format: z.enum(['json', 'markdown']).optional().default('json'),
|
|
141
|
-
start_date: z.string().optional(),
|
|
142
|
-
end_date: z.string().optional(),
|
|
143
|
-
entry_types: z.array(z.
|
|
177
|
+
start_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
|
|
178
|
+
end_date: z.string().regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE).optional(),
|
|
179
|
+
entry_types: z.array(z.enum(ENTRY_TYPES)).optional(),
|
|
144
180
|
tags: z.array(z.string()).optional(),
|
|
145
181
|
limit: z.number().optional().default(100).describe('Maximum entries to export (default: 100)'),
|
|
146
182
|
})
|
|
@@ -148,7 +184,7 @@ const ExportEntriesSchema = z.object({
|
|
|
148
184
|
const UpdateEntrySchema = z.object({
|
|
149
185
|
entry_id: z.number(),
|
|
150
186
|
content: z.string().optional(),
|
|
151
|
-
entry_type: z.
|
|
187
|
+
entry_type: z.enum(ENTRY_TYPES).optional(),
|
|
152
188
|
is_personal: z.boolean().optional(),
|
|
153
189
|
tags: z.array(z.string()).optional(),
|
|
154
190
|
})
|
|
@@ -1036,10 +1072,10 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
|
|
|
1036
1072
|
|
|
1037
1073
|
const entry = db.createEntry({
|
|
1038
1074
|
content: input.content,
|
|
1039
|
-
entryType: input.entry_type
|
|
1075
|
+
entryType: input.entry_type,
|
|
1040
1076
|
tags: input.tags,
|
|
1041
|
-
isPersonal: input.is_personal,
|
|
1042
|
-
significanceType:
|
|
1077
|
+
isPersonal: input.share_with_team ? false : input.is_personal,
|
|
1078
|
+
significanceType: input.significance_type ?? null,
|
|
1043
1079
|
projectNumber: input.project_number,
|
|
1044
1080
|
projectOwner: input.project_owner,
|
|
1045
1081
|
issueNumber: input.issue_number,
|
|
@@ -1184,7 +1220,7 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
|
|
|
1184
1220
|
handler: (params: unknown) => {
|
|
1185
1221
|
const input = SearchByDateRangeSchema.parse(params)
|
|
1186
1222
|
const entries = db.searchByDateRange(input.start_date, input.end_date, {
|
|
1187
|
-
entryType: input.entry_type
|
|
1223
|
+
entryType: input.entry_type,
|
|
1188
1224
|
tags: input.tags,
|
|
1189
1225
|
isPersonal: input.is_personal,
|
|
1190
1226
|
projectNumber: input.project_number,
|
|
@@ -1277,8 +1313,16 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
|
|
|
1277
1313
|
description: 'Analyze patterns across all GitHub Projects tracked in journal entries',
|
|
1278
1314
|
group: 'analytics',
|
|
1279
1315
|
inputSchema: z.object({
|
|
1280
|
-
start_date: z
|
|
1281
|
-
|
|
1316
|
+
start_date: z
|
|
1317
|
+
.string()
|
|
1318
|
+
.regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
|
|
1319
|
+
.optional()
|
|
1320
|
+
.describe('Start date (YYYY-MM-DD)'),
|
|
1321
|
+
end_date: z
|
|
1322
|
+
.string()
|
|
1323
|
+
.regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
|
|
1324
|
+
.optional()
|
|
1325
|
+
.describe('End date (YYYY-MM-DD)'),
|
|
1282
1326
|
min_entries: z
|
|
1283
1327
|
.number()
|
|
1284
1328
|
.optional()
|
|
@@ -1290,8 +1334,14 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
|
|
|
1290
1334
|
handler: (params: unknown) => {
|
|
1291
1335
|
const input = z
|
|
1292
1336
|
.object({
|
|
1293
|
-
start_date: z
|
|
1294
|
-
|
|
1337
|
+
start_date: z
|
|
1338
|
+
.string()
|
|
1339
|
+
.regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
|
|
1340
|
+
.optional(),
|
|
1341
|
+
end_date: z
|
|
1342
|
+
.string()
|
|
1343
|
+
.regex(DATE_FORMAT_REGEX, DATE_FORMAT_MESSAGE)
|
|
1344
|
+
.optional(),
|
|
1295
1345
|
min_entries: z.number().optional().default(3),
|
|
1296
1346
|
})
|
|
1297
1347
|
.parse(params)
|
|
@@ -1744,7 +1794,7 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
|
|
|
1744
1794
|
const input = UpdateEntrySchema.parse(params)
|
|
1745
1795
|
const entry = db.updateEntry(input.entry_id, {
|
|
1746
1796
|
content: input.content,
|
|
1747
|
-
entryType: input.entry_type
|
|
1797
|
+
entryType: input.entry_type,
|
|
1748
1798
|
isPersonal: input.is_personal,
|
|
1749
1799
|
tags: input.tags,
|
|
1750
1800
|
})
|
package/src/server/McpServer.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
13
13
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
14
14
|
import express from 'express'
|
|
15
15
|
import type { Express, Request, Response } from 'express'
|
|
16
|
+
import rateLimit from 'express-rate-limit'
|
|
16
17
|
import { z } from 'zod'
|
|
17
18
|
|
|
18
19
|
import { SqliteAdapter } from '../database/SqliteAdapter.js'
|
|
@@ -29,6 +30,7 @@ import { getTools, callTool } from '../handlers/tools/index.js'
|
|
|
29
30
|
import { getResources, readResource } from '../handlers/resources/index.js'
|
|
30
31
|
import { getPrompts, getPrompt } from '../handlers/prompts/index.js'
|
|
31
32
|
import { generateInstructions } from '../constants/ServerInstructions.js'
|
|
33
|
+
import { Scheduler, type SchedulerOptions } from './Scheduler.js'
|
|
32
34
|
import pkg from '../../package.json' with { type: 'json' }
|
|
33
35
|
|
|
34
36
|
/** Session timeout for stateful HTTP mode (30 minutes) */
|
|
@@ -47,6 +49,7 @@ export interface ServerOptions {
|
|
|
47
49
|
autoRebuildIndex?: boolean
|
|
48
50
|
statelessHttp?: boolean
|
|
49
51
|
corsOrigin?: string
|
|
52
|
+
scheduler?: SchedulerOptions
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/**
|
|
@@ -290,7 +293,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
290
293
|
db,
|
|
291
294
|
vectorManager,
|
|
292
295
|
filterConfig,
|
|
293
|
-
github
|
|
296
|
+
github,
|
|
297
|
+
scheduler
|
|
294
298
|
)
|
|
295
299
|
const dataStr =
|
|
296
300
|
typeof result.data === 'string'
|
|
@@ -324,7 +328,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
324
328
|
db,
|
|
325
329
|
vectorManager,
|
|
326
330
|
filterConfig,
|
|
327
|
-
github
|
|
331
|
+
github,
|
|
332
|
+
scheduler
|
|
328
333
|
)
|
|
329
334
|
const dataStr =
|
|
330
335
|
typeof result.data === 'string'
|
|
@@ -388,6 +393,25 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
388
393
|
)
|
|
389
394
|
}
|
|
390
395
|
|
|
396
|
+
// Initialize scheduler (HTTP/SSE only)
|
|
397
|
+
let scheduler: Scheduler | null = null
|
|
398
|
+
if (options.scheduler) {
|
|
399
|
+
const hasAnyJob =
|
|
400
|
+
options.scheduler.backupIntervalMinutes > 0 ||
|
|
401
|
+
options.scheduler.vacuumIntervalMinutes > 0 ||
|
|
402
|
+
options.scheduler.rebuildIndexIntervalMinutes > 0
|
|
403
|
+
|
|
404
|
+
if (hasAnyJob && transport === 'stdio') {
|
|
405
|
+
logger.warning(
|
|
406
|
+
'Scheduler options ignored for stdio transport (session is ephemeral). ' +
|
|
407
|
+
'Use HTTP/SSE transport for automated scheduling.',
|
|
408
|
+
{ module: 'Scheduler' }
|
|
409
|
+
)
|
|
410
|
+
} else if (hasAnyJob) {
|
|
411
|
+
scheduler = new Scheduler(options.scheduler, db, vectorManager)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
391
415
|
// Start server based on transport
|
|
392
416
|
if (transport === 'stdio') {
|
|
393
417
|
const stdioTransport = new StdioServerTransport()
|
|
@@ -405,12 +429,24 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
405
429
|
const port = options.port ?? 3000
|
|
406
430
|
const host = options.host ?? 'localhost'
|
|
407
431
|
const corsOrigin = options.corsOrigin ?? process.env['MCP_CORS_ORIGIN'] ?? '*'
|
|
432
|
+
|
|
433
|
+
if (corsOrigin === '*') {
|
|
434
|
+
logger.warning(
|
|
435
|
+
'CORS origin is set to "*" (all origins). ' +
|
|
436
|
+
'Set --cors-origin or MCP_CORS_ORIGIN for production deployments.',
|
|
437
|
+
{ module: 'McpServer' }
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
408
441
|
const app: Express = express()
|
|
409
442
|
|
|
410
443
|
// Security headers middleware
|
|
411
444
|
app.use((_req: Request, res: Response, next: () => void) => {
|
|
412
445
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
413
446
|
res.setHeader('X-Frame-Options', 'DENY')
|
|
447
|
+
res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'")
|
|
448
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
449
|
+
res.setHeader('Referrer-Policy', 'no-referrer')
|
|
414
450
|
next()
|
|
415
451
|
})
|
|
416
452
|
|
|
@@ -437,6 +473,19 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
437
473
|
// JSON body parser with size limit to prevent memory exhaustion (DoS)
|
|
438
474
|
app.use(express.json({ limit: '1mb' }))
|
|
439
475
|
|
|
476
|
+
// Rate limiting to prevent abuse (100 requests/minute per IP)
|
|
477
|
+
const limiter = rateLimit({
|
|
478
|
+
windowMs: 60 * 1000,
|
|
479
|
+
limit: 100,
|
|
480
|
+
standardHeaders: 'draft-8',
|
|
481
|
+
legacyHeaders: false,
|
|
482
|
+
message: { error: 'Too many requests, please try again later' },
|
|
483
|
+
})
|
|
484
|
+
app.use(limiter)
|
|
485
|
+
logger.info('Rate limiting enabled: 100 requests/minute per IP', {
|
|
486
|
+
module: 'McpServer',
|
|
487
|
+
})
|
|
488
|
+
|
|
440
489
|
// Explicit OPTIONS handler for /mcp - MUST be before other /mcp routes
|
|
441
490
|
// Using app.all to intercept before Express 5's auto-OPTIONS
|
|
442
491
|
app.all('/mcp', (req: Request, res: Response, next: () => void) => {
|
|
@@ -498,6 +547,9 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
498
547
|
})
|
|
499
548
|
})
|
|
500
549
|
|
|
550
|
+
// Start scheduler after HTTP server is listening
|
|
551
|
+
scheduler?.start()
|
|
552
|
+
|
|
501
553
|
httpServer.on('close', () => {
|
|
502
554
|
logger.info('HTTP server closed', { module: 'McpServer' })
|
|
503
555
|
})
|
|
@@ -506,6 +558,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
506
558
|
process.on('SIGINT', () => {
|
|
507
559
|
logger.info('Shutting down HTTP server...', { module: 'McpServer' })
|
|
508
560
|
void (async () => {
|
|
561
|
+
scheduler?.stop()
|
|
509
562
|
try {
|
|
510
563
|
await statelessTransport.close()
|
|
511
564
|
} catch (error) {
|
|
@@ -711,6 +764,9 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
711
764
|
})
|
|
712
765
|
})
|
|
713
766
|
|
|
767
|
+
// Start scheduler after HTTP server is listening
|
|
768
|
+
scheduler?.start()
|
|
769
|
+
|
|
714
770
|
// Keep process alive - httpServer keeps the event loop active
|
|
715
771
|
// but we also ensure it doesn't close prematurely
|
|
716
772
|
httpServer.on('close', () => {
|
|
@@ -722,6 +778,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
722
778
|
logger.info('Shutting down HTTP server...', { module: 'McpServer' })
|
|
723
779
|
|
|
724
780
|
void (async () => {
|
|
781
|
+
scheduler?.stop()
|
|
782
|
+
|
|
725
783
|
// Close all active transports
|
|
726
784
|
for (const [sessionId, httpTransport] of transports) {
|
|
727
785
|
try {
|