napistu 0.2.4.dev4__tar.gz → 0.2.5__tar.gz
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-0.2.4.dev4 → napistu-0.2.5}/PKG-INFO +1 -1
- {napistu-0.2.4.dev4 → napistu-0.2.5}/setup.cfg +1 -1
- napistu-0.2.5/src/napistu/mcp/__main__.py +205 -0
- napistu-0.2.5/src/napistu/mcp/client.py +207 -0
- napistu-0.2.5/src/napistu/mcp/health.py +247 -0
- napistu-0.2.5/src/napistu/mcp/server.py +190 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/PKG-INFO +1 -1
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/SOURCES.txt +2 -0
- napistu-0.2.4.dev4/src/napistu/mcp/__main__.py +0 -180
- napistu-0.2.4.dev4/src/napistu/mcp/server.py +0 -86
- {napistu-0.2.4.dev4 → napistu-0.2.5}/LICENSE +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/README.md +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/pyproject.toml +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/setup.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/__main__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/consensus.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/downloads.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/identifiers.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/indices.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/bigg.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/cpr_edgelist.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/identifiers_etl.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/obo.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/psi_mi.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/reactome.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/sbml.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/string.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/trrust.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/yeast.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/codebase.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/codebase_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/documentation.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/documentation_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/execution.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/profiles.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/tutorials.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/tutorials_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mechanism_matching.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/curation.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/gaps.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/pathwayannot.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/uncompartmentalize.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/neighborhoods.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_create.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_propagation.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/paths.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/precompute.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/callr.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/netcontextr.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/rids.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/sbml_dfs_core.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/sbml_dfs_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/source.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/dependency_links.txt +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/entry_points.txt +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/requires.txt +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/top_level.txt +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/conftest.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_consensus.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_constants.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_curation.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_data/__init__.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_edgelist.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_gaps.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_gcs.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_identifiers.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_igraph.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_indices.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_mcp_documentation_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_mechanism_matching.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_net_propagation.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_net_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_netcontextr.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_obo.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_pathwayannot.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_precomputed_distances.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_rpy2.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml_dfs_core.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml_dfs_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbo.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_set_coverage.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_source.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_uncompartmentalize.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_utils.py +0 -0
- {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/utils.py +0 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
# src/napistu/mcp/__main__.py
|
2
|
+
"""
|
3
|
+
MCP (Model Context Protocol) Server CLI for Napistu.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import asyncio
|
7
|
+
import click
|
8
|
+
import click_logging
|
9
|
+
import logging
|
10
|
+
|
11
|
+
import napistu
|
12
|
+
from napistu.mcp.server import start_mcp_server
|
13
|
+
from napistu.mcp.client import (
|
14
|
+
check_server_health,
|
15
|
+
print_health_status,
|
16
|
+
list_server_resources,
|
17
|
+
read_server_resource,
|
18
|
+
)
|
19
|
+
|
20
|
+
logger = logging.getLogger(napistu.__name__)
|
21
|
+
click_logging.basic_config(logger)
|
22
|
+
|
23
|
+
|
24
|
+
@click.group()
|
25
|
+
def cli():
|
26
|
+
"""The Napistu MCP (Model Context Protocol) Server CLI"""
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
@click.group()
|
31
|
+
def server():
|
32
|
+
"""Start and manage MCP servers."""
|
33
|
+
pass
|
34
|
+
|
35
|
+
|
36
|
+
@server.command(name="start")
|
37
|
+
@click.option(
|
38
|
+
"--profile", type=click.Choice(["local", "remote", "full"]), default="remote"
|
39
|
+
)
|
40
|
+
@click.option("--host", type=str, default="127.0.0.1")
|
41
|
+
@click.option("--port", type=int, default=8765)
|
42
|
+
@click.option("--server-name", type=str)
|
43
|
+
@click_logging.simple_verbosity_option(logger)
|
44
|
+
def start_server(profile, host, port, server_name):
|
45
|
+
"""Start an MCP server with the specified profile."""
|
46
|
+
start_mcp_server(profile, host, port, server_name)
|
47
|
+
|
48
|
+
|
49
|
+
@server.command(name="local")
|
50
|
+
@click.option("--server-name", type=str, default="napistu-local")
|
51
|
+
@click_logging.simple_verbosity_option(logger)
|
52
|
+
def start_local(server_name):
|
53
|
+
"""Start a local MCP server optimized for function execution."""
|
54
|
+
start_mcp_server("local", "127.0.0.1", 8765, server_name)
|
55
|
+
|
56
|
+
|
57
|
+
@server.command(name="full")
|
58
|
+
@click.option("--server-name", type=str, default="napistu-full")
|
59
|
+
@click_logging.simple_verbosity_option(logger)
|
60
|
+
def start_full(server_name):
|
61
|
+
"""Start a full MCP server with all components enabled (local debugging)."""
|
62
|
+
start_mcp_server("full", "127.0.0.1", 8765, server_name)
|
63
|
+
|
64
|
+
|
65
|
+
@cli.command()
|
66
|
+
@click.option("--url", default="http://127.0.0.1:8765", help="Server URL")
|
67
|
+
@click_logging.simple_verbosity_option(logger)
|
68
|
+
def health(url):
|
69
|
+
"""Quick health check of MCP server."""
|
70
|
+
|
71
|
+
async def run_health_check():
|
72
|
+
print("🏥 Napistu MCP Server Health Check")
|
73
|
+
print("=" * 40)
|
74
|
+
print(f"Server URL: {url}")
|
75
|
+
print()
|
76
|
+
|
77
|
+
health = await check_server_health(server_url=url)
|
78
|
+
print_health_status(health)
|
79
|
+
|
80
|
+
asyncio.run(run_health_check())
|
81
|
+
|
82
|
+
|
83
|
+
@cli.command()
|
84
|
+
@click.option("--url", default="http://127.0.0.1:8765", help="Server URL")
|
85
|
+
@click_logging.simple_verbosity_option(logger)
|
86
|
+
def resources(url):
|
87
|
+
"""List all available resources on the MCP server."""
|
88
|
+
|
89
|
+
async def run_list_resources():
|
90
|
+
print("📋 Napistu MCP Server Resources")
|
91
|
+
print("=" * 40)
|
92
|
+
print(f"Server URL: {url}")
|
93
|
+
print()
|
94
|
+
|
95
|
+
resources = await list_server_resources(server_url=url)
|
96
|
+
|
97
|
+
if resources:
|
98
|
+
print(f"Found {len(resources)} resources:")
|
99
|
+
for resource in resources:
|
100
|
+
print(f" 📄 {resource.uri}")
|
101
|
+
if resource.name != resource.uri:
|
102
|
+
print(f" Name: {resource.name}")
|
103
|
+
if hasattr(resource, "description") and resource.description:
|
104
|
+
print(f" Description: {resource.description}")
|
105
|
+
print()
|
106
|
+
else:
|
107
|
+
print("❌ Could not retrieve resources")
|
108
|
+
|
109
|
+
asyncio.run(run_list_resources())
|
110
|
+
|
111
|
+
|
112
|
+
@cli.command()
|
113
|
+
@click.argument("resource_uri")
|
114
|
+
@click.option("--url", default="http://127.0.0.1:8765", help="Server URL")
|
115
|
+
@click.option(
|
116
|
+
"--output", type=click.File("w"), default="-", help="Output file (default: stdout)"
|
117
|
+
)
|
118
|
+
@click_logging.simple_verbosity_option(logger)
|
119
|
+
def read(resource_uri, url, output):
|
120
|
+
"""Read a specific resource from the MCP server."""
|
121
|
+
|
122
|
+
async def run_read_resource():
|
123
|
+
print(
|
124
|
+
f"📖 Reading Resource: {resource_uri}",
|
125
|
+
file=output if output.name != "<stdout>" else None,
|
126
|
+
)
|
127
|
+
print(f"Server URL: {url}", file=output if output.name != "<stdout>" else None)
|
128
|
+
print("=" * 50, file=output if output.name != "<stdout>" else None)
|
129
|
+
|
130
|
+
content = await read_server_resource(resource_uri, server_url=url)
|
131
|
+
|
132
|
+
if content:
|
133
|
+
print(content, file=output)
|
134
|
+
else:
|
135
|
+
print(
|
136
|
+
"❌ Could not read resource",
|
137
|
+
file=output if output.name != "<stdout>" else None,
|
138
|
+
)
|
139
|
+
|
140
|
+
asyncio.run(run_read_resource())
|
141
|
+
|
142
|
+
|
143
|
+
@cli.command()
|
144
|
+
@click.option("--local-url", default="http://127.0.0.1:8765", help="Local server URL")
|
145
|
+
@click.option("--remote-url", required=True, help="Remote server URL")
|
146
|
+
@click_logging.simple_verbosity_option(logger)
|
147
|
+
def compare(local_url, remote_url):
|
148
|
+
"""Compare health between local and remote servers."""
|
149
|
+
|
150
|
+
async def run_comparison():
|
151
|
+
print("🔍 Local vs Remote Server Comparison")
|
152
|
+
print("=" * 50)
|
153
|
+
|
154
|
+
print(f"\n📍 Local Server: {local_url}")
|
155
|
+
local_health = await check_server_health(server_url=local_url)
|
156
|
+
print_health_status(local_health)
|
157
|
+
|
158
|
+
print(f"\n🌐 Remote Server: {remote_url}")
|
159
|
+
remote_health = await check_server_health(server_url=remote_url)
|
160
|
+
print_health_status(remote_health)
|
161
|
+
|
162
|
+
# Compare results
|
163
|
+
print("\n📊 Comparison Summary:")
|
164
|
+
if local_health and remote_health:
|
165
|
+
local_components = local_health.get("components", {})
|
166
|
+
remote_components = remote_health.get("components", {})
|
167
|
+
|
168
|
+
all_components = set(local_components.keys()) | set(
|
169
|
+
remote_components.keys()
|
170
|
+
)
|
171
|
+
|
172
|
+
for component in sorted(all_components):
|
173
|
+
local_status = local_components.get(component, {}).get(
|
174
|
+
"status", "missing"
|
175
|
+
)
|
176
|
+
remote_status = remote_components.get(component, {}).get(
|
177
|
+
"status", "missing"
|
178
|
+
)
|
179
|
+
|
180
|
+
if local_status == remote_status == "healthy":
|
181
|
+
icon = "✅"
|
182
|
+
elif local_status != remote_status:
|
183
|
+
icon = "⚠️ "
|
184
|
+
else:
|
185
|
+
icon = "❌"
|
186
|
+
|
187
|
+
print(
|
188
|
+
f" {icon} {component}: Local={local_status}, Remote={remote_status}"
|
189
|
+
)
|
190
|
+
else:
|
191
|
+
print(" ❌ Cannot compare - one or both servers unreachable")
|
192
|
+
|
193
|
+
asyncio.run(run_comparison())
|
194
|
+
|
195
|
+
|
196
|
+
# Add commands to the CLI
|
197
|
+
cli.add_command(server)
|
198
|
+
cli.add_command(health)
|
199
|
+
cli.add_command(resources)
|
200
|
+
cli.add_command(read)
|
201
|
+
cli.add_command(compare)
|
202
|
+
|
203
|
+
|
204
|
+
if __name__ == "__main__":
|
205
|
+
cli()
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# src/napistu/mcp/client.py
|
2
|
+
"""
|
3
|
+
MCP client for testing and interacting with Napistu MCP servers.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
from typing import Optional, Dict, Any, Mapping
|
9
|
+
|
10
|
+
from fastmcp import Client
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
async def check_server_health(
|
16
|
+
server_url: str = "http://127.0.0.1:8765",
|
17
|
+
) -> Optional[Dict[str, Any]]:
|
18
|
+
"""
|
19
|
+
Health check using FastMCP client.
|
20
|
+
|
21
|
+
Parameters
|
22
|
+
----------
|
23
|
+
server_url : str, optional
|
24
|
+
Server URL for HTTP transport. Defaults to 'http://127.0.0.1:8765'.
|
25
|
+
|
26
|
+
Returns
|
27
|
+
-------
|
28
|
+
Optional[Dict[str, Any]]
|
29
|
+
Dictionary containing health status information if successful, None if failed.
|
30
|
+
The dictionary contains:
|
31
|
+
- status : str
|
32
|
+
Overall server status ('healthy', 'degraded', or 'unhealthy')
|
33
|
+
- timestamp : str
|
34
|
+
ISO format timestamp of the health check
|
35
|
+
- version : str
|
36
|
+
Version of the Napistu package
|
37
|
+
- components : Dict[str, Dict[str, str]]
|
38
|
+
Status of each component ('healthy', 'inactive', or 'unavailable')
|
39
|
+
"""
|
40
|
+
|
41
|
+
try:
|
42
|
+
# FastMCP streamable-http requires the /mcp path
|
43
|
+
mcp_url = server_url.rstrip("/") + "/mcp"
|
44
|
+
logger.info(f"Connecting to MCP server at: {mcp_url}")
|
45
|
+
|
46
|
+
client = Client(mcp_url)
|
47
|
+
|
48
|
+
async with client:
|
49
|
+
logger.info("✅ FastMCP client connected")
|
50
|
+
|
51
|
+
# List all available resources
|
52
|
+
resources = await client.list_resources()
|
53
|
+
logger.info(f"Found {len(resources)} resources")
|
54
|
+
|
55
|
+
# Find health resource
|
56
|
+
health_resource = None
|
57
|
+
for resource in resources:
|
58
|
+
uri_str = str(resource.uri).lower()
|
59
|
+
if "health" in uri_str:
|
60
|
+
health_resource = resource
|
61
|
+
logger.info(f"Found health resource: {resource.uri}")
|
62
|
+
break
|
63
|
+
|
64
|
+
if not health_resource:
|
65
|
+
logger.error("No health resource found")
|
66
|
+
logger.info(f"Available resources: {[str(r.uri) for r in resources]}")
|
67
|
+
return None
|
68
|
+
|
69
|
+
# Read the health resource
|
70
|
+
logger.info(f"Reading health resource: {health_resource.uri}")
|
71
|
+
result = await client.read_resource(str(health_resource.uri))
|
72
|
+
|
73
|
+
if result and len(result) > 0 and hasattr(result[0], "text"):
|
74
|
+
try:
|
75
|
+
health_data = json.loads(result[0].text)
|
76
|
+
logger.info("✅ Health check successful")
|
77
|
+
return health_data
|
78
|
+
except json.JSONDecodeError as e:
|
79
|
+
logger.error(f"Failed to parse health JSON: {e}")
|
80
|
+
logger.error(f"Raw response: {result[0].text}")
|
81
|
+
return None
|
82
|
+
else:
|
83
|
+
logger.error(f"No valid response from health resource: {result}")
|
84
|
+
return None
|
85
|
+
|
86
|
+
except Exception as e:
|
87
|
+
logger.error(f"Health check failed: {str(e)}")
|
88
|
+
if hasattr(e, "__traceback__"):
|
89
|
+
import traceback
|
90
|
+
|
91
|
+
logger.error("Traceback:\n" + "".join(traceback.format_tb(e.__traceback__)))
|
92
|
+
return None
|
93
|
+
|
94
|
+
|
95
|
+
def print_health_status(health: Optional[Mapping[str, Any]]) -> None:
|
96
|
+
"""
|
97
|
+
Pretty print health status information.
|
98
|
+
|
99
|
+
Parameters
|
100
|
+
----------
|
101
|
+
health : Optional[Mapping[str, Any]]
|
102
|
+
Health status dictionary from check_server_health, or None if health check failed.
|
103
|
+
Expected to contain:
|
104
|
+
- status : str
|
105
|
+
Overall server status
|
106
|
+
- components : Dict[str, Dict[str, str]]
|
107
|
+
Status of each component
|
108
|
+
- timestamp : str, optional
|
109
|
+
ISO format timestamp
|
110
|
+
- version : str, optional
|
111
|
+
Package version
|
112
|
+
|
113
|
+
Returns
|
114
|
+
-------
|
115
|
+
None
|
116
|
+
Prints health status information to stdout.
|
117
|
+
"""
|
118
|
+
if not health:
|
119
|
+
print("❌ Could not get health status")
|
120
|
+
print("Check the logs above for detailed error information")
|
121
|
+
return
|
122
|
+
|
123
|
+
status = health.get("status", "unknown")
|
124
|
+
print(f"\nServer Status: {status}")
|
125
|
+
|
126
|
+
components = health.get("components", {})
|
127
|
+
if components:
|
128
|
+
print("\nComponents:")
|
129
|
+
for name, comp_status in components.items():
|
130
|
+
icon = "✅" if comp_status.get("status") == "healthy" else "❌"
|
131
|
+
print(f" {icon} {name}: {comp_status.get('status', 'unknown')}")
|
132
|
+
|
133
|
+
# Show additional info if available
|
134
|
+
if "timestamp" in health:
|
135
|
+
print(f"\nTimestamp: {health['timestamp']}")
|
136
|
+
if "version" in health:
|
137
|
+
print(f"Version: {health['version']}")
|
138
|
+
|
139
|
+
|
140
|
+
async def list_server_resources(
|
141
|
+
server_url: str = "http://127.0.0.1:8765",
|
142
|
+
) -> Optional[list]:
|
143
|
+
"""
|
144
|
+
List all available resources on the MCP server.
|
145
|
+
|
146
|
+
Parameters
|
147
|
+
----------
|
148
|
+
server_url : str, optional
|
149
|
+
Server URL for HTTP transport. Defaults to 'http://127.0.0.1:8765'.
|
150
|
+
|
151
|
+
Returns
|
152
|
+
-------
|
153
|
+
Optional[list]
|
154
|
+
List of available resources, or None if failed.
|
155
|
+
"""
|
156
|
+
try:
|
157
|
+
mcp_url = server_url.rstrip("/") + "/mcp"
|
158
|
+
logger.info(f"Listing resources from: {mcp_url}")
|
159
|
+
|
160
|
+
client = Client(mcp_url)
|
161
|
+
|
162
|
+
async with client:
|
163
|
+
resources = await client.list_resources()
|
164
|
+
logger.info(f"Found {len(resources)} resources")
|
165
|
+
return resources
|
166
|
+
|
167
|
+
except Exception as e:
|
168
|
+
logger.error(f"Failed to list resources: {str(e)}")
|
169
|
+
return None
|
170
|
+
|
171
|
+
|
172
|
+
async def read_server_resource(
|
173
|
+
resource_uri: str, server_url: str = "http://127.0.0.1:8765"
|
174
|
+
) -> Optional[str]:
|
175
|
+
"""
|
176
|
+
Read a specific resource from the MCP server.
|
177
|
+
|
178
|
+
Parameters
|
179
|
+
----------
|
180
|
+
resource_uri : str
|
181
|
+
URI of the resource to read (e.g., 'napistu://health')
|
182
|
+
server_url : str, optional
|
183
|
+
Server URL for HTTP transport. Defaults to 'http://127.0.0.1:8765'.
|
184
|
+
|
185
|
+
Returns
|
186
|
+
-------
|
187
|
+
Optional[str]
|
188
|
+
Resource content as text, or None if failed.
|
189
|
+
"""
|
190
|
+
try:
|
191
|
+
mcp_url = server_url.rstrip("/") + "/mcp"
|
192
|
+
logger.info(f"Reading resource {resource_uri} from: {mcp_url}")
|
193
|
+
|
194
|
+
client = Client(mcp_url)
|
195
|
+
|
196
|
+
async with client:
|
197
|
+
result = await client.read_resource(resource_uri)
|
198
|
+
|
199
|
+
if result and len(result) > 0 and hasattr(result[0], "text"):
|
200
|
+
return result[0].text
|
201
|
+
else:
|
202
|
+
logger.error(f"No content found for resource: {resource_uri}")
|
203
|
+
return None
|
204
|
+
|
205
|
+
except Exception as e:
|
206
|
+
logger.error(f"Failed to read resource {resource_uri}: {str(e)}")
|
207
|
+
return None
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# src/napistu/mcp/health.py
|
2
|
+
"""
|
3
|
+
Health check endpoint for the MCP server when deployed to Cloud Run.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Dict, Any, TypeVar
|
8
|
+
from datetime import datetime
|
9
|
+
from fastmcp import FastMCP
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
# Type variable for the FastMCP decorator return type
|
14
|
+
T = TypeVar("T")
|
15
|
+
|
16
|
+
# Global cache for component health status
|
17
|
+
_health_cache = {"status": "initializing", "components": {}, "last_check": None}
|
18
|
+
|
19
|
+
|
20
|
+
def register_components(mcp: FastMCP) -> None:
|
21
|
+
"""
|
22
|
+
Register health check components with the MCP server.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
mcp : FastMCP
|
27
|
+
FastMCP server instance to register the health endpoint with.
|
28
|
+
"""
|
29
|
+
|
30
|
+
@mcp.resource("napistu://health")
|
31
|
+
async def health_check() -> Dict[str, Any]:
|
32
|
+
"""
|
33
|
+
Health check endpoint for deployment monitoring.
|
34
|
+
Returns current cached health status.
|
35
|
+
"""
|
36
|
+
return _health_cache
|
37
|
+
|
38
|
+
@mcp.tool("napistu://health/check")
|
39
|
+
async def check_current_health() -> Dict[str, Any]:
|
40
|
+
"""
|
41
|
+
Tool to actively check current component health.
|
42
|
+
This performs real-time checks and updates the cached status.
|
43
|
+
"""
|
44
|
+
global _health_cache
|
45
|
+
try:
|
46
|
+
health_status = {
|
47
|
+
"status": "healthy",
|
48
|
+
"timestamp": datetime.utcnow().isoformat(),
|
49
|
+
"version": _get_version(),
|
50
|
+
"components": await _check_components(),
|
51
|
+
}
|
52
|
+
|
53
|
+
# Check if any components failed
|
54
|
+
failed_components = [
|
55
|
+
name
|
56
|
+
for name, status in health_status["components"].items()
|
57
|
+
if status["status"] == "unavailable"
|
58
|
+
]
|
59
|
+
|
60
|
+
if failed_components:
|
61
|
+
health_status["status"] = "degraded"
|
62
|
+
health_status["failed_components"] = failed_components
|
63
|
+
|
64
|
+
# Update the global cache with latest status
|
65
|
+
health_status["last_check"] = datetime.utcnow().isoformat()
|
66
|
+
_health_cache.update(health_status)
|
67
|
+
logger.info(f"Updated health cache - Status: {health_status['status']}")
|
68
|
+
|
69
|
+
return health_status
|
70
|
+
|
71
|
+
except Exception as e:
|
72
|
+
logger.error(f"Health check failed: {e}")
|
73
|
+
error_status = {
|
74
|
+
"status": "unhealthy",
|
75
|
+
"error": str(e),
|
76
|
+
"timestamp": datetime.utcnow().isoformat(),
|
77
|
+
"last_check": datetime.utcnow().isoformat(),
|
78
|
+
}
|
79
|
+
# Update cache even on error
|
80
|
+
_health_cache.update(error_status)
|
81
|
+
return error_status
|
82
|
+
|
83
|
+
|
84
|
+
async def initialize_components() -> bool:
|
85
|
+
"""
|
86
|
+
Initialize health check components.
|
87
|
+
Performs initial health check and caches the result.
|
88
|
+
|
89
|
+
Returns
|
90
|
+
-------
|
91
|
+
bool
|
92
|
+
True if initialization is successful
|
93
|
+
"""
|
94
|
+
global _health_cache
|
95
|
+
|
96
|
+
logger.info("Initializing health check components...")
|
97
|
+
|
98
|
+
try:
|
99
|
+
# Check initial component health
|
100
|
+
component_status = await _check_components()
|
101
|
+
|
102
|
+
# Update cache
|
103
|
+
_health_cache.update(
|
104
|
+
{
|
105
|
+
"status": "healthy",
|
106
|
+
"components": component_status,
|
107
|
+
"timestamp": datetime.utcnow().isoformat(),
|
108
|
+
"version": _get_version(),
|
109
|
+
"last_check": datetime.utcnow().isoformat(),
|
110
|
+
}
|
111
|
+
)
|
112
|
+
|
113
|
+
# Check for failed components
|
114
|
+
failed_components = [
|
115
|
+
name
|
116
|
+
for name, status in component_status.items()
|
117
|
+
if status["status"] == "unavailable"
|
118
|
+
]
|
119
|
+
|
120
|
+
if failed_components:
|
121
|
+
_health_cache["status"] = "degraded"
|
122
|
+
_health_cache["failed_components"] = failed_components
|
123
|
+
|
124
|
+
logger.info(f"Health check initialization complete: {_health_cache['status']}")
|
125
|
+
return True
|
126
|
+
|
127
|
+
except Exception as e:
|
128
|
+
logger.error(f"Health check initialization failed: {e}")
|
129
|
+
_health_cache["status"] = "unhealthy"
|
130
|
+
_health_cache["error"] = str(e)
|
131
|
+
return False
|
132
|
+
|
133
|
+
|
134
|
+
def _check_component_health(
|
135
|
+
component_name: str, module_name: str, cache_attr: str
|
136
|
+
) -> Dict[str, str]:
|
137
|
+
"""
|
138
|
+
Check the health of a single MCP component by verifying its cache is initialized and contains data.
|
139
|
+
|
140
|
+
Parameters
|
141
|
+
----------
|
142
|
+
component_name : str
|
143
|
+
Name of the component (for importing)
|
144
|
+
module_name : str
|
145
|
+
Full module path for importing
|
146
|
+
cache_attr : str
|
147
|
+
Name of the cache/context attribute to check
|
148
|
+
|
149
|
+
Returns
|
150
|
+
-------
|
151
|
+
Dict[str, str]
|
152
|
+
Dictionary containing component health status:
|
153
|
+
- status : str
|
154
|
+
One of: 'healthy', 'inactive', or 'unavailable'
|
155
|
+
- error : str, optional
|
156
|
+
Error message if status is 'unavailable'
|
157
|
+
"""
|
158
|
+
try:
|
159
|
+
module = __import__(module_name, fromlist=[component_name])
|
160
|
+
cache = getattr(module, cache_attr, None)
|
161
|
+
logger.info(
|
162
|
+
f"Checking {component_name} health - Cache exists: {cache is not None}"
|
163
|
+
)
|
164
|
+
|
165
|
+
# Component specific checks for actual data
|
166
|
+
if cache:
|
167
|
+
if component_name == "documentation":
|
168
|
+
# Check if any documentation section has content
|
169
|
+
has_data = any(bool(section) for section in cache.values())
|
170
|
+
logger.info(f"Documentation sections: {list(cache.keys())}")
|
171
|
+
elif component_name == "codebase":
|
172
|
+
# Check if any codebase section has content
|
173
|
+
has_data = any(bool(section) for section in cache.values())
|
174
|
+
logger.info(f"Codebase sections: {list(cache.keys())}")
|
175
|
+
elif component_name == "tutorials":
|
176
|
+
# Check if tutorials section has content
|
177
|
+
has_data = bool(cache.get("tutorials", {}))
|
178
|
+
logger.info(f"Tutorials cache: {bool(cache.get('tutorials', {}))}")
|
179
|
+
elif component_name == "execution":
|
180
|
+
# Check if session context has more than just napistu module
|
181
|
+
has_data = len(cache) > 0
|
182
|
+
logger.info(f"Execution context: {list(cache.keys())}")
|
183
|
+
else:
|
184
|
+
has_data = bool(cache)
|
185
|
+
|
186
|
+
if has_data:
|
187
|
+
logger.info(f"{component_name} is healthy")
|
188
|
+
return {"status": "healthy"}
|
189
|
+
|
190
|
+
logger.info(f"{component_name} is inactive")
|
191
|
+
return {"status": "inactive"}
|
192
|
+
except Exception as e:
|
193
|
+
logger.error(f"{component_name} check failed: {str(e)}")
|
194
|
+
return {"status": "unavailable", "error": str(e)}
|
195
|
+
|
196
|
+
|
197
|
+
async def _check_components() -> Dict[str, Dict[str, Any]]:
|
198
|
+
"""
|
199
|
+
Check the health of individual MCP components by verifying their caches.
|
200
|
+
|
201
|
+
Returns
|
202
|
+
-------
|
203
|
+
Dict[str, Dict[str, Any]]
|
204
|
+
Dictionary mapping component names to their health status:
|
205
|
+
- {component_name} : Dict[str, str]
|
206
|
+
Health status for each component, containing:
|
207
|
+
- status : str
|
208
|
+
One of: 'healthy', 'inactive', or 'unavailable'
|
209
|
+
- error : str, optional
|
210
|
+
Error message if status is 'unavailable'
|
211
|
+
"""
|
212
|
+
# Define component configurations - cache vars that indicate initialization
|
213
|
+
component_configs = {
|
214
|
+
"documentation": ("napistu.mcp.documentation", "_docs_cache"),
|
215
|
+
"codebase": ("napistu.mcp.codebase", "_codebase_cache"),
|
216
|
+
"tutorials": ("napistu.mcp.tutorials", "_tutorial_cache"),
|
217
|
+
"execution": ("napistu.mcp.execution", "_session_context"),
|
218
|
+
}
|
219
|
+
|
220
|
+
logger.info("Starting component health checks...")
|
221
|
+
logger.info(f"Checking components: {list(component_configs.keys())}")
|
222
|
+
|
223
|
+
# Check each component's cache
|
224
|
+
results = {
|
225
|
+
name: _check_component_health(name, module_path, cache_attr)
|
226
|
+
for name, (module_path, cache_attr) in component_configs.items()
|
227
|
+
}
|
228
|
+
|
229
|
+
logger.info(f"Health check results: {results}")
|
230
|
+
return results
|
231
|
+
|
232
|
+
|
233
|
+
def _get_version() -> str:
|
234
|
+
"""
|
235
|
+
Get the Napistu version.
|
236
|
+
|
237
|
+
Returns
|
238
|
+
-------
|
239
|
+
str
|
240
|
+
Version string of the Napistu package, or 'unknown' if not available.
|
241
|
+
"""
|
242
|
+
try:
|
243
|
+
import napistu
|
244
|
+
|
245
|
+
return getattr(napistu, "__version__", "unknown")
|
246
|
+
except ImportError:
|
247
|
+
return "unknown"
|