@@ -34,6 +34,9 @@ class MCPSSEHandler(RequestHandler):
3434 The MCP protocol uses SSE for streaming responses from the server to the client.
3535 """
3636
37+ # Cache of jupyter_mcp_tools tool names for routing decisions
38+ _jupyter_tool_names = set ()
39+
3740 def check_xsrf_cookie (self ):
3841 """Disable XSRF checking for MCP protocol requests."""
3942 pass
@@ -113,76 +116,103 @@ async def post(self):
113116 logger .info ("Listing tools from FastMCP and jupyter_mcp_tools..." )
114117
115118 try :
116- # Get FastMCP tools
119+ # Get FastMCP tools first
117120 tools_list = await mcp .list_tools ()
118121 logger .info (f"Got { len (tools_list )} tools from FastMCP" )
119122
120- # Convert FastMCP tools to MCP protocol format
121- tools = []
122- for tool in tools_list :
123- tools .append ({
124- "name" : tool .name ,
125- "description" : tool .description ,
126- "inputSchema" : tool .inputSchema
127- })
123+ # Map FastMCP tool names to their jupyter-mcp-tools equivalents
124+ fastmcp_to_jupyter_mapping = {
125+ "insert_execute_code_cell" : "notebook_append-execute" ,
126+ # Add more mappings as needed
127+ }
128128
129- # Get tools from jupyter_mcp_tools extension
129+ # Track jupyter_mcp_tools tool names to check for duplicates
130+ jupyter_tool_names = set ()
131+
132+ # Get tools from jupyter_mcp_tools extension first to identify duplicates
133+ jupyter_tools_data = []
130134 try :
131135 from jupyter_mcp_tools import get_tools
132136
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 )
137+ # Get the server's base URL dynamically from ServerApp
138+ context = get_server_context ()
139+ if context .serverapp is not None :
140+ base_url = context .serverapp .connection_url
141+ token = context .serverapp .token
142+ logger .info (f"Using Jupyter ServerApp connection URL: { base_url } " )
143+ else :
144+ # Fallback to hardcoded localhost (should not happen in JUPYTER_SERVER mode)
145+ port = self .settings .get ('port' , 8888 )
146+ base_url = f"http://localhost:{ port } "
147+ token = self .settings .get ('token' , None )
148+ logger .warning (f"ServerApp not available, using fallback: { base_url } " )
140149
141150 logger .info (f"Querying jupyter_mcp_tools at { base_url } " )
142151
152+ # Get ALL tools from jupyter_mcp_tools (no query filter)
143153 jupyter_tools_data = await get_tools (
144154 base_url = base_url ,
145155 token = token ,
146- query = "console_create" ,
156+ query = None , # Get all tools
147157 enabled_only = True ,
148158 wait_timeout = 5 # Short timeout
149159 )
150160
151161 logger .info (f"Got { len (jupyter_tools_data )} tools from jupyter_mcp_tools extension" )
152162
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" )
163+ # Build set of jupyter tool names and cache it for routing decisions
164+ jupyter_tool_names = {tool_data .get ('id' , '' ) for tool_data in jupyter_tools_data }
165+ MCPSSEHandler ._jupyter_tool_names = jupyter_tool_names
166+ logger .info (f"Cached { len (jupyter_tool_names )} jupyter_mcp_tools names for routing: { jupyter_tool_names } " )
181167
182168 except Exception as jupyter_error :
183169 # Log but don't fail - just return FastMCP tools
184170 logger .warning (f"Could not fetch tools from jupyter_mcp_tools: { jupyter_error } " )
185171
172+ # Convert FastMCP tools to MCP protocol format, excluding duplicates
173+ tools = []
174+ for tool in tools_list :
175+ # Check if this FastMCP tool has a jupyter-mcp-tools equivalent
176+ jupyter_equivalent = fastmcp_to_jupyter_mapping .get (tool .name )
177+
178+ if jupyter_equivalent and jupyter_equivalent in jupyter_tool_names :
179+ logger .info (f"Skipping FastMCP tool '{ tool .name } ' - equivalent '{ jupyter_equivalent } ' available from jupyter-mcp-tools" )
180+ continue
181+
182+ tools .append ({
183+ "name" : tool .name ,
184+ "description" : tool .description ,
185+ "inputSchema" : tool .inputSchema
186+ })
187+
188+ # Now add jupyter_mcp_tools
189+ for tool_data in jupyter_tools_data :
190+ # Only include MCP protocol fields (exclude internal fields like commandId)
191+ tool_dict = {
192+ "name" : tool_data .get ('id' , '' ),
193+ "description" : tool_data .get ('caption' , tool_data .get ('label' , '' )),
194+ }
195+
196+ # Convert parameters to inputSchema
197+ # The parameters field contains the JSON Schema for the tool's arguments
198+ params = tool_data .get ('parameters' , {})
199+ if params and isinstance (params , dict ) and params .get ('properties' ):
200+ # Tool has parameters - use them as inputSchema
201+ tool_dict ["inputSchema" ] = params
202+ logger .debug (f"Tool { tool_dict ['name' ]} has parameters: { list (params .get ('properties' , {}).keys ())} " )
203+ else :
204+ # Tool has no parameters - use empty schema
205+ tool_dict ["inputSchema" ] = {
206+ "type" : "object" ,
207+ "properties" : {},
208+ "description" : tool_data .get ('usage' , '' )
209+ }
210+
211+ tools .append (tool_dict )
212+
213+ logger .info (f"Added { len (jupyter_tools_data )} tool(s) from jupyter_mcp_tools" )
214+
215+
186216 logger .info (f"Returning total of { len (tools )} tools" )
187217
188218 response = {
@@ -212,15 +242,24 @@ async def post(self):
212242 logger .info (f"Calling tool: { tool_name } " )
213243
214244 try :
215- # Check if this is a jupyter_mcp_tools tool (e.g., console_create)
216- if tool_name == "console_create" :
245+ # Check if this is a jupyter_mcp_tools tool
246+ # Use the cached set of jupyter tool names from tools/list
247+ if tool_name in MCPSSEHandler ._jupyter_tool_names :
217248 # Route to jupyter_mcp_tools extension via HTTP execute endpoint
218- logger .info (f"Routing { tool_name } to jupyter_mcp_tools extension" )
249+ logger .info (f"Routing { tool_name } to jupyter_mcp_tools extension (recognized from cache) " )
219250
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 )
251+ # Get server configuration from ServerApp
252+ context = get_server_context ()
253+ if context .serverapp is not None :
254+ base_url = context .serverapp .connection_url
255+ token = context .serverapp .token
256+ logger .info (f"Using Jupyter ServerApp connection URL: { base_url } " )
257+ else :
258+ # Fallback to hardcoded localhost (should not happen in JUPYTER_SERVER mode)
259+ port = self .settings .get ('port' , 8888 )
260+ base_url = f"http://localhost:{ port } "
261+ token = self .settings .get ('token' , None )
262+ logger .warning (f"ServerApp not available, using fallback: { base_url } " )
224263
225264 # Use the MCPToolsClient to execute the tool
226265 from jupyter_mcp_tools .client import MCPToolsClient
@@ -261,6 +300,7 @@ async def post(self):
261300 }
262301 else :
263302 # Use FastMCP's call_tool method for regular tools
303+ logger .info (f"Routing { tool_name } to FastMCP (not in jupyter_mcp_tools cache)" )
264304 result = await mcp .call_tool (tool_name , tool_arguments )
265305
266306 # Handle tuple results from FastMCP
0 commit comments