Skip to content

Commit a85ee20

Browse files
committed
chore: depend on jupyter-mcp-tools 0.1.2
1 parent 58846df commit a85ee20

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
@@ -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

jupyter_mcp_server/server.py

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -869,64 +869,122 @@ async def get_registered_tools():
869869
context = ServerContext.get_instance()
870870
mode = context._mode
871871

872-
# For JUPYTER_SERVER mode, query jupyter-mcp-tools extension
872+
# For JUPYTER_SERVER mode, expose BOTH FastMCP tools AND jupyter-mcp-tools
873873
if mode == ServerMode.JUPYTER_SERVER:
874+
all_tools = []
875+
jupyter_tool_names = set()
876+
877+
# First, get tools from jupyter-mcp-tools extension
874878
try:
875879
from jupyter_mcp_tools import get_tools
876880

877-
# Query for a specific tool as the first implementation
878-
# Get the base_url and token from context
879-
# For now, use localhost with assumption the extension is running
880-
base_url = "http://localhost:8888" # TODO: Get from context
881-
token = None # TODO: Get from context if available
881+
# Get the base_url and token from server context
882+
# In JUPYTER_SERVER mode, we should use the actual serverapp URL, not hardcoded localhost
883+
if server_context.serverapp is not None:
884+
# Use the actual Jupyter server connection URL
885+
base_url = server_context.serverapp.connection_url
886+
token = server_context.serverapp.token
887+
logger.info(f"Using Jupyter ServerApp connection URL: {base_url}")
888+
else:
889+
# Fallback to configuration (for remote scenarios)
890+
config = get_config()
891+
base_url = config.runtime_url if config.runtime_url else "http://localhost:8888"
892+
token = config.runtime_token
893+
logger.info(f"Using config runtime URL: {base_url}")
894+
895+
logger.info(f"Querying jupyter-mcp-tools at {base_url}")
882896

883897
tools_data = await get_tools(
884898
base_url=base_url,
885899
token=token,
886-
query="console_create", # Start with just one tool
900+
query=None, # Get all tools
887901
enabled_only=True
888902
)
889903

890-
# Validate that exactly one tool was returned
891-
if len(tools_data) != 1:
892-
raise ValueError(
893-
f"Expected exactly 1 tool matching 'console_create', "
894-
f"but got {len(tools_data)} tools"
895-
)
904+
logger.info(f"Retrieved {len(tools_data)} tools from jupyter-mcp-tools extension")
896905

897906
# Convert jupyter-mcp-tools format to MCP format
898-
tools = []
899907
for tool_data in tools_data:
908+
tool_name = tool_data.get('id', '')
909+
jupyter_tool_names.add(tool_name)
910+
911+
# Only include MCP protocol fields (exclude internal fields like commandId)
900912
tool_dict = {
901-
"name": tool_data.get('id', ''), # Use command ID as name
913+
"name": tool_name,
902914
"description": tool_data.get('caption', tool_data.get('label', '')),
903915
}
904916

905917
# Convert parameters to inputSchema
918+
# The parameters field contains the JSON Schema for the tool's arguments
906919
params = tool_data.get('parameters', {})
907-
if params and isinstance(params, dict):
920+
if params and isinstance(params, dict) and params.get('properties'):
921+
# Tool has parameters - use them as inputSchema
908922
tool_dict["inputSchema"] = params
909-
if 'properties' in params:
910-
tool_dict["parameters"] = list(params['properties'].keys())
911-
else:
912-
tool_dict["parameters"] = []
923+
tool_dict["parameters"] = list(params['properties'].keys())
924+
logger.debug(f"Tool {tool_dict['name']} has parameters: {tool_dict['parameters']}")
913925
else:
926+
# Tool has no parameters - use empty schema
914927
tool_dict["parameters"] = []
915928
tool_dict["inputSchema"] = {
916929
"type": "object",
917930
"properties": {},
918931
"description": tool_data.get('usage', '')
919932
}
920933

921-
tools.append(tool_dict)
934+
all_tools.append(tool_dict)
922935

923-
logger.info(f"Retrieved {len(tools)} tool(s) from jupyter-mcp-tools extension")
924-
return tools
936+
logger.info(f"Converted {len(all_tools)} tool(s) from jupyter-mcp-tools with parameter schemas")
925937

926938
except Exception as e:
927939
logger.error(f"Error querying jupyter-mcp-tools extension: {e}", exc_info=True)
928-
# Re-raise the exception since we require exactly 1 tool
929-
raise
940+
# Continue to add FastMCP tools even if jupyter-mcp-tools fails
941+
942+
# Second, add FastMCP tools (excluding duplicates)
943+
# Map FastMCP tool names to their jupyter-mcp-tools equivalents
944+
fastmcp_to_jupyter_mapping = {
945+
"insert_execute_code_cell": "notebook_append-execute",
946+
# Add more mappings as needed
947+
}
948+
949+
try:
950+
tools_list = await mcp.list_tools()
951+
logger.info(f"Retrieved {len(tools_list)} tools from FastMCP registry")
952+
953+
for tool in tools_list:
954+
# Check if this FastMCP tool has a jupyter-mcp-tools equivalent
955+
jupyter_equivalent = fastmcp_to_jupyter_mapping.get(tool.name)
956+
957+
if jupyter_equivalent and jupyter_equivalent in jupyter_tool_names:
958+
logger.info(f"Skipping FastMCP tool '{tool.name}' - equivalent '{jupyter_equivalent}' available from jupyter-mcp-tools")
959+
continue
960+
961+
# Add FastMCP tool
962+
tool_dict = {
963+
"name": tool.name,
964+
"description": tool.description,
965+
}
966+
967+
# Extract parameter names from inputSchema
968+
if hasattr(tool, 'inputSchema') and tool.inputSchema:
969+
input_schema = tool.inputSchema
970+
if 'properties' in input_schema:
971+
tool_dict["parameters"] = list(input_schema['properties'].keys())
972+
else:
973+
tool_dict["parameters"] = []
974+
975+
# Include full inputSchema for MCP protocol compatibility
976+
tool_dict["inputSchema"] = input_schema
977+
else:
978+
tool_dict["parameters"] = []
979+
980+
all_tools.append(tool_dict)
981+
982+
logger.info(f"Added {len(all_tools) - len(jupyter_tool_names)} FastMCP tool(s), total: {len(all_tools)}")
983+
984+
except Exception as e:
985+
logger.error(f"Error retrieving FastMCP tools: {e}", exc_info=True)
986+
987+
return all_tools
930988

931989
# For MCP_SERVER mode, use local FastMCP registry
932990
# 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)