memento-mcp-server 1.19.3-c → 1.19.3-e
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/package.json
CHANGED
|
@@ -37,6 +37,10 @@ let server;
|
|
|
37
37
|
let db = null;
|
|
38
38
|
// core에서 반환된 서비스 (ToolContext 생성 시 사용)
|
|
39
39
|
let serverServices = null;
|
|
40
|
+
/** Cursor 등 클라이언트가 7초 내에 Initialize 응답을 받도록, 무거운 초기화는 transport 연결 후 백그라운드에서 수행 */
|
|
41
|
+
let initPromise;
|
|
42
|
+
let resolveInit;
|
|
43
|
+
let rejectInit;
|
|
40
44
|
// MCP 서버에서는 모든 로그 출력을 완전히 차단
|
|
41
45
|
// 모든 console 메서드를 빈 함수로 교체
|
|
42
46
|
// MCP 프로토콜 스펙: stdio 전송 시 stdout에는 오직 JSON-RPC 메시지만 출력되어야 함
|
|
@@ -145,28 +149,26 @@ async function checkInitialDatabaseStatus() {
|
|
|
145
149
|
mcpLogger.logServer('warn', '초기 데이터베이스 상태 확인 실패', { error: error instanceof Error ? error.message : String(error) });
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
/**
|
|
153
|
+
* 무거운 초기화(DB·서비스·툴 등). transport 연결 후 백그라운드에서 실행하며,
|
|
154
|
+
* 완료 시 resolveInit()을 호출해 도구/리소스 요청이 동작하도록 함.
|
|
155
|
+
*/
|
|
156
|
+
async function runHeavyInit() {
|
|
150
157
|
try {
|
|
151
158
|
mcpLogger.logServer('info', `Memento MCP Server v${packageJson.version}`);
|
|
152
|
-
mcpLogger.logServer('info', 'MCP 서버 초기화 시작...');
|
|
153
|
-
// 설정 검증
|
|
159
|
+
mcpLogger.logServer('info', 'MCP 서버 초기화(백그라운드) 시작...');
|
|
154
160
|
validateConfig();
|
|
155
161
|
mcpLogger.logServer('info', '설정 검증 완료');
|
|
156
|
-
// @memento/core로 DB·서비스 초기화
|
|
157
162
|
const core = await createMementoCore({
|
|
158
163
|
dbPath: process.env.DB_PATH ?? mementoConfig.dbPath
|
|
159
164
|
});
|
|
160
165
|
db = core.db;
|
|
161
|
-
|
|
166
|
+
serverServices = core.services;
|
|
162
167
|
mcpLogger.logServer('info', '데이터베이스·서비스 초기화 완료');
|
|
163
|
-
// 초기 데이터베이스 상태 확인 (주기적 모니터링은 DatabaseLockMonitor가 담당)
|
|
164
168
|
await checkInitialDatabaseStatus();
|
|
165
169
|
mcpLogger.logServer('info', '초기 데이터베이스 상태 확인 완료');
|
|
166
|
-
|
|
167
|
-
// 배치 스케줄러는 core bootstrap에서 이미 시작됨 (services.batchScheduler)
|
|
170
|
+
const services = serverServices;
|
|
168
171
|
mcpLogger.logServer('info', '서비스 초기화 완료');
|
|
169
|
-
// 배치 스케줄러 상태 확인
|
|
170
172
|
const batchScheduler = services.batchScheduler;
|
|
171
173
|
const status = batchScheduler ? batchScheduler.getStatus() : { isRunning: false, activeJobs: 0, uptime: 0 };
|
|
172
174
|
mcpLogger.logServer('info', `배치 스케줄러 상태: ${JSON.stringify({
|
|
@@ -174,7 +176,6 @@ async function initializeServer() {
|
|
|
174
176
|
activeJobs: status.activeJobs,
|
|
175
177
|
uptime: status.uptime
|
|
176
178
|
}, null, 2)}`);
|
|
177
|
-
// 임베딩 프로바이더 정보 표시
|
|
178
179
|
const providerInfo = {
|
|
179
180
|
provider: mementoConfig.embeddingProvider.toUpperCase()
|
|
180
181
|
};
|
|
@@ -192,274 +193,255 @@ async function initializeServer() {
|
|
|
192
193
|
}
|
|
193
194
|
mcpLogger.logServer('info', '임베딩 프로바이더 정보', providerInfo);
|
|
194
195
|
mcpLogger.logServer('info', '검색 엔진 초기화 완료');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
196
|
+
resolveInit();
|
|
197
|
+
mcpLogger.logServer('info', 'MCP 서버 초기화(백그라운드) 완료');
|
|
198
|
+
mcpLogger.logServer('info', 'Memento MCP Server가 시작되었습니다!');
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
202
|
+
rejectInit(err);
|
|
203
|
+
mcpLogger.logServer('error', `서버 초기화 실패: ${err.message}`, { error: err.message, stack: err.stack });
|
|
204
|
+
process.stderr.write(`\n[ERROR] MCP Server Initialization Failed (server stays up; tools will return this error)\n`);
|
|
205
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
206
|
+
if (err.stack)
|
|
207
|
+
process.stderr.write(`Stack:\n${err.stack}\n`);
|
|
208
|
+
process.stderr.write(`\n`);
|
|
209
|
+
// Do NOT process.exit(1): client would see "Connection closed" during initializing.
|
|
210
|
+
// Keep server alive so the client gets Initialize response; tool calls will fail with this error.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** MCP 핸들러 등록. 각 핸들러는 await initPromise 후 실제 동작 수행(7초 내 Initialize 응답을 위해) */
|
|
214
|
+
function registerHandlers() {
|
|
215
|
+
// Tools 등록
|
|
216
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
217
|
+
await initPromise;
|
|
218
|
+
await mcpLogger.logMCPProtocol('debug', '도구 목록 요청 처리');
|
|
217
219
|
const toolRegistry = getToolRegistry();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
name: 'memory_injection',
|
|
256
|
-
description: '관련 기억을 요약하여 프롬프트에 주입',
|
|
257
|
-
arguments: [
|
|
258
|
-
{ name: 'query', description: '검색할 쿼리', required: true },
|
|
259
|
-
{ name: 'token_budget', description: '토큰 예산 (기본값: 1000)', required: false },
|
|
260
|
-
{ name: 'max_memories', description: '최대 기억 개수 (기본값: 5)', required: false }
|
|
261
|
-
]
|
|
262
|
-
}
|
|
263
|
-
]
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
// Prompt 조회 핸들러
|
|
267
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
268
|
-
const { name } = request.params;
|
|
269
|
-
await mcpLogger.logMCPProtocol('debug', `프롬프트 조회 요청: ${name}`, { name });
|
|
270
|
-
if (name === 'memory_injection') {
|
|
271
|
-
return {
|
|
220
|
+
const tools = toolRegistry.getAll();
|
|
221
|
+
await mcpLogger.logMCPProtocol('debug', `등록된 도구 개수: ${tools.length}`, { count: tools.length });
|
|
222
|
+
return {
|
|
223
|
+
tools: tools.map(tool => ({
|
|
224
|
+
name: tool.name,
|
|
225
|
+
description: tool.description,
|
|
226
|
+
inputSchema: tool.inputSchema
|
|
227
|
+
}))
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
// Resources 목록 핸들러
|
|
231
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
232
|
+
await initPromise;
|
|
233
|
+
await mcpLogger.logMCPProtocol('debug', '리소스 목록 요청 처리');
|
|
234
|
+
if (!db) {
|
|
235
|
+
throw new Error('Database not initialized');
|
|
236
|
+
}
|
|
237
|
+
// 모든 메모리 ID 조회
|
|
238
|
+
const memories = await DatabaseUtils.all(db, 'SELECT id FROM memory_item ORDER BY created_at DESC LIMIT 1000');
|
|
239
|
+
await mcpLogger.logMCPProtocol('debug', `리소스 개수: ${memories.length}`, { count: memories.length });
|
|
240
|
+
return {
|
|
241
|
+
resources: memories.map((memory) => ({
|
|
242
|
+
uri: `memory://${memory.id}`,
|
|
243
|
+
name: `Memory ${memory.id}`,
|
|
244
|
+
description: `Memory item with ID: ${memory.id}`,
|
|
245
|
+
mimeType: 'application/json'
|
|
246
|
+
}))
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
// Prompts 목록 핸들러 (listOfferingsForUI 등 클라이언트 호출 시 Method not found 방지)
|
|
250
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
251
|
+
await initPromise;
|
|
252
|
+
await mcpLogger.logMCPProtocol('debug', '프롬프트 목록 요청 처리');
|
|
253
|
+
return {
|
|
254
|
+
prompts: [
|
|
255
|
+
{
|
|
256
|
+
name: 'memory_injection',
|
|
272
257
|
description: '관련 기억을 요약하여 프롬프트에 주입',
|
|
273
258
|
arguments: [
|
|
274
259
|
{ name: 'query', description: '검색할 쿼리', required: true },
|
|
275
260
|
{ name: 'token_budget', description: '토큰 예산 (기본값: 1000)', required: false },
|
|
276
261
|
{ name: 'max_memories', description: '최대 기억 개수 (기본값: 5)', required: false }
|
|
277
262
|
]
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
throw new Error(`Prompt not found: ${name}`);
|
|
281
|
-
});
|
|
282
|
-
// Resource 읽기 핸들러
|
|
283
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
284
|
-
const { uri } = request.params;
|
|
285
|
-
await mcpLogger.logMCPProtocol('debug', `리소스 읽기 요청: ${uri}`, { uri });
|
|
286
|
-
// URI 파싱: memory://{id}?include_neighbors=true
|
|
287
|
-
const uriMatch = uri.match(/^memory:\/\/([^?]+)(\?.*)?$/);
|
|
288
|
-
if (!uriMatch) {
|
|
289
|
-
throw new Error(`Invalid resource URI: ${uri}`);
|
|
290
|
-
}
|
|
291
|
-
const memoryId = uriMatch[1];
|
|
292
|
-
if (!memoryId) {
|
|
293
|
-
throw new Error(`Invalid memory ID in URI: ${uri}`);
|
|
294
|
-
}
|
|
295
|
-
const queryString = uriMatch[2] || '';
|
|
296
|
-
const includeNeighbors = queryString.includes('include_neighbors=true');
|
|
297
|
-
if (!db) {
|
|
298
|
-
throw new Error('Database not initialized');
|
|
299
|
-
}
|
|
300
|
-
// 메모리 조회
|
|
301
|
-
const memory = await DatabaseUtils.get(db, 'SELECT id, type, content, importance, privacy_scope, tags, source, created_at, last_accessed, pinned FROM memory_item WHERE id = ?', [memoryId]);
|
|
302
|
-
if (!memory) {
|
|
303
|
-
throw new Error(`Memory not found: ${memoryId}`);
|
|
304
|
-
}
|
|
305
|
-
// 메모리 데이터 구성
|
|
306
|
-
const memoryData = {
|
|
307
|
-
id: memory.id,
|
|
308
|
-
type: memory.type,
|
|
309
|
-
content: memory.content,
|
|
310
|
-
importance: memory.importance,
|
|
311
|
-
privacy_scope: memory.privacy_scope,
|
|
312
|
-
tags: memory.tags ? JSON.parse(memory.tags) : [],
|
|
313
|
-
source: memory.source,
|
|
314
|
-
created_at: memory.created_at,
|
|
315
|
-
last_accessed: memory.last_accessed,
|
|
316
|
-
pinned: memory.pinned === 1
|
|
317
|
-
};
|
|
318
|
-
// 이웃 기억 포함 여부 확인
|
|
319
|
-
if (includeNeighbors) {
|
|
320
|
-
try {
|
|
321
|
-
const vectorSearchEngine = getVectorSearchEngine();
|
|
322
|
-
const neighborService = new MemoryNeighborService(vectorSearchEngine, serverServices.embeddingService, db);
|
|
323
|
-
const neighborsResult = await neighborService.getNeighbors(memoryId, {
|
|
324
|
-
limit: 5,
|
|
325
|
-
similarity_threshold: 0.8
|
|
326
|
-
});
|
|
327
|
-
memoryData.neighbors = neighborsResult.neighbors;
|
|
328
|
-
memoryData.neighbors_count = neighborsResult.total_count;
|
|
329
|
-
memoryData.neighbors_query_time = neighborsResult.query_time;
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
mcpLogger.logServer('warn', `이웃 기억 조회 실패: ${error instanceof Error ? error.message : String(error)}`, { error: error instanceof Error ? error.message : String(error) });
|
|
333
|
-
memoryData.neighbors = [];
|
|
334
|
-
memoryData.neighbors_count = 0;
|
|
335
263
|
}
|
|
336
|
-
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
// Prompt 조회 핸들러
|
|
268
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
269
|
+
await initPromise;
|
|
270
|
+
const { name } = request.params;
|
|
271
|
+
await mcpLogger.logMCPProtocol('debug', `프롬프트 조회 요청: ${name}`, { name });
|
|
272
|
+
if (name === 'memory_injection') {
|
|
337
273
|
return {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
274
|
+
description: '관련 기억을 요약하여 프롬프트에 주입',
|
|
275
|
+
arguments: [
|
|
276
|
+
{ name: 'query', description: '검색할 쿼리', required: true },
|
|
277
|
+
{ name: 'token_budget', description: '토큰 예산 (기본값: 1000)', required: false },
|
|
278
|
+
{ name: 'max_memories', description: '최대 기억 개수 (기본값: 5)', required: false }
|
|
344
279
|
]
|
|
345
280
|
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`Prompt not found: ${name}`);
|
|
283
|
+
});
|
|
284
|
+
// Resource 읽기 핸들러
|
|
285
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
286
|
+
await initPromise;
|
|
287
|
+
const { uri } = request.params;
|
|
288
|
+
await mcpLogger.logMCPProtocol('debug', `리소스 읽기 요청: ${uri}`, { uri });
|
|
289
|
+
// URI 파싱: memory://{id}?include_neighbors=true
|
|
290
|
+
const uriMatch = uri.match(/^memory:\/\/([^?]+)(\?.*)?$/);
|
|
291
|
+
if (!uriMatch) {
|
|
292
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
293
|
+
}
|
|
294
|
+
const memoryId = uriMatch[1];
|
|
295
|
+
if (!memoryId) {
|
|
296
|
+
throw new Error(`Invalid memory ID in URI: ${uri}`);
|
|
297
|
+
}
|
|
298
|
+
const queryString = uriMatch[2] || '';
|
|
299
|
+
const includeNeighbors = queryString.includes('include_neighbors=true');
|
|
300
|
+
if (!db) {
|
|
301
|
+
throw new Error('Database not initialized');
|
|
302
|
+
}
|
|
303
|
+
// 메모리 조회
|
|
304
|
+
const memory = await DatabaseUtils.get(db, 'SELECT id, type, content, importance, privacy_scope, tags, source, created_at, last_accessed, pinned FROM memory_item WHERE id = ?', [memoryId]);
|
|
305
|
+
if (!memory) {
|
|
306
|
+
throw new Error(`Memory not found: ${memoryId}`);
|
|
307
|
+
}
|
|
308
|
+
// 메모리 데이터 구성
|
|
309
|
+
const memoryData = {
|
|
310
|
+
id: memory.id,
|
|
311
|
+
type: memory.type,
|
|
312
|
+
content: memory.content,
|
|
313
|
+
importance: memory.importance,
|
|
314
|
+
privacy_scope: memory.privacy_scope,
|
|
315
|
+
tags: memory.tags ? JSON.parse(memory.tags) : [],
|
|
316
|
+
source: memory.source,
|
|
317
|
+
created_at: memory.created_at,
|
|
318
|
+
last_accessed: memory.last_accessed,
|
|
319
|
+
pinned: memory.pinned === 1
|
|
320
|
+
};
|
|
321
|
+
// 이웃 기억 포함 여부 확인
|
|
322
|
+
if (includeNeighbors) {
|
|
353
323
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
if (!db) {
|
|
361
|
-
throw new Error('데이터베이스가 초기화되지 않았습니다');
|
|
362
|
-
}
|
|
363
|
-
// Phase 7.4: 표준 팩토리 함수 사용
|
|
364
|
-
const context = createToolContext(db, serverServices);
|
|
365
|
-
// 도구 실행
|
|
366
|
-
const toolResult = await toolRegistry.execute(name, args, context);
|
|
367
|
-
await mcpLogger.logMCPProtocol('debug', `도구 실행 완료: ${name}`, { toolName: name });
|
|
368
|
-
// MCP 형식으로 변환
|
|
369
|
-
return {
|
|
370
|
-
content: [
|
|
371
|
-
{
|
|
372
|
-
type: 'text',
|
|
373
|
-
text: JSON.stringify(toolResult)
|
|
374
|
-
}
|
|
375
|
-
]
|
|
376
|
-
};
|
|
377
|
-
}, {
|
|
378
|
-
operation: 'tool_execution',
|
|
379
|
-
toolName: name,
|
|
380
|
-
requestId: `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
381
|
-
}, {
|
|
382
|
-
errorLoggingService: serverServices.errorLoggingService,
|
|
383
|
-
severity: ErrorSeverity.HIGH,
|
|
384
|
-
category: ErrorCategory.TOOL_EXECUTION,
|
|
385
|
-
transformError: (error) => new Error(`Tool execution failed: ${error.message}`)
|
|
324
|
+
const vectorSearchEngine = getVectorSearchEngine();
|
|
325
|
+
const neighborService = new MemoryNeighborService(vectorSearchEngine, serverServices.embeddingService, db);
|
|
326
|
+
const neighborsResult = await neighborService.getNeighbors(memoryId, {
|
|
327
|
+
limit: 5,
|
|
328
|
+
similarity_threshold: 0.8
|
|
386
329
|
});
|
|
330
|
+
memoryData.neighbors = neighborsResult.neighbors;
|
|
331
|
+
memoryData.neighbors_count = neighborsResult.total_count;
|
|
332
|
+
memoryData.neighbors_query_time = neighborsResult.query_time;
|
|
387
333
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
});
|
|
393
|
-
// logging/setLevel 핸들러 - MCP 스펙 준수
|
|
394
|
-
// MCP 스펙: logging/setLevel 요청 처리 필수
|
|
395
|
-
// 참조: https://spec.modelcontextprotocol.io/specification/server/#logging
|
|
396
|
-
//
|
|
397
|
-
// 구현 사항:
|
|
398
|
-
// - 유효한 로그 레벨 검증 (debug, info, warn, error)
|
|
399
|
-
// - 환경 변수에 로그 레벨 설정 (MCPLogger가 환경 변수를 읽음)
|
|
400
|
-
// - 에러 처리: 잘못된 레벨 시 명확한 에러 메시지 반환
|
|
401
|
-
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
402
|
-
const { level } = request.params;
|
|
403
|
-
await mcpLogger.logMCPProtocol('debug', `로그 레벨 변경 요청: ${level}`, { level });
|
|
404
|
-
// 로그 레벨 검증 (MCP 스펙: 유효한 레벨만 허용)
|
|
405
|
-
const validLevels = ['debug', 'info', 'warn', 'error'];
|
|
406
|
-
if (!validLevels.includes(level)) {
|
|
407
|
-
throw new Error(`Invalid log level: ${level}. Valid levels are: ${validLevels.join(', ')}`);
|
|
334
|
+
catch (error) {
|
|
335
|
+
mcpLogger.logServer('warn', `이웃 기억 조회 실패: ${error instanceof Error ? error.message : String(error)}`, { error: error instanceof Error ? error.message : String(error) });
|
|
336
|
+
memoryData.neighbors = [];
|
|
337
|
+
memoryData.neighbors_count = 0;
|
|
408
338
|
}
|
|
409
|
-
// 환경 변수에 로그 레벨 설정 (MCPLogger의 getCurrentLogLevel()이 환경 변수를 읽음)
|
|
410
|
-
// MCP 스펙: 로그 레벨 변경은 즉시 적용되어야 함
|
|
411
|
-
process.env.LOG_LEVEL = level;
|
|
412
|
-
await mcpLogger.logMCPProtocol('info', `로그 레벨이 ${level}로 변경되었습니다`, { level });
|
|
413
|
-
return {};
|
|
414
|
-
});
|
|
415
|
-
mcpLogger.logServer('info', 'MCP 서버 초기화 완료');
|
|
416
|
-
// 실시간 성능 모니터링 시작
|
|
417
|
-
// performanceMonitoringIntegration.startRealTimeMonitoring();
|
|
418
|
-
mcpLogger.logServer('info', 'Memento MCP Server가 시작되었습니다!');
|
|
419
|
-
// process.stderr.write('📊 실시간 성능 모니터링이 활성화되었습니다\n');
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
// 에러 발생 시 상세 정보를 stderr에 출력
|
|
423
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
424
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
425
|
-
mcpLogger.logServer('error', `서버 초기화 실패: ${errorMessage}`, {
|
|
426
|
-
error: errorMessage,
|
|
427
|
-
stack: errorStack,
|
|
428
|
-
type: error instanceof Error ? error.constructor.name : typeof error
|
|
429
|
-
});
|
|
430
|
-
// stderr에 직접 출력하여 Cursor에서도 확인 가능하도록
|
|
431
|
-
process.stderr.write(`\n[ERROR] MCP Server Initialization Failed\n`);
|
|
432
|
-
process.stderr.write(`Error: ${errorMessage}\n`);
|
|
433
|
-
if (errorStack) {
|
|
434
|
-
process.stderr.write(`Stack:\n${errorStack}\n`);
|
|
435
339
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
340
|
+
return {
|
|
341
|
+
contents: [
|
|
342
|
+
{
|
|
343
|
+
uri,
|
|
344
|
+
mimeType: 'application/json',
|
|
345
|
+
text: JSON.stringify(memoryData, null, 2)
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
// Tool 실행 핸들러 - 동시성 제한 적용
|
|
351
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
352
|
+
await initPromise;
|
|
353
|
+
const { name, arguments: args } = request.params;
|
|
354
|
+
await mcpLogger.logMCPProtocol('debug', `도구 실행 요청: ${name}`, { toolName: name, args });
|
|
355
|
+
await concurrencyLimiter.acquire();
|
|
356
|
+
try {
|
|
357
|
+
return await withErrorHandling(async () => {
|
|
358
|
+
if (!serverServices)
|
|
359
|
+
throw new Error('서비스가 초기화되지 않았습니다');
|
|
360
|
+
if (!db)
|
|
361
|
+
throw new Error('데이터베이스가 초기화되지 않았습니다');
|
|
362
|
+
const context = createToolContext(db, serverServices);
|
|
363
|
+
const toolRegistry = getToolRegistry();
|
|
364
|
+
const toolResult = await toolRegistry.execute(name, args, context);
|
|
365
|
+
await mcpLogger.logMCPProtocol('debug', `도구 실행 완료: ${name}`, { toolName: name });
|
|
366
|
+
// MCP 형식으로 변환
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: 'text',
|
|
371
|
+
text: JSON.stringify(toolResult)
|
|
372
|
+
}
|
|
373
|
+
]
|
|
374
|
+
};
|
|
375
|
+
}, {
|
|
376
|
+
operation: 'tool_execution',
|
|
377
|
+
toolName: name,
|
|
378
|
+
requestId: `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
379
|
+
}, {
|
|
380
|
+
errorLoggingService: serverServices.errorLoggingService,
|
|
381
|
+
severity: ErrorSeverity.HIGH,
|
|
382
|
+
category: ErrorCategory.TOOL_EXECUTION,
|
|
383
|
+
transformError: (error) => new Error(`Tool execution failed: ${error.message}`)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
// 동시성 제한 해제
|
|
388
|
+
concurrencyLimiter.release();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
// logging/setLevel 핸들러 - MCP 스펙 준수
|
|
392
|
+
// MCP 스펙: logging/setLevel 요청 처리 필수
|
|
393
|
+
// 참조: https://spec.modelcontextprotocol.io/specification/server/#logging
|
|
394
|
+
//
|
|
395
|
+
// 구현 사항:
|
|
396
|
+
// - 유효한 로그 레벨 검증 (debug, info, warn, error)
|
|
397
|
+
// - 환경 변수에 로그 레벨 설정 (MCPLogger가 환경 변수를 읽음)
|
|
398
|
+
// - 에러 처리: 잘못된 레벨 시 명확한 에러 메시지 반환
|
|
399
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
400
|
+
await initPromise;
|
|
401
|
+
const { level } = request.params;
|
|
402
|
+
await mcpLogger.logMCPProtocol('debug', `로그 레벨 변경 요청: ${level}`, { level });
|
|
403
|
+
// 로그 레벨 검증 (MCP 스펙: 유효한 레벨만 허용)
|
|
404
|
+
const validLevels = ['debug', 'info', 'warn', 'error'];
|
|
405
|
+
if (!validLevels.includes(level)) {
|
|
406
|
+
throw new Error(`Invalid log level: ${level}. Valid levels are: ${validLevels.join(', ')}`);
|
|
407
|
+
}
|
|
408
|
+
// 환경 변수에 로그 레벨 설정 (MCPLogger의 getCurrentLogLevel()이 환경 변수를 읽음)
|
|
409
|
+
// MCP 스펙: 로그 레벨 변경은 즉시 적용되어야 함
|
|
410
|
+
process.env.LOG_LEVEL = level;
|
|
411
|
+
await mcpLogger.logMCPProtocol('info', `로그 레벨이 ${level}로 변경되었습니다`, { level });
|
|
412
|
+
return {};
|
|
413
|
+
});
|
|
439
414
|
}
|
|
440
415
|
// 서버 시작
|
|
441
416
|
async function startServer() {
|
|
442
417
|
try {
|
|
443
|
-
// MCP 프로토콜 준수: transport 연결 전까지는 로그를 억제하여 stdout 오염 방지
|
|
444
|
-
// initializeServer() 호출 전에 플래그를 설정하여 초기화 중 로그 출력 방지
|
|
445
|
-
// ServerState를 통해 mcpLogger에서 접근 가능하도록 설정
|
|
446
418
|
serverState.setMcpTransportConnected(false);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
419
|
+
initPromise = new Promise((resolve, reject) => {
|
|
420
|
+
resolveInit = resolve;
|
|
421
|
+
rejectInit = reject;
|
|
422
|
+
});
|
|
423
|
+
server = new Server({
|
|
424
|
+
name: mementoConfig.serverName,
|
|
425
|
+
version: mementoConfig.serverVersion,
|
|
426
|
+
}, {
|
|
427
|
+
capabilities: {
|
|
428
|
+
tools: {},
|
|
429
|
+
resources: {},
|
|
430
|
+
prompts: {},
|
|
431
|
+
logging: {}
|
|
432
|
+
},
|
|
433
|
+
instructions: MEMENTO_SERVER_INSTRUCTIONS
|
|
434
|
+
});
|
|
435
|
+
mcpLogger.setServer(server);
|
|
436
|
+
registerHandlers();
|
|
437
|
+
// transport를 먼저 연결해 Cursor가 7초 내에 Initialize 응답을 받도록 함
|
|
452
438
|
const transport = new StdioServerTransport();
|
|
453
439
|
await server.connect(transport);
|
|
454
|
-
// transport 연결 완료 후 로그 출력 허용
|
|
455
440
|
serverState.setMcpTransportConnected(true);
|
|
456
|
-
// 서버 초기화 완료 플래그 설정
|
|
457
|
-
// console.error 오버라이드가 MCP Logger를 사용하도록 전환
|
|
458
|
-
// MCP 스펙 준수 범위: 서버 초기화 완료 후부터 적용
|
|
459
|
-
// 참조: https://spec.modelcontextprotocol.io/specification/server/#logging
|
|
460
441
|
serverState.setMcpServerInitialized(true);
|
|
461
442
|
mcpLogger.logServer('info', 'MCP 전송 계층 연결 완료');
|
|
462
|
-
//
|
|
443
|
+
// 무거운 초기화(DB·서비스)는 백그라운드에서 수행
|
|
444
|
+
void runHeavyInit();
|
|
463
445
|
mcpLogger.logServer('info', 'MCP 클라이언트 연결 대기 중...');
|
|
464
446
|
// 서버가 종료될 때까지 대기
|
|
465
447
|
return new Promise((resolve) => {
|