claude-os/mcp_server/claude_code_mcp.py
Rosie Val e374440c67 Add a service-token bypass for code-forge, gated on auth being enabled
code-forge (the stdio MCP server) calls the REST API directly and has
no login flow of its own. With auth disabled it already passes
through as guest, same as before - but the moment CLAUDE_OS_EMAIL/
PASSWORD get set, it would start getting 401'd with no way to
authenticate.

get_current_user now accepts a CLAUDE_OS_SERVICE_TOKEN as a bearer
token, checked with a constant-time compare, alongside normal user
JWTs. claude_code_mcp.py sends it (via a shared _auth_headers() used
by all 5 of its httpx.AsyncClient() call sites) whenever
CLAUDE_OS_SERVICE_TOKEN is set, and sends nothing when it isn't - a
no-op change for anyone not using auth.

Since this process can be launched directly (e.g. over SSH as a stdio
server) without claude-os's own .env sourced into its shell or its cwd
set to the project root, it now loads .env explicitly by path relative
to the script's own location rather than relying on load_dotenv()'s
cwd-relative default search.
2026-08-22 16:42:03 -04:00

790 lines
32 KiB
Python

#!/usr/bin/env python3
"""
True MCP Server for Claude Code integration.
Wraps the Claude OS REST API as a proper MCP server with stdio transport.
Server name is "code-forge" for backwards compatibility with existing
memories and documentation, but it IS Claude OS.
Usage:
Run: ./install.sh (this will configure everything)
Or manually add to ~/.claude/settings.json:
{
"mcpServers": {
"code-forge": {
"command": "/path/to/claude-os/venv/bin/python3",
"args": ["/path/to/claude-os/mcp_server/claude_code_mcp.py"],
"env": {
"CLAUDE_OS_API": "http://localhost:8051"
}
}
}
}
"""
import asyncio
import json
import os
import sys
import time
from pathlib import Path
from typing import Any
import httpx
from dotenv import load_dotenv
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# This process is launched directly (e.g. over SSH as a stdio MCP server),
# not necessarily with claude-os's own .env sourced into its environment or
# its working directory set to the project root - so load it explicitly by
# path rather than relying on load_dotenv()'s cwd-relative default search.
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
# Configuration
API_BASE = os.environ.get("CLAUDE_OS_API", "http://localhost:8051")
SERVICE_TOKEN = os.environ.get("CLAUDE_OS_SERVICE_TOKEN")
def _auth_headers() -> dict:
"""Auth header for calling claude-os's own API. No-op unless a service
token is configured, which only matters once CLAUDE_OS_EMAIL/PASSWORD
are set - the API allows all requests through as a guest otherwise."""
return {"Authorization": f"Bearer {SERVICE_TOKEN}"} if SERVICE_TOKEN else {}
# Health cache — tracks last health check time per KB and any warnings
HEALTH_CACHE: dict[str, dict] = {}
HEALTH_CACHE_FILE = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "health_cache.json"
)
HEALTH_CHECK_INTERVAL = 86400 # 24 hours
def _load_health_cache() -> None:
"""Load health cache from disk."""
global HEALTH_CACHE
try:
if os.path.exists(HEALTH_CACHE_FILE):
with open(HEALTH_CACHE_FILE, "r") as f:
HEALTH_CACHE = json.load(f)
except Exception:
HEALTH_CACHE = {}
def _save_health_cache() -> None:
"""Persist health cache to disk."""
try:
Path(HEALTH_CACHE_FILE).parent.mkdir(parents=True, exist_ok=True)
with open(HEALTH_CACHE_FILE, "w") as f:
json.dump(HEALTH_CACHE, f)
except Exception:
pass
async def _maybe_check_health(kb_name: str) -> list[str]:
"""Run a health check if stale (>24h) for this KB. Returns warning strings."""
_load_health_cache()
now = time.time()
entry = HEALTH_CACHE.get(kb_name)
if entry and (now - entry.get("checked_at", 0)) < HEALTH_CHECK_INTERVAL:
return entry.get("warnings", [])
# Run health check
try:
async with httpx.AsyncClient(headers=_auth_headers()) as client:
resp = await client.get(api_url(f"/api/kb/{kb_name}/lifecycle/health"), timeout=15.0)
resp.raise_for_status()
health = resp.json()
warnings = []
recs = health.get("recommendations", [])
for rec in recs:
severity = rec.get("severity", "")
msg = rec.get("message", rec.get("recommendation", ""))
if severity in ("high", "critical") and msg:
warnings.append(f"[{severity.upper()}] {msg}")
HEALTH_CACHE[kb_name] = {"checked_at": now, "warnings": warnings}
_save_health_cache()
return warnings
except Exception:
# Never break search — just skip the health check
return []
# Initialize MCP server - named "code-forge" for backwards compatibility
server = Server("code-forge")
def api_url(path: str) -> str:
"""Build full API URL."""
return f"{API_BASE}{path}"
async def api_get(path: str) -> dict:
"""Make GET request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
response = await client.get(api_url(path), timeout=30.0)
response.raise_for_status()
return response.json()
async def api_post(path: str, data: dict = None) -> dict:
"""Make POST request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
response = await client.post(api_url(path), json=data or {}, timeout=60.0)
response.raise_for_status()
return response.json()
async def api_delete(path: str) -> dict:
"""Make DELETE request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
response = await client.delete(api_url(path), timeout=30.0)
response.raise_for_status()
return response.json()
async def api_put(path: str, data: dict = None) -> dict:
"""Make PUT request to API."""
async with httpx.AsyncClient(headers=_auth_headers()) as client:
response = await client.put(api_url(path), json=data or {}, timeout=30.0)
response.raise_for_status()
return response.json()
@server.list_tools()
async def list_tools() -> list[Tool]:
"""List available Claude OS tools.
Tool names match what templates/commands expect:
- mcp__code-forge__search_knowledge_base
- mcp__code-forge__list_knowledge_bases
- mcp__code-forge__list_documents
- etc.
"""
return [
# Knowledge Base Tools
Tool(
name="list_knowledge_bases",
description="List all knowledge bases in Claude OS",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="create_knowledge_base",
description="Create a new knowledge base",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name of the knowledge base"},
"kb_type": {"type": "string", "enum": ["generic", "code", "documentation"], "default": "generic"},
"description": {"type": "string", "description": "Description of the KB"}
},
"required": ["name"]
}
),
Tool(
name="search_knowledge_base",
description="Search/query a knowledge base using RAG. Returns relevant documents and an AI-generated answer.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base to search"},
"query": {"type": "string", "description": "Search query"},
"top_k": {"type": "integer", "default": 5, "description": "Number of results to return"},
"use_hybrid": {"type": "boolean", "default": True, "description": "Use hybrid search (semantic + keyword)"}
},
"required": ["kb_name", "query"]
}
),
Tool(
name="get_kb_stats",
description="Get statistics for a knowledge base (document count, chunk count, etc.)",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"}
},
"required": ["kb_name"]
}
),
Tool(
name="list_documents",
description="List all documents in a knowledge base",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"}
},
"required": ["kb_name"]
}
),
Tool(
name="delete_knowledge_base",
description="Delete a knowledge base and all its documents",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base to delete"}
},
"required": ["kb_name"]
}
),
# Project Tools
Tool(
name="list_projects",
description="List all projects registered with Claude OS",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="create_project",
description="Create/register a new project with Claude OS",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Project name"},
"path": {"type": "string", "description": "Absolute path to project directory"},
"description": {"type": "string", "description": "Project description"}
},
"required": ["name", "path"]
}
),
Tool(
name="get_project",
description="Get details for a specific project",
inputSchema={
"type": "object",
"properties": {
"project_id": {"type": "integer", "description": "Project ID"}
},
"required": ["project_id"]
}
),
# Indexing Tools
Tool(
name="index_structural",
description="Run tree-sitter structural indexing on code files. Fast indexing that extracts functions, classes, imports.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"path": {"type": "string", "description": "Path to directory to index"}
},
"required": ["kb_name", "path"]
}
),
Tool(
name="index_semantic",
description="Run semantic embedding indexing on a knowledge base. Creates vector embeddings for similarity search. Runs in background by default - returns job_id to check progress at /api/jobs/{job_id}.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"project_path": {"type": "string", "description": "Path to project to index"},
"selective": {"type": "boolean", "description": "If true, only index top 20% most important files + docs (default: true)"},
"background": {"type": "boolean", "description": "If true, run in background (default: true). Set false to block until complete."}
},
"required": ["kb_name", "project_path"]
}
),
# Document Management
Tool(
name="upload_document",
description="Upload/save a document or memory to a knowledge base",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"content": {"type": "string", "description": "Content to save (markdown supported)"},
"filename": {"type": "string", "description": "Filename for the document (e.g., 'my-memory.md')"},
"title": {"type": "string", "description": "Title for the document"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags for categorization"}
},
"required": ["kb_name", "content", "filename"]
}
),
Tool(
name="delete_document",
description="Delete a document from a knowledge base",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"filename": {"type": "string", "description": "Filename of the document to delete"}
},
"required": ["kb_name", "filename"]
}
),
# Utility Tools
Tool(
name="get_ollama_models",
description="List available Ollama models for embeddings and LLM",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="health_check",
description="Check if Claude OS API server is running and healthy",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
# Session Tools
Tool(
name="list_sessions",
description="List Claude Code session files for a project. Sessions are stored in ~/.claude/projects/{encoded-path}/ and contain conversation history.",
inputSchema={
"type": "object",
"properties": {
"project_path": {"type": "string", "description": "Absolute path to project directory (e.g., /Users/x/Projects/myapp)"},
"limit": {"type": "integer", "default": 50, "description": "Maximum sessions to return (default: 50)"}
},
"required": ["project_path"]
}
),
Tool(
name="extract_session_insights",
description="Extract insights from a Claude Code session file using LLM. Analyzes conversation to find decisions, patterns, solutions, and blockers. Optionally saves insights to a knowledge base.",
inputSchema={
"type": "object",
"properties": {
"session_path": {"type": "string", "description": "Absolute path to .jsonl session file"},
"kb_name": {"type": "string", "description": "Knowledge base name to save insights (optional)"},
"auto_save": {"type": "boolean", "default": False, "description": "Auto-save all insights without prompting"},
"insight_types": {
"type": "array",
"items": {"type": "string", "enum": ["decision", "pattern", "solution", "blocker"]},
"default": ["decision", "pattern", "solution", "blocker"],
"description": "Types of insights to extract"
},
"min_confidence": {"type": "number", "default": 0.7, "description": "Minimum confidence threshold (0.0-1.0)"}
},
"required": ["session_path"]
}
),
# Skill Management Tools
Tool(
name="list_skills",
description="List all skills (global and project-level). Global skills are in ~/.claude/skills/, project skills are in {project}/.claude/skills/.",
inputSchema={
"type": "object",
"properties": {
"project_path": {"type": "string", "description": "Project path to include project-level skills (optional)"},
"include_content": {"type": "boolean", "default": False, "description": "Include full skill content"}
},
"required": []
}
),
Tool(
name="list_skill_templates",
description="List available skill templates that can be installed to projects. Templates are organized by category.",
inputSchema={
"type": "object",
"properties": {
"category": {"type": "string", "description": "Filter by category (optional)"}
},
"required": []
}
),
Tool(
name="get_skill",
description="Get skill details including full content.",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Skill name"},
"scope": {"type": "string", "enum": ["global", "project"], "description": "Where the skill is located"},
"project_path": {"type": "string", "description": "Project path (required if scope is 'project')"}
},
"required": ["name", "scope"]
}
),
Tool(
name="install_skill_template",
description="Install a skill template to a project. Copies the template to {project}/.claude/skills/.",
inputSchema={
"type": "object",
"properties": {
"template_name": {"type": "string", "description": "Name of template to install"},
"project_path": {"type": "string", "description": "Project path to install to"},
"custom_name": {"type": "string", "description": "Custom name for installed skill (optional)"}
},
"required": ["template_name", "project_path"]
}
),
Tool(
name="create_skill",
description="Create a custom skill for a project.",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Skill name"},
"description": {"type": "string", "description": "Skill description"},
"content": {"type": "string", "description": "Skill content (markdown)"},
"project_path": {"type": "string", "description": "Project path"},
"category": {"type": "string", "description": "Category (optional)"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Tags (optional)"}
},
"required": ["name", "description", "content", "project_path"]
}
),
Tool(
name="update_skill",
description="Update an existing project skill.",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Skill name"},
"project_path": {"type": "string", "description": "Project path"},
"content": {"type": "string", "description": "New content (optional)"},
"description": {"type": "string", "description": "New description (optional)"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "New tags (optional)"}
},
"required": ["name", "project_path"]
}
),
Tool(
name="delete_skill",
description="Delete a project skill. Cannot delete global skills.",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Skill name to delete"},
"project_path": {"type": "string", "description": "Project path"}
},
"required": ["name", "project_path"]
}
),
# Knowledge Lifecycle Tools
Tool(
name="kb_lifecycle_health",
description="Get a comprehensive health report for a knowledge base including duplicate detection, embedding coverage, age distribution, and recommendations.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"}
},
"required": ["kb_name"]
}
),
Tool(
name="kb_lifecycle_dedup",
description="Scan for and merge duplicate documents in a knowledge base. Use action='scan' to find duplicates, action='merge' to merge them.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"action": {"type": "string", "enum": ["scan", "merge"], "description": "Action to perform: scan for duplicates or merge them"},
"threshold": {"type": "number", "default": 0.85, "description": "Similarity threshold for scan (0.0-1.0)"},
"max_pairs": {"type": "integer", "default": 100, "description": "Maximum duplicate pairs to return"},
"keep_doc_id": {"type": "string", "description": "Document ID to keep (required for merge)"},
"remove_doc_ids": {"type": "array", "items": {"type": "string"}, "description": "Document IDs to remove (required for merge)"},
"dry_run": {"type": "boolean", "default": False, "description": "Preview changes without applying"}
},
"required": ["kb_name", "action"]
}
),
Tool(
name="kb_lifecycle_consolidate",
description="Consolidate multiple related documents into a single merged document using LLM-powered summarization.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"doc_ids": {"type": "array", "items": {"type": "string"}, "description": "Document IDs to consolidate"},
"new_filename": {"type": "string", "description": "Filename for the consolidated document"},
"dry_run": {"type": "boolean", "default": False, "description": "Preview without consolidating"}
},
"required": ["kb_name", "doc_ids", "new_filename"]
}
),
Tool(
name="kb_lifecycle_archive",
description="Manage document archival: archive, restore, list archived, or find stale documents.",
inputSchema={
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"},
"action": {"type": "string", "enum": ["archive", "restore", "list", "stale"], "description": "Archival action to perform"},
"doc_ids": {"type": "array", "items": {"type": "string"}, "description": "Document IDs (required for archive/restore)"},
"reason": {"type": "string", "default": "manual", "description": "Reason for archiving"},
"stale_days": {"type": "integer", "default": 90, "description": "Days threshold for stale detection"}
},
"required": ["kb_name", "action"]
}
),
# Cross-KB Search
Tool(
name="search_all_knowledge_bases",
description="Search across multiple knowledge bases at once. Returns merged results sorted by relevance with KB attribution.",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"top_k": {"type": "integer", "default": 10, "description": "Number of results to return"},
"kb_filter": {"type": "string", "description": "Optional prefix filter for KB names (e.g. 'Pistn-' to search only Pistn KBs)"}
},
"required": ["query"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
"""Execute a Claude OS tool."""
try:
result = await _execute_tool(name, arguments)
return [TextContent(type="text", text=json.dumps(result, indent=2))]
except httpx.HTTPStatusError as e:
error_msg = f"API error: {e.response.status_code} - {e.response.text}"
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
except httpx.ConnectError:
return [TextContent(type="text", text=json.dumps({
"error": "Cannot connect to Claude OS API. Is the server running? Start with: ./start_all_services.sh"
}))]
except Exception as e:
return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
async def _execute_tool(name: str, args: dict[str, Any]) -> dict:
"""Route tool calls to appropriate API endpoints."""
# Knowledge Base Tools
if name == "list_knowledge_bases":
return await api_get("/api/kb")
elif name == "create_knowledge_base":
return await api_post("/api/kb", {
"name": args["name"],
"kb_type": args.get("kb_type", "generic"),
"description": args.get("description", "")
})
elif name == "search_knowledge_base":
result = await api_post(f"/api/kb/{args['kb_name']}/chat", {
"query": args["query"],
"top_k": args.get("top_k", 5),
"use_hybrid": args.get("use_hybrid", True)
})
# Inline health check — append warnings if stale
warnings = await _maybe_check_health(args["kb_name"])
if warnings:
result["_health_warnings"] = warnings
return result
elif name == "get_kb_stats":
return await api_get(f"/api/kb/{args['kb_name']}/stats")
elif name == "list_documents":
return await api_get(f"/api/kb/{args['kb_name']}/documents")
elif name == "delete_knowledge_base":
return await api_delete(f"/api/kb/{args['kb_name']}")
# Project Tools
elif name == "list_projects":
return await api_get("/api/projects")
elif name == "create_project":
return await api_post("/api/projects", {
"name": args["name"],
"path": args["path"],
"description": args.get("description", "")
})
elif name == "get_project":
return await api_get(f"/api/projects/{args['project_id']}")
# Indexing Tools
elif name == "index_structural":
return await api_post(f"/api/kb/{args['kb_name']}/index-structural", {
"path": args["path"]
})
elif name == "index_semantic":
return await api_post(f"/api/kb/{args['kb_name']}/index-semantic", {
"project_path": args["project_path"],
"selective": args.get("selective", True),
"background": args.get("background", True)
})
# Document Management
elif name == "upload_document":
return await api_post(f"/api/kb/{args['kb_name']}/documents/content", {
"content": args["content"],
"filename": args["filename"],
"metadata": {
"title": args.get("title", ""),
"tags": args.get("tags", [])
}
})
elif name == "delete_document":
return await api_delete(f"/api/kb/{args['kb_name']}/documents/{args['filename']}")
# Utility Tools
elif name == "get_ollama_models":
return await api_get("/api/ollama/models")
elif name == "health_check":
try:
# Simple health check - list KBs should work if server is up
result = await api_get("/api/kb")
return {"status": "healthy", "message": "Claude OS is running", "kb_count": len(result.get("knowledge_bases", []))}
except Exception as e:
return {"status": "unhealthy", "error": str(e)}
# Session Tools
elif name == "list_sessions":
return await api_get(f"/api/sessions/list?project_path={args['project_path']}&limit={args.get('limit', 50)}")
elif name == "extract_session_insights":
return await api_post("/api/sessions/extract", {
"session_path": args["session_path"],
"kb_name": args.get("kb_name"),
"auto_save": args.get("auto_save", False),
"insight_types": args.get("insight_types"),
"min_confidence": args.get("min_confidence", 0.7)
})
# Skill Management Tools
elif name == "list_skills":
project_path = args.get("project_path", "")
include_content = args.get("include_content", False)
url = f"/api/skills?include_content={include_content}"
if project_path:
url += f"&project_path={project_path}"
return await api_get(url)
elif name == "list_skill_templates":
category = args.get("category")
url = "/api/skills/templates"
if category:
url += f"?category={category}"
return await api_get(url)
elif name == "get_skill":
scope = args["scope"]
name_param = args["name"]
project_path = args.get("project_path", "")
url = f"/api/skills/{scope}/{name_param}"
if project_path:
url += f"?project_path={project_path}"
return await api_get(url)
elif name == "install_skill_template":
return await api_post(f"/api/skills/install?project_path={args['project_path']}", {
"template_name": args["template_name"],
"custom_name": args.get("custom_name")
})
elif name == "create_skill":
return await api_post(f"/api/skills?project_path={args['project_path']}", {
"name": args["name"],
"description": args["description"],
"content": args["content"],
"category": args.get("category"),
"tags": args.get("tags")
})
elif name == "update_skill":
return await api_put(f"/api/skills/{args['name']}?project_path={args['project_path']}", {
"content": args.get("content"),
"description": args.get("description"),
"tags": args.get("tags")
})
elif name == "delete_skill":
return await api_delete(f"/api/skills/{args['name']}?project_path={args['project_path']}")
# Knowledge Lifecycle Tools
elif name == "kb_lifecycle_health":
return await api_get(f"/api/kb/{args['kb_name']}/lifecycle/health")
elif name == "kb_lifecycle_dedup":
action = args["action"]
kb_name = args["kb_name"]
if action == "scan":
return await api_post(f"/api/kb/{kb_name}/lifecycle/dedup-scan", {
"threshold": args.get("threshold", 0.85),
"max_pairs": args.get("max_pairs", 100)
})
elif action == "merge":
return await api_post(f"/api/kb/{kb_name}/lifecycle/dedup-merge", {
"keep_doc_id": args["keep_doc_id"],
"remove_doc_ids": args["remove_doc_ids"],
"dry_run": args.get("dry_run", False)
})
else:
return {"error": f"Unknown dedup action: {action}"}
elif name == "kb_lifecycle_consolidate":
return await api_post(f"/api/kb/{args['kb_name']}/lifecycle/consolidate", {
"doc_ids": args["doc_ids"],
"new_filename": args["new_filename"],
"dry_run": args.get("dry_run", False)
})
elif name == "kb_lifecycle_archive":
action = args["action"]
kb_name = args["kb_name"]
if action == "archive":
return await api_post(f"/api/kb/{kb_name}/lifecycle/archive", {
"doc_ids": args["doc_ids"],
"reason": args.get("reason", "manual")
})
elif action == "restore":
return await api_post(f"/api/kb/{kb_name}/lifecycle/restore", {
"doc_ids": args["doc_ids"]
})
elif action == "list":
return await api_get(f"/api/kb/{kb_name}/lifecycle/archived")
elif action == "stale":
stale_days = args.get("stale_days", 90)
return await api_get(f"/api/kb/{kb_name}/lifecycle/stale?stale_days={stale_days}")
else:
return {"error": f"Unknown archive action: {action}"}
# Cross-KB Search
elif name == "search_all_knowledge_bases":
return await api_post("/api/kb/search-all", {
"query": args["query"],
"top_k": args.get("top_k", 10),
"kb_filter": args.get("kb_filter")
})
else:
return {"error": f"Unknown tool: {name}"}
async def main():
"""Run the MCP server."""
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())