galcan commited on
Commit
ec5b689
·
1 Parent(s): 8b215ec

Convert to proper MCP server with SSE transport for HTTP access

Browse files
app.py CHANGED
@@ -1,35 +1,33 @@
1
- import json
2
- import os
3
- from typing import List, Dict, Any, Optional
4
- from fastapi import FastAPI, HTTPException
5
- from fastapi.middleware.cors import CORSMiddleware
6
- from pydantic import BaseModel
7
-
8
- app = FastAPI(title="MCP Documentation Server", version="1.0.0")
9
 
10
- # Add CORS middleware
11
- app.add_middleware(
12
- CORSMiddleware,
13
- allow_origins=["*"],
14
- allow_credentials=True,
15
- allow_methods=["*"],
16
- allow_headers=["*"],
 
 
 
 
 
17
  )
18
 
19
- # Global variables for loaded data
 
 
 
 
20
  chunks_data = None
21
  docs_data = None
22
 
23
- class SearchRequest(BaseModel):
24
- query: str
25
- limit: int = 5
26
-
27
- class SearchResponse(BaseModel):
28
- results: List[Dict[str, Any]]
29
- total: int
30
-
31
  def load_data():
32
- """Load the embedded chunks data"""
33
  global chunks_data, docs_data
34
 
35
  try:
@@ -41,48 +39,122 @@ def load_data():
41
  with open('mcp_docs/index/docs_md.json', 'r', encoding='utf-8') as f:
42
  docs_data = json.load(f)
43
 
44
- print(f"Loaded {len(chunks_data)} chunks and {len(docs_data)} documents")
45
- print("Using text-based search (no FAISS index required)")
46
 
47
  except Exception as e:
48
- print(f"Error loading data: {e}")
49
  raise
50
 
51
- @app.on_event("startup")
52
- async def startup_event():
53
- """Load data on startup"""
54
- load_data()
55
 
56
- @app.get("/")
57
- async def root():
58
- """Health check endpoint"""
59
- return {
60
- "message": "MCP Documentation Server",
61
- "status": "running",
62
- "chunks_loaded": len(chunks_data) if chunks_data else 0,
63
- "docs_loaded": len(docs_data) if docs_data else 0
64
- }
 
 
 
 
 
 
 
65
 
66
- @app.post("/search", response_model=SearchResponse)
67
- async def search_docs(request: SearchRequest):
68
- """Search through documentation chunks using text matching"""
69
  if not chunks_data:
70
- raise HTTPException(status_code=500, detail="Data not loaded")
71
 
72
- try:
73
- query_lower = request.query.lower()
74
- results = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
 
76
  for chunk in chunks_data:
77
  text = chunk.get('text', '').lower()
78
  title = chunk.get('title', '').lower()
79
 
80
- # Simple scoring based on query matches
81
  score = 0
82
- if query_lower in text:
83
- score += text.count(query_lower) * 2 # Text matches worth more
84
- if query_lower in title:
85
- score += title.count(query_lower) * 5 # Title matches worth most
86
 
87
  if score > 0:
88
  results.append({
@@ -91,42 +163,70 @@ async def search_docs(request: SearchRequest):
91
  "text": chunk.get('text'),
92
  "url": chunk.get('url'),
93
  "filename": chunk.get('filename'),
94
- "chunk_index": chunk.get('chunk_index'),
95
- "total_chunks": chunk.get('total_chunks'),
96
  "score": score
97
  })
98
 
99
- # Sort by relevance score
100
- results = sorted(results, key=lambda x: x['score'], reverse=True)
101
 
102
- return SearchResponse(
103
- results=results[:request.limit],
104
- total=len(results)
105
- )
 
 
 
 
106
 
107
- except Exception as e:
108
- raise HTTPException(status_code=500, detail=f"Search error: {str(e)}")
109
-
110
- @app.get("/chunks/{chunk_id}")
111
- async def get_chunk(chunk_id: str):
112
- """Get a specific chunk by ID"""
113
- if not chunks_data:
114
- raise HTTPException(status_code=500, detail="Data not loaded")
115
 
116
- for chunk in chunks_data:
117
- if chunk.get('chunk_id') == chunk_id:
118
- return chunk
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- raise HTTPException(status_code=404, detail="Chunk not found")
 
121
 
122
- @app.get("/docs")
123
- async def list_docs():
124
- """List all available documents"""
125
- if not docs_data:
126
- raise HTTPException(status_code=500, detail="Data not loaded")
127
 
128
- return {"documents": docs_data}
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  if __name__ == "__main__":
131
- import uvicorn
132
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server for MCP Documentation
4
+ Hosted on Hugging Face Spaces with HTTP transport
5
+ """
 
 
 
6
 
7
+ 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
28
 
 
 
 
 
 
 
 
 
29
  def load_data():
30
+ """Load the documentation chunks and metadata"""
31
  global chunks_data, docs_data
32
 
33
  try:
 
39
  with open('mcp_docs/index/docs_md.json', 'r', encoding='utf-8') as f:
40
  docs_data = json.load(f)
41
 
42
+ logger.info(f"Loaded {len(chunks_data)} chunks and {len(docs_data)} documents")
 
43
 
44
  except Exception as e:
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": {
101
+ "type": "string",
102
+ "description": "Search query"
103
+ },
104
+ "limit": {
105
+ "type": "integer",
106
+ "description": "Maximum number of results",
107
+ "default": 5
108
+ }
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": {
120
+ "type": "string",
121
+ "description": "Chunk ID to retrieve"
122
+ }
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()
145
+ limit = arguments.get("limit", 5)
146
 
147
+ results = []
148
  for chunk in chunks_data:
149
  text = chunk.get('text', '').lower()
150
  title = chunk.get('title', '').lower()
151
 
152
+ # Simple scoring
153
  score = 0
154
+ if query in text:
155
+ score += text.count(query) * 2
156
+ if query in title:
157
+ score += title.count(query) * 5
158
 
159
  if score > 0:
160
  results.append({
 
163
  "text": chunk.get('text'),
164
  "url": chunk.get('url'),
165
  "filename": chunk.get('filename'),
 
 
166
  "score": score
167
  })
168
 
169
+ # Sort by score and limit results
170
+ results = sorted(results, key=lambda x: x['score'], reverse=True)[:limit]
171
 
172
+ if results:
173
+ response = f"Found {len(results)} results for '{arguments.get('query', '')}':\n\n"
174
+ for i, result in enumerate(results, 1):
175
+ response += f"{i}. **{result['title']}** (Score: {result['score']})\n"
176
+ response += f" {result['text'][:200]}...\n"
177
+ response += f" Source: {result['filename']}\n\n"
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", "")
185
+
186
+ for chunk in chunks_data:
187
+ if chunk.get('chunk_id') == chunk_id:
188
+ response = f"**{chunk.get('title', 'Untitled')}**\n\n"
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:
202
+ response += f"- **{doc.get('title', 'Untitled')}**\n"
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())
 
cursor_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "mcp-docs-client": {
4
+ "command": "python",
5
+ "args": ["mcp_client_server.py"],
6
+ "cwd": "C:\\crawl\\mcp-docs-hf-space\\mcp-docs-server"
7
+ }
8
+ }
9
+ }
mcp_client_server.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Client Server that connects to Hugging Face Spaces API
4
+ This acts as a bridge between Cursor and your Hugging Face Spaces server
5
+ """
6
+
7
+ import json
8
+ import asyncio
9
+ import logging
10
+ import aiohttp
11
+ from typing import Any, Dict, List, Optional
12
+ from mcp.server import Server
13
+ from mcp.server.models import InitializationOptions
14
+ from mcp.server.stdio import stdio_server
15
+ from mcp.types import (
16
+ Resource,
17
+ Tool,
18
+ TextContent,
19
+ LoggingLevel
20
+ )
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Hugging Face Spaces URL - replace with your actual space URL
27
+ HF_SPACE_URL = "https://galcan-mcp-docs-server.hf.space"
28
+
29
+ # Initialize the MCP server
30
+ server = Server("mcp-docs-client")
31
+
32
+ async def make_request(endpoint: str, method: str = "GET", data: dict = None) -> dict:
33
+ """Make HTTP request to Hugging Face Spaces API"""
34
+ url = f"{HF_SPACE_URL}{endpoint}"
35
+
36
+ try:
37
+ async with aiohttp.ClientSession() as session:
38
+ if method == "GET":
39
+ async with session.get(url) as response:
40
+ return await response.json()
41
+ elif method == "POST":
42
+ async with session.post(url, json=data) as response:
43
+ return await response.json()
44
+ except Exception as e:
45
+ logger.error(f"Request failed: {e}")
46
+ return {"error": str(e)}
47
+
48
+ @server.list_resources()
49
+ async def list_resources() -> List[Resource]:
50
+ """List available documentation resources"""
51
+ try:
52
+ # Get docs from HF Spaces
53
+ response = await make_request("/docs")
54
+ if "error" in response:
55
+ return []
56
+
57
+ resources = []
58
+ for doc in response.get("documents", []):
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
+ except Exception as e:
68
+ logger.error(f"Error listing resources: {e}")
69
+ return []
70
+
71
+ @server.read_resource()
72
+ async def read_resource(uri: str) -> str:
73
+ """Read a specific documentation resource"""
74
+ try:
75
+ # Extract document ID from URI
76
+ if uri.startswith("mcp://docs/"):
77
+ doc_id = uri.replace("mcp://docs/", "")
78
+
79
+ # Search for chunks related to this document
80
+ search_response = await make_request("/search", "POST", {
81
+ "query": doc_id,
82
+ "limit": 10
83
+ })
84
+
85
+ if "error" in search_response:
86
+ return f"Error: {search_response['error']}"
87
+
88
+ results = search_response.get("results", [])
89
+ if results:
90
+ content = "\n\n".join([result.get("text", "") for result in results])
91
+ return content
92
+ else:
93
+ return f"Document {doc_id} not found"
94
+
95
+ return "Invalid URI"
96
+ except Exception as e:
97
+ return f"Error reading resource: {e}"
98
+
99
+ @server.list_tools()
100
+ async def list_tools() -> List[Tool]:
101
+ """List available tools"""
102
+ return [
103
+ Tool(
104
+ name="search_docs",
105
+ description="Search through MCP documentation chunks on Hugging Face Spaces",
106
+ inputSchema={
107
+ "type": "object",
108
+ "properties": {
109
+ "query": {
110
+ "type": "string",
111
+ "description": "Search query for MCP documentation"
112
+ },
113
+ "limit": {
114
+ "type": "integer",
115
+ "description": "Maximum number of results",
116
+ "default": 5
117
+ }
118
+ },
119
+ "required": ["query"]
120
+ }
121
+ ),
122
+ Tool(
123
+ name="get_chunk",
124
+ description="Get a specific documentation chunk by ID from Hugging Face Spaces",
125
+ inputSchema={
126
+ "type": "object",
127
+ "properties": {
128
+ "chunk_id": {
129
+ "type": "string",
130
+ "description": "Chunk ID to retrieve"
131
+ }
132
+ },
133
+ "required": ["chunk_id"]
134
+ }
135
+ ),
136
+ Tool(
137
+ name="list_docs",
138
+ description="List all available documents from Hugging Face Spaces",
139
+ inputSchema={
140
+ "type": "object",
141
+ "properties": {}
142
+ }
143
+ ),
144
+ Tool(
145
+ name="health_check",
146
+ description="Check if the Hugging Face Spaces server is running",
147
+ inputSchema={
148
+ "type": "object",
149
+ "properties": {}
150
+ }
151
+ )
152
+ ]
153
+
154
+ @server.call_tool()
155
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
156
+ """Handle tool calls by forwarding to Hugging Face Spaces"""
157
+
158
+ if name == "search_docs":
159
+ query = arguments.get("query", "")
160
+ limit = arguments.get("limit", 5)
161
+
162
+ try:
163
+ response = await make_request("/search", "POST", {
164
+ "query": query,
165
+ "limit": limit
166
+ })
167
+
168
+ if "error" in response:
169
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
170
+
171
+ results = response.get("results", [])
172
+ total = response.get("total", 0)
173
+
174
+ if results:
175
+ response_text = f"Found {total} results for '{query}':\n\n"
176
+ for i, result in enumerate(results, 1):
177
+ response_text += f"{i}. **{result.get('title', 'Untitled')}**\n"
178
+ response_text += f" {result.get('text', '')[:200]}...\n"
179
+ response_text += f" Source: {result.get('filename', 'Unknown')}\n"
180
+ if result.get('score'):
181
+ response_text += f" Score: {result['score']}\n"
182
+ response_text += "\n"
183
+ else:
184
+ response_text = f"No results found for '{query}'"
185
+
186
+ return [TextContent(type="text", text=response_text)]
187
+
188
+ except Exception as e:
189
+ return [TextContent(type="text", text=f"Error searching: {e}")]
190
+
191
+ elif name == "get_chunk":
192
+ chunk_id = arguments.get("chunk_id", "")
193
+
194
+ try:
195
+ response = await make_request(f"/chunks/{chunk_id}")
196
+
197
+ if "error" in response:
198
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
199
+
200
+ if response:
201
+ result_text = f"**{response.get('title', 'Untitled')}**\n\n"
202
+ result_text += f"{response.get('text', '')}\n\n"
203
+ result_text += f"Source: {response.get('filename', 'Unknown')}\n"
204
+ result_text += f"URL: {response.get('url', 'N/A')}"
205
+ return [TextContent(type="text", text=result_text)]
206
+ else:
207
+ return [TextContent(type="text", text=f"Chunk {chunk_id} not found")]
208
+
209
+ except Exception as e:
210
+ return [TextContent(type="text", text=f"Error getting chunk: {e}")]
211
+
212
+ elif name == "list_docs":
213
+ try:
214
+ response = await make_request("/docs")
215
+
216
+ if "error" in response:
217
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
218
+
219
+ docs = response.get("documents", [])
220
+ if docs:
221
+ response_text = "Available documents:\n\n"
222
+ for doc in docs:
223
+ response_text += f"- **{doc.get('title', 'Untitled')}**\n"
224
+ response_text += f" ID: {doc.get('id', 'Unknown')}\n"
225
+ response_text += f" URL: {doc.get('url', 'N/A')}\n\n"
226
+ else:
227
+ response_text = "No documents available"
228
+
229
+ return [TextContent(type="text", text=response_text)]
230
+
231
+ except Exception as e:
232
+ return [TextContent(type="text", text=f"Error listing docs: {e}")]
233
+
234
+ elif name == "health_check":
235
+ try:
236
+ response = await make_request("/")
237
+
238
+ if "error" in response:
239
+ return [TextContent(type="text", text=f"Server error: {response['error']}")]
240
+
241
+ status = response.get("status", "unknown")
242
+ chunks_loaded = response.get("chunks_loaded", 0)
243
+ docs_loaded = response.get("docs_loaded", 0)
244
+
245
+ health_text = f"**Hugging Face Spaces Server Status**\n\n"
246
+ health_text += f"Status: {status}\n"
247
+ health_text += f"Chunks loaded: {chunks_loaded}\n"
248
+ health_text += f"Documents loaded: {docs_loaded}\n"
249
+ health_text += f"Server URL: {HF_SPACE_URL}"
250
+
251
+ return [TextContent(type="text", text=health_text)]
252
+
253
+ except Exception as e:
254
+ return [TextContent(type="text", text=f"Health check failed: {e}")]
255
+
256
+ else:
257
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
258
+
259
+ async def main():
260
+ """Main entry point"""
261
+ logger.info(f"Starting MCP client server for {HF_SPACE_URL}")
262
+
263
+ # Run the server
264
+ async with stdio_server() as (read_stream, write_stream):
265
+ await server.run(
266
+ read_stream,
267
+ write_stream,
268
+ InitializationOptions(
269
+ server_name="mcp-docs-client",
270
+ server_version="1.0.0",
271
+ capabilities=server.get_capabilities(
272
+ notification_options=None,
273
+ experimental_capabilities=None
274
+ )
275
+ )
276
+ )
277
+
278
+ if __name__ == "__main__":
279
+ asyncio.run(main())
mcp_server.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server for MCP Documentation
4
+ Hosted on Hugging Face Spaces with HTTP transport
5
+ """
6
+
7
+ 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
28
+
29
+ def load_data():
30
+ """Load the documentation chunks and metadata"""
31
+ global chunks_data, docs_data
32
+
33
+ try:
34
+ # Load chunks data
35
+ with open('mcp_docs/index/chunks_md.json', 'r', encoding='utf-8') as f:
36
+ chunks_data = json.load(f)
37
+
38
+ # Load docs data
39
+ with open('mcp_docs/index/docs_md.json', 'r', encoding='utf-8') as f:
40
+ docs_data = json.load(f)
41
+
42
+ logger.info(f"Loaded {len(chunks_data)} chunks and {len(docs_data)} documents")
43
+
44
+ except Exception as e:
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": {
101
+ "type": "string",
102
+ "description": "Search query"
103
+ },
104
+ "limit": {
105
+ "type": "integer",
106
+ "description": "Maximum number of results",
107
+ "default": 5
108
+ }
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": {
120
+ "type": "string",
121
+ "description": "Chunk ID to retrieve"
122
+ }
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()
145
+ limit = arguments.get("limit", 5)
146
+
147
+ results = []
148
+ for chunk in chunks_data:
149
+ text = chunk.get('text', '').lower()
150
+ title = chunk.get('title', '').lower()
151
+
152
+ # Simple scoring
153
+ score = 0
154
+ if query in text:
155
+ score += text.count(query) * 2
156
+ if query in title:
157
+ score += title.count(query) * 5
158
+
159
+ if score > 0:
160
+ results.append({
161
+ "chunk_id": chunk.get('chunk_id'),
162
+ "title": chunk.get('title'),
163
+ "text": chunk.get('text'),
164
+ "url": chunk.get('url'),
165
+ "filename": chunk.get('filename'),
166
+ "score": score
167
+ })
168
+
169
+ # Sort by score and limit results
170
+ results = sorted(results, key=lambda x: x['score'], reverse=True)[:limit]
171
+
172
+ if results:
173
+ response = f"Found {len(results)} results for '{arguments.get('query', '')}':\n\n"
174
+ for i, result in enumerate(results, 1):
175
+ response += f"{i}. **{result['title']}** (Score: {result['score']})\n"
176
+ response += f" {result['text'][:200]}...\n"
177
+ response += f" Source: {result['filename']}\n\n"
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", "")
185
+
186
+ for chunk in chunks_data:
187
+ if chunk.get('chunk_id') == chunk_id:
188
+ response = f"**{chunk.get('title', 'Untitled')}**\n\n"
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:
202
+ response += f"- **{doc.get('title', 'Untitled')}**\n"
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())
requirements.txt CHANGED
@@ -1,4 +1 @@
1
- fastapi==0.104.1
2
- uvicorn==0.24.0
3
- pydantic==2.5.0
4
- python-multipart==0.0.6
 
1
+ mcp==1.0.0
 
 
 
test_hf_connection.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify connection to Hugging Face Spaces server
4
+ """
5
+
6
+ import asyncio
7
+ import aiohttp
8
+ import json
9
+
10
+ HF_SPACE_URL = "https://galcan-mcp-docs-server.hf.space"
11
+
12
+ async def test_connection():
13
+ """Test connection to Hugging Face Spaces server"""
14
+ print("Testing connection to Hugging Face Spaces server...")
15
+ print(f"URL: {HF_SPACE_URL}")
16
+
17
+ try:
18
+ async with aiohttp.ClientSession() as session:
19
+ # Test health endpoint
20
+ print("\n1. Testing health endpoint...")
21
+ async with session.get(f"{HF_SPACE_URL}/") as response:
22
+ if response.status == 200:
23
+ data = await response.json()
24
+ print(f"[OK] Health check passed")
25
+ print(f" Status: {data.get('status', 'unknown')}")
26
+ print(f" Chunks loaded: {data.get('chunks_loaded', 0)}")
27
+ print(f" Docs loaded: {data.get('docs_loaded', 0)}")
28
+ else:
29
+ print(f"[ERROR] Health check failed: {response.status}")
30
+ return False
31
+
32
+ # Test search endpoint
33
+ print("\n2. Testing search endpoint...")
34
+ search_data = {"query": "MCP architecture", "limit": 3}
35
+ async with session.post(f"{HF_SPACE_URL}/search", json=search_data) as response:
36
+ if response.status == 200:
37
+ data = await response.json()
38
+ results = data.get('results', [])
39
+ total = data.get('total', 0)
40
+ print(f"[OK] Search test passed")
41
+ print(f" Found {total} results")
42
+ if results:
43
+ print(f" First result: {results[0].get('title', 'Unknown')}")
44
+ else:
45
+ print(f"[ERROR] Search test failed: {response.status}")
46
+ return False
47
+
48
+ # Test docs endpoint
49
+ print("\n3. Testing docs endpoint...")
50
+ async with session.get(f"{HF_SPACE_URL}/docs") as response:
51
+ if response.status == 200:
52
+ data = await response.json()
53
+ docs = data.get('documents', [])
54
+ print(f"[OK] Docs endpoint working")
55
+ print(f" Found {len(docs)} documents")
56
+ else:
57
+ print(f"[ERROR] Docs endpoint failed: {response.status}")
58
+ return False
59
+
60
+ print("\n[SUCCESS] All tests passed! Your Hugging Face Spaces server is working correctly.")
61
+ return True
62
+
63
+ except Exception as e:
64
+ print(f"[ERROR] Connection failed: {e}")
65
+ return False
66
+
67
+ async def main():
68
+ print("Hugging Face Spaces Connection Test")
69
+ print("=" * 50)
70
+
71
+ success = await test_connection()
72
+
73
+ if success:
74
+ print("\n✅ Your server is ready to use with Cursor!")
75
+ print("\nNext steps:")
76
+ print("1. Add the MCP client to your Cursor configuration")
77
+ print("2. Restart Cursor")
78
+ print("3. Use the tools: search_docs, get_chunk, list_docs, health_check")
79
+ else:
80
+ print("\n❌ Connection failed. Please check your Hugging Face Spaces deployment.")
81
+
82
+ if __name__ == "__main__":
83
+ asyncio.run(main())
test_mcp_server.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for the MCP Documentation Server
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import os
10
+
11
+ def test_data_loading():
12
+ """Test that data can be loaded"""
13
+ print("Testing data loading...")
14
+
15
+ try:
16
+ # Test chunks data
17
+ with open('mcp_docs/index/chunks_md.json', 'r', encoding='utf-8') as f:
18
+ chunks = json.load(f)
19
+ print(f"[OK] Loaded {len(chunks)} chunks")
20
+
21
+ # Test docs data
22
+ with open('mcp_docs/index/docs_md.json', 'r', encoding='utf-8') as f:
23
+ docs = json.load(f)
24
+ print(f"[OK] Loaded {len(docs)} documents")
25
+
26
+ return True
27
+
28
+ except Exception as e:
29
+ print(f"[ERROR] Error loading data: {e}")
30
+ return False
31
+
32
+ def test_mcp_server_import():
33
+ """Test that the MCP server can be imported"""
34
+ print("\nTesting MCP server import...")
35
+
36
+ try:
37
+ # Import the MCP server
38
+ from mcp_server import server, load_data
39
+ print("[OK] MCP server imported successfully")
40
+
41
+ # Test data loading
42
+ load_data()
43
+ print("[OK] Data loading function works")
44
+
45
+ print("[SUCCESS] MCP server is ready!")
46
+ return True
47
+
48
+ except Exception as e:
49
+ print(f"[ERROR] Error importing MCP server: {e}")
50
+ return False
51
+
52
+ def test_mcp_dependencies():
53
+ """Test that MCP dependencies are available"""
54
+ print("\nTesting MCP dependencies...")
55
+
56
+ try:
57
+ import mcp
58
+ print("[OK] MCP SDK available")
59
+
60
+ from mcp.server import Server
61
+ print("[OK] MCP Server class available")
62
+
63
+ from mcp.server.stdio import stdio_server
64
+ print("[OK] MCP stdio server available")
65
+
66
+ return True
67
+
68
+ except ImportError as e:
69
+ print(f"[ERROR] MCP dependencies not available: {e}")
70
+ print("Install with: pip install mcp")
71
+ return False
72
+
73
+ if __name__ == "__main__":
74
+ print("MCP Documentation Server - Test Script")
75
+ print("=" * 50)
76
+
77
+ success = True
78
+
79
+ # Test data loading
80
+ success &= test_data_loading()
81
+
82
+ # Test MCP dependencies
83
+ success &= test_mcp_dependencies()
84
+
85
+ # Test MCP server import
86
+ success &= test_mcp_server_import()
87
+
88
+ if success:
89
+ print("\n[SUCCESS] All tests passed! The MCP server is ready for Cursor.")
90
+ print("\nTo use in Cursor:")
91
+ print("1. Add the server to your Cursor MCP configuration")
92
+ print("2. Restart Cursor")
93
+ print("3. Use the tools: search_docs, get_chunk, list_docs")
94
+ sys.exit(0)
95
+ else:
96
+ print("\n[FAILED] Some tests failed. Please check the errors above.")
97
+ sys.exit(1)