mcp-proxy-adapter 2.1.16__py3-none-any.whl → 3.0.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.
- examples/__init__.py +19 -0
- examples/anti_patterns/README.md +51 -0
- examples/anti_patterns/__init__.py +9 -0
- examples/anti_patterns/bad_design/README.md +72 -0
- examples/anti_patterns/bad_design/global_state.py +170 -0
- examples/anti_patterns/bad_design/monolithic_command.py +272 -0
- examples/basic_example/README.md +131 -0
- examples/basic_example/__init__.py +8 -0
- examples/basic_example/commands/__init__.py +5 -0
- examples/basic_example/commands/echo_command.py +95 -0
- examples/basic_example/commands/math_command.py +151 -0
- examples/basic_example/commands/time_command.py +152 -0
- examples/basic_example/config.json +21 -0
- examples/basic_example/config.yaml +20 -0
- examples/basic_example/docs/EN/README.md +136 -0
- examples/basic_example/docs/RU/README.md +136 -0
- examples/basic_example/main.py +50 -0
- examples/basic_example/server.py +45 -0
- examples/basic_example/tests/conftest.py +243 -0
- examples/commands/echo_command.py +52 -0
- examples/commands/echo_result.py +65 -0
- examples/commands/get_date_command.py +98 -0
- examples/commands/new_uuid4_command.py +91 -0
- examples/complete_example/Dockerfile +24 -0
- examples/complete_example/README.md +92 -0
- examples/complete_example/__init__.py +8 -0
- examples/complete_example/commands/__init__.py +5 -0
- examples/complete_example/commands/system_command.py +327 -0
- examples/complete_example/config.json +41 -0
- examples/complete_example/configs/config.dev.yaml +40 -0
- examples/complete_example/configs/config.docker.yaml +40 -0
- examples/complete_example/docker-compose.yml +35 -0
- examples/complete_example/main.py +67 -0
- examples/complete_example/requirements.txt +20 -0
- examples/complete_example/server.py +85 -0
- examples/minimal_example/README.md +51 -0
- examples/minimal_example/__init__.py +8 -0
- examples/minimal_example/config.json +21 -0
- examples/minimal_example/config.yaml +26 -0
- examples/minimal_example/main.py +67 -0
- examples/minimal_example/simple_server.py +124 -0
- examples/minimal_example/tests/conftest.py +171 -0
- examples/minimal_example/tests/test_hello_command.py +111 -0
- examples/minimal_example/tests/test_integration.py +183 -0
- examples/server.py +69 -0
- examples/simple_server.py +137 -0
- examples/test_server.py +126 -0
- mcp_proxy_adapter/__init__.py +33 -1
- mcp_proxy_adapter/config.py +186 -0
- mcp_proxy_adapter/custom_openapi.py +125 -0
- mcp_proxy_adapter/framework.py +109 -0
- mcp_proxy_adapter/openapi.py +403 -0
- mcp_proxy_adapter/version.py +3 -0
- mcp_proxy_adapter-3.0.0.dist-info/METADATA +200 -0
- mcp_proxy_adapter-3.0.0.dist-info/RECORD +58 -0
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/top_level.txt +1 -0
- mcp_proxy_adapter/adapter.py +0 -697
- mcp_proxy_adapter/analyzers/__init__.py +0 -1
- mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -199
- mcp_proxy_adapter/analyzers/type_analyzer.py +0 -151
- mcp_proxy_adapter/dispatchers/__init__.py +0 -1
- mcp_proxy_adapter/dispatchers/base_dispatcher.py +0 -85
- mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +0 -235
- mcp_proxy_adapter/examples/analyze_config.py +0 -141
- mcp_proxy_adapter/examples/basic_integration.py +0 -155
- mcp_proxy_adapter/examples/docstring_and_schema_example.py +0 -69
- mcp_proxy_adapter/examples/extension_example.py +0 -72
- mcp_proxy_adapter/examples/help_best_practices.py +0 -67
- mcp_proxy_adapter/examples/help_usage.py +0 -64
- mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -131
- mcp_proxy_adapter/examples/openapi_server.py +0 -383
- mcp_proxy_adapter/examples/project_structure_example.py +0 -47
- mcp_proxy_adapter/examples/testing_example.py +0 -64
- mcp_proxy_adapter/models.py +0 -47
- mcp_proxy_adapter/registry.py +0 -439
- mcp_proxy_adapter/schema.py +0 -257
- mcp_proxy_adapter/testing_utils.py +0 -112
- mcp_proxy_adapter/validators/__init__.py +0 -1
- mcp_proxy_adapter/validators/docstring_validator.py +0 -75
- mcp_proxy_adapter/validators/metadata_validator.py +0 -76
- mcp_proxy_adapter-2.1.16.dist-info/METADATA +0 -341
- mcp_proxy_adapter-2.1.16.dist-info/RECORD +0 -30
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.16.dist-info → mcp_proxy_adapter-3.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
"""
|
2
|
+
Module for microservice configuration management.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Any, Dict, Optional
|
9
|
+
|
10
|
+
|
11
|
+
class Config:
|
12
|
+
"""
|
13
|
+
Configuration management class for the microservice.
|
14
|
+
Allows loading settings from configuration file and environment variables.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, config_path: Optional[str] = None):
|
18
|
+
"""
|
19
|
+
Initialize configuration.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
config_path: Path to configuration file. If not specified, "./config.json" is used.
|
23
|
+
"""
|
24
|
+
self.config_path = config_path or "./config.json"
|
25
|
+
self.config_data: Dict[str, Any] = {}
|
26
|
+
self.load_config()
|
27
|
+
|
28
|
+
def load_config(self) -> None:
|
29
|
+
"""
|
30
|
+
Load configuration from file and environment variables.
|
31
|
+
"""
|
32
|
+
# Set default config values
|
33
|
+
self.config_data = {
|
34
|
+
"server": {
|
35
|
+
"host": "0.0.0.0",
|
36
|
+
"port": 8000,
|
37
|
+
"debug": False,
|
38
|
+
"log_level": "INFO"
|
39
|
+
},
|
40
|
+
"logging": {
|
41
|
+
"level": "INFO",
|
42
|
+
"file": None
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
# Try to load configuration from file
|
47
|
+
if os.path.exists(self.config_path):
|
48
|
+
try:
|
49
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
50
|
+
file_config = json.load(f)
|
51
|
+
self._update_nested_dict(self.config_data, file_config)
|
52
|
+
except Exception as e:
|
53
|
+
print(f"Error loading config from {self.config_path}: {e}")
|
54
|
+
|
55
|
+
# Load configuration from environment variables
|
56
|
+
self._load_env_variables()
|
57
|
+
|
58
|
+
def load_from_file(self, config_path: str) -> None:
|
59
|
+
"""
|
60
|
+
Load configuration from the specified file.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
config_path: Path to configuration file.
|
64
|
+
"""
|
65
|
+
self.config_path = config_path
|
66
|
+
self.load_config()
|
67
|
+
|
68
|
+
def _load_env_variables(self) -> None:
|
69
|
+
"""
|
70
|
+
Load configuration from environment variables.
|
71
|
+
Environment variables should be in format SERVICE_SECTION_KEY=value.
|
72
|
+
For example, SERVICE_SERVER_PORT=8080.
|
73
|
+
"""
|
74
|
+
prefix = "SERVICE_"
|
75
|
+
for key, value in os.environ.items():
|
76
|
+
if key.startswith(prefix):
|
77
|
+
parts = key[len(prefix):].lower().split("_", 1)
|
78
|
+
if len(parts) == 2:
|
79
|
+
section, param = parts
|
80
|
+
if section not in self.config_data:
|
81
|
+
self.config_data[section] = {}
|
82
|
+
self.config_data[section][param] = self._convert_env_value(value)
|
83
|
+
|
84
|
+
def _convert_env_value(self, value: str) -> Any:
|
85
|
+
"""
|
86
|
+
Convert environment variable value to appropriate type.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
value: Value as string
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Converted value
|
93
|
+
"""
|
94
|
+
# Try to convert to appropriate type
|
95
|
+
if value.lower() == "true":
|
96
|
+
return True
|
97
|
+
elif value.lower() == "false":
|
98
|
+
return False
|
99
|
+
elif value.isdigit():
|
100
|
+
return int(value)
|
101
|
+
else:
|
102
|
+
try:
|
103
|
+
return float(value)
|
104
|
+
except ValueError:
|
105
|
+
return value
|
106
|
+
|
107
|
+
def get(self, key: str, default: Any = None) -> Any:
|
108
|
+
"""
|
109
|
+
Get configuration value for key.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
key: Configuration key in format "section.param"
|
113
|
+
default: Default value if key not found
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Configuration value
|
117
|
+
"""
|
118
|
+
parts = key.split(".")
|
119
|
+
|
120
|
+
# Get value from config
|
121
|
+
value = self.config_data
|
122
|
+
for part in parts:
|
123
|
+
if not isinstance(value, dict) or part not in value:
|
124
|
+
return default
|
125
|
+
value = value[part]
|
126
|
+
|
127
|
+
return value
|
128
|
+
|
129
|
+
def set(self, key: str, value: Any) -> None:
|
130
|
+
"""
|
131
|
+
Set configuration value for key.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
key: Configuration key in format "section.param"
|
135
|
+
value: Configuration value
|
136
|
+
"""
|
137
|
+
parts = key.split(".")
|
138
|
+
if len(parts) == 1:
|
139
|
+
self.config_data[key] = value
|
140
|
+
else:
|
141
|
+
section = parts[0]
|
142
|
+
param = ".".join(parts[1:])
|
143
|
+
|
144
|
+
if section not in self.config_data:
|
145
|
+
self.config_data[section] = {}
|
146
|
+
|
147
|
+
current = self.config_data[section]
|
148
|
+
for part in parts[1:-1]:
|
149
|
+
if part not in current:
|
150
|
+
current[part] = {}
|
151
|
+
current = current[part]
|
152
|
+
|
153
|
+
current[parts[-1]] = value
|
154
|
+
|
155
|
+
def save(self, path: Optional[str] = None) -> None:
|
156
|
+
"""
|
157
|
+
Save configuration to file.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
path: Path to configuration file. If not specified, self.config_path is used.
|
161
|
+
"""
|
162
|
+
save_path = path or self.config_path
|
163
|
+
with open(save_path, 'w', encoding='utf-8') as f:
|
164
|
+
json.dump(self.config_data, f, indent=2)
|
165
|
+
|
166
|
+
def _update_nested_dict(self, d: Dict, u: Dict) -> Dict:
|
167
|
+
"""
|
168
|
+
Update nested dictionary recursively.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
d: Dictionary to update
|
172
|
+
u: Dictionary with new values
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Updated dictionary
|
176
|
+
"""
|
177
|
+
for k, v in u.items():
|
178
|
+
if isinstance(v, dict) and k in d and isinstance(d[k], dict):
|
179
|
+
self._update_nested_dict(d[k], v)
|
180
|
+
else:
|
181
|
+
d[k] = v
|
182
|
+
return d
|
183
|
+
|
184
|
+
|
185
|
+
# Singleton instance
|
186
|
+
config = Config()
|
@@ -0,0 +1,125 @@
|
|
1
|
+
"""
|
2
|
+
Custom OpenAPI schema generator for MCP Microservice compatible with MCP-Proxy.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
from copy import deepcopy
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Dict, List, Optional, Set, Type
|
8
|
+
|
9
|
+
from fastapi import FastAPI
|
10
|
+
from fastapi.openapi.utils import get_openapi
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
13
|
+
from mcp_proxy_adapter.commands.base import Command
|
14
|
+
from mcp_proxy_adapter.core.logging import logger
|
15
|
+
|
16
|
+
|
17
|
+
class CustomOpenAPIGenerator:
|
18
|
+
"""
|
19
|
+
Custom OpenAPI schema generator for compatibility with MCP-Proxy.
|
20
|
+
|
21
|
+
This generator creates an OpenAPI schema that matches the format expected by MCP-Proxy,
|
22
|
+
enabling dynamic command loading and proper tool representation in AI models.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self):
|
26
|
+
"""Initialize the generator."""
|
27
|
+
self.base_schema_path = Path(__file__).parent / "schemas" / "openapi_schema.json"
|
28
|
+
self.base_schema = self._load_base_schema()
|
29
|
+
|
30
|
+
def _load_base_schema(self) -> Dict[str, Any]:
|
31
|
+
"""
|
32
|
+
Load the base OpenAPI schema from file.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Dict containing the base OpenAPI schema.
|
36
|
+
"""
|
37
|
+
with open(self.base_schema_path, "r", encoding="utf-8") as f:
|
38
|
+
return json.load(f)
|
39
|
+
|
40
|
+
def _add_commands_to_schema(self, schema: Dict[str, Any]) -> None:
|
41
|
+
"""
|
42
|
+
Add all registered commands to the OpenAPI schema.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
schema: The OpenAPI schema to update.
|
46
|
+
"""
|
47
|
+
# Get all commands from the registry
|
48
|
+
commands = registry.get_all_commands()
|
49
|
+
|
50
|
+
# Add command names to the CommandRequest enum
|
51
|
+
schema["components"]["schemas"]["CommandRequest"]["properties"]["command"]["enum"] = [
|
52
|
+
cmd for cmd in commands.keys()
|
53
|
+
]
|
54
|
+
|
55
|
+
# Add command parameters to oneOf
|
56
|
+
params_refs = []
|
57
|
+
|
58
|
+
for name, cmd_class in commands.items():
|
59
|
+
# Create schema for command parameters
|
60
|
+
param_schema_name = f"{name.capitalize()}Params"
|
61
|
+
schema["components"]["schemas"][param_schema_name] = self._create_params_schema(cmd_class)
|
62
|
+
|
63
|
+
# Add to oneOf
|
64
|
+
params_refs.append({"$ref": f"#/components/schemas/{param_schema_name}"})
|
65
|
+
|
66
|
+
# Add null option for commands without parameters
|
67
|
+
params_refs.append({"type": "null"})
|
68
|
+
|
69
|
+
# Set oneOf for params
|
70
|
+
schema["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"] = params_refs
|
71
|
+
|
72
|
+
def _create_params_schema(self, cmd_class: Type[Command]) -> Dict[str, Any]:
|
73
|
+
"""
|
74
|
+
Create a schema for command parameters.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
cmd_class: The command class.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict containing the parameter schema.
|
81
|
+
"""
|
82
|
+
# Get command schema
|
83
|
+
cmd_schema = cmd_class.get_schema()
|
84
|
+
|
85
|
+
# Add title and description
|
86
|
+
cmd_schema["title"] = f"Parameters for {cmd_class.name}"
|
87
|
+
cmd_schema["description"] = f"Parameters for the {cmd_class.name} command"
|
88
|
+
|
89
|
+
return cmd_schema
|
90
|
+
|
91
|
+
def generate(self) -> Dict[str, Any]:
|
92
|
+
"""
|
93
|
+
Generate the complete OpenAPI schema compatible with MCP-Proxy.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Dict containing the complete OpenAPI schema.
|
97
|
+
"""
|
98
|
+
# Deep copy the base schema to avoid modifying it
|
99
|
+
schema = deepcopy(self.base_schema)
|
100
|
+
|
101
|
+
# Add commands to schema
|
102
|
+
self._add_commands_to_schema(schema)
|
103
|
+
|
104
|
+
logger.info(f"Generated OpenAPI schema with {len(registry.get_all_commands())} commands")
|
105
|
+
|
106
|
+
return schema
|
107
|
+
|
108
|
+
|
109
|
+
def custom_openapi(app: FastAPI) -> Dict[str, Any]:
|
110
|
+
"""
|
111
|
+
Create a custom OpenAPI schema for the FastAPI application.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
app: The FastAPI application.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Dict containing the custom OpenAPI schema.
|
118
|
+
"""
|
119
|
+
generator = CustomOpenAPIGenerator()
|
120
|
+
openapi_schema = generator.generate()
|
121
|
+
|
122
|
+
# Cache the schema
|
123
|
+
app.openapi_schema = openapi_schema
|
124
|
+
|
125
|
+
return openapi_schema
|
@@ -0,0 +1,109 @@
|
|
1
|
+
"""
|
2
|
+
Framework module for creating microservices.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import uvicorn
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
8
|
+
|
9
|
+
from fastapi import FastAPI
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.api.app import create_app
|
12
|
+
from mcp_proxy_adapter.commands.base import Command
|
13
|
+
from mcp_proxy_adapter.commands.command_registry import registry
|
14
|
+
from mcp_proxy_adapter.config import config
|
15
|
+
from mcp_proxy_adapter.core.logging import logger, setup_logging
|
16
|
+
|
17
|
+
|
18
|
+
class MicroService:
|
19
|
+
"""
|
20
|
+
Main class for creating and managing microservices.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
title: str = "MicroService",
|
26
|
+
description: str = "JSON-RPC microservice",
|
27
|
+
version: str = "1.0.0",
|
28
|
+
config_path: Optional[str] = None,
|
29
|
+
log_level: str = "INFO",
|
30
|
+
):
|
31
|
+
"""
|
32
|
+
Initialize microservice.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
title: Service title (displayed in docs)
|
36
|
+
description: Service description (displayed in docs)
|
37
|
+
version: Service version
|
38
|
+
config_path: Path to configuration file
|
39
|
+
log_level: Logging level
|
40
|
+
"""
|
41
|
+
# Set configuration
|
42
|
+
if config_path and os.path.exists(config_path):
|
43
|
+
config.load_from_file(config_path)
|
44
|
+
|
45
|
+
# Set logging
|
46
|
+
setup_logging(log_level)
|
47
|
+
|
48
|
+
# Create FastAPI app
|
49
|
+
self.app = create_app()
|
50
|
+
self.app.title = title
|
51
|
+
self.app.description = description
|
52
|
+
self.app.version = version
|
53
|
+
|
54
|
+
logger.info(f"MicroService '{title}' v{version} initialized")
|
55
|
+
|
56
|
+
def register_command(self, command_class: Type[Command]) -> None:
|
57
|
+
"""
|
58
|
+
Register command in the service.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
command_class: Command class to register
|
62
|
+
"""
|
63
|
+
registry.register(command_class)
|
64
|
+
logger.info(f"Command '{command_class.name if hasattr(command_class, 'name') else command_class.__name__}' registered")
|
65
|
+
|
66
|
+
def discover_commands(self, package_path: str) -> None:
|
67
|
+
"""
|
68
|
+
Automatically discover and register commands in package.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
package_path: Path to package with commands
|
72
|
+
"""
|
73
|
+
registry.discover_commands(package_path)
|
74
|
+
logger.info(f"Commands discovered in package '{package_path}'")
|
75
|
+
|
76
|
+
def run(
|
77
|
+
self,
|
78
|
+
host: str = "0.0.0.0",
|
79
|
+
port: int = 8000,
|
80
|
+
reload: bool = False,
|
81
|
+
workers: int = 1,
|
82
|
+
) -> None:
|
83
|
+
"""
|
84
|
+
Run microservice.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
host: Host to bind server
|
88
|
+
port: Port to bind server
|
89
|
+
reload: Enable auto-reload on code changes
|
90
|
+
workers: Number of worker processes
|
91
|
+
"""
|
92
|
+
logger.info(f"Starting microservice on {host}:{port}")
|
93
|
+
uvicorn.run(
|
94
|
+
self.app,
|
95
|
+
host=host,
|
96
|
+
port=port,
|
97
|
+
reload=reload,
|
98
|
+
workers=workers,
|
99
|
+
log_level=config.get("logging.level", "info").lower()
|
100
|
+
)
|
101
|
+
|
102
|
+
def get_app(self) -> FastAPI:
|
103
|
+
"""
|
104
|
+
Get FastAPI application instance.
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
FastAPI application
|
108
|
+
"""
|
109
|
+
return self.app
|