memento-mcp-server 1.11.0-a1 → 1.11.1-a
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/README.md +7 -0
- package/dist/server/context.d.ts +36 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +45 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/handlers/anchor-map.handler.d.ts +60 -0
- package/dist/server/handlers/anchor-map.handler.d.ts.map +1 -0
- package/dist/server/handlers/anchor-map.handler.js +190 -0
- package/dist/server/handlers/anchor-map.handler.js.map +1 -0
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +41 -1131
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/middleware/error-handler.middleware.d.ts +25 -0
- package/dist/server/middleware/error-handler.middleware.d.ts.map +1 -0
- package/dist/server/middleware/error-handler.middleware.js +97 -0
- package/dist/server/middleware/error-handler.middleware.js.map +1 -0
- package/dist/server/middleware/index.d.ts +8 -0
- package/dist/server/middleware/index.d.ts.map +1 -0
- package/dist/server/middleware/index.js +8 -0
- package/dist/server/middleware/index.js.map +1 -0
- package/dist/server/middleware/service-injector.middleware.d.ts +30 -0
- package/dist/server/middleware/service-injector.middleware.d.ts.map +1 -0
- package/dist/server/middleware/service-injector.middleware.js +20 -0
- package/dist/server/middleware/service-injector.middleware.js.map +1 -0
- package/dist/server/middleware/tool-context.middleware.d.ts +29 -0
- package/dist/server/middleware/tool-context.middleware.d.ts.map +1 -0
- package/dist/server/middleware/tool-context.middleware.js +34 -0
- package/dist/server/middleware/tool-context.middleware.js.map +1 -0
- package/dist/server/routes/admin.routes.d.ts +12 -0
- package/dist/server/routes/admin.routes.d.ts.map +1 -0
- package/dist/server/routes/admin.routes.js +338 -0
- package/dist/server/routes/admin.routes.js.map +1 -0
- package/dist/server/routes/api.routes.d.ts +13 -0
- package/dist/server/routes/api.routes.d.ts.map +1 -0
- package/dist/server/routes/api.routes.js +122 -0
- package/dist/server/routes/api.routes.js.map +1 -0
- package/dist/server/routes/mcp.routes.d.ts +22 -0
- package/dist/server/routes/mcp.routes.d.ts.map +1 -0
- package/dist/server/routes/mcp.routes.js +383 -0
- package/dist/server/routes/mcp.routes.js.map +1 -0
- package/dist/server/routes/tools.routes.d.ts +13 -0
- package/dist/server/routes/tools.routes.d.ts.map +1 -0
- package/dist/server/routes/tools.routes.js +99 -0
- package/dist/server/routes/tools.routes.js.map +1 -0
- package/dist/services/anchor/anchor-cache-service.d.ts +77 -0
- package/dist/services/anchor/anchor-cache-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-cache-service.js +193 -0
- package/dist/services/anchor/anchor-cache-service.js.map +1 -0
- package/dist/services/anchor/anchor-interfaces.d.ts +143 -0
- package/dist/services/anchor/anchor-interfaces.d.ts.map +1 -0
- package/dist/services/anchor/anchor-interfaces.js +24 -0
- package/dist/services/anchor/anchor-interfaces.js.map +1 -0
- package/dist/services/anchor/anchor-manager.d.ts +71 -0
- package/dist/services/anchor/anchor-manager.d.ts.map +1 -0
- package/dist/services/anchor/anchor-manager.js +205 -0
- package/dist/services/anchor/anchor-manager.js.map +1 -0
- package/dist/services/anchor/anchor-search-service.d.ts +115 -0
- package/dist/services/anchor/anchor-search-service.d.ts.map +1 -0
- package/dist/services/anchor/anchor-search-service.js +799 -0
- package/dist/services/anchor/anchor-search-service.js.map +1 -0
- package/dist/services/anchor/index.d.ts +11 -0
- package/dist/services/anchor/index.d.ts.map +1 -0
- package/dist/services/anchor/index.js +10 -0
- package/dist/services/anchor/index.js.map +1 -0
- package/dist/services/anchor-manager.d.ts +22 -208
- package/dist/services/anchor-manager.d.ts.map +1 -1
- package/dist/services/anchor-manager.js +72 -1088
- package/dist/services/anchor-manager.js.map +1 -1
- package/dist/services/error-logging-service.d.ts +1 -0
- package/dist/services/error-logging-service.d.ts.map +1 -1
- package/dist/services/error-logging-service.js +2 -0
- package/dist/services/error-logging-service.js.map +1 -1
- package/dist/tools/forget-tool.js +1 -1
- package/dist/tools/forget-tool.js.map +1 -1
- package/package.json +3 -1
|
@@ -24,6 +24,14 @@ import { DatabaseUtils } from '../utils/database.js';
|
|
|
24
24
|
import { getToolRegistry } from '../tools/index.js';
|
|
25
25
|
import Database from 'better-sqlite3';
|
|
26
26
|
import packageJson from '../../package.json' with { type: 'json' };
|
|
27
|
+
// Phase 1.2: 라우터 import
|
|
28
|
+
import { createToolsRouter } from './routes/tools.routes.js';
|
|
29
|
+
import { createAdminRouter } from './routes/admin.routes.js';
|
|
30
|
+
import { createApiRouter } from './routes/api.routes.js';
|
|
31
|
+
import { createMcpRouter } from './routes/mcp.routes.js';
|
|
32
|
+
import { broadcastAnchorMapUpdate } from './handlers/anchor-map.handler.js';
|
|
33
|
+
// Phase 0: 공통 미들웨어 import
|
|
34
|
+
import { createServiceInjector, createToolContextMiddleware, errorHandler } from './middleware/index.js';
|
|
27
35
|
// 전역 변수
|
|
28
36
|
let db = null;
|
|
29
37
|
let searchEngine;
|
|
@@ -39,6 +47,9 @@ let consolidationScoreService = null;
|
|
|
39
47
|
let writeCoalescingManager = null;
|
|
40
48
|
// 부트스트랩에서 반환된 전체 서비스 객체 (ToolContext 생성 시 사용)
|
|
41
49
|
let serverServices = null;
|
|
50
|
+
// Phase 1.2: 라우터에서 사용할 전역 변수들
|
|
51
|
+
// SSE Transport 저장소 (MCP 라우터용)
|
|
52
|
+
const transports = {};
|
|
42
53
|
function setTestDependencies({ database, searchEngine: search, hybridSearchEngine: hybrid, embeddingService: embedding }) {
|
|
43
54
|
db = database;
|
|
44
55
|
searchEngine = search ?? new SearchEngine();
|
|
@@ -68,240 +79,17 @@ app.get('/health', (req, res) => {
|
|
|
68
79
|
timestamp: new Date().toISOString()
|
|
69
80
|
});
|
|
70
81
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.error('❌ 도구 목록 조회 실패:', error);
|
|
83
|
-
res.status(500).json({
|
|
84
|
-
error: 'Failed to get tools',
|
|
85
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
/**
|
|
90
|
-
* Anchor Map 업데이트를 WebSocket 구독자에게 브로드캐스트
|
|
91
|
-
*/
|
|
92
|
-
async function broadcastAnchorMapUpdate(agentId) {
|
|
93
|
-
if (!anchorMapSubscribers.has(agentId) || anchorMapSubscribers.get(agentId).size === 0) {
|
|
94
|
-
return; // 구독자가 없으면 브로드캐스트하지 않음
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
// Anchor Map 데이터 생성 (API 엔드포인트와 동일한 로직)
|
|
98
|
-
if (!db || !serverServices || !serverServices.anchorManager) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const anchorManager = serverServices.anchorManager;
|
|
102
|
-
const anchors = await anchorManager.getAnchor(agentId);
|
|
103
|
-
if (!anchors || (Array.isArray(anchors) && anchors.length === 0)) {
|
|
104
|
-
// 앵커가 없어도 빈 데이터로 브로드캐스트
|
|
105
|
-
const updateData = {
|
|
106
|
-
agent_id: agentId,
|
|
107
|
-
anchors: [],
|
|
108
|
-
nodes: [],
|
|
109
|
-
links: [],
|
|
110
|
-
timestamp: new Date().toISOString()
|
|
111
|
-
};
|
|
112
|
-
const subscribers = anchorMapSubscribers.get(agentId);
|
|
113
|
-
for (const ws of subscribers) {
|
|
114
|
-
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
115
|
-
ws.send(JSON.stringify({
|
|
116
|
-
type: 'anchor_map_update',
|
|
117
|
-
data: updateData
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const anchorList = Array.isArray(anchors) ? anchors : [anchors];
|
|
124
|
-
// 각 앵커 주변의 관련 메모리 검색 및 네트워크 데이터 구성
|
|
125
|
-
const nodes = [];
|
|
126
|
-
const links = [];
|
|
127
|
-
const processedMemoryIds = new Set();
|
|
128
|
-
// 각 앵커에 대해 처리
|
|
129
|
-
for (const anchor of anchorList) {
|
|
130
|
-
if (!anchor.memory_id)
|
|
131
|
-
continue;
|
|
132
|
-
const anchorMemory = db.prepare(`
|
|
133
|
-
SELECT id, content, type, importance, created_at
|
|
134
|
-
FROM memory_item
|
|
135
|
-
WHERE id = ?
|
|
136
|
-
`).get(anchor.memory_id);
|
|
137
|
-
if (anchorMemory) {
|
|
138
|
-
nodes.push({
|
|
139
|
-
id: anchor.memory_id,
|
|
140
|
-
type: 'anchor',
|
|
141
|
-
slot: anchor.slot,
|
|
142
|
-
content: anchorMemory.content.substring(0, 100),
|
|
143
|
-
importance: anchorMemory.importance,
|
|
144
|
-
created_at: anchorMemory.created_at
|
|
145
|
-
});
|
|
146
|
-
processedMemoryIds.add(anchor.memory_id);
|
|
147
|
-
try {
|
|
148
|
-
const slotConfig = anchorManager.getSlotConfig(anchor.slot);
|
|
149
|
-
const searchResult = await anchorManager.searchLocal(agentId, anchor.slot, undefined, slotConfig.hop_limit, { limit: 50 });
|
|
150
|
-
for (const item of searchResult.items) {
|
|
151
|
-
if (processedMemoryIds.has(item.id))
|
|
152
|
-
continue;
|
|
153
|
-
nodes.push({
|
|
154
|
-
id: item.id,
|
|
155
|
-
type: 'memory',
|
|
156
|
-
content: item.content.substring(0, 100),
|
|
157
|
-
hop_distance: item.hop_distance || 0,
|
|
158
|
-
similarity: item.similarity,
|
|
159
|
-
importance: item.importance,
|
|
160
|
-
created_at: item.created_at
|
|
161
|
-
});
|
|
162
|
-
processedMemoryIds.add(item.id);
|
|
163
|
-
if (item.hop_distance === 1) {
|
|
164
|
-
links.push({
|
|
165
|
-
source: anchor.memory_id,
|
|
166
|
-
target: item.id,
|
|
167
|
-
type: 'hop',
|
|
168
|
-
hop_distance: 1,
|
|
169
|
-
similarity: item.similarity
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
console.error(`❌ 앵커 ${anchor.slot} 주변 메모리 검색 실패:`, error);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// memory_link 테이블 활용
|
|
180
|
-
for (const node of nodes) {
|
|
181
|
-
if (node.type === 'anchor')
|
|
182
|
-
continue;
|
|
183
|
-
const linkedMemories = db.prepare(`
|
|
184
|
-
SELECT target_memory_id, similarity, created_at
|
|
185
|
-
FROM memory_link
|
|
186
|
-
WHERE source_memory_id = ?
|
|
187
|
-
UNION
|
|
188
|
-
SELECT source_memory_id, similarity, created_at
|
|
189
|
-
FROM memory_link
|
|
190
|
-
WHERE target_memory_id = ?
|
|
191
|
-
`).all(node.id, node.id);
|
|
192
|
-
for (const link of linkedMemories) {
|
|
193
|
-
const linkedId = link.target_memory_id || link.source_memory_id;
|
|
194
|
-
if (linkedId && processedMemoryIds.has(linkedId)) {
|
|
195
|
-
links.push({
|
|
196
|
-
source: node.id,
|
|
197
|
-
target: linkedId,
|
|
198
|
-
type: 'link',
|
|
199
|
-
similarity: link.similarity
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const updateData = {
|
|
205
|
-
agent_id: agentId,
|
|
206
|
-
anchors: anchorList,
|
|
207
|
-
nodes,
|
|
208
|
-
links,
|
|
209
|
-
timestamp: new Date().toISOString()
|
|
210
|
-
};
|
|
211
|
-
// 구독자에게 브로드캐스트
|
|
212
|
-
const subscribers = anchorMapSubscribers.get(agentId);
|
|
213
|
-
for (const ws of subscribers) {
|
|
214
|
-
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
215
|
-
try {
|
|
216
|
-
ws.send(JSON.stringify({
|
|
217
|
-
type: 'anchor_map_update',
|
|
218
|
-
data: updateData
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
console.error('❌ WebSocket 브로드캐스트 실패:', error);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
console.log(`📡 Anchor Map 업데이트 브로드캐스트: agent_id=${agentId}, subscribers=${subscribers.size}`);
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
console.error(`❌ Anchor Map 브로드캐스트 실패 (agent: ${agentId}):`, error);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// 도구 실행 엔드포인트
|
|
233
|
-
app.post('/tools/:name', async (req, res) => {
|
|
234
|
-
const { name } = req.params;
|
|
235
|
-
const params = req.body;
|
|
236
|
-
try {
|
|
237
|
-
const toolRegistry = getToolRegistry();
|
|
238
|
-
// 부트스트랩에서 초기화된 서비스 객체를 사용하여 ToolContext 생성
|
|
239
|
-
if (!serverServices) {
|
|
240
|
-
return res.status(500).json({
|
|
241
|
-
error: 'Services not initialized',
|
|
242
|
-
message: '서비스가 초기화되지 않았습니다'
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
const context = {
|
|
246
|
-
db,
|
|
247
|
-
services: {
|
|
248
|
-
searchEngine: serverServices.searchEngine,
|
|
249
|
-
hybridSearchEngine: serverServices.hybridSearchEngine,
|
|
250
|
-
embeddingService: serverServices.embeddingService,
|
|
251
|
-
forgettingPolicyService: serverServices.forgettingPolicyService,
|
|
252
|
-
performanceMonitor: serverServices.performanceMonitor,
|
|
253
|
-
databaseOptimizer: serverServices.databaseOptimizer,
|
|
254
|
-
errorLoggingService: serverServices.errorLoggingService,
|
|
255
|
-
performanceAlertService: serverServices.performanceAlertService,
|
|
256
|
-
consolidationScoreService: serverServices.consolidationScoreService,
|
|
257
|
-
writeCoalescingManager: serverServices.writeCoalescingManager,
|
|
258
|
-
anchorManager: serverServices.anchorManager
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
// 도구 실행
|
|
262
|
-
const toolResult = await toolRegistry.execute(name, params, context);
|
|
263
|
-
// MCP 형식의 ToolResult에서 실제 데이터 추출
|
|
264
|
-
// content 배열의 첫 번째 항목의 text를 JSON 파싱
|
|
265
|
-
let actualResult = toolResult;
|
|
266
|
-
if (toolResult.content && Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
267
|
-
const firstContent = toolResult.content[0];
|
|
268
|
-
if (firstContent && firstContent.text) {
|
|
269
|
-
try {
|
|
270
|
-
const textContent = firstContent.text;
|
|
271
|
-
actualResult = JSON.parse(textContent);
|
|
272
|
-
}
|
|
273
|
-
catch (parseError) {
|
|
274
|
-
// JSON 파싱 실패 시 원본 content 사용
|
|
275
|
-
actualResult = toolResult;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// 앵커 관련 도구 실행 후 WebSocket 브로드캐스트
|
|
280
|
-
if (name === 'set_anchor' || name === 'clear_anchor') {
|
|
281
|
-
const agentId = params.agent_id || 'default';
|
|
282
|
-
// 비동기로 브로드캐스트 (응답 지연 방지)
|
|
283
|
-
setImmediate(() => {
|
|
284
|
-
broadcastAnchorMapUpdate(agentId).catch(error => {
|
|
285
|
-
console.error('❌ Anchor Map 브로드캐스트 실패:', error);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
return res.json({
|
|
290
|
-
result: actualResult,
|
|
291
|
-
tool: name,
|
|
292
|
-
timestamp: new Date().toISOString()
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
console.error(`❌ Tool ${name} 실행 실패:`, error);
|
|
297
|
-
return res.status(500).json({
|
|
298
|
-
error: 'Tool execution failed',
|
|
299
|
-
tool: name,
|
|
300
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
// 대시보드 라우트
|
|
82
|
+
// Phase 1.2: 라우터 등록
|
|
83
|
+
// WebSocket 클라이언트 관리 (Anchor Map 업데이트용) - 라우터에서도 사용
|
|
84
|
+
const anchorMapSubscribers = new Map(); // agent_id -> WebSocket Set
|
|
85
|
+
// 라우터 등록 (서비스 초기화 후 업데이트됨)
|
|
86
|
+
let toolsRouter = null;
|
|
87
|
+
let adminRouter = null;
|
|
88
|
+
let apiRouter = null;
|
|
89
|
+
let mcpRouter = null;
|
|
90
|
+
// Phase 1.2: 기존 엔드포인트는 모두 라우터로 이동됨
|
|
91
|
+
// 주석 처리된 기존 코드는 제거됨 (tools.routes.ts, admin.routes.ts, api.routes.ts, mcp.routes.ts로 이동)
|
|
92
|
+
// 대시보드 라우트 (정적 파일 서빙)
|
|
305
93
|
app.get('/dashboard', (req, res) => {
|
|
306
94
|
res.sendFile('dashboard.html', { root: 'static' }, (err) => {
|
|
307
95
|
if (err) {
|
|
@@ -310,901 +98,8 @@ app.get('/dashboard', (req, res) => {
|
|
|
310
98
|
}
|
|
311
99
|
});
|
|
312
100
|
});
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
const agentId = req.query.agent_id || 'default';
|
|
316
|
-
try {
|
|
317
|
-
// 데이터베이스 연결 확인
|
|
318
|
-
if (!db || !serverServices) {
|
|
319
|
-
return res.status(500).json({
|
|
320
|
-
error: 'Services not initialized',
|
|
321
|
-
message: '서비스가 초기화되지 않았습니다'
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
const anchorManager = serverServices.anchorManager;
|
|
325
|
-
if (!anchorManager) {
|
|
326
|
-
return res.status(500).json({
|
|
327
|
-
error: 'AnchorManager not available',
|
|
328
|
-
message: 'AnchorManager가 사용할 수 없습니다'
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
// 앵커 정보 조회
|
|
332
|
-
const anchors = await anchorManager.getAnchor(agentId);
|
|
333
|
-
if (!anchors || (Array.isArray(anchors) && anchors.length === 0)) {
|
|
334
|
-
return res.json({
|
|
335
|
-
agent_id: agentId,
|
|
336
|
-
anchors: [],
|
|
337
|
-
nodes: [],
|
|
338
|
-
links: [],
|
|
339
|
-
timestamp: new Date().toISOString()
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
const anchorList = Array.isArray(anchors) ? anchors : [anchors];
|
|
343
|
-
// 각 앵커 주변의 관련 메모리 검색 및 네트워크 데이터 구성
|
|
344
|
-
const nodes = [];
|
|
345
|
-
const links = [];
|
|
346
|
-
const processedMemoryIds = new Set();
|
|
347
|
-
// 각 앵커에 대해 처리
|
|
348
|
-
for (const anchor of anchorList) {
|
|
349
|
-
if (!anchor.memory_id)
|
|
350
|
-
continue;
|
|
351
|
-
// 앵커 노드 추가
|
|
352
|
-
const anchorMemory = db.prepare(`
|
|
353
|
-
SELECT id, content, type, importance, created_at
|
|
354
|
-
FROM memory_item
|
|
355
|
-
WHERE id = ?
|
|
356
|
-
`).get(anchor.memory_id);
|
|
357
|
-
if (anchorMemory) {
|
|
358
|
-
nodes.push({
|
|
359
|
-
id: anchor.memory_id,
|
|
360
|
-
type: 'anchor',
|
|
361
|
-
slot: anchor.slot,
|
|
362
|
-
content: anchorMemory.content.substring(0, 100), // 처음 100자만
|
|
363
|
-
importance: anchorMemory.importance,
|
|
364
|
-
created_at: anchorMemory.created_at
|
|
365
|
-
});
|
|
366
|
-
processedMemoryIds.add(anchor.memory_id);
|
|
367
|
-
// 앵커 주변 메모리 검색 (hop 거리별)
|
|
368
|
-
try {
|
|
369
|
-
const slotConfig = anchorManager.getSlotConfig(anchor.slot);
|
|
370
|
-
const searchResult = await anchorManager.searchLocal(agentId, anchor.slot, undefined, // query 없이 앵커 기반 recall
|
|
371
|
-
slotConfig.hop_limit, { limit: 50 } // 충분한 수의 메모리 가져오기
|
|
372
|
-
);
|
|
373
|
-
// 검색 결과를 노드와 링크로 변환
|
|
374
|
-
for (const item of searchResult.items) {
|
|
375
|
-
if (processedMemoryIds.has(item.id))
|
|
376
|
-
continue;
|
|
377
|
-
nodes.push({
|
|
378
|
-
id: item.id,
|
|
379
|
-
type: 'memory',
|
|
380
|
-
content: item.content.substring(0, 100),
|
|
381
|
-
hop_distance: item.hop_distance || 0,
|
|
382
|
-
similarity: item.similarity,
|
|
383
|
-
importance: item.importance,
|
|
384
|
-
created_at: item.created_at
|
|
385
|
-
});
|
|
386
|
-
processedMemoryIds.add(item.id);
|
|
387
|
-
// 링크 추가 (앵커에서 메모리로)
|
|
388
|
-
if (item.hop_distance === 1) {
|
|
389
|
-
links.push({
|
|
390
|
-
source: anchor.memory_id,
|
|
391
|
-
target: item.id,
|
|
392
|
-
type: 'hop',
|
|
393
|
-
hop_distance: 1,
|
|
394
|
-
similarity: item.similarity
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
console.error(`❌ 앵커 ${anchor.slot} 주변 메모리 검색 실패:`, error);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// memory_link 테이블을 활용한 직접 연결 정보 추가
|
|
405
|
-
for (const node of nodes) {
|
|
406
|
-
if (node.type === 'anchor')
|
|
407
|
-
continue;
|
|
408
|
-
// memory_link 테이블 스키마: source_id, target_id, relation_type, created_at
|
|
409
|
-
const linkedMemories = db.prepare(`
|
|
410
|
-
SELECT target_id, relation_type, created_at
|
|
411
|
-
FROM memory_link
|
|
412
|
-
WHERE source_id = ?
|
|
413
|
-
UNION
|
|
414
|
-
SELECT source_id, relation_type, created_at
|
|
415
|
-
FROM memory_link
|
|
416
|
-
WHERE target_id = ?
|
|
417
|
-
`).all(node.id, node.id);
|
|
418
|
-
for (const link of linkedMemories) {
|
|
419
|
-
const linkedId = link.target_id || link.source_id;
|
|
420
|
-
if (linkedId && processedMemoryIds.has(linkedId)) {
|
|
421
|
-
// 이미 노드에 있는 메모리와의 직접 연결
|
|
422
|
-
// relation_type을 기반으로 similarity 추정 (기본값 0.7)
|
|
423
|
-
const similarity = link.relation_type === 'derived_from' ? 0.9 :
|
|
424
|
-
link.relation_type === 'cause_of' ? 0.8 : 0.7;
|
|
425
|
-
links.push({
|
|
426
|
-
source: node.id,
|
|
427
|
-
target: linkedId,
|
|
428
|
-
type: 'link',
|
|
429
|
-
similarity: similarity
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return res.json({
|
|
435
|
-
agent_id: agentId,
|
|
436
|
-
anchors: anchorList,
|
|
437
|
-
nodes,
|
|
438
|
-
links,
|
|
439
|
-
timestamp: new Date().toISOString()
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
console.error(`❌ Anchor Map 데이터 조회 실패 (agent: ${agentId}):`, error);
|
|
444
|
-
return res.status(500).json({
|
|
445
|
-
error: 'Failed to get anchor map data',
|
|
446
|
-
message: error instanceof Error ? error.message : 'Unknown error',
|
|
447
|
-
agent_id: agentId
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
// 이웃 기억 조회 엔드포인트
|
|
452
|
-
app.get('/memories/:id/neighbors', async (req, res) => {
|
|
453
|
-
const { id } = req.params;
|
|
454
|
-
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 5;
|
|
455
|
-
const similarityThreshold = req.query.similarity_threshold
|
|
456
|
-
? parseFloat(req.query.similarity_threshold)
|
|
457
|
-
: 0.8;
|
|
458
|
-
try {
|
|
459
|
-
// 데이터베이스 연결 확인
|
|
460
|
-
if (!db) {
|
|
461
|
-
return res.status(500).json({
|
|
462
|
-
error: 'Database not connected',
|
|
463
|
-
message: '데이터베이스가 연결되지 않았습니다'
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
// 파라미터 검증
|
|
467
|
-
if (isNaN(limit) || limit < 1 || limit > 50) {
|
|
468
|
-
return res.status(400).json({
|
|
469
|
-
error: 'Invalid limit parameter',
|
|
470
|
-
message: 'limit은 1 이상 50 이하여야 합니다'
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
if (isNaN(similarityThreshold) || similarityThreshold < 0 || similarityThreshold > 1) {
|
|
474
|
-
return res.status(400).json({
|
|
475
|
-
error: 'Invalid similarity_threshold parameter',
|
|
476
|
-
message: 'similarity_threshold는 0 이상 1 이하여야 합니다'
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
// MemoryNeighborService 인스턴스 생성
|
|
480
|
-
const vectorSearchEngine = getVectorSearchEngine();
|
|
481
|
-
const neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingService);
|
|
482
|
-
// 데이터베이스 설정
|
|
483
|
-
neighborService.setDatabase(db);
|
|
484
|
-
// 이웃 기억 조회
|
|
485
|
-
const result = await neighborService.getNeighbors(id, {
|
|
486
|
-
limit,
|
|
487
|
-
similarity_threshold: similarityThreshold
|
|
488
|
-
});
|
|
489
|
-
return res.json({
|
|
490
|
-
memory_id: result.memory_id,
|
|
491
|
-
neighbors: result.neighbors,
|
|
492
|
-
total_count: result.total_count,
|
|
493
|
-
query_time: result.query_time,
|
|
494
|
-
timestamp: new Date().toISOString()
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
catch (error) {
|
|
498
|
-
// MemoryNotFoundError 처리 (404)
|
|
499
|
-
if (error instanceof MemoryNotFoundError) {
|
|
500
|
-
return res.status(404).json({
|
|
501
|
-
error: 'Memory not found',
|
|
502
|
-
message: `메모리를 찾을 수 없습니다: ${id}`,
|
|
503
|
-
memory_id: id
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
// 기타 에러 처리 (500)
|
|
507
|
-
console.error(`❌ 이웃 기억 조회 실패 (${id}):`, error);
|
|
508
|
-
return res.status(500).json({
|
|
509
|
-
error: 'Failed to get memory neighbors',
|
|
510
|
-
message: error instanceof Error ? error.message : 'Unknown error',
|
|
511
|
-
memory_id: id
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
// MCP SSE 엔드포인트 - MCP SDK 호환 구현
|
|
516
|
-
// Store transports by session ID
|
|
517
|
-
const transports = {};
|
|
518
|
-
// SSE endpoint for establishing the stream
|
|
519
|
-
app.get('/mcp', async (req, res) => {
|
|
520
|
-
console.log('🔗 MCP SSE 클라이언트 연결 요청');
|
|
521
|
-
try {
|
|
522
|
-
// SSE 헤더 설정
|
|
523
|
-
res.writeHead(200, {
|
|
524
|
-
'Content-Type': 'text/event-stream',
|
|
525
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
526
|
-
'Connection': 'keep-alive',
|
|
527
|
-
'Access-Control-Allow-Origin': '*',
|
|
528
|
-
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, Authorization',
|
|
529
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
530
|
-
'X-Accel-Buffering': 'no' // nginx 버퍼링 비활성화
|
|
531
|
-
});
|
|
532
|
-
// Generate session ID
|
|
533
|
-
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
534
|
-
// Send the endpoint event with session ID
|
|
535
|
-
const endpointUrl = `/messages?sessionId=${sessionId}`;
|
|
536
|
-
res.write(`event: endpoint\ndata: ${endpointUrl}\n\n`);
|
|
537
|
-
// MCP 서버 준비 완료 알림 (클라이언트가 initialize를 보내야 함)
|
|
538
|
-
res.write(`data: {"type": "ready"}\n\n`);
|
|
539
|
-
// Keep-alive ping 전송
|
|
540
|
-
const keepAliveInterval = setInterval(() => {
|
|
541
|
-
if (res.writableEnded) {
|
|
542
|
-
clearInterval(keepAliveInterval);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
try {
|
|
546
|
-
res.write(`data: {"type": "ping"}\n\n`);
|
|
547
|
-
}
|
|
548
|
-
catch (error) {
|
|
549
|
-
clearInterval(keepAliveInterval);
|
|
550
|
-
}
|
|
551
|
-
}, 30000); // 30초마다 ping
|
|
552
|
-
// Store the transport info
|
|
553
|
-
transports[sessionId] = {
|
|
554
|
-
res: res,
|
|
555
|
-
sessionId: sessionId,
|
|
556
|
-
keepAliveInterval: keepAliveInterval
|
|
557
|
-
};
|
|
558
|
-
// 연결 종료 처리
|
|
559
|
-
req.on('close', () => {
|
|
560
|
-
console.log(`🔌 MCP SSE 클라이언트 연결 정상 종료됨 (session: ${sessionId})`);
|
|
561
|
-
clearInterval(keepAliveInterval);
|
|
562
|
-
delete transports[sessionId];
|
|
563
|
-
});
|
|
564
|
-
req.on('error', (error) => {
|
|
565
|
-
// ECONNRESET은 정상적인 연결 종료이므로 에러로 처리하지 않음
|
|
566
|
-
if (error.code === 'ECONNRESET') {
|
|
567
|
-
console.log(`🔌 MCP SSE 클라이언트 연결 정상 종료됨 (session: ${sessionId})`);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
console.error(`❌ MCP SSE 연결 에러 (session: ${sessionId}):`, error);
|
|
571
|
-
}
|
|
572
|
-
clearInterval(keepAliveInterval);
|
|
573
|
-
delete transports[sessionId];
|
|
574
|
-
});
|
|
575
|
-
console.log(`✅ MCP SSE 스트림 설정 완료 (session: ${sessionId})`);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
console.error('❌ SSE 스트림 설정 실패:', error);
|
|
580
|
-
if (!res.headersSent) {
|
|
581
|
-
res.status(500).send('Error establishing SSE stream');
|
|
582
|
-
}
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
// Messages endpoint for receiving client JSON-RPC requests
|
|
587
|
-
app.post('/messages', express.json(), async (req, res) => {
|
|
588
|
-
console.log('📨 MCP 메시지 수신:', req.body.method);
|
|
589
|
-
// Extract session ID from URL query parameter
|
|
590
|
-
const sessionId = req.query.sessionId;
|
|
591
|
-
if (!sessionId) {
|
|
592
|
-
console.error('❌ No session ID provided in request URL');
|
|
593
|
-
res.status(400).send('Missing sessionId parameter');
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
const transport = transports[sessionId];
|
|
597
|
-
if (!transport) {
|
|
598
|
-
console.error(`❌ No active transport found for session ID: ${sessionId}`);
|
|
599
|
-
res.status(404).send('Session not found');
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
const message = req.body;
|
|
603
|
-
let result;
|
|
604
|
-
console.log(`🔍 MCP 메시지 처리 중: ${message.method}`, JSON.stringify(message, null, 2));
|
|
605
|
-
try {
|
|
606
|
-
if (message.method === 'initialize') {
|
|
607
|
-
console.log('🚀 MCP initialize 요청 처리 중...');
|
|
608
|
-
result = {
|
|
609
|
-
jsonrpc: '2.0',
|
|
610
|
-
id: message.id,
|
|
611
|
-
result: {
|
|
612
|
-
protocolVersion: '2024-11-05',
|
|
613
|
-
capabilities: {
|
|
614
|
-
tools: {}
|
|
615
|
-
},
|
|
616
|
-
serverInfo: {
|
|
617
|
-
name: 'memento-memory',
|
|
618
|
-
version: '0.1.0'
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
console.log('✅ MCP initialize 응답 생성 완료:', JSON.stringify(result, null, 2));
|
|
623
|
-
}
|
|
624
|
-
else if (message.method === 'notifications/initialized') {
|
|
625
|
-
console.log('🔔 MCP initialized 알림 수신');
|
|
626
|
-
result = {
|
|
627
|
-
jsonrpc: '2.0',
|
|
628
|
-
id: message.id,
|
|
629
|
-
result: {}
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
else if (message.method === 'tools/list') {
|
|
633
|
-
console.log('📋 MCP tools/list 요청 처리 중...');
|
|
634
|
-
try {
|
|
635
|
-
const toolRegistry = getToolRegistry();
|
|
636
|
-
const tools = toolRegistry.getAll();
|
|
637
|
-
console.log('🔍 도구 목록 사용, 길이:', tools.length);
|
|
638
|
-
result = {
|
|
639
|
-
jsonrpc: '2.0',
|
|
640
|
-
id: message.id,
|
|
641
|
-
result: { tools }
|
|
642
|
-
};
|
|
643
|
-
console.log('✅ MCP tools/list 응답 생성 완료, tools 개수:', tools.length);
|
|
644
|
-
console.log('🔍 응답 크기:', JSON.stringify(result).length, 'bytes');
|
|
645
|
-
// SSE 응답 즉시 전송
|
|
646
|
-
console.log('📤 SSE 응답 즉시 전송 중...');
|
|
647
|
-
if (transport && transport.res && !transport.res.writableEnded) {
|
|
648
|
-
const sseData = `data: ${JSON.stringify(result)}\n\n`;
|
|
649
|
-
transport.res.write(sseData);
|
|
650
|
-
console.log('✅ SSE 응답 즉시 전송 완료, 크기:', sseData.length, 'bytes');
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
console.error('❌ SSE transport가 유효하지 않음');
|
|
654
|
-
}
|
|
655
|
-
// HTTP 응답 전송
|
|
656
|
-
res.json({ status: 'ok' });
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
catch (toolsError) {
|
|
660
|
-
console.error('❌ tools/list 처리 중 오류:', toolsError);
|
|
661
|
-
const errorResult = {
|
|
662
|
-
jsonrpc: '2.0',
|
|
663
|
-
id: message.id,
|
|
664
|
-
error: {
|
|
665
|
-
code: -32603,
|
|
666
|
-
message: 'Internal error',
|
|
667
|
-
data: toolsError instanceof Error ? toolsError.message : String(toolsError)
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
if (transport && transport.res && !transport.res.writableEnded) {
|
|
671
|
-
transport.res.write(`data: ${JSON.stringify(errorResult)}\n\n`);
|
|
672
|
-
}
|
|
673
|
-
res.json({ status: 'error' });
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
else if (message.method === 'tools/call') {
|
|
678
|
-
const { name, arguments: args } = message.params;
|
|
679
|
-
const toolRegistry = getToolRegistry();
|
|
680
|
-
// 부트스트랩에서 초기화된 서비스 객체를 사용하여 ToolContext 생성
|
|
681
|
-
if (!serverServices) {
|
|
682
|
-
result = {
|
|
683
|
-
jsonrpc: '2.0',
|
|
684
|
-
id: message.id,
|
|
685
|
-
error: {
|
|
686
|
-
code: -32603,
|
|
687
|
-
message: 'Internal error',
|
|
688
|
-
data: '서비스가 초기화되지 않았습니다'
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
const context = {
|
|
694
|
-
db,
|
|
695
|
-
services: {
|
|
696
|
-
searchEngine: serverServices.searchEngine,
|
|
697
|
-
hybridSearchEngine: serverServices.hybridSearchEngine,
|
|
698
|
-
embeddingService: serverServices.embeddingService,
|
|
699
|
-
forgettingPolicyService: serverServices.forgettingPolicyService,
|
|
700
|
-
performanceMonitor: serverServices.performanceMonitor,
|
|
701
|
-
databaseOptimizer: serverServices.databaseOptimizer,
|
|
702
|
-
errorLoggingService: serverServices.errorLoggingService,
|
|
703
|
-
performanceAlertService: serverServices.performanceAlertService,
|
|
704
|
-
consolidationScoreService: serverServices.consolidationScoreService,
|
|
705
|
-
writeCoalescingManager: serverServices.writeCoalescingManager,
|
|
706
|
-
anchorManager: serverServices.anchorManager
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
// 도구 실행
|
|
710
|
-
const toolResult = await toolRegistry.execute(name, args, context);
|
|
711
|
-
result = {
|
|
712
|
-
jsonrpc: '2.0',
|
|
713
|
-
id: message.id,
|
|
714
|
-
result: { content: [{ type: 'text', text: JSON.stringify(toolResult) }] }
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
else if (message.method === 'prompts/list') {
|
|
719
|
-
console.log('📋 MCP prompts/list 요청 처리 중...');
|
|
720
|
-
const prompts = [
|
|
721
|
-
{
|
|
722
|
-
name: 'memory_injection',
|
|
723
|
-
description: '관련 기억을 요약하여 프롬프트에 주입',
|
|
724
|
-
arguments: [
|
|
725
|
-
{
|
|
726
|
-
name: 'query',
|
|
727
|
-
description: '검색할 쿼리',
|
|
728
|
-
required: true
|
|
729
|
-
},
|
|
730
|
-
{
|
|
731
|
-
name: 'token_budget',
|
|
732
|
-
description: '토큰 예산 (기본값: 1000)',
|
|
733
|
-
required: false
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
name: 'max_memories',
|
|
737
|
-
description: '최대 기억 개수 (기본값: 5)',
|
|
738
|
-
required: false
|
|
739
|
-
}
|
|
740
|
-
]
|
|
741
|
-
}
|
|
742
|
-
];
|
|
743
|
-
result = {
|
|
744
|
-
jsonrpc: '2.0',
|
|
745
|
-
id: message.id,
|
|
746
|
-
result: { prompts }
|
|
747
|
-
};
|
|
748
|
-
console.log('✅ MCP prompts/list 응답 생성 완료');
|
|
749
|
-
}
|
|
750
|
-
else if (message.method === 'prompts/get') {
|
|
751
|
-
const { name } = message.params;
|
|
752
|
-
if (name === 'memory_injection') {
|
|
753
|
-
result = {
|
|
754
|
-
jsonrpc: '2.0',
|
|
755
|
-
id: message.id,
|
|
756
|
-
result: {
|
|
757
|
-
description: '관련 기억을 요약하여 프롬프트에 주입',
|
|
758
|
-
arguments: [
|
|
759
|
-
{
|
|
760
|
-
name: 'query',
|
|
761
|
-
description: '검색할 쿼리',
|
|
762
|
-
required: true
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
name: 'token_budget',
|
|
766
|
-
description: '토큰 예산 (기본값: 1000)',
|
|
767
|
-
required: false
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
name: 'max_memories',
|
|
771
|
-
description: '최대 기억 개수 (기본값: 5)',
|
|
772
|
-
required: false
|
|
773
|
-
}
|
|
774
|
-
]
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
result = {
|
|
780
|
-
jsonrpc: '2.0',
|
|
781
|
-
id: message.id,
|
|
782
|
-
error: {
|
|
783
|
-
code: -32601,
|
|
784
|
-
message: 'Prompt not found'
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
else if (message.method === 'prompts/call') {
|
|
790
|
-
const { name, arguments: args } = message.params;
|
|
791
|
-
if (name === 'memory_injection') {
|
|
792
|
-
try {
|
|
793
|
-
// 부트스트랩에서 초기화된 서비스 객체를 사용하여 ToolContext 생성
|
|
794
|
-
if (!serverServices) {
|
|
795
|
-
result = {
|
|
796
|
-
jsonrpc: '2.0',
|
|
797
|
-
id: message.id,
|
|
798
|
-
error: {
|
|
799
|
-
code: -32603,
|
|
800
|
-
message: 'Internal error',
|
|
801
|
-
data: '서비스가 초기화되지 않았습니다'
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
// MemoryInjectionPrompt 도구 사용
|
|
807
|
-
const toolRegistry = getToolRegistry();
|
|
808
|
-
const context = {
|
|
809
|
-
db,
|
|
810
|
-
services: {
|
|
811
|
-
searchEngine: serverServices.searchEngine,
|
|
812
|
-
hybridSearchEngine: serverServices.hybridSearchEngine,
|
|
813
|
-
embeddingService: serverServices.embeddingService,
|
|
814
|
-
forgettingPolicyService: serverServices.forgettingPolicyService,
|
|
815
|
-
performanceMonitor: serverServices.performanceMonitor,
|
|
816
|
-
databaseOptimizer: serverServices.databaseOptimizer,
|
|
817
|
-
errorLoggingService: serverServices.errorLoggingService,
|
|
818
|
-
performanceAlertService: serverServices.performanceAlertService,
|
|
819
|
-
consolidationScoreService: serverServices.consolidationScoreService,
|
|
820
|
-
writeCoalescingManager: serverServices.writeCoalescingManager
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
const promptResult = await toolRegistry.execute('memory_injection', args, context);
|
|
824
|
-
result = {
|
|
825
|
-
jsonrpc: '2.0',
|
|
826
|
-
id: message.id,
|
|
827
|
-
result: promptResult
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
result = {
|
|
833
|
-
jsonrpc: '2.0',
|
|
834
|
-
id: message.id,
|
|
835
|
-
error: {
|
|
836
|
-
code: -32603,
|
|
837
|
-
message: 'Prompt execution failed',
|
|
838
|
-
data: error instanceof Error ? error.message : 'Unknown error'
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
result = {
|
|
845
|
-
jsonrpc: '2.0',
|
|
846
|
-
id: message.id,
|
|
847
|
-
error: {
|
|
848
|
-
code: -32601,
|
|
849
|
-
message: 'Prompt not found'
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
result = {
|
|
856
|
-
jsonrpc: '2.0',
|
|
857
|
-
id: message.id,
|
|
858
|
-
error: {
|
|
859
|
-
code: -32601,
|
|
860
|
-
message: 'Method not found'
|
|
861
|
-
}
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
// Send response via SSE
|
|
865
|
-
console.log('📤 SSE 응답 전송 중:', JSON.stringify(result).substring(0, 200) + '...');
|
|
866
|
-
try {
|
|
867
|
-
// transport 객체 유효성 확인
|
|
868
|
-
if (!transport || !transport.res || transport.res.writableEnded) {
|
|
869
|
-
console.error('❌ SSE transport가 유효하지 않음');
|
|
870
|
-
res.status(500).json({ error: 'SSE transport invalid' });
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
// SSE 응답 전송
|
|
874
|
-
const sseData = `data: ${JSON.stringify(result)}\n\n`;
|
|
875
|
-
transport.res.write(sseData);
|
|
876
|
-
console.log('✅ SSE 응답 전송 완료, 크기:', sseData.length, 'bytes');
|
|
877
|
-
}
|
|
878
|
-
catch (sseError) {
|
|
879
|
-
console.error('❌ SSE 응답 전송 실패:', sseError);
|
|
880
|
-
// SSE 전송 실패 시에도 HTTP 응답은 정상 처리
|
|
881
|
-
}
|
|
882
|
-
// Send HTTP response
|
|
883
|
-
res.json({ status: 'ok' });
|
|
884
|
-
}
|
|
885
|
-
catch (error) {
|
|
886
|
-
console.error('❌ MCP 메시지 처리 실패:', error);
|
|
887
|
-
const errorResponse = {
|
|
888
|
-
jsonrpc: '2.0',
|
|
889
|
-
id: message?.id || null,
|
|
890
|
-
error: {
|
|
891
|
-
code: -32603,
|
|
892
|
-
message: 'Internal error',
|
|
893
|
-
data: error instanceof Error ? error.message : 'Unknown error'
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
// Send error via SSE
|
|
897
|
-
try {
|
|
898
|
-
if (transport && transport.res && !transport.res.writableEnded) {
|
|
899
|
-
const errorSseData = `data: ${JSON.stringify(errorResponse)}\n\n`;
|
|
900
|
-
transport.res.write(errorSseData);
|
|
901
|
-
console.log('✅ SSE 에러 응답 전송 완료');
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
console.error('❌ SSE transport가 유효하지 않아 에러 응답 전송 실패');
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
catch (errorSseError) {
|
|
908
|
-
console.error('❌ SSE 에러 응답 전송 실패:', errorSseError);
|
|
909
|
-
}
|
|
910
|
-
// Send HTTP response
|
|
911
|
-
res.json({ status: 'error' });
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
// 관리자 API 엔드포인트들
|
|
915
|
-
app.post('/admin/memory/cleanup', async (req, res) => {
|
|
916
|
-
try {
|
|
917
|
-
// 메모리 정리 로직 (기존 CleanupMemoryTool 로직)
|
|
918
|
-
if (!db) {
|
|
919
|
-
return res.status(500).json({ error: '데이터베이스가 연결되지 않았습니다' });
|
|
920
|
-
}
|
|
921
|
-
// 간단한 메모리 정리 구현
|
|
922
|
-
const result = await db.prepare(`
|
|
923
|
-
DELETE FROM memory_item
|
|
924
|
-
WHERE pinned = FALSE
|
|
925
|
-
AND type = 'working'
|
|
926
|
-
AND created_at < datetime('now', '-2 days')
|
|
927
|
-
`).run();
|
|
928
|
-
return res.json({
|
|
929
|
-
message: '메모리 정리 완료',
|
|
930
|
-
deleted_count: result.changes,
|
|
931
|
-
timestamp: new Date().toISOString()
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
catch (error) {
|
|
935
|
-
console.error('❌ 메모리 정리 실패:', error);
|
|
936
|
-
return res.status(500).json({
|
|
937
|
-
error: '메모리 정리 실패',
|
|
938
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
app.get('/admin/stats/forgetting', async (req, res) => {
|
|
943
|
-
try {
|
|
944
|
-
// 망각 통계 로직 (기존 ForgettingStatsTool 로직)
|
|
945
|
-
if (!db) {
|
|
946
|
-
return res.status(500).json({ error: '데이터베이스가 연결되지 않았습니다' });
|
|
947
|
-
}
|
|
948
|
-
const stats = await db.prepare(`
|
|
949
|
-
SELECT
|
|
950
|
-
type,
|
|
951
|
-
COUNT(*) as total_count,
|
|
952
|
-
COUNT(CASE WHEN pinned = TRUE THEN 1 END) as pinned_count,
|
|
953
|
-
COUNT(CASE WHEN created_at < datetime('now', '-30 days') THEN 1 END) as old_count
|
|
954
|
-
FROM memory_item
|
|
955
|
-
GROUP BY type
|
|
956
|
-
`).all();
|
|
957
|
-
return res.json({
|
|
958
|
-
message: '망각 통계 조회 완료',
|
|
959
|
-
stats,
|
|
960
|
-
timestamp: new Date().toISOString()
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
catch (error) {
|
|
964
|
-
console.error('❌ 망각 통계 조회 실패:', error);
|
|
965
|
-
return res.status(500).json({
|
|
966
|
-
error: '망각 통계 조회 실패',
|
|
967
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
app.get('/admin/stats/performance', async (req, res) => {
|
|
972
|
-
try {
|
|
973
|
-
// 성능 통계 로직 (기존 PerformanceStatsTool 로직)
|
|
974
|
-
if (!db) {
|
|
975
|
-
return res.status(500).json({ error: '데이터베이스가 연결되지 않았습니다' });
|
|
976
|
-
}
|
|
977
|
-
const stats = await db.prepare(`
|
|
978
|
-
SELECT
|
|
979
|
-
COUNT(*) as total_memories,
|
|
980
|
-
COUNT(CASE WHEN type = 'working' THEN 1 END) as working_memories,
|
|
981
|
-
COUNT(CASE WHEN type = 'episodic' THEN 1 END) as episodic_memories,
|
|
982
|
-
COUNT(CASE WHEN type = 'semantic' THEN 1 END) as semantic_memories,
|
|
983
|
-
COUNT(CASE WHEN type = 'procedural' THEN 1 END) as procedural_memories,
|
|
984
|
-
COUNT(CASE WHEN pinned = TRUE THEN 1 END) as pinned_memories
|
|
985
|
-
FROM memory_item
|
|
986
|
-
`).get();
|
|
987
|
-
return res.json({
|
|
988
|
-
message: '성능 통계 조회 완료',
|
|
989
|
-
stats,
|
|
990
|
-
timestamp: new Date().toISOString()
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
catch (error) {
|
|
994
|
-
console.error('❌ 성능 통계 조회 실패:', error);
|
|
995
|
-
return res.status(500).json({
|
|
996
|
-
error: '성능 통계 조회 실패',
|
|
997
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
app.post('/admin/database/optimize', async (req, res) => {
|
|
1002
|
-
try {
|
|
1003
|
-
// 데이터베이스 최적화 로직 (기존 DatabaseOptimizeTool 로직)
|
|
1004
|
-
if (!db) {
|
|
1005
|
-
return res.status(500).json({ error: '데이터베이스가 연결되지 않았습니다' });
|
|
1006
|
-
}
|
|
1007
|
-
// VACUUM 실행
|
|
1008
|
-
await db.prepare('VACUUM').run();
|
|
1009
|
-
// ANALYZE 실행
|
|
1010
|
-
await db.prepare('ANALYZE').run();
|
|
1011
|
-
return res.json({
|
|
1012
|
-
message: '데이터베이스 최적화 완료',
|
|
1013
|
-
timestamp: new Date().toISOString()
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
catch (error) {
|
|
1017
|
-
console.error('❌ 데이터베이스 최적화 실패:', error);
|
|
1018
|
-
return res.status(500).json({
|
|
1019
|
-
error: '데이터베이스 최적화 실패',
|
|
1020
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
app.get('/admin/stats/errors', async (req, res) => {
|
|
1025
|
-
try {
|
|
1026
|
-
// 에러 통계 로직 (기존 errorStatsTool 로직)
|
|
1027
|
-
res.json({
|
|
1028
|
-
message: '에러 통계 조회 완료',
|
|
1029
|
-
stats: {
|
|
1030
|
-
total_errors: 0,
|
|
1031
|
-
recent_errors: [],
|
|
1032
|
-
error_types: {}
|
|
1033
|
-
},
|
|
1034
|
-
timestamp: new Date().toISOString()
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
catch (error) {
|
|
1038
|
-
console.error('❌ 에러 통계 조회 실패:', error);
|
|
1039
|
-
res.status(500).json({
|
|
1040
|
-
error: '에러 통계 조회 실패',
|
|
1041
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
app.post('/admin/errors/resolve', async (req, res) => {
|
|
1046
|
-
try {
|
|
1047
|
-
const { errorId, resolvedBy, reason } = req.body;
|
|
1048
|
-
// 에러 해결 로직 (기존 resolveErrorTool 로직)
|
|
1049
|
-
res.json({
|
|
1050
|
-
message: '에러 해결 완료',
|
|
1051
|
-
errorId,
|
|
1052
|
-
resolvedBy,
|
|
1053
|
-
reason,
|
|
1054
|
-
timestamp: new Date().toISOString()
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
catch (error) {
|
|
1058
|
-
console.error('❌ 에러 해결 실패:', error);
|
|
1059
|
-
res.status(500).json({
|
|
1060
|
-
error: '에러 해결 실패',
|
|
1061
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
app.get('/admin/alerts/performance', async (req, res) => {
|
|
1066
|
-
try {
|
|
1067
|
-
// 성능 알림 로직 (기존 performanceAlertsTool 로직)
|
|
1068
|
-
res.json({
|
|
1069
|
-
message: '성능 알림 조회 완료',
|
|
1070
|
-
alerts: [],
|
|
1071
|
-
timestamp: new Date().toISOString()
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
catch (error) {
|
|
1075
|
-
console.error('❌ 성능 알림 조회 실패:', error);
|
|
1076
|
-
res.status(500).json({
|
|
1077
|
-
error: '성능 알림 조회 실패',
|
|
1078
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
});
|
|
1082
|
-
// 배치 스케줄러 관리 API
|
|
1083
|
-
app.get('/admin/batch/status', async (req, res) => {
|
|
1084
|
-
try {
|
|
1085
|
-
const batchScheduler = getBatchScheduler();
|
|
1086
|
-
const status = batchScheduler.getStatus();
|
|
1087
|
-
res.json({
|
|
1088
|
-
message: '배치 스케줄러 상태 조회 완료',
|
|
1089
|
-
status,
|
|
1090
|
-
timestamp: new Date().toISOString()
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
catch (error) {
|
|
1094
|
-
console.error('❌ 배치 스케줄러 상태 조회 실패:', error);
|
|
1095
|
-
res.status(500).json({
|
|
1096
|
-
error: '배치 스케줄러 상태 조회 실패',
|
|
1097
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
app.post('/admin/batch/run', async (req, res) => {
|
|
1102
|
-
try {
|
|
1103
|
-
const { jobType } = req.body;
|
|
1104
|
-
if (!jobType || !['cleanup', 'monitoring'].includes(jobType)) {
|
|
1105
|
-
return res.status(400).json({
|
|
1106
|
-
error: 'Invalid job type. Must be "cleanup" or "monitoring"'
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
const batchScheduler = getBatchScheduler();
|
|
1110
|
-
const result = await batchScheduler.runJob(jobType);
|
|
1111
|
-
return res.json({
|
|
1112
|
-
message: `배치 작업 ${jobType} 실행 완료`,
|
|
1113
|
-
result,
|
|
1114
|
-
timestamp: new Date().toISOString()
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
catch (error) {
|
|
1118
|
-
console.error('❌ 배치 작업 실행 실패:', error);
|
|
1119
|
-
return res.status(500).json({
|
|
1120
|
-
error: '배치 작업 실행 실패',
|
|
1121
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
|
-
// 성능 모니터링 API
|
|
1126
|
-
app.get('/admin/performance/metrics', async (req, res) => {
|
|
1127
|
-
try {
|
|
1128
|
-
const monitor = getPerformanceMonitor();
|
|
1129
|
-
const metrics = await monitor.collectMetrics();
|
|
1130
|
-
res.json({
|
|
1131
|
-
message: '성능 지표 수집 완료',
|
|
1132
|
-
metrics,
|
|
1133
|
-
timestamp: new Date().toISOString()
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
catch (error) {
|
|
1137
|
-
console.error('❌ 성능 지표 수집 실패:', error);
|
|
1138
|
-
res.status(500).json({
|
|
1139
|
-
error: '성능 지표 수집 실패',
|
|
1140
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
app.get('/admin/performance/alerts', async (req, res) => {
|
|
1145
|
-
try {
|
|
1146
|
-
const monitor = getPerformanceMonitor();
|
|
1147
|
-
const alerts = monitor.getActiveAlerts();
|
|
1148
|
-
res.json({
|
|
1149
|
-
message: '성능 알림 조회 완료',
|
|
1150
|
-
alerts,
|
|
1151
|
-
count: alerts.length,
|
|
1152
|
-
timestamp: new Date().toISOString()
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
catch (error) {
|
|
1156
|
-
console.error('❌ 성능 알림 조회 실패:', error);
|
|
1157
|
-
res.status(500).json({
|
|
1158
|
-
error: '성능 알림 조회 실패',
|
|
1159
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
});
|
|
1163
|
-
app.get('/admin/performance/summary', async (req, res) => {
|
|
1164
|
-
try {
|
|
1165
|
-
const monitor = getPerformanceMonitor();
|
|
1166
|
-
const summary = monitor.getPerformanceSummary();
|
|
1167
|
-
res.json({
|
|
1168
|
-
message: '성능 요약 조회 완료',
|
|
1169
|
-
summary,
|
|
1170
|
-
timestamp: new Date().toISOString()
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
catch (error) {
|
|
1174
|
-
console.error('❌ 성능 요약 조회 실패:', error);
|
|
1175
|
-
res.status(500).json({
|
|
1176
|
-
error: '성능 요약 조회 실패',
|
|
1177
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
app.post('/admin/performance/alerts/:alertId/resolve', async (req, res) => {
|
|
1182
|
-
try {
|
|
1183
|
-
const { alertId } = req.params;
|
|
1184
|
-
const monitor = getPerformanceMonitor();
|
|
1185
|
-
const resolved = monitor.resolveAlert(alertId);
|
|
1186
|
-
if (resolved) {
|
|
1187
|
-
res.json({
|
|
1188
|
-
message: '알림 해결 완료',
|
|
1189
|
-
alertId,
|
|
1190
|
-
timestamp: new Date().toISOString()
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
else {
|
|
1194
|
-
res.status(404).json({
|
|
1195
|
-
error: '알림을 찾을 수 없습니다',
|
|
1196
|
-
alertId
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
catch (error) {
|
|
1201
|
-
console.error('❌ 알림 해결 실패:', error);
|
|
1202
|
-
res.status(500).json({
|
|
1203
|
-
error: '알림 해결 실패',
|
|
1204
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
101
|
+
// Phase 1.2: 기존 엔드포인트는 모두 라우터로 이동됨
|
|
102
|
+
// 주석 처리된 코드는 제거됨 (tools.routes.ts, admin.routes.ts, api.routes.ts, mcp.routes.ts로 이동)
|
|
1208
103
|
// 서버 초기화
|
|
1209
104
|
async function initializeServer() {
|
|
1210
105
|
try {
|
|
@@ -1232,6 +127,22 @@ async function initializeServer() {
|
|
|
1232
127
|
// Vector Search Engine 초기화 (HTTP 서버 전용)
|
|
1233
128
|
vectorSearchEngine = getVectorSearchEngine();
|
|
1234
129
|
vectorSearchEngine.initialize(db);
|
|
130
|
+
// Phase 0: 공통 미들웨어 적용
|
|
131
|
+
// 서비스 주입 미들웨어 (모든 라우터에 적용)
|
|
132
|
+
app.use(createServiceInjector(serverServices, db));
|
|
133
|
+
// Phase 1.2: 라우터 초기화 및 등록
|
|
134
|
+
toolsRouter = createToolsRouter(db, serverServices, anchorMapSubscribers);
|
|
135
|
+
adminRouter = createAdminRouter(db);
|
|
136
|
+
apiRouter = createApiRouter(db, serverServices);
|
|
137
|
+
mcpRouter = createMcpRouter(db, serverServices, transports);
|
|
138
|
+
// 라우터 등록
|
|
139
|
+
// ToolContext 미들웨어는 /tools 라우터에만 적용 (도구 실행 시 필요)
|
|
140
|
+
app.use('/tools', createToolContextMiddleware, toolsRouter);
|
|
141
|
+
app.use('/admin', adminRouter);
|
|
142
|
+
app.use('/api', apiRouter);
|
|
143
|
+
app.use('/', mcpRouter); // /mcp, /messages는 루트에 등록
|
|
144
|
+
// Phase 0: 공통 에러 핸들러 미들웨어 (모든 라우터 이후에 적용)
|
|
145
|
+
app.use(errorHandler);
|
|
1235
146
|
console.log('✅ 서비스 초기화 완료');
|
|
1236
147
|
// 배치 스케줄러 시작 (이미 실행 중이면 먼저 중지)
|
|
1237
148
|
const batchScheduler = getBatchScheduler();
|
|
@@ -1312,8 +223,7 @@ function registerCleanupHandlers() {
|
|
|
1312
223
|
}
|
|
1313
224
|
// WebSocket 서버 설정
|
|
1314
225
|
const wss = new WebSocketServer({ server });
|
|
1315
|
-
//
|
|
1316
|
-
const anchorMapSubscribers = new Map(); // agent_id -> WebSocket Set
|
|
226
|
+
// Phase 1.2: anchorMapSubscribers는 위에서 이미 선언됨
|
|
1317
227
|
wss.on('connection', (ws) => {
|
|
1318
228
|
console.log('🔗 WebSocket 클라이언트 연결됨');
|
|
1319
229
|
ws.on('message', async (data) => {
|