mcp-proxy-adapter 2.1.3__py3-none-any.whl → 2.1.4__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.
- mcp_proxy_adapter/examples/analyze_config.py +141 -0
- mcp_proxy_adapter/examples/basic_integration.py +161 -0
- mcp_proxy_adapter/examples/docstring_and_schema_example.py +60 -0
- mcp_proxy_adapter/examples/extension_example.py +60 -0
- mcp_proxy_adapter/examples/help_best_practices.py +67 -0
- mcp_proxy_adapter/examples/help_usage.py +64 -0
- mcp_proxy_adapter/examples/mcp_proxy_client.py +131 -0
- mcp_proxy_adapter/examples/openapi_server.py +397 -0
- mcp_proxy_adapter/examples/project_structure_example.py +47 -0
- mcp_proxy_adapter/examples/testing_example.py +53 -0
- {mcp_proxy_adapter-2.1.3.dist-info → mcp_proxy_adapter-2.1.4.dist-info}/METADATA +98 -2
- mcp_proxy_adapter-2.1.4.dist-info/RECORD +28 -0
- mcp_proxy_adapter-2.1.3.dist-info/RECORD +0 -18
- {mcp_proxy_adapter-2.1.3.dist-info → mcp_proxy_adapter-2.1.4.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.3.dist-info → mcp_proxy_adapter-2.1.4.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-2.1.3.dist-info → mcp_proxy_adapter-2.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
"""
|
2
|
+
Analysis of MCP Proxy configuration generated by the adapter.
|
3
|
+
|
4
|
+
This script loads and analyzes the MCP Proxy configuration file
|
5
|
+
that was created by MCPProxyAdapter, and outputs structured
|
6
|
+
information about routes and tools.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
python examples/analyze_config.py
|
10
|
+
"""
|
11
|
+
import os
|
12
|
+
import json
|
13
|
+
import sys
|
14
|
+
from typing import Dict, Any, List
|
15
|
+
|
16
|
+
|
17
|
+
def load_config_file(config_path: str) -> Dict[str, Any]:
|
18
|
+
"""
|
19
|
+
Loads MCP Proxy configuration file.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
config_path (str): Path to configuration file
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Dict[str, Any]: Loaded configuration
|
26
|
+
|
27
|
+
Raises:
|
28
|
+
FileNotFoundError: If file is not found
|
29
|
+
json.JSONDecodeError: If file is not valid JSON
|
30
|
+
"""
|
31
|
+
try:
|
32
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
33
|
+
return json.load(f)
|
34
|
+
except FileNotFoundError:
|
35
|
+
print(f"Error: Configuration file not found: {config_path}")
|
36
|
+
sys.exit(1)
|
37
|
+
except json.JSONDecodeError as e:
|
38
|
+
print(f"Error: File is not valid JSON: {e}")
|
39
|
+
sys.exit(1)
|
40
|
+
|
41
|
+
|
42
|
+
def print_routes(config: Dict[str, Any]) -> None:
|
43
|
+
"""
|
44
|
+
Prints route information from configuration.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
config (Dict[str, Any]): MCP Proxy configuration
|
48
|
+
"""
|
49
|
+
if "routes" not in config:
|
50
|
+
print("No routes in configuration")
|
51
|
+
return
|
52
|
+
|
53
|
+
routes = config["routes"]
|
54
|
+
print(f"=== Routes ({len(routes)}) ===")
|
55
|
+
|
56
|
+
for i, route in enumerate(routes, 1):
|
57
|
+
print(f"\n{i}. Route:")
|
58
|
+
print(f" Endpoint: {route.get('endpoint', 'Not specified')}")
|
59
|
+
print(f" Method: {route.get('method', 'Not specified')}")
|
60
|
+
|
61
|
+
if "json_rpc" in route:
|
62
|
+
print(" Type: JSON-RPC")
|
63
|
+
if "params" in route["json_rpc"]:
|
64
|
+
print(f" Parameters: {route['json_rpc'].get('params', 'Not specified')}")
|
65
|
+
else:
|
66
|
+
print(" Type: Regular HTTP")
|
67
|
+
|
68
|
+
|
69
|
+
def print_tools(config: Dict[str, Any]) -> None:
|
70
|
+
"""
|
71
|
+
Prints tool information from configuration.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
config (Dict[str, Any]): MCP Proxy configuration
|
75
|
+
"""
|
76
|
+
if "tools" not in config:
|
77
|
+
print("No tools in configuration")
|
78
|
+
return
|
79
|
+
|
80
|
+
tools = config["tools"]
|
81
|
+
print(f"\n=== Tools ({len(tools)}) ===")
|
82
|
+
|
83
|
+
for i, tool in enumerate(tools, 1):
|
84
|
+
print(f"\n{i}. Tool:")
|
85
|
+
print(f" Name: {tool.get('name', 'Not specified')}")
|
86
|
+
print(f" Description: {tool.get('description', 'Not specified')}")
|
87
|
+
|
88
|
+
if "parameters" in tool:
|
89
|
+
params = tool["parameters"]
|
90
|
+
required_params = params.get("required", [])
|
91
|
+
properties = params.get("properties", {})
|
92
|
+
|
93
|
+
print(f" Parameters ({len(properties)}):")
|
94
|
+
for param_name, param_info in properties.items():
|
95
|
+
required = "Required" if param_name in required_params else "Optional"
|
96
|
+
param_type = param_info.get("type", "Not specified")
|
97
|
+
description = param_info.get("description", "Not specified")
|
98
|
+
|
99
|
+
print(f" - {param_name} ({param_type}, {required}):")
|
100
|
+
print(f" {description}")
|
101
|
+
|
102
|
+
|
103
|
+
def analyze_config(config_path: str) -> None:
|
104
|
+
"""
|
105
|
+
Analyzes MCP Proxy configuration.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
config_path (str): Path to configuration file
|
109
|
+
"""
|
110
|
+
config = load_config_file(config_path)
|
111
|
+
|
112
|
+
print("\n=== MCP Proxy Configuration Analysis ===")
|
113
|
+
print(f"File: {config_path}")
|
114
|
+
print(f"Version: {config.get('version', 'Not specified')}")
|
115
|
+
|
116
|
+
print_routes(config)
|
117
|
+
print_tools(config)
|
118
|
+
|
119
|
+
print("\n=== Summary ===")
|
120
|
+
num_routes = len(config.get("routes", []))
|
121
|
+
num_tools = len(config.get("tools", []))
|
122
|
+
|
123
|
+
print(f"Configuration contains {num_routes} routes and {num_tools} tools.")
|
124
|
+
|
125
|
+
if num_tools > 0 and num_routes > 0:
|
126
|
+
print("MCP Proxy is configured correctly and ready to use.")
|
127
|
+
else:
|
128
|
+
print("WARNING: Configuration may be incomplete or incorrect.")
|
129
|
+
|
130
|
+
|
131
|
+
def main():
|
132
|
+
"""Main script function."""
|
133
|
+
# Define path to configuration file
|
134
|
+
config_path = os.path.join(os.path.dirname(__file__), "mcp_proxy_config.json")
|
135
|
+
|
136
|
+
# Analyze configuration
|
137
|
+
analyze_config(config_path)
|
138
|
+
|
139
|
+
|
140
|
+
if __name__ == "__main__":
|
141
|
+
main()
|
@@ -0,0 +1,161 @@
|
|
1
|
+
"""
|
2
|
+
Example of basic MCPProxyAdapter integration with an existing FastAPI application.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
from typing import Dict, Any, List, Optional
|
8
|
+
|
9
|
+
# Add parent directory to import path
|
10
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
11
|
+
parent_dir = os.path.dirname(current_dir)
|
12
|
+
if parent_dir not in sys.path:
|
13
|
+
sys.path.insert(0, parent_dir)
|
14
|
+
|
15
|
+
from fastapi import FastAPI, HTTPException, APIRouter
|
16
|
+
from pydantic import BaseModel
|
17
|
+
|
18
|
+
# Import from installed package or local file
|
19
|
+
try:
|
20
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter, configure_logger
|
21
|
+
from mcp_proxy_adapter.registry import CommandRegistry
|
22
|
+
except ImportError:
|
23
|
+
from src.adapter import MCPProxyAdapter, configure_logger
|
24
|
+
from src.registry import CommandRegistry
|
25
|
+
|
26
|
+
# Configure project logging
|
27
|
+
logging.basicConfig(
|
28
|
+
level=logging.INFO,
|
29
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
30
|
+
)
|
31
|
+
|
32
|
+
# Create project logger
|
33
|
+
project_logger = logging.getLogger("my_project")
|
34
|
+
project_logger.setLevel(logging.DEBUG)
|
35
|
+
|
36
|
+
# Configure adapter logger using project logger
|
37
|
+
adapter_logger = configure_logger(project_logger)
|
38
|
+
|
39
|
+
# Create FastAPI application
|
40
|
+
app = FastAPI(
|
41
|
+
title="My API with MCP Proxy Integration",
|
42
|
+
description="API with Command Registry integration, supporting MCP Proxy",
|
43
|
+
version="1.0.0"
|
44
|
+
)
|
45
|
+
|
46
|
+
# Define existing endpoints
|
47
|
+
router = APIRouter()
|
48
|
+
|
49
|
+
class Item(BaseModel):
|
50
|
+
name: str
|
51
|
+
price: float
|
52
|
+
is_active: bool = True
|
53
|
+
|
54
|
+
@router.get("/items", response_model=List[Item])
|
55
|
+
async def get_items():
|
56
|
+
"""Returns list of items."""
|
57
|
+
return [
|
58
|
+
{"name": "Item 1", "price": 10.5, "is_active": True},
|
59
|
+
{"name": "Item 2", "price": 20.0, "is_active": False},
|
60
|
+
{"name": "Item 3", "price": 30.0, "is_active": True},
|
61
|
+
]
|
62
|
+
|
63
|
+
@router.get("/items/{item_id}", response_model=Item)
|
64
|
+
async def get_item(item_id: int):
|
65
|
+
"""Returns item information by ID."""
|
66
|
+
items = [
|
67
|
+
{"name": "Item 1", "price": 10.5, "is_active": True},
|
68
|
+
{"name": "Item 2", "price": 20.0, "is_active": False},
|
69
|
+
{"name": "Item 3", "price": 30.0, "is_active": True},
|
70
|
+
]
|
71
|
+
|
72
|
+
if item_id < 1 or item_id > len(items):
|
73
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
74
|
+
|
75
|
+
return items[item_id - 1]
|
76
|
+
|
77
|
+
# Add existing endpoints to application
|
78
|
+
app.include_router(router)
|
79
|
+
|
80
|
+
# Define commands for Command Registry
|
81
|
+
def list_items() -> List[Dict[str, Any]]:
|
82
|
+
"""
|
83
|
+
Returns list of all items.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
List[Dict[str, Any]]: List of items
|
87
|
+
"""
|
88
|
+
return [
|
89
|
+
{"name": "Item 1", "price": 10.5, "is_active": True},
|
90
|
+
{"name": "Item 2", "price": 20.0, "is_active": False},
|
91
|
+
{"name": "Item 3", "price": 30.0, "is_active": True},
|
92
|
+
]
|
93
|
+
|
94
|
+
def get_item_by_id(item_id: int) -> Dict[str, Any]:
|
95
|
+
"""
|
96
|
+
Returns item information by ID.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
item_id: Item ID
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Dict[str, Any]: Item information
|
103
|
+
|
104
|
+
Raises:
|
105
|
+
ValueError: If item is not found
|
106
|
+
"""
|
107
|
+
items = list_items()
|
108
|
+
|
109
|
+
if item_id < 1 or item_id > len(items):
|
110
|
+
raise ValueError(f"Item with ID {item_id} not found")
|
111
|
+
|
112
|
+
return items[item_id - 1]
|
113
|
+
|
114
|
+
def search_items(query: str, min_price: Optional[float] = None, max_price: Optional[float] = None) -> List[Dict[str, Any]]:
|
115
|
+
"""
|
116
|
+
Searches for items by name and price range.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
query: Search query for name
|
120
|
+
min_price: Minimum price (optional)
|
121
|
+
max_price: Maximum price (optional)
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
List[Dict[str, Any]]: List of found items
|
125
|
+
"""
|
126
|
+
items = list_items()
|
127
|
+
|
128
|
+
# Filter by name
|
129
|
+
filtered_items = [item for item in items if query.lower() in item["name"].lower()]
|
130
|
+
|
131
|
+
# Filter by minimum price
|
132
|
+
if min_price is not None:
|
133
|
+
filtered_items = [item for item in filtered_items if item["price"] >= min_price]
|
134
|
+
|
135
|
+
# Filter by maximum price
|
136
|
+
if max_price is not None:
|
137
|
+
filtered_items = [item for item in filtered_items if item["price"] <= max_price]
|
138
|
+
|
139
|
+
return filtered_items
|
140
|
+
|
141
|
+
# Create CommandRegistry instance
|
142
|
+
registry = CommandRegistry()
|
143
|
+
|
144
|
+
# Register commands
|
145
|
+
registry.register_command("list_items", list_items)
|
146
|
+
registry.register_command("get_item", get_item_by_id)
|
147
|
+
registry.register_command("search_items", search_items)
|
148
|
+
|
149
|
+
# Create MCP Proxy adapter
|
150
|
+
adapter = MCPProxyAdapter(registry)
|
151
|
+
|
152
|
+
# Register endpoints in existing application
|
153
|
+
adapter.register_endpoints(app)
|
154
|
+
|
155
|
+
# Save configuration for MCP Proxy
|
156
|
+
adapter.save_config_to_file("mcp_proxy_config.json")
|
157
|
+
|
158
|
+
# Entry point for running the application
|
159
|
+
if __name__ == "__main__":
|
160
|
+
import uvicorn
|
161
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""
|
2
|
+
Docstring and Schema Example for MCPProxyAdapter
|
3
|
+
|
4
|
+
- How to write docstrings for commands
|
5
|
+
- How docstrings are used in OpenAPI/schema
|
6
|
+
- Best practices for documenting parameters and return values
|
7
|
+
|
8
|
+
Run:
|
9
|
+
python examples/docstring_and_schema_example.py
|
10
|
+
"""
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
14
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
15
|
+
|
16
|
+
class MyRegistry:
|
17
|
+
def __init__(self):
|
18
|
+
self.dispatcher = self
|
19
|
+
self.commands = {"sum": self.sum_numbers}
|
20
|
+
self.commands_info = {
|
21
|
+
"sum": {
|
22
|
+
"description": self.sum_numbers.__doc__,
|
23
|
+
"params": {
|
24
|
+
"a": {"type": "integer", "description": "First number", "required": True},
|
25
|
+
"b": {"type": "integer", "description": "Second number", "required": True}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
def get_valid_commands(self):
|
30
|
+
return list(self.commands.keys())
|
31
|
+
def get_command_info(self, command):
|
32
|
+
return self.commands_info.get(command)
|
33
|
+
def get_commands_info(self):
|
34
|
+
return self.commands_info
|
35
|
+
def execute(self, command, **params):
|
36
|
+
if command == "sum":
|
37
|
+
return self.sum_numbers(**params)
|
38
|
+
raise KeyError(f"Unknown command: {command}")
|
39
|
+
def add_generator(self, generator):
|
40
|
+
pass
|
41
|
+
def sum_numbers(self, a: int, b: int) -> int:
|
42
|
+
"""
|
43
|
+
Returns the sum of two numbers.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
a (int): First number
|
47
|
+
b (int): Second number
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
int: The sum of a and b
|
51
|
+
"""
|
52
|
+
return a + b
|
53
|
+
|
54
|
+
if __name__ == "__main__":
|
55
|
+
registry = MyRegistry()
|
56
|
+
adapter = MCPProxyAdapter(registry)
|
57
|
+
# Print OpenAPI schema (simulated)
|
58
|
+
schema = adapter.generate_mcp_proxy_config()
|
59
|
+
print("=== Tool description from docstring ===")
|
60
|
+
print(schema.tools[0].description)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""
|
2
|
+
Extension Example for MCPProxyAdapter
|
3
|
+
|
4
|
+
- How to add custom commands
|
5
|
+
- How to extend help logic
|
6
|
+
- How to customize error handling
|
7
|
+
|
8
|
+
Run:
|
9
|
+
python examples/extension_example.py
|
10
|
+
"""
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
14
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
15
|
+
|
16
|
+
class MyRegistry:
|
17
|
+
def __init__(self):
|
18
|
+
self.dispatcher = self
|
19
|
+
self.commands = {"ping": self.ping, "help": self.help_command}
|
20
|
+
self.commands_info = {
|
21
|
+
"ping": {"description": "Ping command (returns pong)", "params": {}},
|
22
|
+
"help": {"description": "Show help for commands", "params": {"command": {"type": "string", "description": "Command name", "required": False}}}
|
23
|
+
}
|
24
|
+
def get_valid_commands(self):
|
25
|
+
return list(self.commands.keys())
|
26
|
+
def get_command_info(self, command):
|
27
|
+
return self.commands_info.get(command)
|
28
|
+
def get_commands_info(self):
|
29
|
+
return self.commands_info
|
30
|
+
def execute(self, *args, **params):
|
31
|
+
if args:
|
32
|
+
command = args[0]
|
33
|
+
params = {k: v for k, v in params.items()}
|
34
|
+
else:
|
35
|
+
command = params.pop("command", None)
|
36
|
+
if command == "ping":
|
37
|
+
return self.ping()
|
38
|
+
if command == "help":
|
39
|
+
return self.help_command(**params)
|
40
|
+
raise KeyError(f"Unknown command: {command}")
|
41
|
+
def add_generator(self, generator):
|
42
|
+
pass
|
43
|
+
def ping(self):
|
44
|
+
"""Ping command."""
|
45
|
+
return {"result": "pong"}
|
46
|
+
def help_command(self, command: str = None):
|
47
|
+
"""Custom help logic: returns info for command or all commands."""
|
48
|
+
if not command:
|
49
|
+
return {"commands": list(self.commands_info.keys())}
|
50
|
+
if command in self.commands_info:
|
51
|
+
return {"command": command, "info": self.commands_info[command]}
|
52
|
+
return {"error": f"Command '{command}' not found"}
|
53
|
+
|
54
|
+
if __name__ == "__main__":
|
55
|
+
registry = MyRegistry()
|
56
|
+
adapter = MCPProxyAdapter(registry)
|
57
|
+
print("[EXT] Ping:", registry.execute("ping"))
|
58
|
+
print("[EXT] Help (all):", registry.execute("help"))
|
59
|
+
print("[EXT] Help (ping):", registry.execute("help", command="ping"))
|
60
|
+
print("[EXT] Help (notfound):", registry.execute("help", command="notfound"))
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""
|
2
|
+
Best Practices: Integrating and Testing 'help' Command in MCPProxyAdapter
|
3
|
+
|
4
|
+
This example demonstrates:
|
5
|
+
- How to robustly integrate a project-level 'help' command
|
6
|
+
- How to extend help with custom logic
|
7
|
+
- How to test help scenarios (including fallback and error cases)
|
8
|
+
- How to avoid common mistakes
|
9
|
+
|
10
|
+
Run:
|
11
|
+
python examples/help_best_practices.py
|
12
|
+
"""
|
13
|
+
import os
|
14
|
+
import sys
|
15
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
16
|
+
from typing import Any, Dict
|
17
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
18
|
+
from tests.test_mcp_proxy_adapter import MockRegistry
|
19
|
+
|
20
|
+
# --- Setup registry and adapter ---
|
21
|
+
registry = MockRegistry()
|
22
|
+
adapter = MCPProxyAdapter(registry)
|
23
|
+
|
24
|
+
def robust_help(command: str = None) -> Dict[str, Any]:
|
25
|
+
"""
|
26
|
+
Best practice: always check for project help, handle errors, fallback to adapter help.
|
27
|
+
"""
|
28
|
+
dispatcher = registry.dispatcher
|
29
|
+
if "help" in dispatcher.get_valid_commands():
|
30
|
+
try:
|
31
|
+
if command:
|
32
|
+
return dispatcher.help_command(command=command)
|
33
|
+
return dispatcher.help_command()
|
34
|
+
except Exception as e:
|
35
|
+
# Log error, fallback to adapter help
|
36
|
+
print(f"[WARN] Project help failed: {e}. Fallback to adapter help.")
|
37
|
+
return fallback_adapter_help(command)
|
38
|
+
else:
|
39
|
+
return fallback_adapter_help(command)
|
40
|
+
|
41
|
+
def fallback_adapter_help(command: str = None) -> Dict[str, Any]:
|
42
|
+
"""
|
43
|
+
Fallback: call adapter's help (simulate REST/JSON-RPC call).
|
44
|
+
"""
|
45
|
+
dispatcher = registry.dispatcher
|
46
|
+
if not command:
|
47
|
+
return {"source": "adapter", "commands": dispatcher.get_valid_commands()}
|
48
|
+
if command in dispatcher.get_valid_commands():
|
49
|
+
return {"source": "adapter", "command": command, "info": {"description": "Adapter help for command"}}
|
50
|
+
return {"source": "adapter", "error": f"Command '{command}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
|
51
|
+
|
52
|
+
# --- Example test cases ---
|
53
|
+
def test_help():
|
54
|
+
"""Test all help scenarios."""
|
55
|
+
print("[TEST] Project help (no param):", robust_help())
|
56
|
+
print("[TEST] Project help (existing command):", robust_help("success"))
|
57
|
+
print("[TEST] Project help (nonexistent command, triggers fallback):", robust_help("not_a_command"))
|
58
|
+
# Simulate registry without help
|
59
|
+
registry_no_help = MockRegistry()
|
60
|
+
if "help" in registry_no_help.dispatcher.commands:
|
61
|
+
del registry_no_help.dispatcher.commands["help"]
|
62
|
+
global registry
|
63
|
+
registry = registry_no_help
|
64
|
+
print("[TEST] Adapter help (no project help present):", robust_help("success"))
|
65
|
+
|
66
|
+
if __name__ == "__main__":
|
67
|
+
test_help()
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Example: Using 'help' command with MCPProxyAdapter
|
3
|
+
|
4
|
+
This script demonstrates how to:
|
5
|
+
- Call the 'help' command (if present in the project)
|
6
|
+
- Call 'help' with a parameter (for a specific command)
|
7
|
+
- Handle errors and fallback to adapter help
|
8
|
+
- Best practices for integrating and extending help
|
9
|
+
|
10
|
+
Run:
|
11
|
+
python examples/help_usage.py
|
12
|
+
"""
|
13
|
+
import os
|
14
|
+
import sys
|
15
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
16
|
+
from typing import Any, Dict
|
17
|
+
|
18
|
+
# Assume MCPProxyAdapter and MockRegistry are available from src and tests
|
19
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
20
|
+
from tests.test_mcp_proxy_adapter import MockRegistry
|
21
|
+
|
22
|
+
# --- Setup registry and adapter ---
|
23
|
+
registry = MockRegistry()
|
24
|
+
adapter = MCPProxyAdapter(registry)
|
25
|
+
|
26
|
+
# --- Best practice: always check if 'help' is in commands ---
|
27
|
+
def call_help(command: str = None) -> Dict[str, Any]:
|
28
|
+
"""Call help command with or without parameter."""
|
29
|
+
dispatcher = registry.dispatcher
|
30
|
+
if "help" in dispatcher.get_valid_commands():
|
31
|
+
if command:
|
32
|
+
try:
|
33
|
+
return dispatcher.help_command(command=command)
|
34
|
+
except Exception as e:
|
35
|
+
print(f"Project help failed: {e}. Fallback to adapter help.")
|
36
|
+
return adapter_help(command)
|
37
|
+
else:
|
38
|
+
return dispatcher.help_command()
|
39
|
+
else:
|
40
|
+
return adapter_help(command)
|
41
|
+
|
42
|
+
def adapter_help(command: str = None) -> Dict[str, Any]:
|
43
|
+
"""Fallback: call adapter's help (simulate)."""
|
44
|
+
dispatcher = registry.dispatcher
|
45
|
+
if not command:
|
46
|
+
return {"source": "adapter", "commands": dispatcher.get_valid_commands()}
|
47
|
+
if command in dispatcher.get_valid_commands():
|
48
|
+
return {"source": "adapter", "command": command, "info": {"description": "Adapter help for command"}}
|
49
|
+
return {"source": "adapter", "error": f"Command '{command}' not found (adapter)", "available_commands": dispatcher.get_valid_commands()}
|
50
|
+
|
51
|
+
if __name__ == "__main__":
|
52
|
+
print("=== Project help (no param) ===")
|
53
|
+
print(call_help())
|
54
|
+
print("\n=== Project help (existing command) ===")
|
55
|
+
print(call_help("success"))
|
56
|
+
print("\n=== Project help (nonexistent command, triggers fallback) ===")
|
57
|
+
print(call_help("not_a_command"))
|
58
|
+
print("\n=== Adapter help (no project help present) ===")
|
59
|
+
# Simulate registry without help
|
60
|
+
registry_no_help = MockRegistry()
|
61
|
+
if "help" in registry_no_help.dispatcher.commands:
|
62
|
+
del registry_no_help.dispatcher.commands["help"]
|
63
|
+
adapter_no_help = MCPProxyAdapter(registry_no_help)
|
64
|
+
print(adapter_help("success"))
|
@@ -0,0 +1,131 @@
|
|
1
|
+
"""
|
2
|
+
Client for testing interaction with OpenAPI server through MCP Proxy.
|
3
|
+
|
4
|
+
This script demonstrates how a client can send JSON-RPC requests
|
5
|
+
to MCP Proxy, which forwards them to the OpenAPI server.
|
6
|
+
|
7
|
+
Usage:
|
8
|
+
python examples/mcp_proxy_client.py
|
9
|
+
"""
|
10
|
+
import os
|
11
|
+
import sys
|
12
|
+
import json
|
13
|
+
import requests
|
14
|
+
from typing import Dict, Any, Optional
|
15
|
+
|
16
|
+
def send_jsonrpc_request(
|
17
|
+
endpoint: str,
|
18
|
+
method: str,
|
19
|
+
params: Optional[Dict[str, Any]] = None,
|
20
|
+
request_id: int = 1
|
21
|
+
) -> Dict[str, Any]:
|
22
|
+
"""
|
23
|
+
Sends JSON-RPC request to specified endpoint.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
endpoint (str): Endpoint URL
|
27
|
+
method (str): Method name to call
|
28
|
+
params (Optional[Dict[str, Any]], optional): Method parameters. Defaults to None.
|
29
|
+
request_id (int, optional): Request ID. Defaults to 1.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Dict[str, Any]: Server response
|
33
|
+
"""
|
34
|
+
# Form JSON-RPC request
|
35
|
+
payload = {
|
36
|
+
"jsonrpc": "2.0",
|
37
|
+
"method": method,
|
38
|
+
"id": request_id
|
39
|
+
}
|
40
|
+
|
41
|
+
# Add parameters if they exist
|
42
|
+
if params:
|
43
|
+
payload["params"] = params
|
44
|
+
|
45
|
+
# Send request
|
46
|
+
response = requests.post(endpoint, json=payload)
|
47
|
+
|
48
|
+
# Return response in JSON format
|
49
|
+
return response.json()
|
50
|
+
|
51
|
+
def print_response(response: Dict[str, Any]) -> None:
|
52
|
+
"""
|
53
|
+
Prints response in formatted view.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
response (Dict[str, Any]): Server response
|
57
|
+
"""
|
58
|
+
print("Response:")
|
59
|
+
print(json.dumps(response, indent=2, ensure_ascii=False))
|
60
|
+
print("-" * 50)
|
61
|
+
|
62
|
+
def main():
|
63
|
+
"""Main function for demonstrating work with MCP Proxy."""
|
64
|
+
# Base server URL
|
65
|
+
server_url = "http://localhost:8000"
|
66
|
+
|
67
|
+
# JSON-RPC endpoint (should match cmd_endpoint in MCPProxyAdapter)
|
68
|
+
jsonrpc_endpoint = f"{server_url}/api/command"
|
69
|
+
|
70
|
+
print("=== Testing JSON-RPC requests through MCP Proxy ===\n")
|
71
|
+
|
72
|
+
# Example 1: Get all items
|
73
|
+
print("1. Getting all items:")
|
74
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "get_items")
|
75
|
+
print_response(response)
|
76
|
+
|
77
|
+
# Example 2: Get item by ID
|
78
|
+
print("2. Getting item by ID:")
|
79
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "get_item", {"item_id": 1})
|
80
|
+
print_response(response)
|
81
|
+
|
82
|
+
# Example 3: Create new item
|
83
|
+
print("3. Creating new item:")
|
84
|
+
new_item = {
|
85
|
+
"name": "Test Item",
|
86
|
+
"description": "Item for API testing",
|
87
|
+
"price": 123.45,
|
88
|
+
"is_available": True
|
89
|
+
}
|
90
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "create_item", {"item": new_item})
|
91
|
+
print_response(response)
|
92
|
+
|
93
|
+
# Get created item ID for further operations
|
94
|
+
if "result" in response:
|
95
|
+
created_item_id = response["result"]["id"]
|
96
|
+
else:
|
97
|
+
# Use fixed ID in case of error
|
98
|
+
created_item_id = 4
|
99
|
+
|
100
|
+
# Example 4: Update item
|
101
|
+
print(f"4. Updating item with ID {created_item_id}:")
|
102
|
+
updated_data = {
|
103
|
+
"name": "Updated Item",
|
104
|
+
"price": 199.99
|
105
|
+
}
|
106
|
+
response = send_jsonrpc_request(
|
107
|
+
jsonrpc_endpoint,
|
108
|
+
"update_item",
|
109
|
+
{"item_id": created_item_id, "updated_data": updated_data}
|
110
|
+
)
|
111
|
+
print_response(response)
|
112
|
+
|
113
|
+
# Example 5: Search items
|
114
|
+
print("5. Searching items by keyword:")
|
115
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "search_items", {"keyword": "updated"})
|
116
|
+
print_response(response)
|
117
|
+
|
118
|
+
# Example 6: Delete item
|
119
|
+
print(f"6. Deleting item with ID {created_item_id}:")
|
120
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "delete_item", {"item_id": created_item_id})
|
121
|
+
print_response(response)
|
122
|
+
|
123
|
+
# Example 7: Check after deletion
|
124
|
+
print("7. Checking items list after deletion:")
|
125
|
+
response = send_jsonrpc_request(jsonrpc_endpoint, "get_items")
|
126
|
+
print_response(response)
|
127
|
+
|
128
|
+
print("Testing completed.")
|
129
|
+
|
130
|
+
if __name__ == "__main__":
|
131
|
+
main()
|
@@ -0,0 +1,397 @@
|
|
1
|
+
"""
|
2
|
+
Example of creating an OpenAPI server using MCP Proxy Adapter.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python examples/openapi_server.py
|
6
|
+
|
7
|
+
Server will be available at: http://localhost:8000
|
8
|
+
"""
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
import logging
|
12
|
+
from typing import Dict, List, Any
|
13
|
+
|
14
|
+
# Add project root directory to sys.path
|
15
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
16
|
+
|
17
|
+
from fastapi import FastAPI, Query, Path, Body, Request
|
18
|
+
from pydantic import BaseModel, Field
|
19
|
+
import uvicorn
|
20
|
+
|
21
|
+
# Import MCP Proxy Adapter
|
22
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
23
|
+
|
24
|
+
# Import models for JSON-RPC
|
25
|
+
from mcp_proxy_adapter.models import JsonRpcRequest, JsonRpcResponse
|
26
|
+
|
27
|
+
# Import MockRegistry from tests for example
|
28
|
+
# (in a real project, CommandRegistry would be used)
|
29
|
+
from tests.test_mcp_proxy_adapter import MockRegistry
|
30
|
+
|
31
|
+
# Configure logging
|
32
|
+
logging.basicConfig(level=logging.DEBUG)
|
33
|
+
logger = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
# Data model definitions
|
36
|
+
class Item(BaseModel):
|
37
|
+
"""Data model for an item."""
|
38
|
+
id: int = Field(..., description="Unique item identifier")
|
39
|
+
name: str = Field(..., description="Item name")
|
40
|
+
description: str = Field(None, description="Item description")
|
41
|
+
price: float = Field(..., description="Item price")
|
42
|
+
is_available: bool = Field(True, description="Item availability")
|
43
|
+
|
44
|
+
model_config = {
|
45
|
+
"json_schema_extra": {
|
46
|
+
"example": {
|
47
|
+
"id": 1,
|
48
|
+
"name": "Super Product",
|
49
|
+
"description": "Best product on the market",
|
50
|
+
"price": 99.99,
|
51
|
+
"is_available": True
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
# Example data
|
57
|
+
items_db = [
|
58
|
+
{
|
59
|
+
"id": 1,
|
60
|
+
"name": "Smartphone X",
|
61
|
+
"description": "Latest smartphone with cutting-edge technology",
|
62
|
+
"price": 999.99,
|
63
|
+
"is_available": True
|
64
|
+
},
|
65
|
+
{
|
66
|
+
"id": 2,
|
67
|
+
"name": "Laptop Y",
|
68
|
+
"description": "Powerful laptop for professionals",
|
69
|
+
"price": 1499.99,
|
70
|
+
"is_available": True
|
71
|
+
},
|
72
|
+
{
|
73
|
+
"id": 3,
|
74
|
+
"name": "Tablet Z",
|
75
|
+
"description": "Compact tablet for creativity",
|
76
|
+
"price": 599.99,
|
77
|
+
"is_available": False
|
78
|
+
}
|
79
|
+
]
|
80
|
+
|
81
|
+
# Define commands for MockRegistry
|
82
|
+
class MockDispatcher:
|
83
|
+
"""Mock for command dispatcher in example."""
|
84
|
+
|
85
|
+
def __init__(self):
|
86
|
+
self.commands = {
|
87
|
+
"get_items": self.get_items,
|
88
|
+
"get_item": self.get_item,
|
89
|
+
"create_item": self.create_item,
|
90
|
+
"update_item": self.update_item,
|
91
|
+
"delete_item": self.delete_item,
|
92
|
+
"search_items": self.search_items,
|
93
|
+
"execute": self.execute_command,
|
94
|
+
"help": self.help_command
|
95
|
+
}
|
96
|
+
self.commands_info = {
|
97
|
+
"get_items": {
|
98
|
+
"description": "Get list of all items",
|
99
|
+
"params": {}
|
100
|
+
},
|
101
|
+
"get_item": {
|
102
|
+
"description": "Get item by ID",
|
103
|
+
"params": {
|
104
|
+
"item_id": {
|
105
|
+
"type": "integer",
|
106
|
+
"description": "Item ID to search for",
|
107
|
+
"required": True
|
108
|
+
}
|
109
|
+
}
|
110
|
+
},
|
111
|
+
"create_item": {
|
112
|
+
"description": "Create new item",
|
113
|
+
"params": {
|
114
|
+
"item": {
|
115
|
+
"type": "object",
|
116
|
+
"description": "Item data",
|
117
|
+
"required": True
|
118
|
+
}
|
119
|
+
}
|
120
|
+
},
|
121
|
+
"update_item": {
|
122
|
+
"description": "Update item by ID",
|
123
|
+
"params": {
|
124
|
+
"item_id": {
|
125
|
+
"type": "integer",
|
126
|
+
"description": "Item ID to update",
|
127
|
+
"required": True
|
128
|
+
},
|
129
|
+
"updated_data": {
|
130
|
+
"type": "object",
|
131
|
+
"description": "Updated data",
|
132
|
+
"required": True
|
133
|
+
}
|
134
|
+
}
|
135
|
+
},
|
136
|
+
"delete_item": {
|
137
|
+
"description": "Delete item by ID",
|
138
|
+
"params": {
|
139
|
+
"item_id": {
|
140
|
+
"type": "integer",
|
141
|
+
"description": "Item ID to delete",
|
142
|
+
"required": True
|
143
|
+
}
|
144
|
+
}
|
145
|
+
},
|
146
|
+
"search_items": {
|
147
|
+
"description": "Search items by keyword",
|
148
|
+
"params": {
|
149
|
+
"keyword": {
|
150
|
+
"type": "string",
|
151
|
+
"description": "Search keyword",
|
152
|
+
"required": True
|
153
|
+
}
|
154
|
+
}
|
155
|
+
},
|
156
|
+
"execute": {
|
157
|
+
"description": "Universal command for executing queries",
|
158
|
+
"params": {
|
159
|
+
"query": {
|
160
|
+
"type": "string",
|
161
|
+
"description": "Query to execute",
|
162
|
+
"required": False
|
163
|
+
},
|
164
|
+
"subcommand": {
|
165
|
+
"type": "string",
|
166
|
+
"description": "Subcommand to execute",
|
167
|
+
"required": False
|
168
|
+
}
|
169
|
+
}
|
170
|
+
},
|
171
|
+
"help": {
|
172
|
+
"description": "Show information about available commands or a specific command.",
|
173
|
+
"params": {
|
174
|
+
"command": {
|
175
|
+
"type": "string",
|
176
|
+
"description": "Command name for detailed info",
|
177
|
+
"required": False
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
def execute(self, command_name, **params):
|
184
|
+
"""Executes command with specified parameters."""
|
185
|
+
if command_name not in self.commands:
|
186
|
+
raise KeyError(f"Unknown command: {command_name}")
|
187
|
+
return self.commands[command_name](**params)
|
188
|
+
|
189
|
+
def execute_command(self, **params):
|
190
|
+
"""Universal method for executing commands."""
|
191
|
+
query = params.get("query", "")
|
192
|
+
subcommand = params.get("subcommand", "")
|
193
|
+
|
194
|
+
# Debug logging
|
195
|
+
logger.info(f"Executing universal command with query={query}, subcommand={subcommand}, params={params}")
|
196
|
+
|
197
|
+
# Handle different command types
|
198
|
+
if query.lower() in ["list", "all", "items"]:
|
199
|
+
# Return list of all items
|
200
|
+
return self.get_items()
|
201
|
+
elif query.lower() == "search" and "keyword" in params:
|
202
|
+
# Search by keyword
|
203
|
+
return self.search_items(params["keyword"])
|
204
|
+
elif query.isdigit() or (isinstance(query, int) and query > 0):
|
205
|
+
# If query looks like an ID, return item by ID
|
206
|
+
try:
|
207
|
+
return self.get_item(int(query))
|
208
|
+
except ValueError:
|
209
|
+
return {"error": f"Item with ID {query} not found"}
|
210
|
+
else:
|
211
|
+
# By default return information about available commands
|
212
|
+
commands = list(self.commands.keys())
|
213
|
+
return {
|
214
|
+
"available_commands": commands,
|
215
|
+
"message": "Use one of the available commands or specify query for executing query",
|
216
|
+
"received_params": params
|
217
|
+
}
|
218
|
+
|
219
|
+
def get_valid_commands(self):
|
220
|
+
"""Returns list of available commands."""
|
221
|
+
return list(self.commands.keys())
|
222
|
+
|
223
|
+
def get_command_info(self, command_name):
|
224
|
+
"""Returns information about command."""
|
225
|
+
return self.commands_info.get(command_name)
|
226
|
+
|
227
|
+
def get_commands_info(self):
|
228
|
+
"""Returns information about all commands."""
|
229
|
+
return self.commands_info
|
230
|
+
|
231
|
+
# Command implementations
|
232
|
+
def get_items(self):
|
233
|
+
"""Get list of all items."""
|
234
|
+
return items_db
|
235
|
+
|
236
|
+
def get_item(self, item_id):
|
237
|
+
"""Get item by ID."""
|
238
|
+
for item in items_db:
|
239
|
+
if item["id"] == item_id:
|
240
|
+
return item
|
241
|
+
raise ValueError(f"Item with ID {item_id} not found")
|
242
|
+
|
243
|
+
def create_item(self, item):
|
244
|
+
"""Create new item."""
|
245
|
+
# Find maximum ID
|
246
|
+
max_id = max([i["id"] for i in items_db]) if items_db else 0
|
247
|
+
# Create new item with increased ID
|
248
|
+
new_item = {**item, "id": max_id + 1}
|
249
|
+
items_db.append(new_item)
|
250
|
+
return new_item
|
251
|
+
|
252
|
+
def update_item(self, item_id, updated_data):
|
253
|
+
"""Update item by ID."""
|
254
|
+
for i, item in enumerate(items_db):
|
255
|
+
if item["id"] == item_id:
|
256
|
+
# Update item, keeping original ID
|
257
|
+
updated_item = {**item, **updated_data, "id": item_id}
|
258
|
+
items_db[i] = updated_item
|
259
|
+
return updated_item
|
260
|
+
raise ValueError(f"Item with ID {item_id} not found")
|
261
|
+
|
262
|
+
def delete_item(self, item_id):
|
263
|
+
"""Delete item by ID."""
|
264
|
+
for i, item in enumerate(items_db):
|
265
|
+
if item["id"] == item_id:
|
266
|
+
del items_db[i]
|
267
|
+
return {"message": f"Item with ID {item_id} successfully deleted"}
|
268
|
+
raise ValueError(f"Item with ID {item_id} not found")
|
269
|
+
|
270
|
+
def search_items(self, keyword):
|
271
|
+
"""Search items by keyword."""
|
272
|
+
keyword = keyword.lower()
|
273
|
+
return [
|
274
|
+
item for item in items_db
|
275
|
+
if keyword in item["name"].lower() or
|
276
|
+
(item["description"] and keyword in item["description"].lower())
|
277
|
+
]
|
278
|
+
|
279
|
+
def help_command(self, **params):
|
280
|
+
"""Return info about all commands or a specific command."""
|
281
|
+
# Если в будущем появится пользовательская команда help, можно реализовать её здесь
|
282
|
+
command = params.get("command")
|
283
|
+
if command:
|
284
|
+
info = self.commands_info.get(command)
|
285
|
+
if info:
|
286
|
+
return {"command": command, "info": info}
|
287
|
+
else:
|
288
|
+
return {"error": f"Command '{command}' not found", "available_commands": list(self.commands_info.keys())}
|
289
|
+
# Если параметр command не указан, возвращаем краткую информацию обо всех
|
290
|
+
return {
|
291
|
+
"commands": {cmd: {"description": info["description"], "params": info["params"]} for cmd, info in self.commands_info.items()},
|
292
|
+
"total": len(self.commands_info),
|
293
|
+
"note": "Use the 'command' parameter to get detailed information about a specific command"
|
294
|
+
}
|
295
|
+
|
296
|
+
class CustomMockRegistry(MockRegistry):
|
297
|
+
"""Custom command registry for example."""
|
298
|
+
|
299
|
+
def __init__(self):
|
300
|
+
"""Initialization with custom dispatcher."""
|
301
|
+
self.dispatcher = MockDispatcher()
|
302
|
+
self.generators = []
|
303
|
+
|
304
|
+
def main():
|
305
|
+
"""Main function to start server."""
|
306
|
+
# Create command registry
|
307
|
+
registry = CustomMockRegistry()
|
308
|
+
|
309
|
+
# Create FastAPI object
|
310
|
+
app = FastAPI(
|
311
|
+
title="OpenAPI Server Example",
|
312
|
+
description="Example OpenAPI server with MCP Proxy Adapter integration",
|
313
|
+
version="1.0.0"
|
314
|
+
)
|
315
|
+
|
316
|
+
# Configure CORS
|
317
|
+
from fastapi.middleware.cors import CORSMiddleware
|
318
|
+
app.add_middleware(
|
319
|
+
CORSMiddleware,
|
320
|
+
allow_origins=["*"], # Allow requests from all sources
|
321
|
+
allow_credentials=True,
|
322
|
+
allow_methods=["*"], # Allow all methods
|
323
|
+
allow_headers=["*"], # Allow all headers
|
324
|
+
)
|
325
|
+
|
326
|
+
# Create MCP Proxy adapter with explicit endpoint
|
327
|
+
adapter = MCPProxyAdapter(registry, cmd_endpoint="/cmd")
|
328
|
+
|
329
|
+
# Register adapter endpoints
|
330
|
+
adapter.register_endpoints(app)
|
331
|
+
|
332
|
+
# Save MCP Proxy configuration to file
|
333
|
+
config_path = os.path.join(os.path.dirname(__file__), "mcp_proxy_config.json")
|
334
|
+
adapter.save_config_to_file(config_path)
|
335
|
+
logger.info(f"MCP Proxy configuration saved to {config_path}")
|
336
|
+
|
337
|
+
# Define REST endpoints for example (not related to MCP Proxy)
|
338
|
+
@app.get("/")
|
339
|
+
def read_root():
|
340
|
+
"""Root endpoint."""
|
341
|
+
return {
|
342
|
+
"message": "OpenAPI Server Example with MCP Proxy Adapter integration",
|
343
|
+
"endpoints": {
|
344
|
+
"items": "/items",
|
345
|
+
"item": "/items/{item_id}",
|
346
|
+
"search": "/items/search",
|
347
|
+
"mcp_proxy": "/cmd"
|
348
|
+
}
|
349
|
+
}
|
350
|
+
|
351
|
+
@app.get("/items", response_model=List[Item])
|
352
|
+
def read_items():
|
353
|
+
"""Get all items."""
|
354
|
+
return items_db
|
355
|
+
|
356
|
+
@app.get("/items/{item_id}", response_model=Item)
|
357
|
+
def read_item(item_id: int = Path(..., description="Item ID", gt=0)):
|
358
|
+
"""Get item by ID."""
|
359
|
+
try:
|
360
|
+
return registry.dispatcher.get_item(item_id)
|
361
|
+
except ValueError as e:
|
362
|
+
return {"error": str(e)}
|
363
|
+
|
364
|
+
@app.post("/items", response_model=Item)
|
365
|
+
def create_new_item(item: Item = Body(..., description="Data of new item")):
|
366
|
+
"""Create new item."""
|
367
|
+
return registry.dispatcher.create_item(item.model_dump())
|
368
|
+
|
369
|
+
@app.put("/items/{item_id}", response_model=Item)
|
370
|
+
def update_existing_item(
|
371
|
+
item_id: int = Path(..., description="Item ID to update", gt=0),
|
372
|
+
item: Item = Body(..., description="Updated item data")
|
373
|
+
):
|
374
|
+
"""Update item by ID."""
|
375
|
+
try:
|
376
|
+
return registry.dispatcher.update_item(item_id, item.model_dump())
|
377
|
+
except ValueError as e:
|
378
|
+
return {"error": str(e)}
|
379
|
+
|
380
|
+
@app.delete("/items/{item_id}")
|
381
|
+
def delete_existing_item(item_id: int = Path(..., description="Item ID to delete", gt=0)):
|
382
|
+
"""Delete item by ID."""
|
383
|
+
try:
|
384
|
+
return registry.dispatcher.delete_item(item_id)
|
385
|
+
except ValueError as e:
|
386
|
+
return {"error": str(e)}
|
387
|
+
|
388
|
+
@app.get("/items/search", response_model=List[Item])
|
389
|
+
def search_items_by_keyword(keyword: str = Query(..., description="Search keyword")):
|
390
|
+
"""Search items by keyword."""
|
391
|
+
return registry.dispatcher.search_items(keyword)
|
392
|
+
|
393
|
+
# Start server
|
394
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
395
|
+
|
396
|
+
if __name__ == "__main__":
|
397
|
+
main()
|
@@ -0,0 +1,47 @@
|
|
1
|
+
"""
|
2
|
+
Project Structure Example for MCPProxyAdapter
|
3
|
+
|
4
|
+
- How to organize your project for clean integration
|
5
|
+
- Where to place registry, commands, adapter
|
6
|
+
- How to register endpoints in FastAPI
|
7
|
+
|
8
|
+
Run:
|
9
|
+
python examples/project_structure_example.py
|
10
|
+
"""
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
14
|
+
from fastapi import FastAPI
|
15
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
16
|
+
|
17
|
+
# --- Command registry and commands ---
|
18
|
+
class MyRegistry:
|
19
|
+
def __init__(self):
|
20
|
+
self.dispatcher = self
|
21
|
+
self.commands = {"hello": self.hello}
|
22
|
+
self.commands_info = {"hello": {"description": "Say hello", "params": {}}}
|
23
|
+
def get_valid_commands(self):
|
24
|
+
return list(self.commands.keys())
|
25
|
+
def get_command_info(self, command):
|
26
|
+
return self.commands_info.get(command)
|
27
|
+
def get_commands_info(self):
|
28
|
+
return self.commands_info
|
29
|
+
def execute(self, command, **params):
|
30
|
+
if command == "hello":
|
31
|
+
return {"message": "Hello, world!"}
|
32
|
+
raise KeyError(f"Unknown command: {command}")
|
33
|
+
def add_generator(self, generator):
|
34
|
+
pass
|
35
|
+
def hello(self):
|
36
|
+
"""Say hello."""
|
37
|
+
return {"message": "Hello, world!"}
|
38
|
+
|
39
|
+
# --- FastAPI app and adapter ---
|
40
|
+
app = FastAPI()
|
41
|
+
registry = MyRegistry()
|
42
|
+
adapter = MCPProxyAdapter(registry)
|
43
|
+
adapter.register_endpoints(app)
|
44
|
+
|
45
|
+
if __name__ == "__main__":
|
46
|
+
import uvicorn
|
47
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
Testing Example for MCPProxyAdapter
|
3
|
+
|
4
|
+
- How to write unit and integration tests for commands
|
5
|
+
- How to test help and error handling
|
6
|
+
- Best practices for test structure
|
7
|
+
|
8
|
+
Run:
|
9
|
+
python examples/testing_example.py
|
10
|
+
"""
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
|
14
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter
|
15
|
+
|
16
|
+
class MyRegistry:
|
17
|
+
def __init__(self):
|
18
|
+
self.dispatcher = self
|
19
|
+
self.commands = {"echo": self.echo}
|
20
|
+
self.commands_info = {"echo": {"description": "Echo input string", "params": {"text": {"type": "string", "description": "Text to echo", "required": True}}}}
|
21
|
+
def get_valid_commands(self):
|
22
|
+
return list(self.commands.keys())
|
23
|
+
def get_command_info(self, command):
|
24
|
+
return self.commands_info.get(command)
|
25
|
+
def get_commands_info(self):
|
26
|
+
return self.commands_info
|
27
|
+
def execute(self, command, **params):
|
28
|
+
if command == "echo":
|
29
|
+
return self.echo(**params)
|
30
|
+
raise KeyError(f"Unknown command: {command}")
|
31
|
+
def add_generator(self, generator):
|
32
|
+
pass
|
33
|
+
def echo(self, text: str) -> str:
|
34
|
+
"""Echo input string."""
|
35
|
+
return text
|
36
|
+
|
37
|
+
def test_echo():
|
38
|
+
registry = MyRegistry()
|
39
|
+
adapter = MCPProxyAdapter(registry)
|
40
|
+
# Unit test
|
41
|
+
assert registry.execute("echo", text="hi") == "hi"
|
42
|
+
# Integration test (simulate JSON-RPC)
|
43
|
+
class Request:
|
44
|
+
method = "echo"
|
45
|
+
params = {"text": "hello"}
|
46
|
+
id = 1
|
47
|
+
response = adapter.router.routes[0].endpoint(Request())
|
48
|
+
# Not a real FastAPI call, just for illustration
|
49
|
+
print("[TEST] Echo command passed.")
|
50
|
+
|
51
|
+
if __name__ == "__main__":
|
52
|
+
test_echo()
|
53
|
+
print("All tests passed.")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-proxy-adapter
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.4
|
4
4
|
Summary: Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.
|
5
5
|
Home-page: https://github.com/vasilyvz/mcp-proxy-adapter
|
6
6
|
Author: Vasiliy VZ
|
@@ -303,4 +303,100 @@ See [docs/](docs/) for detailed guides, architecture, and examples.
|
|
303
303
|
7. **Add usage example** to examples/
|
304
304
|
8. **Check help integration** (with/without param)
|
305
305
|
9. **Check schema/OpenAPI generation**
|
306
|
-
10. **Document in README.md** (EN/RU)
|
306
|
+
10. **Document in README.md** (EN/RU)
|
307
|
+
|
308
|
+
## ❓ FAQ
|
309
|
+
|
310
|
+
### Ошибка: got multiple values for argument 'command' при вызове команды help
|
311
|
+
|
312
|
+
**Проблема:**
|
313
|
+
|
314
|
+
Если в JSON-RPC запросе к endpoint `/cmd` используется команда `help` с параметром `command`, может возникнуть ошибка:
|
315
|
+
|
316
|
+
```
|
317
|
+
TypeError: help_command() got multiple values for argument 'command'
|
318
|
+
```
|
319
|
+
|
320
|
+
**Причина:**
|
321
|
+
|
322
|
+
В Python, если метод `execute(self, command, **params)` получает параметр `command` и в `params` также есть ключ `command`, возникает конфликт имён.
|
323
|
+
|
324
|
+
**Решение:**
|
325
|
+
|
326
|
+
Переименуйте первый аргумент метода `execute` в классе `MockDispatcher` (и аналогичных) с `command` на `command_name`:
|
327
|
+
|
328
|
+
```python
|
329
|
+
def execute(self, command_name, **params):
|
330
|
+
if command_name not in self.commands:
|
331
|
+
raise KeyError(f"Unknown command: {command_name}")
|
332
|
+
return self.commands[command_name](**params)
|
333
|
+
```
|
334
|
+
|
335
|
+
Это устранит конфликт и позволит корректно вызывать команду help с параметром `command` через JSON-RPC.
|
336
|
+
|
337
|
+
## 🚀 Deployment & Packaging FAQ
|
338
|
+
|
339
|
+
### Как собрать, проверить и опубликовать пакет (wheel/sdist) с примерами и документацией
|
340
|
+
|
341
|
+
1. **Перенесите каталоги `examples` и `docs` внутрь основного пакета** (например, `mcp_proxy_adapter/examples`, `mcp_proxy_adapter/docs`).
|
342
|
+
2. **Обновите `setup.py`:**
|
343
|
+
- Укажите `include_package_data=True`.
|
344
|
+
- В `package_data` добавьте:
|
345
|
+
```python
|
346
|
+
package_data={
|
347
|
+
'mcp_proxy_adapter': ['examples/*.py', 'examples/*.json', 'docs/*.md', '../README.md'],
|
348
|
+
},
|
349
|
+
```
|
350
|
+
3. **Обновите `MANIFEST.in`:**
|
351
|
+
- Убедитесь, что включены нужные файлы:
|
352
|
+
```
|
353
|
+
include README.md
|
354
|
+
include LICENSE
|
355
|
+
include requirements.txt
|
356
|
+
include pyproject.toml
|
357
|
+
include code_index.yaml
|
358
|
+
recursive-include mcp_proxy_adapter/examples *.py *.json
|
359
|
+
recursive-include mcp_proxy_adapter/docs *.md
|
360
|
+
```
|
361
|
+
4. **Соберите пакет:**
|
362
|
+
```bash
|
363
|
+
rm -rf dist build mcp_proxy_adapter.egg-info
|
364
|
+
python3 -m build
|
365
|
+
```
|
366
|
+
5. **Создайте новое виртуальное окружение и установите пакет:**
|
367
|
+
```bash
|
368
|
+
python3 -m venv ../mcp_proxy_adapter_test_env
|
369
|
+
source ../mcp_proxy_adapter_test_env/bin/activate
|
370
|
+
pip install --upgrade pip
|
371
|
+
pip install dist/mcp_proxy_adapter-*.whl
|
372
|
+
```
|
373
|
+
6. **Проверьте, что примеры и документация попали в пакет:**
|
374
|
+
```bash
|
375
|
+
ls -l ../mcp_proxy_adapter_test_env/lib/python*/site-packages/mcp_proxy_adapter/examples
|
376
|
+
ls -l ../mcp_proxy_adapter_test_env/lib/python*/site-packages/mcp_proxy_adapter/docs
|
377
|
+
```
|
378
|
+
7. **Запустите пример сервера:**
|
379
|
+
```bash
|
380
|
+
python ../mcp_proxy_adapter_test_env/lib/python*/site-packages/mcp_proxy_adapter/examples/openapi_server.py
|
381
|
+
```
|
382
|
+
8. **Проверьте работоспособность через curl:**
|
383
|
+
```bash
|
384
|
+
curl http://localhost:8000/openapi.json | jq .
|
385
|
+
```
|
386
|
+
9. **Публикация на PyPI:**
|
387
|
+
- Проверьте, что у вас настроен `~/.pypirc` и установлен twine:
|
388
|
+
```bash
|
389
|
+
pip install twine
|
390
|
+
twine upload dist/*
|
391
|
+
```
|
392
|
+
|
393
|
+
### Типовые проблемы и решения
|
394
|
+
- **Примеры или документация не попадают в пакет:**
|
395
|
+
- Убедитесь, что они находятся внутри основного пакета и правильно указаны в `package_data` и `MANIFEST.in`.
|
396
|
+
- **Каталог docs не виден в wheel:**
|
397
|
+
- Проверьте расширения файлов и шаблоны в `package_data`/`MANIFEST.in`.
|
398
|
+
- **Проверяйте установку только через wheel, а не через sdist!**
|
399
|
+
|
400
|
+
**Best practice:**
|
401
|
+
- Для публикации документации используйте GitHub и PyPI project page (README.md).
|
402
|
+
- Для примеров — всегда размещайте их внутри пакета, если хотите распространять с wheel.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
mcp_proxy_adapter/__init__.py,sha256=_6D-TfANWp9zc550M5LUeGPvioFqG1bAl3tZj-gNmJU,463
|
2
|
+
mcp_proxy_adapter/adapter.py,sha256=76dkVeDuqLsJ5AhuftzLlwy2M6yr_PfNbmNfo9dXVhc,28844
|
3
|
+
mcp_proxy_adapter/models.py,sha256=acqVQBYAojHXeJ1MJyvpMyT6-J6aMxWuZMszn_-RsOU,2338
|
4
|
+
mcp_proxy_adapter/registry.py,sha256=jgC4TKaPbMbAsoxvGp2ToaOE4drD-VfZug7WJbm4IW4,15853
|
5
|
+
mcp_proxy_adapter/schema.py,sha256=HZM0TTQTSi8ha1TEeVevdCyGZOUPoT1soB7Nex0hV50,10947
|
6
|
+
mcp_proxy_adapter/analyzers/__init__.py,sha256=2rcYZDP-bXq078MQpxP32lAwYYyRhOwAQGBcefBfBzY,368
|
7
|
+
mcp_proxy_adapter/analyzers/docstring_analyzer.py,sha256=T3FLJEo_uChShfiEKRl8GpVoHvh5HiudZkxnj4KixfA,7541
|
8
|
+
mcp_proxy_adapter/analyzers/type_analyzer.py,sha256=6Wac7osKwF03waFSwQ8ZM0Wqn_zAP2D-I4WMEpR0hQM,5230
|
9
|
+
mcp_proxy_adapter/dispatchers/__init__.py,sha256=FWgimgInGphIjCEnvA3-ZExiapUzYAVis2H9C5IWivU,365
|
10
|
+
mcp_proxy_adapter/dispatchers/base_dispatcher.py,sha256=S5_Xri058jAmOWeit1tedB_GMZQ9RLcNcYabA83ZF6k,2288
|
11
|
+
mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py,sha256=ffu1M32E1AdC7IB44mlbV2L56eJQMsp-7fYi_r4rmHc,6331
|
12
|
+
mcp_proxy_adapter/examples/analyze_config.py,sha256=vog7TNHDw5ZoYhQLbAvZvEoufmQwH54KJzQBJrSq5w4,4283
|
13
|
+
mcp_proxy_adapter/examples/basic_integration.py,sha256=w_oA777YiQt36gzI113KPQ6k45caXbMCqW9hD8sy8zo,4657
|
14
|
+
mcp_proxy_adapter/examples/docstring_and_schema_example.py,sha256=c96L4KF_7yWzffmvd4hyeQuXSdYyYkv7Uvuy0QxgMcQ,1929
|
15
|
+
mcp_proxy_adapter/examples/extension_example.py,sha256=vnatnFdNTapMpPcQ79Ugitk92ZiUfpLTs7Dvsodf1og,2277
|
16
|
+
mcp_proxy_adapter/examples/help_best_practices.py,sha256=wUtZRnAktnpfAc9vAvqSxUquHEr5ewaPDPyc6BoCqdQ,2637
|
17
|
+
mcp_proxy_adapter/examples/help_usage.py,sha256=UOd3HJeYlQpQkAyceGNm66jXX_h-T05pjIGD-b7-Pfg,2568
|
18
|
+
mcp_proxy_adapter/examples/mcp_proxy_client.py,sha256=z4IzFlGigVTQSb8TpcrQ_a0migsmC58LnNwc8wZmTfw,3811
|
19
|
+
mcp_proxy_adapter/examples/openapi_server.py,sha256=xAt-aUEz5vusvz7fd1vFIybi4EpwlnGPXLsce3j3YZw,14024
|
20
|
+
mcp_proxy_adapter/examples/project_structure_example.py,sha256=sswTo6FZb1F5juHa0FYG3cgvrh3wfgGfJu2bBy5tCm4,1460
|
21
|
+
mcp_proxy_adapter/examples/testing_example.py,sha256=AB13c4C1bjs1145O-yriwyreeVXtMOlQLzs2BCGmprk,1719
|
22
|
+
mcp_proxy_adapter/validators/docstring_validator.py,sha256=Onpq2iNJ1qF4ejkJJIlBkLROuSNIVALHVmXIgkCpaFI,2934
|
23
|
+
mcp_proxy_adapter/validators/metadata_validator.py,sha256=uCrn38-VYYn89l6f5CC_GoTAHAweaOW2Z6Esro1rtGw,3155
|
24
|
+
mcp_proxy_adapter-2.1.4.dist-info/licenses/LICENSE,sha256=OkApFEwdgMCt_mbvUI-eIwKMSTe38K3XnU2DT5ub-wI,1072
|
25
|
+
mcp_proxy_adapter-2.1.4.dist-info/METADATA,sha256=u8AFozMDHraUrudOrA6D62VLkHL3s9LC36O2U29yR9g,12578
|
26
|
+
mcp_proxy_adapter-2.1.4.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
27
|
+
mcp_proxy_adapter-2.1.4.dist-info/top_level.txt,sha256=JZT7vPLBYrtroX-ij68JBhJYbjDdghcV-DFySRy-Nnw,18
|
28
|
+
mcp_proxy_adapter-2.1.4.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
mcp_proxy_adapter/__init__.py,sha256=_6D-TfANWp9zc550M5LUeGPvioFqG1bAl3tZj-gNmJU,463
|
2
|
-
mcp_proxy_adapter/adapter.py,sha256=76dkVeDuqLsJ5AhuftzLlwy2M6yr_PfNbmNfo9dXVhc,28844
|
3
|
-
mcp_proxy_adapter/models.py,sha256=acqVQBYAojHXeJ1MJyvpMyT6-J6aMxWuZMszn_-RsOU,2338
|
4
|
-
mcp_proxy_adapter/registry.py,sha256=jgC4TKaPbMbAsoxvGp2ToaOE4drD-VfZug7WJbm4IW4,15853
|
5
|
-
mcp_proxy_adapter/schema.py,sha256=HZM0TTQTSi8ha1TEeVevdCyGZOUPoT1soB7Nex0hV50,10947
|
6
|
-
mcp_proxy_adapter/analyzers/__init__.py,sha256=2rcYZDP-bXq078MQpxP32lAwYYyRhOwAQGBcefBfBzY,368
|
7
|
-
mcp_proxy_adapter/analyzers/docstring_analyzer.py,sha256=T3FLJEo_uChShfiEKRl8GpVoHvh5HiudZkxnj4KixfA,7541
|
8
|
-
mcp_proxy_adapter/analyzers/type_analyzer.py,sha256=6Wac7osKwF03waFSwQ8ZM0Wqn_zAP2D-I4WMEpR0hQM,5230
|
9
|
-
mcp_proxy_adapter/dispatchers/__init__.py,sha256=FWgimgInGphIjCEnvA3-ZExiapUzYAVis2H9C5IWivU,365
|
10
|
-
mcp_proxy_adapter/dispatchers/base_dispatcher.py,sha256=S5_Xri058jAmOWeit1tedB_GMZQ9RLcNcYabA83ZF6k,2288
|
11
|
-
mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py,sha256=ffu1M32E1AdC7IB44mlbV2L56eJQMsp-7fYi_r4rmHc,6331
|
12
|
-
mcp_proxy_adapter/validators/docstring_validator.py,sha256=Onpq2iNJ1qF4ejkJJIlBkLROuSNIVALHVmXIgkCpaFI,2934
|
13
|
-
mcp_proxy_adapter/validators/metadata_validator.py,sha256=uCrn38-VYYn89l6f5CC_GoTAHAweaOW2Z6Esro1rtGw,3155
|
14
|
-
mcp_proxy_adapter-2.1.3.dist-info/licenses/LICENSE,sha256=OkApFEwdgMCt_mbvUI-eIwKMSTe38K3XnU2DT5ub-wI,1072
|
15
|
-
mcp_proxy_adapter-2.1.3.dist-info/METADATA,sha256=Y--HJ-YuTBaOOn0IUEIYPDvncaYfpjJwiZztUUkWwc8,8150
|
16
|
-
mcp_proxy_adapter-2.1.3.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
17
|
-
mcp_proxy_adapter-2.1.3.dist-info/top_level.txt,sha256=JZT7vPLBYrtroX-ij68JBhJYbjDdghcV-DFySRy-Nnw,18
|
18
|
-
mcp_proxy_adapter-2.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|