@@ -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
@@ -111,76 +114,103 @@ async def post(self):
111114 logger .info ("Listing tools from FastMCP and jupyter_mcp_tools..." )
112115
113116 try :
114- # Get FastMCP tools
117+ # Get FastMCP tools first
115118 tools_list = await mcp .list_tools ()
116119 logger .info (f"Got { len (tools_list )} tools from FastMCP" )
117120
118- # Convert FastMCP tools to MCP protocol format
119- tools = []
120- for tool in tools_list :
121- tools .append ({
122- "name" : tool .name ,
123- "description" : tool .description ,
124- "inputSchema" : tool .inputSchema
125- })
121+ # Map FastMCP tool names to their jupyter-mcp-tools equivalents
122+ fastmcp_to_jupyter_mapping = {
123+ "insert_execute_code_cell" : "notebook_append-execute" ,
124+ # Add more mappings as needed
125+ }
126126
127- # Get tools from jupyter_mcp_tools extension
127+ # Track jupyter_mcp_tools tool names to check for duplicates
128+ jupyter_tool_names = set ()
129+
130+ # Get tools from jupyter_mcp_tools extension first to identify duplicates
131+ jupyter_tools_data = []
128132 try :
129133 from jupyter_mcp_tools import get_tools
130134
131- # Get the server's base URL dynamically
132- # The server is running on the port configured in settings
133- port = self .settings .get ('port' , 4040 ) # Default to 4040 if not set
134- base_url = f"http://localhost:{ port } "
135-
136- # Get token from settings if available
137- token = self .settings .get ('token' , None )
135+ # Get the server's base URL dynamically from ServerApp
136+ context = get_server_context ()
137+ if context .serverapp is not None :
138+ base_url = context .serverapp .connection_url
139+ token = context .serverapp .token
140+ logger .info (f"Using Jupyter ServerApp connection URL: { base_url } " )
141+ else :
142+ # Fallback to hardcoded localhost (should not happen in JUPYTER_SERVER mode)
143+ port = self .settings .get ('port' , 8888 )
144+ base_url = f"http://localhost:{ port } "
145+ token = self .settings .get ('token' , None )
146+ logger .warning (f"ServerApp not available, using fallback: { base_url } " )
138147
139148 logger .info (f"Querying jupyter_mcp_tools at { base_url } " )
140149
150+ # Get ALL tools from jupyter_mcp_tools (no query filter)
141151 jupyter_tools_data = await get_tools (
142152 base_url = base_url ,
143153 token = token ,
144- query = "console_create" ,
154+ query = None , # Get all tools
145155 enabled_only = True ,
146156 wait_timeout = 5 # Short timeout
147157 )
148158
149159 logger .info (f"Got { len (jupyter_tools_data )} tools from jupyter_mcp_tools extension" )
150160
151- # Validate that exactly one tool was returned
152- if len (jupyter_tools_data ) != 1 :
153- logger .warning (
154- f"Expected exactly 1 tool matching 'console_create', "
155- f"but got { len (jupyter_tools_data )} tools"
156- )
157- else :
158- # Convert jupyter_mcp_tools format to MCP format and add to tools list
159- for tool_data in jupyter_tools_data :
160- tool_dict = {
161- "name" : tool_data .get ('id' , '' ),
162- "description" : tool_data .get ('caption' , tool_data .get ('label' , '' )),
163- }
164-
165- # Convert parameters to inputSchema
166- params = tool_data .get ('parameters' , {})
167- if params and isinstance (params , dict ):
168- tool_dict ["inputSchema" ] = params
169- else :
170- tool_dict ["inputSchema" ] = {
171- "type" : "object" ,
172- "properties" : {},
173- "description" : tool_data .get ('usage' , '' )
174- }
175-
176- tools .append (tool_dict )
177-
178- logger .info (f"Added { len (jupyter_tools_data )} tool(s) from jupyter_mcp_tools" )
161+ # Build set of jupyter tool names and cache it for routing decisions
162+ jupyter_tool_names = {tool_data .get ('id' , '' ) for tool_data in jupyter_tools_data }
163+ MCPSSEHandler ._jupyter_tool_names = jupyter_tool_names
164+ logger .info (f"Cached { len (jupyter_tool_names )} jupyter_mcp_tools names for routing: { jupyter_tool_names } " )
179165
180166 except Exception as jupyter_error :
181167 # Log but don't fail - just return FastMCP tools
182168 logger .warning (f"Could not fetch tools from jupyter_mcp_tools: { jupyter_error } " )
183169
170+ # Convert FastMCP tools to MCP protocol format, excluding duplicates
171+ tools = []
172+ for tool in tools_list :
173+ # Check if this FastMCP tool has a jupyter-mcp-tools equivalent
174+ jupyter_equivalent = fastmcp_to_jupyter_mapping .get (tool .name )
175+
176+ if jupyter_equivalent and jupyter_equivalent in jupyter_tool_names :
177+ logger .info (f"Skipping FastMCP tool '{ tool .name } ' - equivalent '{ jupyter_equivalent } ' available from jupyter-mcp-tools" )
178+ continue
179+
180+ tools .append ({
181+ "name" : tool .name ,
182+ "description" : tool .description ,
183+ "inputSchema" : tool .inputSchema
184+ })
185+
186+ # Now add jupyter_mcp_tools
187+ for tool_data in jupyter_tools_data :
188+ # Only include MCP protocol fields (exclude internal fields like commandId)
189+ tool_dict = {
190+ "name" : tool_data .get ('id' , '' ),
191+ "description" : tool_data .get ('caption' , tool_data .get ('label' , '' )),
192+ }
193+
194+ # Convert parameters to inputSchema
195+ # The parameters field contains the JSON Schema for the tool's arguments
196+ params = tool_data .get ('parameters' , {})
197+ if params and isinstance (params , dict ) and params .get ('properties' ):
198+ # Tool has parameters - use them as inputSchema
199+ tool_dict ["inputSchema" ] = params
200+ logger .debug (f"Tool { tool_dict ['name' ]} has parameters: { list (params .get ('properties' , {}).keys ())} " )
201+ else :
202+ # Tool has no parameters - use empty schema
203+ tool_dict ["inputSchema" ] = {
204+ "type" : "object" ,
205+ "properties" : {},
206+ "description" : tool_data .get ('usage' , '' )
207+ }
208+
209+ tools .append (tool_dict )
210+
211+ logger .info (f"Added { len (jupyter_tools_data )} tool(s) from jupyter_mcp_tools" )
212+
213+
184214 logger .info (f"Returning total of { len (tools )} tools" )
185215
186216 response = {
@@ -210,15 +240,24 @@ async def post(self):
210240 logger .info (f"Calling tool: { tool_name } " )
211241
212242 try :
213- # Check if this is a jupyter_mcp_tools tool (e.g., console_create)
214- if tool_name == "console_create" :
243+ # Check if this is a jupyter_mcp_tools tool
244+ # Use the cached set of jupyter tool names from tools/list
245+ if tool_name in MCPSSEHandler ._jupyter_tool_names :
215246 # Route to jupyter_mcp_tools extension via HTTP execute endpoint
216- logger .info (f"Routing { tool_name } to jupyter_mcp_tools extension" )
247+ logger .info (f"Routing { tool_name } to jupyter_mcp_tools extension (recognized from cache) " )
217248
218- # Get server configuration
219- port = self .settings .get ('port' , 4040 )
220- base_url = f"http://localhost:{ port } "
221- token = self .settings .get ('token' , None )
249+ # Get server configuration from ServerApp
250+ context = get_server_context ()
251+ if context .serverapp is not None :
252+ base_url = context .serverapp .connection_url
253+ token = context .serverapp .token
254+ logger .info (f"Using Jupyter ServerApp connection URL: { base_url } " )
255+ else :
256+ # Fallback to hardcoded localhost (should not happen in JUPYTER_SERVER mode)
257+ port = self .settings .get ('port' , 8888 )
258+ base_url = f"http://localhost:{ port } "
259+ token = self .settings .get ('token' , None )
260+ logger .warning (f"ServerApp not available, using fallback: { base_url } " )
222261
223262 # Use the MCPToolsClient to execute the tool
224263 from jupyter_mcp_tools .client import MCPToolsClient
@@ -259,6 +298,7 @@ async def post(self):
259298 }
260299 else :
261300 # Use FastMCP's call_tool method for regular tools
301+ logger .info (f"Routing { tool_name } to FastMCP (not in jupyter_mcp_tools cache)" )
262302 result = await mcp .call_tool (tool_name , tool_arguments )
263303
264304 # Handle tuple results from FastMCP
0 commit comments