sarveshpatel commited on
Commit
ca9559a
Β·
verified Β·
1 Parent(s): 612b769

Update app/routes/mcp.py

Browse files
Files changed (1) hide show
  1. app/routes/mcp.py +233 -55
app/routes/mcp.py CHANGED
@@ -1,9 +1,10 @@
1
- """MCP protocol endpoints - SSE and JSON-RPC style."""
2
 
3
  from __future__ import annotations
4
 
5
  import json
6
  import logging
 
7
  from typing import Any
8
 
9
  from fastapi import APIRouter, Request
@@ -17,16 +18,244 @@ logger = logging.getLogger(__name__)
17
 
18
  router = APIRouter(prefix="/mcp", tags=["mcp"])
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  @router.get("/tools")
22
  async def list_tools() -> MCPListToolsResponse:
23
- """List all available MCP tools."""
24
  return MCPListToolsResponse(tools=TOOL_DEFINITIONS)
25
 
26
 
27
  @router.post("/execute", response_model=MCPToolResult)
28
  async def execute_tool(tool_input: MCPToolInput) -> MCPToolResult:
29
- """Execute an MCP tool call."""
30
  logger.info("MCP tool call: %s", tool_input.name)
31
  result = await handle_tool_call(tool_input.name, tool_input.arguments)
32
  return result
@@ -54,55 +283,4 @@ async def execute_tool_stream(tool_input: MCPToolInput) -> EventSourceResponse:
54
  "data": json.dumps({"status": "completed"}),
55
  }
56
 
57
- return EventSourceResponse(event_generator())
58
-
59
-
60
- # ─── JSON-RPC 2.0 Compatible Endpoint ─────────────────────────────
61
-
62
- @router.post("/jsonrpc")
63
- async def jsonrpc_handler(request: Request) -> JSONResponse:
64
- """Handle JSON-RPC 2.0 style MCP requests."""
65
- try:
66
- body = await request.json()
67
- except Exception:
68
- return JSONResponse(
69
- status_code=400,
70
- content={
71
- "jsonrpc": "2.0",
72
- "error": {"code": -32700, "message": "Parse error"},
73
- "id": None,
74
- },
75
- )
76
-
77
- request_id = body.get("id")
78
- method = body.get("method", "")
79
- params = body.get("params", {})
80
-
81
- if method == "tools/list":
82
- return JSONResponse(
83
- content={
84
- "jsonrpc": "2.0",
85
- "result": {"tools": TOOL_DEFINITIONS},
86
- "id": request_id,
87
- }
88
- )
89
- elif method == "tools/call":
90
- tool_name = params.get("name", "")
91
- tool_args = params.get("arguments", {})
92
- result = await handle_tool_call(tool_name, tool_args)
93
- return JSONResponse(
94
- content={
95
- "jsonrpc": "2.0",
96
- "result": result.model_dump(),
97
- "id": request_id,
98
- }
99
- )
100
- else:
101
- return JSONResponse(
102
- status_code=400,
103
- content={
104
- "jsonrpc": "2.0",
105
- "error": {"code": -32601, "message": f"Method not found: {method}"},
106
- "id": request_id,
107
- },
108
- )
 
1
+ """MCP protocol endpoints - Full MCP 2025-03-26 Streamable HTTP + legacy SSE."""
2
 
3
  from __future__ import annotations
4
 
5
  import json
6
  import logging
7
+ import uuid
8
  from typing import Any
9
 
10
  from fastapi import APIRouter, Request
 
18
 
19
  router = APIRouter(prefix="/mcp", tags=["mcp"])
20
 
21
+ # ─── MCP Server Info ──────────────────────────────────────────
22
+
23
+ MCP_SERVER_INFO = {
24
+ "name": "mcp-code-executor",
25
+ "version": "1.0.0",
26
+ }
27
+
28
+ MCP_CAPABILITIES = {
29
+ "tools": {
30
+ "listChanged": False,
31
+ },
32
+ }
33
+
34
+ SUPPORTED_PROTOCOL_VERSIONS = [
35
+ "2025-03-26",
36
+ "2024-11-05",
37
+ ]
38
+
39
+
40
+ # ─── Helper: Build JSON-RPC Response ──────────────────────────
41
+
42
+ def jsonrpc_response(result: Any, request_id: Any) -> dict:
43
+ return {
44
+ "jsonrpc": "2.0",
45
+ "result": result,
46
+ "id": request_id,
47
+ }
48
+
49
+
50
+ def jsonrpc_error(code: int, message: str, request_id: Any, data: Any = None) -> dict:
51
+ err = {"code": code, "message": message}
52
+ if data is not None:
53
+ err["data"] = data
54
+ return {
55
+ "jsonrpc": "2.0",
56
+ "error": err,
57
+ "id": request_id,
58
+ }
59
+
60
+
61
+ # ─── Streamable HTTP Endpoint (MCP 2025-03-26) ────────────────
62
+ # Groq and other MCP clients POST JSON-RPC messages here.
63
+ # This single endpoint handles initialize, tools/list, tools/call,
64
+ # ping, and notifications.
65
+
66
+ @router.post("/jsonrpc")
67
+ async def mcp_jsonrpc_endpoint(request: Request) -> JSONResponse:
68
+ """
69
+ Full MCP Streamable HTTP transport endpoint.
70
+ Handles the complete MCP JSON-RPC protocol including:
71
+ - initialize / initialized
72
+ - tools/list
73
+ - tools/call
74
+ - ping
75
+ - notifications (no response needed)
76
+ """
77
+ try:
78
+ body = await request.json()
79
+ except Exception:
80
+ return JSONResponse(
81
+ status_code=400,
82
+ content=jsonrpc_error(-32700, "Parse error", None),
83
+ )
84
+
85
+ # Handle batch requests
86
+ if isinstance(body, list):
87
+ responses = []
88
+ for item in body:
89
+ resp = await _handle_single_jsonrpc(item)
90
+ if resp is not None: # Notifications don't get responses
91
+ responses.append(resp)
92
+ if not responses:
93
+ return JSONResponse(status_code=204, content=None)
94
+ return JSONResponse(content=responses)
95
+
96
+ # Single request
97
+ result = await _handle_single_jsonrpc(body)
98
+ if result is None:
99
+ # Notification - no response
100
+ return JSONResponse(status_code=204, content=None)
101
+ return JSONResponse(content=result)
102
+
103
+
104
+ @router.get("/jsonrpc")
105
+ async def mcp_jsonrpc_sse_get(request: Request) -> EventSourceResponse:
106
+ """
107
+ SSE GET endpoint for MCP Streamable HTTP transport.
108
+ Some clients open a GET to receive server-initiated messages.
109
+ We keep it open as a heartbeat/keepalive.
110
+ """
111
+ async def event_generator():
112
+ # Send an initial endpoint event (for SSE transport compatibility)
113
+ yield {
114
+ "event": "endpoint",
115
+ "data": "/mcp/jsonrpc",
116
+ }
117
+ # Keep connection alive
118
+ import asyncio
119
+ try:
120
+ while True:
121
+ await asyncio.sleep(30)
122
+ yield {
123
+ "event": "ping",
124
+ "data": "{}",
125
+ }
126
+ except asyncio.CancelledError:
127
+ pass
128
+
129
+ return EventSourceResponse(event_generator())
130
+
131
+
132
+ @router.delete("/jsonrpc")
133
+ async def mcp_jsonrpc_delete():
134
+ """Handle session termination."""
135
+ return JSONResponse(status_code=200, content={"status": "session_terminated"})
136
+
137
+
138
+ async def _handle_single_jsonrpc(body: dict) -> dict | None:
139
+ """Process a single JSON-RPC message."""
140
+ if not isinstance(body, dict):
141
+ return jsonrpc_error(-32600, "Invalid Request", None)
142
+
143
+ method = body.get("method", "")
144
+ request_id = body.get("id") # None for notifications
145
+ params = body.get("params", {})
146
+
147
+ logger.info("MCP JSON-RPC method: %s (id=%s)", method, request_id)
148
+
149
+ # ── Notifications (no id = no response) ──
150
+ if request_id is None:
151
+ if method == "notifications/initialized":
152
+ logger.info("Client sent initialized notification")
153
+ elif method == "notifications/cancelled":
154
+ logger.info("Client cancelled request: %s", params)
155
+ else:
156
+ logger.info("Received notification: %s", method)
157
+ return None
158
+
159
+ # ── Requests (have id = need response) ──
160
+
161
+ if method == "initialize":
162
+ return _handle_initialize(params, request_id)
163
+
164
+ elif method == "ping":
165
+ return jsonrpc_response({}, request_id)
166
+
167
+ elif method == "tools/list":
168
+ return _handle_tools_list(params, request_id)
169
+
170
+ elif method == "tools/call":
171
+ return await _handle_tools_call(params, request_id)
172
+
173
+ elif method == "resources/list":
174
+ return jsonrpc_response({"resources": []}, request_id)
175
+
176
+ elif method == "resources/templates/list":
177
+ return jsonrpc_response({"resourceTemplates": []}, request_id)
178
+
179
+ elif method == "prompts/list":
180
+ return jsonrpc_response({"prompts": []}, request_id)
181
+
182
+ elif method == "completion/complete":
183
+ return jsonrpc_response({"completion": {"values": []}}, request_id)
184
+
185
+ else:
186
+ return jsonrpc_error(-32601, f"Method not found: {method}", request_id)
187
+
188
+
189
+ def _handle_initialize(params: dict, request_id: Any) -> dict:
190
+ """Handle MCP initialize handshake."""
191
+ client_protocol = params.get("protocolVersion", "2024-11-05")
192
+ client_info = params.get("clientInfo", {})
193
+
194
+ logger.info(
195
+ "MCP Initialize: client=%s, protocol=%s",
196
+ client_info.get("name", "unknown"),
197
+ client_protocol,
198
+ )
199
+
200
+ # Negotiate protocol version
201
+ if client_protocol in SUPPORTED_PROTOCOL_VERSIONS:
202
+ negotiated_version = client_protocol
203
+ else:
204
+ # Fall back to our latest supported
205
+ negotiated_version = SUPPORTED_PROTOCOL_VERSIONS[0]
206
+
207
+ return jsonrpc_response(
208
+ {
209
+ "protocolVersion": negotiated_version,
210
+ "capabilities": MCP_CAPABILITIES,
211
+ "serverInfo": MCP_SERVER_INFO,
212
+ },
213
+ request_id,
214
+ )
215
+
216
+
217
+ def _handle_tools_list(params: dict, request_id: Any) -> dict:
218
+ """Handle tools/list request."""
219
+ # MCP supports pagination via cursor, but we return all tools at once
220
+ return jsonrpc_response(
221
+ {"tools": TOOL_DEFINITIONS},
222
+ request_id,
223
+ )
224
+
225
+
226
+ async def _handle_tools_call(params: dict, request_id: Any) -> dict:
227
+ """Handle tools/call request."""
228
+ tool_name = params.get("name", "")
229
+ tool_args = params.get("arguments", {})
230
+
231
+ if not tool_name:
232
+ return jsonrpc_error(
233
+ -32602, "Invalid params: 'name' is required", request_id
234
+ )
235
+
236
+ logger.info("MCP tools/call: %s", tool_name)
237
+ result = await handle_tool_call(tool_name, tool_args)
238
+
239
+ return jsonrpc_response(
240
+ {
241
+ "content": result.content,
242
+ "isError": result.isError,
243
+ },
244
+ request_id,
245
+ )
246
+
247
+
248
+ # ─── Legacy REST Endpoints (non-MCP clients) ──────────────────
249
 
250
  @router.get("/tools")
251
  async def list_tools() -> MCPListToolsResponse:
252
+ """List all available MCP tools (simple REST)."""
253
  return MCPListToolsResponse(tools=TOOL_DEFINITIONS)
254
 
255
 
256
  @router.post("/execute", response_model=MCPToolResult)
257
  async def execute_tool(tool_input: MCPToolInput) -> MCPToolResult:
258
+ """Execute an MCP tool call (simple REST)."""
259
  logger.info("MCP tool call: %s", tool_input.name)
260
  result = await handle_tool_call(tool_input.name, tool_input.arguments)
261
  return result
 
283
  "data": json.dumps({"status": "completed"}),
284
  }
285
 
286
+ return EventSourceResponse(event_generator())