@@ -21,47 +21,105 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
2121
2222DEFAULT_PORT = 11436
2323OLLAMA_URL = "http://localhost:11434/api/generate"
24+ SUPPORTED_PROTOCOL_VERSION = "2024-11-05"
2425
2526
2627class MCPServer (BaseHTTPRequestHandler ):
2728 """MCP protocol server for NeurOS."""
2829
30+ def _send_error (self , code , message , req_id = None , data = None , status = 200 ):
31+ error = {"code" : code , "message" : message }
32+ if data is not None :
33+ error ["data" ] = data
34+ self .send_json ({"jsonrpc" : "2.0" , "error" : error , "id" : req_id }, status = status )
35+
36+ def _send_no_content (self ):
37+ self .send_response (202 )
38+ self .send_header ("Access-Control-Allow-Origin" , "*" )
39+ self .send_header ("Content-Length" , "0" )
40+ self .end_headers ()
41+
2942 def do_POST (self ):
30- content_length = int (self .headers .get ('Content-Length' , 0 ))
43+ content_type = self .headers .get ("Content-Type" , "" )
44+ accept = {value .strip ().split (";" , 1 )[0 ].lower ()
45+ for value in self .headers .get ("Accept" , "" ).split ("," )}
46+ media_type = content_type .split (";" , 1 )[0 ].strip ().lower ()
47+ if media_type != "application/json" :
48+ self ._send_error (- 32600 , "Content-Type must be application/json" , status = 400 )
49+ return
50+ if not {"application/json" , "text/event-stream" }.issubset (accept ):
51+ self ._send_error (- 32600 ,
52+ "Accept must include application/json and text/event-stream" ,
53+ status = 400 )
54+ return
55+
56+ try :
57+ content_length = int (self .headers .get ("Content-Length" , "" ))
58+ except ValueError :
59+ self ._send_error (- 32600 , "Content-Length must be a valid integer" , status = 400 )
60+ return
61+ if content_length < 0 :
62+ self ._send_error (- 32600 , "Content-Length must not be negative" , status = 400 )
63+ return
3164 body = self .rfile .read (content_length )
3265
3366 try :
3467 request = json .loads (body )
3568 except json .JSONDecodeError :
36- self .send_error (400 , "Invalid JSON" )
69+ self ._send_error (- 32700 , "Parse error" , status = 400 )
70+ return
71+
72+ if not isinstance (request , dict ):
73+ self ._send_error (- 32600 , "Invalid Request" , status = 400 )
3774 return
3875
39- method = request .get ("method" , "" )
40- params = request .get ("params" , {})
4176 req_id = request .get ("id" )
77+ if request .get ("jsonrpc" ) != "2.0" or not isinstance (request .get ("method" ), str ):
78+ self ._send_error (- 32600 , "Invalid Request" , status = 400 )
79+ return
80+ if "id" in request and (req_id is None or not isinstance (req_id , (str , int ))
81+ or isinstance (req_id , bool )):
82+ self ._send_error (- 32600 , "Invalid Request" , status = 400 )
83+ return
84+ params = request .get ("params" , {})
85+ if not isinstance (params , dict ):
86+ self ._send_error (- 32602 , "Invalid params" , req_id = req_id )
87+ return
88+
89+ method = request ["method" ]
90+ is_notification = "id" not in request
4291
4392 if method == "initialize" :
44- result = self .handle_initialize (params )
93+ result , error = self .handle_initialize (params )
4594 elif method == "tools/list" :
46- result = self .handle_list_tools ()
95+ result , error = self .handle_list_tools (), None
4796 elif method == "tools/call" :
48- result = self .handle_call_tool (params )
97+ result , error = self .handle_call_tool (params )
4998 elif method == "resources/list" :
50- result = self .handle_list_resources ()
99+ result , error = self .handle_list_resources (), None
51100 elif method == "resources/read" :
52- result = self .handle_read_resource (params )
101+ result , error = self .handle_read_resource (params )
53102 else :
54- self .send_json ({"jsonrpc" : "2.0" , "error" : {"code" : - 32601 , "message" : "Method not found" }, "id" : req_id })
103+ error = {"code" : - 32601 , "message" : "Method not found" }
104+ result = None
105+
106+ if is_notification :
107+ self ._send_no_content ()
55108 return
56109
57- self .send_json ({"jsonrpc" : "2.0" , "result" : result , "id" : req_id })
110+ if error is not None :
111+ self .send_json ({"jsonrpc" : "2.0" , "error" : error , "id" : req_id })
112+ else :
113+ self .send_json ({"jsonrpc" : "2.0" , "result" : result , "id" : req_id })
58114
59- def send_json (self , data ):
60- self .send_response (200 )
115+ def send_json (self , data , status = 200 ):
116+ body = json .dumps (data ).encode ("utf-8" )
117+ self .send_response (status )
61118 self .send_header ("Content-Type" , "application/json" )
62119 self .send_header ("Access-Control-Allow-Origin" , "*" )
120+ self .send_header ("Content-Length" , str (len (body )))
63121 self .end_headers ()
64- self .wfile .write (json . dumps ( data ). encode ( 'utf-8' ) )
122+ self .wfile .write (body )
65123
66124 def do_GET (self ):
67125 if self .path == "/health" :
@@ -81,8 +139,25 @@ class MCPServer(BaseHTTPRequestHandler):
81139 self .end_headers ()
82140
83141 def handle_initialize (self , params ):
142+ if not isinstance (params .get ("protocolVersion" ), str ):
143+ return None , {"code" : - 32602 , "message" : "Invalid params" }
144+ client_info = params .get ("clientInfo" )
145+ if not isinstance (params .get ("capabilities" ), dict ) or not isinstance (client_info , dict ):
146+ return None , {"code" : - 32602 , "message" : "Invalid params" }
147+ if not isinstance (client_info .get ("name" ), str ) or not isinstance (client_info .get ("version" ), str ):
148+ return None , {"code" : - 32602 , "message" : "Invalid params" }
149+ requested_version = params ["protocolVersion" ]
150+ if requested_version != SUPPORTED_PROTOCOL_VERSION :
151+ return None , {
152+ "code" : - 32602 ,
153+ "message" : "Unsupported protocol version" ,
154+ "data" : {
155+ "supported" : [SUPPORTED_PROTOCOL_VERSION ],
156+ "requested" : requested_version ,
157+ },
158+ }
84159 return {
85- "protocolVersion" : "2024-11-05" ,
160+ "protocolVersion" : SUPPORTED_PROTOCOL_VERSION ,
86161 "serverInfo" : {
87162 "name" : "NeurOS MCP Server" ,
88163 "version" : "1.0.0"
@@ -91,7 +166,7 @@ class MCPServer(BaseHTTPRequestHandler):
91166 "tools" : {},
92167 "resources" : {}
93168 }
94- }
169+ }, None
95170
96171 def handle_list_tools (self ):
97172 return {
@@ -163,21 +238,23 @@ class MCPServer(BaseHTTPRequestHandler):
163238 def handle_call_tool (self , params ):
164239 tool_name = params .get ("name" , "" )
165240 arguments = params .get ("arguments" , {})
241+ if not isinstance (tool_name , str ) or not isinstance (arguments , dict ):
242+ return None , {"code" : - 32602 , "message" : "Invalid params" }
166243
167244 if tool_name == "read_file" :
168- return self .tool_read_file (arguments )
245+ return self .tool_read_file (arguments ), None
169246 elif tool_name == "list_directory" :
170- return self .tool_list_directory (arguments )
247+ return self .tool_list_directory (arguments ), None
171248 elif tool_name == "run_command" :
172- return self .tool_run_command (arguments )
249+ return self .tool_run_command (arguments ), None
173250 elif tool_name == "ask_llm" :
174- return self .tool_ask_llm (arguments )
251+ return self .tool_ask_llm (arguments ), None
175252 elif tool_name == "get_system_info" :
176- return self .tool_get_system_info (arguments )
253+ return self .tool_get_system_info (arguments ), None
177254 elif tool_name == "git_status" :
178- return self .tool_git_status (arguments )
255+ return self .tool_git_status (arguments ), None
179256 else :
180- return { "error " : f"Unknown tool: { tool_name } " }
257+ return None , { "code" : - 32602 , "message " : f"Unknown tool: { tool_name } " }
181258
182259 def tool_read_file (self , args ):
183260 path = os .path .expanduser (args .get ("path" , "" ))
@@ -316,15 +393,21 @@ class MCPServer(BaseHTTPRequestHandler):
316393
317394 def handle_read_resource (self , params ):
318395 uri = params .get ("uri" , "" )
396+ if not isinstance (uri , str ) or not uri :
397+ return None , {"code" : - 32602 , "message" : "Invalid params" }
319398
320399 if uri == "neuros://system/info" :
321- return self .tool_get_system_info ({})
400+ return self .tool_get_system_info ({}), None
322401 elif uri == "neuros://ollama/models" :
323- return self .tool_ollama_models ()
402+ return self .tool_ollama_models (), None
324403 elif uri == "neuros://git/status" :
325- return self .tool_git_status ({})
404+ return self .tool_git_status ({}), None
326405 else :
327- return {"contents" : [{"uri" : uri , "mimeType" : "text/plain" , "text" : "Resource not found" }]}
406+ return None , {
407+ "code" : - 32002 ,
408+ "message" : "Resource not found" ,
409+ "data" : {"uri" : uri },
410+ }
328411
329412 def tool_ollama_models (self ):
330413 try :
0 commit comments