Skip to content

Commit dfc50f8

Browse files
committed
chore: depend on jupyter-mcp-tools 0.1.2
1 parent c4b3b00 commit dfc50f8

File tree

3 files changed

+177
-79
lines changed

3 files changed

+177
-79
lines changed

jupyter_mcp_server/jupyter_extension/handlers.py

Lines changed: 93 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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

jupyter_mcp_server/server.py

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -559,64 +559,122 @@ async def get_registered_tools():
559559
context = ServerContext.get_instance()
560560
mode = context._mode
561561

562-
# For JUPYTER_SERVER mode, query jupyter-mcp-tools extension
562+
# For JUPYTER_SERVER mode, expose BOTH FastMCP tools AND jupyter-mcp-tools
563563
if mode == ServerMode.JUPYTER_SERVER:
564+
all_tools = []
565+
jupyter_tool_names = set()
566+
567+
# First, get tools from jupyter-mcp-tools extension
564568
try:
565569
from jupyter_mcp_tools import get_tools
566570

567-
# Query for a specific tool as the first implementation
568-
# Get the base_url and token from context
569-
# For now, use localhost with assumption the extension is running
570-
base_url = "http://localhost:8888" # TODO: Get from context
571-
token = None # TODO: Get from context if available
571+
# Get the base_url and token from server context
572+
# In JUPYTER_SERVER mode, we should use the actual serverapp URL, not hardcoded localhost
573+
if server_context.serverapp is not None:
574+
# Use the actual Jupyter server connection URL
575+
base_url = server_context.serverapp.connection_url
576+
token = server_context.serverapp.token
577+
logger.info(f"Using Jupyter ServerApp connection URL: {base_url}")
578+
else:
579+
# Fallback to configuration (for remote scenarios)
580+
config = get_config()
581+
base_url = config.runtime_url if config.runtime_url else "http://localhost:8888"
582+
token = config.runtime_token
583+
logger.info(f"Using config runtime URL: {base_url}")
584+
585+
logger.info(f"Querying jupyter-mcp-tools at {base_url}")
572586

573587
tools_data = await get_tools(
574588
base_url=base_url,
575589
token=token,
576-
query="console_create", # Start with just one tool
590+
query=None, # Get all tools
577591
enabled_only=True
578592
)
579593

580-
# Validate that exactly one tool was returned
581-
if len(tools_data) != 1:
582-
raise ValueError(
583-
f"Expected exactly 1 tool matching 'console_create', "
584-
f"but got {len(tools_data)} tools"
585-
)
594+
logger.info(f"Retrieved {len(tools_data)} tools from jupyter-mcp-tools extension")
586595

587596
# Convert jupyter-mcp-tools format to MCP format
588-
tools = []
589597
for tool_data in tools_data:
598+
tool_name = tool_data.get('id', '')
599+
jupyter_tool_names.add(tool_name)
600+
601+
# Only include MCP protocol fields (exclude internal fields like commandId)
590602
tool_dict = {
591-
"name": tool_data.get('id', ''), # Use command ID as name
603+
"name": tool_name,
592604
"description": tool_data.get('caption', tool_data.get('label', '')),
593605
}
594606

595607
# Convert parameters to inputSchema
608+
# The parameters field contains the JSON Schema for the tool's arguments
596609
params = tool_data.get('parameters', {})
597-
if params and isinstance(params, dict):
610+
if params and isinstance(params, dict) and params.get('properties'):
611+
# Tool has parameters - use them as inputSchema
598612
tool_dict["inputSchema"] = params
599-
if 'properties' in params:
600-
tool_dict["parameters"] = list(params['properties'].keys())
601-
else:
602-
tool_dict["parameters"] = []
613+
tool_dict["parameters"] = list(params['properties'].keys())
614+
logger.debug(f"Tool {tool_dict['name']} has parameters: {tool_dict['parameters']}")
603615
else:
616+
# Tool has no parameters - use empty schema
604617
tool_dict["parameters"] = []
605618
tool_dict["inputSchema"] = {
606619
"type": "object",
607620
"properties": {},
608621
"description": tool_data.get('usage', '')
609622
}
610623

611-
tools.append(tool_dict)
624+
all_tools.append(tool_dict)
612625

613-
logger.info(f"Retrieved {len(tools)} tool(s) from jupyter-mcp-tools extension")
614-
return tools
626+
logger.info(f"Converted {len(all_tools)} tool(s) from jupyter-mcp-tools with parameter schemas")
615627

616628
except Exception as e:
617629
logger.error(f"Error querying jupyter-mcp-tools extension: {e}", exc_info=True)
618-
# Re-raise the exception since we require exactly 1 tool
619-
raise
630+
# Continue to add FastMCP tools even if jupyter-mcp-tools fails
631+
632+
# Second, add FastMCP tools (excluding duplicates)
633+
# Map FastMCP tool names to their jupyter-mcp-tools equivalents
634+
fastmcp_to_jupyter_mapping = {
635+
"insert_execute_code_cell": "notebook_append-execute",
636+
# Add more mappings as needed
637+
}
638+
639+
try:
640+
tools_list = await mcp.list_tools()
641+
logger.info(f"Retrieved {len(tools_list)} tools from FastMCP registry")
642+
643+
for tool in tools_list:
644+
# Check if this FastMCP tool has a jupyter-mcp-tools equivalent
645+
jupyter_equivalent = fastmcp_to_jupyter_mapping.get(tool.name)
646+
647+
if jupyter_equivalent and jupyter_equivalent in jupyter_tool_names:
648+
logger.info(f"Skipping FastMCP tool '{tool.name}' - equivalent '{jupyter_equivalent}' available from jupyter-mcp-tools")
649+
continue
650+
651+
# Add FastMCP tool
652+
tool_dict = {
653+
"name": tool.name,
654+
"description": tool.description,
655+
}
656+
657+
# Extract parameter names from inputSchema
658+
if hasattr(tool, 'inputSchema') and tool.inputSchema:
659+
input_schema = tool.inputSchema
660+
if 'properties' in input_schema:
661+
tool_dict["parameters"] = list(input_schema['properties'].keys())
662+
else:
663+
tool_dict["parameters"] = []
664+
665+
# Include full inputSchema for MCP protocol compatibility
666+
tool_dict["inputSchema"] = input_schema
667+
else:
668+
tool_dict["parameters"] = []
669+
670+
all_tools.append(tool_dict)
671+
672+
logger.info(f"Added {len(all_tools) - len(jupyter_tool_names)} FastMCP tool(s), total: {len(all_tools)}")
673+
674+
except Exception as e:
675+
logger.error(f"Error retrieving FastMCP tools: {e}", exc_info=True)
676+
677+
return all_tools
620678

621679
# For MCP_SERVER mode, use local FastMCP registry
622680
# Use FastMCP's list_tools method which returns Tool objects

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ classifiers = [
2222
]
2323
dependencies = [
2424
"jupyter-kernel-client>=0.7.3",
25-
"jupyter-mcp-tools>=0.1.1",
25+
"jupyter-mcp-tools>=0.1.2",
2626
"jupyter-nbmodel-client>=0.14.2",
2727
"jupyter-server-nbmodel",
2828
"jupyter-server-api",

0 commit comments

Comments
 (0)