Skip to content

Commit 2fbfbf6

Browse files
committed
added tools property to ExtensionPoint, get_tools to ExtensionManager and ServerApp
1 parent e12feb9 commit 2fbfbf6

File tree

2 files changed

+75
-0
lines changed

2 files changed

+75
-0
lines changed

jupyter_server/extension/manager.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,43 @@
99
from traitlets import Any, Bool, Dict, HasTraits, Instance, List, Unicode, default, observe
1010
from traitlets import validate as validate_trait
1111
from traitlets.config import LoggingConfigurable
12+
import jsonschema
13+
1214

1315
from .config import ExtensionConfigManager
1416
from .utils import ExtensionMetadataError, ExtensionModuleNotFound, get_loader, get_metadata
1517

1618

19+
MCP_TOOL_SCHEMA = {
20+
"type": "object",
21+
"properties": {
22+
"name": {"type": "string"},
23+
"description": {"type": "string"},
24+
"inputSchema": {
25+
"type": "object",
26+
"properties": {
27+
"type": {"type": "string", "enum": ["object"]},
28+
"properties": {"type": "object"},
29+
"required": {"type": "array", "items": {"type": "string"}}
30+
},
31+
"required": ["type", "properties"]
32+
},
33+
"annotations": {
34+
"type": "object",
35+
"properties": {
36+
"title": {"type": "string"},
37+
"readOnlyHint": {"type": "boolean"},
38+
"destructiveHint": {"type": "boolean"},
39+
"idempotentHint": {"type": "boolean"},
40+
"openWorldHint": {"type": "boolean"}
41+
},
42+
"additionalProperties": True
43+
}
44+
},
45+
"required": ["name", "inputSchema"],
46+
"additionalProperties": False
47+
}
48+
1749
class ExtensionPoint(HasTraits):
1850
"""A simple API for connecting to a Jupyter Server extension
1951
point defined by metadata and importable from a Python package.
@@ -96,6 +128,28 @@ def name(self):
96128
def module(self):
97129
"""The imported module (using importlib.import_module)"""
98130
return self._module
131+
132+
@property
133+
def tools(self):
134+
"""Structured tools exposed by this extension point, if any."""
135+
loc = self.app or self.module
136+
if not loc:
137+
return {}
138+
139+
tools_func = getattr(loc, "jupyter_server_extension_tools", None)
140+
if not callable(tools_func):
141+
return {}
142+
143+
tools = {}
144+
try:
145+
definitions = tools_func()
146+
for name, tool in definitions.items():
147+
jsonschema.validate(instance=tool["metadata"], schema=MCP_TOOL_SCHEMA)
148+
tools[name] = tool
149+
except Exception as e:
150+
# You could also `self.log.warning(...)` if you pass the log around
151+
print(f"[tool-discovery] Failed to load tools from {self.module_name}: {e}")
152+
return tools
99153

100154
def _get_linker(self):
101155
"""Get a linker."""
@@ -111,6 +165,7 @@ def _get_linker(self):
111165
)
112166
return linker
113167

168+
114169
def _get_loader(self):
115170
"""Get a loader."""
116171
loc = self.app
@@ -443,6 +498,23 @@ def load_all_extensions(self):
443498
for name in self.sorted_extensions:
444499
self.load_extension(name)
445500

501+
502+
def get_tools(self) -> Dict[str, Any]:
503+
"""Aggregate tools from all extensions that expose them."""
504+
all_tools = {}
505+
506+
for ext_name, ext_pkg in self.extensions.items():
507+
if not ext_pkg.enabled:
508+
continue
509+
510+
for point in ext_pkg.extension_points.values():
511+
for name, tool in point.tools.items(): # 🔥 <— new property!
512+
if name in all_tools:
513+
raise ValueError(f"Duplicate tool name detected: '{name}'")
514+
all_tools[name] = tool
515+
516+
return all_tools
517+
446518
async def start_all_extensions(self):
447519
"""Start all enabled extensions."""
448520
# Sort the extension names to enforce deterministic loading

jupyter_server/serverapp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2540,6 +2540,9 @@ def load_server_extensions(self) -> None:
25402540
"""
25412541
self.extension_manager.load_all_extensions()
25422542

2543+
def get_tools(self):
2544+
return self.extension_manager.get_tools()
2545+
25432546
def init_mime_overrides(self) -> None:
25442547
# On some Windows machines, an application has registered incorrect
25452548
# mimetypes in the registry.

0 commit comments

Comments
 (0)