galcan commited on
Commit
65d9f7d
·
1 Parent(s): 28d475f
Files changed (1) hide show
  1. app.py +67 -100
app.py CHANGED
@@ -7,14 +7,13 @@ Hosted on Hugging Face Spaces with HTTP transport
7
  import json
8
  import asyncio
9
  import logging
 
10
  from typing import Any, Dict, List, Optional
11
- from fastapi import FastAPI, HTTPException
12
  from fastapi.middleware.cors import CORSMiddleware
13
  from fastapi.responses import StreamingResponse
14
  from pydantic import BaseModel
15
  import uvicorn
16
- import asyncio
17
- import json
18
 
19
  # Configure logging
20
  logging.basicConfig(level=logging.INFO)
@@ -36,6 +35,9 @@ app.add_middleware(
36
  chunks_data = None
37
  docs_data = None
38
 
 
 
 
39
  def load_data():
40
  """Load the documentation chunks and metadata"""
41
  global chunks_data, docs_data
@@ -321,109 +323,73 @@ async def list_docs():
321
 
322
  return {"documents": docs_data}
323
 
324
- @app.get("/mcp/stream")
325
- async def mcp_sse():
326
- """MCP SSE endpoint for mcp-remote"""
327
- async def event_generator():
328
- # Send MCP initialization response (correct format from docs)
329
- init_response = {
330
- "jsonrpc": "2.0",
331
- "id": 1,
332
- "result": {
333
- "protocolVersion": "2025-06-18",
334
- "capabilities": {
335
- "tools": {
336
- "listChanged": True
337
- },
338
- "resources": {}
339
- },
340
- "serverInfo": {
341
- "name": "mcp-docs-server",
342
- "version": "1.0.0"
343
- }
344
- }
345
  }
346
- yield f"data: {json.dumps(init_response)}\n\n"
 
 
 
 
 
 
 
347
 
348
- # Send tools list response (correct format from docs)
349
- tools_response = {
350
- "jsonrpc": "2.0",
351
- "id": 2,
352
- "result": {
353
- "tools": [
354
- {
355
- "name": "search_docs",
356
- "title": "Search Documentation",
357
- "description": "Search through MCP documentation chunks",
358
- "inputSchema": {
359
- "type": "object",
360
- "properties": {
361
- "query": {
362
- "type": "string",
363
- "description": "Search query for MCP documentation"
364
- },
365
- "limit": {
366
- "type": "integer",
367
- "description": "Maximum number of results",
368
- "default": 5
369
- }
370
- },
371
- "required": ["query"]
372
- }
373
- },
374
- {
375
- "name": "get_chunk",
376
- "title": "Get Documentation Chunk",
377
- "description": "Get a specific documentation chunk by ID",
378
- "inputSchema": {
379
- "type": "object",
380
- "properties": {
381
- "chunk_id": {
382
- "type": "string",
383
- "description": "Chunk ID to retrieve"
384
- }
385
- },
386
- "required": ["chunk_id"]
387
- }
388
  },
389
- {
390
- "name": "list_docs",
391
- "title": "List Documents",
392
- "description": "List all available documents",
393
- "inputSchema": {
394
- "type": "object",
395
- "properties": {}
396
- }
397
  }
398
- ]
399
  }
400
- }
401
- yield f"data: {json.dumps(tools_response)}\n\n"
402
 
403
- # Keep connection alive
404
- while True:
405
- await asyncio.sleep(30)
406
- yield f"data: {json.dumps({'jsonrpc': '2.0', 'method': 'ping', 'params': {}})}\n\n"
407
-
408
- return StreamingResponse(
409
- event_generator(),
410
- media_type="text/event-stream",
411
- headers={
412
- "Cache-Control": "no-cache",
413
- "Connection": "keep-alive",
414
- "Access-Control-Allow-Origin": "*",
415
- "Access-Control-Allow-Headers": "*",
416
- }
417
- )
418
-
419
- @app.post("/mcp/stream")
420
- async def mcp_post(request: dict):
421
- """Handle MCP requests via POST (for mcp-remote bidirectional communication)"""
422
- try:
423
- method = request.get("method")
424
- params = request.get("params", {})
425
- request_id = request.get("id")
426
 
 
427
  if method == "tools/list":
428
  return {
429
  "jsonrpc": "2.0",
@@ -478,6 +444,7 @@ async def mcp_post(request: dict):
478
  }
479
  }
480
 
 
481
  elif method == "tools/call":
482
  tool_name = params.get("name")
483
  arguments = params.get("arguments", {})
@@ -624,7 +591,7 @@ async def mcp_post(request: dict):
624
  except Exception as e:
625
  return {
626
  "jsonrpc": "2.0",
627
- "id": request.get("id"),
628
  "error": {
629
  "code": -32603,
630
  "message": f"Internal error: {str(e)}"
 
7
  import json
8
  import asyncio
9
  import logging
10
+ import uuid
11
  from typing import Any, Dict, List, Optional
12
+ from fastapi import FastAPI, HTTPException, Request
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from fastapi.responses import StreamingResponse
15
  from pydantic import BaseModel
16
  import uvicorn
 
 
17
 
18
  # Configure logging
19
  logging.basicConfig(level=logging.INFO)
 
35
  chunks_data = None
36
  docs_data = None
37
 
38
+ # Session management for MCP
39
+ sessions = {}
40
+
41
  def load_data():
42
  """Load the documentation chunks and metadata"""
43
  global chunks_data, docs_data
 
323
 
324
  return {"documents": docs_data}
325
 
326
+ def is_initialize_request(body: dict) -> bool:
327
+ """Check if request is an MCP initialize request"""
328
+ return (body.get("jsonrpc") == "2.0" and
329
+ body.get("method") == "initialize" and
330
+ "id" in body)
331
+
332
+ def create_mcp_server():
333
+ """Create MCP server instance"""
334
+ return {
335
+ "name": "mcp-docs-server",
336
+ "version": "1.0.0",
337
+ "capabilities": {
338
+ "tools": {"listChanged": True},
339
+ "resources": {}
 
 
 
 
 
 
 
340
  }
341
+ }
342
+
343
+ @app.post("/mcp")
344
+ async def mcp_post_handler(request: Request):
345
+ """Handle MCP requests with proper session management"""
346
+ try:
347
+ body = await request.json()
348
+ session_id = request.headers.get("mcp-session-id")
349
 
350
+ # Handle initialization request
351
+ if is_initialize_request(body):
352
+ # Create new session
353
+ new_session_id = str(uuid.uuid4())
354
+ sessions[new_session_id] = {
355
+ "server": create_mcp_server(),
356
+ "initialized": True
357
+ }
358
+
359
+ # Return initialization response
360
+ return {
361
+ "jsonrpc": "2.0",
362
+ "id": body.get("id"),
363
+ "result": {
364
+ "protocolVersion": "2025-06-18",
365
+ "capabilities": {
366
+ "tools": {"listChanged": True},
367
+ "resources": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  },
369
+ "serverInfo": {
370
+ "name": "mcp-docs-server",
371
+ "version": "1.0.0"
 
 
 
 
 
372
  }
373
+ }
374
  }
 
 
375
 
376
+ # Handle other requests with session
377
+ if not session_id or session_id not in sessions:
378
+ return {
379
+ "jsonrpc": "2.0",
380
+ "error": {
381
+ "code": -32000,
382
+ "message": "Bad Request: No valid session ID provided"
383
+ },
384
+ "id": body.get("id")
385
+ }
386
+
387
+ session = sessions[session_id]
388
+ method = body.get("method")
389
+ params = body.get("params", {})
390
+ request_id = body.get("id")
 
 
 
 
 
 
 
 
391
 
392
+ # Handle tools/list
393
  if method == "tools/list":
394
  return {
395
  "jsonrpc": "2.0",
 
444
  }
445
  }
446
 
447
+ # Handle tools/call
448
  elif method == "tools/call":
449
  tool_name = params.get("name")
450
  arguments = params.get("arguments", {})
 
591
  except Exception as e:
592
  return {
593
  "jsonrpc": "2.0",
594
+ "id": body.get("id") if 'body' in locals() else None,
595
  "error": {
596
  "code": -32603,
597
  "message": f"Internal error: {str(e)}"