@@ -107,17 +107,17 @@ async def post(self):
107107 }
108108 logger .info (f"Sending initialize response: { response } " )
109109 elif method == "tools/list" :
110- # List available tools from FastMCP
110+ # List available tools from FastMCP and jupyter_mcp_tools
111111 from jupyter_mcp_server .server import mcp
112112
113- logger .info ("Calling mcp.list_tools() ..." )
113+ logger .info ("Listing tools from FastMCP and jupyter_mcp_tools ..." )
114114
115115 try :
116- # Use FastMCP's list_tools method - returns list of Tool objects
116+ # Get FastMCP tools
117117 tools_list = await mcp .list_tools ()
118118 logger .info (f"Got { len (tools_list )} tools from FastMCP" )
119119
120- # Convert to MCP protocol format
120+ # Convert FastMCP tools to MCP protocol format
121121 tools = []
122122 for tool in tools_list :
123123 tools .append ({
@@ -126,7 +126,64 @@ async def post(self):
126126 "inputSchema" : tool .inputSchema
127127 })
128128
129- logger .info (f"Converted { len (tools )} tools to MCP format" )
129+ # Get tools from jupyter_mcp_tools extension
130+ try :
131+ from jupyter_mcp_tools import get_tools
132+
133+ # Get the server's base URL dynamically
134+ # The server is running on the port configured in settings
135+ port = self .settings .get ('port' , 4040 ) # Default to 4040 if not set
136+ base_url = f"http://localhost:{ port } "
137+
138+ # Get token from settings if available
139+ token = self .settings .get ('token' , None )
140+
141+ logger .info (f"Querying jupyter_mcp_tools at { base_url } " )
142+
143+ jupyter_tools_data = await get_tools (
144+ base_url = base_url ,
145+ token = token ,
146+ query = "console_create" ,
147+ enabled_only = True ,
148+ wait_timeout = 5 # Short timeout
149+ )
150+
151+ logger .info (f"Got { len (jupyter_tools_data )} tools from jupyter_mcp_tools extension" )
152+
153+ # Validate that exactly one tool was returned
154+ if len (jupyter_tools_data ) != 1 :
155+ logger .warning (
156+ f"Expected exactly 1 tool matching 'console_create', "
157+ f"but got { len (jupyter_tools_data )} tools"
158+ )
159+ else :
160+ # Convert jupyter_mcp_tools format to MCP format and add to tools list
161+ for tool_data in jupyter_tools_data :
162+ tool_dict = {
163+ "name" : tool_data .get ('id' , '' ),
164+ "description" : tool_data .get ('caption' , tool_data .get ('label' , '' )),
165+ }
166+
167+ # Convert parameters to inputSchema
168+ params = tool_data .get ('parameters' , {})
169+ if params and isinstance (params , dict ):
170+ tool_dict ["inputSchema" ] = params
171+ else :
172+ tool_dict ["inputSchema" ] = {
173+ "type" : "object" ,
174+ "properties" : {},
175+ "description" : tool_data .get ('usage' , '' )
176+ }
177+
178+ tools .append (tool_dict )
179+
180+ logger .info (f"Added { len (jupyter_tools_data )} tool(s) from jupyter_mcp_tools" )
181+
182+ except Exception as jupyter_error :
183+ # Log but don't fail - just return FastMCP tools
184+ logger .warning (f"Could not fetch tools from jupyter_mcp_tools: { jupyter_error } " )
185+
186+ logger .info (f"Returning total of { len (tools )} tools" )
130187
131188 response = {
132189 "jsonrpc" : "2.0" ,
@@ -152,47 +209,95 @@ async def post(self):
152209 tool_name = params .get ("name" )
153210 tool_arguments = params .get ("arguments" , {})
154211
155- logger .info (f"Calling tool: { tool_name } with args: { tool_arguments } " )
212+ logger .info (f"Calling tool: { tool_name } " )
156213
157214 try :
158- # Use FastMCP's call_tool method
159- result = await mcp .call_tool (tool_name , tool_arguments )
160-
161- # Handle tuple results from FastMCP
162- if isinstance (result , tuple ) and len (result ) >= 1 :
163- # FastMCP returns (content_list, metadata_dict)
164- content_list = result [0 ]
165- if isinstance (content_list , list ):
166- # Serialize TextContent objects to dicts
167- serialized_content = []
168- for item in content_list :
169- if hasattr (item , 'model_dump' ):
170- serialized_content .append (item .model_dump ())
171- elif hasattr (item , 'dict' ):
172- serialized_content .append (item .dict ())
173- elif isinstance (item , dict ):
174- serialized_content .append (item )
215+ # Check if this is a jupyter_mcp_tools tool (e.g., console_create)
216+ if tool_name == "console_create" :
217+ # Route to jupyter_mcp_tools extension via HTTP execute endpoint
218+ logger .info (f"Routing { tool_name } to jupyter_mcp_tools extension" )
219+
220+ # Get server configuration
221+ port = self .settings .get ('port' , 4040 )
222+ base_url = f"http://localhost:{ port } "
223+ token = self .settings .get ('token' , None )
224+
225+ # Use the MCPToolsClient to execute the tool
226+ from jupyter_mcp_tools .client import MCPToolsClient
227+
228+ try :
229+ async with MCPToolsClient (base_url = base_url , token = token ) as client :
230+ execution_result = await client .execute_tool (
231+ tool_id = tool_name ,
232+ parameters = tool_arguments
233+ )
234+
235+ if execution_result .get ('success' ):
236+ result_data = execution_result .get ('result' , {})
237+ result_text = str (result_data ) if result_data else "Tool executed successfully"
238+ result_dict = {
239+ "content" : [{
240+ "type" : "text" ,
241+ "text" : result_text
242+ }]
243+ }
175244 else :
176- serialized_content .append ({"type" : "text" , "text" : str (item )})
177- result_dict = {"content" : serialized_content }
178- else :
179- result_dict = {"content" : [{"type" : "text" , "text" : str (result )}]}
180- # Convert result to dict - it's a CallToolResult with content list
181- elif hasattr (result , 'model_dump' ):
182- result_dict = result .model_dump ()
183- elif hasattr (result , 'dict' ):
184- result_dict = result .dict ()
185- elif hasattr (result , 'content' ):
186- # Extract content directly if it has a content attribute
187- result_dict = {"content" : result .content }
245+ error_msg = execution_result .get ('error' , 'Unknown error' )
246+ result_dict = {
247+ "content" : [{
248+ "type" : "text" ,
249+ "text" : f"Error executing tool: { error_msg } "
250+ }],
251+ "isError" : True
252+ }
253+ except Exception as exec_error :
254+ logger .error (f"Error executing { tool_name } : { exec_error } " )
255+ result_dict = {
256+ "content" : [{
257+ "type" : "text" ,
258+ "text" : f"Failed to execute tool: { str (exec_error )} "
259+ }],
260+ "isError" : True
261+ }
188262 else :
189- # Last resort: check if it's already a string
190- if isinstance (result , str ):
191- result_dict = {"content" : [{"type" : "text" , "text" : result }]}
263+ # Use FastMCP's call_tool method for regular tools
264+ result = await mcp .call_tool (tool_name , tool_arguments )
265+
266+ # Handle tuple results from FastMCP
267+ if isinstance (result , tuple ) and len (result ) >= 1 :
268+ # FastMCP returns (content_list, metadata_dict)
269+ content_list = result [0 ]
270+ if isinstance (content_list , list ):
271+ # Serialize TextContent objects to dicts
272+ serialized_content = []
273+ for item in content_list :
274+ if hasattr (item , 'model_dump' ):
275+ serialized_content .append (item .model_dump ())
276+ elif hasattr (item , 'dict' ):
277+ serialized_content .append (item .dict ())
278+ elif isinstance (item , dict ):
279+ serialized_content .append (item )
280+ else :
281+ serialized_content .append ({"type" : "text" , "text" : str (item )})
282+ result_dict = {"content" : serialized_content }
283+ else :
284+ result_dict = {"content" : [{"type" : "text" , "text" : str (result )}]}
285+ # Convert result to dict - it's a CallToolResult with content list
286+ elif hasattr (result , 'model_dump' ):
287+ result_dict = result .model_dump ()
288+ elif hasattr (result , 'dict' ):
289+ result_dict = result .dict ()
290+ elif hasattr (result , 'content' ):
291+ # Extract content directly if it has a content attribute
292+ result_dict = {"content" : result .content }
192293 else :
193- # If it's some other type, try to serialize it
194- result_dict = {"content" : [{"type" : "text" , "text" : str (result )}]}
195- logger .warning (f"Used fallback str() conversion for type { type (result )} " )
294+ # Last resort: check if it's already a string
295+ if isinstance (result , str ):
296+ result_dict = {"content" : [{"type" : "text" , "text" : result }]}
297+ else :
298+ # If it's some other type, try to serialize it
299+ result_dict = {"content" : [{"type" : "text" , "text" : str (result )}]}
300+ logger .warning (f"Used fallback str() conversion for type { type (result )} " )
196301
197302 logger .info (f"Converted result to dict" )
198303
0 commit comments