11"""Jupyter Server extension for managing MCP server."""
22
33import asyncio
4+ import contextlib
45import importlib
56import logging
6- from 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-
19- name = "jupyter_server_docs_mcp "
18+
19+ name = "jupyter_server_mcp "
2020 description = "Jupyter Server extension providing MCP server for tool registration"
21-
21+
2222 # Configurable traits
23- mcp_port = Int (
24- default_value = 3001 ,
25- help = "Port for the MCP server to listen on"
26- ).tag (config = True )
27-
23+ mcp_port = Int (default_value = 3001 , help = "Port for the MCP server to listen on" ).tag (
24+ config = True
25+ )
26+
2827 mcp_name = Unicode (
29- default_value = "Jupyter MCP Server" ,
30- help = "Name for the MCP server"
28+ default_value = "Jupyter MCP Server" , help = "Name for the MCP server"
3129 ).tag (config = True )
32-
30+
3331 mcp_tools = List (
3432 trait = Unicode (),
3533 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')"
34+ help = (
35+ "List of tools to register with the MCP server. "
36+ "Format: 'module_path:function_name' "
37+ "(e.g., 'os:getcwd', 'math:sqrt')"
38+ ),
3839 ).tag (config = True )
39-
40- mcp_server_instance : Optional [ object ] = None
41- mcp_server_task : Optional [ asyncio .Task ] = None
42-
40+
41+ mcp_server_instance : object | None = None
42+ mcp_server_task : asyncio .Task | None = None
43+
4344 def _load_function_from_string (self , tool_spec : str ):
4445 """Load a function from a string specification.
45-
46+
4647 Args:
47- tool_spec: Function specification in format 'module_path:function_name'
48-
48+ tool_spec: Function specification in format
49+ 'module_path:function_name'
50+
4951 Returns:
5052 The loaded function object
51-
53+
5254 Raises:
5355 ValueError: If tool_spec format is invalid
5456 ImportError: If module cannot be imported
5557 AttributeError: If function not found in module
5658 """
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-
59+ if ":" not in tool_spec :
60+ msg = (
61+ f"Invalid tool specification '{ tool_spec } '. "
62+ f"Expected format: 'module_path:function_name'"
63+ )
64+ raise ValueError (msg )
65+
66+ module_path , function_name = tool_spec .rsplit (":" , 1 )
67+
6268 try :
6369 module = importlib .import_module (module_path )
64- function = getattr (module , function_name )
65- return function
70+ return getattr (module , function_name )
6671 except ImportError as e :
67- raise ImportError (f"Could not import module '{ module_path } ': { e } " )
72+ msg = f"Could not import module '{ module_path } ': { e } "
73+ raise ImportError (msg ) from e
6874 except AttributeError as e :
69- raise AttributeError (f"Function '{ function_name } ' not found in module '{ module_path } ': { e } " )
70-
75+ msg = f"Function '{ function_name } ' not found in module '{ module_path } ': { e } "
76+ raise AttributeError (msg ) from e
77+
7178 def _register_configured_tools (self ):
7279 """Register tools specified in the mcp_tools configuration."""
7380 if not self .mcp_tools :
7481 return
75-
82+
7683 logger .info (f"Registering { len (self .mcp_tools )} configured tools" )
77-
84+
7885 for tool_spec in self .mcp_tools :
7986 try :
8087 function = self ._load_function_from_string (tool_spec )
@@ -83,69 +90,66 @@ def _register_configured_tools(self):
8390 except Exception as e :
8491 logger .error (f"❌ Failed to register tool '{ tool_spec } ': { e } " )
8592 continue
86-
93+
8794 def initialize (self ):
8895 """Initialize the extension."""
8996 super ().initialize ()
9097 # serverapp will be available as self.serverapp after parent initialization
91-
98+
9299 def initialize_handlers (self ):
93100 """Initialize the handlers for the extension."""
94101 # No HTTP handlers needed - MCP server runs on separate port
95- pass
96-
102+
97103 def initialize_settings (self ):
98- """Initialize settings for the extension."""
104+ """Initialize settings for the extension."""
99105 # Configuration is handled by traitlets
100- pass
101-
106+
102107 async def start_extension (self ):
103108 """Start the extension - called after Jupyter Server starts."""
104109 try :
105- self .log .info (f"Starting MCP server '{ self .mcp_name } ' on port { self .mcp_port } " )
106-
110+ self .log .info (
111+ f"Starting MCP server '{ self .mcp_name } ' on port { self .mcp_port } "
112+ )
113+
107114 self .mcp_server_instance = MCPServer (
108- parent = self ,
109- name = self .mcp_name ,
110- port = self .mcp_port
115+ parent = self , name = self .mcp_name , port = self .mcp_port
111116 )
112-
117+
113118 # Register configured tools
114119 self ._register_configured_tools ()
115-
120+
116121 # Start the MCP server in a background task
117122 self .mcp_server_task = asyncio .create_task (
118123 self .mcp_server_instance .start_server ()
119124 )
120-
125+
121126 # Give the server a moment to start
122127 await asyncio .sleep (0.5 )
123-
128+
124129 self .log .info (f"✅ MCP server started on port { self .mcp_port } " )
125130 if self .mcp_tools :
126- self .log .info (f"Registered { len (self .mcp_server_instance ._registered_tools )} tools from configuration" )
131+ registered_count = len (self .mcp_server_instance ._registered_tools )
132+ self .log .info (f"Registered { registered_count } tools from configuration" )
127133 else :
128134 self .log .info ("Use mcp_server_instance.register_tool() to add tools" )
129-
135+
130136 except Exception as e :
131137 self .log .error (f"Failed to start MCP server: { e } " )
132138 raise
133-
134- async def _start_jupyter_server_extension (self , serverapp ):
139+
140+ async def _start_jupyter_server_extension (self , serverapp ): # noqa: ARG002
135141 """Start the extension - called after Jupyter Server starts."""
136142 await self .start_extension ()
137-
143+
138144 async def stop_extension (self ):
139145 """Stop the extension - called when Jupyter Server shuts down."""
140146 if self .mcp_server_task and not self .mcp_server_task .done ():
141147 self .log .info ("Stopping MCP server" )
142148 self .mcp_server_task .cancel ()
143- try :
149+ with contextlib . suppress ( asyncio . CancelledError ) :
144150 await self .mcp_server_task
145- except asyncio .CancelledError :
146- pass
147-
151+
148152 # Always clean up
149153 self .mcp_server_task = None
150154 self .mcp_server_instance = None
151- self .log .info ("MCP server stopped" )
155+ self .log .info ("MCP server stopped" )
0 commit comments