galcan commited on
Commit
837c66d
·
1 Parent(s): ec5b689

Fix MCP server - use FastAPI instead of MCP SDK for Hugging Face Spaces

Browse files
Files changed (3) hide show
  1. app.py +185 -94
  2. requirements.txt +4 -1
  3. test_mcp_connection.py +50 -0
app.py CHANGED
@@ -8,20 +8,28 @@ import json
8
  import asyncio
9
  import logging
10
  from typing import Any, Dict, List, Optional
11
- from mcp.server import Server
12
- from mcp.server.models import InitializationOptions
13
- from mcp.server.sse import sse_server
14
- from mcp.types import (
15
- Resource,
16
- Tool,
17
- TextContent,
18
- LoggingLevel
19
- )
20
 
21
  # Configure logging
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger(__name__)
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # Global data storage
26
  chunks_data = None
27
  docs_data = None
@@ -45,56 +53,58 @@ def load_data():
45
  logger.error(f"Error loading data: {e}")
46
  raise
47
 
48
- # Initialize the MCP server
49
- server = Server("mcp-docs-server")
 
 
50
 
51
- @server.list_resources()
52
- async def list_resources() -> List[Resource]:
53
- """List available documentation resources"""
54
- if not docs_data:
55
- return []
56
-
57
- resources = []
58
- for doc in docs_data:
59
- resources.append(Resource(
60
- uri=f"mcp://docs/{doc.get('id', 'unknown')}",
61
- name=doc.get('title', 'Untitled'),
62
- description=doc.get('content', '')[:200] + "..." if len(doc.get('content', '')) > 200 else doc.get('content', ''),
63
- mimeType="text/plain"
64
- ))
65
-
66
- return resources
67
 
68
- @server.read_resource()
69
- async def read_resource(uri: str) -> str:
70
- """Read a specific documentation resource"""
71
- if not chunks_data:
72
- return "Data not loaded"
73
-
74
- # Extract document ID from URI
75
- if uri.startswith("mcp://docs/"):
76
- doc_id = uri.replace("mcp://docs/", "")
77
-
78
- # Find chunks for this document
79
- doc_chunks = [chunk for chunk in chunks_data if chunk.get('doc_id') == doc_id]
80
-
81
- if doc_chunks:
82
- # Combine all chunks for the document
83
- content = "\n\n".join([chunk.get('text', '') for chunk in doc_chunks])
84
- return content
85
- else:
86
- return f"Document {doc_id} not found"
87
-
88
- return "Invalid URI"
89
 
90
- @server.list_tools()
91
- async def list_tools() -> List[Tool]:
92
- """List available tools"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  return [
94
- Tool(
95
- name="search_docs",
96
- description="Search through MCP documentation chunks",
97
- inputSchema={
98
  "type": "object",
99
  "properties": {
100
  "query": {
@@ -109,11 +119,11 @@ async def list_tools() -> List[Tool]:
109
  },
110
  "required": ["query"]
111
  }
112
- ),
113
- Tool(
114
- name="get_chunk",
115
- description="Get a specific documentation chunk by ID",
116
- inputSchema={
117
  "type": "object",
118
  "properties": {
119
  "chunk_id": {
@@ -123,22 +133,25 @@ async def list_tools() -> List[Tool]:
123
  },
124
  "required": ["chunk_id"]
125
  }
126
- ),
127
- Tool(
128
- name="list_docs",
129
- description="List all available documents",
130
- inputSchema={
131
  "type": "object",
132
  "properties": {}
133
  }
134
- )
135
  ]
136
 
137
- @server.call_tool()
138
- async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
139
- """Handle tool calls"""
140
  if not chunks_data:
141
- return [TextContent(type="text", text="Data not loaded")]
 
 
 
142
 
143
  if name == "search_docs":
144
  query = arguments.get("query", "").lower()
@@ -178,7 +191,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
178
  else:
179
  response = f"No results found for '{arguments.get('query', '')}'"
180
 
181
- return [TextContent(type="text", text=response)]
182
 
183
  elif name == "get_chunk":
184
  chunk_id = arguments.get("chunk_id", "")
@@ -189,13 +202,13 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
189
  response += f"{chunk.get('text', '')}\n\n"
190
  response += f"Source: {chunk.get('filename', 'Unknown')}\n"
191
  response += f"URL: {chunk.get('url', 'N/A')}"
192
- return [TextContent(type="text", text=response)]
193
 
194
- return [TextContent(type="text", text=f"Chunk {chunk_id} not found")]
195
 
196
  elif name == "list_docs":
197
  if not docs_data:
198
- return [TextContent(type="text", text="No documents available")]
199
 
200
  response = "Available documents:\n\n"
201
  for doc in docs_data:
@@ -203,30 +216,108 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
203
  response += f" ID: {doc.get('id', 'Unknown')}\n"
204
  response += f" URL: {doc.get('url', 'N/A')}\n\n"
205
 
206
- return [TextContent(type="text", text=response)]
207
 
208
  else:
209
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
210
 
211
- async def main():
212
- """Main entry point"""
213
- # Load data
214
- load_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- # Run the server with SSE transport for HTTP access
217
- async with sse_server() as (read_stream, write_stream):
218
- await server.run(
219
- read_stream,
220
- write_stream,
221
- InitializationOptions(
222
- server_name="mcp-docs-server",
223
- server_version="1.0.0",
224
- capabilities=server.get_capabilities(
225
- notification_options=None,
226
- experimental_capabilities=None
227
- )
228
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  if __name__ == "__main__":
232
- asyncio.run(main())
 
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
 
17
  # Configure logging
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
+ # Initialize FastAPI app
22
+ app = FastAPI(title="MCP Documentation Server", version="1.0.0")
23
+
24
+ # Add CORS middleware
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
  # Global data storage
34
  chunks_data = None
35
  docs_data = None
 
53
  logger.error(f"Error loading data: {e}")
54
  raise
55
 
56
+ # Pydantic models
57
+ class SearchRequest(BaseModel):
58
+ query: str
59
+ limit: int = 5
60
 
61
+ class SearchResponse(BaseModel):
62
+ results: List[Dict[str, Any]]
63
+ total: int
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ class ToolCallRequest(BaseModel):
66
+ name: str
67
+ arguments: Dict[str, Any]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ class ToolCallResponse(BaseModel):
70
+ content: List[Dict[str, str]]
71
+
72
+ @app.on_event("startup")
73
+ async def startup_event():
74
+ """Load data on startup"""
75
+ load_data()
76
+
77
+ @app.get("/")
78
+ async def root():
79
+ """Health check endpoint"""
80
+ return {
81
+ "message": "MCP Documentation Server",
82
+ "status": "running",
83
+ "chunks_loaded": len(chunks_data) if chunks_data else 0,
84
+ "docs_loaded": len(docs_data) if docs_data else 0,
85
+ "mcp_server": True
86
+ }
87
+
88
+ @app.get("/mcp/info")
89
+ async def mcp_info():
90
+ """MCP server information"""
91
+ return {
92
+ "name": "mcp-docs-server",
93
+ "version": "1.0.0",
94
+ "capabilities": {
95
+ "tools": True,
96
+ "resources": True
97
+ }
98
+ }
99
+
100
+ @app.get("/mcp/tools")
101
+ async def list_tools():
102
+ """List available MCP tools"""
103
  return [
104
+ {
105
+ "name": "search_docs",
106
+ "description": "Search through MCP documentation chunks",
107
+ "inputSchema": {
108
  "type": "object",
109
  "properties": {
110
  "query": {
 
119
  },
120
  "required": ["query"]
121
  }
122
+ },
123
+ {
124
+ "name": "get_chunk",
125
+ "description": "Get a specific documentation chunk by ID",
126
+ "inputSchema": {
127
  "type": "object",
128
  "properties": {
129
  "chunk_id": {
 
133
  },
134
  "required": ["chunk_id"]
135
  }
136
+ },
137
+ {
138
+ "name": "list_docs",
139
+ "description": "List all available documents",
140
+ "inputSchema": {
141
  "type": "object",
142
  "properties": {}
143
  }
144
+ }
145
  ]
146
 
147
+ @app.post("/mcp/tools/call")
148
+ async def call_tool(request: ToolCallRequest):
149
+ """Call an MCP tool"""
150
  if not chunks_data:
151
+ raise HTTPException(status_code=500, detail="Data not loaded")
152
+
153
+ name = request.name
154
+ arguments = request.arguments
155
 
156
  if name == "search_docs":
157
  query = arguments.get("query", "").lower()
 
191
  else:
192
  response = f"No results found for '{arguments.get('query', '')}'"
193
 
194
+ return ToolCallResponse(content=[{"type": "text", "text": response}])
195
 
196
  elif name == "get_chunk":
197
  chunk_id = arguments.get("chunk_id", "")
 
202
  response += f"{chunk.get('text', '')}\n\n"
203
  response += f"Source: {chunk.get('filename', 'Unknown')}\n"
204
  response += f"URL: {chunk.get('url', 'N/A')}"
205
+ return ToolCallResponse(content=[{"type": "text", "text": response}])
206
 
207
+ return ToolCallResponse(content=[{"type": "text", "text": f"Chunk {chunk_id} not found"}])
208
 
209
  elif name == "list_docs":
210
  if not docs_data:
211
+ return ToolCallResponse(content=[{"type": "text", "text": "No documents available"}])
212
 
213
  response = "Available documents:\n\n"
214
  for doc in docs_data:
 
216
  response += f" ID: {doc.get('id', 'Unknown')}\n"
217
  response += f" URL: {doc.get('url', 'N/A')}\n\n"
218
 
219
+ return ToolCallResponse(content=[{"type": "text", "text": response}])
220
 
221
  else:
222
+ return ToolCallResponse(content=[{"type": "text", "text": f"Unknown tool: {name}"}])
223
 
224
+ @app.get("/mcp/resources")
225
+ async def list_resources():
226
+ """List available MCP resources"""
227
+ if not docs_data:
228
+ return []
229
+
230
+ resources = []
231
+ for doc in docs_data:
232
+ resources.append({
233
+ "uri": f"mcp://docs/{doc.get('id', 'unknown')}",
234
+ "name": doc.get('title', 'Untitled'),
235
+ "description": doc.get('content', '')[:200] + "..." if len(doc.get('content', '')) > 200 else doc.get('content', ''),
236
+ "mimeType": "text/plain"
237
+ })
238
+
239
+ return resources
240
+
241
+ @app.get("/mcp/resources/{resource_id}")
242
+ async def read_resource(resource_id: str):
243
+ """Read a specific MCP resource"""
244
+ if not chunks_data:
245
+ return "Data not loaded"
246
+
247
+ # Find chunks for this document
248
+ doc_chunks = [chunk for chunk in chunks_data if chunk.get('doc_id') == resource_id]
249
 
250
+ if doc_chunks:
251
+ # Combine all chunks for the document
252
+ content = "\n\n".join([chunk.get('text', '') for chunk in doc_chunks])
253
+ return content
254
+ else:
255
+ return f"Document {resource_id} not found"
256
+
257
+ # Legacy REST API endpoints for backward compatibility
258
+ @app.post("/search", response_model=SearchResponse)
259
+ async def search_docs(request: SearchRequest):
260
+ """Search through documentation chunks using text matching"""
261
+ if not chunks_data:
262
+ raise HTTPException(status_code=500, detail="Data not loaded")
263
+
264
+ try:
265
+ query_lower = request.query.lower()
266
+ results = []
267
+
268
+ for chunk in chunks_data:
269
+ text = chunk.get('text', '').lower()
270
+ title = chunk.get('title', '').lower()
271
+
272
+ # Simple scoring based on query matches
273
+ score = 0
274
+ if query_lower in text:
275
+ score += text.count(query_lower) * 2 # Text matches worth more
276
+ if query_lower in title:
277
+ score += title.count(query_lower) * 5 # Title matches worth most
278
+
279
+ if score > 0:
280
+ results.append({
281
+ "chunk_id": chunk.get('chunk_id'),
282
+ "title": chunk.get('title'),
283
+ "text": chunk.get('text'),
284
+ "url": chunk.get('url'),
285
+ "filename": chunk.get('filename'),
286
+ "chunk_index": chunk.get('chunk_index'),
287
+ "total_chunks": chunk.get('total_chunks'),
288
+ "score": score
289
+ })
290
+
291
+ # Sort by relevance score
292
+ results = sorted(results, key=lambda x: x['score'], reverse=True)
293
+
294
+ return SearchResponse(
295
+ results=results[:request.limit],
296
+ total=len(results)
297
  )
298
+
299
+ except Exception as e:
300
+ raise HTTPException(status_code=500, detail=f"Search error: {str(e)}")
301
+
302
+ @app.get("/chunks/{chunk_id}")
303
+ async def get_chunk(chunk_id: str):
304
+ """Get a specific chunk by ID"""
305
+ if not chunks_data:
306
+ raise HTTPException(status_code=500, detail="Data not loaded")
307
+
308
+ for chunk in chunks_data:
309
+ if chunk.get('chunk_id') == chunk_id:
310
+ return chunk
311
+
312
+ raise HTTPException(status_code=404, detail="Chunk not found")
313
+
314
+ @app.get("/docs")
315
+ async def list_docs():
316
+ """List all available documents"""
317
+ if not docs_data:
318
+ raise HTTPException(status_code=500, detail="Data not loaded")
319
+
320
+ return {"documents": docs_data}
321
 
322
  if __name__ == "__main__":
323
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1 +1,4 @@
1
- mcp==1.0.0
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ pydantic==2.5.0
4
+ python-multipart==0.0.6
test_mcp_connection.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify MCP server connection
4
+ """
5
+
6
+ import asyncio
7
+ import aiohttp
8
+ import json
9
+
10
+ async def test_mcp_server():
11
+ """Test the MCP server on Hugging Face Spaces"""
12
+ url = "https://galcan-mcp-docs-server.hf.space"
13
+
14
+ print("Testing MCP server connection...")
15
+ print(f"URL: {url}")
16
+
17
+ try:
18
+ async with aiohttp.ClientSession() as session:
19
+ # Test health check
20
+ async with session.get(f"{url}/") as response:
21
+ if response.status == 200:
22
+ data = await response.json()
23
+ print(f"[OK] Health check passed: {data}")
24
+ else:
25
+ print(f"[ERROR] Health check failed: {response.status}")
26
+ return False
27
+
28
+ # Test search endpoint
29
+ search_data = {
30
+ "query": "MCP architecture",
31
+ "limit": 3
32
+ }
33
+
34
+ async with session.post(f"{url}/search", json=search_data) as response:
35
+ if response.status == 200:
36
+ data = await response.json()
37
+ print(f"[OK] Search test passed: Found {data.get('total', 0)} results")
38
+ else:
39
+ print(f"[ERROR] Search test failed: {response.status}")
40
+ return False
41
+
42
+ print("[SUCCESS] MCP server is working correctly!")
43
+ return True
44
+
45
+ except Exception as e:
46
+ print(f"[ERROR] Connection failed: {e}")
47
+ return False
48
+
49
+ if __name__ == "__main__":
50
+ asyncio.run(test_mcp_server())