66from typing import Optional
77
88from jupyter_server .extension .application import ExtensionApp
9- from traitlets import Int , Unicode , List
9+ from traitlets import Int , List , Unicode
1010
1111from .mcp_server import MCPServer
1212
1515
1616class MCPExtensionApp (ExtensionApp ):
1717 """The Jupyter Server MCP extension app."""
18-
18+
1919 name = "jupyter_server_mcp"
20- description = "Jupyter Server extension providing MCP server for tool registration"
21-
20+ description = (
21+ "Jupyter Server extension providing MCP server for tool registration"
22+ )
23+
2224 # Configurable traits
2325 mcp_port = Int (
24- default_value = 3001 ,
25- help = "Port for the MCP server to listen on"
26+ default_value = 3001 , help = "Port for the MCP server to listen on"
2627 ).tag (config = True )
27-
28+
2829 mcp_name = Unicode (
29- default_value = "Jupyter MCP Server" ,
30- help = "Name for the MCP server"
30+ default_value = "Jupyter MCP Server" , help = "Name for the MCP server"
3131 ).tag (config = True )
32-
32+
3333 mcp_tools = List (
3434 trait = Unicode (),
3535 default_value = [],
36- help = "List of tools to register with the MCP server. "
37- "Format: 'module_path:function_name' (e.g., 'os:getcwd', 'math:sqrt')"
36+ help = (
37+ "List of tools to register with the MCP server. "
38+ "Format: 'module_path:function_name' "
39+ "(e.g., 'os:getcwd', 'math:sqrt')"
40+ ),
3841 ).tag (config = True )
39-
42+
4043 mcp_server_instance : Optional [object ] = None
4144 mcp_server_task : Optional [asyncio .Task ] = None
42-
45+
4346 def _load_function_from_string (self , tool_spec : str ):
4447 """Load a function from a string specification.
45-
48+
4649 Args:
47- tool_spec: Function specification in format 'module_path:function_name'
48-
50+ tool_spec: Function specification in format
51+ 'module_path:function_name'
52+
4953 Returns:
5054 The loaded function object
51-
55+
5256 Raises:
5357 ValueError: If tool_spec format is invalid
5458 ImportError: If module cannot be imported
5559 AttributeError: If function not found in module
5660 """
57- if ':' not in tool_spec :
58- raise ValueError (f"Invalid tool specification '{ tool_spec } '. Expected format: 'module_path:function_name'" )
59-
60- module_path , function_name = tool_spec .rsplit (':' , 1 )
61-
61+ if ":" not in tool_spec :
62+ raise ValueError (
63+ f"Invalid tool specification '{ tool_spec } '. "
64+ f"Expected format: 'module_path:function_name'"
65+ )
66+
67+ module_path , function_name = tool_spec .rsplit (":" , 1 )
68+
6269 try :
6370 module = importlib .import_module (module_path )
6471 function = getattr (module , function_name )
6572 return function
6673 except ImportError as e :
67- raise ImportError (f"Could not import module '{ module_path } ': { e } " )
74+ raise ImportError (
75+ f"Could not import module '{ module_path } ': { e } "
76+ )
6877 except AttributeError as e :
69- raise AttributeError (f"Function '{ function_name } ' not found in module '{ module_path } ': { e } " )
70-
78+ raise AttributeError (
79+ f"Function '{ function_name } ' not found in module '{ module_path } ': { e } "
80+ )
81+
7182 def _register_configured_tools (self ):
7283 """Register tools specified in the mcp_tools configuration."""
7384 if not self .mcp_tools :
7485 return
75-
86+
7687 logger .info (f"Registering { len (self .mcp_tools )} configured tools" )
77-
88+
7889 for tool_spec in self .mcp_tools :
7990 try :
8091 function = self ._load_function_from_string (tool_spec )
8192 self .mcp_server_instance .register_tool (function )
8293 logger .info (f"✅ Registered tool: { tool_spec } " )
8394 except Exception as e :
84- logger .error (f"❌ Failed to register tool '{ tool_spec } ': { e } " )
95+ logger .error (
96+ f"❌ Failed to register tool '{ tool_spec } ': { e } "
97+ )
8598 continue
86-
99+
87100 def initialize (self ):
88101 """Initialize the extension."""
89102 super ().initialize ()
90103 # serverapp will be available as self.serverapp after parent initialization
91-
104+
92105 def initialize_handlers (self ):
93106 """Initialize the handlers for the extension."""
94107 # No HTTP handlers needed - MCP server runs on separate port
95108 pass
96-
109+
97110 def initialize_settings (self ):
98- """Initialize settings for the extension."""
111+ """Initialize settings for the extension."""
99112 # Configuration is handled by traitlets
100113 pass
101-
114+
102115 async def start_extension (self ):
103116 """Start the extension - called after Jupyter Server starts."""
104117 try :
105- self .log .info (f"Starting MCP server '{ self .mcp_name } ' on port { self .mcp_port } " )
106-
118+ self .log .info (
119+ f"Starting MCP server '{ self .mcp_name } ' on port { self .mcp_port } "
120+ )
121+
107122 self .mcp_server_instance = MCPServer (
108- parent = self ,
109- name = self .mcp_name ,
110- port = self .mcp_port
123+ parent = self , name = self .mcp_name , port = self .mcp_port
111124 )
112-
125+
113126 # Register configured tools
114127 self ._register_configured_tools ()
115-
128+
116129 # Start the MCP server in a background task
117130 self .mcp_server_task = asyncio .create_task (
118131 self .mcp_server_instance .start_server ()
119132 )
120-
133+
121134 # Give the server a moment to start
122135 await asyncio .sleep (0.5 )
123-
136+
124137 self .log .info (f"✅ MCP server started on port { self .mcp_port } " )
125138 if self .mcp_tools :
126- self .log .info (f"Registered { len (self .mcp_server_instance ._registered_tools )} tools from configuration" )
139+ registered_count = len (self .mcp_server_instance ._registered_tools )
140+ self .log .info (
141+ f"Registered { registered_count } tools from configuration"
142+ )
127143 else :
128- self .log .info ("Use mcp_server_instance.register_tool() to add tools" )
129-
144+ self .log .info (
145+ "Use mcp_server_instance.register_tool() to add tools"
146+ )
147+
130148 except Exception as e :
131149 self .log .error (f"Failed to start MCP server: { e } " )
132150 raise
133-
151+
134152 async def _start_jupyter_server_extension (self , serverapp ):
135153 """Start the extension - called after Jupyter Server starts."""
136154 await self .start_extension ()
137-
155+
138156 async def stop_extension (self ):
139157 """Stop the extension - called when Jupyter Server shuts down."""
140158 if self .mcp_server_task and not self .mcp_server_task .done ():
@@ -144,8 +162,8 @@ async def stop_extension(self):
144162 await self .mcp_server_task
145163 except asyncio .CancelledError :
146164 pass
147-
165+
148166 # Always clean up
149167 self .mcp_server_task = None
150168 self .mcp_server_instance = None
151- self .log .info ("MCP server stopped" )
169+ self .log .info ("MCP server stopped" )
0 commit comments