Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:42:37 +08:00
commit 097db974e4
10 changed files with 2407 additions and 0 deletions

View File

@@ -0,0 +1,387 @@
#!/usr/bin/env python3
"""
MCP Wrapper Generator
Creates type-safe Python wrappers for MCP tools based on their JSON Schema definitions.
Usage:
python generate_mcp_wrappers.py <introspection.json> <output_dir>
"""
import json
import sys
from pathlib import Path
from typing import Any
def json_schema_to_python_type(schema: dict) -> str:
"""Convert JSON Schema type to Python type hint"""
if not schema or 'type' not in schema:
return 'Any'
type_map = {
'string': 'str',
'integer': 'int',
'number': 'float',
'boolean': 'bool',
'array': 'list',
'object': 'dict',
}
schema_type = schema.get('type')
if schema_type in type_map:
python_type = type_map[schema_type]
# Handle arrays with item types
if schema_type == 'array' and 'items' in schema:
item_type = json_schema_to_python_type(schema['items'])
return f'list[{item_type}]'
return python_type
return 'Any'
def sanitize_name(name: str) -> str:
"""Sanitize tool name for Python function name"""
# Replace hyphens and dots with underscores
return name.replace('-', '_').replace('.', '_')
def generate_wrapper_function(tool: dict, server_name: str) -> str:
"""Generate Python wrapper function for a single MCP tool"""
name = sanitize_name(tool['name'])
original_name = tool['name']
description = tool.get('description', 'No description available')
input_schema = tool.get('input_schema', {})
# Parse parameters
properties = input_schema.get('properties', {})
required = input_schema.get('required', [])
# Build parameter list
params = []
param_docs = []
for param_name, param_schema in properties.items():
python_type = json_schema_to_python_type(param_schema)
param_desc = param_schema.get('description', 'No description')
if param_name in required:
params.append(f"{param_name}: {python_type}")
else:
params.append(f"{param_name}: {python_type} | None = None")
param_docs.append(f" {param_name}: {param_desc}")
params_str = ', '.join(params)
param_docs_str = '\n'.join(param_docs) if param_docs else ' No parameters'
# Generate function
function_code = f'''async def {name}({params_str}) -> dict:
"""
{description}
Args:
{param_docs_str}
Returns:
dict: Result from MCP tool call
"""
from .mcp_client import call_mcp_tool
params = {{}}
'''
# Add parameter packing code
for param_name in properties.keys():
function_code += f''' if {param_name} is not None:
params['{param_name}'] = {param_name}
'''
function_code += f'''
return await call_mcp_tool('{server_name}', '{original_name}', params)
'''
return function_code
def generate_mcp_client(output_dir: Path):
"""Generate the base MCP client module with working implementation"""
client_code = '''"""
Base MCP Client
Provides foundational infrastructure for calling MCP tools.
Progressive disclosure: Load tool definitions on-demand using list_mcp_tools.py
"""
import asyncio
import json
import sys
from pathlib import Path
from typing import Any
# Check for MCP SDK and provide helpful error message
try:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
except ImportError:
print("\\n" + "="*70)
print("❌ ERROR: MCP SDK not installed")
print("="*70)
print("\\nThis skill requires the MCP SDK to connect to MCP servers.")
print("\\n📦 Install it with:")
print("\\n pip3 install mcp --break-system-packages")
print("\\n✓ Verify installation:")
print("\\n python3 -c \\"import mcp; print('MCP SDK ready!')\\"")
print("\\n💡 See SKILL.md Prerequisites section for more details.")
print("\\n" + "="*70 + "\\n")
sys.exit(1)
def load_mcp_config():
"""Load MCP server configuration from config file"""
# Look for config in skill directory
skill_dir = Path(__file__).parent.parent
config_path = skill_dir / 'mcp_config.json'
if config_path.exists():
with open(config_path) as f:
config = json.load(f)
return {s['name']: s for s in config.get('servers', [])}
return {}
MCP_SERVER_CONFIG = load_mcp_config()
async def call_mcp_tool(server_name: str, tool_name: str, params: dict) -> Any:
"""
Call an MCP tool
Args:
server_name: Name of the MCP server
tool_name: Name of the tool to call
params: Parameters to pass to the tool
Returns:
Result from the MCP tool
"""
if server_name not in MCP_SERVER_CONFIG:
raise ValueError(
f"MCP server '{server_name}' not configured. "
f"Please add it to mcp_config.json"
)
server_config = MCP_SERVER_CONFIG[server_name]
server_command = server_config['command']
server_params = StdioServerParameters(
command=server_command[0],
args=server_command[1:] if len(server_command) > 1 else [],
)
# Use context manager properly - create new connection for each call
# This is simpler and avoids connection management issues
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(tool_name, params)
# Extract content from result
if hasattr(result, 'content') and result.content:
# Return the first content item's text
return result.content[0].text if result.content[0].text else result.content[0]
return result
'''
client_path = output_dir / 'scripts' / 'mcp_client.py'
client_path.parent.mkdir(parents=True, exist_ok=True)
client_path.write_text(client_code)
print(f" Generated: {client_path}")
def generate_server_wrappers(server_name: str, tools: list[dict], output_dir: Path):
"""Generate wrapper files for all tools in a server"""
server_dir = output_dir / 'scripts' / 'tools' / server_name
server_dir.mkdir(parents=True, exist_ok=True)
print(f"\nGenerating wrappers for '{server_name}' ({len(tools)} tools)...")
# Generate __init__.py
init_imports = []
for tool in tools:
func_name = sanitize_name(tool['name'])
init_imports.append(f"from .{func_name} import {func_name}")
init_code = '\n'.join(init_imports) + '\n\n__all__ = [' + ', '.join(f"'{sanitize_name(t['name'])}'" for t in tools) + ']\n'
(server_dir / '__init__.py').write_text(init_code)
# Generate individual tool files
for tool in tools:
func_name = sanitize_name(tool['name'])
tool_code = generate_wrapper_function(tool, server_name)
(server_dir / f"{func_name}.py").write_text(tool_code)
print(f" ✓ Generated wrappers in: {server_dir}")
def generate_list_tools_script(output_dir: Path):
"""Generate script to list all MCP tools dynamically (Progressive Disclosure)"""
script_code = '''"""
List all available MCP tools from configured servers
This implements Progressive Disclosure - tools are discovered on-demand
rather than pre-loading all definitions into context.
"""
import asyncio
import json
import sys
from pathlib import Path
# Check for MCP SDK and provide helpful error message
try:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
except ImportError:
print("\\n" + "="*70)
print("❌ ERROR: MCP SDK not installed")
print("="*70)
print("\\nThis skill requires the MCP SDK to list available tools.")
print("\\n📦 Install it with:")
print("\\n pip3 install mcp --break-system-packages")
print("\\n✓ Verify installation:")
print("\\n python3 -c \\"import mcp; print('MCP SDK ready!')\\"")
print("\\n💡 See SKILL.md Prerequisites section for more details.")
print("\\n" + "="*70 + "\\n")
sys.exit(1)
async def list_server_tools(server_name: str, server_command: list):
"""List all tools from an MCP server"""
server_params = StdioServerParameters(
command=server_command[0],
args=server_command[1:] if len(server_command) > 1 else [],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List all tools
tools_result = await session.list_tools()
return [
{
'name': tool.name,
'description': tool.description,
'input_schema': tool.inputSchema
}
for tool in tools_result.tools
]
async def main():
# Load config
skill_dir = Path(__file__).parent.parent
config_path = skill_dir / 'mcp_config.json'
with open(config_path) as f:
config = json.load(f)
print("="*70)
print("MCP TOOLS REFERENCE")
print("="*70)
for server in config['servers']:
server_name = server['name']
server_command = server['command']
print(f"\\n📦 Server: {server_name}")
print("-"*70)
try:
tools = await list_server_tools(server_name, server_command)
print(f"Total tools: {len(tools)}\\n")
for tool in tools:
print(f"🔧 {tool['name']}")
print(f" Description: {tool['description']}")
# Show parameters
schema = tool.get('input_schema', {})
properties = schema.get('properties', {})
required = schema.get('required', [])
if properties:
print(f" Parameters:")
for param_name, param_info in properties.items():
param_type = param_info.get('type', 'unknown')
param_desc = param_info.get('description', 'No description')
req = " (required)" if param_name in required else " (optional)"
print(f" - {param_name}: {param_type}{req}")
if param_desc != 'No description':
print(f" {param_desc}")
print()
except Exception as e:
print(f"✗ Error: {e}\\n")
if __name__ == '__main__':
asyncio.run(main())
'''
script_path = output_dir / 'scripts' / 'list_mcp_tools.py'
script_path.parent.mkdir(parents=True, exist_ok=True)
script_path.write_text(script_code)
print(f" Generated: {script_path}")
def main():
if len(sys.argv) < 3:
print("Usage: generate_mcp_wrappers.py <introspection.json> <output_dir>")
print("\nGenerates Python wrapper code for MCP tools based on introspection results.")
sys.exit(1)
introspection_file = sys.argv[1]
output_dir = Path(sys.argv[2])
# Load introspection data
print(f"Loading introspection data from {introspection_file}")
with open(introspection_file) as f:
introspection_data = json.load(f)
# Generate base MCP client
print("\nGenerating base MCP client...")
generate_mcp_client(output_dir)
# Generate tool listing script (Progressive Disclosure)
print("\nGenerating list_mcp_tools.py...")
generate_list_tools_script(output_dir)
# Generate wrappers for each server
for server_name, server_data in introspection_data.items():
if server_data.get('status') == 'success':
generate_server_wrappers(
server_name,
server_data['tools'],
output_dir
)
else:
print(f"\n✗ Skipping '{server_name}' (introspection failed)")
print("\n✓ Wrapper generation complete!")
print(f"\nGenerated files in: {output_dir / 'scripts'}")
print(" - mcp_client.py: Base client infrastructure (working implementation)")
print(" - list_mcp_tools.py: Dynamic tool discovery (Progressive Disclosure)")
print(" - tools/<server>/*.py: Type-safe tool wrappers (optional)")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
MCP Server Introspector
Connects to MCP servers and extracts tool definitions,
generating structured data for skill creation.
Usage:
python mcp_introspector.py config.json output.json
"""
import asyncio
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Optional
# Try to import MCP SDK, provide helpful error if not available
try:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
except ImportError:
print("\n" + "="*70)
print("❌ ERROR: MCP SDK not installed")
print("="*70)
print("\nThe MCP SDK is required to introspect MCP servers.")
print("\n📦 Install it with:")
print("\n pip3 install mcp --break-system-packages")
print("\n✓ Verify installation:")
print("\n python3 -c \"import mcp; print('MCP SDK ready!')\"")
print("\n" + "="*70 + "\n")
sys.exit(1)
def check_command_available(command: str) -> bool:
"""Check if a command is available in PATH"""
try:
subprocess.run([command, "--version"], capture_output=True, check=False)
return True
except FileNotFoundError:
return False
def extract_npm_package(server_command: list[str]) -> Optional[str]:
"""
Extract npm package name from MCP server command.
Examples:
["npx", "-y", "@modelcontextprotocol/server-filesystem", ...]
-> "@modelcontextprotocol/server-filesystem"
"""
if not server_command or server_command[0] not in ["npx", "npm"]:
return None
# Find the package name (first argument that starts with @ or is not a flag)
for arg in server_command[1:]:
if not arg.startswith("-"):
return arg
return None
def install_npm_package(package_name: str) -> bool:
"""
Install npm package globally.
Args:
package_name: npm package to install (e.g., "@modelcontextprotocol/server-filesystem")
Returns:
True if installation succeeded
"""
print(f"📦 Installing MCP server: {package_name}")
try:
# Try global install first
result = subprocess.run(
["npm", "install", "-g", package_name],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
print(f" ✓ Installed {package_name} globally")
return True
else:
# If global install fails, try local install
print(f" ⚠️ Global install failed, trying local install...")
result = subprocess.run(
["npm", "install", package_name],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
print(f" ✓ Installed {package_name} locally")
return True
else:
print(f" ✗ Failed to install {package_name}")
print(f" Error: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f" ✗ Installation timed out (>120s)")
return False
except Exception as e:
print(f" ✗ Installation error: {e}")
return False
def ensure_mcp_server_installed(server_name: str, server_command: list[str]) -> bool:
"""
Ensure MCP server is installed before introspection.
Returns:
True if server is ready (already installed or successfully installed)
"""
# Check if npm/npx is available
if not check_command_available("npm"):
print("⚠️ npm not found. Skipping automatic installation.")
print(" The server command will be tried as-is.")
return True # Continue anyway, might work
# Extract package name
package_name = extract_npm_package(server_command)
if not package_name:
# Not an npm-based server, skip
return True
print(f"Checking MCP server: {package_name}")
# Check if already installed globally
try:
result = subprocess.run(
["npm", "list", "-g", package_name],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
print(f" ✓ Already installed globally")
return True
except:
pass
# Check if installed locally
try:
result = subprocess.run(
["npm", "list", package_name],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
print(f" ✓ Already installed locally")
return True
except:
pass
# Not installed, attempt installation
print(f" ⚠️ Not installed, installing now...")
return install_npm_package(package_name)
async def introspect_mcp_server(server_name: str, server_command: list[str]) -> dict:
"""
Connect to an MCP server and list all available tools
Args:
server_name: Name of the MCP server
server_command: Command to start MCP server (e.g., ['npx', '-y', '@modelcontextprotocol/server-filesystem', '/tmp'])
Returns:
Dictionary with server capabilities and tool definitions
"""
print(f"Introspecting MCP server: {server_name}")
try:
server_params = StdioServerParameters(
command=server_command[0],
args=server_command[1:] if len(server_command) > 1 else [],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List all tools
tools_result = await session.list_tools()
tools = []
for tool in tools_result.tools:
tools.append({
'name': tool.name,
'description': tool.description,
'input_schema': tool.inputSchema
})
print(f" ✓ Found {len(tools)} tools in {server_name}")
return {
'server_name': server_name,
'server_command': server_command,
'tool_count': len(tools),
'tools': tools,
'status': 'success'
}
except Exception as e:
error_msg = str(e)
print(f" ✗ Error introspecting {server_name}: {error_msg}")
# Provide helpful hints for common errors
if "npm" in error_msg.lower() or "npx" in error_msg.lower():
print("\n💡 Troubleshooting npm/npx errors:")
print(" 1. Check network connection (can you access npmjs.org?)")
print(" 2. Try running the MCP server command manually:")
print(f" {' '.join(server_command)}")
print(" 3. Check npm configuration: cat ~/.npmrc")
print(" 4. If behind a proxy, configure npm proxy settings")
print(" 5. Try clearing npm cache: npm cache clean --force")
elif "connection" in error_msg.lower() or "timeout" in error_msg.lower():
print("\n💡 Connection issue - the MCP server may not be responding.")
print(" 1. Verify the server command is correct")
print(" 2. Check if the server requires authentication or additional setup")
print(" 3. Try running the command manually to see detailed error")
return {
'server_name': server_name,
'server_command': server_command,
'status': 'error',
'error': error_msg
}
async def introspect_all_servers(server_configs: list[dict]) -> dict:
"""
Introspect multiple MCP servers
Args:
server_configs: List of {name, command} dicts
Returns:
Dictionary mapping server names to their capabilities
"""
results = {}
for config in server_configs:
name = config['name']
command = config['command']
# Ensure MCP server is installed before introspection
if not ensure_mcp_server_installed(name, command):
print(f" ⚠️ Skipping {name} - installation failed")
results[name] = {
'server_name': name,
'server_command': command,
'status': 'error',
'error': 'Failed to install MCP server'
}
continue
result = await introspect_mcp_server(name, command)
results[name] = result
return results
def load_config(config_path: str) -> list[dict]:
"""
Load MCP server configuration from JSON file
Expected format:
{
"servers": [
{
"name": "filesystem",
"command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
...
]
}
"""
with open(config_path) as f:
config = json.load(f)
return config.get('servers', [])
def main():
if len(sys.argv) < 3:
print("Usage: mcp_introspector.py <config.json> <output.json>")
print("\nConfig format:")
print(json.dumps({
"servers": [
{
"name": "example-server",
"command": ["npx", "-y", "@modelcontextprotocol/server-example"]
}
]
}, indent=2))
sys.exit(1)
config_path = sys.argv[1]
output_path = sys.argv[2]
# Load configuration
print(f"Loading MCP server configuration from {config_path}")
server_configs = load_config(config_path)
print(f"Found {len(server_configs)} servers to introspect\n")
# Introspect all servers
results = asyncio.run(introspect_all_servers(server_configs))
# Save results
with open(output_path, 'w') as f:
json.dump(results, f, indent=2)
print(f"\n✓ Introspection results saved to {output_path}")
# Summary
success_count = sum(1 for r in results.values() if r.get('status') == 'success')
total_tools = sum(r.get('tool_count', 0) for r in results.values() if r.get('status') == 'success')
print(f"\nSummary:")
print(f" Servers successfully introspected: {success_count}/{len(server_configs)}")
print(f" Total tools discovered: {total_tools}")
if __name__ == '__main__':
main()