Skip to content

Commit 4075699

Browse files
aksOpsclaude
andcommitted
Add unified REST API + MCP server on a single port
- FastAPI server with /api (REST), /mcp (MCP via fastmcp), / (welcome UI) - Shared CodeIQService layer wrapping GraphStore, FlowEngine, GraphQuery - 20 MCP tools: 15 core graph queries + 5 agentic triage tools (find_component_by_file, trace_impact, find_related_endpoints, search_graph, read_file) - 22 REST endpoints with pagination, filtering, and error handling - Auth middleware stub for future authentication - CLI: `code-iq serve [path] --port 8080` - Streamable HTTP transport for MCP (no SSE) - 26 new tests, all 1688 tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8d5822a commit 4075699

11 files changed

Lines changed: 1183 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ dev = [
3030
]
3131
kuzu = ["kuzu>=0.6"]
3232
all-backends = ["kuzu>=0.6"]
33+
server = [
34+
"fastapi>=0.115",
35+
"uvicorn[standard]>=0.34",
36+
"fastmcp>=2.0",
37+
]
3338

3439
[project.scripts]
3540
code-iq = "code_intelligence.cli:app"
@@ -41,7 +46,7 @@ code-intelligence = "code_intelligence.cli:app"
4146
where = ["src"]
4247

4348
[tool.setuptools.package-data]
44-
code_intelligence = ["flow/templates/*.html", "flow/vendor/*.js"]
49+
code_intelligence = ["flow/templates/*.html", "flow/vendor/*.js", "server/templates/*.html"]
4550

4651
[tool.pytest.ini_options]
4752
testpaths = ["tests"]

src/code_intelligence/cli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,5 +684,34 @@ def flow(
684684
store.close()
685685

686686

687+
@app.command()
688+
def serve(
689+
path: Annotated[Path, typer.Argument(help="Path to the codebase")] = Path("."),
690+
port: Annotated[int, typer.Option("--port", "-p", help="Port to listen on")] = 8080,
691+
host: Annotated[str, typer.Option("--host", help="Host to bind to")] = "0.0.0.0",
692+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "networkx",
693+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
694+
) -> None:
695+
"""Start the Code IQ server (API + MCP on one port)."""
696+
try:
697+
import uvicorn
698+
except ImportError:
699+
console.print("Server dependencies not installed. Run: pip install code-intelligence[server]")
700+
raise typer.Exit(1)
701+
from code_intelligence.server.app import create_app
702+
703+
console.print(f"[bold]Code IQ Server[/bold]")
704+
console.print(f" Codebase: {path.resolve()}")
705+
console.print(f" Backend: {backend}")
706+
console.print(f" API docs: http://{host}:{port}/docs")
707+
console.print(f" MCP: http://{host}:{port}/mcp")
708+
console.print()
709+
710+
application = create_app(
711+
codebase_path=path.resolve(), backend=backend, config_path=config
712+
)
713+
uvicorn.run(application, host=host, port=port)
714+
715+
687716
if __name__ == "__main__":
688717
app()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Code IQ server — unified REST API + MCP on a single port."""
2+
3+
from __future__ import annotations
4+
5+
from code_intelligence.server.app import create_app
6+
7+
__all__ = ["create_app"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""FastAPI application assembly — mounts REST API, MCP server, and welcome page."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from fastapi import FastAPI
8+
from fastapi.responses import HTMLResponse
9+
10+
from code_intelligence.server.middleware import AuthMiddleware
11+
from code_intelligence.server.mcp_server import get_mcp_app, set_service
12+
from code_intelligence.server.routes import create_router
13+
from code_intelligence.server.service import CodeIQService
14+
15+
16+
def create_app(
17+
codebase_path: Path = Path("."),
18+
backend: str = "networkx",
19+
config_path: Path | None = None,
20+
) -> FastAPI:
21+
"""Create and configure the unified Code IQ server."""
22+
service = CodeIQService(
23+
path=codebase_path, backend=backend, config_path=config_path
24+
)
25+
26+
# Set up MCP server
27+
set_service(service)
28+
mcp_app = get_mcp_app()
29+
30+
# Create FastAPI with MCP lifespan
31+
app = FastAPI(
32+
title="Code IQ",
33+
description="Code Intelligence — graph queries, flow diagrams, and codebase analysis",
34+
lifespan=mcp_app.lifespan,
35+
)
36+
37+
# Auth middleware stub (no-op, ready for future auth)
38+
app.add_middleware(AuthMiddleware)
39+
40+
# Mount MCP at /mcp (streamable HTTP)
41+
app.mount("/mcp", mcp_app)
42+
43+
# Include REST routes at /api
44+
router = create_router(service)
45+
app.include_router(router)
46+
47+
# Welcome page at /
48+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
49+
async def welcome():
50+
template_path = Path(__file__).parent / "templates" / "welcome.html"
51+
return HTMLResponse(template_path.read_text(encoding="utf-8"))
52+
53+
return app
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""MCP server tools for Code IQ."""
2+
from __future__ import annotations
3+
4+
import json
5+
6+
from fastmcp import FastMCP
7+
8+
mcp = FastMCP(
9+
"Code IQ",
10+
instructions="Code intelligence graph query tools for exploring a codebase's architecture. "
11+
"Use these tools to query nodes, edges, find components, trace impact, and generate flow diagrams.",
12+
)
13+
14+
_service = None # Set during app startup
15+
16+
17+
def set_service(svc) -> None:
18+
global _service
19+
_service = svc
20+
21+
22+
def _svc():
23+
if _service is None:
24+
raise RuntimeError("Service not initialized")
25+
return _service
26+
27+
28+
def get_mcp_app():
29+
"""Return the MCP ASGI app for mounting into FastAPI."""
30+
return mcp.http_app(path="/", transport="streamable-http")
31+
32+
33+
# ── Core tools ───────────────────────────────────────────────────────────────
34+
35+
36+
@mcp.tool()
37+
def get_stats() -> str:
38+
"""Get project graph statistics — node counts, edge counts, backend info."""
39+
return json.dumps(_svc().get_stats(), indent=2)
40+
41+
42+
@mcp.tool()
43+
def query_nodes(kind: str | None = None, limit: int = 50) -> str:
44+
"""Query nodes in the code graph. Filter by kind (endpoint, entity, guard, class, method, component, module, etc.)."""
45+
return json.dumps(_svc().list_nodes(kind=kind, limit=limit, offset=0), indent=2)
46+
47+
48+
@mcp.tool()
49+
def query_edges(kind: str | None = None, limit: int = 50) -> str:
50+
"""Query edges in the code graph. Filter by kind (calls, imports, depends_on, queries, protects, etc.)."""
51+
return json.dumps(_svc().list_edges(kind=kind, limit=limit, offset=0), indent=2)
52+
53+
54+
@mcp.tool()
55+
def get_node_neighbors(node_id: str, direction: str = "both") -> str:
56+
"""Get all nodes connected to a given node. Direction: both, in, out."""
57+
return json.dumps(
58+
_svc().get_neighbors(node_id, direction=direction, edge_kinds=None), indent=2
59+
)
60+
61+
62+
@mcp.tool()
63+
def get_ego_graph(center: str, radius: int = 2) -> str:
64+
"""Get the subgraph within N hops of a center node. Returns all nodes and edges in the neighborhood."""
65+
return json.dumps(
66+
_svc().get_ego(center, radius=radius, edge_kinds=None), indent=2
67+
)
68+
69+
70+
@mcp.tool()
71+
def find_cycles(limit: int = 100) -> str:
72+
"""Find circular dependency cycles in the graph."""
73+
return json.dumps(_svc().find_cycles(limit=limit), indent=2)
74+
75+
76+
@mcp.tool()
77+
def find_shortest_path(source: str, target: str) -> str:
78+
"""Find the shortest path between two nodes."""
79+
result = _svc().shortest_path(source, target)
80+
if result is None:
81+
return json.dumps({"error": f"No path found between {source} and {target}"}, indent=2)
82+
return json.dumps(result, indent=2)
83+
84+
85+
@mcp.tool()
86+
def find_consumers(target_id: str) -> str:
87+
"""Find nodes that consume from a target (CONSUMES/LISTENS edges)."""
88+
return json.dumps(_svc().consumers_of(target_id), indent=2)
89+
90+
91+
@mcp.tool()
92+
def find_producers(target_id: str) -> str:
93+
"""Find nodes that produce to a target (PRODUCES/PUBLISHES edges)."""
94+
return json.dumps(_svc().producers_of(target_id), indent=2)
95+
96+
97+
@mcp.tool()
98+
def find_callers(target_id: str) -> str:
99+
"""Find nodes that call a target (CALLS edges)."""
100+
return json.dumps(_svc().callers_of(target_id), indent=2)
101+
102+
103+
@mcp.tool()
104+
def find_dependencies(module_id: str) -> str:
105+
"""Find modules that a given module depends on."""
106+
return json.dumps(_svc().dependencies_of(module_id), indent=2)
107+
108+
109+
@mcp.tool()
110+
def find_dependents(module_id: str) -> str:
111+
"""Find modules that depend on a given module."""
112+
return json.dumps(_svc().dependents_of(module_id), indent=2)
113+
114+
115+
@mcp.tool()
116+
def generate_flow(view: str = "overview", format: str = "json") -> str:
117+
"""Generate an architecture flow diagram. Views: overview, ci, deploy, runtime, auth. Formats: json, mermaid."""
118+
return json.dumps(_svc().generate_flow(view, format=format), indent=2)
119+
120+
121+
@mcp.tool()
122+
def analyze_codebase(incremental: bool = True) -> str:
123+
"""Trigger codebase analysis. Scans files, runs detectors, builds the code graph."""
124+
try:
125+
result = _svc().run_analysis(incremental)
126+
return json.dumps(result, indent=2)
127+
except Exception as exc:
128+
return json.dumps({"error": str(exc)}, indent=2)
129+
130+
131+
@mcp.tool()
132+
def run_cypher(query: str) -> str:
133+
"""Execute a raw Cypher query (requires KuzuDB backend)."""
134+
try:
135+
result = _svc().query_cypher(query, None)
136+
return json.dumps(result, indent=2)
137+
except ValueError as exc:
138+
return json.dumps({"error": str(exc)}, indent=2)
139+
140+
141+
# ── Agentic triage tools ────────────────────────────────────────────────────
142+
143+
144+
@mcp.tool()
145+
def find_component_by_file(file_path: str) -> str:
146+
"""Given a file path (e.g. from a stacktrace), find the component/module it belongs to, its layer, and all connected nodes. Use this to map stack traces to architecture."""
147+
return json.dumps(_svc().find_component_by_file(file_path), indent=2)
148+
149+
150+
@mcp.tool()
151+
def trace_impact(node_id: str, depth: int = 3) -> str:
152+
"""Trace downstream impact of a node — what depends on it, what breaks if it fails. Returns all transitively affected nodes."""
153+
return json.dumps(_svc().trace_impact(node_id, depth=depth), indent=2)
154+
155+
156+
@mcp.tool()
157+
def find_related_endpoints(identifier: str) -> str:
158+
"""Given a file, class, or entity name, find all API endpoints that interact with it. Useful for mapping business operations to code."""
159+
return json.dumps(_svc().find_related_endpoints(identifier), indent=2)
160+
161+
162+
@mcp.tool()
163+
def search_graph(query: str, limit: int = 20) -> str:
164+
"""Free-text search across node labels, IDs, and properties. Find components by name or keyword."""
165+
return json.dumps(_svc().search_graph(query, limit=limit), indent=2)
166+
167+
168+
@mcp.tool()
169+
def read_file(file_path: str) -> str:
170+
"""Read a source file's content for deep analysis. Path is relative to the codebase root."""
171+
try:
172+
return _svc().read_file(file_path)
173+
except ValueError as exc:
174+
return f"Error: {exc}"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Authentication middleware stub for Code IQ server."""
2+
from __future__ import annotations
3+
4+
from starlette.middleware.base import BaseHTTPMiddleware
5+
from starlette.requests import Request
6+
from starlette.responses import Response
7+
8+
9+
class AuthMiddleware(BaseHTTPMiddleware):
10+
"""No-op auth middleware. Replace dispatch logic to add authentication."""
11+
12+
async def dispatch(self, request: Request, call_next):
13+
# Future: validate request.headers.get("Authorization")
14+
# request.state.user = validated_user
15+
response = await call_next(request)
16+
return response

0 commit comments

Comments
 (0)