napistu 0.2.4.dev5__py3-none-any.whl → 0.2.5__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/mcp/__main__.py +152 -127
- napistu/mcp/client.py +207 -0
- napistu/mcp/health.py +247 -0
- napistu/mcp/server.py +105 -1
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/METADATA +1 -1
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/RECORD +10 -8
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/WHEEL +0 -0
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/entry_points.txt +0 -0
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {napistu-0.2.4.dev5.dist-info → napistu-0.2.5.dist-info}/top_level.txt +0 -0
napistu/mcp/__main__.py
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
# src/napistu/mcp/__main__.py
|
1
2
|
"""
|
2
|
-
MCP (Model Context Protocol) Server for Napistu.
|
3
|
+
MCP (Model Context Protocol) Server CLI for Napistu.
|
3
4
|
"""
|
4
5
|
|
5
6
|
import asyncio
|
6
|
-
import logging
|
7
7
|
import click
|
8
8
|
import click_logging
|
9
|
+
import logging
|
9
10
|
|
10
11
|
import napistu
|
11
|
-
from napistu.mcp.
|
12
|
-
from napistu.mcp.
|
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
|
+
)
|
13
19
|
|
14
20
|
logger = logging.getLogger(napistu.__name__)
|
15
21
|
click_logging.basic_config(logger)
|
@@ -29,152 +35,171 @@ def server():
|
|
29
35
|
|
30
36
|
@server.command(name="start")
|
31
37
|
@click.option(
|
32
|
-
"--profile",
|
33
|
-
type=click.Choice(["local", "remote", "full"]),
|
34
|
-
default="remote",
|
35
|
-
help="Predefined configuration profile",
|
38
|
+
"--profile", type=click.Choice(["local", "remote", "full"]), default="remote"
|
36
39
|
)
|
37
|
-
@click.option("--
|
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)
|
38
43
|
@click_logging.simple_verbosity_option(logger)
|
39
|
-
def start_server(profile, server_name):
|
44
|
+
def start_server(profile, host, port, server_name):
|
40
45
|
"""Start an MCP server with the specified profile."""
|
41
|
-
|
42
|
-
config = {}
|
43
|
-
if server_name:
|
44
|
-
config["server_name"] = server_name
|
45
|
-
|
46
|
-
# Get profile with overrides
|
47
|
-
server_profile = get_profile(profile, **config)
|
48
|
-
|
49
|
-
# Create and start the server
|
50
|
-
logger.info(f"Starting Napistu MCP Server with {profile} profile...")
|
51
|
-
server = create_server(server_profile)
|
52
|
-
asyncio.run(server.start())
|
46
|
+
start_mcp_server(profile, host, port, server_name)
|
53
47
|
|
54
48
|
|
55
49
|
@server.command(name="local")
|
56
|
-
@click.option(
|
57
|
-
"--server-name", type=str, default="napistu-local", help="Name of the MCP server"
|
58
|
-
)
|
50
|
+
@click.option("--server-name", type=str, default="napistu-local")
|
59
51
|
@click_logging.simple_verbosity_option(logger)
|
60
52
|
def start_local(server_name):
|
61
53
|
"""Start a local MCP server optimized for function execution."""
|
62
|
-
|
63
|
-
server_profile = get_profile("local", server_name=server_name)
|
54
|
+
start_mcp_server("local", "127.0.0.1", 8765, server_name)
|
64
55
|
|
65
|
-
# Create and start the server
|
66
|
-
logger.info("Starting Napistu local MCP Server...")
|
67
|
-
server = create_server(server_profile)
|
68
|
-
asyncio.run(server.start())
|
69
56
|
|
70
|
-
|
71
|
-
@
|
72
|
-
@click.option(
|
73
|
-
"--server-name", type=str, default="napistu-docs", help="Name of the MCP server"
|
74
|
-
)
|
75
|
-
@click.option("--codebase-path", type=str, help="Path to the Napistu codebase")
|
76
|
-
@click.option(
|
77
|
-
"--docs-paths",
|
78
|
-
type=str,
|
79
|
-
help="Comma-separated list of paths to documentation files",
|
80
|
-
)
|
81
|
-
@click.option("--tutorials-path", type=str, help="Path to the tutorials directory")
|
57
|
+
@server.command(name="full")
|
58
|
+
@click.option("--server-name", type=str, default="napistu-full")
|
82
59
|
@click_logging.simple_verbosity_option(logger)
|
83
|
-
def
|
84
|
-
"""Start a
|
85
|
-
|
86
|
-
config = {"server_name": server_name}
|
87
|
-
if tutorials_path:
|
88
|
-
config["tutorials_path"] = tutorials_path
|
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)
|
89
63
|
|
90
|
-
# Get profile with overrides
|
91
|
-
server_profile = get_profile("remote", **config)
|
92
64
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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."""
|
97
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()
|
98
76
|
|
99
|
-
|
100
|
-
|
101
|
-
"""Enable or disable specific MCP server components."""
|
102
|
-
pass
|
103
|
-
|
77
|
+
health = await check_server_health(server_url=url)
|
78
|
+
print_health_status(health)
|
104
79
|
|
105
|
-
|
106
|
-
def list_components():
|
107
|
-
"""List available MCP server components."""
|
108
|
-
click.echo("Available MCP server components:")
|
109
|
-
click.echo(" - documentation: Documentation components")
|
110
|
-
click.echo(" - codebase: Codebase exploration components")
|
111
|
-
click.echo(" - execution: Function execution components")
|
112
|
-
click.echo(" - tutorials: Tutorial components")
|
80
|
+
asyncio.run(run_health_check())
|
113
81
|
|
114
82
|
|
115
|
-
@
|
116
|
-
@click.option(
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
)
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
)
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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")
|
140
115
|
@click.option(
|
141
|
-
"--
|
142
|
-
type=str,
|
143
|
-
help="Comma-separated list of paths to documentation files",
|
116
|
+
"--output", type=click.File("w"), default="-", help="Output file (default: stdout)"
|
144
117
|
)
|
145
|
-
@click.option("--tutorials-path", type=str, help="Path to the tutorials directory")
|
146
118
|
@click_logging.simple_verbosity_option(logger)
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
176
197
|
cli.add_command(server)
|
177
|
-
cli.add_command(
|
198
|
+
cli.add_command(health)
|
199
|
+
cli.add_command(resources)
|
200
|
+
cli.add_command(read)
|
201
|
+
cli.add_command(compare)
|
202
|
+
|
178
203
|
|
179
204
|
if __name__ == "__main__":
|
180
205
|
cli()
|
napistu/mcp/client.py
ADDED
@@ -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
|
napistu/mcp/health.py
ADDED
@@ -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"
|
napistu/mcp/server.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# src/napistu/mcp/server.py
|
1
2
|
"""
|
2
3
|
Core MCP server implementation for Napistu.
|
3
4
|
"""
|
4
5
|
|
6
|
+
import asyncio
|
5
7
|
import logging
|
8
|
+
import os
|
6
9
|
|
7
10
|
from mcp.server import FastMCP
|
8
11
|
|
@@ -10,8 +13,9 @@ from napistu.mcp import codebase
|
|
10
13
|
from napistu.mcp import documentation
|
11
14
|
from napistu.mcp import execution
|
12
15
|
from napistu.mcp import tutorials
|
16
|
+
from napistu.mcp import health
|
13
17
|
|
14
|
-
from napistu.mcp.profiles import ServerProfile
|
18
|
+
from napistu.mcp.profiles import ServerProfile, get_profile
|
15
19
|
|
16
20
|
logger = logging.getLogger(__name__)
|
17
21
|
|
@@ -55,6 +59,11 @@ def create_server(profile: ServerProfile, **kwargs) -> FastMCP:
|
|
55
59
|
if config["enable_tutorials"]:
|
56
60
|
logger.info("Registering tutorials components")
|
57
61
|
tutorials.register_components(mcp)
|
62
|
+
|
63
|
+
# Always register health components
|
64
|
+
health.register_components(mcp)
|
65
|
+
logger.info("Registered health components")
|
66
|
+
|
58
67
|
return mcp
|
59
68
|
|
60
69
|
|
@@ -72,6 +81,7 @@ async def initialize_components(profile: ServerProfile) -> None:
|
|
72
81
|
None
|
73
82
|
"""
|
74
83
|
config = profile.get_config()
|
84
|
+
|
75
85
|
if config["enable_documentation"]:
|
76
86
|
logger.info("Initializing documentation components")
|
77
87
|
await documentation.initialize_components()
|
@@ -84,3 +94,97 @@ async def initialize_components(profile: ServerProfile) -> None:
|
|
84
94
|
if config["enable_execution"]:
|
85
95
|
logger.info("Initializing execution components")
|
86
96
|
await execution.initialize_components()
|
97
|
+
|
98
|
+
# Initialize health components last since they monitor the other components
|
99
|
+
logger.info("Initializing health components")
|
100
|
+
await health.initialize_components()
|
101
|
+
|
102
|
+
|
103
|
+
def start_mcp_server(
|
104
|
+
profile_name: str = "remote",
|
105
|
+
host: str = "0.0.0.0",
|
106
|
+
port: int = 8080,
|
107
|
+
server_name: str | None = None,
|
108
|
+
) -> None:
|
109
|
+
"""
|
110
|
+
Start MCP server - main entry point for server startup.
|
111
|
+
|
112
|
+
The server will be started with HTTP transport on the specified host and port.
|
113
|
+
Environment variables can override the default configuration:
|
114
|
+
- MCP_PROFILE: Server profile to use
|
115
|
+
- HOST: Host to bind to
|
116
|
+
- PORT: Port to bind to
|
117
|
+
- MCP_SERVER_NAME: Name of the MCP server
|
118
|
+
|
119
|
+
Parameters
|
120
|
+
----------
|
121
|
+
profile_name : str, optional
|
122
|
+
Server profile to use ('local', 'remote', 'full'). Defaults to 'remote'.
|
123
|
+
host : str, optional
|
124
|
+
Host address to bind the server to. Defaults to '0.0.0.0'.
|
125
|
+
port : int, optional
|
126
|
+
Port number to listen on. Defaults to 8080.
|
127
|
+
server_name : str | None, optional
|
128
|
+
Custom name for the MCP server. If None, will be generated from profile name.
|
129
|
+
Defaults to None.
|
130
|
+
|
131
|
+
Returns
|
132
|
+
-------
|
133
|
+
None
|
134
|
+
This function runs indefinitely until interrupted.
|
135
|
+
|
136
|
+
Notes
|
137
|
+
-----
|
138
|
+
The server uses HTTP transport (streamable-http) for all connections.
|
139
|
+
Components are initialized asynchronously before the server starts.
|
140
|
+
Health components are always registered and initialized last.
|
141
|
+
"""
|
142
|
+
# Set up logging
|
143
|
+
logging.basicConfig(level=logging.INFO)
|
144
|
+
logger = logging.getLogger("napistu")
|
145
|
+
|
146
|
+
# Get configuration from environment variables (for Cloud Run)
|
147
|
+
env_profile = os.getenv("MCP_PROFILE", profile_name)
|
148
|
+
env_host = os.getenv("HOST", host)
|
149
|
+
env_port = int(os.getenv("PORT", port))
|
150
|
+
env_server_name = os.getenv(
|
151
|
+
"MCP_SERVER_NAME", server_name or f"napistu-{env_profile}"
|
152
|
+
)
|
153
|
+
|
154
|
+
logger.info("Starting Napistu MCP Server")
|
155
|
+
logger.info(f" Profile: {env_profile}")
|
156
|
+
logger.info(f" Host: {env_host}")
|
157
|
+
logger.info(f" Port: {env_port}")
|
158
|
+
logger.info(f" Server Name: {env_server_name}")
|
159
|
+
logger.info(" Transport: streamable-http")
|
160
|
+
|
161
|
+
# Create session context for execution components
|
162
|
+
session_context = {}
|
163
|
+
object_registry = {}
|
164
|
+
|
165
|
+
# Get profile with configuration
|
166
|
+
profile: ServerProfile = get_profile(
|
167
|
+
env_profile,
|
168
|
+
session_context=session_context,
|
169
|
+
object_registry=object_registry,
|
170
|
+
server_name=env_server_name,
|
171
|
+
)
|
172
|
+
|
173
|
+
# Create server
|
174
|
+
mcp = create_server(profile, host=env_host, port=env_port)
|
175
|
+
|
176
|
+
# Initialize components first (separate async call)
|
177
|
+
async def init_components():
|
178
|
+
logger.info("Initializing MCP components...")
|
179
|
+
await initialize_components(profile)
|
180
|
+
logger.info("✅ Component initialization complete")
|
181
|
+
|
182
|
+
# Run initialization
|
183
|
+
asyncio.run(init_components())
|
184
|
+
|
185
|
+
# Debug info
|
186
|
+
logger.info(f"Server settings: {mcp.settings}")
|
187
|
+
|
188
|
+
logger.info("🚀 Starting MCP server...")
|
189
|
+
logger.info(f"Using HTTP transport on http://{env_host}:{env_port}")
|
190
|
+
mcp.run(transport="streamable-http")
|
@@ -26,15 +26,17 @@ napistu/ingestion/string.py,sha256=tsaHrjppgFbl9NnRcB2DytpoontqrpfQL65zD9HPgEM,1
|
|
26
26
|
napistu/ingestion/trrust.py,sha256=ccjZc_eF3PdxxurnukiEo_e0-aKc_3z22NYbaJBtHdY,9774
|
27
27
|
napistu/ingestion/yeast.py,sha256=bwFBNxRq-dLDaddgBL1hpfZj0eQ56nBXyR_9n0NZT9Y,5233
|
28
28
|
napistu/mcp/__init__.py,sha256=gDkP4J4vAjgq96_760lXKDURPUpQxVIxaRO9XzYrqdA,1970
|
29
|
-
napistu/mcp/__main__.py,sha256=
|
29
|
+
napistu/mcp/__main__.py,sha256=UcxLERiTFp7H8OvA5bQ1KhCnmOQKp1DsQ4oYr5WFVw0,6410
|
30
|
+
napistu/mcp/client.py,sha256=nTMKgEG-yEGM0bS54LXK85jVtlJvDXOniWsQbnj4FOI,6616
|
30
31
|
napistu/mcp/codebase.py,sha256=H32R5vRJqsLpClURIUpemTO8F_YnFW1RDdaeEbYWotk,5674
|
31
32
|
napistu/mcp/codebase_utils.py,sha256=r1nbDmGzq-NaH9cT11jC53mEjszQpwQ0uZUJkMHvgVs,10567
|
32
33
|
napistu/mcp/constants.py,sha256=s0anHxVre6i6JYFQimB_ppRLDdqiCxbMHNVEYvf6O0U,2852
|
33
34
|
napistu/mcp/documentation.py,sha256=L99GF5ilhq7CpzYkRy2BeOzKVQnolK_S9wqf8ChZY2Y,5216
|
34
35
|
napistu/mcp/documentation_utils.py,sha256=JH3BfVk2dpSLUvnC77iaeTIRknOBpNMBQ2qhquUhuJM,7099
|
35
36
|
napistu/mcp/execution.py,sha256=cYhLzIvihtLjG4J195FZuCM8uVihtgW-R6J4zuPAY5s,12422
|
37
|
+
napistu/mcp/health.py,sha256=l6Y8houdip7IOtYZ_pPJrFAFfynN6PntVJwdkcoQKmg,8249
|
36
38
|
napistu/mcp/profiles.py,sha256=Nbr1e-n7muJMcY0HxuEJQePUQWPM2koQ9roVLEZa7Pg,2027
|
37
|
-
napistu/mcp/server.py,sha256=
|
39
|
+
napistu/mcp/server.py,sha256=R-8BrBccT-bpc3OoKk1TY9wBZek4EUyE_5VDHCAou9c,6010
|
38
40
|
napistu/mcp/tutorials.py,sha256=QMX32aWaqRqj4vIc_PVDdT_t55ZBpw4xOWnfQyewZMk,3395
|
39
41
|
napistu/mcp/tutorials_utils.py,sha256=6_s6FP2i8Na6VaKVzLDnSnA5JQVgyd3ZHEIz8HSHtzU,6599
|
40
42
|
napistu/mcp/utils.py,sha256=WB4c6s8aPZLgi_Wvhhq0DE8Cnz2QGff0V8hrF1feVRg,1296
|
@@ -57,7 +59,7 @@ napistu/rpy2/callr.py,sha256=76ICWj7Jso-qrYLNfiV-DgPyrMTdRXz_EhyGOD9CbFM,4301
|
|
57
59
|
napistu/rpy2/constants.py,sha256=JpJqsxImZis8fFFfePXYdbkhUZhXDZoHSHVf92w1h8U,2619
|
58
60
|
napistu/rpy2/netcontextr.py,sha256=fZKd3NXXu5nfB-z0UW6wedxBkZA8dJ4uB1uOD9wI_eg,16523
|
59
61
|
napistu/rpy2/rids.py,sha256=lKyOujjdosuN-Oc54uCQI1nKchTx4zXPwgFbG7QX7d8,24153
|
60
|
-
napistu-0.2.
|
62
|
+
napistu-0.2.5.dist-info/licenses/LICENSE,sha256=kW8wVT__JWoHjl2BbbJDAZInWa9AxzJeR_uv6-i5x1g,1063
|
61
63
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
62
64
|
tests/conftest.py,sha256=Xj5PWhedsR7HELbJfBY5XXzv_BAHbFMc1K2n6Nrbrno,2595
|
63
65
|
tests/test_consensus.py,sha256=3dJvvPsPG7bHbw_FY4Pm647N_Gt_Ud9157OKYfPCUd4,9502
|
@@ -88,8 +90,8 @@ tests/test_uncompartmentalize.py,sha256=nAk5kfAVLU9a2VWe2x2HYVcKqj-EnwmwddERIPRa
|
|
88
90
|
tests/test_utils.py,sha256=B9frW_ugWtGWsM-q7Lw7pnfE9a_d6LZTow9BrWU9UDw,17939
|
89
91
|
tests/utils.py,sha256=SoWQ_5roJteFGcMaOeEiQ5ucwq3Z2Fa3AAs9iXHTsJY,749
|
90
92
|
tests/test_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
91
|
-
napistu-0.2.
|
92
|
-
napistu-0.2.
|
93
|
-
napistu-0.2.
|
94
|
-
napistu-0.2.
|
95
|
-
napistu-0.2.
|
93
|
+
napistu-0.2.5.dist-info/METADATA,sha256=0jOcMA8xm4Amf49L-zrgpSiWlvgernzgeA6Y_jv243c,3197
|
94
|
+
napistu-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
95
|
+
napistu-0.2.5.dist-info/entry_points.txt,sha256=_QnaPOvJNA3IltxmZgWIiBoen-L1bPYX18YQfC7oJgQ,41
|
96
|
+
napistu-0.2.5.dist-info/top_level.txt,sha256=Gpvk0a_PjrtqhYcQ9IDr3zR5LqpZ-uIHidQMIpjlvhY,14
|
97
|
+
napistu-0.2.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|