Skip to content

Commit 7092922

Browse files
committed
deps: jupyter-mcp-tools 0.1.1
1 parent 3f88f66 commit 7092922

File tree

3 files changed

+214
-42
lines changed

3 files changed

+214
-42
lines changed

jupyter_mcp_server/jupyter_extension/handlers.py

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

jupyter_mcp_server/server.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,9 +550,75 @@ async def get_registered_tools():
550550
This function is used by the Jupyter extension to dynamically expose
551551
the tool registry without hardcoding tool names and parameters.
552552
553+
For JUPYTER_SERVER mode, it queries the jupyter-mcp-tools extension.
554+
For MCP_SERVER mode, it uses the local FastMCP registry.
555+
553556
Returns:
554557
list: List of tool dictionaries with name, description, and inputSchema
555558
"""
559+
context = ServerContext.get_instance()
560+
mode = context._mode
561+
562+
# For JUPYTER_SERVER mode, query jupyter-mcp-tools extension
563+
if mode == ServerMode.JUPYTER_SERVER:
564+
try:
565+
from jupyter_mcp_tools import get_tools
566+
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
572+
573+
tools_data = await get_tools(
574+
base_url=base_url,
575+
token=token,
576+
query="console_create", # Start with just one tool
577+
enabled_only=True
578+
)
579+
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+
)
586+
587+
# Convert jupyter-mcp-tools format to MCP format
588+
tools = []
589+
for tool_data in tools_data:
590+
tool_dict = {
591+
"name": tool_data.get('id', ''), # Use command ID as name
592+
"description": tool_data.get('caption', tool_data.get('label', '')),
593+
}
594+
595+
# Convert parameters to inputSchema
596+
params = tool_data.get('parameters', {})
597+
if params and isinstance(params, dict):
598+
tool_dict["inputSchema"] = params
599+
if 'properties' in params:
600+
tool_dict["parameters"] = list(params['properties'].keys())
601+
else:
602+
tool_dict["parameters"] = []
603+
else:
604+
tool_dict["parameters"] = []
605+
tool_dict["inputSchema"] = {
606+
"type": "object",
607+
"properties": {},
608+
"description": tool_data.get('usage', '')
609+
}
610+
611+
tools.append(tool_dict)
612+
613+
logger.info(f"Retrieved {len(tools)} tool(s) from jupyter-mcp-tools extension")
614+
return tools
615+
616+
except Exception as e:
617+
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
620+
621+
# For MCP_SERVER mode, use local FastMCP registry
556622
# Use FastMCP's list_tools method which returns Tool objects
557623
tools_list = await mcp.list_tools()
558624

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ classifiers = [
2121
"Programming Language :: Python :: 3",
2222
]
2323
dependencies = [
24-
"jupyter-server-nbmodel",
2524
"jupyter-kernel-client>=0.7.3",
25+
"jupyter-mcp-tools>=0.1.1",
2626
"jupyter-nbmodel-client>=0.14.2",
27+
"jupyter-server-nbmodel",
2728
"jupyter-server-api",
2829
"jupyter_server>=1.6,<3",
2930
"tornado>=6.1",

0 commit comments

Comments
 (0)