diff --git a/Dockerfile 2 b/Dockerfile 2 new file mode 100644 index 0000000..df34608 --- /dev/null +++ b/Dockerfile 2 @@ -0,0 +1,23 @@ +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the application code +COPY biel_mcp_server.py . + +# Expose the hardcoded port +EXPOSE 7832 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:7832/sse || exit 1 + +# Run the server +CMD ["python", "biel_mcp_server.py"] \ No newline at end of file diff --git a/LICENSE 2.md b/LICENSE 2.md new file mode 100644 index 0000000..0d3e391 --- /dev/null +++ b/LICENSE 2.md @@ -0,0 +1,20 @@ +Copyright (c) 2025 TechDocs Studio + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README 2.md b/README 2.md new file mode 100644 index 0000000..df4fc80 --- /dev/null +++ b/README 2.md @@ -0,0 +1,87 @@ +
+ + + Biel.ai + +

Biel.ai MCP Server

+

Connect your IDE to your product docs

+
+ +[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/TechDocsStudio/biel-mcp)](https://archestra.ai/mcp-catalog/techdocsstudio__biel-mcp) + + +Give AI tools like Cursor, VS Code, and Claude Desktop access to your company's product knowledge through the [Biel.ai platform](https://biel.ai). + +Biel.ai provides a hosted Retrieval-Augmented Generation (RAG) layer that makes your documentation searchable and useful to AI tools. This enables smarter completions, accurate technical answers, and context-aware suggestions—directly in your IDE or chat environment. + +![Demo](./demo.png) + +When AI tools can read your product documentation, they become **significantly** more helpful—generating more accurate code completions, answering technical questions with context, and guiding developers with real-time product knowledge. + + +> **Note:** Requires a Biel.ai account and project setup. **[Start your free 15-day trial](https://app.biel.ai/accounts/signup/)**. + +

See quickstart instructions →

+ +## Getting started + +### 1. Get your MCP configuration + +```json +{ + "mcpServers": { + "biel-ai": { + "description": "Query your product's documentation, APIs, and knowledge base.", + "command": "npx", + "args": [ + "mcp-remote", + "https://mcp.biel.ai/sse?project_slug=YOUR_PROJECT_SLUG&domain=https://your-docs-domain.com" + ] + } + } +} +``` + +**Required:** `project_slug` and `domain` +**Optional:** `api_key` (only needed for private projects) + +### 2. Add to your AI tool + +* **Cursor**: **Settings** → **Tools & Integrations* → **New MCP server**. +* **Claude Desktop**: Edit `claude_desktop_config.json` +* **VS Code**: Install **MCP extension**. + +### 3. Start asking questions + +``` +Can you check in biel_ai what the auth headers are for the /users endpoint? +``` + +## Self-hosting (Optional) + +For advanced users who prefer to run their own MCP server instance: + +### Local development +```bash +# Clone and run locally +git clone https://github.com/techdocsStudio/biel-mcp +cd biel-mcp +pip install -r requirements.txt +python biel_mcp_server.py +``` + +### Docker deployment +```bash +# Docker Compose (recommended) +docker-compose up -d --build + +# Or Docker directly +docker build -t biel-mcp . +docker run -d -p 7832:7832 biel-mcp +``` + +## Support + +- **Issues**: [GitHub Issues](https://github.com/techdocsStudio/biel-mcp/issues) +- **Contact**: [support@biel.ai](mailto:support@biel.ai) +- **Custom Demo**: [Book a demo](https://biel.ai/contact) diff --git a/README.md b/README.md index 3d25fe3..f8b139a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ When AI tools can read your product documentation, they become **significantly**

See quickstart instructions →

## Getting started +[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/TechDocsStudio/biel-mcp)](https://archestra.ai/mcp-catalog/techdocsstudio__biel-mcp) ### 1. Get your MCP configuration diff --git a/biel_mcp_server 2.py b/biel_mcp_server 2.py new file mode 100644 index 0000000..9911b12 --- /dev/null +++ b/biel_mcp_server 2.py @@ -0,0 +1,406 @@ +""" +MCP Server for Biel.ai +Remote MCP server accessible via HTTP +Allows querying your AI from editors like Cursor via MCP over HTTP +""" + +import asyncio +import json +import logging +from typing import Dict, Optional, Any + +import httpx +import uvicorn +from fastapi import FastAPI, Request, Query +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from sse_starlette import EventSourceResponse + +# Constants +SERVER_VERSION = "0.2.0" +SERVER_NAME = "biel-ai-mcp" +DEFAULT_PORT = 7832 +DEFAULT_BASE_URL = "https://app.biel.ai" +BIEL_API_PATH = "/api/v1/chats" +MCP_PROTOCOL_VERSION = "2024-11-05" +REQUEST_TIMEOUT = 30.0 +KEEPALIVE_INTERVAL = 30 + +# Error codes +JSON_PARSE_ERROR = -32700 +UNKNOWN_METHOD_ERROR = -1 + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("biel-mcp") + +# Tool definitions +TOOLS = [ + { + "name": "biel_ai", + "description": "Query Biel.ai's specialized AI about code, SDKs and documentation", + "inputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Your question about code, SDK or documentation" + }, + "base_url": { + "type": "string", + "description": "Base URL of your Biel.ai instance", + "default": DEFAULT_BASE_URL + }, + "project_slug": { + "type": "string", + "description": "Project slug for your Biel.ai project" + }, + "api_key": { + "type": "string", + "description": "API key for authentication (optional)", + "default": "" + }, + "chat_uuid": { + "type": "string", + "description": "Chat UUID to continue conversation (optional)", + "default": "" + }, + "domain": { + "type": "string", + "description": "Domain URL to pass to Biel.ai as context (optional)", + "default": "app.biel.ai" + } + }, + "required": ["message"] + } + } +] + +# FastAPI app setup +app = FastAPI(title="Biel.ai MCP Server", version=SERVER_VERSION) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def create_error_response(message: str) -> Dict[str, str]: + """Create a standardized error response.""" + return {"type": "text", "text": f"Error: {message}"} + + +def create_success_response(text: str) -> Dict[str, str]: + """Create a standardized success response.""" + return {"type": "text", "text": text} + + +def validate_biel_request(arguments: Dict[str, Any]) -> Optional[str]: + """Validate Biel.ai request arguments. Returns error message if invalid, None if valid.""" + if not arguments.get("message", "").strip(): + return "Message cannot be empty" + + if not arguments.get("project_slug", "").strip(): + return "Project slug is required" + + return None + + +def format_biel_response(data: Dict[str, Any]) -> str: + """Format the response from Biel.ai API into a readable string.""" + ai_message = data.get("ai_message", {}) + ai_response = ai_message.get("message", "No response received") + chat_uuid = data.get("chat_uuid", "") + sources = ai_message.get("sources", []) + + response_parts = [f"🤖 **Biel.ai responds:**\n\n{ai_response}"] + + if sources: + response_parts.append("\n\n📚 **Sources consulted:**") + for source in sources: + response_parts.append(f"• [{source['title']}]({source['url']})") + + if chat_uuid: + response_parts.append(f"\n💬 *Chat UUID: {chat_uuid}* (to continue conversation)") + + return "\n".join(response_parts) + + +async def query_biel_ai(arguments: Dict[str, Any], defaults: Dict[str, str] = None) -> Dict[str, str]: + """Query Biel.ai API with the provided arguments.""" + # Apply defaults from connection if not provided in arguments + if defaults: + if not arguments.get("project_slug") and defaults.get("project_slug"): + arguments["project_slug"] = defaults["project_slug"] + + if not arguments.get("api_key") and defaults.get("api_key"): + arguments["api_key"] = defaults["api_key"] + + if not arguments.get("base_url") and defaults.get("base_url"): + arguments["base_url"] = defaults["base_url"] + + if not arguments.get("domain") and defaults.get("domain"): + arguments["domain"] = defaults["domain"] + + # Validate input + validation_error = validate_biel_request(arguments) + if validation_error: + return create_error_response(validation_error) + + # Extract arguments + message = arguments["message"] + base_url = arguments.get("base_url", DEFAULT_BASE_URL) + project_slug = arguments["project_slug"] + api_key = arguments.get("api_key", "") + chat_uuid = arguments.get("chat_uuid", "") + domain = arguments.get("domain", "") + + # Prepare request + payload = { + "message": message, + "project_slug": project_slug, + "url": domain if domain else base_url + } + + if chat_uuid: + payload["chat_uuid"] = chat_uuid + + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Api-Key {api_key}" + + full_url = f"{base_url.rstrip('/')}{BIEL_API_PATH}/" + + logger.info(f"Querying Biel.ai: {message[:50]}... (project: {project_slug})") + + try: + async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client: + response = await client.post(full_url, json=payload, headers=headers) + + if response.status_code in (200, 201): + data = response.json() + formatted_response = format_biel_response(data) + return create_success_response(formatted_response) + else: + error_msg = f"HTTP {response.status_code}: {response.text}" + logger.error(f"Biel.ai API error: {error_msg}") + return create_error_response(f"Biel.ai API error: {error_msg}") + + except httpx.TimeoutException: + logger.error("Timeout querying Biel.ai") + return create_error_response("⏱️ Timeout: Biel.ai took too long to respond") + except Exception as e: + logger.error(f"Unexpected error querying Biel.ai: {e}") + return create_error_response(f"Unexpected error: {str(e)}") + + +def create_mcp_response(msg_id: Optional[str], result: Optional[Dict] = None, + error: Optional[Dict] = None) -> Dict[str, Any]: + """Create a standardized MCP JSON-RPC response.""" + response = { + "jsonrpc": "2.0", + "id": msg_id + } + + if error: + response["error"] = error + else: + response["result"] = result or {} + + return response + + +async def handle_mcp_request(data: Dict[str, Any], defaults: Dict[str, str] = None) -> Dict[str, Any]: + """Handle MCP protocol messages.""" + try: + method = data.get("method") + msg_id = data.get("id") + + logger.info(f"Handling MCP request: {method}") + + if method == "initialize": + return create_mcp_response(msg_id, { + "protocolVersion": MCP_PROTOCOL_VERSION, + "capabilities": {"tools": {}}, + "serverInfo": { + "name": SERVER_NAME, + "version": SERVER_VERSION + } + }) + + elif method == "tools/list": + return create_mcp_response(msg_id, {"tools": TOOLS}) + + elif method == "tools/call": + params = data.get("params", {}) + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + if tool_name == "biel_ai": + result = await query_biel_ai(arguments, defaults) + return create_mcp_response(msg_id, {"content": [result]}) + else: + return create_mcp_response( + msg_id, + error={"code": UNKNOWN_METHOD_ERROR, "message": f"Unknown tool: {tool_name}"} + ) + + else: + return create_mcp_response( + msg_id, + error={"code": UNKNOWN_METHOD_ERROR, "message": f"Unknown method: {method}"} + ) + + except Exception as e: + logger.error(f"Error handling MCP message: {e}") + return create_mcp_response( + data.get("id") if isinstance(data, dict) else None, + error={"code": UNKNOWN_METHOD_ERROR, "message": str(e)} + ) + + +async def mcp_sse_generator(request: Request, message: Optional[str] = None, defaults: Dict[str, str] = None): + """Generate SSE events for MCP protocol.""" + try: + if message: + try: + mcp_request = json.loads(message) + logger.info(f"Processing MCP request: {mcp_request.get('method', 'unknown')}") + + response = await handle_mcp_request(mcp_request, defaults) + yield { + "event": "message", + "data": json.dumps(response) + } + + except json.JSONDecodeError: + yield { + "event": "message", + "data": json.dumps(create_mcp_response( + None, + error={"code": JSON_PARSE_ERROR, "message": "Parse error"} + )) + } + else: + # Send initial connection event + yield { + "event": "message", + "data": json.dumps({ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {} + }) + } + + # Keep connection alive + while True: + await asyncio.sleep(KEEPALIVE_INTERVAL) + yield {"event": "ping", "data": ""} + + except asyncio.CancelledError: + logger.info("SSE connection cancelled") + except Exception as e: + logger.error(f"SSE error: {e}") + + +# API Routes +@app.get("/") +async def health_check(): + """Health check endpoint.""" + return { + "status": "healthy", + "service": SERVER_NAME, + "version": SERVER_VERSION, + "usage": { + "endpoint": "/sse", + "query_params": { + "project_slug": "Your Biel.ai project slug", + "api_key": "Your API key (optional)", + "base_url": "Biel.ai instance URL (optional, defaults to https://app.biel.ai)", + "domain": "Domain URL to pass as context to Biel.ai (optional)" + }, + "example": "/sse?project_slug=your-slug&api_key=your-key&domain=https://example.com" + } + } + + +@app.get("/sse") +async def sse_endpoint( + request: Request, + message: Optional[str] = Query(None), + project_slug: Optional[str] = Query(None), + api_key: Optional[str] = Query(None), + base_url: Optional[str] = Query(None), + domain: Optional[str] = Query(None) +): + """MCP Server-Sent Events endpoint with query parameters for configuration.""" + # Build defaults from query parameters + defaults = {} + if project_slug: + defaults["project_slug"] = project_slug + if api_key: + defaults["api_key"] = api_key + if base_url: + defaults["base_url"] = base_url + if domain: + defaults["domain"] = domain + + return EventSourceResponse(mcp_sse_generator(request, message, defaults)) + + +@app.post("/sse") +async def sse_post_endpoint( + request: Request, + project_slug: Optional[str] = Query(None), + api_key: Optional[str] = Query(None), + base_url: Optional[str] = Query(None), + domain: Optional[str] = Query(None) +): + """Handle POST requests to SSE endpoint with query parameters for configuration.""" + # Build defaults from query parameters + defaults = {} + if project_slug: + defaults["project_slug"] = project_slug + if api_key: + defaults["api_key"] = api_key + if base_url: + defaults["base_url"] = base_url + if domain: + defaults["domain"] = domain + + try: + data = await request.json() + response = await handle_mcp_request(data, defaults) + return JSONResponse(response) + except Exception as e: + logger.error(f"Error handling POST to SSE: {e}") + return JSONResponse( + create_mcp_response(None, error={"code": UNKNOWN_METHOD_ERROR, "message": str(e)}), + status_code=500 + ) + + +@app.options("/sse") +async def sse_options(): + """Handle OPTIONS requests for CORS preflight.""" + return JSONResponse( + content={}, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization" + } + ) + + +if __name__ == "__main__": + logger.info(f"🚀 Starting {SERVER_NAME} server v{SERVER_VERSION} on port {DEFAULT_PORT}") + logger.info(f"🌐 Access via: http://localhost:{DEFAULT_PORT}/sse") + + uvicorn.run(app, host="0.0.0.0", port=DEFAULT_PORT) \ No newline at end of file diff --git a/demo 2.png b/demo 2.png new file mode 100644 index 0000000..db6471a Binary files /dev/null and b/demo 2.png differ diff --git a/docker-compose 2.yml b/docker-compose 2.yml new file mode 100644 index 0000000..38d77ca --- /dev/null +++ b/docker-compose 2.yml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + biel-mcp-server: + build: . + ports: + - "7832:7832" + environment: + - PYTHONUNBUFFERED=1 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7832/sse"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/example-mcp 2.json b/example-mcp 2.json new file mode 100644 index 0000000..e3b272f --- /dev/null +++ b/example-mcp 2.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "biel-ai": { + "command": "npx", + "args": [ + "mcp-remote", + "https://mcp.biel.ai/sse?project_slug=YOUR_PROJECT_SLUG&domain=https://your-docs-domain.com" + ], + "description": "Query your product's documentation, APIs, and knowledge base. Ask about API specs, guides, and troubleshooting." + } + } +} \ No newline at end of file diff --git a/logo 2.jpg b/logo 2.jpg new file mode 100644 index 0000000..8dfb31b Binary files /dev/null and b/logo 2.jpg differ diff --git a/logo-dark 2.jpg b/logo-dark 2.jpg new file mode 100644 index 0000000..01bbc84 Binary files /dev/null and b/logo-dark 2.jpg differ diff --git a/requirements 2.txt b/requirements 2.txt new file mode 100644 index 0000000..07a845f --- /dev/null +++ b/requirements 2.txt @@ -0,0 +1,4 @@ +mcp>=1.0.0 +httpx>=0.24.0 +uvicorn>=0.24.0 +fastapi>=0.104.0 \ No newline at end of file