chuk-mcp-runtime 0.1.0__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.
- chuk_mcp_runtime/__init__.py +11 -0
- chuk_mcp_runtime/common/__init__.py +0 -0
- chuk_mcp_runtime/common/errors.py +32 -0
- chuk_mcp_runtime/common/mcp_tool_decorator.py +186 -0
- chuk_mcp_runtime/main.py +76 -0
- chuk_mcp_runtime/server/__init__.py +0 -0
- chuk_mcp_runtime/server/config_loader.py +132 -0
- chuk_mcp_runtime/server/logging_config.py +86 -0
- chuk_mcp_runtime/server/server.py +194 -0
- chuk_mcp_runtime/server/server_registry.py +195 -0
- chuk_mcp_runtime-0.1.0.dist-info/METADATA +235 -0
- chuk_mcp_runtime-0.1.0.dist-info/RECORD +16 -0
- chuk_mcp_runtime-0.1.0.dist-info/WHEEL +5 -0
- chuk_mcp_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- chuk_mcp_runtime-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- chuk_mcp_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# chuk_mcp_runtime/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
CHUK MCP Runtime Package
|
|
4
|
+
|
|
5
|
+
This package provides a runtime for CHUK MCP (Messaging Control Protocol) servers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# Import main functions for convenience
|
|
11
|
+
from chuk_mcp_runtime.main import run_server, main
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# chuk_mcp_runtime/common/errors.py
|
|
2
|
+
"""
|
|
3
|
+
Error classes for the CHUK MCP runtime.
|
|
4
|
+
|
|
5
|
+
This module defines standard error classes used across the CHUK MCP runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
class ChukMcpRuntimeError(Exception):
|
|
9
|
+
"""Base exception class for all CHUK MCP runtime errors."""
|
|
10
|
+
def __init__(self, message: str):
|
|
11
|
+
self.message = message
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
|
|
14
|
+
class ConfigurationError(ChukMcpRuntimeError):
|
|
15
|
+
"""Exception raised for configuration errors."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class ImportError(ChukMcpRuntimeError):
|
|
19
|
+
"""Exception raised for errors during module importing."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class ToolExecutionError(ChukMcpRuntimeError):
|
|
23
|
+
"""Exception raised for errors during tool execution."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
class ServerError(ChukMcpRuntimeError):
|
|
27
|
+
"""Exception raised for server errors."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
class ValidationError(ChukMcpRuntimeError):
|
|
31
|
+
"""Exception raised for validation errors."""
|
|
32
|
+
pass
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# chuk_mcp_runtime/common/mcp_tool_decorator.py
|
|
2
|
+
"""
|
|
3
|
+
CHUK MCP Tool Decorator Module
|
|
4
|
+
|
|
5
|
+
This module provides decorators for registering functions as CHUK MCP tools
|
|
6
|
+
with automatic input schema generation based on function signatures.
|
|
7
|
+
"""
|
|
8
|
+
import inspect
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Dict, Any, Callable, Optional, Type, get_type_hints
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
# We'll try to import pydantic, but provide fallbacks if it's not available
|
|
14
|
+
try:
|
|
15
|
+
from pydantic import create_model
|
|
16
|
+
HAS_PYDANTIC = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_PYDANTIC = False
|
|
19
|
+
logging.getLogger("chuk_mcp_runtime.tools").warning(
|
|
20
|
+
"Pydantic not available, using fallback schema generation"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Import MCP types, with fallback if not available
|
|
24
|
+
try:
|
|
25
|
+
from mcp.types import Tool
|
|
26
|
+
HAS_MCP_TYPES = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
HAS_MCP_TYPES = False
|
|
29
|
+
logging.getLogger("chuk_mcp_runtime.tools").warning(
|
|
30
|
+
"MCP types not available, using fallback Tool class"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Fallback Tool class for when MCP types are not available
|
|
34
|
+
class Tool:
|
|
35
|
+
def __init__(self, name: str, description: str, inputSchema: Dict[str, Any]):
|
|
36
|
+
self.name = name
|
|
37
|
+
self.description = description
|
|
38
|
+
self.inputSchema = inputSchema
|
|
39
|
+
|
|
40
|
+
# Global registry for MCP tool functions
|
|
41
|
+
TOOLS_REGISTRY = {}
|
|
42
|
+
|
|
43
|
+
def _get_type_schema(annotation: Type) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Convert a Python type annotation to a JSON Schema type.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
annotation: Type annotation to convert.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
JSON Schema representation of the type.
|
|
52
|
+
"""
|
|
53
|
+
# Map Python types to JSON Schema types
|
|
54
|
+
if annotation == str:
|
|
55
|
+
return {"type": "string"}
|
|
56
|
+
elif annotation == int:
|
|
57
|
+
return {"type": "integer"}
|
|
58
|
+
elif annotation == float:
|
|
59
|
+
return {"type": "number"}
|
|
60
|
+
elif annotation == bool:
|
|
61
|
+
return {"type": "boolean"}
|
|
62
|
+
elif annotation == list or getattr(annotation, "__origin__", None) == list:
|
|
63
|
+
# Handle List[...] type
|
|
64
|
+
return {"type": "array"}
|
|
65
|
+
elif annotation == dict or getattr(annotation, "__origin__", None) == dict:
|
|
66
|
+
# Handle Dict[...] type
|
|
67
|
+
return {"type": "object"}
|
|
68
|
+
# Add more type mappings as needed
|
|
69
|
+
return {"type": "string"} # Default to string
|
|
70
|
+
|
|
71
|
+
def create_input_schema(func: Callable) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Create a JSON Schema for the function's parameters.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
func: The function to create a schema for.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A JSON Schema object representing the function's parameters.
|
|
80
|
+
"""
|
|
81
|
+
sig = inspect.signature(func)
|
|
82
|
+
|
|
83
|
+
if HAS_PYDANTIC:
|
|
84
|
+
# Use Pydantic for schema generation if available
|
|
85
|
+
fields = {}
|
|
86
|
+
for param in sig.parameters.values():
|
|
87
|
+
annotation = param.annotation if param.annotation != inspect.Parameter.empty else str
|
|
88
|
+
fields[param.name] = (annotation, ...)
|
|
89
|
+
|
|
90
|
+
InputModel = create_model(f"{func.__name__.capitalize()}Input", **fields)
|
|
91
|
+
return InputModel.model_json_schema()
|
|
92
|
+
else:
|
|
93
|
+
# Fallback schema generation without Pydantic
|
|
94
|
+
properties = {}
|
|
95
|
+
required = []
|
|
96
|
+
|
|
97
|
+
type_hints = get_type_hints(func)
|
|
98
|
+
|
|
99
|
+
for param in sig.parameters.values():
|
|
100
|
+
if param.name == 'self':
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Get type annotation if available
|
|
104
|
+
annotation = type_hints.get(param.name, str)
|
|
105
|
+
|
|
106
|
+
# Add to properties
|
|
107
|
+
properties[param.name] = _get_type_schema(annotation)
|
|
108
|
+
|
|
109
|
+
# Add required if no default value
|
|
110
|
+
if param.default == inspect.Parameter.empty:
|
|
111
|
+
required.append(param.name)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": properties,
|
|
116
|
+
"required": required
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def mcp_tool(name: str = None, description: str = None):
|
|
120
|
+
"""
|
|
121
|
+
Decorator to register an MCP tool function and auto-generate its input JSON schema.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Name of the tool. If None, uses the function name.
|
|
125
|
+
description: Description of the tool. If None, uses the function's docstring.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Decorated function with _mcp_tool attribute.
|
|
129
|
+
"""
|
|
130
|
+
def decorator(func):
|
|
131
|
+
# Use function name if name not provided
|
|
132
|
+
tool_name = name or func.__name__
|
|
133
|
+
|
|
134
|
+
# Use docstring if description not provided
|
|
135
|
+
tool_description = description
|
|
136
|
+
if tool_description is None and func.__doc__:
|
|
137
|
+
tool_description = func.__doc__.strip()
|
|
138
|
+
elif tool_description is None:
|
|
139
|
+
tool_description = f"Tool: {tool_name}"
|
|
140
|
+
|
|
141
|
+
# Create input schema based on function signature
|
|
142
|
+
input_schema = create_input_schema(func)
|
|
143
|
+
|
|
144
|
+
# Create Tool instance
|
|
145
|
+
tool = Tool(
|
|
146
|
+
name=tool_name,
|
|
147
|
+
description=tool_description,
|
|
148
|
+
inputSchema=input_schema
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Register the function in the global registry.
|
|
152
|
+
TOOLS_REGISTRY[tool_name] = func
|
|
153
|
+
|
|
154
|
+
# Attach the tool metadata to the function for introspection.
|
|
155
|
+
func._mcp_tool = tool
|
|
156
|
+
|
|
157
|
+
@wraps(func)
|
|
158
|
+
def wrapper(*args, **kwargs):
|
|
159
|
+
return func(*args, **kwargs)
|
|
160
|
+
return wrapper
|
|
161
|
+
return decorator
|
|
162
|
+
|
|
163
|
+
def register_tools_from_module(module):
|
|
164
|
+
"""
|
|
165
|
+
Scan a module for functions decorated with @mcp_tool and register them.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
module: The module to scan for tools.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Dictionary of registered tools from the module.
|
|
172
|
+
"""
|
|
173
|
+
module_tools = {}
|
|
174
|
+
|
|
175
|
+
for name in dir(module):
|
|
176
|
+
item = getattr(module, name)
|
|
177
|
+
if callable(item) and hasattr(item, '_mcp_tool'):
|
|
178
|
+
tool_name = item._mcp_tool.name
|
|
179
|
+
TOOLS_REGISTRY[tool_name] = item
|
|
180
|
+
module_tools[tool_name] = item
|
|
181
|
+
|
|
182
|
+
return module_tools
|
|
183
|
+
|
|
184
|
+
def clear_tools_registry():
|
|
185
|
+
"""Clear the global tools registry."""
|
|
186
|
+
TOOLS_REGISTRY.clear()
|
chuk_mcp_runtime/main.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# chuk_mcp_runtime/main.py
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
|
|
7
|
+
from chuk_mcp_runtime.server.config_loader import load_config, find_project_root
|
|
8
|
+
from chuk_mcp_runtime.server.logging_config import get_logger, configure_logging
|
|
9
|
+
from chuk_mcp_runtime.server.server_registry import ServerRegistry
|
|
10
|
+
from chuk_mcp_runtime.server.server import MCPServer
|
|
11
|
+
from chuk_mcp_runtime.common.errors import ChukMcpRuntimeError
|
|
12
|
+
|
|
13
|
+
async def run_server(config_paths: Optional[List[str]] = None,
|
|
14
|
+
default_config: Optional[Dict[str, Any]] = None) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Initialize and run an MCP server with the given configuration.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config_paths: List of paths to search for configuration files.
|
|
20
|
+
default_config: Default configuration to use if no config file is found.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ConfigurationError: If there is an error in the configuration.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
# Load configuration
|
|
27
|
+
config = load_config(config_paths, default_config)
|
|
28
|
+
|
|
29
|
+
# Configure logging
|
|
30
|
+
configure_logging(config)
|
|
31
|
+
logger = get_logger("chuk_mcp_runtime")
|
|
32
|
+
|
|
33
|
+
# Find project root
|
|
34
|
+
project_root = find_project_root()
|
|
35
|
+
|
|
36
|
+
# Initialize server registry
|
|
37
|
+
logger.info("Initializing server registry")
|
|
38
|
+
server_registry = ServerRegistry(project_root, config)
|
|
39
|
+
|
|
40
|
+
# Load server components
|
|
41
|
+
logger.info("Loading server components")
|
|
42
|
+
loaded_modules = server_registry.load_server_components()
|
|
43
|
+
|
|
44
|
+
# Initialize MCP server
|
|
45
|
+
logger.info("Initializing MCP server")
|
|
46
|
+
mcp_server = MCPServer(config)
|
|
47
|
+
|
|
48
|
+
# Run server
|
|
49
|
+
logger.info("Starting MCP server")
|
|
50
|
+
await mcp_server.serve()
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger = get_logger("chuk_mcp_runtime")
|
|
54
|
+
logger.error(f"Error starting CHUK MCP server: {e}", exc_info=True)
|
|
55
|
+
raise ChukMcpRuntimeError(f"Error starting CHUK MCP server: {e}")
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""
|
|
59
|
+
Entry point for the MCP runtime command-line script.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
# Get configuration path from environment or command line
|
|
63
|
+
config_path = os.environ.get("CHUK_MCP_CONFIG_PATH")
|
|
64
|
+
if len(sys.argv) > 1:
|
|
65
|
+
config_path = sys.argv[1]
|
|
66
|
+
|
|
67
|
+
config_paths = [config_path] if config_path else None
|
|
68
|
+
|
|
69
|
+
# Run the server
|
|
70
|
+
asyncio.run(run_server(config_paths))
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error starting CHUK MCP server: {e}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# chuk_mcp_runtime/server/config_loader.py
|
|
2
|
+
"""
|
|
3
|
+
Configuration Loader Module
|
|
4
|
+
|
|
5
|
+
This module provides functionality to load and manage
|
|
6
|
+
configuration files for CHUK MCP servers from multiple potential locations.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import yaml
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
|
|
13
|
+
# logger
|
|
14
|
+
logger = logging.getLogger("chuk_mcp_runtime.config")
|
|
15
|
+
|
|
16
|
+
def load_config(config_paths: Optional[List[str]] = None,
|
|
17
|
+
default_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Load server configuration from config files.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config_paths: List of paths to search for config files. If None, uses default search paths.
|
|
23
|
+
default_config: Default configuration to use if no config file is found.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A dictionary containing the configuration settings.
|
|
27
|
+
"""
|
|
28
|
+
if config_paths is None:
|
|
29
|
+
# Default search paths relative to current working directory
|
|
30
|
+
config_paths = [
|
|
31
|
+
os.path.join(os.getcwd(), "config.yaml"),
|
|
32
|
+
os.path.join(os.getcwd(), "config.yml"),
|
|
33
|
+
os.environ.get("CHUK_MCP_CONFIG_PATH", ""),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Add config in package directory if run from package
|
|
37
|
+
package_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
+
config_paths.append(os.path.join(package_dir, "config.yaml"))
|
|
39
|
+
|
|
40
|
+
# Filter out empty paths
|
|
41
|
+
config_paths = [p for p in config_paths if p]
|
|
42
|
+
|
|
43
|
+
# Try to load from each path
|
|
44
|
+
for path in config_paths:
|
|
45
|
+
if os.path.exists(path):
|
|
46
|
+
logger.info(f"Loading configuration from {path}")
|
|
47
|
+
try:
|
|
48
|
+
with open(path, 'r') as f:
|
|
49
|
+
config = yaml.safe_load(f)
|
|
50
|
+
if config is None: # Empty YAML file
|
|
51
|
+
continue
|
|
52
|
+
return config
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.warning(f"Error loading config from {path}: {e}")
|
|
55
|
+
|
|
56
|
+
# Use default configuration if provided or create a minimal one
|
|
57
|
+
if default_config is None:
|
|
58
|
+
logger.warning("No configuration file found, using minimal default configuration")
|
|
59
|
+
default_config = {
|
|
60
|
+
"host": {
|
|
61
|
+
"name": "generic-mcp-server",
|
|
62
|
+
"log_level": "INFO"
|
|
63
|
+
},
|
|
64
|
+
"mcp_servers": {}
|
|
65
|
+
}
|
|
66
|
+
else:
|
|
67
|
+
logger.warning("No configuration file found, using provided default configuration")
|
|
68
|
+
|
|
69
|
+
return default_config
|
|
70
|
+
|
|
71
|
+
def find_project_root(start_dir: Optional[str] = None) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Find the project root directory by looking for markers like config.yaml,
|
|
74
|
+
pyproject.toml, etc.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
start_dir: Directory to start the search from. If None, uses current directory.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Absolute path to the project root directory.
|
|
81
|
+
"""
|
|
82
|
+
if start_dir is None:
|
|
83
|
+
start_dir = os.getcwd()
|
|
84
|
+
|
|
85
|
+
current_dir = os.path.abspath(start_dir)
|
|
86
|
+
|
|
87
|
+
# Markers that indicate a project root
|
|
88
|
+
markers = ['config.yaml', 'config.yml', 'pyproject.toml', 'setup.py']
|
|
89
|
+
|
|
90
|
+
# Maximum depth to search up
|
|
91
|
+
max_depth = 10
|
|
92
|
+
depth = 0
|
|
93
|
+
|
|
94
|
+
while depth < max_depth:
|
|
95
|
+
# Check if any markers exist in current directory
|
|
96
|
+
if any(os.path.exists(os.path.join(current_dir, marker)) for marker in markers):
|
|
97
|
+
return current_dir
|
|
98
|
+
|
|
99
|
+
# Go up one directory
|
|
100
|
+
parent_dir = os.path.dirname(current_dir)
|
|
101
|
+
if parent_dir == current_dir: # Reached the filesystem root
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
current_dir = parent_dir
|
|
105
|
+
depth += 1
|
|
106
|
+
|
|
107
|
+
# If no project root found, return the starting directory
|
|
108
|
+
logger.warning(f"No project root markers found, using {start_dir} as project root")
|
|
109
|
+
return os.path.abspath(start_dir)
|
|
110
|
+
|
|
111
|
+
def get_config_value(config: Dict[str, Any], path: str, default: Any = None) -> Any:
|
|
112
|
+
"""
|
|
113
|
+
Get a value from a nested configuration dictionary using a dot-separated path.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
config: Configuration dictionary.
|
|
117
|
+
path: Dot-separated path to the value (e.g., "host.name").
|
|
118
|
+
default: Default value to return if the path is not found.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The value at the specified path, or the default value if not found.
|
|
122
|
+
"""
|
|
123
|
+
keys = path.split('.')
|
|
124
|
+
result = config
|
|
125
|
+
|
|
126
|
+
for key in keys:
|
|
127
|
+
if isinstance(result, dict) and key in result:
|
|
128
|
+
result = result[key]
|
|
129
|
+
else:
|
|
130
|
+
return default
|
|
131
|
+
|
|
132
|
+
return result
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# chuk_mcp_runtime/server/logging_config.py
|
|
2
|
+
"""
|
|
3
|
+
Logging configuration module for CHUK MCP servers.
|
|
4
|
+
|
|
5
|
+
This module sets up a shared logger with configurable logging levels,
|
|
6
|
+
formats, and output handlers based on configuration.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
from logging import Logger
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
def configure_logging(config: Dict[str, Any] = None) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Configure the root logger based on the provided configuration.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: Configuration dictionary containing logging settings.
|
|
20
|
+
"""
|
|
21
|
+
# Extract logging configuration
|
|
22
|
+
log_config = config.get("logging", {}) if config else {}
|
|
23
|
+
|
|
24
|
+
# Determine log level from config or environment
|
|
25
|
+
log_level_name = log_config.get("level", os.getenv("CHUK_MCP_LOG_LEVEL", "INFO"))
|
|
26
|
+
log_level = getattr(logging, log_level_name.upper(), logging.INFO)
|
|
27
|
+
|
|
28
|
+
# Configure root logger
|
|
29
|
+
root_logger = logging.getLogger()
|
|
30
|
+
root_logger.setLevel(log_level)
|
|
31
|
+
|
|
32
|
+
# Clear existing handlers if requested
|
|
33
|
+
if log_config.get("reset_handlers", False):
|
|
34
|
+
for handler in root_logger.handlers[:]:
|
|
35
|
+
root_logger.removeHandler(handler)
|
|
36
|
+
|
|
37
|
+
# Add handlers if none exist
|
|
38
|
+
if not root_logger.handlers:
|
|
39
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
40
|
+
|
|
41
|
+
# Set formatter
|
|
42
|
+
log_format = log_config.get("format",
|
|
43
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
44
|
+
formatter = logging.Formatter(log_format)
|
|
45
|
+
handler.setFormatter(formatter)
|
|
46
|
+
|
|
47
|
+
root_logger.addHandler(handler)
|
|
48
|
+
|
|
49
|
+
# Set library loggers to higher level to reduce noise
|
|
50
|
+
if log_config.get("quiet_libraries", True):
|
|
51
|
+
for lib in ["urllib3", "requests", "asyncio"]:
|
|
52
|
+
logging.getLogger(lib).setLevel(logging.WARNING)
|
|
53
|
+
|
|
54
|
+
def get_logger(name: str = None, config: Dict[str, Any] = None) -> Logger:
|
|
55
|
+
"""
|
|
56
|
+
Get a configured logger with the specified name.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: The name of the logger. If None, uses the calling module's name.
|
|
60
|
+
config: Optional configuration dictionary.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A logger instance.
|
|
64
|
+
"""
|
|
65
|
+
# If name is None, try to infer from caller's module
|
|
66
|
+
if name is None:
|
|
67
|
+
import inspect
|
|
68
|
+
frame = inspect.currentframe().f_back
|
|
69
|
+
module = inspect.getmodule(frame)
|
|
70
|
+
name = module.__name__ if module else "chuk_mcp_runtime"
|
|
71
|
+
|
|
72
|
+
# Ensure our base prefix is in the name
|
|
73
|
+
if not name.startswith("chuk_mcp_runtime"):
|
|
74
|
+
name = f"chuk_mcp_runtime.{name}"
|
|
75
|
+
|
|
76
|
+
# Configure global logging if config is provided
|
|
77
|
+
if config:
|
|
78
|
+
configure_logging(config)
|
|
79
|
+
|
|
80
|
+
# Get logger
|
|
81
|
+
logger = logging.getLogger(name)
|
|
82
|
+
|
|
83
|
+
return logger
|
|
84
|
+
|
|
85
|
+
# Create a default logger for the module
|
|
86
|
+
logger = get_logger("chuk_mcp_runtime.logging")
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# chuk_mcp_runtime/server/server.py
|
|
2
|
+
"""
|
|
3
|
+
CHUK MCP Server Module
|
|
4
|
+
|
|
5
|
+
This module provides the core CHUK MCP server functionality for
|
|
6
|
+
running tools and managing server operations.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import importlib
|
|
11
|
+
from typing import Dict, Any, List, Optional, Union, Callable
|
|
12
|
+
|
|
13
|
+
# MCP imports (assuming these are from an external package)
|
|
14
|
+
from mcp.server import Server
|
|
15
|
+
from mcp.server.stdio import stdio_server
|
|
16
|
+
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
|
|
17
|
+
|
|
18
|
+
# Local imports
|
|
19
|
+
from chuk_mcp_runtime.server.logging_config import get_logger
|
|
20
|
+
|
|
21
|
+
class MCPServer:
|
|
22
|
+
"""
|
|
23
|
+
Manages the MCP (Messaging Control Protocol) server operations.
|
|
24
|
+
|
|
25
|
+
Handles tool discovery, registration, and execution.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, config: Dict[str, Any], tools_registry: Optional[Dict[str, Callable]] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the MCP server.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config: Configuration dictionary for the server.
|
|
33
|
+
tools_registry: Optional registry of tools to use instead of importing.
|
|
34
|
+
"""
|
|
35
|
+
self.config = config
|
|
36
|
+
|
|
37
|
+
# Initialize logger
|
|
38
|
+
self.logger = get_logger("chuk_mcp_runtime.server", config)
|
|
39
|
+
|
|
40
|
+
# Server name from configuration
|
|
41
|
+
self.server_name = config.get("host", {}).get("name", "generic-mcp")
|
|
42
|
+
|
|
43
|
+
# Tools registry
|
|
44
|
+
self.tools_registry = tools_registry or self._import_tools_registry()
|
|
45
|
+
|
|
46
|
+
def _import_tools_registry(self) -> Dict[str, Callable]:
|
|
47
|
+
"""
|
|
48
|
+
Dynamically import the tools registry.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary of available tools.
|
|
52
|
+
"""
|
|
53
|
+
# Get registry module path from config
|
|
54
|
+
registry_module_path = self.config.get("tools", {}).get(
|
|
55
|
+
"registry_module", "chuk_mcp_runtime.common.mcp_tool_decorator"
|
|
56
|
+
)
|
|
57
|
+
registry_attr = self.config.get("tools", {}).get(
|
|
58
|
+
"registry_attr", "TOOLS_REGISTRY"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
tools_decorator_module = importlib.import_module(registry_module_path)
|
|
63
|
+
tools_registry = getattr(tools_decorator_module, registry_attr, {})
|
|
64
|
+
except (ImportError, AttributeError) as e:
|
|
65
|
+
self.logger.error(f"Failed to import TOOLS_REGISTRY from {registry_module_path}: {e}")
|
|
66
|
+
tools_registry = {}
|
|
67
|
+
|
|
68
|
+
if not tools_registry:
|
|
69
|
+
self.logger.warning("No tools available")
|
|
70
|
+
else:
|
|
71
|
+
self.logger.info(f"Loaded {len(tools_registry)} tools")
|
|
72
|
+
self.logger.debug(f"Available tools: {', '.join(tools_registry.keys())}")
|
|
73
|
+
|
|
74
|
+
return tools_registry
|
|
75
|
+
|
|
76
|
+
async def serve(self, custom_handlers: Optional[Dict[str, Callable]] = None) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Run the MCP server with stdio communication.
|
|
79
|
+
|
|
80
|
+
Sets up server, tool listing, and tool execution handlers.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
custom_handlers: Optional dictionary of custom handlers to add to the server.
|
|
84
|
+
"""
|
|
85
|
+
# Create MCP server instance
|
|
86
|
+
server = Server(self.server_name)
|
|
87
|
+
|
|
88
|
+
@server.list_tools()
|
|
89
|
+
async def list_tools() -> List[Tool]:
|
|
90
|
+
"""
|
|
91
|
+
List available tools.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of tool descriptions.
|
|
95
|
+
"""
|
|
96
|
+
if not self.tools_registry:
|
|
97
|
+
self.logger.warning("No tools available")
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
tool_list = []
|
|
101
|
+
for func in self.tools_registry.values():
|
|
102
|
+
if hasattr(func, '_mcp_tool'):
|
|
103
|
+
tool_list.append(func._mcp_tool)
|
|
104
|
+
|
|
105
|
+
return tool_list
|
|
106
|
+
|
|
107
|
+
@server.call_tool()
|
|
108
|
+
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[Union[TextContent, ImageContent, EmbeddedResource]]:
|
|
109
|
+
"""
|
|
110
|
+
Execute a specific tool with given arguments.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
name: Name of the tool to execute.
|
|
114
|
+
arguments: Arguments for the tool.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of content resulting from tool execution.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If tool is not found or fails to execute.
|
|
121
|
+
"""
|
|
122
|
+
if name not in self.tools_registry:
|
|
123
|
+
raise ValueError(f"Tool not found: {name}")
|
|
124
|
+
|
|
125
|
+
func = self.tools_registry[name]
|
|
126
|
+
try:
|
|
127
|
+
self.logger.info(f"Executing tool '{name}' with arguments: {arguments}")
|
|
128
|
+
result = func(**arguments)
|
|
129
|
+
|
|
130
|
+
# If result is already a list of content objects, return as is
|
|
131
|
+
if isinstance(result, list) and all(isinstance(item, (TextContent, ImageContent, EmbeddedResource))
|
|
132
|
+
for item in result):
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
# For simple text results, wrap in TextContent
|
|
136
|
+
if isinstance(result, str):
|
|
137
|
+
return [TextContent(type="text", text=result)]
|
|
138
|
+
|
|
139
|
+
# Otherwise, serialize to JSON
|
|
140
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self.logger.error(f"Error processing tool '{name}': {e}", exc_info=True)
|
|
144
|
+
raise ValueError(f"Error processing tool '{name}': {str(e)}")
|
|
145
|
+
|
|
146
|
+
# Add custom handlers if provided
|
|
147
|
+
if custom_handlers:
|
|
148
|
+
for handler_name, handler_func in custom_handlers.items():
|
|
149
|
+
self.logger.info(f"Adding custom handler: {handler_name}")
|
|
150
|
+
setattr(server, handler_name, handler_func)
|
|
151
|
+
|
|
152
|
+
# Create initialization options
|
|
153
|
+
options = server.create_initialization_options()
|
|
154
|
+
|
|
155
|
+
# Run server based on configuration
|
|
156
|
+
server_type = self.config.get("server", {}).get("type", "stdio")
|
|
157
|
+
|
|
158
|
+
if server_type == "stdio":
|
|
159
|
+
self.logger.info("Starting stdio server")
|
|
160
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
161
|
+
await server.run(read_stream, write_stream, options)
|
|
162
|
+
elif server_type == "websocket":
|
|
163
|
+
# Example for websocket server - replace with actual implementation
|
|
164
|
+
ws_host = self.config.get("server", {}).get("host", "localhost")
|
|
165
|
+
ws_port = self.config.get("server", {}).get("port", 8080)
|
|
166
|
+
self.logger.info(f"Starting WebSocket server on {ws_host}:{ws_port}")
|
|
167
|
+
# Implement WebSocket server here
|
|
168
|
+
raise NotImplementedError("WebSocket server not implemented yet")
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unknown server type: {server_type}")
|
|
171
|
+
|
|
172
|
+
def register_tool(self, name: str, func: Callable) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Register a tool function with the server.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: Name of the tool.
|
|
178
|
+
func: Function to register.
|
|
179
|
+
"""
|
|
180
|
+
if not hasattr(func, '_mcp_tool'):
|
|
181
|
+
self.logger.warning(f"Function {func.__name__} does not have _mcp_tool attribute")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
self.tools_registry[name] = func
|
|
185
|
+
self.logger.info(f"Registered tool: {name}")
|
|
186
|
+
|
|
187
|
+
def get_tool_names(self) -> List[str]:
|
|
188
|
+
"""
|
|
189
|
+
Get names of all registered tools.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of tool names.
|
|
193
|
+
"""
|
|
194
|
+
return list(self.tools_registry.keys())
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# chuk_mcp_runtime/server/server_registry.py
|
|
2
|
+
"""
|
|
3
|
+
Server Registry Module for CHUK MCP Tool Servers
|
|
4
|
+
|
|
5
|
+
This module provides a ServerRegistry class for managing
|
|
6
|
+
CHUK MCP tool servers and their components.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import importlib
|
|
12
|
+
from typing import List, Dict, Any, Tuple, Optional
|
|
13
|
+
|
|
14
|
+
from chuk_mcp_runtime.server.logging_config import get_logger
|
|
15
|
+
|
|
16
|
+
class ServerRegistry:
|
|
17
|
+
"""Registry for managing MCP tool servers with components"""
|
|
18
|
+
def __init__(self, project_root: str, config: Dict[str, Any]):
|
|
19
|
+
"""
|
|
20
|
+
Initialize the server registry.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
project_root: The root directory of the project.
|
|
24
|
+
config: Configuration dictionary.
|
|
25
|
+
"""
|
|
26
|
+
self.project_root = project_root
|
|
27
|
+
self.config = config
|
|
28
|
+
|
|
29
|
+
# Initialize logger
|
|
30
|
+
self.logger = get_logger("chuk_mcp_runtime.server_registry", config)
|
|
31
|
+
|
|
32
|
+
self.server_paths, self.components = self._setup_server_paths()
|
|
33
|
+
self._setup_python_paths()
|
|
34
|
+
self.loaded_modules = {}
|
|
35
|
+
|
|
36
|
+
def _setup_server_paths(self) -> Tuple[Dict[str, str], Dict[str, List[Dict[str, Any]]]]:
|
|
37
|
+
"""
|
|
38
|
+
Process server configurations and resolve paths.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (server_paths, components) dictionaries.
|
|
42
|
+
"""
|
|
43
|
+
server_paths = {}
|
|
44
|
+
components = {}
|
|
45
|
+
|
|
46
|
+
# Add core paths
|
|
47
|
+
core_paths = self.config.get("core", {})
|
|
48
|
+
for name, location in core_paths.items():
|
|
49
|
+
full_path = os.path.join(self.project_root, location)
|
|
50
|
+
# Always add path, even if it doesn't exist
|
|
51
|
+
server_paths[name] = full_path
|
|
52
|
+
|
|
53
|
+
if not os.path.exists(full_path):
|
|
54
|
+
self.logger.warning(f"Core path does not exist: {full_path}")
|
|
55
|
+
|
|
56
|
+
# Process MCP servers
|
|
57
|
+
mcp_servers = self.config.get("mcp_servers", {})
|
|
58
|
+
for server_name, server_config in mcp_servers.items():
|
|
59
|
+
if isinstance(server_config, dict) and server_config.get("enabled", True):
|
|
60
|
+
location = server_config.get("location")
|
|
61
|
+
if location:
|
|
62
|
+
# Handle both absolute and relative paths
|
|
63
|
+
if os.path.isabs(location):
|
|
64
|
+
full_path = location
|
|
65
|
+
else:
|
|
66
|
+
full_path = os.path.join(self.project_root, location)
|
|
67
|
+
|
|
68
|
+
# Always add server path
|
|
69
|
+
server_paths[server_name] = full_path
|
|
70
|
+
components[server_name] = []
|
|
71
|
+
|
|
72
|
+
# Process components (tools, resources, prompts)
|
|
73
|
+
self._add_component(server_name, server_config, "tools", components)
|
|
74
|
+
self._add_component(server_name, server_config, "resources", components)
|
|
75
|
+
self._add_component(server_name, server_config, "prompts", components)
|
|
76
|
+
|
|
77
|
+
if not os.path.exists(full_path):
|
|
78
|
+
self.logger.warning(f"MCP server location does not exist: {full_path}")
|
|
79
|
+
|
|
80
|
+
# Auto-discovery for testing
|
|
81
|
+
if self.config.get("auto_discover", False):
|
|
82
|
+
self._auto_discover_servers(server_paths, components)
|
|
83
|
+
|
|
84
|
+
core_servers = [name for name in server_paths.keys() if name in core_paths]
|
|
85
|
+
mcp_servers_list = [name for name in server_paths.keys() if name not in core_paths]
|
|
86
|
+
|
|
87
|
+
self.logger.info(f"Core paths: {', '.join(core_servers) if core_servers else 'None'}")
|
|
88
|
+
self.logger.info(f"MCP servers: {', '.join(mcp_servers_list) if mcp_servers_list else 'None'}")
|
|
89
|
+
|
|
90
|
+
return server_paths, components
|
|
91
|
+
|
|
92
|
+
def _auto_discover_servers(self, server_paths: Dict[str, str],
|
|
93
|
+
components: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Automatically discover test servers.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
server_paths: Dictionary to add server paths to.
|
|
99
|
+
components: Dictionary to add components to.
|
|
100
|
+
"""
|
|
101
|
+
# Add known test servers
|
|
102
|
+
test_servers = self.config.get("auto_discover_servers", [
|
|
103
|
+
"time_server",
|
|
104
|
+
"echo_server",
|
|
105
|
+
"test_server"
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
for server_name in test_servers:
|
|
109
|
+
if server_name not in server_paths:
|
|
110
|
+
# Create a mock path
|
|
111
|
+
mock_path = os.path.join(self.project_root, "servers", server_name, "src")
|
|
112
|
+
server_paths[server_name] = mock_path
|
|
113
|
+
components[server_name] = []
|
|
114
|
+
|
|
115
|
+
# Add a mock tools component
|
|
116
|
+
components[server_name].append({
|
|
117
|
+
"type": "tools",
|
|
118
|
+
"module": f"{server_name}.tools",
|
|
119
|
+
"auto_discovered": True
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
def _add_component(self, server_name: str, server_config: Dict[str, Any],
|
|
123
|
+
component_type: str, components: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Add a component (tools, resources, prompts) to the components dictionary.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
server_name: Name of the server.
|
|
129
|
+
server_config: Configuration for the server.
|
|
130
|
+
component_type: Type of component (tools, resources, prompts).
|
|
131
|
+
components: Dictionary to add the component to.
|
|
132
|
+
"""
|
|
133
|
+
component_config = server_config.get(component_type, {})
|
|
134
|
+
if not isinstance(component_config, dict):
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
enabled = component_config.get("enabled", True)
|
|
138
|
+
module = component_config.get("module")
|
|
139
|
+
|
|
140
|
+
if enabled and module:
|
|
141
|
+
components[server_name].append({
|
|
142
|
+
"type": component_type,
|
|
143
|
+
"module": module,
|
|
144
|
+
"auto_discovered": False
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
def _setup_python_paths(self) -> None:
|
|
148
|
+
"""Add server source directories to Python path"""
|
|
149
|
+
# Add paths to sys.path in reverse order for correct priority
|
|
150
|
+
paths = list(self.server_paths.values())
|
|
151
|
+
for path in reversed(paths):
|
|
152
|
+
if path not in sys.path:
|
|
153
|
+
self.logger.debug(f"Adding {path} to sys.path")
|
|
154
|
+
sys.path.insert(0, path)
|
|
155
|
+
|
|
156
|
+
def load_server_components(self) -> Dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Load all enabled components from configured servers.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dictionary of loaded modules.
|
|
162
|
+
"""
|
|
163
|
+
for server_name, server_components in self.components.items():
|
|
164
|
+
for component in server_components:
|
|
165
|
+
module_name = component["module"]
|
|
166
|
+
component_type = component["type"]
|
|
167
|
+
auto_discovered = component.get("auto_discovered", False)
|
|
168
|
+
|
|
169
|
+
# Skip if already loaded
|
|
170
|
+
if module_name in self.loaded_modules:
|
|
171
|
+
self.logger.debug(f"Module {module_name} already loaded")
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
# For testing, always try to import the module
|
|
175
|
+
try:
|
|
176
|
+
self.logger.info(f"Loading {component_type} from {module_name}" +
|
|
177
|
+
(" (auto-discovered)" if auto_discovered else ""))
|
|
178
|
+
self.loaded_modules[module_name] = importlib.import_module(module_name)
|
|
179
|
+
except ImportError as e:
|
|
180
|
+
# If it's for testing, we'll catch the import error but not raise it
|
|
181
|
+
if auto_discovered:
|
|
182
|
+
self.logger.debug(f"Auto-discovered module {module_name} not found: {e}")
|
|
183
|
+
else:
|
|
184
|
+
self.logger.warning(f"Failed to import {module_name}: {e}")
|
|
185
|
+
|
|
186
|
+
return self.loaded_modules
|
|
187
|
+
|
|
188
|
+
def get_loaded_modules(self) -> Dict[str, Any]:
|
|
189
|
+
"""
|
|
190
|
+
Get all loaded modules.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dictionary of loaded modules.
|
|
194
|
+
"""
|
|
195
|
+
return self.loaded_modules
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chuk-mcp-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic CHUK MCP Runtime for MCP Servers
|
|
5
|
+
Author-email: Chris Hay <your.email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE.md
|
|
13
|
+
Requires-Dist: mcp>=1.6.0
|
|
14
|
+
Requires-Dist: pydantic>=2.10.6
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
16
|
+
Provides-Extra: websocket
|
|
17
|
+
Requires-Dist: websockets>=10.0; extra == "websocket"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# CHUK MCP Runtime
|
|
22
|
+
A generic framework for creating MCP (Messaging Control Protocol) servers that can be installed as a package on any MCP server.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Flexible configuration system with YAML support
|
|
27
|
+
- Automatic tool discovery and registration
|
|
28
|
+
- Configurable logging
|
|
29
|
+
- Server registry for managing multiple MCP servers
|
|
30
|
+
- Runtime for hosting MCP tools
|
|
31
|
+
- Easy-to-use tool decorator for registering functions as MCP tools
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Install from PyPI
|
|
37
|
+
pip install chuk-mcp-runtime
|
|
38
|
+
|
|
39
|
+
# Install with optional dependencies
|
|
40
|
+
pip install chuk-mcp-runtime[websocket,dev]
|
|
41
|
+
|
|
42
|
+
# Install from source
|
|
43
|
+
git clone https://github.com/yourusername/chuk-mcp-runtime.git
|
|
44
|
+
cd chuk-mcp-runtime
|
|
45
|
+
pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### 1. Create a configuration file
|
|
51
|
+
|
|
52
|
+
Create a `config.yaml` file:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
host:
|
|
56
|
+
name: "my-chuk-mcp-server"
|
|
57
|
+
log_level: "INFO"
|
|
58
|
+
|
|
59
|
+
server:
|
|
60
|
+
type: "stdio"
|
|
61
|
+
|
|
62
|
+
tools:
|
|
63
|
+
registry_module: "chuk_mcp_runtime.common.mcp_tool_decorator"
|
|
64
|
+
registry_attr: "TOOLS_REGISTRY"
|
|
65
|
+
|
|
66
|
+
mcp_servers:
|
|
67
|
+
my_server:
|
|
68
|
+
location: "./my_server"
|
|
69
|
+
enabled: true
|
|
70
|
+
tools:
|
|
71
|
+
module: "my_server.tools"
|
|
72
|
+
enabled: true
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Create a tools module
|
|
76
|
+
|
|
77
|
+
Create a file `my_server/tools.py`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool
|
|
81
|
+
|
|
82
|
+
@mcp_tool(name="greet", description="Generate a greeting")
|
|
83
|
+
def greet(name: str, formal: bool = False) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Generate a greeting message.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: Name to greet.
|
|
89
|
+
formal: Whether to use a formal greeting.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Greeting message.
|
|
93
|
+
"""
|
|
94
|
+
if formal:
|
|
95
|
+
return f"Good day, {name}. It is a pleasure to meet you."
|
|
96
|
+
else:
|
|
97
|
+
return f"Hey {name}! How's it going?"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Run the server
|
|
101
|
+
|
|
102
|
+
You can run the server in several ways:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Using the command-line entry point
|
|
106
|
+
chuk-mcp-server
|
|
107
|
+
|
|
108
|
+
# Using the Python module
|
|
109
|
+
python -m chuk_mcp_runtime
|
|
110
|
+
|
|
111
|
+
# With a specific config file
|
|
112
|
+
CHUK_MCP_CONFIG_PATH=./my_config.yaml chuk-mcp-server
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Creating a Custom MCP Server
|
|
116
|
+
|
|
117
|
+
### Basic Server
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import asyncio
|
|
121
|
+
from chuk_mcp_runtime.server.server import MCPServer
|
|
122
|
+
from chuk_mcp_runtime.server.config_loader import load_config
|
|
123
|
+
from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool
|
|
124
|
+
|
|
125
|
+
# Define tools
|
|
126
|
+
@mcp_tool(name="example", description="Example tool")
|
|
127
|
+
def example_tool(param: str) -> dict:
|
|
128
|
+
return {"result": f"Processed: {param}"}
|
|
129
|
+
|
|
130
|
+
async def main():
|
|
131
|
+
# Load config
|
|
132
|
+
config = load_config()
|
|
133
|
+
|
|
134
|
+
# Create tools registry
|
|
135
|
+
tools_registry = {"example": example_tool}
|
|
136
|
+
|
|
137
|
+
# Create and run server
|
|
138
|
+
server = MCPServer(config, tools_registry=tools_registry)
|
|
139
|
+
await server.serve()
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
asyncio.run(main())
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Using Server Registry
|
|
146
|
+
|
|
147
|
+
For more complex setups with multiple server components:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import asyncio
|
|
151
|
+
from chuk_mcp_runtime.server.config_loader import load_config, find_project_root
|
|
152
|
+
from chuk_mcp_runtime.server.server_registry import ServerRegistry
|
|
153
|
+
from chuk_mcp_runtime.server.server import MCPServer
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
# Load config
|
|
157
|
+
config = load_config()
|
|
158
|
+
|
|
159
|
+
# Find project root
|
|
160
|
+
project_root = find_project_root()
|
|
161
|
+
|
|
162
|
+
# Initialize server registry
|
|
163
|
+
server_registry = ServerRegistry(project_root, config)
|
|
164
|
+
|
|
165
|
+
# Load server components
|
|
166
|
+
loaded_modules = server_registry.load_server_components()
|
|
167
|
+
|
|
168
|
+
# Create and run server
|
|
169
|
+
server = MCPServer(config)
|
|
170
|
+
await server.serve()
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
asyncio.run(main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Configuration Options
|
|
177
|
+
|
|
178
|
+
The configuration system supports the following settings:
|
|
179
|
+
|
|
180
|
+
### Host Configuration
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
host:
|
|
184
|
+
name: "my-mcp-server" # Server name
|
|
185
|
+
log_level: "INFO" # Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Server Configuration
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
server:
|
|
192
|
+
type: "stdio" # Server type (stdio, websocket)
|
|
193
|
+
host: "localhost" # For websocket server
|
|
194
|
+
port: 8080 # For websocket server
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Logging Configuration
|
|
198
|
+
|
|
199
|
+
```yaml
|
|
200
|
+
logging:
|
|
201
|
+
level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
|
202
|
+
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
203
|
+
reset_handlers: false
|
|
204
|
+
quiet_libraries: true
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Tool Configuration
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
tools:
|
|
211
|
+
registry_module: "mcp_runtime.common.mcp_tool_decorator"
|
|
212
|
+
registry_attr: "TOOLS_REGISTRY"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### MCP Server Configurations
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
mcp_servers:
|
|
219
|
+
example_server:
|
|
220
|
+
location: "servers/example_server"
|
|
221
|
+
enabled: true
|
|
222
|
+
tools:
|
|
223
|
+
module: "example_server.tools"
|
|
224
|
+
enabled: true
|
|
225
|
+
resources:
|
|
226
|
+
module: "example_server.resources"
|
|
227
|
+
enabled: true
|
|
228
|
+
prompts:
|
|
229
|
+
module: "example_server.prompts"
|
|
230
|
+
enabled: true
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
chuk_mcp_runtime/__init__.py,sha256=Lls8OFNHJylYQcWMOS80QA6iK0O--Q6mQXT_TGhD15U,262
|
|
2
|
+
chuk_mcp_runtime/main.py,sha256=f8Mmb8aYVcbk7R7KUbenf7Yqg6myeDJoVX9LeLPDIrs,2621
|
|
3
|
+
chuk_mcp_runtime/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
chuk_mcp_runtime/common/errors.py,sha256=N4nuF-Zg5NSF-fmzrmCu4JpAl9qwN34vtj9amBznlAg,908
|
|
5
|
+
chuk_mcp_runtime/common/mcp_tool_decorator.py,sha256=NReZRebT7iqDopko5QiJDCINFI4F5vI2sErLjvVwAws,5978
|
|
6
|
+
chuk_mcp_runtime/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
chuk_mcp_runtime/server/config_loader.py,sha256=Fe1XHOqpXDMGiK-a6v9jHRvplPOayj9lJDeSouvtL04,4455
|
|
8
|
+
chuk_mcp_runtime/server/logging_config.py,sha256=AlAt4xxSOfMVqdIDveY-k1RyM4nqEAuUCWo0OIS4G8Y,2839
|
|
9
|
+
chuk_mcp_runtime/server/server.py,sha256=ce2X2CG4mnrf6j4qRlLjr7D1QbCQBKMh0vPPBRknWkU,7437
|
|
10
|
+
chuk_mcp_runtime/server/server_registry.py,sha256=MSY0Fu7414niECRxTr3gZ-ODhCXmYx7WzJUbHGrrtkI,8001
|
|
11
|
+
chuk_mcp_runtime-0.1.0.dist-info/licenses/LICENSE.md,sha256=HqgRrOv3qCAICv8XxRZLyNAHz1TJg1v9vb34MnE9mcY,1065
|
|
12
|
+
chuk_mcp_runtime-0.1.0.dist-info/METADATA,sha256=to6aM-RPBsm1L1TINwtxMUpqMK-9Eeduuou3Mnbueg4,5327
|
|
13
|
+
chuk_mcp_runtime-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
14
|
+
chuk_mcp_runtime-0.1.0.dist-info/entry_points.txt,sha256=oJatwTJgNhlcpCK17ohLAy3q154_Ui0jO76aEMIqTVQ,63
|
|
15
|
+
chuk_mcp_runtime-0.1.0.dist-info/top_level.txt,sha256=ljLxbagMOu5eOz634lTmgTFP8lNP5qfSMIdT6rwWQMg,17
|
|
16
|
+
chuk_mcp_runtime-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chris Hay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chuk_mcp_runtime
|