claude-session-continuity-mcp 1.17.0 → 1.17.2

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/dist/index-v3.js DELETED
@@ -1,1024 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Project Manager MCP v3
4
- *
5
- * 18개 도구로 리팩토링된 버전
6
- * - mcp-memory-service 스타일 채택
7
- * - Hook 자동 주입 + 도구 최소화
8
- *
9
- * 카테고리:
10
- * 1. 세션/컨텍스트 (4개): session_start, session_end, session_history, search_sessions
11
- * 2. 프로젝트 관리 (4개): project_status, project_init, project_analyze, list_projects
12
- * 3. 태스크/백로그 (4개): task_add, task_update, task_list, task_suggest
13
- * 4. 솔루션 아카이브 (3개): solution_record, solution_find, solution_suggest
14
- * 5. 검증/품질 (3개): verify_build, verify_test, verify_all
15
- */
16
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
19
- import * as fs from 'fs/promises';
20
- import * as path from 'path';
21
- import { execSync } from 'child_process';
22
- import Database from 'better-sqlite3';
23
- // @ts-ignore - transformers.js
24
- import { pipeline, env } from '@xenova/transformers';
25
- // 모델 캐시 설정
26
- env.cacheDir = path.join(process.env.HOME || '/tmp', '.cache', 'transformers');
27
- env.allowLocalModels = true;
28
- // 기본 경로 설정
29
- const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/Users/ibyeongchang/Documents/dev/ai-service-generator';
30
- const APPS_DIR = path.join(WORKSPACE_ROOT, 'apps');
31
- const DB_PATH = path.join(WORKSPACE_ROOT, '.claude', 'sessions.db');
32
- // ===== SQLite 데이터베이스 초기화 =====
33
- const db = new Database(DB_PATH);
34
- // v3 스키마 - 기존 테이블과 호환 유지
35
- db.exec(`
36
- -- 기존 sessions 테이블 사용 (스키마 변경 없음)
37
- -- last_work = summary
38
- -- current_status = work_done
39
- -- next_tasks = next_steps (JSON array)
40
- -- modified_files = modified_files (JSON array)
41
- -- issues = blockers
42
-
43
- -- 프로젝트 컨텍스트 (고정)
44
- CREATE TABLE IF NOT EXISTS project_context (
45
- project TEXT PRIMARY KEY,
46
- tech_stack TEXT,
47
- architecture_decisions TEXT,
48
- code_patterns TEXT,
49
- special_notes TEXT,
50
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
51
- );
52
-
53
- -- 활성 컨텍스트 (자주 변경)
54
- CREATE TABLE IF NOT EXISTS active_context (
55
- project TEXT PRIMARY KEY,
56
- current_state TEXT,
57
- active_tasks TEXT,
58
- recent_files TEXT,
59
- blockers TEXT,
60
- last_verification TEXT,
61
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
62
- );
63
-
64
- -- 태스크 백로그
65
- CREATE TABLE IF NOT EXISTS tasks (
66
- id INTEGER PRIMARY KEY AUTOINCREMENT,
67
- project TEXT NOT NULL,
68
- title TEXT NOT NULL,
69
- description TEXT,
70
- status TEXT DEFAULT 'pending',
71
- priority INTEGER DEFAULT 5,
72
- related_files TEXT,
73
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
74
- completed_at DATETIME
75
- );
76
- CREATE INDEX IF NOT EXISTS idx_tasks_project_status ON tasks(project, status);
77
-
78
- -- 솔루션 아카이브
79
- CREATE TABLE IF NOT EXISTS solutions (
80
- id INTEGER PRIMARY KEY AUTOINCREMENT,
81
- project TEXT,
82
- error_signature TEXT NOT NULL,
83
- error_message TEXT,
84
- solution TEXT NOT NULL,
85
- related_files TEXT,
86
- keywords TEXT,
87
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
88
- );
89
- CREATE INDEX IF NOT EXISTS idx_solutions_signature ON solutions(error_signature);
90
- CREATE INDEX IF NOT EXISTS idx_solutions_project ON solutions(project);
91
-
92
- -- 임베딩 v3 (시맨틱 검색용) - 기존 embeddings 테이블과 별도
93
- CREATE TABLE IF NOT EXISTS embeddings_v3 (
94
- id INTEGER PRIMARY KEY AUTOINCREMENT,
95
- type TEXT NOT NULL,
96
- ref_id INTEGER NOT NULL,
97
- embedding BLOB NOT NULL,
98
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
99
- UNIQUE(type, ref_id)
100
- );
101
- CREATE INDEX IF NOT EXISTS idx_embeddings_v3_type ON embeddings_v3(type, ref_id);
102
- `);
103
- // ===== 임베딩 엔진 =====
104
- let embeddingPipeline = null;
105
- async function initEmbedding() {
106
- if (embeddingPipeline)
107
- return;
108
- try {
109
- embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
110
- }
111
- catch (error) {
112
- console.error('Failed to load embedding model:', error);
113
- }
114
- }
115
- // 백그라운드 로드
116
- initEmbedding();
117
- async function generateEmbedding(text) {
118
- if (!embeddingPipeline)
119
- await initEmbedding();
120
- if (!embeddingPipeline)
121
- return null;
122
- try {
123
- const output = await embeddingPipeline(text, { pooling: 'mean', normalize: true });
124
- return Array.from(output.data);
125
- }
126
- catch {
127
- return null;
128
- }
129
- }
130
- function cosineSimilarity(a, b) {
131
- if (a.length !== b.length)
132
- return 0;
133
- let dot = 0, normA = 0, normB = 0;
134
- for (let i = 0; i < a.length; i++) {
135
- dot += a[i] * b[i];
136
- normA += a[i] * a[i];
137
- normB += b[i] * b[i];
138
- }
139
- return dot / (Math.sqrt(normA) * Math.sqrt(normB));
140
- }
141
- // ===== 유틸리티 함수 =====
142
- async function fileExists(filePath) {
143
- try {
144
- await fs.access(filePath);
145
- return true;
146
- }
147
- catch {
148
- return false;
149
- }
150
- }
151
- async function detectPlatform(projectPath) {
152
- if (await fileExists(path.join(projectPath, 'pubspec.yaml')))
153
- return 'flutter';
154
- if (await fileExists(path.join(projectPath, 'build.gradle.kts')))
155
- return 'android';
156
- if (await fileExists(path.join(projectPath, 'package.json')))
157
- return 'web';
158
- return 'unknown';
159
- }
160
- async function detectTechStack(projectPath) {
161
- const stack = {};
162
- // Flutter
163
- if (await fileExists(path.join(projectPath, 'pubspec.yaml'))) {
164
- stack.framework = 'Flutter';
165
- const content = await fs.readFile(path.join(projectPath, 'pubspec.yaml'), 'utf-8');
166
- if (content.includes('flutter_riverpod'))
167
- stack.state = 'Riverpod';
168
- if (content.includes('provider:'))
169
- stack.state = 'Provider';
170
- if (content.includes('bloc:'))
171
- stack.state = 'BLoC';
172
- }
173
- // Web (Next.js, etc.)
174
- const pkgPath = path.join(projectPath, 'package.json');
175
- if (await fileExists(pkgPath)) {
176
- const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
177
- if (pkg.dependencies?.next)
178
- stack.framework = 'Next.js';
179
- else if (pkg.dependencies?.react)
180
- stack.framework = 'React';
181
- else if (pkg.dependencies?.vue)
182
- stack.framework = 'Vue';
183
- if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript)
184
- stack.language = 'TypeScript';
185
- }
186
- return stack;
187
- }
188
- function runCommand(cmd, cwd) {
189
- try {
190
- const output = execSync(cmd, { cwd, encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
191
- return { success: true, output };
192
- }
193
- catch (error) {
194
- const e = error;
195
- return { success: false, output: e.stdout || e.stderr || e.message || 'Unknown error' };
196
- }
197
- }
198
- // ===== MCP 서버 =====
199
- const server = new Server({ name: 'project-manager-v3', version: '3.0.0' }, { capabilities: { tools: { listChanged: true } } });
200
- const tools = [
201
- // ===== 1. 세션/컨텍스트 (4개) =====
202
- {
203
- name: 'session_start',
204
- description: '세션 시작 시 프로젝트 컨텍스트를 로드합니다. Hook에서 자동 호출되지만 수동 호출도 가능합니다.',
205
- inputSchema: {
206
- type: 'object',
207
- properties: {
208
- project: { type: 'string', description: '프로젝트 이름' },
209
- compact: { type: 'boolean', description: '간결한 포맷 (기본: true)' }
210
- },
211
- required: ['project']
212
- }
213
- },
214
- {
215
- name: 'session_end',
216
- description: '세션 종료 시 현재 상태를 저장합니다. 다음 세션에서 자동 복구됩니다.',
217
- inputSchema: {
218
- type: 'object',
219
- properties: {
220
- project: { type: 'string', description: '프로젝트 이름' },
221
- summary: { type: 'string', description: '이번 세션 요약 (1-2줄)' },
222
- workDone: { type: 'string', description: '완료한 작업' },
223
- nextSteps: { type: 'array', items: { type: 'string' }, description: '다음 할 일' },
224
- modifiedFiles: { type: 'array', items: { type: 'string' }, description: '수정한 파일' },
225
- blockers: { type: 'string', description: '막힌 것/이슈' }
226
- },
227
- required: ['project', 'summary']
228
- }
229
- },
230
- {
231
- name: 'session_history',
232
- description: '프로젝트의 세션 이력을 조회합니다.',
233
- inputSchema: {
234
- type: 'object',
235
- properties: {
236
- project: { type: 'string', description: '프로젝트 이름' },
237
- limit: { type: 'number', description: '조회 개수 (기본: 5)' },
238
- days: { type: 'number', description: '최근 N일 (기본: 7)' }
239
- },
240
- required: ['project']
241
- }
242
- },
243
- {
244
- name: 'search_sessions',
245
- description: '세션 이력을 시맨틱 검색합니다. "저번에 인증 작업했을 때" 같은 검색에 유용합니다.',
246
- inputSchema: {
247
- type: 'object',
248
- properties: {
249
- query: { type: 'string', description: '검색어' },
250
- project: { type: 'string', description: '프로젝트 (선택)' },
251
- limit: { type: 'number', description: '결과 개수 (기본: 5)' }
252
- },
253
- required: ['query']
254
- }
255
- },
256
- // ===== 2. 프로젝트 관리 (4개) =====
257
- {
258
- name: 'project_status',
259
- description: '프로젝트 진행 현황을 조회합니다. 완성도, 태스크, 최근 변경 등.',
260
- inputSchema: {
261
- type: 'object',
262
- properties: {
263
- project: { type: 'string', description: '프로젝트 이름' }
264
- },
265
- required: ['project']
266
- }
267
- },
268
- {
269
- name: 'project_init',
270
- description: '새 프로젝트를 초기화합니다. 컨텍스트 테이블에 기본 정보를 저장합니다.',
271
- inputSchema: {
272
- type: 'object',
273
- properties: {
274
- project: { type: 'string', description: '프로젝트 이름' },
275
- techStack: { type: 'object', description: '기술 스택 (자동 감지 가능)' },
276
- description: { type: 'string', description: '프로젝트 설명' }
277
- },
278
- required: ['project']
279
- }
280
- },
281
- {
282
- name: 'project_analyze',
283
- description: '프로젝트를 분석하여 기술 스택, 구조 등을 자동 감지합니다.',
284
- inputSchema: {
285
- type: 'object',
286
- properties: {
287
- project: { type: 'string', description: '프로젝트 이름' }
288
- },
289
- required: ['project']
290
- }
291
- },
292
- {
293
- name: 'list_projects',
294
- description: 'apps/ 디렉토리의 모든 프로젝트 목록을 반환합니다.',
295
- inputSchema: {
296
- type: 'object',
297
- properties: {}
298
- }
299
- },
300
- // ===== 3. 태스크/백로그 (4개) =====
301
- {
302
- name: 'task_add',
303
- description: '새 태스크를 추가합니다.',
304
- inputSchema: {
305
- type: 'object',
306
- properties: {
307
- project: { type: 'string', description: '프로젝트 이름' },
308
- title: { type: 'string', description: '태스크 제목' },
309
- description: { type: 'string', description: '상세 설명' },
310
- priority: { type: 'number', description: '우선순위 1-10 (기본: 5)' },
311
- relatedFiles: { type: 'array', items: { type: 'string' }, description: '관련 파일' }
312
- },
313
- required: ['project', 'title']
314
- }
315
- },
316
- {
317
- name: 'task_update',
318
- description: '태스크 상태를 변경합니다.',
319
- inputSchema: {
320
- type: 'object',
321
- properties: {
322
- taskId: { type: 'number', description: '태스크 ID' },
323
- status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: '새 상태' },
324
- note: { type: 'string', description: '메모 (완료 시 결과 등)' }
325
- },
326
- required: ['taskId', 'status']
327
- }
328
- },
329
- {
330
- name: 'task_list',
331
- description: '프로젝트의 태스크 목록을 조회합니다.',
332
- inputSchema: {
333
- type: 'object',
334
- properties: {
335
- project: { type: 'string', description: '프로젝트 이름' },
336
- status: { type: 'string', enum: ['all', 'pending', 'in_progress', 'done', 'blocked'], description: '필터 (기본: pending)' }
337
- },
338
- required: ['project']
339
- }
340
- },
341
- {
342
- name: 'task_suggest',
343
- description: '코드 분석 기반으로 TODO, FIXME 등에서 태스크를 추출하여 제안합니다.',
344
- inputSchema: {
345
- type: 'object',
346
- properties: {
347
- project: { type: 'string', description: '프로젝트 이름' },
348
- path: { type: 'string', description: '특정 경로만 분석 (선택)' }
349
- },
350
- required: ['project']
351
- }
352
- },
353
- // ===== 4. 솔루션 아카이브 (3개) =====
354
- {
355
- name: 'solution_record',
356
- description: '에러 해결 방법을 기록합니다. 나중에 같은 에러 발생 시 자동 검색됩니다.',
357
- inputSchema: {
358
- type: 'object',
359
- properties: {
360
- project: { type: 'string', description: '프로젝트 이름' },
361
- errorSignature: { type: 'string', description: '에러 패턴/시그니처 (검색 키)' },
362
- errorMessage: { type: 'string', description: '전체 에러 메시지' },
363
- solution: { type: 'string', description: '해결 방법' },
364
- relatedFiles: { type: 'array', items: { type: 'string' }, description: '관련 파일' }
365
- },
366
- required: ['errorSignature', 'solution']
367
- }
368
- },
369
- {
370
- name: 'solution_find',
371
- description: '유사한 에러의 해결 방법을 검색합니다.',
372
- inputSchema: {
373
- type: 'object',
374
- properties: {
375
- query: { type: 'string', description: '에러 메시지 또는 키워드' },
376
- project: { type: 'string', description: '프로젝트 (선택)' },
377
- limit: { type: 'number', description: '결과 개수 (기본: 3)' }
378
- },
379
- required: ['query']
380
- }
381
- },
382
- {
383
- name: 'solution_suggest',
384
- description: '과거 솔루션 기반으로 현재 에러에 대한 해결책을 AI가 제안합니다.',
385
- inputSchema: {
386
- type: 'object',
387
- properties: {
388
- errorMessage: { type: 'string', description: '현재 에러 메시지' },
389
- project: { type: 'string', description: '프로젝트' }
390
- },
391
- required: ['errorMessage']
392
- }
393
- },
394
- // ===== 5. 검증/품질 (3개) =====
395
- {
396
- name: 'verify_build',
397
- description: '프로젝트 빌드를 실행합니다.',
398
- inputSchema: {
399
- type: 'object',
400
- properties: {
401
- project: { type: 'string', description: '프로젝트 이름' }
402
- },
403
- required: ['project']
404
- }
405
- },
406
- {
407
- name: 'verify_test',
408
- description: '프로젝트 테스트를 실행합니다.',
409
- inputSchema: {
410
- type: 'object',
411
- properties: {
412
- project: { type: 'string', description: '프로젝트 이름' },
413
- testPath: { type: 'string', description: '특정 테스트 파일/폴더 (선택)' }
414
- },
415
- required: ['project']
416
- }
417
- },
418
- {
419
- name: 'verify_all',
420
- description: '빌드 + 테스트 + 린트를 한 번에 실행합니다.',
421
- inputSchema: {
422
- type: 'object',
423
- properties: {
424
- project: { type: 'string', description: '프로젝트 이름' },
425
- stopOnFail: { type: 'boolean', description: '실패 시 중단 (기본: false)' }
426
- },
427
- required: ['project']
428
- }
429
- }
430
- ];
431
- // ===== 도구 핸들러 =====
432
- async function handleTool(name, args) {
433
- try {
434
- switch (name) {
435
- // ===== 세션/컨텍스트 =====
436
- case 'session_start': {
437
- const project = args.project;
438
- const compact = args.compact !== false;
439
- const projectPath = path.join(APPS_DIR, project);
440
- if (!await fileExists(projectPath)) {
441
- return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
442
- }
443
- // 고정 컨텍스트
444
- const fixedRow = db.prepare('SELECT * FROM project_context WHERE project = ?').get(project);
445
- // 활성 컨텍스트
446
- const activeRow = db.prepare('SELECT * FROM active_context WHERE project = ?').get(project);
447
- // 최근 세션
448
- const lastSession = db.prepare('SELECT * FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
449
- // 미완료 태스크
450
- const pendingTasks = db.prepare(`
451
- SELECT id, title, status, priority FROM tasks
452
- WHERE project = ? AND status IN ('pending', 'in_progress')
453
- ORDER BY priority DESC LIMIT 5
454
- `).all(project);
455
- if (compact) {
456
- const lines = [`# ${project} Context`];
457
- if (fixedRow?.tech_stack) {
458
- const stack = JSON.parse(fixedRow.tech_stack);
459
- lines.push(`**Stack**: ${Object.entries(stack).map(([k, v]) => `${k}: ${v}`).join(', ')}`);
460
- }
461
- if (activeRow?.current_state) {
462
- lines.push(`**State**: ${activeRow.current_state}`);
463
- }
464
- if (lastSession?.last_work) {
465
- lines.push(`**Last**: ${lastSession.last_work}`);
466
- }
467
- if (pendingTasks.length > 0) {
468
- lines.push(`**Tasks**: ${pendingTasks.map(t => `[P${t.priority}] ${t.title}`).join(' | ')}`);
469
- }
470
- if (activeRow?.blockers) {
471
- lines.push(`**Blocker**: ${activeRow.blockers}`);
472
- }
473
- return { content: [{ type: 'text', text: lines.join('\n') }] };
474
- }
475
- return {
476
- content: [{
477
- type: 'text',
478
- text: JSON.stringify({
479
- project,
480
- fixed: fixedRow ? {
481
- techStack: fixedRow.tech_stack ? JSON.parse(fixedRow.tech_stack) : {},
482
- architectureDecisions: fixedRow.architecture_decisions ? JSON.parse(fixedRow.architecture_decisions) : [],
483
- codePatterns: fixedRow.code_patterns ? JSON.parse(fixedRow.code_patterns) : []
484
- } : null,
485
- active: activeRow ? {
486
- currentState: activeRow.current_state,
487
- activeTasks: activeRow.active_tasks ? JSON.parse(activeRow.active_tasks) : [],
488
- recentFiles: activeRow.recent_files ? JSON.parse(activeRow.recent_files) : [],
489
- blockers: activeRow.blockers,
490
- lastVerification: activeRow.last_verification
491
- } : null,
492
- lastSession: lastSession ? {
493
- summary: lastSession.last_work,
494
- workDone: lastSession.current_status,
495
- nextSteps: lastSession.next_tasks ? JSON.parse(lastSession.next_tasks) : [],
496
- timestamp: lastSession.timestamp
497
- } : null,
498
- pendingTasks
499
- }, null, 2)
500
- }]
501
- };
502
- }
503
- case 'session_end': {
504
- const project = args.project;
505
- const summary = args.summary;
506
- const workDone = args.workDone;
507
- const nextSteps = args.nextSteps;
508
- const modifiedFiles = args.modifiedFiles;
509
- const blockers = args.blockers;
510
- // 세션 저장 (기존 스키마 호환)
511
- // last_work = summary, current_status = workDone, issues = blockers
512
- db.prepare(`
513
- INSERT INTO sessions (project, last_work, current_status, next_tasks, modified_files, issues)
514
- VALUES (?, ?, ?, ?, ?, ?)
515
- `).run(project, summary, workDone || null, nextSteps ? JSON.stringify(nextSteps) : null, modifiedFiles ? JSON.stringify(modifiedFiles) : null, blockers || null);
516
- // 활성 컨텍스트 업데이트
517
- db.prepare(`
518
- INSERT OR REPLACE INTO active_context (project, current_state, recent_files, blockers, updated_at)
519
- VALUES (?, ?, ?, ?, datetime('now'))
520
- `).run(project, summary, modifiedFiles ? JSON.stringify(modifiedFiles) : null, blockers || null);
521
- return { content: [{ type: 'text', text: `✅ Session saved for ${project}` }] };
522
- }
523
- case 'session_history': {
524
- const project = args.project;
525
- const limit = args.limit || 5;
526
- const days = args.days || 7;
527
- const sessions = db.prepare(`
528
- SELECT * FROM sessions
529
- WHERE project = ? AND timestamp > datetime('now', '-${days} days')
530
- ORDER BY timestamp DESC LIMIT ?
531
- `).all(project, limit);
532
- return {
533
- content: [{
534
- type: 'text',
535
- text: JSON.stringify(sessions.map(s => ({
536
- id: s.id,
537
- summary: s.last_work,
538
- workDone: s.current_status,
539
- nextSteps: s.next_tasks ? JSON.parse(s.next_tasks) : [],
540
- timestamp: s.timestamp
541
- })), null, 2)
542
- }]
543
- };
544
- }
545
- case 'search_sessions': {
546
- const query = args.query;
547
- const project = args.project;
548
- const limit = args.limit || 5;
549
- // 시맨틱 검색 (임베딩 사용)
550
- const queryEmbedding = await generateEmbedding(query);
551
- if (!queryEmbedding) {
552
- // 폴백: 키워드 검색 (기존 스키마: last_work, current_status)
553
- const sql = project
554
- ? 'SELECT * FROM sessions WHERE project = ? AND (last_work LIKE ? OR current_status LIKE ?) ORDER BY timestamp DESC LIMIT ?'
555
- : 'SELECT * FROM sessions WHERE last_work LIKE ? OR current_status LIKE ? ORDER BY timestamp DESC LIMIT ?';
556
- const params = project
557
- ? [project, `%${query}%`, `%${query}%`, limit]
558
- : [`%${query}%`, `%${query}%`, limit];
559
- const results = db.prepare(sql).all(...params);
560
- return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
561
- }
562
- // 모든 세션 가져와서 유사도 계산
563
- const allSessions = db.prepare(project
564
- ? 'SELECT * FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 100'
565
- : 'SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 100').all(project ? [project] : []);
566
- const scored = await Promise.all(allSessions.map(async (s) => {
567
- const text = `${s.last_work} ${s.current_status || ''}`;
568
- const emb = await generateEmbedding(text);
569
- const similarity = emb ? cosineSimilarity(queryEmbedding, emb) : 0;
570
- return { ...s, similarity };
571
- }));
572
- scored.sort((a, b) => b.similarity - a.similarity);
573
- const top = scored.slice(0, limit);
574
- return {
575
- content: [{
576
- type: 'text',
577
- text: JSON.stringify(top.map(s => ({
578
- id: s.id,
579
- project: s.project,
580
- summary: s.last_work,
581
- similarity: Math.round(s.similarity * 100) + '%',
582
- timestamp: s.timestamp
583
- })), null, 2)
584
- }]
585
- };
586
- }
587
- // ===== 프로젝트 관리 =====
588
- case 'project_status': {
589
- const project = args.project;
590
- const projectPath = path.join(APPS_DIR, project);
591
- if (!await fileExists(projectPath)) {
592
- return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
593
- }
594
- // 태스크 통계
595
- const taskStats = db.prepare(`
596
- SELECT status, COUNT(*) as count FROM tasks WHERE project = ? GROUP BY status
597
- `).all(project);
598
- // 최근 세션
599
- const recentSessions = db.prepare(`
600
- SELECT last_work as summary, timestamp FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 3
601
- `).all(project);
602
- // 활성 컨텍스트
603
- const active = db.prepare('SELECT * FROM active_context WHERE project = ?').get(project);
604
- // 진행도 계산
605
- const done = taskStats.find(t => t.status === 'done')?.count || 0;
606
- const total = taskStats.reduce((sum, t) => sum + t.count, 0);
607
- const progress = total > 0 ? Math.round((done / total) * 100) : 0;
608
- return {
609
- content: [{
610
- type: 'text',
611
- text: JSON.stringify({
612
- project,
613
- progress: `${progress}%`,
614
- tasks: {
615
- done,
616
- inProgress: taskStats.find(t => t.status === 'in_progress')?.count || 0,
617
- pending: taskStats.find(t => t.status === 'pending')?.count || 0,
618
- blocked: taskStats.find(t => t.status === 'blocked')?.count || 0,
619
- total
620
- },
621
- currentState: active?.current_state || 'No active context',
622
- lastVerification: active?.last_verification || 'N/A',
623
- recentActivity: recentSessions.map(s => ({
624
- summary: s.summary,
625
- date: s.timestamp
626
- }))
627
- }, null, 2)
628
- }]
629
- };
630
- }
631
- case 'project_init': {
632
- const project = args.project;
633
- const techStack = args.techStack;
634
- const description = args.description;
635
- const projectPath = path.join(APPS_DIR, project);
636
- // 기술 스택 자동 감지
637
- const detectedStack = await detectTechStack(projectPath);
638
- const finalStack = { ...detectedStack, ...techStack };
639
- db.prepare(`
640
- INSERT OR REPLACE INTO project_context (project, tech_stack, special_notes, updated_at)
641
- VALUES (?, ?, ?, datetime('now'))
642
- `).run(project, JSON.stringify(finalStack), description || null);
643
- db.prepare(`
644
- INSERT OR REPLACE INTO active_context (project, current_state, updated_at)
645
- VALUES (?, 'Project initialized', datetime('now'))
646
- `).run(project);
647
- return {
648
- content: [{
649
- type: 'text',
650
- text: `✅ Project "${project}" initialized\nTech Stack: ${JSON.stringify(finalStack)}`
651
- }]
652
- };
653
- }
654
- case 'project_analyze': {
655
- const project = args.project;
656
- const projectPath = path.join(APPS_DIR, project);
657
- if (!await fileExists(projectPath)) {
658
- return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
659
- }
660
- const platform = await detectPlatform(projectPath);
661
- const techStack = await detectTechStack(projectPath);
662
- // 파일 구조 분석
663
- const structure = [];
664
- try {
665
- const entries = await fs.readdir(projectPath, { withFileTypes: true });
666
- for (const entry of entries) {
667
- if (entry.name.startsWith('.'))
668
- continue;
669
- structure.push(entry.isDirectory() ? `📁 ${entry.name}/` : `📄 ${entry.name}`);
670
- }
671
- }
672
- catch { }
673
- return {
674
- content: [{
675
- type: 'text',
676
- text: JSON.stringify({
677
- project,
678
- platform,
679
- techStack,
680
- structure: structure.slice(0, 20)
681
- }, null, 2)
682
- }]
683
- };
684
- }
685
- case 'list_projects': {
686
- try {
687
- const entries = await fs.readdir(APPS_DIR, { withFileTypes: true });
688
- const projects = entries
689
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
690
- .map(e => e.name);
691
- // 각 프로젝트 상태 조회
692
- const projectsWithStatus = await Promise.all(projects.map(async (p) => {
693
- const active = db.prepare('SELECT current_state FROM active_context WHERE project = ?').get(p);
694
- const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project = ? AND status != ?').get(p, 'done');
695
- return {
696
- name: p,
697
- status: active?.current_state || 'No context',
698
- pendingTasks: taskCount?.count || 0
699
- };
700
- }));
701
- return { content: [{ type: 'text', text: JSON.stringify(projectsWithStatus, null, 2) }] };
702
- }
703
- catch (error) {
704
- return { content: [{ type: 'text', text: `Failed to list projects: ${error instanceof Error ? error.message : String(error)}` }] };
705
- }
706
- }
707
- // ===== 태스크/백로그 =====
708
- case 'task_add': {
709
- const project = args.project;
710
- const title = args.title;
711
- const description = args.description;
712
- const priority = args.priority || 5;
713
- const relatedFiles = args.relatedFiles;
714
- const result = db.prepare(`
715
- INSERT INTO tasks (project, title, description, priority, related_files)
716
- VALUES (?, ?, ?, ?, ?)
717
- `).run(project, title, description || null, priority, relatedFiles ? JSON.stringify(relatedFiles) : null);
718
- return {
719
- content: [{
720
- type: 'text',
721
- text: `✅ Task added (ID: ${result.lastInsertRowid})\n[P${priority}] ${title}`
722
- }]
723
- };
724
- }
725
- case 'task_update': {
726
- const taskId = args.taskId;
727
- const status = args.status;
728
- const note = args.note;
729
- const completedAt = status === 'done' ? "datetime('now')" : 'NULL';
730
- db.prepare(`
731
- UPDATE tasks SET status = ?, completed_at = ${status === 'done' ? "datetime('now')" : 'NULL'}
732
- WHERE id = ?
733
- `).run(status, taskId);
734
- const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
735
- return {
736
- content: [{
737
- type: 'text',
738
- text: `✅ Task #${taskId} → ${status}${note ? `\nNote: ${note}` : ''}\n${task?.title || ''}`
739
- }]
740
- };
741
- }
742
- case 'task_list': {
743
- const project = args.project;
744
- const status = args.status || 'pending';
745
- const sql = status === 'all'
746
- ? 'SELECT * FROM tasks WHERE project = ? ORDER BY priority DESC, created_at DESC'
747
- : 'SELECT * FROM tasks WHERE project = ? AND status = ? ORDER BY priority DESC, created_at DESC';
748
- const tasks = status === 'all'
749
- ? db.prepare(sql).all(project)
750
- : db.prepare(sql).all(project, status);
751
- return { content: [{ type: 'text', text: JSON.stringify(tasks, null, 2) }] };
752
- }
753
- case 'task_suggest': {
754
- const project = args.project;
755
- const searchPath = args.path;
756
- const projectPath = path.join(APPS_DIR, project, searchPath || '');
757
- // TODO, FIXME 등 검색
758
- try {
759
- const result = runCommand(`grep -rn "TODO\\|FIXME\\|HACK\\|XXX" --include="*.ts" --include="*.tsx" --include="*.dart" --include="*.kt" . | head -20`, projectPath);
760
- if (!result.success || !result.output.trim()) {
761
- return { content: [{ type: 'text', text: 'No TODO/FIXME comments found' }] };
762
- }
763
- const lines = result.output.trim().split('\n');
764
- const suggestions = lines.map(line => {
765
- const match = line.match(/^(.+?):(\d+):(.+)$/);
766
- if (match) {
767
- return {
768
- file: match[1],
769
- line: parseInt(match[2]),
770
- comment: match[3].trim()
771
- };
772
- }
773
- return { comment: line };
774
- });
775
- return {
776
- content: [{
777
- type: 'text',
778
- text: `Found ${suggestions.length} potential tasks:\n\n${JSON.stringify(suggestions, null, 2)}`
779
- }]
780
- };
781
- }
782
- catch {
783
- return { content: [{ type: 'text', text: 'Failed to search for tasks' }] };
784
- }
785
- }
786
- // ===== 솔루션 아카이브 =====
787
- case 'solution_record': {
788
- const project = args.project;
789
- const errorSignature = args.errorSignature;
790
- const errorMessage = args.errorMessage;
791
- const solution = args.solution;
792
- const relatedFiles = args.relatedFiles;
793
- // 키워드 자동 추출
794
- const keywords = errorSignature.toLowerCase()
795
- .replace(/[^a-z0-9\s]/g, ' ')
796
- .split(/\s+/)
797
- .filter(w => w.length > 2)
798
- .slice(0, 10)
799
- .join(',');
800
- const result = db.prepare(`
801
- INSERT INTO solutions (project, error_signature, error_message, solution, related_files, keywords)
802
- VALUES (?, ?, ?, ?, ?, ?)
803
- `).run(project || null, errorSignature, errorMessage || null, solution, relatedFiles ? JSON.stringify(relatedFiles) : null, keywords);
804
- // 임베딩 저장 (시맨틱 검색용)
805
- const embedding = await generateEmbedding(`${errorSignature} ${errorMessage || ''} ${solution}`);
806
- if (embedding) {
807
- const buffer = Buffer.from(new Float32Array(embedding).buffer);
808
- db.prepare('INSERT OR REPLACE INTO embeddings_v3 (type, ref_id, embedding) VALUES (?, ?, ?)').run('solution', result.lastInsertRowid, buffer);
809
- }
810
- return {
811
- content: [{
812
- type: 'text',
813
- text: `✅ Solution recorded (ID: ${result.lastInsertRowid})\nSignature: ${errorSignature}`
814
- }]
815
- };
816
- }
817
- case 'solution_find': {
818
- const query = args.query;
819
- const project = args.project;
820
- const limit = args.limit || 3;
821
- // 키워드 검색 먼저
822
- const keywordResults = db.prepare(`
823
- SELECT * FROM solutions
824
- WHERE error_signature LIKE ? OR error_message LIKE ? OR keywords LIKE ?
825
- ${project ? 'AND project = ?' : ''}
826
- ORDER BY created_at DESC LIMIT ?
827
- `).all(`%${query}%`, `%${query}%`, `%${query}%`, ...(project ? [project, limit] : [limit]));
828
- if (keywordResults.length > 0) {
829
- return {
830
- content: [{
831
- type: 'text',
832
- text: JSON.stringify(keywordResults.map(r => ({
833
- id: r.id,
834
- errorSignature: r.error_signature,
835
- solution: r.solution,
836
- project: r.project,
837
- created: r.created_at
838
- })), null, 2)
839
- }]
840
- };
841
- }
842
- // 시맨틱 검색 폴백
843
- const queryEmb = await generateEmbedding(query);
844
- if (!queryEmb) {
845
- return { content: [{ type: 'text', text: 'No solutions found' }] };
846
- }
847
- const allSolutions = db.prepare(`
848
- SELECT s.*, e.embedding FROM solutions s
849
- LEFT JOIN embeddings_v3 e ON e.type = 'solution' AND e.ref_id = s.id
850
- ${project ? 'WHERE s.project = ?' : ''}
851
- LIMIT 50
852
- `).all(project ? [project] : []);
853
- const scored = allSolutions.map(s => {
854
- if (!s.embedding)
855
- return { ...s, similarity: 0 };
856
- const emb = Array.from(new Float32Array(s.embedding.buffer));
857
- return { ...s, similarity: cosineSimilarity(queryEmb, emb) };
858
- });
859
- scored.sort((a, b) => b.similarity - a.similarity);
860
- const topResults = scored.slice(0, limit);
861
- return {
862
- content: [{
863
- type: 'text',
864
- text: JSON.stringify(topResults.map(r => ({
865
- id: r.id,
866
- errorSignature: r.error_signature,
867
- solution: r.solution,
868
- similarity: Math.round(r.similarity * 100) + '%'
869
- })), null, 2)
870
- }]
871
- };
872
- }
873
- case 'solution_suggest': {
874
- const errorMessage = args.errorMessage;
875
- const project = args.project;
876
- // solution_find 결과를 기반으로 제안
877
- const similar = db.prepare(`
878
- SELECT * FROM solutions
879
- WHERE error_signature LIKE ? OR error_message LIKE ?
880
- ${project ? 'AND project = ?' : ''}
881
- ORDER BY created_at DESC LIMIT 3
882
- `).all(`%${errorMessage.substring(0, 50)}%`, `%${errorMessage.substring(0, 50)}%`, ...(project ? [project] : []));
883
- if (similar.length === 0) {
884
- return { content: [{ type: 'text', text: 'No similar solutions found. This might be a new error.' }] };
885
- }
886
- const suggestions = similar.map((s, i) => `
887
- ### Solution ${i + 1} (from: ${s.project || 'global'})
888
- **Error**: ${s.error_signature}
889
- **Solution**: ${s.solution}
890
- `).join('\n');
891
- return {
892
- content: [{
893
- type: 'text',
894
- text: `Found ${similar.length} similar solutions:\n${suggestions}`
895
- }]
896
- };
897
- }
898
- // ===== 검증/품질 =====
899
- case 'verify_build': {
900
- const project = args.project;
901
- const projectPath = path.join(APPS_DIR, project);
902
- const platform = await detectPlatform(projectPath);
903
- let cmd;
904
- switch (platform) {
905
- case 'flutter':
906
- cmd = 'flutter build apk --debug';
907
- break;
908
- case 'android':
909
- cmd = './gradlew assembleDebug';
910
- break;
911
- case 'web':
912
- cmd = 'pnpm build';
913
- break;
914
- default:
915
- return { content: [{ type: 'text', text: `Unknown platform: ${platform}` }] };
916
- }
917
- const result = runCommand(cmd, projectPath);
918
- // 결과 저장
919
- db.prepare(`
920
- INSERT OR REPLACE INTO active_context (project, last_verification, updated_at)
921
- VALUES (?, ?, datetime('now'))
922
- `).run(project, result.success ? 'build:passed' : 'build:failed');
923
- return {
924
- content: [{
925
- type: 'text',
926
- text: `${result.success ? '✅' : '❌'} Build ${result.success ? 'passed' : 'failed'}\n\n${result.output.slice(-1000)}`
927
- }]
928
- };
929
- }
930
- case 'verify_test': {
931
- const project = args.project;
932
- const testPath = args.testPath;
933
- const projectPath = path.join(APPS_DIR, project);
934
- const platform = await detectPlatform(projectPath);
935
- let cmd;
936
- switch (platform) {
937
- case 'flutter':
938
- cmd = testPath ? `flutter test ${testPath}` : 'flutter test';
939
- break;
940
- case 'web':
941
- cmd = testPath ? `pnpm test ${testPath}` : 'pnpm test';
942
- break;
943
- default:
944
- return { content: [{ type: 'text', text: `Tests not configured for platform: ${platform}` }] };
945
- }
946
- const result = runCommand(cmd, projectPath);
947
- return {
948
- content: [{
949
- type: 'text',
950
- text: `${result.success ? '✅' : '❌'} Tests ${result.success ? 'passed' : 'failed'}\n\n${result.output.slice(-1000)}`
951
- }]
952
- };
953
- }
954
- case 'verify_all': {
955
- const project = args.project;
956
- const stopOnFail = args.stopOnFail === true;
957
- const projectPath = path.join(APPS_DIR, project);
958
- const platform = await detectPlatform(projectPath);
959
- const results = [];
960
- // Build
961
- let buildCmd;
962
- let testCmd;
963
- let lintCmd;
964
- switch (platform) {
965
- case 'flutter':
966
- buildCmd = 'flutter build apk --debug';
967
- testCmd = 'flutter test';
968
- lintCmd = 'flutter analyze';
969
- break;
970
- case 'web':
971
- buildCmd = 'pnpm build';
972
- testCmd = 'pnpm test';
973
- lintCmd = 'pnpm lint';
974
- break;
975
- default:
976
- return { content: [{ type: 'text', text: `Unknown platform: ${platform}` }] };
977
- }
978
- // Execute each step
979
- for (const [name, cmd] of [['build', buildCmd], ['test', testCmd], ['lint', lintCmd]]) {
980
- const result = runCommand(cmd, projectPath);
981
- results.push({ step: name, success: result.success, output: result.output.slice(-500) });
982
- if (!result.success && stopOnFail)
983
- break;
984
- }
985
- const allPassed = results.every(r => r.success);
986
- // 결과 저장
987
- db.prepare(`
988
- INSERT OR REPLACE INTO active_context (project, last_verification, updated_at)
989
- VALUES (?, ?, datetime('now'))
990
- `).run(project, allPassed ? 'all:passed' : 'all:failed');
991
- const summary = results.map(r => `${r.success ? '✅' : '❌'} ${r.step}: ${r.success ? 'passed' : 'failed'}`).join('\n');
992
- return {
993
- content: [{
994
- type: 'text',
995
- text: `## Verification Results\n\n${summary}\n\n### Details\n${results.map(r => `**${r.step}**:\n${r.output}`).join('\n\n')}`
996
- }]
997
- };
998
- }
999
- default:
1000
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
1001
- }
1002
- }
1003
- catch (error) {
1004
- return {
1005
- content: [{
1006
- type: 'text',
1007
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
1008
- }],
1009
- isError: true
1010
- };
1011
- }
1012
- }
1013
- // ===== MCP 요청 핸들러 =====
1014
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
1015
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1016
- return handleTool(request.params.name, request.params.arguments || {});
1017
- });
1018
- // ===== 서버 시작 =====
1019
- async function main() {
1020
- const transport = new StdioServerTransport();
1021
- await server.connect(transport);
1022
- console.error('Project Manager MCP v3 started');
1023
- }
1024
- main().catch(console.error);