ltcai 0.1.25 → 0.1.26

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 CHANGED
@@ -294,7 +294,7 @@ Or: `./start_ai.sh` (auto-restart + caffeinate)
294
294
  | VS Code Marketplace | [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=parktaesoo.ltcai) |
295
295
  | Open VSX | [open-vsx.org](https://open-vsx.org/extension/parktaesoo/ltcai) |
296
296
 
297
- Current version: **0.1.25** — [Changelog](docs/CHANGELOG.md)
297
+ Current version: **0.1.26** — [Changelog](docs/CHANGELOG.md)
298
298
 
299
299
  ---
300
300
 
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.26] - 2026-05-24
4
+
5
+ ### MCP 관리 대폭 확장 — 3-탭 UI
6
+
7
+ **새 기능**
8
+
9
+ - **레지스트리 탭** — 기존 MCP 목록 (빌트인 + 원격 레지스트리)
10
+ - 인기 MCP 20개 추가: `mcp-postgres`, `mcp-sqlite`, `mcp-brave-search`, `mcp-tavily`, `mcp-puppeteer`, `mcp-vercel`, `mcp-cloudflare`, `mcp-docker`, `mcp-stripe`, `mcp-supabase`, `mcp-hubspot`, `mcp-memory`, `mcp-sequential-thinking`, `mcp-discord`, `mcp-telegram`, `mcp-everything` 등
11
+ - 각 항목에 `env_vars` 필드 (설치 시 필요한 환경변수 안내)
12
+
13
+ - **Claude Code 탭** — `~/.claude/settings.json` mcpServers 자동 동기화
14
+ - Claude Code에서 설치한 MCP 목록을 Lattice AI UI에서 바로 확인
15
+ - 이름·패키지·환경변수 정보 표시, "Claude Code" 소스 배지
16
+
17
+ - **직접 추가 탭** — 커스텀 MCP 폼
18
+ - 이름·패키지·설명·환경변수·아이콘 직접 입력
19
+ - 추가된 항목은 `~/.ltcai/custom_mcps.json`에 저장 (서버 재시작 후에도 유지)
20
+ - 삭제 버튼 (어드민 전용)
21
+
22
+ **API 엔드포인트**
23
+ - `GET /mcp/claude-code-servers` — Claude Code settings.json mcpServers 반환
24
+ - `GET /mcp/custom` — 사용자 추가 커스텀 MCP 목록
25
+ - `POST /mcp/custom` — 커스텀 MCP 추가
26
+ - `DELETE /mcp/custom/{id}` — 커스텀 MCP 삭제 (어드민)
27
+
28
+ ---
29
+
3
30
  ## [0.1.25] - 2026-05-24
4
31
 
5
32
  ### Knowledge Graph 전면 재설계 — 점=명사, 선=동사
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Lattice AI local MLX/cloud LLM workspace server",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
package/server.py CHANGED
@@ -387,6 +387,14 @@ class McpRecommendRequest(BaseModel):
387
387
  class McpInstallRequest(BaseModel):
388
388
  mcp_id: str
389
389
 
390
+ class McpCustomRequest(BaseModel):
391
+ name: str
392
+ package: str
393
+ description: str = ""
394
+ category: str = "custom"
395
+ icon: str = "🔌"
396
+ env_vars: List[Dict] = []
397
+
390
398
  class SkillInstallRequest(BaseModel):
391
399
  plugin: str
392
400
  skill: str
@@ -673,6 +681,187 @@ MCP_REGISTRY = [
673
681
  "keywords": ["canva", "design", "poster", "card", "캔바", "디자인"],
674
682
  "capabilities": ["디자인 템플릿", "이미지 제작 워크플로"],
675
683
  },
684
+ # ── 데이터베이스 ─────────────────────────────────────────────────────────
685
+ {
686
+ "id": "mcp-postgres",
687
+ "name": "PostgreSQL MCP",
688
+ "category": "Database",
689
+ "install_mode": "npm",
690
+ "package": "@modelcontextprotocol/server-postgres",
691
+ "description": "PostgreSQL 데이터베이스에 연결해 쿼리 실행, 스키마 탐색, 데이터 분석을 수행합니다.",
692
+ "keywords": ["postgres", "postgresql", "database", "sql", "db", "데이터베이스", "쿼리"],
693
+ "capabilities": ["SQL 쿼리 실행", "스키마 탐색", "테이블 분석"],
694
+ "env_vars": [{"name": "POSTGRES_CONNECTION_STRING", "description": "postgresql://user:pass@host:5432/db"}],
695
+ },
696
+ {
697
+ "id": "mcp-sqlite",
698
+ "name": "SQLite MCP",
699
+ "category": "Database",
700
+ "install_mode": "npm",
701
+ "package": "@modelcontextprotocol/server-sqlite",
702
+ "description": "로컬 SQLite 파일에 쿼리를 실행하고 데이터를 탐색합니다.",
703
+ "keywords": ["sqlite", "database", "sql", "local", "로컬", "데이터베이스"],
704
+ "capabilities": ["SQLite 쿼리", "테이블 탐색", "데이터 집계"],
705
+ "env_vars": [{"name": "SQLITE_DB_PATH", "description": "/path/to/database.db"}],
706
+ },
707
+ # ── 검색 / 웹 ────────────────────────────────────────────────────────────
708
+ {
709
+ "id": "mcp-brave-search",
710
+ "name": "Brave Search MCP",
711
+ "category": "Search / web",
712
+ "install_mode": "npm",
713
+ "package": "@modelcontextprotocol/server-brave-search",
714
+ "description": "Brave Search API로 실시간 웹 검색 결과를 가져옵니다.",
715
+ "keywords": ["search", "web", "brave", "websearch", "검색", "웹검색"],
716
+ "capabilities": ["실시간 웹 검색", "뉴스 검색", "이미지 검색"],
717
+ "env_vars": [{"name": "BRAVE_API_KEY", "description": "Brave Search API 키 (search.brave.com)"}],
718
+ },
719
+ {
720
+ "id": "mcp-tavily",
721
+ "name": "Tavily Search MCP",
722
+ "category": "Search / web",
723
+ "install_mode": "npm",
724
+ "package": "tavily-mcp",
725
+ "description": "AI 최적화 웹 검색 엔진 Tavily로 고품질 검색 결과를 가져옵니다.",
726
+ "keywords": ["search", "tavily", "ai search", "검색", "AI검색"],
727
+ "capabilities": ["AI 최적화 검색", "요약 검색 결과"],
728
+ "env_vars": [{"name": "TAVILY_API_KEY", "description": "app.tavily.com에서 발급"}],
729
+ },
730
+ {
731
+ "id": "mcp-puppeteer",
732
+ "name": "Puppeteer MCP",
733
+ "category": "Browser automation",
734
+ "install_mode": "npm",
735
+ "package": "@modelcontextprotocol/server-puppeteer",
736
+ "description": "Puppeteer로 브라우저를 제어하고 웹 스크래핑, 스크린샷, 자동화를 수행합니다.",
737
+ "keywords": ["puppeteer", "browser", "scraping", "screenshot", "automation", "스크래핑", "자동화"],
738
+ "capabilities": ["웹 스크래핑", "스크린샷", "폼 자동화", "클릭/입력"],
739
+ },
740
+ # ── 배포 / 인프라 ─────────────────────────────────────────────────────────
741
+ {
742
+ "id": "mcp-vercel",
743
+ "name": "Vercel MCP",
744
+ "category": "Deployment",
745
+ "install_mode": "npm",
746
+ "package": "@vercel/mcp-adapter",
747
+ "description": "Vercel 프로젝트 배포 상태 확인, 로그 조회, 환경 변수 관리를 수행합니다.",
748
+ "keywords": ["vercel", "deploy", "deployment", "serverless", "배포", "버셀"],
749
+ "capabilities": ["배포 상태 확인", "로그 조회", "환경 변수 관리"],
750
+ "env_vars": [{"name": "VERCEL_API_TOKEN", "description": "Vercel 계정 토큰"}],
751
+ },
752
+ {
753
+ "id": "mcp-cloudflare",
754
+ "name": "Cloudflare MCP",
755
+ "category": "Deployment / CDN",
756
+ "install_mode": "npm",
757
+ "package": "@cloudflare/mcp-server-cloudflare",
758
+ "description": "Cloudflare Workers, KV, R2, D1 등 Cloudflare 서비스를 관리합니다.",
759
+ "keywords": ["cloudflare", "workers", "cdn", "kv", "r2", "클라우드플레어"],
760
+ "capabilities": ["Workers 배포", "KV/R2 관리", "DNS 조회", "D1 쿼리"],
761
+ "env_vars": [{"name": "CLOUDFLARE_API_TOKEN", "description": "Cloudflare API 토큰"}],
762
+ },
763
+ {
764
+ "id": "mcp-docker",
765
+ "name": "Docker MCP",
766
+ "category": "Infrastructure",
767
+ "install_mode": "npm",
768
+ "package": "docker-mcp",
769
+ "description": "Docker 컨테이너 목록 조회, 실행/중지, 로그 확인을 수행합니다.",
770
+ "keywords": ["docker", "container", "devops", "도커", "컨테이너", "인프라"],
771
+ "capabilities": ["컨테이너 관리", "이미지 조회", "로그 확인", "실행/중지"],
772
+ },
773
+ # ── SaaS / 결제 ───────────────────────────────────────────────────────────
774
+ {
775
+ "id": "mcp-stripe",
776
+ "name": "Stripe MCP",
777
+ "category": "Payments",
778
+ "install_mode": "npm",
779
+ "package": "@stripe/agent-toolkit",
780
+ "description": "Stripe 결제, 고객, 구독, 인보이스를 조회하고 관리합니다.",
781
+ "keywords": ["stripe", "payment", "billing", "subscription", "결제", "스트라이프"],
782
+ "capabilities": ["결제 조회", "고객 관리", "구독 확인", "인보이스"],
783
+ "env_vars": [{"name": "STRIPE_SECRET_KEY", "description": "Stripe Secret Key (sk_...)"}],
784
+ },
785
+ {
786
+ "id": "mcp-supabase",
787
+ "name": "Supabase MCP",
788
+ "category": "Database / BaaS",
789
+ "install_mode": "npm",
790
+ "package": "@supabase/mcp-server-supabase",
791
+ "description": "Supabase 프로젝트의 DB 쿼리, Auth 관리, Storage 파일 접근을 수행합니다.",
792
+ "keywords": ["supabase", "database", "auth", "storage", "supabase", "슈퍼베이스"],
793
+ "capabilities": ["DB 쿼리", "Auth 사용자 조회", "Storage 파일 관리"],
794
+ "env_vars": [
795
+ {"name": "SUPABASE_URL", "description": "https://xxx.supabase.co"},
796
+ {"name": "SUPABASE_SERVICE_ROLE_KEY", "description": "service_role 키"},
797
+ ],
798
+ },
799
+ {
800
+ "id": "mcp-hubspot",
801
+ "name": "HubSpot MCP",
802
+ "category": "CRM / marketing",
803
+ "install_mode": "npm",
804
+ "package": "@hubspot/mcp-server",
805
+ "description": "HubSpot CRM의 연락처, 딜, 캠페인 데이터를 조회하고 분석합니다.",
806
+ "keywords": ["hubspot", "crm", "marketing", "sales", "허브스팟", "CRM"],
807
+ "capabilities": ["연락처 조회", "딜 파이프라인", "캠페인 분석"],
808
+ "env_vars": [{"name": "HUBSPOT_ACCESS_TOKEN", "description": "HubSpot Private App 토큰"}],
809
+ },
810
+ # ── AI / 메모리 ───────────────────────────────────────────────────────────
811
+ {
812
+ "id": "mcp-memory",
813
+ "name": "Memory MCP (공식)",
814
+ "category": "Memory / knowledge",
815
+ "install_mode": "npm",
816
+ "package": "@modelcontextprotocol/server-memory",
817
+ "description": "대화 간 지속 메모리를 저장하고 검색하는 공식 MCP 서버입니다.",
818
+ "keywords": ["memory", "remember", "knowledge", "기억", "메모리", "지식"],
819
+ "capabilities": ["장기 기억 저장", "메모리 검색", "엔티티 추적"],
820
+ },
821
+ {
822
+ "id": "mcp-sequential-thinking",
823
+ "name": "Sequential Thinking MCP",
824
+ "category": "AI / reasoning",
825
+ "install_mode": "npm",
826
+ "package": "@modelcontextprotocol/server-sequential-thinking",
827
+ "description": "복잡한 문제를 단계별로 분해해 추론하는 사고 흐름 도구입니다.",
828
+ "keywords": ["reasoning", "thinking", "chain of thought", "추론", "사고"],
829
+ "capabilities": ["단계별 추론", "문제 분해", "사고 흐름 추적"],
830
+ },
831
+ # ── 커뮤니케이션 ──────────────────────────────────────────────────────────
832
+ {
833
+ "id": "mcp-discord",
834
+ "name": "Discord MCP",
835
+ "category": "Communication",
836
+ "install_mode": "npm",
837
+ "package": "discord-mcp",
838
+ "description": "Discord 서버 채널 메시지 전송, 읽기, 관리 자동화를 수행합니다.",
839
+ "keywords": ["discord", "message", "channel", "디스코드", "메시지", "알림"],
840
+ "capabilities": ["메시지 전송", "채널 읽기", "알림 자동화"],
841
+ "env_vars": [{"name": "DISCORD_BOT_TOKEN", "description": "Discord Bot 토큰"}],
842
+ },
843
+ {
844
+ "id": "mcp-telegram",
845
+ "name": "Telegram MCP",
846
+ "category": "Communication",
847
+ "install_mode": "npm",
848
+ "package": "telegram-mcp",
849
+ "description": "Telegram 봇을 통한 메시지 전송, 수신, 알림 자동화를 수행합니다.",
850
+ "keywords": ["telegram", "bot", "message", "텔레그램", "봇", "메시지"],
851
+ "capabilities": ["메시지 전송/수신", "알림 자동화", "그룹 관리"],
852
+ "env_vars": [{"name": "TELEGRAM_BOT_TOKEN", "description": "BotFather에서 발급한 토큰"}],
853
+ },
854
+ # ── 개발 도구 ─────────────────────────────────────────────────────────────
855
+ {
856
+ "id": "mcp-everything",
857
+ "name": "Everything MCP (테스트)",
858
+ "category": "Developer tools",
859
+ "install_mode": "npm",
860
+ "package": "@modelcontextprotocol/server-everything",
861
+ "description": "MCP 연결 테스트용 모든 기능이 포함된 데모 서버입니다.",
862
+ "keywords": ["test", "demo", "everything", "테스트", "개발"],
863
+ "capabilities": ["MCP 기능 테스트", "프로토타입"],
864
+ },
676
865
  ]
677
866
 
678
867
  # ── Remote MCP Registry (registry.modelcontextprotocol.io) ───────────────────
@@ -6316,6 +6505,106 @@ async def mcp_registry_refresh(request: Request):
6316
6505
  return {"status": "ok", "total": len(registry), "remote": len(_REMOTE_REGISTRY_CACHE)}
6317
6506
 
6318
6507
 
6508
+ @app.get("/mcp/claude-code-servers")
6509
+ async def mcp_claude_code_servers(request: Request):
6510
+ """Read ~/.claude/settings.json mcpServers and return them as Lattice MCP items."""
6511
+ require_user(request)
6512
+ settings_path = Path.home() / ".claude" / "settings.json"
6513
+ if not settings_path.exists():
6514
+ return {"servers": []}
6515
+ try:
6516
+ with open(settings_path, "r", encoding="utf-8") as f:
6517
+ settings = json.load(f)
6518
+ mcp_servers = settings.get("mcpServers", {})
6519
+ servers = []
6520
+ for name, cfg in mcp_servers.items():
6521
+ cmd = cfg.get("command", "")
6522
+ args = cfg.get("args", [])
6523
+ package = " ".join([cmd] + args) if args else cmd
6524
+ env = cfg.get("env", {})
6525
+ env_vars = [{"name": k, "value": v} for k, v in env.items()]
6526
+ servers.append({
6527
+ "id": f"claude-code:{name}",
6528
+ "name": name,
6529
+ "description": f"Claude Code MCP: {package}",
6530
+ "package": package,
6531
+ "icon": "🤖",
6532
+ "category": "Claude Code",
6533
+ "source": "claude-code",
6534
+ "installed": True,
6535
+ "env_vars": env_vars,
6536
+ })
6537
+ return {"servers": servers}
6538
+ except Exception as e:
6539
+ logging.warning("mcp_claude_code_servers failed: %s", e)
6540
+ return {"servers": []}
6541
+
6542
+
6543
+ _CUSTOM_MCP_FILE = DATA_DIR / "custom_mcps.json"
6544
+
6545
+ def _load_custom_mcps() -> List[Dict]:
6546
+ if not _CUSTOM_MCP_FILE.exists():
6547
+ return []
6548
+ try:
6549
+ with open(_CUSTOM_MCP_FILE, "r", encoding="utf-8") as f:
6550
+ return json.load(f)
6551
+ except Exception:
6552
+ return []
6553
+
6554
+ def _save_custom_mcps(items: List[Dict]):
6555
+ with open(_CUSTOM_MCP_FILE, "w", encoding="utf-8") as f:
6556
+ json.dump(items, f, ensure_ascii=False, indent=2)
6557
+
6558
+
6559
+ @app.get("/mcp/custom")
6560
+ async def mcp_custom_list(request: Request):
6561
+ """Return user-added custom MCP entries."""
6562
+ require_user(request)
6563
+ return {"custom": _load_custom_mcps()}
6564
+
6565
+
6566
+ @app.post("/mcp/custom")
6567
+ async def mcp_custom_add(req: McpCustomRequest, request: Request):
6568
+ """Save a custom MCP entry (user-defined)."""
6569
+ require_user(request)
6570
+ if not req.name.strip():
6571
+ raise HTTPException(status_code=400, detail="name은 필수입니다.")
6572
+ if not req.package.strip():
6573
+ raise HTTPException(status_code=400, detail="package는 필수입니다.")
6574
+ items = _load_custom_mcps()
6575
+ entry = {
6576
+ "id": f"custom:{req.name.strip().lower().replace(' ', '-')}",
6577
+ "name": req.name.strip(),
6578
+ "package": req.package.strip(),
6579
+ "description": req.description.strip(),
6580
+ "category": req.category or "custom",
6581
+ "icon": req.icon or "🔌",
6582
+ "env_vars": req.env_vars or [],
6583
+ "install_mode": "npm",
6584
+ "source": "custom",
6585
+ "installed": False,
6586
+ "added_at": datetime.now().isoformat(),
6587
+ }
6588
+ # overwrite if same id
6589
+ items = [e for e in items if e["id"] != entry["id"]]
6590
+ items.append(entry)
6591
+ _save_custom_mcps(items)
6592
+ return {"status": "ok", "entry": entry}
6593
+
6594
+
6595
+ @app.delete("/mcp/custom/{mcp_id:path}")
6596
+ async def mcp_custom_delete(mcp_id: str, request: Request):
6597
+ """Remove a custom MCP entry."""
6598
+ require_admin(request)
6599
+ items = _load_custom_mcps()
6600
+ before = len(items)
6601
+ items = [e for e in items if e["id"] != mcp_id]
6602
+ if len(items) == before:
6603
+ raise HTTPException(status_code=404, detail="항목을 찾을 수 없습니다.")
6604
+ _save_custom_mcps(items)
6605
+ return {"status": "ok"}
6606
+
6607
+
6319
6608
  # ── Skills & Plugin Directory endpoints ───────────────────────────────────────
6320
6609
 
6321
6610
  @app.get("/skills/marketplace")
package/static/chat.html CHANGED
@@ -534,6 +534,44 @@
534
534
  }
535
535
  .mcp-install-btn:hover { background: rgba(34,211,160,0.15); }
536
536
  .mcp-install-btn.installed { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.04); color: var(--faint); }
537
+ /* MCP 소스 배지 */
538
+ .mcp-source-badge {
539
+ font-size: 9.5px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
540
+ text-transform: uppercase; letter-spacing: .05em; flex-shrink: 0;
541
+ }
542
+ .mcp-source-badge.claude-code { background: rgba(100,180,255,0.12); color: #64b4ff; border: 1px solid rgba(100,180,255,0.2); }
543
+ .mcp-source-badge.custom { background: rgba(255,180,60,0.12); color: #ffb43c; border: 1px solid rgba(255,180,60,0.2); }
544
+ /* MCP 탭 */
545
+ .mcp-tabs { display: flex; gap: 4px; padding: 12px 20px 0; border-bottom: 1px solid rgba(255,255,255,0.07); }
546
+ .mcp-tab {
547
+ padding: 7px 14px; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600;
548
+ color: var(--faint); cursor: pointer; border: none; background: none;
549
+ border-bottom: 2px solid transparent; transition: all .15s;
550
+ }
551
+ .mcp-tab.active { color: var(--accent); border-bottom-color: var(--accent); background: rgba(34,211,160,0.06); }
552
+ .mcp-tab:hover:not(.active) { color: var(--text); background: rgba(255,255,255,0.04); }
553
+ /* Custom MCP 추가 폼 */
554
+ .mcp-add-form { display: flex; flex-direction: column; gap: 10px; padding: 4px 0; }
555
+ .mcp-add-form label { font-size: 11px; font-weight: 600; color: var(--faint); text-transform: uppercase; letter-spacing: .06em; }
556
+ .mcp-add-form input, .mcp-add-form textarea, .mcp-add-form select {
557
+ width: 100%; padding: 8px 11px; border-radius: 8px; font-size: 13px;
558
+ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
559
+ color: var(--text); outline: none; transition: border .15s; box-sizing: border-box;
560
+ }
561
+ .mcp-add-form input:focus, .mcp-add-form textarea:focus { border-color: rgba(34,211,160,0.4); }
562
+ .mcp-add-form .field-hint { font-size: 10.5px; color: var(--faint); margin-top: -6px; }
563
+ .mcp-submit-btn {
564
+ padding: 9px 18px; border-radius: 8px; font-size: 13px; font-weight: 700;
565
+ background: rgba(34,211,160,0.12); border: 1px solid rgba(34,211,160,0.3);
566
+ color: var(--accent); cursor: pointer; transition: all .15s; align-self: flex-end;
567
+ }
568
+ .mcp-submit-btn:hover { background: rgba(34,211,160,0.22); }
569
+ .mcp-delete-btn {
570
+ padding: 5px 10px; border-radius: 6px; font-size: 11px; font-weight: 600;
571
+ background: rgba(255,80,80,0.08); border: 1px solid rgba(255,80,80,0.2);
572
+ color: #ff6b6b; cursor: pointer; transition: all .15s; flex-shrink: 0;
573
+ }
574
+ .mcp-delete-btn:hover { background: rgba(255,80,80,0.18); }
537
575
  .acct-modal {
538
576
  background: var(--surface, #1e293b);
539
577
  border: 1px solid rgba(255,255,255,0.08);
@@ -3160,6 +3198,11 @@
3160
3198
  <h3><i class="ti ti-plug-connected"></i> MCP 서버 관리</h3>
3161
3199
  <button class="mcp-modal-close" onclick="closeMcpModal()"><i class="ti ti-x"></i></button>
3162
3200
  </div>
3201
+ <div class="mcp-tabs" id="mcp-tabs">
3202
+ <button class="mcp-tab active" onclick="switchMcpTab('registry',this)">🗂 레지스트리</button>
3203
+ <button class="mcp-tab" onclick="switchMcpTab('claude-code',this)">🤖 Claude Code</button>
3204
+ <button class="mcp-tab" onclick="switchMcpTab('custom',this)">➕ 직접 추가</button>
3205
+ </div>
3163
3206
  <div class="mcp-modal-body" id="mcp-modal-body">
3164
3207
  <div style="color:var(--faint);font-size:13px;text-align:center;padding:24px">로딩 중...</div>
3165
3208
  </div>
@@ -6952,18 +6995,39 @@
6952
6995
 
6953
6996
  <script>
6954
6997
  // ── MCP 관리 모달 ────────────────────────────────────────────────────────
6998
+ let _mcpCurrentTab = 'registry';
6999
+
6955
7000
  async function openMcpModal() {
6956
7001
  document.getElementById('mcp-modal-overlay').classList.add('open');
6957
- await renderMcpModal();
7002
+ await renderMcpModal(_mcpCurrentTab);
6958
7003
  }
6959
7004
 
6960
7005
  function closeMcpModal() {
6961
7006
  document.getElementById('mcp-modal-overlay').classList.remove('open');
6962
7007
  }
6963
7008
 
6964
- async function renderMcpModal() {
7009
+ function switchMcpTab(tab, btn) {
7010
+ _mcpCurrentTab = tab;
7011
+ document.querySelectorAll('.mcp-tab').forEach(b => b.classList.remove('active'));
7012
+ btn.classList.add('active');
7013
+ renderMcpModal(tab);
7014
+ }
7015
+
7016
+ async function renderMcpModal(tab) {
7017
+ tab = tab || _mcpCurrentTab || 'registry';
6965
7018
  const body = document.getElementById('mcp-modal-body');
6966
7019
  body.innerHTML = '<div style="color:var(--faint);font-size:13px;text-align:center;padding:24px">로딩 중...</div>';
7020
+
7021
+ if (tab === 'claude-code') {
7022
+ await renderMcpClaudeCode(body);
7023
+ } else if (tab === 'custom') {
7024
+ await renderMcpCustom(body);
7025
+ } else {
7026
+ await renderMcpRegistry(body);
7027
+ }
7028
+ }
7029
+
7030
+ async function renderMcpRegistry(body) {
6967
7031
  try {
6968
7032
  const [installedRes, toolsRes] = await Promise.all([
6969
7033
  apiFetch('/mcp/installed'),
@@ -7015,6 +7079,148 @@
7015
7079
  }
7016
7080
  }
7017
7081
 
7082
+ async function renderMcpClaudeCode(body) {
7083
+ try {
7084
+ const res = await apiFetch('/mcp/claude-code-servers');
7085
+ const data = res.ok ? await res.json() : { servers: [] };
7086
+ const servers = data.servers || [];
7087
+
7088
+ if (!servers.length) {
7089
+ body.innerHTML = `
7090
+ <div style="color:var(--faint);font-size:13px;text-align:center;padding:32px 24px;">
7091
+ <div style="font-size:28px;margin-bottom:10px">🤖</div>
7092
+ <div><strong>Claude Code MCP 없음</strong></div>
7093
+ <div style="margin-top:6px;font-size:11px">~/.claude/settings.json에 mcpServers 항목이 없습니다.</div>
7094
+ <div style="margin-top:4px;font-size:11px">Claude Code에서 MCP를 설치하면 여기에 자동으로 표시됩니다.</div>
7095
+ </div>`;
7096
+ return;
7097
+ }
7098
+
7099
+ let html = '<div class="mcp-section-label">Claude Code에서 설치된 MCP</div>';
7100
+ html += servers.map(srv => `
7101
+ <div class="mcp-item">
7102
+ <div class="mcp-item-icon">${srv.icon || '🤖'}</div>
7103
+ <div class="mcp-item-info">
7104
+ <div class="mcp-item-name" style="display:flex;align-items:center;gap:6px;">
7105
+ ${escapeHtml(srv.name)}
7106
+ <span class="mcp-source-badge claude-code">Claude Code</span>
7107
+ </div>
7108
+ <div class="mcp-item-desc" title="${escapeHtml(srv.package || '')}">${escapeHtml(srv.package || '')}</div>
7109
+ ${srv.env_vars && srv.env_vars.length ? `<div class="mcp-item-desc" style="margin-top:2px">ENV: ${escapeHtml(srv.env_vars.map(e=>e.name).join(', '))}</div>` : ''}
7110
+ </div>
7111
+ <span class="mcp-item-status">활성</span>
7112
+ </div>
7113
+ `).join('');
7114
+ body.innerHTML = html;
7115
+ } catch (e) {
7116
+ body.innerHTML = `<div style="color:#ff6b6b;font-size:13px;text-align:center;padding:24px">로드 실패: ${escapeHtml(e.message)}</div>`;
7117
+ }
7118
+ }
7119
+
7120
+ async function renderMcpCustom(body) {
7121
+ // Load existing custom MCPs
7122
+ let existingHtml = '';
7123
+ try {
7124
+ const res = await apiFetch('/mcp/custom');
7125
+ const data = res.ok ? await res.json() : { custom: [] };
7126
+ const customs = data.custom || [];
7127
+ if (customs.length) {
7128
+ existingHtml = '<div class="mcp-section-label">직접 추가한 MCP</div>';
7129
+ existingHtml += customs.map(c => `
7130
+ <div class="mcp-item" id="mcp-custom-item-${escapeHtml(c.id)}">
7131
+ <div class="mcp-item-icon">${c.icon || '🔌'}</div>
7132
+ <div class="mcp-item-info">
7133
+ <div class="mcp-item-name" style="display:flex;align-items:center;gap:6px;">
7134
+ ${escapeHtml(c.name)}
7135
+ <span class="mcp-source-badge custom">Custom</span>
7136
+ </div>
7137
+ <div class="mcp-item-desc" title="${escapeHtml(c.package||'')}">${escapeHtml(c.package||'')}</div>
7138
+ ${c.description ? `<div class="mcp-item-desc">${escapeHtml(c.description)}</div>` : ''}
7139
+ </div>
7140
+ <button class="mcp-delete-btn" onclick="deleteCustomMcp('${escapeHtml(c.id)}')">삭제</button>
7141
+ </div>
7142
+ `).join('');
7143
+ }
7144
+ } catch {}
7145
+
7146
+ body.innerHTML = existingHtml + `
7147
+ <div class="mcp-section-label" style="margin-top:${existingHtml?'16px':'4px'}">새 MCP 추가</div>
7148
+ <div class="mcp-add-form">
7149
+ <div>
7150
+ <label>이름 *</label>
7151
+ <input id="mcp-add-name" type="text" placeholder="예: my-database" />
7152
+ </div>
7153
+ <div>
7154
+ <label>패키지 / 명령어 *</label>
7155
+ <input id="mcp-add-package" type="text" placeholder="예: @company/mcp-server 또는 npx mcp-server" />
7156
+ <div class="field-hint">npm 패키지명 또는 실행 명령어</div>
7157
+ </div>
7158
+ <div>
7159
+ <label>설명</label>
7160
+ <input id="mcp-add-desc" type="text" placeholder="이 MCP가 하는 일 (선택)" />
7161
+ </div>
7162
+ <div>
7163
+ <label>필요한 환경변수 (쉼표 구분)</label>
7164
+ <input id="mcp-add-envs" type="text" placeholder="예: API_KEY, BASE_URL" />
7165
+ <div class="field-hint">서버 실행 시 필요한 env var 이름들</div>
7166
+ </div>
7167
+ <div style="display:flex;align-items:center;gap:8px;">
7168
+ <input id="mcp-add-icon" type="text" placeholder="🔌" style="width:56px;text-align:center;" maxlength="4" />
7169
+ <label style="margin:0">아이콘 (이모지)</label>
7170
+ </div>
7171
+ <div id="mcp-add-error" style="color:#ff6b6b;font-size:12px;display:none;"></div>
7172
+ <button class="mcp-submit-btn" onclick="submitCustomMcp()">➕ MCP 추가</button>
7173
+ </div>
7174
+ `;
7175
+ }
7176
+
7177
+ async function submitCustomMcp() {
7178
+ const name = document.getElementById('mcp-add-name').value.trim();
7179
+ const pkg = document.getElementById('mcp-add-package').value.trim();
7180
+ const desc = document.getElementById('mcp-add-desc').value.trim();
7181
+ const envsRaw = document.getElementById('mcp-add-envs').value.trim();
7182
+ const icon = document.getElementById('mcp-add-icon').value.trim() || '🔌';
7183
+ const errEl = document.getElementById('mcp-add-error');
7184
+ errEl.style.display = 'none';
7185
+
7186
+ if (!name) { errEl.textContent = '이름을 입력해주세요.'; errEl.style.display='block'; return; }
7187
+ if (!pkg) { errEl.textContent = '패키지를 입력해주세요.'; errEl.style.display='block'; return; }
7188
+
7189
+ const env_vars = envsRaw ? envsRaw.split(',').map(s=>({name:s.trim(),description:''})).filter(e=>e.name) : [];
7190
+ try {
7191
+ const res = await apiFetch('/mcp/custom', {
7192
+ method: 'POST',
7193
+ headers: { 'Content-Type': 'application/json' },
7194
+ body: JSON.stringify({ name, package: pkg, description: desc, icon, env_vars }),
7195
+ });
7196
+ if (res.ok) {
7197
+ await renderMcpModal('custom');
7198
+ } else {
7199
+ const d = await res.json().catch(()=>({}));
7200
+ errEl.textContent = d.detail || '추가 실패';
7201
+ errEl.style.display = 'block';
7202
+ }
7203
+ } catch (e) {
7204
+ errEl.textContent = e.message || '네트워크 오류';
7205
+ errEl.style.display = 'block';
7206
+ }
7207
+ }
7208
+
7209
+ async function deleteCustomMcp(id) {
7210
+ if (!confirm('이 MCP를 삭제하시겠습니까?')) return;
7211
+ try {
7212
+ const res = await apiFetch('/mcp/custom/' + encodeURIComponent(id), { method: 'DELETE' });
7213
+ if (res.ok) {
7214
+ await renderMcpModal('custom');
7215
+ } else {
7216
+ const d = await res.json().catch(()=>({}));
7217
+ alert(d.detail || '삭제 실패');
7218
+ }
7219
+ } catch (e) {
7220
+ alert(e.message || '네트워크 오류');
7221
+ }
7222
+ }
7223
+
7018
7224
  async function installMcp(id) {
7019
7225
  const btn = document.querySelector(`#mcp-item-${CSS.escape(id)} .mcp-install-btn`);
7020
7226
  if (btn) { btn.disabled = true; btn.textContent = '설치 중...'; }
@@ -7025,7 +7231,7 @@
7025
7231
  body: JSON.stringify({ mcp_id: id }),
7026
7232
  });
7027
7233
  if (res.ok) {
7028
- await renderMcpModal();
7234
+ await renderMcpModal('registry');
7029
7235
  } else {
7030
7236
  const d = await res.json().catch(() => ({}));
7031
7237
  if (btn) { btn.disabled = false; btn.textContent = '설치'; }