-
Notifications
You must be signed in to change notification settings - Fork 5
feat: remote MCP server via Streamable HTTP + api_keys table (OPE-91) #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
DevanshuNEU
merged 4 commits into
OpenCodeIntel:main
from
DevanshuNEU:feat/remote-mcp-server
Mar 7, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9e9a0e4
feat: remote MCP server via Streamable HTTP + api_keys migration (OPE…
DevanshuNEU dd040c4
fix: review findings + CI pipeline for MCP server tests
DevanshuNEU b1edc43
fix: validate transport, add lint+coverage to MCP CI job
DevanshuNEU 34e33f8
fix: PORT range validation, MCP auth middleware, last_used_at tracking
DevanshuNEU File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,14 @@ | ||
| # Backend API Configuration | ||
| API_KEY=your-api-key-here | ||
| BACKEND_API_URL=http://localhost:8000 | ||
| BACKEND_API_URL=https://api.opencodeintel.com | ||
| API_KEY=ci_your-api-key-here | ||
|
|
||
| # Transport: "stdio" for local dev, "streamable-http" for remote | ||
| MCP_TRANSPORT=stdio | ||
|
|
||
| # Server host/port (only used for streamable-http transport) | ||
| MCP_HOST=0.0.0.0 | ||
| PORT=8080 | ||
|
|
||
| # Auth token for protecting /mcp endpoint (optional, recommended for remote) | ||
| # If set, clients must send Authorization: Bearer <token> | ||
| MCP_AUTH_TOKEN= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [flake8] | ||
| max-line-length = 120 | ||
|
|
||
| # Match backend config: only check for real bugs, not style. | ||
| select = E9,F,W6 | ||
|
|
||
| exclude = | ||
| .git, | ||
| __pycache__, | ||
| venv, | ||
| .venv | ||
|
|
||
| per-file-ignores = | ||
| # handlers.py imports formatters after logger (intentional grouping) | ||
| handlers.py:E402 | ||
| # Test files may import pytest for fixtures/marks even if not directly referenced | ||
| tests/*.py:F401 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # MCP Server Dockerfile - Remote Streamable HTTP deployment | ||
| FROM python:3.13-slim | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY requirements.txt . | ||
| RUN pip install --no-cache-dir -r requirements.txt | ||
|
|
||
| COPY . . | ||
|
|
||
| # Railway sets PORT automatically | ||
| ENV MCP_TRANSPORT=streamable-http | ||
| ENV MCP_HOST=0.0.0.0 | ||
|
|
||
| EXPOSE 8080 | ||
|
|
||
| RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ | ||
| CMD curl -f http://localhost:${PORT:-8080}/health || exit 1 | ||
|
|
||
| CMD ["python", "server.py"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,54 +1,119 @@ | ||
| #!/usr/bin/env python3 | ||
| """CodeIntel MCP Server entry point. | ||
|
|
||
| Provides codebase intelligence tools for LLMs via Model Context Protocol. | ||
| All tool definitions, handlers, and formatters are in their own modules. | ||
| Supports two transport modes: | ||
| stdio - Local dev, Claude Desktop config (default) | ||
| streamable-http - Remote deployment, Claude.ai Connectors | ||
|
|
||
| Usage: | ||
| python server.py # stdio (default) | ||
| python server.py --transport http # streamable HTTP on $PORT | ||
| """ | ||
| import asyncio | ||
| import sys | ||
|
|
||
| from mcp.server import Server | ||
| from mcp.server.models import InitializationOptions, ServerCapabilities | ||
| import mcp.server.stdio | ||
| from mcp.server.fastmcp import FastMCP | ||
| import mcp.types as types | ||
| from starlette.routing import Route | ||
| from starlette.responses import JSONResponse | ||
|
|
||
| from config import SERVER_NAME, SERVER_VERSION | ||
| from config import SERVER_NAME, SERVER_VERSION, TRANSPORT, HOST, PORT, MCP_AUTH_TOKEN | ||
| from tools import get_tool_schemas | ||
| from handlers import call_tool | ||
| from api_client import close_client | ||
|
|
||
| server = Server(SERVER_NAME) | ||
| mcp = FastMCP( | ||
| name=SERVER_NAME, | ||
| instructions=( | ||
| "CodeIntel provides semantic code search, dependency analysis, " | ||
| "and codebase intelligence. Use list_repositories first to find " | ||
| "repo IDs, then search_code or get_codebase_dna for context." | ||
| ), | ||
| host=HOST, | ||
| port=PORT, | ||
| streamable_http_path="/mcp", | ||
| stateless_http=True, | ||
| log_level="INFO", | ||
| ) | ||
|
|
||
| # Register tools at the low-level MCP server layer. | ||
| # FastMCP's @tool decorator infers schemas from function signatures, | ||
| # but we have well-tested schemas in tools.py and dispatch in handlers.py. | ||
| # We access the private _mcp_server to register custom inputSchema directly. | ||
| # TODO: monitor mcp-python for a public API to register tools with custom schemas | ||
| # (see: https://github.com/modelcontextprotocol/python-sdk/issues) | ||
| _server = mcp._mcp_server # pinned to mcp>=1.25.0,<2.0.0 | ||
|
|
||
|
|
||
| @server.list_tools() | ||
| @_server.list_tools() | ||
| async def handle_list_tools() -> list[types.Tool]: | ||
| """Return all available tool schemas.""" | ||
| return get_tool_schemas() | ||
|
|
||
|
|
||
| @server.call_tool() | ||
| @_server.call_tool() | ||
| async def handle_call_tool( | ||
| name: str, arguments: dict | None | ||
| ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: | ||
| """Dispatch tool calls to the handler layer.""" | ||
| return await call_tool(name, arguments) | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| """Run the MCP server over stdio transport.""" | ||
| try: | ||
| async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): | ||
| await server.run( | ||
| read_stream, | ||
| write_stream, | ||
| InitializationOptions( | ||
| server_name=SERVER_NAME, | ||
| server_version=SERVER_VERSION, | ||
| capabilities=ServerCapabilities(tools={}), | ||
| ), | ||
| ) | ||
| finally: | ||
| await close_client() | ||
| # Health check for Railway/load balancers | ||
| async def _health(request): | ||
| return JSONResponse({"status": "ok", "server": SERVER_NAME, "version": SERVER_VERSION}) | ||
|
|
||
|
|
||
| def _get_http_app(): | ||
| """Build the Starlette app with health check + MCP endpoint.""" | ||
| from starlette.middleware.base import BaseHTTPMiddleware | ||
| from starlette.requests import Request | ||
|
|
||
| app = mcp.streamable_http_app() | ||
| app.routes.insert(0, Route("/health", _health, methods=["GET"])) | ||
|
|
||
| if MCP_AUTH_TOKEN: | ||
| class MCPAuthMiddleware(BaseHTTPMiddleware): | ||
| """Require Bearer token on /mcp, leave /health public.""" | ||
| async def dispatch(self, request: Request, call_next): | ||
| if request.url.path == "/health": | ||
| return await call_next(request) | ||
| auth = request.headers.get("authorization", "") | ||
| if not auth.startswith("Bearer ") or auth[7:] != MCP_AUTH_TOKEN: | ||
| return JSONResponse({"error": "Unauthorized"}, status_code=401) | ||
| return await call_next(request) | ||
|
|
||
| app.add_middleware(MCPAuthMiddleware) | ||
|
|
||
| return app | ||
|
DevanshuNEU marked this conversation as resolved.
|
||
|
|
||
|
|
||
| _VALID_TRANSPORTS = {"stdio", "streamable-http", "http"} | ||
|
|
||
|
|
||
| def main(): | ||
| """Run with configured transport.""" | ||
| transport = TRANSPORT | ||
|
|
||
| # CLI override: --transport http | ||
| if "--transport" in sys.argv: | ||
| idx = sys.argv.index("--transport") | ||
| if idx + 1 < len(sys.argv): | ||
| transport = sys.argv[idx + 1] | ||
|
|
||
| # Normalize alias | ||
| if transport == "http": | ||
| transport = "streamable-http" | ||
|
|
||
| if transport not in ("stdio", "streamable-http"): | ||
| print(f"Error: unknown transport '{transport}'. Use 'stdio' or 'http'.") | ||
| sys.exit(1) | ||
|
|
||
| if transport == "streamable-http": | ||
| import uvicorn | ||
| print(f"Starting {SERVER_NAME} v{SERVER_VERSION} on {HOST}:{PORT}/mcp") | ||
| uvicorn.run(_get_http_app(), host=HOST, port=PORT) | ||
| else: | ||
| mcp.run(transport="stdio") | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| -- Migration: Create api_keys table for MCP authentication | ||
| -- Run this in Supabase SQL Editor | ||
| -- | ||
| -- API keys use the format ci_<hex> and are stored as SHA-256 hashes. | ||
| -- The backend validates keys by hashing the incoming token and looking | ||
| -- up the hash in this table. | ||
|
|
||
| CREATE TABLE IF NOT EXISTS api_keys ( | ||
| id uuid DEFAULT gen_random_uuid() PRIMARY KEY, | ||
| -- NULL user_id = system/service key (e.g. dogfood-mcp). | ||
| -- RLS policies using auth.uid() = user_id will NOT match these rows; | ||
| -- only the service_role (backend) can access system keys. | ||
| user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE, | ||
| name text NOT NULL, | ||
| key_hash text NOT NULL UNIQUE, | ||
| tier text DEFAULT 'free' CHECK (tier IN ('free', 'pro', 'enterprise')), | ||
| active boolean DEFAULT true, | ||
| created_at timestamptz DEFAULT now(), | ||
| last_used_at timestamptz | ||
|
DevanshuNEU marked this conversation as resolved.
|
||
| ); | ||
|
|
||
| -- Index for fast hash lookups during auth | ||
| CREATE INDEX IF NOT EXISTS idx_api_keys_hash_active | ||
| ON api_keys (key_hash) WHERE active = true; | ||
|
|
||
| -- Prevent users from modifying sensitive columns via UPDATE. | ||
| -- Only active and last_used_at can change; key_hash, tier, name are immutable. | ||
| -- Service role (backend/admin) bypasses this check. | ||
| CREATE OR REPLACE FUNCTION protect_api_key_immutable_cols() | ||
| RETURNS TRIGGER AS $fn$ | ||
| BEGIN | ||
| IF current_setting('role', true) = 'service_role' THEN | ||
| RETURN NEW; | ||
| END IF; | ||
| IF NEW.key_hash IS DISTINCT FROM OLD.key_hash THEN | ||
| RAISE EXCEPTION 'Cannot modify key_hash'; | ||
| END IF; | ||
| IF NEW.tier IS DISTINCT FROM OLD.tier THEN | ||
| RAISE EXCEPTION 'Cannot modify tier'; | ||
| END IF; | ||
| IF NEW.name IS DISTINCT FROM OLD.name THEN | ||
| RAISE EXCEPTION 'Cannot modify name'; | ||
| END IF; | ||
| RETURN NEW; | ||
| END; | ||
| $fn$ LANGUAGE plpgsql; | ||
|
|
||
| CREATE TRIGGER api_keys_immutable_guard | ||
| BEFORE UPDATE ON api_keys | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION protect_api_key_immutable_cols(); | ||
|
|
||
| -- Enable RLS | ||
| ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| CREATE POLICY "Users can view own keys" | ||
| ON api_keys FOR SELECT | ||
| USING (auth.uid() = user_id); | ||
|
|
||
| CREATE POLICY "Users can create own keys" | ||
| ON api_keys FOR INSERT | ||
| WITH CHECK (auth.uid() = user_id); | ||
|
|
||
| CREATE POLICY "Users can deactivate own keys" | ||
| ON api_keys FOR UPDATE | ||
| USING (auth.uid() = user_id) | ||
| WITH CHECK (auth.uid() = user_id); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| -- No DELETE policy: users cannot hard-delete keys. | ||
| -- Deactivation (active=false) is the intended revocation mechanism. | ||
| -- Service role (backend) can hard-delete for compliance if needed. | ||
| -- Service role has full access via service_role key; bypasses RLS. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.