mcp-proxy-adapter 2.1.17__py3-none-any.whl → 3.0.1__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 +245 -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 +25 -0
- examples/basic_example/docs/EN/README.md +177 -0
- examples/basic_example/docs/RU/README.md +177 -0
- examples/basic_example/server.py +151 -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 +328 -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/requirements.txt +20 -0
- examples/complete_example/server.py +139 -0
- examples/minimal_example/README.md +65 -0
- examples/minimal_example/__init__.py +8 -0
- examples/minimal_example/config.json +14 -0
- examples/minimal_example/main.py +136 -0
- examples/minimal_example/simple_server.py +163 -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 +181 -0
- examples/server.py +69 -0
- examples/simple_server.py +128 -0
- examples/test_server.py +134 -0
- examples/tool_description_example.py +82 -0
- mcp_proxy_adapter/__init__.py +33 -1
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +391 -0
- mcp_proxy_adapter/api/handlers.py +229 -0
- mcp_proxy_adapter/api/middleware/__init__.py +49 -0
- mcp_proxy_adapter/api/middleware/auth.py +146 -0
- mcp_proxy_adapter/api/middleware/base.py +79 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
- mcp_proxy_adapter/api/middleware/logging.py +96 -0
- mcp_proxy_adapter/api/middleware/performance.py +83 -0
- mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
- mcp_proxy_adapter/api/schemas.py +305 -0
- mcp_proxy_adapter/api/tool_integration.py +223 -0
- mcp_proxy_adapter/api/tools.py +198 -0
- mcp_proxy_adapter/commands/__init__.py +19 -0
- mcp_proxy_adapter/commands/base.py +301 -0
- mcp_proxy_adapter/commands/command_registry.py +231 -0
- mcp_proxy_adapter/commands/config_command.py +113 -0
- mcp_proxy_adapter/commands/health_command.py +136 -0
- mcp_proxy_adapter/commands/help_command.py +193 -0
- mcp_proxy_adapter/commands/result.py +215 -0
- mcp_proxy_adapter/config.py +195 -0
- mcp_proxy_adapter/core/__init__.py +0 -0
- mcp_proxy_adapter/core/errors.py +173 -0
- mcp_proxy_adapter/core/logging.py +205 -0
- mcp_proxy_adapter/core/utils.py +138 -0
- mcp_proxy_adapter/custom_openapi.py +125 -0
- mcp_proxy_adapter/openapi.py +403 -0
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +3 -0
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
- mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
- mcp_proxy_adapter/tests/commands/__init__.py +3 -0
- mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
- mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
- mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
- mcp_proxy_adapter/tests/conftest.py +131 -0
- mcp_proxy_adapter/tests/functional/__init__.py +3 -0
- mcp_proxy_adapter/tests/functional/test_api.py +235 -0
- mcp_proxy_adapter/tests/integration/__init__.py +3 -0
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
- mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
- mcp_proxy_adapter/tests/performance/__init__.py +3 -0
- mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
- mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
- mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
- mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
- mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
- mcp_proxy_adapter/tests/test_base_command.py +123 -0
- mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
- mcp_proxy_adapter/tests/test_command_registry.py +245 -0
- mcp_proxy_adapter/tests/test_config.py +127 -0
- mcp_proxy_adapter/tests/test_utils.py +65 -0
- mcp_proxy_adapter/tests/unit/__init__.py +3 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
- mcp_proxy_adapter/tests/unit/test_config.py +217 -0
- mcp_proxy_adapter/version.py +3 -0
- mcp_proxy_adapter-3.0.1.dist-info/METADATA +200 -0
- mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.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 -262
- 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.17.dist-info/METADATA +0 -376
- mcp_proxy_adapter-2.1.17.dist-info/RECORD +0 -30
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +0 -0
mcp_proxy_adapter/adapter.py
DELETED
@@ -1,697 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Main adapter module for MCPProxy.
|
3
|
-
|
4
|
-
This module contains the MCPProxyAdapter class, which provides
|
5
|
-
integration of Command Registry with MCPProxy for working with AI model tools.
|
6
|
-
"""
|
7
|
-
import logging
|
8
|
-
import json
|
9
|
-
from typing import Dict, Any, List, Optional, Union, Callable, Protocol, Type
|
10
|
-
from fastapi import FastAPI, APIRouter, Request, Response, HTTPException, Depends
|
11
|
-
from pydantic import BaseModel, Field
|
12
|
-
|
13
|
-
try:
|
14
|
-
# Import when package is installed
|
15
|
-
from mcp_proxy_adapter.models import (
|
16
|
-
JsonRpcRequest,
|
17
|
-
JsonRpcResponse,
|
18
|
-
CommandInfo,
|
19
|
-
MCPProxyTool,
|
20
|
-
MCPProxyConfig
|
21
|
-
)
|
22
|
-
from mcp_proxy_adapter.schema import SchemaOptimizer
|
23
|
-
except ImportError:
|
24
|
-
# Import during local development
|
25
|
-
try:
|
26
|
-
from .models import (
|
27
|
-
JsonRpcRequest,
|
28
|
-
JsonRpcResponse,
|
29
|
-
CommandInfo,
|
30
|
-
MCPProxyTool,
|
31
|
-
MCPProxyConfig
|
32
|
-
)
|
33
|
-
from .schema import SchemaOptimizer
|
34
|
-
except ImportError:
|
35
|
-
# Direct import for tests
|
36
|
-
from src.models import (
|
37
|
-
JsonRpcRequest,
|
38
|
-
JsonRpcResponse,
|
39
|
-
CommandInfo,
|
40
|
-
MCPProxyTool,
|
41
|
-
MCPProxyConfig
|
42
|
-
)
|
43
|
-
from src.schema import SchemaOptimizer
|
44
|
-
|
45
|
-
# Initialize logger with default settings
|
46
|
-
logger = logging.getLogger("mcp_proxy_adapter")
|
47
|
-
|
48
|
-
def configure_logger(parent_logger=None):
|
49
|
-
"""
|
50
|
-
Configures the adapter logger with the ability to use a parent logger.
|
51
|
-
|
52
|
-
Args:
|
53
|
-
parent_logger: Parent project logger, if available
|
54
|
-
|
55
|
-
Returns:
|
56
|
-
logging.Logger: Configured adapter logger
|
57
|
-
"""
|
58
|
-
global logger
|
59
|
-
if parent_logger:
|
60
|
-
logger = parent_logger.getChild('mcp_proxy_adapter')
|
61
|
-
else:
|
62
|
-
logger = logging.getLogger("mcp_proxy_adapter")
|
63
|
-
return logger
|
64
|
-
|
65
|
-
class CommandRegistry(Protocol):
|
66
|
-
"""Protocol for CommandRegistry."""
|
67
|
-
|
68
|
-
@property
|
69
|
-
def dispatcher(self) -> Any:
|
70
|
-
"""Get the command dispatcher."""
|
71
|
-
...
|
72
|
-
|
73
|
-
def get_commands_info(self) -> Dict[str, Dict[str, Any]]:
|
74
|
-
"""Get information about all registered commands."""
|
75
|
-
...
|
76
|
-
|
77
|
-
def add_generator(self, generator: Any) -> None:
|
78
|
-
"""Add an API generator."""
|
79
|
-
...
|
80
|
-
|
81
|
-
class OpenApiGenerator(Protocol):
|
82
|
-
"""Protocol for OpenAPI schema generator."""
|
83
|
-
|
84
|
-
def generate_schema(self) -> Dict[str, Any]:
|
85
|
-
"""Generate OpenAPI schema."""
|
86
|
-
...
|
87
|
-
|
88
|
-
def set_dispatcher(self, dispatcher: Any) -> None:
|
89
|
-
"""Set the command dispatcher."""
|
90
|
-
...
|
91
|
-
|
92
|
-
class MCPProxyAdapter:
|
93
|
-
"""
|
94
|
-
Adapter for integrating Command Registry with MCPProxy.
|
95
|
-
|
96
|
-
This adapter creates a hybrid API that supports both REST and JSON-RPC
|
97
|
-
requests, and optimizes it for use with MCPProxy.
|
98
|
-
"""
|
99
|
-
|
100
|
-
def __init__(
|
101
|
-
self,
|
102
|
-
registry: CommandRegistry,
|
103
|
-
cmd_endpoint: str = "/cmd", # Added ability to specify cmd_endpoint
|
104
|
-
include_schema: bool = True,
|
105
|
-
optimize_schema: bool = True,
|
106
|
-
tool_name_prefix: str = "mcp_"
|
107
|
-
):
|
108
|
-
"""
|
109
|
-
Initializes the adapter for MCPProxy.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
registry: CommandRegistry instance
|
113
|
-
cmd_endpoint: Path for universal JSON-RPC endpoint
|
114
|
-
include_schema: Whether to include endpoint for getting OpenAPI schema
|
115
|
-
optimize_schema: Whether to optimize schema for AI models
|
116
|
-
tool_name_prefix: Prefix for tool names
|
117
|
-
"""
|
118
|
-
self.registry = registry
|
119
|
-
self.cmd_endpoint = cmd_endpoint # Use the provided parameter
|
120
|
-
self.include_schema = include_schema
|
121
|
-
self.optimize_schema = optimize_schema
|
122
|
-
self.tool_name_prefix = tool_name_prefix
|
123
|
-
self.router = APIRouter()
|
124
|
-
|
125
|
-
# Schema optimizer
|
126
|
-
self.schema_optimizer = SchemaOptimizer()
|
127
|
-
|
128
|
-
# Route configuration
|
129
|
-
self._generate_router()
|
130
|
-
|
131
|
-
# OpenAPI generator setup, if provided
|
132
|
-
try:
|
133
|
-
# Import here to avoid requiring the dependency if not used
|
134
|
-
from command_registry.generators.openapi_generator import OpenApiGenerator
|
135
|
-
self.openapi_generator = OpenApiGenerator(
|
136
|
-
title="Command Registry API",
|
137
|
-
description="API for executing commands through MCPProxy",
|
138
|
-
version="1.0.0"
|
139
|
-
)
|
140
|
-
self.registry.add_generator(self.openapi_generator)
|
141
|
-
except ImportError:
|
142
|
-
logger.info("OpenApiGenerator not found, schema generation will be limited")
|
143
|
-
self.openapi_generator = None
|
144
|
-
|
145
|
-
def _validate_param_types(self, command: str, params: Dict[str, Any]) -> List[str]:
|
146
|
-
"""
|
147
|
-
Validates parameter types and returns validation errors.
|
148
|
-
|
149
|
-
Args:
|
150
|
-
command: Command name
|
151
|
-
params: Command parameters
|
152
|
-
|
153
|
-
Returns:
|
154
|
-
List[str]: List of validation errors
|
155
|
-
"""
|
156
|
-
errors = []
|
157
|
-
command_info = self.registry.dispatcher.get_command_info(command)
|
158
|
-
|
159
|
-
if not command_info or "params" not in command_info:
|
160
|
-
return errors
|
161
|
-
|
162
|
-
for param_name, param_info in command_info["params"].items():
|
163
|
-
if param_name not in params:
|
164
|
-
continue
|
165
|
-
|
166
|
-
param_value = params[param_name]
|
167
|
-
param_type = param_info.get("type", "string")
|
168
|
-
|
169
|
-
# Check basic types
|
170
|
-
if param_type == "string" and not isinstance(param_value, str):
|
171
|
-
errors.append(f"Parameter '{param_name}' must be a string")
|
172
|
-
elif param_type == "integer" and not isinstance(param_value, int):
|
173
|
-
errors.append(f"Parameter '{param_name}' must be an integer")
|
174
|
-
elif param_type == "number" and not isinstance(param_value, (int, float)):
|
175
|
-
errors.append(f"Parameter '{param_name}' must be a number")
|
176
|
-
elif param_type == "boolean" and not isinstance(param_value, bool):
|
177
|
-
errors.append(f"Parameter '{param_name}' must be a boolean")
|
178
|
-
elif param_type == "array" and not isinstance(param_value, list):
|
179
|
-
errors.append(f"Parameter '{param_name}' must be an array")
|
180
|
-
elif param_type == "object" and not isinstance(param_value, dict):
|
181
|
-
errors.append(f"Parameter '{param_name}' must be an object")
|
182
|
-
|
183
|
-
return errors
|
184
|
-
|
185
|
-
def _generate_router(self) -> None:
|
186
|
-
"""Generates FastAPI routes for the adapter."""
|
187
|
-
# Universal endpoint for executing commands via JSON-RPC
|
188
|
-
@self.router.post(self.cmd_endpoint, response_model=JsonRpcResponse)
|
189
|
-
async def execute_command(request: JsonRpcRequest):
|
190
|
-
"""Executes a command via JSON-RPC protocol."""
|
191
|
-
try:
|
192
|
-
# Check if command exists
|
193
|
-
if request.method not in self.registry.dispatcher.get_valid_commands():
|
194
|
-
logger.warning(f"Attempt to call non-existent command: {request.method}")
|
195
|
-
return JsonRpcResponse(
|
196
|
-
jsonrpc="2.0",
|
197
|
-
error={
|
198
|
-
"code": -32601,
|
199
|
-
"message": f"Command '{request.method}' not found"
|
200
|
-
},
|
201
|
-
id=request.id
|
202
|
-
)
|
203
|
-
|
204
|
-
# Check for required parameters
|
205
|
-
command_info = self.registry.dispatcher.get_command_info(request.method)
|
206
|
-
if command_info and "params" in command_info:
|
207
|
-
missing_params = []
|
208
|
-
for param_name, param_info in command_info["params"].items():
|
209
|
-
if param_info.get("required", False) and param_name not in request.params:
|
210
|
-
missing_params.append(param_name)
|
211
|
-
|
212
|
-
if missing_params:
|
213
|
-
logger.warning(f"Missing required parameters for command {request.method}: {missing_params}")
|
214
|
-
return JsonRpcResponse(
|
215
|
-
jsonrpc="2.0",
|
216
|
-
error={
|
217
|
-
"code": -32602,
|
218
|
-
"message": f"Missing required parameters: {', '.join(missing_params)}"
|
219
|
-
},
|
220
|
-
id=request.id
|
221
|
-
)
|
222
|
-
|
223
|
-
# Check parameter types
|
224
|
-
type_errors = self._validate_param_types(request.method, request.params)
|
225
|
-
if type_errors:
|
226
|
-
logger.warning(f"Parameter type errors for command {request.method}: {type_errors}")
|
227
|
-
return JsonRpcResponse(
|
228
|
-
jsonrpc="2.0",
|
229
|
-
error={
|
230
|
-
"code": -32602,
|
231
|
-
"message": f"Invalid parameter types: {', '.join(type_errors)}"
|
232
|
-
},
|
233
|
-
id=request.id
|
234
|
-
)
|
235
|
-
|
236
|
-
# Execute the command
|
237
|
-
logger.debug(f"Executing command {request.method} with parameters {request.params}")
|
238
|
-
try:
|
239
|
-
result = await self.registry.dispatcher.execute(
|
240
|
-
request.method,
|
241
|
-
**request.params
|
242
|
-
)
|
243
|
-
|
244
|
-
# Return the result
|
245
|
-
return JsonRpcResponse(
|
246
|
-
jsonrpc="2.0",
|
247
|
-
result=result,
|
248
|
-
id=request.id
|
249
|
-
)
|
250
|
-
except TypeError as e:
|
251
|
-
# Type error in arguments or unknown argument
|
252
|
-
logger.error(f"Error in command arguments {request.method}: {str(e)}")
|
253
|
-
return JsonRpcResponse(
|
254
|
-
jsonrpc="2.0",
|
255
|
-
error={
|
256
|
-
"code": -32602,
|
257
|
-
"message": f"Invalid parameters: {str(e)}"
|
258
|
-
},
|
259
|
-
id=request.id
|
260
|
-
)
|
261
|
-
except Exception as e:
|
262
|
-
# Other errors during command execution
|
263
|
-
logger.exception(f"Error executing command {request.method}: {str(e)}")
|
264
|
-
return JsonRpcResponse(
|
265
|
-
jsonrpc="2.0",
|
266
|
-
error={
|
267
|
-
"code": -32603,
|
268
|
-
"message": f"Internal error: {str(e)}"
|
269
|
-
},
|
270
|
-
id=request.id
|
271
|
-
)
|
272
|
-
except Exception as e:
|
273
|
-
# Handle unexpected errors
|
274
|
-
logger.exception(f"Unexpected error processing request: {str(e)}")
|
275
|
-
return JsonRpcResponse(
|
276
|
-
jsonrpc="2.0",
|
277
|
-
error={
|
278
|
-
"code": -32603,
|
279
|
-
"message": f"Internal server error: {str(e)}"
|
280
|
-
},
|
281
|
-
id=request.id if hasattr(request, 'id') else None
|
282
|
-
)
|
283
|
-
|
284
|
-
# Add endpoint for getting OpenAPI schema
|
285
|
-
if self.include_schema:
|
286
|
-
@self.router.get("/openapi.json")
|
287
|
-
async def get_openapi_schema():
|
288
|
-
"""Returns optimized OpenAPI schema."""
|
289
|
-
if self.openapi_generator:
|
290
|
-
schema = self.openapi_generator.generate_schema()
|
291
|
-
else:
|
292
|
-
schema = self._generate_basic_schema()
|
293
|
-
|
294
|
-
# Optimize schema for MCP Proxy
|
295
|
-
if self.optimize_schema:
|
296
|
-
schema = self.schema_optimizer.optimize(
|
297
|
-
schema,
|
298
|
-
self.cmd_endpoint,
|
299
|
-
self.registry.get_commands_info()
|
300
|
-
)
|
301
|
-
|
302
|
-
return schema
|
303
|
-
|
304
|
-
def _generate_basic_schema(self) -> Dict[str, Any]:
|
305
|
-
"""
|
306
|
-
Generates a basic OpenAPI schema when OpenApiGenerator is not found.
|
307
|
-
|
308
|
-
Returns:
|
309
|
-
Dict[str, Any]: Basic OpenAPI schema
|
310
|
-
"""
|
311
|
-
schema = {
|
312
|
-
"openapi": "3.0.2",
|
313
|
-
"info": {
|
314
|
-
"title": "Command Registry API",
|
315
|
-
"description": "API for executing commands through MCPProxy",
|
316
|
-
"version": "1.0.0"
|
317
|
-
},
|
318
|
-
"paths": {
|
319
|
-
self.cmd_endpoint: {
|
320
|
-
"post": {
|
321
|
-
"summary": "Execute command via JSON-RPC",
|
322
|
-
"description": "Universal endpoint for executing commands",
|
323
|
-
"operationId": "execute_command",
|
324
|
-
"requestBody": {
|
325
|
-
"content": {
|
326
|
-
"application/json": {
|
327
|
-
"schema": {
|
328
|
-
"$ref": "#/components/schemas/JsonRpcRequest"
|
329
|
-
}
|
330
|
-
}
|
331
|
-
},
|
332
|
-
"required": True
|
333
|
-
},
|
334
|
-
"responses": {
|
335
|
-
"200": {
|
336
|
-
"description": "Command executed successfully",
|
337
|
-
"content": {
|
338
|
-
"application/json": {
|
339
|
-
"schema": {
|
340
|
-
"$ref": "#/components/schemas/JsonRpcResponse"
|
341
|
-
}
|
342
|
-
}
|
343
|
-
}
|
344
|
-
}
|
345
|
-
}
|
346
|
-
}
|
347
|
-
}
|
348
|
-
},
|
349
|
-
"components": {
|
350
|
-
"schemas": {
|
351
|
-
"JsonRpcRequest": {
|
352
|
-
"type": "object",
|
353
|
-
"properties": {
|
354
|
-
"jsonrpc": {
|
355
|
-
"type": "string",
|
356
|
-
"description": "JSON-RPC version",
|
357
|
-
"default": "2.0"
|
358
|
-
},
|
359
|
-
"method": {
|
360
|
-
"type": "string",
|
361
|
-
"description": "Method name for execution"
|
362
|
-
},
|
363
|
-
"params": {
|
364
|
-
"type": "object",
|
365
|
-
"description": "Method parameters",
|
366
|
-
"additionalProperties": True
|
367
|
-
},
|
368
|
-
"id": {
|
369
|
-
"oneOf": [
|
370
|
-
{"type": "string"},
|
371
|
-
{"type": "integer"},
|
372
|
-
{"type": "null"}
|
373
|
-
],
|
374
|
-
"description": "Request identifier"
|
375
|
-
}
|
376
|
-
},
|
377
|
-
"required": ["jsonrpc", "method"]
|
378
|
-
},
|
379
|
-
"JsonRpcResponse": {
|
380
|
-
"type": "object",
|
381
|
-
"properties": {
|
382
|
-
"jsonrpc": {
|
383
|
-
"type": "string",
|
384
|
-
"description": "JSON-RPC version",
|
385
|
-
"default": "2.0"
|
386
|
-
},
|
387
|
-
"result": {
|
388
|
-
"description": "Command execution result",
|
389
|
-
"nullable": True
|
390
|
-
},
|
391
|
-
"error": {
|
392
|
-
"type": "object",
|
393
|
-
"description": "Command execution error information",
|
394
|
-
"nullable": True,
|
395
|
-
"properties": {
|
396
|
-
"code": {
|
397
|
-
"type": "integer",
|
398
|
-
"description": "Error code"
|
399
|
-
},
|
400
|
-
"message": {
|
401
|
-
"type": "string",
|
402
|
-
"description": "Error message"
|
403
|
-
},
|
404
|
-
"data": {
|
405
|
-
"description": "Additional error data",
|
406
|
-
"nullable": True
|
407
|
-
}
|
408
|
-
}
|
409
|
-
},
|
410
|
-
"id": {
|
411
|
-
"oneOf": [
|
412
|
-
{"type": "string"},
|
413
|
-
{"type": "integer"},
|
414
|
-
{"type": "null"}
|
415
|
-
],
|
416
|
-
"description": "Request identifier"
|
417
|
-
}
|
418
|
-
},
|
419
|
-
"required": ["jsonrpc"]
|
420
|
-
}
|
421
|
-
}
|
422
|
-
}
|
423
|
-
}
|
424
|
-
|
425
|
-
return schema
|
426
|
-
|
427
|
-
def _register_mcp_cmd_endpoint(self, app: FastAPI) -> None:
|
428
|
-
"""
|
429
|
-
Registers /cmd endpoint compatible with MCP Proxy format.
|
430
|
-
|
431
|
-
Args:
|
432
|
-
app: FastAPI application instance
|
433
|
-
"""
|
434
|
-
@app.post("/cmd")
|
435
|
-
async def mcp_cmd_endpoint(request: Request):
|
436
|
-
"""Executes a command in MCP Proxy format or JSON-RPC."""
|
437
|
-
try:
|
438
|
-
# Get data from request
|
439
|
-
request_data = await request.json()
|
440
|
-
|
441
|
-
# Detailed logging of the entire request
|
442
|
-
logger.info(f"RECEIVED REQUEST TO /cmd: {json.dumps(request_data)}")
|
443
|
-
|
444
|
-
# Check request format
|
445
|
-
if "command" in request_data:
|
446
|
-
# MCP Proxy format: {"command": "...", "params": {...}}
|
447
|
-
command = request_data["command"]
|
448
|
-
params = request_data.get("params", {})
|
449
|
-
|
450
|
-
logger.debug(f"Received request to /cmd in MCP Proxy format: command={command}, params={params}")
|
451
|
-
elif "method" in request_data:
|
452
|
-
# JSON-RPC format: {"jsonrpc": "2.0", "method": "...", "params": {...}, "id": ...}
|
453
|
-
command = request_data["method"]
|
454
|
-
params = request_data.get("params", {})
|
455
|
-
|
456
|
-
logger.debug(f"Received request to /cmd in JSON-RPC format: method={command}, params={params}")
|
457
|
-
elif "params" in request_data:
|
458
|
-
# Implied command format: {"params": {...}}
|
459
|
-
params = request_data["params"]
|
460
|
-
|
461
|
-
# Check if params contains command, use it as command name
|
462
|
-
if "command" in params:
|
463
|
-
command = params.pop("command") # Use command from params and remove it from params
|
464
|
-
logger.info(f"Extracting command from params.command field: {command}")
|
465
|
-
# Check if params contains field that can be used as command name
|
466
|
-
elif "query" in params:
|
467
|
-
# Use query as command name or subcommand
|
468
|
-
query_value = params["query"]
|
469
|
-
logger.info(f"Extracting command from query field: {query_value}")
|
470
|
-
|
471
|
-
# If query contains "/", split into parts
|
472
|
-
if isinstance(query_value, str) and "/" in query_value:
|
473
|
-
command_parts = query_value.split("/")
|
474
|
-
command = command_parts[0] # First part - command name
|
475
|
-
|
476
|
-
# Add remaining part back to params as subcommand
|
477
|
-
params["subcommand"] = "/".join(command_parts[1:])
|
478
|
-
else:
|
479
|
-
command = "execute" # Use fixed command
|
480
|
-
# Leave query in params as is
|
481
|
-
else:
|
482
|
-
# Use default command
|
483
|
-
command = "execute"
|
484
|
-
|
485
|
-
logger.info(f"Processing request with implied command: using command={command}, params={params}")
|
486
|
-
else:
|
487
|
-
# Unknown format - return error in response body
|
488
|
-
logger.warning(f"Received request with incorrect format: {json.dumps(request_data)}")
|
489
|
-
return {
|
490
|
-
"error": {
|
491
|
-
"code": 422,
|
492
|
-
"message": "Missing required fields",
|
493
|
-
"details": "Request requires 'command', 'method' or 'params' field"
|
494
|
-
}
|
495
|
-
}
|
496
|
-
|
497
|
-
# Check if command exists
|
498
|
-
if command not in self.registry.dispatcher.get_valid_commands():
|
499
|
-
logger.warning(f"Attempt to call non-existent command: {command}")
|
500
|
-
return {
|
501
|
-
"error": {
|
502
|
-
"code": 404,
|
503
|
-
"message": f"Unknown command: {command}",
|
504
|
-
"details": f"Command '{command}' not found in registry. Available commands: {', '.join(self.registry.dispatcher.get_valid_commands())}"
|
505
|
-
}
|
506
|
-
}
|
507
|
-
|
508
|
-
# Check for required parameters
|
509
|
-
command_info = self.registry.dispatcher.get_command_info(command)
|
510
|
-
if command_info and "params" in command_info:
|
511
|
-
missing_params = []
|
512
|
-
for param_name, param_info in command_info["params"].items():
|
513
|
-
if param_info.get("required", False) and param_name not in params:
|
514
|
-
missing_params.append(param_name)
|
515
|
-
|
516
|
-
if missing_params:
|
517
|
-
logger.warning(f"Missing required parameters for command {command}: {missing_params}")
|
518
|
-
return {
|
519
|
-
"error": {
|
520
|
-
"code": 400,
|
521
|
-
"message": f"Missing required parameters: {', '.join(missing_params)}",
|
522
|
-
"details": f"Command '{command}' requires following parameters: {', '.join(missing_params)}"
|
523
|
-
}
|
524
|
-
}
|
525
|
-
|
526
|
-
# Check parameter types
|
527
|
-
type_errors = self._validate_param_types(command, params)
|
528
|
-
if type_errors:
|
529
|
-
logger.warning(f"Parameter type errors for command {command}: {type_errors}")
|
530
|
-
return {
|
531
|
-
"error": {
|
532
|
-
"code": 400,
|
533
|
-
"message": f"Invalid parameter types: {', '.join(type_errors)}",
|
534
|
-
"details": "Check parameter types and try again"
|
535
|
-
}
|
536
|
-
}
|
537
|
-
|
538
|
-
# Execute the command
|
539
|
-
try:
|
540
|
-
result = await self.registry.dispatcher.execute(command, **params)
|
541
|
-
|
542
|
-
# Return result in MCP Proxy format
|
543
|
-
return {"result": result}
|
544
|
-
|
545
|
-
except Exception as e:
|
546
|
-
logger.error(f"Error executing command {command}: {str(e)}")
|
547
|
-
return {
|
548
|
-
"error": {
|
549
|
-
"code": 500,
|
550
|
-
"message": str(e),
|
551
|
-
"details": f"Error executing command '{command}': {str(e)}"
|
552
|
-
}
|
553
|
-
}
|
554
|
-
|
555
|
-
except json.JSONDecodeError:
|
556
|
-
# JSON parsing error
|
557
|
-
logger.error("JSON parsing error from request")
|
558
|
-
return {
|
559
|
-
"error": {
|
560
|
-
"code": 400,
|
561
|
-
"message": "Invalid JSON format",
|
562
|
-
"details": "Request contains incorrect JSON. Check request syntax."
|
563
|
-
}
|
564
|
-
}
|
565
|
-
except Exception as e:
|
566
|
-
logger.error(f"Error processing request to /cmd: {str(e)}")
|
567
|
-
return {
|
568
|
-
"error": {
|
569
|
-
"code": 500,
|
570
|
-
"message": "Internal server error",
|
571
|
-
"details": str(e)
|
572
|
-
}
|
573
|
-
}
|
574
|
-
|
575
|
-
def register_endpoints(self, app: FastAPI) -> None:
|
576
|
-
"""
|
577
|
-
Registers adapter endpoints in FastAPI application.
|
578
|
-
|
579
|
-
Args:
|
580
|
-
app: FastAPI application instance
|
581
|
-
"""
|
582
|
-
# IMPORTANT: first register /cmd endpoint for MCP Proxy compatibility
|
583
|
-
self._register_mcp_cmd_endpoint(app)
|
584
|
-
|
585
|
-
# Then integrate main JSON-RPC router into application
|
586
|
-
app.include_router(self.router)
|
587
|
-
|
588
|
-
# Add endpoint for getting list of commands
|
589
|
-
@app.get("/api/commands")
|
590
|
-
def get_commands():
|
591
|
-
"""Returns list of available commands with their descriptions."""
|
592
|
-
commands_info = self.registry.get_commands_info()
|
593
|
-
return {
|
594
|
-
"commands": commands_info
|
595
|
-
}
|
596
|
-
|
597
|
-
def generate_mcp_proxy_config(self) -> MCPProxyConfig:
|
598
|
-
"""
|
599
|
-
Generates MCP Proxy configuration based on registered commands.
|
600
|
-
|
601
|
-
Returns:
|
602
|
-
MCPProxyConfig: MCP Proxy configuration
|
603
|
-
"""
|
604
|
-
tools = []
|
605
|
-
routes = []
|
606
|
-
|
607
|
-
# Get command information
|
608
|
-
commands_info = self.registry.get_commands_info()
|
609
|
-
|
610
|
-
# Create tools for each command
|
611
|
-
for cmd_name, cmd_info in commands_info.items():
|
612
|
-
# Create parameters schema for command
|
613
|
-
parameters = {
|
614
|
-
"type": "object",
|
615
|
-
"properties": {},
|
616
|
-
"required": []
|
617
|
-
}
|
618
|
-
|
619
|
-
# Add parameter properties
|
620
|
-
for param_name, param_info in cmd_info.get("params", {}).items():
|
621
|
-
# Parameter property
|
622
|
-
param_property = {
|
623
|
-
"type": param_info.get("type", "string"),
|
624
|
-
"description": param_info.get("description", "")
|
625
|
-
}
|
626
|
-
|
627
|
-
# Add additional properties if they exist
|
628
|
-
if "default" in param_info:
|
629
|
-
param_property["default"] = param_info["default"]
|
630
|
-
|
631
|
-
if "enum" in param_info:
|
632
|
-
param_property["enum"] = param_info["enum"]
|
633
|
-
|
634
|
-
# Add property to schema
|
635
|
-
parameters["properties"][param_name] = param_property
|
636
|
-
|
637
|
-
# If parameter is required, add to required list
|
638
|
-
if param_info.get("required", False):
|
639
|
-
parameters["required"].append(param_name)
|
640
|
-
|
641
|
-
# Create tool
|
642
|
-
tool = MCPProxyTool(
|
643
|
-
name=f"{self.tool_name_prefix}{cmd_name}",
|
644
|
-
description=cmd_info.get("description", ""),
|
645
|
-
parameters=parameters
|
646
|
-
)
|
647
|
-
|
648
|
-
tools.append(tool)
|
649
|
-
|
650
|
-
# Add route for tool
|
651
|
-
route = {
|
652
|
-
"tool_name": tool.name,
|
653
|
-
"endpoint": f"{self.cmd_endpoint}",
|
654
|
-
"method": "post",
|
655
|
-
"json_rpc": {
|
656
|
-
"method": cmd_name
|
657
|
-
}
|
658
|
-
}
|
659
|
-
|
660
|
-
routes.append(route)
|
661
|
-
|
662
|
-
# Create MCP Proxy configuration
|
663
|
-
config = MCPProxyConfig(
|
664
|
-
version="1.0",
|
665
|
-
tools=tools,
|
666
|
-
routes=routes
|
667
|
-
)
|
668
|
-
|
669
|
-
return config
|
670
|
-
|
671
|
-
def save_config_to_file(self, filename: str) -> None:
|
672
|
-
"""
|
673
|
-
Saves MCP Proxy configuration to file.
|
674
|
-
|
675
|
-
Args:
|
676
|
-
filename: File name for saving
|
677
|
-
"""
|
678
|
-
config = self.generate_mcp_proxy_config()
|
679
|
-
|
680
|
-
with open(filename, "w", encoding="utf-8") as f:
|
681
|
-
json.dump(config.model_dump(), f, ensure_ascii=False, indent=2)
|
682
|
-
|
683
|
-
logger.info(f"MCP Proxy configuration saved to file {filename}")
|
684
|
-
|
685
|
-
@classmethod
|
686
|
-
def from_registry(cls, registry, **kwargs):
|
687
|
-
"""
|
688
|
-
Creates adapter instance from existing command registry.
|
689
|
-
|
690
|
-
Args:
|
691
|
-
registry: Command registry instance
|
692
|
-
**kwargs: Additional parameters for constructor
|
693
|
-
|
694
|
-
Returns:
|
695
|
-
MCPProxyAdapter: Configured adapter
|
696
|
-
"""
|
697
|
-
return cls(registry, **kwargs)
|