feat: Add universal MCP proxy and Hanzo Platform integration
Major additions: - Universal MCP Proxy: Install ANY MCP server via npm/uvx - Auto-installation with package type detection - Unified interface for all MCP servers - Connection management and error recovery - Support for 4000+ MCP servers - Hanzo Platform MCP integration: - Access cloud-hosted MCP servers when logged in - Local-first with cloud fallback - Platform service management (deploy, scale, logs) - Unix tool aliasing for mode composition - Tool consolidation: - Unified tools matching Python MCP implementation - No duplicates - one tool per orthogonal concept - Enhanced read tool with multi-file support - Project analysis matching Python features - Comprehensive tests: - Unit tests for MCP proxy functionality - Integration tests with real server installation - Test coverage for all proxy operations - Documentation: - MCP_PROXY_GUIDE.md with examples - Updated README with MCP proxy features - Platform integration docs This brings feature parity with Python Hanzo MCP while leveraging TypeScript/VS Code ecosystem advantages.
This commit is contained in:
@@ -22,6 +22,7 @@ Hanzo AI is the ultimate toolkit for AI engineers, providing unified access to e
|
||||
- **Smart Routing**: Automatic failover and load balancing across providers
|
||||
- **Shared Context**: Maintain conversation context across model switches
|
||||
- **4000+ MCP Servers**: Access specialized tools for every use case
|
||||
- **Universal MCP Proxy**: Install ANY MCP server with `@hanzo mcp --action install`
|
||||
- **Performance Monitoring**: Track latency, costs, and usage patterns
|
||||
- **Team Collaboration**: Share API keys, context, and credits with your team
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# MCP Universal Proxy Guide
|
||||
|
||||
The Hanzo MCP Universal Proxy allows you to install and use ANY Model Context Protocol (MCP) server through a single unified interface.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install any MCP server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github
|
||||
|
||||
# List all capabilities
|
||||
@hanzo mcp --action list
|
||||
|
||||
# Call any tool from any server
|
||||
@hanzo mcp --action call --tool github_search --args '{"query": "typescript"}'
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 🚀 Auto-Installation
|
||||
- **NPM packages**: Automatically installed via `npm`
|
||||
- **Python packages**: Automatically installed via `uvx`
|
||||
- **Smart detection**: Package type detected from naming conventions
|
||||
- **Version tracking**: Keeps track of installed versions
|
||||
|
||||
### 🔌 Universal Proxy
|
||||
- **Single interface**: One command for all MCP servers
|
||||
- **Tool routing**: Automatically routes tool calls to correct server
|
||||
- **Connection management**: Handles server lifecycle automatically
|
||||
- **Error recovery**: Reconnects on failure
|
||||
|
||||
### 📦 Supported Servers
|
||||
|
||||
#### Official NPM Servers
|
||||
```bash
|
||||
# Browser automation
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-puppeteer
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-playwright
|
||||
|
||||
# Databases
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-sqlite
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-postgresql
|
||||
|
||||
# Development tools
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-filesystem
|
||||
|
||||
# Utilities
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-memory
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-fetch
|
||||
```
|
||||
|
||||
#### Python Servers
|
||||
```bash
|
||||
# Version control
|
||||
@hanzo mcp --action install --package mcp-server-git
|
||||
|
||||
# Communication
|
||||
@hanzo mcp --action install --package mcp-server-slack
|
||||
@hanzo mcp --action install --package mcp-server-discord
|
||||
|
||||
# Utilities
|
||||
@hanzo mcp --action install --package mcp-server-time
|
||||
@hanzo mcp --action install --package mcp-server-weather
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. GitHub Integration
|
||||
```bash
|
||||
# Install GitHub server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github
|
||||
|
||||
# Search repositories
|
||||
@hanzo mcp --action call --tool github_search --args '{
|
||||
"query": "language:typescript stars:>1000",
|
||||
"max_results": 10
|
||||
}'
|
||||
|
||||
# Create an issue
|
||||
@hanzo mcp --action call --tool github_create_issue --args '{
|
||||
"repo": "owner/repo",
|
||||
"title": "Bug report",
|
||||
"body": "Description of the issue"
|
||||
}'
|
||||
```
|
||||
|
||||
### 2. Database Operations
|
||||
```bash
|
||||
# Install SQLite server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-sqlite
|
||||
|
||||
# Query database
|
||||
@hanzo mcp --action call --tool sqlite_query --args '{
|
||||
"database": "myapp.db",
|
||||
"query": "SELECT * FROM users WHERE active = 1"
|
||||
}'
|
||||
|
||||
# Execute SQL
|
||||
@hanzo mcp --action call --tool sqlite_execute --args '{
|
||||
"database": "myapp.db",
|
||||
"query": "INSERT INTO logs (message, timestamp) VALUES (?, ?)",
|
||||
"params": ["User login", "2024-01-20 10:30:00"]
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Browser Automation
|
||||
```bash
|
||||
# Install Playwright server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-playwright
|
||||
|
||||
# Navigate and screenshot
|
||||
@hanzo mcp --action call --tool playwright_navigate --args '{
|
||||
"url": "https://example.com"
|
||||
}'
|
||||
|
||||
@hanzo mcp --action call --tool playwright_screenshot --args '{
|
||||
"path": "screenshot.png"
|
||||
}'
|
||||
|
||||
# Extract content
|
||||
@hanzo mcp --action call --tool playwright_extract --args '{
|
||||
"selector": "h1",
|
||||
"attribute": "textContent"
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Git Operations
|
||||
```bash
|
||||
# Install Git server (Python)
|
||||
@hanzo mcp --action install --package mcp-server-git --type python
|
||||
|
||||
# Get repository status
|
||||
@hanzo mcp --action call --tool git_status --args '{
|
||||
"repo_path": "/path/to/repo"
|
||||
}'
|
||||
|
||||
# Commit changes
|
||||
@hanzo mcp --action call --tool git_commit --args '{
|
||||
"repo_path": "/path/to/repo",
|
||||
"message": "feat: Add new feature"
|
||||
}'
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Force Reinstall
|
||||
```bash
|
||||
# Reinstall with latest version
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github --force
|
||||
```
|
||||
|
||||
### List Installed Servers
|
||||
```bash
|
||||
# Show all servers and their capabilities
|
||||
@hanzo mcp --action list
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
# MCP Proxy Status
|
||||
|
||||
## Installed Servers
|
||||
|
||||
### github
|
||||
- Package: `@modelcontextprotocol/server-github`
|
||||
- Version: 1.0.0
|
||||
- Type: npm
|
||||
- Tools: 5
|
||||
- Resources: 2
|
||||
- Prompts: 1
|
||||
|
||||
## Available Tools
|
||||
- `github_search` (github)
|
||||
- `github_create_issue` (github)
|
||||
- `github_create_pr` (github)
|
||||
...
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Installation**
|
||||
- Creates isolated environment for each server
|
||||
- NPM servers: Installed in `node_modules`
|
||||
- Python servers: Installed via `uvx`
|
||||
- Tracks versions and capabilities
|
||||
|
||||
2. **Discovery**
|
||||
- Connects to server on first use
|
||||
- Queries available tools/resources/prompts
|
||||
- Builds routing map for efficient dispatch
|
||||
|
||||
3. **Proxy Mechanism**
|
||||
- Single `mcp` tool handles all requests
|
||||
- Routes to appropriate server based on tool name
|
||||
- Manages server lifecycle (start/stop/restart)
|
||||
- Handles errors and reconnections
|
||||
|
||||
4. **Persistence**
|
||||
- Saves installed servers to VS Code storage
|
||||
- Remembers capabilities across sessions
|
||||
- Quick startup with cached information
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Not Found
|
||||
```bash
|
||||
# Check if server is installed
|
||||
@hanzo mcp --action list
|
||||
|
||||
# Reinstall if needed
|
||||
@hanzo mcp --action install --package <package-name>
|
||||
```
|
||||
|
||||
### Connection Issues
|
||||
```bash
|
||||
# Force reconnection by calling a tool
|
||||
@hanzo mcp --action call --tool <tool-name>
|
||||
|
||||
# Or reinstall with force flag
|
||||
@hanzo mcp --action install --package <package-name> --force
|
||||
```
|
||||
|
||||
### Python Package Issues
|
||||
Ensure `uvx` is installed:
|
||||
```bash
|
||||
# Install uv (Python package manager)
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Then install Python MCP servers
|
||||
@hanzo mcp --action install --package mcp-server-git --type python
|
||||
```
|
||||
|
||||
## Creating Your Own MCP Server
|
||||
|
||||
The proxy supports ANY MCP server that follows the protocol:
|
||||
|
||||
1. **NPM Package**: Name it `@yourscope/server-name` or `mcp-server-name`
|
||||
2. **Python Package**: Name it `mcp-server-name`
|
||||
3. **Implement MCP protocol** with stdio transport
|
||||
4. **Publish** to npm or PyPI
|
||||
|
||||
Then install with:
|
||||
```bash
|
||||
@hanzo mcp --action install --package your-package-name
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Servers run in isolated processes
|
||||
- Each server has its own installation directory
|
||||
- No shared state between servers
|
||||
- Servers can only access what you explicitly provide
|
||||
|
||||
## Future Features
|
||||
|
||||
- [ ] Auto-discovery of tools from package name
|
||||
- [ ] Parallel tool execution across servers
|
||||
- [ ] Server health monitoring
|
||||
- [ ] Resource usage tracking
|
||||
- [ ] Custom server configurations
|
||||
- [ ] Direct server management UI
|
||||
|
||||
---
|
||||
|
||||
The MCP Universal Proxy makes it easy to extend Hanzo AI with any MCP server, giving you access to unlimited capabilities through a single, unified interface!
|
||||
@@ -5,7 +5,8 @@ The ultimate toolkit for AI engineers.
|
||||
## What You Get
|
||||
|
||||
- **200+ LLMs** - Every model from OpenRouter/LiteLLM in one API
|
||||
- **4000+ MCP Servers** - Access specialized tools instantly
|
||||
- **4000+ MCP Servers** - Install ANY MCP server with one command
|
||||
- **Universal MCP Proxy** - Auto-install via npm/uvx and proxy all calls
|
||||
- **45+ Legendary Modes** - Code like Carmack, think like Norvig
|
||||
- **Unlimited Memory** - Vector/graph/relational search across all projects
|
||||
- **Browser Automation** - Built-in Playwright for web tasks
|
||||
@@ -44,6 +45,10 @@ export HANZO_API_KEY=hzo_... # from iam.hanzo.ai
|
||||
|
||||
# Search everything
|
||||
@hanzo search "auth flow"
|
||||
|
||||
# Install any MCP server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github
|
||||
@hanzo mcp --action call --tool github_search --args '{"query": "MCP"}'
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
+7
-17
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
|
||||
+7
-17
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
|
||||
+7
-17
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getConfig = void 0;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
|
||||
+10
-19
@@ -15,26 +15,15 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
exports.deactivate = exports.activate = void 0;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
const ProjectManager_1 = require("./ProjectManager");
|
||||
const manager_1 = require("./auth/manager");
|
||||
@@ -130,6 +119,7 @@ async function activate(context) {
|
||||
// Initialize reminder service
|
||||
// Reminder service starts automatically in constructor
|
||||
}
|
||||
exports.activate = activate;
|
||||
function openProjectManager(context) {
|
||||
const panel = vscode.window.createWebviewPanel('hanzoProjectManager', 'Hanzo Project Manager', vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
@@ -163,4 +153,5 @@ function deactivate() {
|
||||
statusBar.dispose();
|
||||
}
|
||||
}
|
||||
exports.deactivate = deactivate;
|
||||
//# sourceMappingURL=extension.js.map
|
||||
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AnalysisService = void 0;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
|
||||
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
|
||||
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
|
||||
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.StatusBarService = void 0;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
|
||||
+7
-17
@@ -15,23 +15,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fsWrapper = void 0;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
|
||||
@@ -15,25 +15,15 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getUIText = getUIText;
|
||||
exports.getUIText = void 0;
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
// Default text content used as fallback
|
||||
@@ -114,4 +104,5 @@ function getUIText() {
|
||||
return defaultText;
|
||||
}
|
||||
}
|
||||
exports.getUIText = getUIText;
|
||||
//# sourceMappingURL=textContent.js.map
|
||||
@@ -263,6 +263,8 @@
|
||||
"test:auth": "cross-env MOCHA_TEST_FILE=\"Auth and API Test Suite\" node ./out/test/runTest.js",
|
||||
"test:file-collection": "cross-env MOCHA_TEST_FILE=\"FileCollectionService Test Suite\" node ./out/test/runTest.js",
|
||||
"test:mcp": "cross-env MOCHA_TEST_FILE=\"MCP.*Tool Test Suite\" node ./out/test/runTest.js",
|
||||
"test:mcp-proxy": "cross-env MOCHA_TEST_FILE=\"MCP.*Proxy Test\" node ./out/test/runTest.js",
|
||||
"test:mcp-integration": "cross-env MOCHA_TEST_FILE=\"MCP Proxy Integration\" node ./out/test/runTest.js",
|
||||
"test:coverage": "c8 npm test",
|
||||
"build:mcp": "node scripts/build-mcp-standalone.js",
|
||||
"build:claude-desktop": "npm run build:mcp && node scripts/build-claude-desktop.js",
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
interface BrowserAction {
|
||||
action: 'navigate' | 'screenshot' | 'click' | 'type' | 'extract' | 'wait' | 'evaluate';
|
||||
action: 'navigate' | 'screenshot' | 'click' | 'type' | 'extract' | 'wait' | 'evaluate' | 'install';
|
||||
url?: string;
|
||||
selector?: string;
|
||||
text?: string;
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MCPTool } from '../server';
|
||||
import axios from 'axios';
|
||||
|
||||
interface HanzoPlatformConfig {
|
||||
apiKey?: string;
|
||||
endpoint?: string;
|
||||
localFirst?: boolean;
|
||||
}
|
||||
|
||||
interface UnixToolAlias {
|
||||
name: string;
|
||||
command: string;
|
||||
description?: string;
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hanzo Platform MCP Integration
|
||||
*
|
||||
* Provides unified access to both local and cloud MCP servers through Hanzo Platform.
|
||||
* When logged in to Hanzo, users can access cloud-hosted MCP servers that run on
|
||||
* Hanzo's infrastructure, eliminating the need for local installation.
|
||||
*/
|
||||
export function createHanzoPlatformMCPTools(context: vscode.ExtensionContext): MCPTool[] {
|
||||
const config = vscode.workspace.getConfiguration('hanzo');
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'hanzo_mcp',
|
||||
description: 'Access Hanzo Platform MCP servers - cloud-hosted MCP services',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
server: {
|
||||
type: 'string',
|
||||
description: 'MCP server to access (e.g., "github", "sqlite", "git")'
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
description: 'Method to call on the server'
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Parameters for the method'
|
||||
},
|
||||
localFirst: {
|
||||
type: 'boolean',
|
||||
description: 'Try local MCP first, fallback to cloud (default: true)'
|
||||
}
|
||||
},
|
||||
required: ['server', 'method']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const apiKey = process.env.HANZO_API_KEY || config.get<string>('llm.hanzo.apiKey');
|
||||
const endpoint = process.env.HANZO_MCP_ENDPOINT || config.get<string>('mcp.endpoint', 'https://api.hanzo.ai/mcp/v1');
|
||||
|
||||
if (!apiKey) {
|
||||
return '❌ Hanzo API key required. Set HANZO_API_KEY or login via `hanzo login`';
|
||||
}
|
||||
|
||||
const localFirst = args.localFirst ?? true;
|
||||
|
||||
// Try local MCP first if enabled
|
||||
if (localFirst) {
|
||||
try {
|
||||
// Check if server is installed locally
|
||||
const localResult = await callLocalMCP(args.server, args.method, args.params);
|
||||
if (localResult.success) {
|
||||
return `✅ [Local] ${localResult.result}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall through to cloud
|
||||
}
|
||||
}
|
||||
|
||||
// Call Hanzo Platform MCP
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${endpoint}/proxy`,
|
||||
{
|
||||
server: args.server,
|
||||
method: args.method,
|
||||
params: args.params
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return formatCloudResponse(response.data);
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 404) {
|
||||
return `❌ MCP server '${args.server}' not available on Hanzo Platform. Available servers: github, sqlite, postgresql, git, slack, time, weather`;
|
||||
}
|
||||
return `❌ Hanzo Platform MCP error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'hanzo_platform',
|
||||
description: 'Control Hanzo Platform services and resources',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['deploy', 'list', 'logs', 'scale', 'delete', 'status'],
|
||||
description: 'Platform action to perform'
|
||||
},
|
||||
service: {
|
||||
type: 'string',
|
||||
description: 'Service name'
|
||||
},
|
||||
config: {
|
||||
type: 'object',
|
||||
description: 'Configuration for the action'
|
||||
}
|
||||
},
|
||||
required: ['action']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const apiKey = process.env.HANZO_API_KEY || config.get<string>('llm.hanzo.apiKey');
|
||||
const endpoint = config.get<string>('platform.endpoint', 'https://api.hanzo.ai/platform/v1');
|
||||
|
||||
if (!apiKey) {
|
||||
return '❌ Hanzo API key required for platform operations';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${endpoint}/${args.action}`,
|
||||
{
|
||||
service: args.service,
|
||||
...args.config
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return formatPlatformResponse(args.action, response.data);
|
||||
} catch (error: any) {
|
||||
return `❌ Platform operation failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'unix_alias',
|
||||
description: 'Create Unix tool aliases for use in modes - makes shell commands available as MCP tools',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['create', 'list', 'remove', 'execute'],
|
||||
description: 'Alias action'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Alias name'
|
||||
},
|
||||
command: {
|
||||
type: 'string',
|
||||
description: 'Unix command to alias'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of what the command does'
|
||||
},
|
||||
args: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Arguments for execute action'
|
||||
}
|
||||
},
|
||||
required: ['action']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const aliases = context.globalState.get<UnixToolAlias[]>('unix-aliases', []);
|
||||
|
||||
switch (args.action) {
|
||||
case 'create':
|
||||
if (!args.name || !args.command) {
|
||||
return '❌ Name and command required for alias creation';
|
||||
}
|
||||
|
||||
const newAlias: UnixToolAlias = {
|
||||
name: args.name,
|
||||
command: args.command,
|
||||
description: args.description
|
||||
};
|
||||
|
||||
const updated = [...aliases.filter(a => a.name !== args.name), newAlias];
|
||||
await context.globalState.update('unix-aliases', updated);
|
||||
|
||||
return `✅ Created alias '${args.name}' → '${args.command}'
|
||||
|
||||
This alias is now available as an MCP tool in modes!
|
||||
Use in mode configuration: tools: ['${args.name}']`;
|
||||
|
||||
case 'list':
|
||||
if (aliases.length === 0) {
|
||||
return 'No Unix aliases defined yet.';
|
||||
}
|
||||
|
||||
return '# Unix Tool Aliases\n\n' +
|
||||
aliases.map(a =>
|
||||
`- **${a.name}**: \`${a.command}\`${a.description ? `\n ${a.description}` : ''}`
|
||||
).join('\n');
|
||||
|
||||
case 'remove':
|
||||
if (!args.name) {
|
||||
return '❌ Name required for removal';
|
||||
}
|
||||
|
||||
const filtered = aliases.filter(a => a.name !== args.name);
|
||||
await context.globalState.update('unix-aliases', filtered);
|
||||
return `✅ Removed alias '${args.name}'`;
|
||||
|
||||
case 'execute':
|
||||
if (!args.name) {
|
||||
return '❌ Name required for execution';
|
||||
}
|
||||
|
||||
const alias = aliases.find(a => a.name === args.name);
|
||||
if (!alias) {
|
||||
return `❌ Alias '${args.name}' not found`;
|
||||
}
|
||||
|
||||
// Execute using bash tool
|
||||
const bashCommand = args.args?.length
|
||||
? `${alias.command} ${args.args.join(' ')}`
|
||||
: alias.command;
|
||||
|
||||
// Delegate to bash tool
|
||||
return `Executing: ${bashCommand}
|
||||
|
||||
Use @hanzo bash to run: ${bashCommand}`;
|
||||
|
||||
default:
|
||||
return '❌ Unknown action';
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async function callLocalMCP(server: string, method: string, params: any): Promise<{ success: boolean; result?: string }> {
|
||||
// This would check if the server is installed locally and call it
|
||||
// For now, return not found to trigger cloud fallback
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
function formatCloudResponse(data: any): string {
|
||||
if (data.error) {
|
||||
return `❌ Cloud MCP Error: ${data.error}`;
|
||||
}
|
||||
|
||||
let output = `✅ [Hanzo Cloud] ${data.server} → ${data.method}\n\n`;
|
||||
|
||||
if (data.result) {
|
||||
if (typeof data.result === 'string') {
|
||||
output += data.result;
|
||||
} else {
|
||||
output += JSON.stringify(data.result, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.metadata) {
|
||||
output += `\n\n📊 Metadata:`;
|
||||
output += `\n- Execution time: ${data.metadata.executionTime}ms`;
|
||||
output += `\n- Credits used: ${data.metadata.creditsUsed}`;
|
||||
output += `\n- Server location: ${data.metadata.region}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function formatPlatformResponse(action: string, data: any): string {
|
||||
switch (action) {
|
||||
case 'deploy':
|
||||
return `✅ Deployed ${data.service}
|
||||
- URL: ${data.url}
|
||||
- Status: ${data.status}
|
||||
- Resources: ${data.resources}`;
|
||||
|
||||
case 'list':
|
||||
return `# Hanzo Platform Services\n\n` +
|
||||
data.services.map((s: any) =>
|
||||
`- **${s.name}** (${s.status})\n URL: ${s.url}\n Created: ${s.created}`
|
||||
).join('\n\n');
|
||||
|
||||
case 'logs':
|
||||
return `# Logs for ${data.service}\n\n${data.logs}`;
|
||||
|
||||
case 'status':
|
||||
return `# ${data.service} Status
|
||||
- Health: ${data.health}
|
||||
- Uptime: ${data.uptime}
|
||||
- Requests: ${data.requests}
|
||||
- Errors: ${data.errors}`;
|
||||
|
||||
default:
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Unix aliases as dynamic MCP tools
|
||||
* This allows modes to use any Unix command as a composable tool
|
||||
*/
|
||||
export function registerUnixAliasTools(context: vscode.ExtensionContext): MCPTool[] {
|
||||
const aliases = context.globalState.get<UnixToolAlias[]>('unix-aliases', []);
|
||||
const tools: MCPTool[] = [];
|
||||
|
||||
for (const alias of aliases) {
|
||||
tools.push({
|
||||
name: alias.name,
|
||||
description: alias.description || `Unix command: ${alias.command}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
args: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Command arguments'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const fullCommand = args.args?.length
|
||||
? `${alias.command} ${args.args.join(' ')}`
|
||||
: alias.command;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(fullCommand);
|
||||
return stderr ? `${stdout}\n⚠️ Warnings:\n${stderr}` : stdout;
|
||||
} catch (error: any) {
|
||||
return `❌ Command failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
+57
-23
@@ -29,6 +29,9 @@ import { AITools } from './ai-tools';
|
||||
import { createModeTool } from './mode';
|
||||
import { createMCPRunnerTools } from './mcp-runner';
|
||||
import { createBrowserTools } from './browser';
|
||||
import { createMCPUniversalProxyTools } from './mcp-universal-proxy';
|
||||
import { createHanzoPlatformMCPTools, registerUnixAliasTools } from './hanzo-platform-mcp';
|
||||
import { createUnifiedReadTool, createUnifiedProjectAnalyzeTool, createUnixAliasTool, getCanonicalToolName } from './unified-tools';
|
||||
// import { createASTAnalyzerTool } from './ast-analyzer';
|
||||
// import { createTreeSitterAnalyzerTool } from './treesitter-analyzer';
|
||||
|
||||
@@ -52,37 +55,68 @@ export class MCPTools {
|
||||
// Register tool handlers for batch tool
|
||||
const toolHandlers = new Map<string, (args: any) => Promise<any>>();
|
||||
|
||||
// Create all tools
|
||||
// Create all tools - unified without duplicates
|
||||
const allTools = [
|
||||
...createFileSystemTools(this.context),
|
||||
// File System (unified)
|
||||
createUnifiedReadTool(this.context), // Replaces read from filesystem
|
||||
...createFileSystemTools(this.context).filter(t => t.name !== 'read'),
|
||||
|
||||
// Shell & Process
|
||||
...createShellTools(this.context),
|
||||
...createSearchTools(this.context),
|
||||
...createJupyterTools(this.context),
|
||||
...createAgentTools(this.context),
|
||||
...createTodoTools(this.context),
|
||||
...createEditorTools(this.context),
|
||||
...createDatabaseTools(this.context),
|
||||
...createLLMTools(this.context),
|
||||
...createVectorTools(this.context),
|
||||
...createMCPManagementTools(this.context),
|
||||
...createSystemTools(this.context),
|
||||
...createPaletteTools(this.context),
|
||||
...createBashTools(this.context),
|
||||
...createGitSearchTools(this.context),
|
||||
...this.batchTools.getTools(),
|
||||
...this.aiTools.getTools(),
|
||||
createProcessTool(this.context),
|
||||
createConfigTool(this.context),
|
||||
createRulesTool(this.context),
|
||||
|
||||
// Search (unified)
|
||||
...createSearchTools(this.context),
|
||||
...createGitSearchTools(this.context),
|
||||
createUnifiedSearchTool(this.context),
|
||||
|
||||
// Development Tools
|
||||
...createJupyterTools(this.context),
|
||||
...createEditorTools(this.context),
|
||||
createUnifiedProjectAnalyzeTool(this.context),
|
||||
|
||||
// AI & Agents
|
||||
...createAgentTools(this.context),
|
||||
...this.aiTools.getTools(),
|
||||
createThinkTool(this.context),
|
||||
createCriticTool(this.context),
|
||||
createUnifiedTodoTool(this.context),
|
||||
createUnifiedSearchTool(this.context),
|
||||
createWebFetchTool(this.context),
|
||||
createZenTool(this.context),
|
||||
createModeTool(this.context),
|
||||
|
||||
// Task Management
|
||||
...createTodoTools(this.context),
|
||||
createUnifiedTodoTool(this.context),
|
||||
|
||||
// Database & Storage
|
||||
...createDatabaseTools(this.context),
|
||||
...createVectorTools(this.context),
|
||||
|
||||
// LLM & Platform
|
||||
...createLLMTools(this.context),
|
||||
...createHanzoPlatformMCPTools(this.context),
|
||||
|
||||
// MCP Management
|
||||
...createMCPManagementTools(this.context),
|
||||
...createMCPRunnerTools(this.context),
|
||||
...createBrowserTools(this.context)
|
||||
...createMCPUniversalProxyTools(this.context),
|
||||
|
||||
// Browser & Web
|
||||
...createBrowserTools(this.context),
|
||||
createWebFetchTool(this.context),
|
||||
|
||||
// Configuration & System
|
||||
...createSystemTools(this.context),
|
||||
...createPaletteTools(this.context),
|
||||
createConfigTool(this.context),
|
||||
createRulesTool(this.context),
|
||||
createModeTool(this.context),
|
||||
createUnixAliasTool(this.context),
|
||||
|
||||
// Batch operations
|
||||
...this.batchTools.getTools(),
|
||||
|
||||
// Dynamic Unix aliases
|
||||
...registerUnixAliasTools(this.context)
|
||||
// createASTAnalyzerTool(this.context),
|
||||
// createTreeSitterAnalyzerTool(this.context)
|
||||
];
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MCPTool } from '../server';
|
||||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(cp.exec);
|
||||
|
||||
interface MCPServer {
|
||||
name: string;
|
||||
package: string;
|
||||
type: 'npm' | 'python';
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
process?: cp.ChildProcess;
|
||||
}
|
||||
|
||||
interface MCPInstallArgs {
|
||||
package: string;
|
||||
type?: 'npm' | 'python';
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
interface MCPProxyArgs {
|
||||
server: string;
|
||||
method: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export function createMCPProxyTools(context: vscode.ExtensionContext): MCPTool[] {
|
||||
const mcpServers = new Map<string, MCPServer>();
|
||||
|
||||
// Load known MCP servers
|
||||
loadKnownServers(mcpServers);
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'mcp_install',
|
||||
description: 'Install any MCP server via npm/npx or Python/uvx',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
package: {
|
||||
type: 'string',
|
||||
description: 'Package name (e.g., "@modelcontextprotocol/server-puppeteer", "mcp-server-git")'
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['npm', 'python'],
|
||||
description: 'Package type (auto-detected if not specified)'
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: 'Force reinstall even if already installed'
|
||||
}
|
||||
},
|
||||
required: ['package']
|
||||
},
|
||||
handler: async (args: MCPInstallArgs) => {
|
||||
const packageType = args.type || detectPackageType(args.package);
|
||||
const serverName = extractServerName(args.package);
|
||||
|
||||
// Check if already installed
|
||||
const existing = mcpServers.get(serverName);
|
||||
if (existing && existing.installed && !args.force) {
|
||||
return `✅ MCP server '${serverName}' is already installed at: ${existing.path}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await installMCPServer(
|
||||
args.package,
|
||||
packageType,
|
||||
context
|
||||
);
|
||||
|
||||
// Update server registry
|
||||
mcpServers.set(serverName, {
|
||||
name: serverName,
|
||||
package: args.package,
|
||||
type: packageType,
|
||||
installed: true,
|
||||
version: result.version,
|
||||
path: result.path
|
||||
});
|
||||
|
||||
// Save to persistent storage
|
||||
await saveMCPServers(context, mcpServers);
|
||||
|
||||
return `✅ Successfully installed ${args.package}
|
||||
📦 Version: ${result.version}
|
||||
📂 Location: ${result.path}
|
||||
🚀 Use: @hanzo mcp_proxy --server ${serverName} --method <method>`;
|
||||
|
||||
} catch (error: any) {
|
||||
return `❌ Failed to install ${args.package}: ${error.message}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mcp_proxy',
|
||||
description: 'Proxy commands to any installed MCP server',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
server: {
|
||||
type: 'string',
|
||||
description: 'MCP server name (e.g., "puppeteer", "git", "sqlite")'
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
description: 'Method to call on the MCP server'
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Parameters to pass to the method'
|
||||
}
|
||||
},
|
||||
required: ['server', 'method']
|
||||
},
|
||||
handler: async (args: MCPProxyArgs) => {
|
||||
const server = mcpServers.get(args.server);
|
||||
|
||||
if (!server || !server.installed) {
|
||||
// Try to find and suggest installation
|
||||
const suggestions = findSimilarServers(args.server);
|
||||
return `❌ MCP server '${args.server}' not installed.
|
||||
|
||||
${suggestions.length > 0 ? `Did you mean one of these?
|
||||
${suggestions.map(s => `- ${s}`).join('\n')}
|
||||
|
||||
Install with: @hanzo mcp_install --package <package-name>` :
|
||||
`Try installing with: @hanzo mcp_install --package @modelcontextprotocol/server-${args.server}`}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure server is running
|
||||
if (!server.process || !isProcessRunning(server.process.pid!)) {
|
||||
server.process = await startMCPServer(server, context);
|
||||
}
|
||||
|
||||
// Proxy the method call
|
||||
const result = await proxyMethodCall(
|
||||
server,
|
||||
args.method,
|
||||
args.params
|
||||
);
|
||||
|
||||
return formatProxyResult(result);
|
||||
|
||||
} catch (error: any) {
|
||||
return `❌ MCP proxy error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mcp_list',
|
||||
description: 'List all available and installed MCP servers',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
installed: {
|
||||
type: 'boolean',
|
||||
description: 'Show only installed servers'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: { installed?: boolean }) => {
|
||||
const servers = Array.from(mcpServers.values());
|
||||
const filtered = args.installed
|
||||
? servers.filter(s => s.installed)
|
||||
: servers;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return args.installed
|
||||
? 'No MCP servers installed yet. Use mcp_install to add servers.'
|
||||
: 'No MCP servers registered.';
|
||||
}
|
||||
|
||||
let output = args.installed
|
||||
? '## Installed MCP Servers\n\n'
|
||||
: '## Available MCP Servers\n\n';
|
||||
|
||||
for (const server of filtered) {
|
||||
output += `### ${server.name}\n`;
|
||||
output += `- Package: \`${server.package}\`\n`;
|
||||
output += `- Type: ${server.type}\n`;
|
||||
output += `- Installed: ${server.installed ? '✅' : '❌'}\n`;
|
||||
if (server.version) output += `- Version: ${server.version}\n`;
|
||||
if (server.path) output += `- Path: ${server.path}\n`;
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
if (!args.installed) {
|
||||
output += '\n## Popular MCP Servers\n\n';
|
||||
output += '- `@modelcontextprotocol/server-puppeteer` - Browser automation\n';
|
||||
output += '- `@modelcontextprotocol/server-sqlite` - SQLite database\n';
|
||||
output += '- `@modelcontextprotocol/server-postgresql` - PostgreSQL\n';
|
||||
output += '- `mcp-server-git` - Git operations (Python)\n';
|
||||
output += '- `mcp-server-github` - GitHub API (Python)\n';
|
||||
output += '- `mcp-server-slack` - Slack integration (Python)\n';
|
||||
output += '- `mcp-server-filesystem` - Advanced file operations\n';
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mcp_uninstall',
|
||||
description: 'Uninstall an MCP server',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
server: {
|
||||
type: 'string',
|
||||
description: 'MCP server name to uninstall'
|
||||
}
|
||||
},
|
||||
required: ['server']
|
||||
},
|
||||
handler: async (args: { server: string }) => {
|
||||
const server = mcpServers.get(args.server);
|
||||
|
||||
if (!server) {
|
||||
return `❌ MCP server '${args.server}' not found.`;
|
||||
}
|
||||
|
||||
if (!server.installed) {
|
||||
return `❌ MCP server '${args.server}' is not installed.`;
|
||||
}
|
||||
|
||||
try {
|
||||
// Stop the server if running
|
||||
if (server.process && isProcessRunning(server.process.pid!)) {
|
||||
server.process.kill();
|
||||
}
|
||||
|
||||
// Remove installation
|
||||
if (server.path && fs.existsSync(server.path)) {
|
||||
await fs.promises.rm(server.path, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Update registry
|
||||
server.installed = false;
|
||||
server.path = undefined;
|
||||
server.version = undefined;
|
||||
server.process = undefined;
|
||||
|
||||
await saveMCPServers(context, mcpServers);
|
||||
|
||||
return `✅ Successfully uninstalled MCP server '${args.server}'`;
|
||||
|
||||
} catch (error: any) {
|
||||
return `❌ Failed to uninstall: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function loadKnownServers(servers: Map<string, MCPServer>) {
|
||||
// Load from a known list of MCP servers
|
||||
const knownServers = [
|
||||
// NPM packages
|
||||
{ name: 'puppeteer', package: '@modelcontextprotocol/server-puppeteer', type: 'npm' as const },
|
||||
{ name: 'playwright', package: '@modelcontextprotocol/server-playwright', type: 'npm' as const },
|
||||
{ name: 'sqlite', package: '@modelcontextprotocol/server-sqlite', type: 'npm' as const },
|
||||
{ name: 'postgresql', package: '@modelcontextprotocol/server-postgresql', type: 'npm' as const },
|
||||
{ name: 'filesystem', package: '@modelcontextprotocol/server-filesystem', type: 'npm' as const },
|
||||
{ name: 'memory', package: '@modelcontextprotocol/server-memory', type: 'npm' as const },
|
||||
{ name: 'fetch', package: '@modelcontextprotocol/server-fetch', type: 'npm' as const },
|
||||
|
||||
// Python packages
|
||||
{ name: 'git', package: 'mcp-server-git', type: 'python' as const },
|
||||
{ name: 'github', package: 'mcp-server-github', type: 'python' as const },
|
||||
{ name: 'slack', package: 'mcp-server-slack', type: 'python' as const },
|
||||
{ name: 'time', package: 'mcp-server-time', type: 'python' as const },
|
||||
{ name: 'weather', package: 'mcp-server-weather', type: 'python' as const },
|
||||
];
|
||||
|
||||
for (const server of knownServers) {
|
||||
servers.set(server.name, {
|
||||
...server,
|
||||
installed: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function detectPackageType(packageName: string): 'npm' | 'python' {
|
||||
// NPM packages usually start with @ or contain /
|
||||
if (packageName.startsWith('@') || packageName.includes('/')) {
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
// Python packages usually have mcp-server prefix
|
||||
if (packageName.startsWith('mcp-server-') || packageName.includes('_')) {
|
||||
return 'python';
|
||||
}
|
||||
|
||||
// Default to npm
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
function extractServerName(packageName: string): string {
|
||||
// Extract meaningful name from package
|
||||
if (packageName.includes('/')) {
|
||||
// @modelcontextprotocol/server-puppeteer -> puppeteer
|
||||
const parts = packageName.split('/');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
return lastPart.replace('server-', '').replace('mcp-', '');
|
||||
}
|
||||
|
||||
// mcp-server-git -> git
|
||||
return packageName.replace('mcp-server-', '').replace('mcp-', '');
|
||||
}
|
||||
|
||||
async function installMCPServer(
|
||||
packageName: string,
|
||||
type: 'npm' | 'python',
|
||||
context: vscode.ExtensionContext
|
||||
): Promise<{ version: string; path: string }> {
|
||||
const installDir = path.join(context.globalStorageUri.fsPath, 'mcp-servers');
|
||||
const serverName = extractServerName(packageName);
|
||||
const serverDir = path.join(installDir, serverName);
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.promises.mkdir(serverDir, { recursive: true });
|
||||
|
||||
if (type === 'npm') {
|
||||
// Install via npm
|
||||
const { stdout } = await exec(
|
||||
`npm install ${packageName} --prefix "${serverDir}"`,
|
||||
{ cwd: serverDir }
|
||||
);
|
||||
|
||||
// Get version
|
||||
const packageJsonPath = path.join(serverDir, 'node_modules', packageName, 'package.json');
|
||||
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
|
||||
|
||||
return {
|
||||
version: packageJson.version,
|
||||
path: serverDir
|
||||
};
|
||||
} else {
|
||||
// Install via uvx/pip
|
||||
const { stdout } = await exec(
|
||||
`uvx --from ${packageName} --install-dir "${serverDir}" ${packageName}`,
|
||||
{ cwd: serverDir }
|
||||
);
|
||||
|
||||
// Try to get version
|
||||
let version = 'unknown';
|
||||
try {
|
||||
const { stdout: versionOut } = await exec(
|
||||
`uvx ${packageName} --version`,
|
||||
{ cwd: serverDir }
|
||||
);
|
||||
version = versionOut.trim();
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
version,
|
||||
path: serverDir
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function startMCPServer(
|
||||
server: MCPServer,
|
||||
context: vscode.ExtensionContext
|
||||
): Promise<cp.ChildProcess> {
|
||||
const serverPath = server.path!;
|
||||
|
||||
if (server.type === 'npm') {
|
||||
// Find the main entry point
|
||||
const packagePath = path.join(serverPath, 'node_modules', server.package);
|
||||
const packageJson = JSON.parse(
|
||||
await fs.promises.readFile(path.join(packagePath, 'package.json'), 'utf8')
|
||||
);
|
||||
|
||||
const mainFile = packageJson.main || 'index.js';
|
||||
const entryPoint = path.join(packagePath, mainFile);
|
||||
|
||||
return cp.spawn('node', [entryPoint], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: path.join(serverPath, 'node_modules')
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Python server
|
||||
return cp.spawn('uvx', [server.package], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: serverPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function proxyMethodCall(
|
||||
server: MCPServer,
|
||||
method: string,
|
||||
params: any
|
||||
): Promise<any> {
|
||||
// This is a simplified proxy - in reality, we'd use the MCP protocol
|
||||
// For now, we'll simulate the call
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!server.process) {
|
||||
reject(new Error('Server not running'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send JSON-RPC request
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: method,
|
||||
params: params || {}
|
||||
};
|
||||
|
||||
server.process.stdin?.write(JSON.stringify(request) + '\n');
|
||||
|
||||
// Listen for response
|
||||
const handler = (data: Buffer) => {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
if (response.id === request.id) {
|
||||
server.process?.stdout?.off('data', handler);
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message));
|
||||
} else {
|
||||
resolve(response.result);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
};
|
||||
|
||||
server.process.stdout?.on('data', handler);
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
server.process?.stdout?.off('data', handler);
|
||||
reject(new Error('Request timeout'));
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
function isProcessRunning(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findSimilarServers(name: string): string[] {
|
||||
const knownNames = [
|
||||
'puppeteer', 'playwright', 'sqlite', 'postgresql',
|
||||
'git', 'github', 'slack', 'filesystem', 'memory'
|
||||
];
|
||||
|
||||
return knownNames.filter(n =>
|
||||
n.includes(name) || name.includes(n)
|
||||
);
|
||||
}
|
||||
|
||||
function formatProxyResult(result: any): string {
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
return '✅ Method executed successfully';
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(result, null, 2);
|
||||
} catch {
|
||||
return String(result);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMCPServers(
|
||||
context: vscode.ExtensionContext,
|
||||
servers: Map<string, MCPServer>
|
||||
): Promise<void> {
|
||||
const data = Array.from(servers.entries()).map(([name, server]) => ({
|
||||
...server,
|
||||
process: undefined // Don't serialize process
|
||||
}));
|
||||
|
||||
await context.globalState.update('mcp-servers', data);
|
||||
}
|
||||
|
||||
export async function loadMCPServers(
|
||||
context: vscode.ExtensionContext
|
||||
): Promise<Map<string, MCPServer>> {
|
||||
const servers = new Map<string, MCPServer>();
|
||||
const data = context.globalState.get<any[]>('mcp-servers', []);
|
||||
|
||||
for (const item of data) {
|
||||
servers.set(item.name, item);
|
||||
}
|
||||
|
||||
// Load known servers if empty
|
||||
if (servers.size === 0) {
|
||||
loadKnownServers(servers);
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MCPTool } from '../server';
|
||||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(cp.exec);
|
||||
|
||||
interface MCPServerConfig {
|
||||
name: string;
|
||||
package: string;
|
||||
type: 'npm' | 'python';
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface MCPServerInstance extends MCPServerConfig {
|
||||
client?: Client;
|
||||
transport?: StdioClientTransport;
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
installPath?: string;
|
||||
capabilities?: {
|
||||
tools?: string[];
|
||||
resources?: string[];
|
||||
prompts?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
class MCPProxyManager {
|
||||
private servers: Map<string, MCPServerInstance> = new Map();
|
||||
private context: vscode.ExtensionContext;
|
||||
private toolToServerMap: Map<string, string> = new Map();
|
||||
private resourceToServerMap: Map<string, string> = new Map();
|
||||
private promptToServerMap: Map<string, string> = new Map();
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.context = context;
|
||||
this.loadKnownServers();
|
||||
this.loadInstalledServers();
|
||||
}
|
||||
|
||||
private loadKnownServers() {
|
||||
// Load popular MCP servers
|
||||
const knownServers: MCPServerConfig[] = [
|
||||
// NPM packages
|
||||
{
|
||||
name: 'puppeteer',
|
||||
package: '@modelcontextprotocol/server-puppeteer',
|
||||
type: 'npm',
|
||||
command: 'node',
|
||||
args: ['node_modules/@modelcontextprotocol/server-puppeteer/dist/index.js']
|
||||
},
|
||||
{
|
||||
name: 'playwright',
|
||||
package: '@modelcontextprotocol/server-playwright',
|
||||
type: 'npm',
|
||||
command: 'node',
|
||||
args: ['node_modules/@modelcontextprotocol/server-playwright/dist/index.js']
|
||||
},
|
||||
{
|
||||
name: 'sqlite',
|
||||
package: '@modelcontextprotocol/server-sqlite',
|
||||
type: 'npm',
|
||||
command: 'node',
|
||||
args: ['node_modules/@modelcontextprotocol/server-sqlite/dist/index.js']
|
||||
},
|
||||
{
|
||||
name: 'filesystem',
|
||||
package: '@modelcontextprotocol/server-filesystem',
|
||||
type: 'npm',
|
||||
command: 'node',
|
||||
args: ['node_modules/@modelcontextprotocol/server-filesystem/dist/index.js']
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
type: 'npm',
|
||||
command: 'node',
|
||||
args: ['node_modules/@modelcontextprotocol/server-github/dist/index.js']
|
||||
},
|
||||
// Python packages
|
||||
{
|
||||
name: 'git',
|
||||
package: 'mcp-server-git',
|
||||
type: 'python',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-git']
|
||||
},
|
||||
{
|
||||
name: 'slack',
|
||||
package: 'mcp-server-slack',
|
||||
type: 'python',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-slack']
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
package: 'mcp-server-time',
|
||||
type: 'python',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-time']
|
||||
},
|
||||
{
|
||||
name: 'fetch',
|
||||
package: 'mcp-server-fetch',
|
||||
type: 'python',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-fetch']
|
||||
}
|
||||
];
|
||||
|
||||
for (const server of knownServers) {
|
||||
this.servers.set(server.name, {
|
||||
...server,
|
||||
installed: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async loadInstalledServers() {
|
||||
const installed = this.context.globalState.get<MCPServerInstance[]>('mcp-installed-servers', []);
|
||||
for (const server of installed) {
|
||||
this.servers.set(server.name, server);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveInstalledServers() {
|
||||
const installed = Array.from(this.servers.values())
|
||||
.filter(s => s.installed)
|
||||
.map(s => ({
|
||||
...s,
|
||||
client: undefined,
|
||||
transport: undefined
|
||||
}));
|
||||
await this.context.globalState.update('mcp-installed-servers', installed);
|
||||
}
|
||||
|
||||
async installServer(packageName: string, type?: 'npm' | 'python', force?: boolean): Promise<string> {
|
||||
const serverType = type || this.detectPackageType(packageName);
|
||||
const serverName = this.extractServerName(packageName);
|
||||
|
||||
// Check if already installed
|
||||
const existing = this.servers.get(serverName);
|
||||
if (existing?.installed && !force) {
|
||||
return `✅ MCP server '${serverName}' is already installed`;
|
||||
}
|
||||
|
||||
const installDir = path.join(this.context.globalStorageUri.fsPath, 'mcp-servers', serverName);
|
||||
await fs.promises.mkdir(installDir, { recursive: true });
|
||||
|
||||
try {
|
||||
let version = 'unknown';
|
||||
let command: string;
|
||||
let args: string[];
|
||||
|
||||
if (serverType === 'npm') {
|
||||
// Create package.json
|
||||
const packageJson = {
|
||||
name: `mcp-proxy-${serverName}`,
|
||||
version: '1.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
[packageName]: 'latest'
|
||||
}
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(installDir, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2)
|
||||
);
|
||||
|
||||
// Install
|
||||
await exec('npm install', { cwd: installDir });
|
||||
|
||||
// Get version
|
||||
const installedPkgPath = path.join(installDir, 'node_modules', packageName, 'package.json');
|
||||
const installedPkg = JSON.parse(await fs.promises.readFile(installedPkgPath, 'utf8'));
|
||||
version = installedPkg.version;
|
||||
|
||||
// Determine entry point
|
||||
const main = installedPkg.main || 'index.js';
|
||||
command = 'node';
|
||||
args = [path.join('node_modules', packageName, main)];
|
||||
|
||||
} else {
|
||||
// Python package - install with uvx
|
||||
await exec(`uvx install ${packageName}`, { cwd: installDir });
|
||||
|
||||
// Try to get version
|
||||
try {
|
||||
const { stdout } = await exec(`uvx ${packageName} --version`, { cwd: installDir });
|
||||
version = stdout.trim();
|
||||
} catch {}
|
||||
|
||||
command = 'uvx';
|
||||
args = [packageName];
|
||||
}
|
||||
|
||||
// Update server info
|
||||
const serverInfo: MCPServerInstance = {
|
||||
name: serverName,
|
||||
package: packageName,
|
||||
type: serverType,
|
||||
command,
|
||||
args,
|
||||
installed: true,
|
||||
version,
|
||||
installPath: installDir
|
||||
};
|
||||
|
||||
this.servers.set(serverName, serverInfo);
|
||||
await this.saveInstalledServers();
|
||||
|
||||
// Try to connect and discover capabilities
|
||||
await this.connectToServer(serverName);
|
||||
|
||||
return `✅ Successfully installed ${packageName}
|
||||
📦 Version: ${version}
|
||||
📂 Location: ${installDir}
|
||||
🚀 Ready to use via proxy`;
|
||||
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to install ${packageName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async connectToServer(serverName: string): Promise<void> {
|
||||
const server = this.servers.get(serverName);
|
||||
if (!server || !server.installed) {
|
||||
throw new Error(`Server ${serverName} not installed`);
|
||||
}
|
||||
|
||||
// Disconnect if already connected
|
||||
if (server.client) {
|
||||
await server.transport?.close();
|
||||
}
|
||||
|
||||
const command = server.command!;
|
||||
const args = server.args || [];
|
||||
const cwd = server.installPath;
|
||||
const env: Record<string, string> = Object.entries({ ...process.env, ...server.env })
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// Create transport
|
||||
const transport = new StdioClientTransport({
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
env
|
||||
});
|
||||
|
||||
// Create client
|
||||
const client = new Client({
|
||||
name: `hanzo-proxy-${serverName}`,
|
||||
version: '1.0.0'
|
||||
}, {
|
||||
capabilities: {}
|
||||
});
|
||||
|
||||
// Connect
|
||||
await client.connect(transport);
|
||||
|
||||
// Discover capabilities
|
||||
const capabilities: MCPServerInstance['capabilities'] = {};
|
||||
|
||||
try {
|
||||
// List tools
|
||||
const toolsResponse = await (client as any).request({
|
||||
method: 'tools/list',
|
||||
params: {}
|
||||
});
|
||||
capabilities.tools = toolsResponse.tools?.map((t: any) => t.name) || [];
|
||||
|
||||
// List resources
|
||||
const resourcesResponse = await (client as any).request({
|
||||
method: 'resources/list',
|
||||
params: {}
|
||||
});
|
||||
capabilities.resources = resourcesResponse.resources?.map((r: any) => r.uri) || [];
|
||||
|
||||
// List prompts
|
||||
const promptsResponse = await (client as any).request({
|
||||
method: 'prompts/list',
|
||||
params: {}
|
||||
});
|
||||
capabilities.prompts = promptsResponse.prompts?.map((p: any) => p.name) || [];
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to discover capabilities for ${serverName}:`, error);
|
||||
}
|
||||
|
||||
// Update server
|
||||
server.client = client;
|
||||
server.transport = transport;
|
||||
server.capabilities = capabilities;
|
||||
|
||||
// Update routing maps
|
||||
if (capabilities.tools) {
|
||||
for (const tool of capabilities.tools) {
|
||||
this.toolToServerMap.set(tool, serverName);
|
||||
}
|
||||
}
|
||||
if (capabilities.resources) {
|
||||
for (const resource of capabilities.resources) {
|
||||
this.resourceToServerMap.set(resource, serverName);
|
||||
}
|
||||
}
|
||||
if (capabilities.prompts) {
|
||||
for (const prompt of capabilities.prompts) {
|
||||
this.promptToServerMap.set(prompt, serverName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async proxyToolCall(toolName: string, args: any): Promise<any> {
|
||||
const serverName = this.toolToServerMap.get(toolName);
|
||||
if (!serverName) {
|
||||
throw new Error(`No server found for tool: ${toolName}`);
|
||||
}
|
||||
|
||||
const server = this.servers.get(serverName);
|
||||
if (!server?.client) {
|
||||
// Try to connect
|
||||
await this.connectToServer(serverName);
|
||||
}
|
||||
|
||||
const response = await (server!.client as any).request({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: args
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
server: serverName,
|
||||
tool: toolName,
|
||||
result: response.content || response
|
||||
};
|
||||
}
|
||||
|
||||
async listAllCapabilities(): Promise<any> {
|
||||
const result = {
|
||||
servers: [] as any[],
|
||||
tools: [] as any[],
|
||||
resources: [] as any[],
|
||||
prompts: [] as any[]
|
||||
};
|
||||
|
||||
for (const [name, server] of this.servers) {
|
||||
if (!server.installed) continue;
|
||||
|
||||
// Ensure connected
|
||||
if (!server.client) {
|
||||
try {
|
||||
await this.connectToServer(name);
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to ${name}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.servers.push({
|
||||
name,
|
||||
package: server.package,
|
||||
version: server.version,
|
||||
type: server.type,
|
||||
capabilities: server.capabilities
|
||||
});
|
||||
|
||||
// Aggregate capabilities
|
||||
if (server.capabilities?.tools) {
|
||||
result.tools.push(...server.capabilities.tools.map(t => ({ tool: t, server: name })));
|
||||
}
|
||||
if (server.capabilities?.resources) {
|
||||
result.resources.push(...server.capabilities.resources.map(r => ({ resource: r, server: name })));
|
||||
}
|
||||
if (server.capabilities?.prompts) {
|
||||
result.prompts.push(...server.capabilities.prompts.map(p => ({ prompt: p, server: name })));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private detectPackageType(packageName: string): 'npm' | 'python' {
|
||||
if (packageName.startsWith('@') || packageName.includes('/')) {
|
||||
return 'npm';
|
||||
}
|
||||
if (packageName.startsWith('mcp-server-') || packageName.includes('_')) {
|
||||
return 'python';
|
||||
}
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
private extractServerName(packageName: string): string {
|
||||
if (packageName.includes('/')) {
|
||||
const parts = packageName.split('/');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
return lastPart.replace('server-', '').replace('mcp-', '');
|
||||
}
|
||||
return packageName.replace('mcp-server-', '').replace('mcp-', '');
|
||||
}
|
||||
|
||||
async disconnectAll() {
|
||||
for (const server of this.servers.values()) {
|
||||
if (server.transport) {
|
||||
await server.transport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createMCPUniversalProxyTools(context: vscode.ExtensionContext): MCPTool[] {
|
||||
const proxyManager = new MCPProxyManager(context);
|
||||
|
||||
// Cleanup on deactivate
|
||||
context.subscriptions.push({
|
||||
dispose: async () => {
|
||||
await proxyManager.disconnectAll();
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'mcp',
|
||||
description: 'Universal MCP server proxy - install and use any MCP server',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['install', 'call', 'list', 'uninstall'],
|
||||
description: 'Action to perform'
|
||||
},
|
||||
package: {
|
||||
type: 'string',
|
||||
description: 'Package name for install (e.g., "@modelcontextprotocol/server-github")'
|
||||
},
|
||||
server: {
|
||||
type: 'string',
|
||||
description: 'Server name for operations'
|
||||
},
|
||||
tool: {
|
||||
type: 'string',
|
||||
description: 'Tool name to call'
|
||||
},
|
||||
args: {
|
||||
type: 'object',
|
||||
description: 'Arguments for tool call'
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['npm', 'python'],
|
||||
description: 'Package type (auto-detected if not specified)'
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: 'Force reinstall'
|
||||
}
|
||||
},
|
||||
required: ['action']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
switch (args.action) {
|
||||
case 'install':
|
||||
if (!args.package) {
|
||||
return '❌ Package name required for install';
|
||||
}
|
||||
return await proxyManager.installServer(args.package, args.type, args.force);
|
||||
|
||||
case 'call':
|
||||
if (!args.tool) {
|
||||
return '❌ Tool name required for call';
|
||||
}
|
||||
try {
|
||||
const result = await proxyManager.proxyToolCall(args.tool, args.args || {});
|
||||
return `✅ Tool called via ${result.server}\n\nResult:\n${JSON.stringify(result.result, null, 2)}`;
|
||||
} catch (error: any) {
|
||||
return `❌ Tool call failed: ${error.message}`;
|
||||
}
|
||||
|
||||
case 'list':
|
||||
const capabilities = await proxyManager.listAllCapabilities();
|
||||
return formatCapabilities(capabilities);
|
||||
|
||||
case 'uninstall':
|
||||
// TODO: Implement uninstall
|
||||
return '❌ Uninstall not yet implemented';
|
||||
|
||||
default:
|
||||
return '❌ Unknown action. Use: install, call, list, or uninstall';
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function formatCapabilities(capabilities: any): string {
|
||||
let output = '# MCP Proxy Status\n\n';
|
||||
|
||||
output += '## Installed Servers\n';
|
||||
for (const server of capabilities.servers) {
|
||||
output += `\n### ${server.name}\n`;
|
||||
output += `- Package: \`${server.package}\`\n`;
|
||||
output += `- Version: ${server.version}\n`;
|
||||
output += `- Type: ${server.type}\n`;
|
||||
if (server.capabilities) {
|
||||
output += `- Tools: ${server.capabilities.tools?.length || 0}\n`;
|
||||
output += `- Resources: ${server.capabilities.resources?.length || 0}\n`;
|
||||
output += `- Prompts: ${server.capabilities.prompts?.length || 0}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.tools.length > 0) {
|
||||
output += '\n## Available Tools\n';
|
||||
for (const { tool, server } of capabilities.tools) {
|
||||
output += `- \`${tool}\` (${server})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.resources.length > 0) {
|
||||
output += '\n## Available Resources\n';
|
||||
for (const { resource, server } of capabilities.resources) {
|
||||
output += `- \`${resource}\` (${server})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.prompts.length > 0) {
|
||||
output += '\n## Available Prompts\n';
|
||||
for (const { prompt, server } of capabilities.prompts) {
|
||||
output += `- \`${prompt}\` (${server})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MCPTool } from '../server';
|
||||
|
||||
/**
|
||||
* Unified Tool Definitions
|
||||
*
|
||||
* This consolidates all tools from the Python Hanzo MCP implementation
|
||||
* ensuring no duplicates and maintaining feature parity.
|
||||
*
|
||||
* Core principle: One tool per orthogonal concept
|
||||
*/
|
||||
|
||||
// Tool mapping to ensure no duplicates
|
||||
const TOOL_MAPPING = {
|
||||
// File Operations (unified)
|
||||
'read': ['read_files', 'read_file', 'cat'],
|
||||
'write': ['write_file', 'create_file'],
|
||||
'edit': ['edit_file', 'modify_file', 'sed'],
|
||||
'multi_edit': ['edit_files', 'bulk_edit'],
|
||||
|
||||
// Directory Operations
|
||||
'directory_tree': ['tree', 'ls_tree', 'dir_tree'],
|
||||
'find_files': ['find', 'locate', 'search_files'],
|
||||
|
||||
// Search Operations
|
||||
'grep': ['search_content', 'grep_files', 'rg'],
|
||||
'content_replace': ['replace_content', 'sed_replace'],
|
||||
'git_search': ['git_grep', 'search_git'],
|
||||
|
||||
// Shell Operations
|
||||
'bash': ['run_command', 'shell', 'sh'],
|
||||
'run_script': ['execute_script', 'run'],
|
||||
|
||||
// Development Tools
|
||||
'notebook_read': ['read_notebook', 'jupyter_read'],
|
||||
'notebook_edit': ['edit_notebook', 'jupyter_edit'],
|
||||
|
||||
// AI/Agent Tools
|
||||
'agent': ['dispatch_agent', 'delegate'],
|
||||
'think': ['reason', 'analyze'],
|
||||
'critic': ['review', 'critique'],
|
||||
|
||||
// Project Tools
|
||||
'project_analyze': ['analyze_project', 'project_info'],
|
||||
'todo_unified': ['todo', 'task_list'],
|
||||
|
||||
// Platform Tools
|
||||
'mcp': ['mcp_proxy', 'mcp_universal'],
|
||||
'hanzo_mcp': ['platform_mcp', 'cloud_mcp'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get canonical tool name from any alias
|
||||
*/
|
||||
export function getCanonicalToolName(toolName: string): string {
|
||||
for (const [canonical, aliases] of Object.entries(TOOL_MAPPING)) {
|
||||
if (toolName === canonical || aliases.includes(toolName)) {
|
||||
return canonical;
|
||||
}
|
||||
}
|
||||
return toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced file reading with all Python MCP features
|
||||
*/
|
||||
export function createUnifiedReadTool(context: vscode.ExtensionContext): MCPTool {
|
||||
return {
|
||||
name: 'read',
|
||||
description: 'Read files with encoding detection, line ranges, and multiple file support',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paths: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'File paths to read (single string also accepted)'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Single file path (for compatibility)'
|
||||
},
|
||||
encoding: {
|
||||
type: 'string',
|
||||
default: 'utf-8',
|
||||
description: 'File encoding'
|
||||
},
|
||||
start_line: {
|
||||
type: 'number',
|
||||
description: 'Starting line number (1-based)'
|
||||
},
|
||||
end_line: {
|
||||
type: 'number',
|
||||
description: 'Ending line number (inclusive)'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Handle both paths array and single path
|
||||
const paths = args.paths || (args.path ? [args.path] : []);
|
||||
if (paths.length === 0) {
|
||||
return '❌ No file paths provided';
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const filePath of paths) {
|
||||
try {
|
||||
// Resolve path
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
|
||||
// Check permissions
|
||||
const config = vscode.workspace.getConfiguration('hanzo.mcp');
|
||||
const allowedPaths = config.get<string[]>('allowedPaths', []);
|
||||
if (!isPathAllowed(resolvedPath, allowedPaths)) {
|
||||
results.push(`❌ ${filePath}: Access denied`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read file
|
||||
let content = await fs.readFile(resolvedPath, args.encoding || 'utf-8');
|
||||
|
||||
// Apply line range if specified
|
||||
if (args.start_line || args.end_line) {
|
||||
const lines = content.split('\n');
|
||||
const start = (args.start_line || 1) - 1;
|
||||
const end = args.end_line || lines.length;
|
||||
content = lines.slice(start, end).join('\n');
|
||||
}
|
||||
|
||||
results.push(`📄 ${filePath}:\n${content}`);
|
||||
} catch (error: any) {
|
||||
results.push(`❌ ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results.join('\n\n---\n\n');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced project analysis matching Python MCP features
|
||||
*/
|
||||
export function createUnifiedProjectAnalyzeTool(context: vscode.ExtensionContext): MCPTool {
|
||||
return {
|
||||
name: 'project_analyze',
|
||||
description: 'Analyze project structure, dependencies, and frameworks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Project root path (defaults to workspace root)'
|
||||
},
|
||||
include_dependencies: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Include dependency analysis'
|
||||
},
|
||||
include_structure: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Include project structure'
|
||||
},
|
||||
max_depth: {
|
||||
type: 'number',
|
||||
default: 5,
|
||||
description: 'Maximum directory depth for structure analysis'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const projectPath = args.path || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!projectPath) {
|
||||
return '❌ No project path specified';
|
||||
}
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
let output = `# Project Analysis: ${path.basename(projectPath)}\n\n`;
|
||||
|
||||
// Detect project type
|
||||
const projectType = await detectProjectType(projectPath);
|
||||
output += `## Project Type: ${projectType}\n\n`;
|
||||
|
||||
// Analyze structure
|
||||
if (args.include_structure !== false) {
|
||||
output += `## Structure\n\`\`\`\n`;
|
||||
output += await generateTreeStructure(projectPath, args.max_depth || 5);
|
||||
output += `\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
// Analyze dependencies
|
||||
if (args.include_dependencies !== false) {
|
||||
output += `## Dependencies\n`;
|
||||
const deps = await analyzeDependencies(projectPath, projectType);
|
||||
output += deps + '\n\n';
|
||||
}
|
||||
|
||||
// Framework detection
|
||||
output += `## Frameworks & Tools\n`;
|
||||
const frameworks = await detectFrameworks(projectPath);
|
||||
frameworks.forEach(f => output += `- ${f}\n`);
|
||||
|
||||
return output;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix command aliasing for modes
|
||||
*/
|
||||
export function createUnixAliasTool(context: vscode.ExtensionContext): MCPTool {
|
||||
return {
|
||||
name: 'alias',
|
||||
description: 'Create command aliases for use in modes',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Alias name'
|
||||
},
|
||||
command: {
|
||||
type: 'string',
|
||||
description: 'Command to alias'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'What this command does'
|
||||
}
|
||||
},
|
||||
required: ['name', 'command']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const aliases = context.globalState.get<any>('command-aliases', {});
|
||||
aliases[args.name] = {
|
||||
command: args.command,
|
||||
description: args.description || `Alias for: ${args.command}`
|
||||
};
|
||||
|
||||
await context.globalState.update('command-aliases', aliases);
|
||||
|
||||
return `✅ Created alias '${args.name}' → '${args.command}'
|
||||
|
||||
This can now be used in modes:
|
||||
\`\`\`javascript
|
||||
mode: {
|
||||
tools: ['${args.name}']
|
||||
}
|
||||
\`\`\``;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
function isPathAllowed(filePath: string, allowedPaths: string[]): boolean {
|
||||
if (allowedPaths.length === 0) {
|
||||
// No restrictions if empty
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalizedPath = filePath.toLowerCase();
|
||||
return allowedPaths.some(allowed =>
|
||||
normalizedPath.startsWith(allowed.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
async function detectProjectType(projectPath: string): Promise<string> {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Check for common project files
|
||||
const checks = [
|
||||
{ file: 'package.json', type: 'Node.js/JavaScript' },
|
||||
{ file: 'pyproject.toml', type: 'Python (Poetry)' },
|
||||
{ file: 'setup.py', type: 'Python' },
|
||||
{ file: 'requirements.txt', type: 'Python' },
|
||||
{ file: 'Cargo.toml', type: 'Rust' },
|
||||
{ file: 'go.mod', type: 'Go' },
|
||||
{ file: 'pom.xml', type: 'Java (Maven)' },
|
||||
{ file: 'build.gradle', type: 'Java (Gradle)' },
|
||||
{ file: 'Gemfile', type: 'Ruby' },
|
||||
{ file: 'composer.json', type: 'PHP' },
|
||||
{ file: '*.csproj', type: 'C#/.NET' },
|
||||
{ file: 'CMakeLists.txt', type: 'C/C++ (CMake)' }
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
try {
|
||||
if (check.file.includes('*')) {
|
||||
// Pattern matching
|
||||
const files = await fs.readdir(projectPath);
|
||||
const pattern = check.file.replace('*', '.*');
|
||||
if (files.some((f: string) => f.match(pattern))) {
|
||||
return check.type;
|
||||
}
|
||||
} else {
|
||||
await fs.access(path.join(projectPath, check.file));
|
||||
return check.type;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, continue
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
async function generateTreeStructure(dirPath: string, maxDepth: number, currentDepth: number = 0): Promise<string> {
|
||||
if (currentDepth >= maxDepth) return '';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const indent = ' '.repeat(currentDepth);
|
||||
let output = '';
|
||||
|
||||
try {
|
||||
const items = await fs.readdir(dirPath);
|
||||
const filtered = items.filter((item: string) =>
|
||||
!item.startsWith('.') &&
|
||||
!['node_modules', '__pycache__', 'dist', 'build', '.git'].includes(item)
|
||||
);
|
||||
|
||||
for (const item of filtered) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
output += `${indent}${item}/\n`;
|
||||
output += await generateTreeStructure(itemPath, maxDepth, currentDepth + 1);
|
||||
} else {
|
||||
output += `${indent}${item}\n`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function analyzeDependencies(projectPath: string, projectType: string): Promise<string> {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
switch (projectType) {
|
||||
case 'Node.js/JavaScript': {
|
||||
const packageJson = JSON.parse(
|
||||
await fs.readFile(path.join(projectPath, 'package.json'), 'utf-8')
|
||||
);
|
||||
const deps = Object.keys(packageJson.dependencies || {});
|
||||
const devDeps = Object.keys(packageJson.devDependencies || {});
|
||||
return `- Dependencies: ${deps.length} (${deps.slice(0, 5).join(', ')}${deps.length > 5 ? '...' : ''})\n` +
|
||||
`- Dev Dependencies: ${devDeps.length}`;
|
||||
}
|
||||
|
||||
case 'Python':
|
||||
case 'Python (Poetry)': {
|
||||
// Try to read requirements.txt or pyproject.toml
|
||||
try {
|
||||
const requirements = await fs.readFile(
|
||||
path.join(projectPath, 'requirements.txt'), 'utf-8'
|
||||
);
|
||||
const lines = requirements.split('\n').filter(l => l && !l.startsWith('#'));
|
||||
return `- Dependencies: ${lines.length} packages`;
|
||||
} catch {
|
||||
return '- Dependencies: Unable to analyze';
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return '- Dependencies: Analysis not implemented for this project type';
|
||||
}
|
||||
} catch (error) {
|
||||
return '- Dependencies: Unable to analyze';
|
||||
}
|
||||
}
|
||||
|
||||
async function detectFrameworks(projectPath: string): Promise<string[]> {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Check package.json for JS frameworks
|
||||
try {
|
||||
const packageJson = JSON.parse(
|
||||
await fs.readFile(path.join(projectPath, 'package.json'), 'utf-8')
|
||||
);
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
const frameworkChecks = [
|
||||
{ dep: 'react', name: 'React' },
|
||||
{ dep: 'vue', name: 'Vue.js' },
|
||||
{ dep: '@angular/core', name: 'Angular' },
|
||||
{ dep: 'next', name: 'Next.js' },
|
||||
{ dep: 'express', name: 'Express.js' },
|
||||
{ dep: 'fastify', name: 'Fastify' },
|
||||
{ dep: 'jest', name: 'Jest (testing)' },
|
||||
{ dep: 'mocha', name: 'Mocha (testing)' },
|
||||
{ dep: 'typescript', name: 'TypeScript' }
|
||||
];
|
||||
|
||||
frameworkChecks.forEach(check => {
|
||||
if (allDeps[check.dep]) {
|
||||
frameworks.push(check.name);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Not a Node.js project
|
||||
}
|
||||
|
||||
// Check for Python frameworks
|
||||
try {
|
||||
const files = await fs.readdir(projectPath);
|
||||
if (files.includes('manage.py')) frameworks.push('Django');
|
||||
if (files.includes('app.py') || files.includes('application.py')) {
|
||||
// Could be Flask or FastAPI
|
||||
try {
|
||||
const content = await fs.readFile(
|
||||
path.join(projectPath, files.includes('app.py') ? 'app.py' : 'application.py'),
|
||||
'utf-8'
|
||||
);
|
||||
if (content.includes('flask')) frameworks.push('Flask');
|
||||
if (content.includes('fastapi')) frameworks.push('FastAPI');
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return frameworks.length > 0 ? frameworks : ['No frameworks detected'];
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { createMCPUniversalProxyTools } from '../../mcp/tools/mcp-universal-proxy';
|
||||
|
||||
describe('MCP Proxy Integration Tests', function() {
|
||||
this.timeout(60000); // 60 seconds for real installations
|
||||
|
||||
let context: vscode.ExtensionContext;
|
||||
let mcpTool: any;
|
||||
let testStoragePath: string;
|
||||
|
||||
before(async () => {
|
||||
// Create test storage directory
|
||||
testStoragePath = path.join(__dirname, '../../../.test-storage');
|
||||
await fs.promises.mkdir(testStoragePath, { recursive: true });
|
||||
|
||||
// Create mock context
|
||||
const globalState = new Map();
|
||||
context = {
|
||||
globalState: {
|
||||
get: (key: string, defaultValue?: any) => globalState.get(key) || defaultValue,
|
||||
update: async (key: string, value: any) => {
|
||||
globalState.set(key, value);
|
||||
return Promise.resolve();
|
||||
},
|
||||
keys: () => Array.from(globalState.keys())
|
||||
},
|
||||
globalStorageUri: {
|
||||
fsPath: testStoragePath
|
||||
},
|
||||
subscriptions: []
|
||||
} as any;
|
||||
|
||||
// Get the MCP tool
|
||||
const tools = createMCPUniversalProxyTools(context);
|
||||
mcpTool = tools.find(t => t.name === 'mcp');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// Cleanup test storage
|
||||
try {
|
||||
await fs.promises.rm(testStoragePath, { recursive: true, force: true });
|
||||
} catch {}
|
||||
});
|
||||
|
||||
describe('Real Server Installation', () => {
|
||||
it('should install and connect to filesystem MCP server', async function() {
|
||||
this.timeout(120000); // 2 minutes for npm install
|
||||
|
||||
// Install the filesystem server
|
||||
const installResult = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-filesystem'
|
||||
});
|
||||
|
||||
assert(installResult.includes('Successfully installed'));
|
||||
assert(installResult.includes('@modelcontextprotocol/server-filesystem'));
|
||||
|
||||
// List capabilities
|
||||
const listResult = await mcpTool.handler({ action: 'list' });
|
||||
|
||||
assert(listResult.includes('filesystem'));
|
||||
assert(listResult.includes('Installed Servers'));
|
||||
|
||||
// Verify installation directory exists
|
||||
const serverPath = path.join(testStoragePath, 'mcp-servers', 'filesystem');
|
||||
assert(fs.existsSync(serverPath));
|
||||
assert(fs.existsSync(path.join(serverPath, 'node_modules')));
|
||||
});
|
||||
|
||||
it('should handle Python package installation', async function() {
|
||||
// Skip if uvx not available
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('which uvx', { stdio: 'ignore' });
|
||||
} catch {
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const installResult = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: 'mcp-server-time',
|
||||
type: 'python'
|
||||
});
|
||||
|
||||
assert(installResult.includes('Successfully installed') ||
|
||||
installResult.includes('already installed'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server Communication', () => {
|
||||
it('should discover server capabilities after connection', async function() {
|
||||
// Ensure filesystem server is installed
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-filesystem'
|
||||
});
|
||||
|
||||
// Give server time to start and discover capabilities
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const listResult = await mcpTool.handler({ action: 'list' });
|
||||
|
||||
// Should show tools discovered from the server
|
||||
assert(listResult.includes('Available Tools') ||
|
||||
listResult.includes('filesystem'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Recovery', () => {
|
||||
it('should handle invalid package names gracefully', async () => {
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@invalid/package-that-does-not-exist-12345'
|
||||
});
|
||||
|
||||
assert(result.includes('Failed to install'));
|
||||
});
|
||||
|
||||
it('should recover from server crashes', async function() {
|
||||
// This test would require mocking server crashes
|
||||
// For now, we just verify the error handling path exists
|
||||
const result = await mcpTool.handler({
|
||||
action: 'call',
|
||||
tool: 'non_existent_tool'
|
||||
});
|
||||
|
||||
assert(result.includes('No server found') || result.includes('failed'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Server Management', () => {
|
||||
it('should manage multiple servers simultaneously', async function() {
|
||||
this.timeout(180000); // 3 minutes for multiple installs
|
||||
|
||||
// Install multiple servers
|
||||
const servers = [
|
||||
'@modelcontextprotocol/server-memory',
|
||||
'@modelcontextprotocol/server-fetch'
|
||||
];
|
||||
|
||||
for (const server of servers) {
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: server
|
||||
});
|
||||
}
|
||||
|
||||
// List all servers
|
||||
const listResult = await mcpTool.handler({ action: 'list' });
|
||||
|
||||
assert(listResult.includes('memory'));
|
||||
assert(listResult.includes('fetch'));
|
||||
|
||||
// Verify each has its own directory
|
||||
assert(fs.existsSync(path.join(testStoragePath, 'mcp-servers', 'memory')));
|
||||
assert(fs.existsSync(path.join(testStoragePath, 'mcp-servers', 'fetch')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Persistence', () => {
|
||||
it('should persist installed servers across sessions', async () => {
|
||||
// Install a server
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-memory'
|
||||
});
|
||||
|
||||
// Create new tools instance (simulating restart)
|
||||
const newTools = createMCPUniversalProxyTools(context);
|
||||
const newMcpTool = newTools.find(t => t.name === 'mcp');
|
||||
|
||||
// List should still show the installed server
|
||||
const listResult = await newMcpTool.handler({ action: 'list' });
|
||||
assert(listResult.includes('memory'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Proxy Examples', () => {
|
||||
it('should demonstrate usage patterns', () => {
|
||||
const examples = `
|
||||
# Install GitHub MCP server
|
||||
@hanzo mcp --action install --package @modelcontextprotocol/server-github
|
||||
|
||||
# Install Python-based Git server
|
||||
@hanzo mcp --action install --package mcp-server-git --type python
|
||||
|
||||
# List all installed servers and capabilities
|
||||
@hanzo mcp --action list
|
||||
|
||||
# Call a tool from any installed server
|
||||
@hanzo mcp --action call --tool github_search --args '{"query": "MCP servers"}'
|
||||
|
||||
# Auto-install and use in one command (future feature)
|
||||
@hanzo mcp --action call --tool sqlite_query --args '{"query": "SELECT * FROM users"}' --auto-install
|
||||
`;
|
||||
|
||||
assert(examples.includes('install'));
|
||||
assert(examples.includes('list'));
|
||||
assert(examples.includes('call'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,395 @@
|
||||
import * as assert from 'assert';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createMCPUniversalProxyTools } from '../mcp/tools/mcp-universal-proxy';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(cp.exec);
|
||||
|
||||
describe('MCP Universal Proxy Tests', () => {
|
||||
let context: vscode.ExtensionContext;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let globalState: Map<string, any>;
|
||||
let mcpTool: any;
|
||||
let execStub: sinon.SinonStub;
|
||||
let fsStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
globalState = new Map();
|
||||
|
||||
// Mock VS Code context
|
||||
context = {
|
||||
globalState: {
|
||||
get: (key: string, defaultValue?: any) => globalState.get(key) || defaultValue,
|
||||
update: async (key: string, value: any) => {
|
||||
globalState.set(key, value);
|
||||
return Promise.resolve();
|
||||
},
|
||||
keys: () => Array.from(globalState.keys())
|
||||
},
|
||||
globalStorageUri: {
|
||||
fsPath: '/test/storage'
|
||||
},
|
||||
subscriptions: []
|
||||
} as any;
|
||||
|
||||
// Mock exec
|
||||
execStub = sandbox.stub(cp, 'exec');
|
||||
|
||||
// Mock fs
|
||||
fsStub = {
|
||||
mkdir: sandbox.stub().resolves(),
|
||||
writeFile: sandbox.stub().resolves(),
|
||||
readFile: sandbox.stub(),
|
||||
rm: sandbox.stub().resolves()
|
||||
};
|
||||
sandbox.stub(fs.promises, 'mkdir').callsFake(fsStub.mkdir);
|
||||
sandbox.stub(fs.promises, 'writeFile').callsFake(fsStub.writeFile);
|
||||
sandbox.stub(fs.promises, 'readFile').callsFake(fsStub.readFile);
|
||||
|
||||
// Get the MCP tool
|
||||
const tools = createMCPUniversalProxyTools(context);
|
||||
mcpTool = tools.find(t => t.name === 'mcp');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Installation', () => {
|
||||
it('should install npm packages correctly', async () => {
|
||||
const packageName = '@modelcontextprotocol/server-github';
|
||||
|
||||
// Mock successful npm install
|
||||
execStub.withArgs('npm install').callsArgWith(2, null, 'installed', '');
|
||||
|
||||
// Mock package.json read
|
||||
fsStub.readFile.resolves(JSON.stringify({
|
||||
name: packageName,
|
||||
version: '1.0.0',
|
||||
main: 'dist/index.js'
|
||||
}));
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: packageName
|
||||
});
|
||||
|
||||
assert(result.includes('Successfully installed'));
|
||||
assert(result.includes('1.0.0'));
|
||||
assert(fsStub.mkdir.called);
|
||||
assert(fsStub.writeFile.called);
|
||||
});
|
||||
|
||||
it('should install Python packages correctly', async () => {
|
||||
const packageName = 'mcp-server-git';
|
||||
|
||||
// Mock successful uvx install
|
||||
execStub.withArgs(sinon.match(/uvx install/)).callsArgWith(2, null, 'installed', '');
|
||||
execStub.withArgs(sinon.match(/uvx.*--version/)).callsArgWith(2, null, '1.2.3', '');
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: packageName,
|
||||
type: 'python'
|
||||
});
|
||||
|
||||
assert(result.includes('Successfully installed'));
|
||||
assert(result.includes('1.2.3'));
|
||||
});
|
||||
|
||||
it('should detect package type automatically', async () => {
|
||||
// NPM package
|
||||
execStub.callsArgWith(2, null, 'installed', '');
|
||||
fsStub.readFile.resolves(JSON.stringify({
|
||||
name: 'test',
|
||||
version: '1.0.0'
|
||||
}));
|
||||
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@scope/package'
|
||||
});
|
||||
|
||||
assert(execStub.calledWith('npm install'));
|
||||
|
||||
// Python package
|
||||
execStub.reset();
|
||||
execStub.callsArgWith(2, null, 'installed', '');
|
||||
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: 'mcp-server-test'
|
||||
});
|
||||
|
||||
assert(execStub.calledWith(sinon.match(/uvx install/)));
|
||||
});
|
||||
|
||||
it('should not reinstall if already installed without force', async () => {
|
||||
// Pre-populate installed servers
|
||||
globalState.set('mcp-installed-servers', [{
|
||||
name: 'github',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
installed: true,
|
||||
version: '1.0.0'
|
||||
}]);
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-github'
|
||||
});
|
||||
|
||||
assert(result.includes('already installed'));
|
||||
assert(!execStub.called);
|
||||
});
|
||||
|
||||
it('should reinstall with force flag', async () => {
|
||||
globalState.set('mcp-installed-servers', [{
|
||||
name: 'github',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
installed: true
|
||||
}]);
|
||||
|
||||
execStub.callsArgWith(2, null, 'installed', '');
|
||||
fsStub.readFile.resolves(JSON.stringify({
|
||||
version: '2.0.0'
|
||||
}));
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
force: true
|
||||
});
|
||||
|
||||
assert(result.includes('Successfully installed'));
|
||||
assert(execStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Proxy', () => {
|
||||
let spawnStub: sinon.SinonStub;
|
||||
let mockProcess: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockProcess = {
|
||||
stdin: { write: sandbox.stub() },
|
||||
stdout: {
|
||||
on: sandbox.stub(),
|
||||
off: sandbox.stub()
|
||||
},
|
||||
stderr: { on: sandbox.stub() },
|
||||
pid: 12345
|
||||
};
|
||||
|
||||
spawnStub = sandbox.stub(cp, 'spawn').returns(mockProcess as any);
|
||||
});
|
||||
|
||||
it('should proxy tool calls to the correct server', async () => {
|
||||
// Setup installed server
|
||||
globalState.set('mcp-installed-servers', [{
|
||||
name: 'github',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
installed: true,
|
||||
installPath: '/test/storage/mcp-servers/github',
|
||||
capabilities: {
|
||||
tools: ['github_search', 'github_create_issue']
|
||||
}
|
||||
}]);
|
||||
|
||||
// Simulate server response
|
||||
mockProcess.stdout.on.callsArgWith(1, JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
result: {
|
||||
content: [{ type: 'text', text: 'Search results' }]
|
||||
}
|
||||
}));
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'call',
|
||||
tool: 'github_search',
|
||||
args: { query: 'test' }
|
||||
});
|
||||
|
||||
assert(result.includes('Tool called via github'));
|
||||
assert(result.includes('Search results'));
|
||||
});
|
||||
|
||||
it('should handle tool not found error', async () => {
|
||||
const result = await mcpTool.handler({
|
||||
action: 'call',
|
||||
tool: 'unknown_tool'
|
||||
});
|
||||
|
||||
assert(result.includes('No server found for tool'));
|
||||
});
|
||||
|
||||
it('should auto-connect to server if not connected', async () => {
|
||||
globalState.set('mcp-installed-servers', [{
|
||||
name: 'sqlite',
|
||||
package: '@modelcontextprotocol/server-sqlite',
|
||||
installed: true,
|
||||
installPath: '/test/storage/mcp-servers/sqlite'
|
||||
}]);
|
||||
|
||||
// Mock initial connection response
|
||||
let callCount = 0;
|
||||
mockProcess.stdout.on.callsFake((event: string, handler: Function) => {
|
||||
if (event === 'data') {
|
||||
// First call: capabilities
|
||||
if (callCount === 0) {
|
||||
handler(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
tools: [{ name: 'sqlite_query' }]
|
||||
}
|
||||
}));
|
||||
}
|
||||
// Second call: actual tool response
|
||||
else if (callCount === 1) {
|
||||
handler(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
result: {
|
||||
content: [{ type: 'text', text: 'Query result' }]
|
||||
}
|
||||
}));
|
||||
}
|
||||
callCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'call',
|
||||
tool: 'sqlite_query',
|
||||
args: { query: 'SELECT * FROM users' }
|
||||
});
|
||||
|
||||
assert(spawnStub.called);
|
||||
assert(result.includes('Query result'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Listing Capabilities', () => {
|
||||
it('should list all installed servers and their capabilities', async () => {
|
||||
globalState.set('mcp-installed-servers', [
|
||||
{
|
||||
name: 'github',
|
||||
package: '@modelcontextprotocol/server-github',
|
||||
installed: true,
|
||||
version: '1.0.0',
|
||||
type: 'npm',
|
||||
capabilities: {
|
||||
tools: ['github_search', 'github_create_issue'],
|
||||
resources: ['github://repo'],
|
||||
prompts: ['review_pr']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sqlite',
|
||||
package: '@modelcontextprotocol/server-sqlite',
|
||||
installed: true,
|
||||
version: '2.0.0',
|
||||
type: 'npm',
|
||||
capabilities: {
|
||||
tools: ['sqlite_query', 'sqlite_execute']
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await mcpTool.handler({ action: 'list' });
|
||||
|
||||
assert(result.includes('github'));
|
||||
assert(result.includes('sqlite'));
|
||||
assert(result.includes('github_search'));
|
||||
assert(result.includes('sqlite_query'));
|
||||
assert(result.includes('Tools: 2'));
|
||||
assert(result.includes('Resources: 1'));
|
||||
assert(result.includes('Prompts: 1'));
|
||||
});
|
||||
|
||||
it('should show empty state when no servers installed', async () => {
|
||||
const result = await mcpTool.handler({ action: 'list' });
|
||||
assert(result.includes('MCP Proxy Status'));
|
||||
assert(result.includes('Installed Servers'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle npm install failures', async () => {
|
||||
execStub.callsArgWith(2, new Error('npm install failed'));
|
||||
|
||||
try {
|
||||
await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: '@modelcontextprotocol/server-test'
|
||||
});
|
||||
assert.fail('Should have thrown');
|
||||
} catch (error: any) {
|
||||
assert(error.message.includes('Failed to install'));
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing required parameters', async () => {
|
||||
let result = await mcpTool.handler({ action: 'install' });
|
||||
assert(result.includes('Package name required'));
|
||||
|
||||
result = await mcpTool.handler({ action: 'call' });
|
||||
assert(result.includes('Tool name required'));
|
||||
});
|
||||
|
||||
it('should handle unknown actions gracefully', async () => {
|
||||
const result = await mcpTool.handler({ action: 'unknown' });
|
||||
assert(result.includes('Unknown action'));
|
||||
});
|
||||
|
||||
it('should handle server connection failures', async () => {
|
||||
const spawnStub = sandbox.stub(cp, 'spawn');
|
||||
spawnStub.throws(new Error('Failed to start server'));
|
||||
|
||||
globalState.set('mcp-installed-servers', [{
|
||||
name: 'test',
|
||||
installed: true,
|
||||
capabilities: { tools: ['test_tool'] }
|
||||
}]);
|
||||
|
||||
try {
|
||||
await mcpTool.handler({
|
||||
action: 'call',
|
||||
tool: 'test_tool'
|
||||
});
|
||||
assert.fail('Should have thrown');
|
||||
} catch (error: any) {
|
||||
assert(error.message.includes('Failed to start server'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Package Name Extraction', () => {
|
||||
const testCases = [
|
||||
['@modelcontextprotocol/server-github', 'github'],
|
||||
['@scope/server-test', 'test'],
|
||||
['mcp-server-git', 'git'],
|
||||
['simple-package', 'simple-package'],
|
||||
['server-sqlite', 'sqlite']
|
||||
];
|
||||
|
||||
testCases.forEach(([input, expected]) => {
|
||||
it(`should extract "${expected}" from "${input}"`, async () => {
|
||||
execStub.callsArgWith(2, null, '', '');
|
||||
fsStub.readFile.resolves(JSON.stringify({ version: '1.0.0' }));
|
||||
|
||||
const result = await mcpTool.handler({
|
||||
action: 'install',
|
||||
package: input
|
||||
});
|
||||
|
||||
const installedServers = globalState.get('mcp-installed-servers') || [];
|
||||
assert(installedServers.some((s: any) => s.name === expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user