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.
Files changed (105) hide show
  1. {napistu-0.2.4.dev4 → napistu-0.2.5}/PKG-INFO +1 -1
  2. {napistu-0.2.4.dev4 → napistu-0.2.5}/setup.cfg +1 -1
  3. napistu-0.2.5/src/napistu/mcp/__main__.py +205 -0
  4. napistu-0.2.5/src/napistu/mcp/client.py +207 -0
  5. napistu-0.2.5/src/napistu/mcp/health.py +247 -0
  6. napistu-0.2.5/src/napistu/mcp/server.py +190 -0
  7. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/PKG-INFO +1 -1
  8. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/SOURCES.txt +2 -0
  9. napistu-0.2.4.dev4/src/napistu/mcp/__main__.py +0 -180
  10. napistu-0.2.4.dev4/src/napistu/mcp/server.py +0 -86
  11. {napistu-0.2.4.dev4 → napistu-0.2.5}/LICENSE +0 -0
  12. {napistu-0.2.4.dev4 → napistu-0.2.5}/README.md +0 -0
  13. {napistu-0.2.4.dev4 → napistu-0.2.5}/pyproject.toml +0 -0
  14. {napistu-0.2.4.dev4 → napistu-0.2.5}/setup.py +0 -0
  15. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/__init__.py +0 -0
  16. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/__main__.py +0 -0
  17. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/consensus.py +0 -0
  18. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/constants.py +0 -0
  19. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/__init__.py +0 -0
  20. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/constants.py +0 -0
  21. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/downloads.py +0 -0
  22. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/gcs/utils.py +0 -0
  23. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/identifiers.py +0 -0
  24. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/indices.py +0 -0
  25. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/__init__.py +0 -0
  26. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/bigg.py +0 -0
  27. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/constants.py +0 -0
  28. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/cpr_edgelist.py +0 -0
  29. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/identifiers_etl.py +0 -0
  30. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/obo.py +0 -0
  31. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/psi_mi.py +0 -0
  32. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/reactome.py +0 -0
  33. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/sbml.py +0 -0
  34. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/string.py +0 -0
  35. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/trrust.py +0 -0
  36. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/ingestion/yeast.py +0 -0
  37. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/__init__.py +0 -0
  38. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/codebase.py +0 -0
  39. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/codebase_utils.py +0 -0
  40. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/constants.py +0 -0
  41. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/documentation.py +0 -0
  42. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/documentation_utils.py +0 -0
  43. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/execution.py +0 -0
  44. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/profiles.py +0 -0
  45. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/tutorials.py +0 -0
  46. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/tutorials_utils.py +0 -0
  47. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mcp/utils.py +0 -0
  48. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/mechanism_matching.py +0 -0
  49. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/__init__.py +0 -0
  50. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/constants.py +0 -0
  51. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/curation.py +0 -0
  52. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/gaps.py +0 -0
  53. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/pathwayannot.py +0 -0
  54. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/modify/uncompartmentalize.py +0 -0
  55. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/__init__.py +0 -0
  56. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/constants.py +0 -0
  57. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/neighborhoods.py +0 -0
  58. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_create.py +0 -0
  59. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_propagation.py +0 -0
  60. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/net_utils.py +0 -0
  61. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/paths.py +0 -0
  62. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/network/precompute.py +0 -0
  63. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/__init__.py +0 -0
  64. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/callr.py +0 -0
  65. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/constants.py +0 -0
  66. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/netcontextr.py +0 -0
  67. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/rpy2/rids.py +0 -0
  68. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/sbml_dfs_core.py +0 -0
  69. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/sbml_dfs_utils.py +0 -0
  70. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/source.py +0 -0
  71. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu/utils.py +0 -0
  72. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/dependency_links.txt +0 -0
  73. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/entry_points.txt +0 -0
  74. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/requires.txt +0 -0
  75. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/napistu.egg-info/top_level.txt +0 -0
  76. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/__init__.py +0 -0
  77. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/conftest.py +0 -0
  78. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_consensus.py +0 -0
  79. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_constants.py +0 -0
  80. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_curation.py +0 -0
  81. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_data/__init__.py +0 -0
  82. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_edgelist.py +0 -0
  83. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_gaps.py +0 -0
  84. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_gcs.py +0 -0
  85. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_identifiers.py +0 -0
  86. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_igraph.py +0 -0
  87. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_indices.py +0 -0
  88. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_mcp_documentation_utils.py +0 -0
  89. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_mechanism_matching.py +0 -0
  90. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_net_propagation.py +0 -0
  91. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_net_utils.py +0 -0
  92. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_netcontextr.py +0 -0
  93. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_obo.py +0 -0
  94. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_pathwayannot.py +0 -0
  95. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_precomputed_distances.py +0 -0
  96. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_rpy2.py +0 -0
  97. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml.py +0 -0
  98. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml_dfs_core.py +0 -0
  99. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbml_dfs_utils.py +0 -0
  100. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_sbo.py +0 -0
  101. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_set_coverage.py +0 -0
  102. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_source.py +0 -0
  103. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_uncompartmentalize.py +0 -0
  104. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/test_utils.py +0 -0
  105. {napistu-0.2.4.dev4 → napistu-0.2.5}/src/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napistu
3
- Version: 0.2.4.dev4
3
+ Version: 0.2.5
4
4
  Summary: Connecting high-dimensional data to curated pathways
5
5
  Home-page: https://github.com/napistu/napistu-py
6
6
  Author: Sean Hackett
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = napistu
3
- version = 0.2.4.dev4
3
+ version = 0.2.5
4
4
  description = Connecting high-dimensional data to curated pathways
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -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"