š„ Code-Forge
-Localized Multi-KB RAG System with MCP Integration
-ā Connected
', unsafe_allow_html=True) - st.metric("Knowledge Bases", len(collections)) - except Exception as e: - st.markdown(f'ā Not Connected
', unsafe_allow_html=True) - st.error(f"Error: {str(e)}") - st.info("Make sure PostgreSQL is running and the `codeforge` database exists.") - -# Check Ollama -with col2: - st.markdown("### Ollama") - try: - response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=2) - if response.status_code == 200: - models = response.json().get("models", []) - st.markdown(f'ā Running
', unsafe_allow_html=True) - st.metric("Models Installed", len(models)) - else: - st.markdown(f'ā ļø Unexpected Response
', unsafe_allow_html=True) - except Exception as e: - st.markdown(f'ā Not Running
', unsafe_allow_html=True) - st.error(f"Error: {str(e)}") - st.info("Make sure Ollama container is running: `docker-compose up -d`") - -# Check Required Models -with col3: - st.markdown("### Required Models") - try: - response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=2) - if response.status_code == 200: - models = response.json().get("models", []) - model_names = [m["name"] for m in models] - - required_models = ["nomic-embed-text:latest", "llama3.2:3b"] - missing_models = [m for m in required_models if m not in model_names] - - if not missing_models: - st.markdown(f'ā All Present
', unsafe_allow_html=True) - st.success("All required models are installed!") - else: - st.markdown(f'ā ļø Missing Models
', unsafe_allow_html=True) - st.warning(f"Missing: {', '.join(missing_models)}") - st.info("Run: `ollama pull nomic-embed-text && ollama pull llama3.2:3b`") - except Exception: - st.markdown(f'ā Cannot Check
', unsafe_allow_html=True) - -st.markdown("---") - -# Quick Start Guide -st.markdown("## šÆ Quick Start Guide") - -tab1, tab2, tab3, tab4 = st.tabs(["1ļøā£ Create KB", "2ļøā£ Ingest Docs", "3ļøā£ Query", "4ļøā£ MCP"]) - -with tab1: - st.markdown(""" - ### Creating Your First Knowledge Base - - 1. **Navigate to the main page** (use the sidebar) - 2. **Expand "ā Create New KB"** in the sidebar - 3. **Choose a KB type**: - - š **GENERIC** - General-purpose knowledge - - š» **CODE** - Source code repositories - - š **DOCUMENTATION** - Technical documentation - - š¤ **AGENT_OS** - Spec-driven development - 4. **Enter a name** (e.g., `my-project`) - 5. **Add a description** (optional) - 6. **Click "Create"** - - ⨠Your knowledge base is now ready for documents! - """) - - st.info("š” **Tip**: Use separate KBs for different projects to keep contexts isolated.") - -with tab2: - st.markdown(""" - ### Ingesting Documents - - **Option 1: File Upload** - 1. Select your KB from the sidebar - 2. Go to the **"š Knowledge Base"** tab - 3. Click **"Choose files"** and select documents - 4. Click **"Upload Files"** - - **Option 2: Directory Import** - 1. Select your KB from the sidebar - 2. Go to the **"š Knowledge Base"** tab - 3. Enter the full path to your project directory - 4. Click **"Import Directory"** - - **Supported File Types:** - - Documents: `.md`, `.txt`, `.pdf` - - Code: `.py`, `.js`, `.jsx`, `.ts`, `.tsx`, `.go`, `.rs`, `.java`, `.cpp`, `.c`, `.h` - - Config: `.json`, `.yaml`, `.yml` - """) - - st.info("š” **Tip**: For large codebases, use directory import to ingest all files at once.") - -with tab3: - st.markdown(""" - ### Querying Your Knowledge Base - - 1. **Select your KB** from the sidebar - 2. **Go to the "š¬ Chat" tab** - 3. **Choose a RAG strategy**: - - š **Base** - Simple vector search (fastest) - - š **Hybrid** - BM25 + vector fusion (better for keywords) - - šÆ **Reranking** - Cross-encoder reranking (most accurate) - - š¤ **Agentic** - Sub-question decomposition (best for complex queries) - 4. **Type your question** and press Enter - 5. **View sources** by expanding "š View Sources" - - **Example Queries:** - - "How does authentication work in this codebase?" - - "What are the main components of the system?" - - "Show me examples of error handling" - """) - - st.info("š” **Tip**: Use hybrid search for technical queries with specific keywords.") - -with tab4: - st.markdown(""" - ### MCP Integration (Model Context Protocol) - - Code-Forge exposes your knowledge bases via HTTP for AI agent integration. - - **MCP Server Endpoint:** - ``` - http://localhost:8051/mcp - ``` - - **Available Tools:** - - `list_knowledge_bases` - List all KBs - - `create_knowledge_base` - Create new KB - - `search_knowledge_base` - Query KB with RAG - - `get_kb_stats` - Get KB statistics - - `ingest_documents` - Add documents - - `get_standards` - Get Agent OS standards - - `get_workflows` - Get Agent OS workflows - - `get_specs` - Get Agent OS specs - - **Example Request:** - ```bash - curl -X POST http://localhost:8051/mcp \\ - -H "Content-Type: application/json" \\ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "search_knowledge_base", - "arguments": { - "kb_name": "my-docs", - "query": "How do I configure authentication?", - "strategy": "hybrid" - } - } - }' - ``` - """) - - st.info("š” **Tip**: See the README for full MCP API documentation.") - -st.markdown("---") - -# Feature Highlights -st.markdown("## ⨠Feature Highlights") - -col1, col2 = st.columns(2) - -with col1: - st.markdown(""" -PostgreSQL + pgvector ensures your data is safe with ACID compliance, - transaction safety, and bulletproof persistence. Your data survives all - Docker operations and system restarts.
-Choose from multiple retrieval strategies: vector search, hybrid search - (BM25 + vector), reranking with cross-encoders, or agentic RAG with - sub-question decomposition.
-All data stays on your machine. No API keys, no cloud dependencies, - no telemetry. Perfect for sensitive codebases and proprietary documentation.
-First-class support for spec-driven development with Agent OS. - Ingest standards, workflows, and specs with specialized retrieval tools.
-Expose your knowledge bases to AI agents via Model Context Protocol. - 12 HTTP endpoints for seamless integration with Claude Desktop and other agents.
-Create isolated knowledge bases for different projects. Each KB has - its own type, metadata, and documents. Perfect for teams working on - multiple projects.
-Ready to get started?
-Use the sidebar to navigate to the main page and create your first knowledge base!
-Built with ā¤ļø for the local-first AI community
-Localized Multi-KB RAG System with MCP Integration
-š¢ Ollama Connected
', unsafe_allow_html=True) - if ollama_status.get("models"): - st.caption(f"Models: {', '.join(ollama_status['models'][:3])}") - else: - st.markdown('š“ Ollama Disconnected
', unsafe_allow_html=True) - st.caption(ollama_status.get("error", "Unknown error")) - - if chroma_status["status"] == "healthy": - st.markdown('š¢ PostgreSQL Connected
', unsafe_allow_html=True) - else: - st.markdown('š“ PostgreSQL Disconnected
', unsafe_allow_html=True) - st.caption(chroma_status.get("error", "Unknown error")) - - st.divider() - - # MCP Server Info - st.markdown("### š MCP Server") - mcp_url = f"http://localhost:{Config.MCP_SERVER_PORT}/mcp" - st.code(mcp_url, language="text") - - st.caption("**Connection Command:**") - st.code(f"claude mcp add --transport http code-forge {mcp_url}", language="bash") - - st.divider() - - # Knowledge Base Selector - st.markdown("### š Knowledge Base") - - pg_manager = get_pg_manager() - collections = pg_manager.list_collections() - - if collections: - # Optional type filter - type_summary = get_kb_type_summary() - filter_options = ["All Types"] + [ - f"{get_kb_type_info(KBType(kb_type)).icon} {get_kb_type_info(KBType(kb_type)).name} ({count})" - for kb_type, count in type_summary.items() if count > 0 - ] - - selected_filter = st.selectbox( - "Filter by Type", - options=filter_options, - key="kb_type_filter" - ) - - # Filter collections if needed - if selected_filter != "All Types": - # Extract the KB type value from the filter string - filter_type_value = None - for kb_type, count in type_summary.items(): - type_info = get_kb_type_info(KBType(kb_type)) - if f"{type_info.icon} {type_info.name}" in selected_filter: - filter_type_value = kb_type - break - - if filter_type_value: - collections = [ - col for col in collections - if col["metadata"].get("kb_type") == filter_type_value - ] - - # Build display options with type badges - kb_display_options = [] - kb_name_map = {} # Map display string to actual name - - for col in collections: - name = col["name"] - metadata = col["metadata"] - kb_type = KBType(metadata.get("kb_type", KBType.GENERIC.value)) - type_info = get_kb_type_info(kb_type) - - display_name = f"{type_info.icon} {name}" - kb_display_options.append(display_name) - kb_name_map[display_name] = name - - selected_display = st.selectbox( - "Select KB", - kb_display_options, - key="kb_selector" - ) - - # Map back to actual KB name - st.session_state.selected_kb = kb_name_map[selected_display] - - # Display selected KB metadata - selected_col = next( - (col for col in collections if col["name"] == st.session_state.selected_kb), - None - ) - - if selected_col: - metadata = selected_col["metadata"] - kb_type = KBType(metadata.get("kb_type", KBType.GENERIC.value)) - - # Show type badge - st.markdown(get_kb_type_badge(kb_type), unsafe_allow_html=True) - - # Show description if available - description = metadata.get("description", "") - if description: - st.caption(description) - else: - st.warning("No knowledge bases found. Create one below!") - st.session_state.selected_kb = None - - # Create new KB - with st.expander("ā Create New KB"): - new_kb_name = st.text_input("KB Name", key="new_kb_name", placeholder="my-project") - - # KB Type Selector - kb_type_choices = get_kb_type_choices() - selected_type_display = st.selectbox( - "KB Type", - options=kb_type_choices, - key="kb_type_selector", - help="Select the type of knowledge base to create" - ) - - # Extract the actual KBType from the display string - # Format is "š¤ Agent OS Profile" -> need to map back to KBType - type_map = {choice: kb_type for kb_type, choice in zip(KBType, kb_type_choices)} - selected_kb_type = type_map[selected_type_display] - - # Show type-specific info - type_info = get_kb_type_info(selected_kb_type) - st.caption(f"**{type_info.description}**") - - # Optional description - kb_description = st.text_area( - "Description (optional)", - key="kb_description", - placeholder="Brief description of this knowledge base...", - height=80 - ) - - if st.button("Create", type="primary"): - if new_kb_name: - if pg_manager.create_collection( - name=new_kb_name, - kb_type=selected_kb_type, - description=kb_description - ): - st.success(f"ā Created {type_info.icon} **{new_kb_name}** ({type_info.name})") - st.rerun() - else: - st.error("Failed to create KB") - else: - st.error("Please enter a KB name") - - st.divider() - - # RAG Strategy Toggles - st.markdown("### šÆ RAG Strategies") - - st.session_state.rag_settings["hybrid"] = st.checkbox( - "š Hybrid Search", - value=st.session_state.rag_settings["hybrid"], - help="**Hybrid Search**: Combines semantic vector search with BM25 keyword search for better recall. Uses reciprocal rank fusion to merge results. Best for queries with specific technical terms." - ) - - st.session_state.rag_settings["rerank"] = st.checkbox( - "šÆ Reranking", - value=st.session_state.rag_settings["rerank"], - help="**Reranking**: Re-scores retrieved chunks using a cross-encoder model (ms-marco-MiniLM) for higher precision. Keeps top 3 most relevant results. Improves answer quality but adds latency." - ) - - st.session_state.rag_settings["agentic"] = st.checkbox( - "š¤ Agentic RAG", - value=st.session_state.rag_settings["agentic"], - help="**Agentic RAG**: Breaks complex questions into sub-questions and answers them independently before synthesizing. Best for multi-part or analytical queries. Slower but more thorough." - ) - -# Main content area (right column) -with main_col: - # Main content tabs - tab1, tab2 = st.tabs(["š Knowledge Base", "š¬ Chat"]) - - # Tab 1: Knowledge Base Management - with tab1: - st.markdown("## š Knowledge Base Management") - - if not st.session_state.selected_kb: - st.info("š Please select or create a knowledge base from the left panel") - else: - st.markdown(f"### Current KB: **{st.session_state.selected_kb}**") - - st.markdown("---") - - # Document Upload - doc_col1, doc_col2 = st.columns(2, gap="large") - - with doc_col1: - st.markdown("#### š¤ Upload Documents") - st.caption("Choose files to upload to the knowledge base") - - # Initialize upload counter in session state - if "upload_counter" not in st.session_state: - st.session_state.upload_counter = 0 - - # Use dynamic key to reset file uploader after upload - uploaded_files = st.file_uploader( - "Choose files", - accept_multiple_files=True, - type=[ext.replace(".", "") for ext in Config.SUPPORTED_FILE_TYPES], - label_visibility="collapsed", - key=f"file_uploader_{st.session_state.upload_counter}" - ) - - if uploaded_files and st.button("Upload Files", key="upload_files_btn", use_container_width=True): - try: - Config.ensure_upload_dir() - progress_bar = st.progress(0) - status_container = st.container() - - for i, uploaded_file in enumerate(uploaded_files): - with status_container: - st.write(f"Processing: {uploaded_file.name}...") - - # Save file - file_path = Path(Config.UPLOAD_DIR) / uploaded_file.name - with open(file_path, "wb") as f: - f.write(uploaded_file.getbuffer()) - - # Ingest file - result = ingest_file( - str(file_path), - st.session_state.selected_kb, - uploaded_file.name - ) - - with status_container: - if result["status"] == "success": - st.success(f"ā {uploaded_file.name}: {result['chunks']} chunks") - else: - st.error(f"ā {uploaded_file.name}: {result.get('error', 'Unknown error')}") - - progress_bar.progress((i + 1) / len(uploaded_files)) - - with status_container: - st.success("š Upload complete! Refreshing document list...") - - # Increment counter to reset file uploader on next render - st.session_state.upload_counter += 1 - - # Wait a moment before rerun so user can see the success message - import time - time.sleep(1.5) - st.rerun() - - except Exception as e: - st.error(f"ā Upload failed: {str(e)}") - import traceback - st.code(traceback.format_exc()) - - with doc_col2: - st.markdown("#### š Import Directory") - st.caption("Import all supported files from a directory") - - st.info("š” **Tip:** Mount your directory as a volume in docker-compose.yml first. Paths are relative to `/workspace` in the container.") - - dir_path = st.text_input( - "Directory Path", - placeholder="/workspace/mounted_docs", - help="Path must exist inside the Docker container", - label_visibility="collapsed" - ) - - if st.button("Import Directory", key="import_dir_btn", use_container_width=True): - if not dir_path: - st.error("Please enter a directory path") - else: - with st.spinner("Importing files..."): - results = ingest_directory(dir_path, st.session_state.selected_kb) - - if not results: - st.warning("No supported files found in directory") - elif results[0].get("error") and "not found" in results[0].get("error", ""): - st.error(f"Directory not found: {dir_path}") - else: - success_count = sum(1 for r in results if r.get("status") == "success") - error_count = len(results) - success_count - - if success_count > 0: - st.success(f"ā Imported {success_count} file(s)") - if error_count > 0: - st.warning(f"ā ļø {error_count} file(s) failed") - - # Show first 5 results - for result in results[:5]: - if result.get("status") == "success": - st.caption(f"ā {result.get('filename', 'Unknown')}") - else: - st.caption(f"ā {result.get('filename', 'Unknown')}: {result.get('error', 'Unknown error')}") - - if len(results) > 5: - st.caption(f"... and {len(results) - 5} more") - - st.rerun() - - # Agent OS Profile Import (only for Agent OS KBs) - kb_metadata = pg_manager.get_collection_metadata(st.session_state.selected_kb) - kb_type_str = kb_metadata.get("kb_type", "generic") - - if kb_type_str == "agent-os": - st.markdown("---") - st.markdown("#### š¤ Import Agent OS Profile") - st.caption("Import an Agent OS profile directory with standards, agents, workflows, and commands") - - st.info("š” **Tip:** Mount your Agent OS profile directory as a volume in docker-compose.yml. The path should point to the profile root (containing standards/, agents/, etc.)") - - agent_os_path = st.text_input( - "Agent OS Profile Path", - placeholder="/workspace/my-agent-os-profile", - help="Path to Agent OS profile directory (must exist inside the Docker container)", - key="agent_os_path_input" - ) - - if st.button("Import Agent OS Profile", key="import_agent_os_btn", use_container_width=True): - if not agent_os_path: - st.error("Please enter an Agent OS profile path") - else: - with st.spinner("Parsing and ingesting Agent OS profile..."): - try: - agent_os_ingestion = AgentOSIngestion(pg_manager) - stats = agent_os_ingestion.ingest_profile( - kb_name=st.session_state.selected_kb, - profile_path=agent_os_path - ) - - if stats.get("success"): - st.success(f"ā Imported {stats['total_documents']} Agent OS documents!") - - # Show breakdown by type - st.markdown("**Documents by type:**") - for content_type, count in stats.get("documents_by_type", {}).items(): - type_emoji = { - "standard": "š", - "agent": "š¤", - "workflow": "š", - "command": "ā”", - "product": "šÆ", - "spec": "š" - }.get(content_type, "š") - st.caption(f"{type_emoji} {content_type}: {count}") - - st.rerun() - else: - st.error("ā Agent OS import failed") - if stats.get("errors"): - for error in stats["errors"]: - st.caption(f"ā ļø {error}") - - except Exception as e: - st.error(f"ā Agent OS import failed: {str(e)}") - import traceback - st.code(traceback.format_exc()) - - st.divider() - - # Agent OS Content Stats (only for Agent OS KBs) - if kb_type_str == "agent-os": - agent_os_ingestion = AgentOSIngestion(pg_manager) - agent_os_stats = agent_os_ingestion.get_profile_stats(st.session_state.selected_kb) - - if agent_os_stats.get("total_documents", 0) > 0: - st.markdown("#### š¤ Agent OS Content") - - # Display stats in columns - stat_cols = st.columns(len(agent_os_stats.get("documents_by_type", {})) or 1) - - type_info = { - "standard": {"emoji": "š", "name": "Standards"}, - "agent": {"emoji": "š¤", "name": "Agents"}, - "workflow": {"emoji": "š", "name": "Workflows"}, - "command": {"emoji": "ā”", "name": "Commands"}, - "product": {"emoji": "šÆ", "name": "Product"}, - "spec": {"emoji": "š", "name": "Specs"} - } - - for idx, (content_type, count) in enumerate(agent_os_stats.get("documents_by_type", {}).items()): - info = type_info.get(content_type, {"emoji": "š", "name": content_type.title()}) - with stat_cols[idx]: - st.metric( - label=f"{info['emoji']} {info['name']}", - value=count - ) - - st.divider() - - # Document Grid - st.markdown("#### š Documents") - - docs = get_documents_metadata(st.session_state.selected_kb) - - if not docs: - st.info("No documents in this knowledge base. Upload files to get started!") - else: - # Display in 3-column grid - cols = st.columns(3) - - for i, doc in enumerate(docs): - with cols[i % 3]: - # File icon based on type - icon_map = { - ".py": "š", ".js": "š", ".jsx": "āļø", - ".ts": "š", ".tsx": "āļø", ".md": "š", - ".pdf": "š", ".json": "š", ".yaml": "āļø" - } - icon = icon_map.get(doc["file_type"], "š") - - # Create card - tags_html = "".join([f'{tag}' for tag in doc["tags"]]) - - st.markdown(f""" -MCP Endpoint: http://localhost:8051/mcp
MCP Endpoints:
+http://localhost:8051/mcp
+ http://localhost:8051/mcp/kb/{''}
+ š” KB-Specific Endpoints (Recommended)
++ Each knowledge base has its own MCP endpoint that only exposes tools for that specific KB. + This keeps your Claude Desktop configuration organized and prevents accidental cross-KB queries. + Find the endpoint URL in the KB Management tab when you select a knowledge base. +
Option 1: Using CLI (Recommended)
-Run this command in your terminal:
+Option 1: KB-Specific (Recommended)
+Add individual knowledge bases using their slug:
npx @modelcontextprotocol/inspector http://localhost:8051/mcp
- Or add directly to Claude Desktop:
-claude mcp add code-forge http://localhost:8051/mcp
+ claude mcp add my-kb http://localhost:8051/mcp/kb/my-kb
+ The slug is shown in the KB Management tab. Examples: "My Docs" ā my-docs, "Agent OS" ā agent-os
+
Option 2: Manual Configuration
+Option 2: Global Endpoint
+Add all knowledge bases at once:
+claude mcp add code-forge http://localhost:8051/mcp
+
+ This exposes all KBs through a single endpoint with tools like search_knowledge_base that require a kb_name parameter.
+
Option 3: Manual Configuration
Edit your Claude Desktop config file:
{`{
"mcpServers": {
- "code-forge": {
- "url": "http://localhost:8051/mcp"
+ "my-kb": {
+ "url": "http://localhost:8051/mcp/kb/my-kb"
+ },
+ "another-kb": {
+ "url": "http://localhost:8051/mcp/kb/another-kb"
}
}
}`}
diff --git a/frontend/src/components/KBManagement.tsx b/frontend/src/components/KBManagement.tsx
index ff99922..e8da46f 100644
--- a/frontend/src/components/KBManagement.tsx
+++ b/frontend/src/components/KBManagement.tsx
@@ -1,23 +1,36 @@
import { useState, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
-import { FileText, BarChart3, Clock, Database, Upload, CheckCircle, XCircle, Info } from 'lucide-react';
+import { FileText, BarChart3, Clock, Database, Upload, CheckCircle, XCircle, Info, Copy, Link } from 'lucide-react';
import { getKBStats, listDocuments, uploadDocument, type KBStats } from '../lib/api';
interface KBManagementProps {
kbName: string;
+ kbSlug?: string;
kbType?: string;
}
-export default function KBManagement({ kbName, kbType }: KBManagementProps) {
+export default function KBManagement({ kbName, kbSlug, kbType }: KBManagementProps) {
const [selectedFiles, setSelectedFiles] = useState([]);
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'success' | 'error'>('idle');
const [uploadMessage, setUploadMessage] = useState('');
const [uploadProgress, setUploadProgress] = useState({ current: 0, total: 0 });
const [isBatchUpload, setIsBatchUpload] = useState(false);
+ const [copiedMCP, setCopiedMCP] = useState(false);
const fileInputRef = useRef(null);
const queryClient = useQueryClient();
+ // Generate KB-specific MCP endpoint using slug (fallback to name if no slug)
+ const slug = kbSlug || kbName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
+ const mcpEndpoint = `http://localhost:8051/mcp/kb/${slug}`;
+ const mcpCommand = `claude mcp add ${slug} ${mcpEndpoint}`;
+
+ const copyMCPEndpoint = () => {
+ navigator.clipboard.writeText(mcpEndpoint);
+ setCopiedMCP(true);
+ setTimeout(() => setCopiedMCP(false), 2000);
+ };
+
// Fetch KB stats
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['kb-stats', kbName],
@@ -174,12 +187,71 @@ export default function KBManagement({ kbName, kbType }: KBManagementProps) {
+ Connect this knowledge base to Claude Desktop using its dedicated MCP endpoint: +
+ +{mcpEndpoint}
+ Add to Claude Desktop:
+{mcpCommand}
+ + Run this command in your terminal, then restart Claude Desktop. +
++ š” Note: This endpoint only exposes tools for the {kbName} knowledge base. + You can add multiple KB-specific endpoints to Claude Desktop to keep them organized. +
+