napistu 0.2.4.dev5__py3-none-any.whl → 0.2.5.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
napistu/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.profiles import get_profile, ServerProfile
12
- from napistu.mcp.server import create_server
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("--server-name", type=str, help="Name of the MCP server")
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
- # Collect configuration
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
- # Get profile with overrides
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
- @server.command(name="remote")
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 start_remote(server_name, tutorials_path):
84
- """Start a remote MCP server for documentation and codebase exploration."""
85
- # Collect configuration
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
- # Create and start the server
94
- logger.info("Starting Napistu remote MCP Server...")
95
- server = create_server(server_profile)
96
- asyncio.run(server.start())
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
- @click.group()
100
- def component():
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
- @component.command(name="list")
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
- @component.command(name="custom")
116
- @click.option(
117
- "--enable-documentation/--disable-documentation",
118
- default=None,
119
- help="Enable/disable documentation components",
120
- )
121
- @click.option(
122
- "--enable-codebase/--disable-codebase",
123
- default=None,
124
- help="Enable/disable codebase exploration components",
125
- )
126
- @click.option(
127
- "--enable-execution/--disable-execution",
128
- default=None,
129
- help="Enable/disable function execution components",
130
- )
131
- @click.option(
132
- "--enable-tutorials/--disable-tutorials",
133
- default=None,
134
- help="Enable/disable tutorial components",
135
- )
136
- @click.option(
137
- "--server-name", type=str, default="napistu-custom", help="Name of the MCP server"
138
- )
139
- @click.option("--codebase-path", type=str, help="Path to the Napistu codebase")
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
- "--docs-paths",
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 custom_server(
148
- enable_documentation,
149
- enable_codebase,
150
- enable_execution,
151
- enable_tutorials,
152
- server_name,
153
- ):
154
- """Start an MCP server with custom component configuration."""
155
- # Collect configuration
156
- config = {"server_name": server_name}
157
- if enable_documentation is not None:
158
- config["enable_documentation"] = enable_documentation
159
- if enable_codebase is not None:
160
- config["enable_codebase"] = enable_codebase
161
- if enable_execution is not None:
162
- config["enable_execution"] = enable_execution
163
- if enable_tutorials is not None:
164
- config["enable_tutorials"] = enable_tutorials
165
-
166
- # Create a custom profile
167
- server_profile = ServerProfile(**config)
168
-
169
- # Create and start the server
170
- logger.info("Starting Napistu custom MCP Server...")
171
- server = create_server(server_profile)
172
- asyncio.run(server.start())
173
-
174
-
175
- # Add command groups to the CLI
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(component)
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napistu
3
- Version: 0.2.4.dev5
3
+ Version: 0.2.5.dev2
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
@@ -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=LzCbgk07XXk0R3JSzcxI2gSX3hZ9a389qt8xujPJR7E,5335
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=Igl4SL3KIuWyByBahHz1K3TQ2Zk1rLbszofHULoHPWs,2649
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.4.dev5.dist-info/licenses/LICENSE,sha256=kW8wVT__JWoHjl2BbbJDAZInWa9AxzJeR_uv6-i5x1g,1063
62
+ napistu-0.2.5.dev2.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.4.dev5.dist-info/METADATA,sha256=09rdIg_WphTdSr584QI52OE7HiB-P9LLNsTsdP5JaMA,3202
92
- napistu-0.2.4.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
- napistu-0.2.4.dev5.dist-info/entry_points.txt,sha256=_QnaPOvJNA3IltxmZgWIiBoen-L1bPYX18YQfC7oJgQ,41
94
- napistu-0.2.4.dev5.dist-info/top_level.txt,sha256=Gpvk0a_PjrtqhYcQ9IDr3zR5LqpZ-uIHidQMIpjlvhY,14
95
- napistu-0.2.4.dev5.dist-info/RECORD,,
93
+ napistu-0.2.5.dev2.dist-info/METADATA,sha256=v4bFzaxkUVXkyLwn5UDJYIdBf1d2e7tVNw0TO68xXeU,3202
94
+ napistu-0.2.5.dev2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
95
+ napistu-0.2.5.dev2.dist-info/entry_points.txt,sha256=_QnaPOvJNA3IltxmZgWIiBoen-L1bPYX18YQfC7oJgQ,41
96
+ napistu-0.2.5.dev2.dist-info/top_level.txt,sha256=Gpvk0a_PjrtqhYcQ9IDr3zR5LqpZ-uIHidQMIpjlvhY,14
97
+ napistu-0.2.5.dev2.dist-info/RECORD,,