napistu 0.1.0__py3-none-any.whl → 0.2.4.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napistu/__init__.py +1 -1
- napistu/consensus.py +1010 -513
- napistu/constants.py +24 -0
- napistu/gcs/constants.py +2 -2
- napistu/gcs/downloads.py +57 -25
- napistu/gcs/utils.py +21 -0
- napistu/identifiers.py +105 -6
- napistu/ingestion/constants.py +0 -1
- napistu/ingestion/obo.py +24 -8
- napistu/ingestion/psi_mi.py +20 -5
- napistu/ingestion/reactome.py +8 -32
- napistu/mcp/__init__.py +69 -0
- napistu/mcp/__main__.py +180 -0
- napistu/mcp/codebase.py +182 -0
- napistu/mcp/codebase_utils.py +298 -0
- napistu/mcp/constants.py +72 -0
- napistu/mcp/documentation.py +166 -0
- napistu/mcp/documentation_utils.py +235 -0
- napistu/mcp/execution.py +382 -0
- napistu/mcp/profiles.py +73 -0
- napistu/mcp/server.py +86 -0
- napistu/mcp/tutorials.py +124 -0
- napistu/mcp/tutorials_utils.py +230 -0
- napistu/mcp/utils.py +47 -0
- napistu/mechanism_matching.py +782 -26
- napistu/modify/constants.py +41 -0
- napistu/modify/curation.py +4 -1
- napistu/modify/gaps.py +243 -156
- napistu/modify/pathwayannot.py +26 -8
- napistu/network/neighborhoods.py +16 -7
- napistu/network/net_create.py +209 -54
- napistu/network/net_propagation.py +118 -0
- napistu/network/net_utils.py +1 -32
- napistu/rpy2/netcontextr.py +10 -7
- napistu/rpy2/rids.py +7 -5
- napistu/sbml_dfs_core.py +46 -29
- napistu/sbml_dfs_utils.py +37 -1
- napistu/source.py +8 -2
- napistu/utils.py +67 -8
- napistu-0.2.4.dev2.dist-info/METADATA +84 -0
- napistu-0.2.4.dev2.dist-info/RECORD +95 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/WHEEL +1 -1
- tests/conftest.py +11 -5
- tests/test_consensus.py +4 -1
- tests/test_gaps.py +127 -0
- tests/test_gcs.py +3 -2
- tests/test_igraph.py +14 -0
- tests/test_mcp_documentation_utils.py +13 -0
- tests/test_mechanism_matching.py +658 -0
- tests/test_net_propagation.py +89 -0
- tests/test_net_utils.py +83 -0
- tests/test_sbml.py +2 -0
- tests/{test_sbml_dfs_create.py → test_sbml_dfs_core.py} +68 -4
- tests/test_utils.py +81 -0
- napistu-0.1.0.dist-info/METADATA +0 -56
- napistu-0.1.0.dist-info/RECORD +0 -77
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/entry_points.txt +0 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/licenses/LICENSE +0 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/top_level.txt +0 -0
napistu/mcp/execution.py
ADDED
@@ -0,0 +1,382 @@
|
|
1
|
+
"""
|
2
|
+
Function execution components for the Napistu MCP server.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Dict, List, Any, Optional
|
6
|
+
import inspect
|
7
|
+
|
8
|
+
# Global storage for session context and objects
|
9
|
+
_session_context = {}
|
10
|
+
_session_objects = {}
|
11
|
+
|
12
|
+
|
13
|
+
async def initialize_components() -> bool:
|
14
|
+
"""
|
15
|
+
Initialize execution components.
|
16
|
+
|
17
|
+
Returns
|
18
|
+
-------
|
19
|
+
bool
|
20
|
+
True if initialization is successful.
|
21
|
+
"""
|
22
|
+
global _session_context, _session_objects
|
23
|
+
import napistu
|
24
|
+
|
25
|
+
_session_context["napistu"] = napistu
|
26
|
+
return True
|
27
|
+
|
28
|
+
|
29
|
+
def register_object(name: str, obj: Any) -> None:
|
30
|
+
"""
|
31
|
+
Register an object with the execution component.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
name: Name to reference the object by
|
35
|
+
obj: The object to register
|
36
|
+
"""
|
37
|
+
global _session_objects
|
38
|
+
_session_objects[name] = obj
|
39
|
+
print(f"Registered object '{name}' with MCP server")
|
40
|
+
|
41
|
+
|
42
|
+
def register_components(mcp, session_context=None, object_registry=None):
|
43
|
+
"""
|
44
|
+
Register function execution components with the MCP server.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
mcp: FastMCP server instance
|
48
|
+
session_context: Dictionary of the user's current session (e.g., globals())
|
49
|
+
object_registry: Dictionary of named objects to make available
|
50
|
+
"""
|
51
|
+
global _session_context, _session_objects
|
52
|
+
|
53
|
+
# Initialize context
|
54
|
+
if session_context:
|
55
|
+
_session_context = session_context
|
56
|
+
|
57
|
+
if object_registry:
|
58
|
+
_session_objects = object_registry
|
59
|
+
|
60
|
+
import napistu
|
61
|
+
|
62
|
+
_session_context["napistu"] = napistu
|
63
|
+
|
64
|
+
# Register resources
|
65
|
+
@mcp.resource("napistu-local://registry")
|
66
|
+
async def get_registry_summary() -> Dict[str, Any]:
|
67
|
+
"""
|
68
|
+
Get a summary of all objects registered with the server.
|
69
|
+
"""
|
70
|
+
return {
|
71
|
+
"object_count": len(_session_objects),
|
72
|
+
"object_names": list(_session_objects.keys()),
|
73
|
+
"object_types": {
|
74
|
+
name: type(obj).__name__ for name, obj in _session_objects.items()
|
75
|
+
},
|
76
|
+
}
|
77
|
+
|
78
|
+
@mcp.resource("napistu-local://environment")
|
79
|
+
async def get_environment_info() -> Dict[str, Any]:
|
80
|
+
"""
|
81
|
+
Get information about the local Python environment.
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
import napistu
|
85
|
+
|
86
|
+
napistu_version = getattr(napistu, "__version__", "unknown")
|
87
|
+
except ImportError:
|
88
|
+
napistu_version = "not installed"
|
89
|
+
|
90
|
+
import sys
|
91
|
+
|
92
|
+
return {
|
93
|
+
"python_version": sys.version,
|
94
|
+
"napistu_version": napistu_version,
|
95
|
+
"platform": sys.platform,
|
96
|
+
"registered_objects": list(_session_objects.keys()),
|
97
|
+
}
|
98
|
+
|
99
|
+
# Register tools
|
100
|
+
@mcp.tool()
|
101
|
+
async def list_registry() -> Dict[str, Any]:
|
102
|
+
"""
|
103
|
+
List all objects registered with the server.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
Dictionary with information about registered objects
|
107
|
+
"""
|
108
|
+
result = {}
|
109
|
+
|
110
|
+
for name, obj in _session_objects.items():
|
111
|
+
obj_type = type(obj).__name__
|
112
|
+
|
113
|
+
# Get additional info based on object type
|
114
|
+
if hasattr(obj, "shape"): # For pandas DataFrame or numpy array
|
115
|
+
obj_info = {
|
116
|
+
"type": obj_type,
|
117
|
+
"shape": str(obj.shape),
|
118
|
+
}
|
119
|
+
elif hasattr(obj, "__len__"): # For lists, dicts, etc.
|
120
|
+
obj_info = {
|
121
|
+
"type": obj_type,
|
122
|
+
"length": len(obj),
|
123
|
+
}
|
124
|
+
else:
|
125
|
+
obj_info = {
|
126
|
+
"type": obj_type,
|
127
|
+
}
|
128
|
+
|
129
|
+
result[name] = obj_info
|
130
|
+
|
131
|
+
return result
|
132
|
+
|
133
|
+
@mcp.tool()
|
134
|
+
async def describe_object(object_name: str) -> Dict[str, Any]:
|
135
|
+
"""
|
136
|
+
Get detailed information about a registered object.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
object_name: Name of the registered object
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
Dictionary with object information
|
143
|
+
"""
|
144
|
+
if object_name not in _session_objects:
|
145
|
+
return {"error": f"Object '{object_name}' not found in registry"}
|
146
|
+
|
147
|
+
obj = _session_objects[object_name]
|
148
|
+
obj_type = type(obj).__name__
|
149
|
+
|
150
|
+
# Basic info for all objects
|
151
|
+
result = {
|
152
|
+
"name": object_name,
|
153
|
+
"type": obj_type,
|
154
|
+
"methods": [],
|
155
|
+
"attributes": [],
|
156
|
+
}
|
157
|
+
|
158
|
+
# Add methods and attributes
|
159
|
+
for name in dir(obj):
|
160
|
+
if name.startswith("_"):
|
161
|
+
continue
|
162
|
+
|
163
|
+
try:
|
164
|
+
attr = getattr(obj, name)
|
165
|
+
|
166
|
+
if callable(attr):
|
167
|
+
# Method
|
168
|
+
sig = str(inspect.signature(attr))
|
169
|
+
doc = inspect.getdoc(attr) or ""
|
170
|
+
result["methods"].append(
|
171
|
+
{
|
172
|
+
"name": name,
|
173
|
+
"signature": sig,
|
174
|
+
"docstring": doc,
|
175
|
+
}
|
176
|
+
)
|
177
|
+
else:
|
178
|
+
# Attribute
|
179
|
+
attr_type = type(attr).__name__
|
180
|
+
result["attributes"].append(
|
181
|
+
{
|
182
|
+
"name": name,
|
183
|
+
"type": attr_type,
|
184
|
+
}
|
185
|
+
)
|
186
|
+
except Exception:
|
187
|
+
# Skip attributes that can't be accessed
|
188
|
+
pass
|
189
|
+
|
190
|
+
return result
|
191
|
+
|
192
|
+
@mcp.tool()
|
193
|
+
async def execute_function(
|
194
|
+
function_name: str,
|
195
|
+
object_name: Optional[str] = None,
|
196
|
+
args: Optional[List] = None,
|
197
|
+
kwargs: Optional[Dict] = None,
|
198
|
+
) -> Dict[str, Any]:
|
199
|
+
"""
|
200
|
+
Execute a Napistu function on a registered object.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
function_name: Name of the function to execute
|
204
|
+
object_name: Name of the registered object to operate on (if method call)
|
205
|
+
args: Positional arguments to pass to the function
|
206
|
+
kwargs: Keyword arguments to pass to the function
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Dictionary with execution results
|
210
|
+
"""
|
211
|
+
args = args or []
|
212
|
+
kwargs = kwargs or {}
|
213
|
+
|
214
|
+
try:
|
215
|
+
if object_name:
|
216
|
+
# Method call on an object
|
217
|
+
if object_name not in _session_objects:
|
218
|
+
return {"error": f"Object '{object_name}' not found in registry"}
|
219
|
+
|
220
|
+
obj = _session_objects[object_name]
|
221
|
+
|
222
|
+
if not hasattr(obj, function_name):
|
223
|
+
return {
|
224
|
+
"error": f"Method '{function_name}' not found on object '{object_name}'"
|
225
|
+
}
|
226
|
+
|
227
|
+
func = getattr(obj, function_name)
|
228
|
+
result = func(*args, **kwargs)
|
229
|
+
else:
|
230
|
+
# Global function call
|
231
|
+
if function_name in _session_context:
|
232
|
+
# Function from session context
|
233
|
+
func = _session_context[function_name]
|
234
|
+
result = func(*args, **kwargs)
|
235
|
+
else:
|
236
|
+
# Try to find the function in Napistu
|
237
|
+
try:
|
238
|
+
import napistu
|
239
|
+
|
240
|
+
# Split function name by dots for nested modules
|
241
|
+
parts = function_name.split(".")
|
242
|
+
current = napistu
|
243
|
+
|
244
|
+
for part in parts[:-1]:
|
245
|
+
current = getattr(current, part)
|
246
|
+
|
247
|
+
func = getattr(current, parts[-1])
|
248
|
+
result = func(*args, **kwargs)
|
249
|
+
except (ImportError, AttributeError):
|
250
|
+
return {"error": f"Function '{function_name}' not found"}
|
251
|
+
|
252
|
+
# Register result if it's a return value
|
253
|
+
if result is not None:
|
254
|
+
result_name = f"result_{len(_session_objects) + 1}"
|
255
|
+
_session_objects[result_name] = result
|
256
|
+
|
257
|
+
# Basic type conversion for JSON serialization
|
258
|
+
if hasattr(result, "to_dict"):
|
259
|
+
# For pandas DataFrame or similar
|
260
|
+
return {
|
261
|
+
"success": True,
|
262
|
+
"result_name": result_name,
|
263
|
+
"result_type": type(result).__name__,
|
264
|
+
"result_preview": (
|
265
|
+
result.to_dict()
|
266
|
+
if hasattr(result, "__len__") and len(result) < 10
|
267
|
+
else "Result too large to preview"
|
268
|
+
),
|
269
|
+
}
|
270
|
+
elif hasattr(result, "to_json"):
|
271
|
+
# For objects with JSON serialization
|
272
|
+
return {
|
273
|
+
"success": True,
|
274
|
+
"result_name": result_name,
|
275
|
+
"result_type": type(result).__name__,
|
276
|
+
"result_preview": result.to_json(),
|
277
|
+
}
|
278
|
+
elif hasattr(result, "__dict__"):
|
279
|
+
# For custom objects
|
280
|
+
return {
|
281
|
+
"success": True,
|
282
|
+
"result_name": result_name,
|
283
|
+
"result_type": type(result).__name__,
|
284
|
+
"result_preview": str(result),
|
285
|
+
}
|
286
|
+
else:
|
287
|
+
# For simple types
|
288
|
+
return {
|
289
|
+
"success": True,
|
290
|
+
"result_name": result_name,
|
291
|
+
"result_type": type(result).__name__,
|
292
|
+
"result_preview": str(result),
|
293
|
+
}
|
294
|
+
else:
|
295
|
+
return {
|
296
|
+
"success": True,
|
297
|
+
"result": None,
|
298
|
+
}
|
299
|
+
except Exception as e:
|
300
|
+
import traceback
|
301
|
+
|
302
|
+
return {
|
303
|
+
"error": str(e),
|
304
|
+
"traceback": traceback.format_exc(),
|
305
|
+
}
|
306
|
+
|
307
|
+
@mcp.tool()
|
308
|
+
async def search_paths(
|
309
|
+
source_node: str,
|
310
|
+
target_node: str,
|
311
|
+
network_object: str,
|
312
|
+
max_depth: int = 3,
|
313
|
+
) -> Dict[str, Any]:
|
314
|
+
"""
|
315
|
+
Find paths between two nodes in a network.
|
316
|
+
|
317
|
+
Args:
|
318
|
+
source_node: Source node identifier
|
319
|
+
target_node: Target node identifier
|
320
|
+
network_object: Name of the registered network object
|
321
|
+
max_depth: Maximum path length
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
Dictionary with paths found
|
325
|
+
"""
|
326
|
+
if network_object not in _session_objects:
|
327
|
+
return {"error": f"Network object '{network_object}' not found in registry"}
|
328
|
+
|
329
|
+
network = _session_objects[network_object]
|
330
|
+
|
331
|
+
try:
|
332
|
+
# Import necessary modules
|
333
|
+
import napistu
|
334
|
+
|
335
|
+
# Check if the object is a valid network type
|
336
|
+
if hasattr(network, "find_paths"):
|
337
|
+
# Direct method call
|
338
|
+
paths = network.find_paths(
|
339
|
+
source_node, target_node, max_depth=max_depth
|
340
|
+
)
|
341
|
+
elif hasattr(napistu.graph, "find_paths"):
|
342
|
+
# Function call
|
343
|
+
paths = napistu.graph.find_paths(
|
344
|
+
network, source_node, target_node, max_depth=max_depth
|
345
|
+
)
|
346
|
+
else:
|
347
|
+
return {"error": "Could not find appropriate path-finding function"}
|
348
|
+
|
349
|
+
# Register result
|
350
|
+
result_name = f"paths_{len(_session_objects) + 1}"
|
351
|
+
_session_objects[result_name] = paths
|
352
|
+
|
353
|
+
# Return results
|
354
|
+
if hasattr(paths, "to_dict"):
|
355
|
+
return {
|
356
|
+
"success": True,
|
357
|
+
"result_name": result_name,
|
358
|
+
"paths_found": (
|
359
|
+
len(paths) if hasattr(paths, "__len__") else "unknown"
|
360
|
+
),
|
361
|
+
"result_preview": (
|
362
|
+
paths.to_dict()
|
363
|
+
if hasattr(paths, "__len__") and len(paths) < 10
|
364
|
+
else "Result too large to preview"
|
365
|
+
),
|
366
|
+
}
|
367
|
+
else:
|
368
|
+
return {
|
369
|
+
"success": True,
|
370
|
+
"result_name": result_name,
|
371
|
+
"paths_found": (
|
372
|
+
len(paths) if hasattr(paths, "__len__") else "unknown"
|
373
|
+
),
|
374
|
+
"result_preview": str(paths),
|
375
|
+
}
|
376
|
+
except Exception as e:
|
377
|
+
import traceback
|
378
|
+
|
379
|
+
return {
|
380
|
+
"error": str(e),
|
381
|
+
"traceback": traceback.format_exc(),
|
382
|
+
}
|
napistu/mcp/profiles.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
from typing import Dict, Any
|
2
|
+
|
3
|
+
|
4
|
+
class ServerProfile:
|
5
|
+
"""Base profile for MCP server configuration."""
|
6
|
+
|
7
|
+
def __init__(self, **kwargs):
|
8
|
+
self.config = {
|
9
|
+
# Default configuration
|
10
|
+
"server_name": "napistu-mcp",
|
11
|
+
"enable_documentation": False,
|
12
|
+
"enable_execution": False,
|
13
|
+
"enable_codebase": False,
|
14
|
+
"enable_tutorials": False,
|
15
|
+
"session_context": None,
|
16
|
+
"object_registry": None,
|
17
|
+
"tutorials_path": None,
|
18
|
+
}
|
19
|
+
# Override with provided kwargs
|
20
|
+
self.config.update(kwargs)
|
21
|
+
|
22
|
+
def get_config(self) -> Dict[str, Any]:
|
23
|
+
"""Return the configuration dictionary."""
|
24
|
+
return self.config.copy()
|
25
|
+
|
26
|
+
def update(self, **kwargs) -> "ServerProfile":
|
27
|
+
"""Update profile with additional configuration."""
|
28
|
+
new_profile = ServerProfile(**self.config)
|
29
|
+
new_profile.config.update(kwargs)
|
30
|
+
return new_profile
|
31
|
+
|
32
|
+
|
33
|
+
# Pre-defined profiles
|
34
|
+
LOCAL_PROFILE = ServerProfile(server_name="napistu-local", enable_execution=True)
|
35
|
+
|
36
|
+
REMOTE_PROFILE = ServerProfile(
|
37
|
+
server_name="napistu-docs",
|
38
|
+
enable_documentation=True,
|
39
|
+
enable_codebase=True,
|
40
|
+
enable_tutorials=True,
|
41
|
+
)
|
42
|
+
|
43
|
+
FULL_PROFILE = ServerProfile(
|
44
|
+
server_name="napistu-full",
|
45
|
+
enable_documentation=True,
|
46
|
+
enable_codebase=True,
|
47
|
+
enable_execution=True,
|
48
|
+
enable_tutorials=True,
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
def get_profile(profile_name: str, **overrides) -> ServerProfile:
|
53
|
+
"""
|
54
|
+
Get a predefined profile with optional overrides.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
profile_name: Name of the profile ('local', 'remote', or 'full')
|
58
|
+
**overrides: Configuration overrides
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
ServerProfile instance
|
62
|
+
"""
|
63
|
+
profiles = {
|
64
|
+
"local": LOCAL_PROFILE,
|
65
|
+
"remote": REMOTE_PROFILE,
|
66
|
+
"full": FULL_PROFILE,
|
67
|
+
}
|
68
|
+
|
69
|
+
if profile_name not in profiles:
|
70
|
+
raise ValueError(f"Unknown profile: {profile_name}")
|
71
|
+
|
72
|
+
# Return a copy of the profile with overrides
|
73
|
+
return profiles[profile_name].update(**overrides)
|
napistu/mcp/server.py
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
"""
|
2
|
+
Core MCP server implementation for Napistu.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from mcp.server import FastMCP
|
8
|
+
|
9
|
+
from napistu.mcp import codebase
|
10
|
+
from napistu.mcp import documentation
|
11
|
+
from napistu.mcp import execution
|
12
|
+
from napistu.mcp import tutorials
|
13
|
+
|
14
|
+
from napistu.mcp.profiles import ServerProfile
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
def create_server(profile: ServerProfile, **kwargs) -> FastMCP:
|
20
|
+
"""
|
21
|
+
Create an MCP server based on a profile configuration.
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
profile : ServerProfile
|
26
|
+
Server profile to use. All configuration must be set in the profile.
|
27
|
+
**kwargs
|
28
|
+
Additional arguments to pass to the FastMCP constructor such as host and port.
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
FastMCP
|
33
|
+
Configured FastMCP server instance.
|
34
|
+
"""
|
35
|
+
|
36
|
+
config = profile.get_config()
|
37
|
+
|
38
|
+
# Create the server with FastMCP-specific parameters
|
39
|
+
# Pass all kwargs directly to the FastMCP constructor
|
40
|
+
mcp = FastMCP(config["server_name"], **kwargs)
|
41
|
+
|
42
|
+
if config["enable_documentation"]:
|
43
|
+
logger.info("Registering documentation components")
|
44
|
+
documentation.register_components(mcp)
|
45
|
+
if config["enable_codebase"]:
|
46
|
+
logger.info("Registering codebase components")
|
47
|
+
codebase.register_components(mcp)
|
48
|
+
if config["enable_execution"]:
|
49
|
+
logger.info("Registering execution components")
|
50
|
+
execution.register_components(
|
51
|
+
mcp,
|
52
|
+
session_context=config["session_context"],
|
53
|
+
object_registry=config["object_registry"],
|
54
|
+
)
|
55
|
+
if config["enable_tutorials"]:
|
56
|
+
logger.info("Registering tutorials components")
|
57
|
+
tutorials.register_components(mcp)
|
58
|
+
return mcp
|
59
|
+
|
60
|
+
|
61
|
+
async def initialize_components(profile: ServerProfile) -> None:
|
62
|
+
"""
|
63
|
+
Asynchronously initialize all enabled components for the MCP server, using the provided ServerProfile.
|
64
|
+
|
65
|
+
Parameters
|
66
|
+
----------
|
67
|
+
profile : ServerProfile
|
68
|
+
The profile whose configuration determines which components to initialize.
|
69
|
+
|
70
|
+
Returns
|
71
|
+
-------
|
72
|
+
None
|
73
|
+
"""
|
74
|
+
config = profile.get_config()
|
75
|
+
if config["enable_documentation"]:
|
76
|
+
logger.info("Initializing documentation components")
|
77
|
+
await documentation.initialize_components()
|
78
|
+
if config["enable_codebase"]:
|
79
|
+
logger.info("Initializing codebase components")
|
80
|
+
await codebase.initialize_components()
|
81
|
+
if config["enable_tutorials"]:
|
82
|
+
logger.info("Initializing tutorials components")
|
83
|
+
await tutorials.initialize_components()
|
84
|
+
if config["enable_execution"]:
|
85
|
+
logger.info("Initializing execution components")
|
86
|
+
await execution.initialize_components()
|
napistu/mcp/tutorials.py
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
Tutorial components for the Napistu MCP server.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Dict, List, Any
|
6
|
+
import logging
|
7
|
+
|
8
|
+
from fastmcp import FastMCP
|
9
|
+
|
10
|
+
from napistu.mcp import tutorials_utils
|
11
|
+
from napistu.mcp import utils as mcp_utils
|
12
|
+
from napistu.mcp.constants import TUTORIALS
|
13
|
+
from napistu.mcp.constants import TUTORIAL_URLS
|
14
|
+
from napistu.mcp.constants import TOOL_VARS
|
15
|
+
|
16
|
+
# Global cache for tutorial content
|
17
|
+
_tutorial_cache: Dict[str, Dict[str, str]] = {
|
18
|
+
TUTORIALS.TUTORIALS: {},
|
19
|
+
}
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
async def initialize_components() -> bool:
|
25
|
+
"""
|
26
|
+
Initialize tutorial components by preloading all tutorials into the cache.
|
27
|
+
|
28
|
+
Returns
|
29
|
+
-------
|
30
|
+
bool
|
31
|
+
True if initialization is successful.
|
32
|
+
"""
|
33
|
+
global _tutorial_cache
|
34
|
+
for k, v in TUTORIAL_URLS.items():
|
35
|
+
_tutorial_cache[TUTORIALS.TUTORIALS][k] = (
|
36
|
+
await tutorials_utils.get_tutorial_markdown(k)
|
37
|
+
)
|
38
|
+
return True
|
39
|
+
|
40
|
+
|
41
|
+
def register_components(mcp: FastMCP) -> None:
|
42
|
+
"""
|
43
|
+
Register tutorial components with the MCP server.
|
44
|
+
|
45
|
+
Parameters
|
46
|
+
----------
|
47
|
+
mcp : FastMCP
|
48
|
+
FastMCP server instance.
|
49
|
+
"""
|
50
|
+
|
51
|
+
# Register resources
|
52
|
+
@mcp.resource("napistu://tutorials/index")
|
53
|
+
async def get_tutorial_index() -> List[Dict[str, Any]]:
|
54
|
+
"""
|
55
|
+
Get the index of all available tutorials.
|
56
|
+
|
57
|
+
Returns
|
58
|
+
-------
|
59
|
+
List[dict]
|
60
|
+
List of dictionaries with tutorial IDs and URLs.
|
61
|
+
"""
|
62
|
+
return [
|
63
|
+
{"id": tutorial_id, "url": url}
|
64
|
+
for tutorial_id, url in TUTORIAL_URLS.items()
|
65
|
+
]
|
66
|
+
|
67
|
+
@mcp.resource("napistu://tutorials/content/{tutorial_id}")
|
68
|
+
async def get_tutorial_content_resource(tutorial_id: str) -> Dict[str, Any]:
|
69
|
+
"""
|
70
|
+
Get the content of a specific tutorial as markdown.
|
71
|
+
|
72
|
+
Parameters
|
73
|
+
----------
|
74
|
+
tutorial_id : str
|
75
|
+
ID of the tutorial.
|
76
|
+
|
77
|
+
Returns
|
78
|
+
-------
|
79
|
+
dict
|
80
|
+
Dictionary with markdown content and format.
|
81
|
+
|
82
|
+
Raises
|
83
|
+
------
|
84
|
+
Exception
|
85
|
+
If the tutorial cannot be loaded.
|
86
|
+
"""
|
87
|
+
content = _tutorial_cache[TUTORIALS.TUTORIALS].get(tutorial_id)
|
88
|
+
if content is None:
|
89
|
+
try:
|
90
|
+
content = await tutorials_utils.get_tutorial_markdown(tutorial_id)
|
91
|
+
_tutorial_cache[TUTORIALS.TUTORIALS][tutorial_id] = content
|
92
|
+
except Exception as e:
|
93
|
+
logger.error(f"Tutorial {tutorial_id} could not be loaded: {e}")
|
94
|
+
raise
|
95
|
+
return {
|
96
|
+
"content": content,
|
97
|
+
"format": "markdown",
|
98
|
+
}
|
99
|
+
|
100
|
+
@mcp.tool()
|
101
|
+
async def search_tutorials(query: str) -> List[Dict[str, Any]]:
|
102
|
+
"""
|
103
|
+
Search tutorials for a specific query.
|
104
|
+
|
105
|
+
Parameters
|
106
|
+
----------
|
107
|
+
query : str
|
108
|
+
Search term.
|
109
|
+
|
110
|
+
Returns
|
111
|
+
-------
|
112
|
+
List[dict]
|
113
|
+
List of matching tutorials with metadata and snippet.
|
114
|
+
"""
|
115
|
+
results: List[Dict[str, Any]] = []
|
116
|
+
for tutorial_id, content in _tutorial_cache[TUTORIALS.TUTORIALS].items():
|
117
|
+
if query.lower() in content.lower():
|
118
|
+
results.append(
|
119
|
+
{
|
120
|
+
TOOL_VARS.ID: tutorial_id,
|
121
|
+
TOOL_VARS.SNIPPET: mcp_utils.get_snippet(content, query),
|
122
|
+
}
|
123
|
+
)
|
124
|
+
return results
|