mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Add CCW MCP server and tools integration
- Introduced `ccw-mcp` command for running CCW tools as an MCP server. - Updated `package.json` to include new MCP dependencies and scripts. - Enhanced CLI with new options for `codex_lens` tool. - Implemented MCP server logic to expose CCW tools via Model Context Protocol. - Added new tools and updated existing ones for better functionality and documentation. - Created quick start and full documentation for MCP server usage. - Added tests for MCP server functionality to ensure reliability.
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
# Development Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines project-specific coding standards and development principles.
|
||||
### CLI Tool Context Protocols
|
||||
For all CLI tool usage, command syntax, and integration guidelines:
|
||||
### Tool Context Protocols
|
||||
For all tool usage, command syntax, and integration guidelines:
|
||||
- **Tool Strategy**: @~/.claude/workflows/tool-strategy.md
|
||||
- **Intelligent Context Strategy**: @~/.claude/workflows/intelligent-tools-strategy.md
|
||||
- **Context Search Commands**: @~/.claude/workflows/context-search-strategy.md
|
||||
|
||||
@@ -10,107 +10,61 @@
|
||||
- Complex API research → Exa Code Context
|
||||
- Real-time information needs → Exa Web Search
|
||||
|
||||
## ⚡ CCW Tool Execution
|
||||
## ⚡ CCW MCP Tools
|
||||
|
||||
### General Usage (JSON Parameters)
|
||||
**优先使用 MCP 工具** (无需 Shell 转义,直接 JSON 参数)
|
||||
|
||||
```bash
|
||||
ccw tool exec <tool_name> '{"param": "value"}'
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
ccw tool exec get_modules_by_depth '{}'
|
||||
ccw tool exec classify_folders '{"path": "./src"}'
|
||||
```
|
||||
|
||||
**Available Tools**: `ccw tool list`
|
||||
|
||||
### edit_file Tool
|
||||
### edit_file
|
||||
|
||||
**When to Use**: Edit tool fails 1+ times on same file
|
||||
|
||||
```bash
|
||||
# Basic edit
|
||||
ccw tool exec edit_file --path "file.py" --old "old code" --new "new code"
|
||||
|
||||
# Preview without modifying (dry run)
|
||||
ccw tool exec edit_file --path "file.py" --old "old" --new "new" --dry-run
|
||||
|
||||
# Replace all occurrences
|
||||
ccw tool exec edit_file --path "file.py" --old "old" --new "new" --replace-all
|
||||
|
||||
# Line mode - insert after line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation insert_after --line 10 --text "new line"
|
||||
|
||||
# Line mode - insert before line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation insert_before --line 5 --text "new line"
|
||||
|
||||
# Line mode - replace line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation replace --line 3 --text "replacement"
|
||||
|
||||
# Line mode - delete line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation delete --line 3
|
||||
```
|
||||
mcp__ccw-tools__edit_file(path="file.py", oldText="old", newText="new")
|
||||
mcp__ccw-tools__edit_file(path="file.py", oldText="old", newText="new", dryRun=true)
|
||||
mcp__ccw-tools__edit_file(path="file.py", oldText="old", newText="new", replaceAll=true)
|
||||
mcp__ccw-tools__edit_file(path="file.py", mode="line", operation="insert_after", line=10, text="new line")
|
||||
```
|
||||
|
||||
**Parameters**: `--path`*, `--old`, `--new`, `--dry-run`, `--replace-all`, `--mode` (update|line), `--operation`, `--line`, `--text`
|
||||
**Options**: `dryRun` (preview diff), `replaceAll`, `mode` (update|line), `operation`, `line`, `text`
|
||||
|
||||
### write_file Tool
|
||||
### write_file
|
||||
|
||||
**When to Use**: Create new files or overwrite existing content
|
||||
|
||||
```bash
|
||||
# Basic write
|
||||
ccw tool exec write_file --path "file.txt" --content "Hello"
|
||||
|
||||
# With backup
|
||||
ccw tool exec write_file --path "file.txt" --content "new content" --backup
|
||||
|
||||
# Create directories if needed
|
||||
ccw tool exec write_file --path "new/path/file.txt" --content "content" --create-directories
|
||||
```
|
||||
mcp__ccw-tools__write_file(path="file.txt", content="Hello")
|
||||
mcp__ccw-tools__write_file(path="file.txt", content="code with `backticks` and ${vars}", backup=true)
|
||||
```
|
||||
|
||||
**Parameters**: `--path`*, `--content`*, `--create-directories`, `--backup`, `--encoding`
|
||||
**Options**: `backup`, `createDirectories`, `encoding`
|
||||
|
||||
### codex_lens
|
||||
|
||||
**When to Use**: Code indexing and semantic search
|
||||
|
||||
```
|
||||
mcp__ccw-tools__codex_lens(action="init", path=".")
|
||||
mcp__ccw-tools__codex_lens(action="search", query="function main", path=".")
|
||||
mcp__ccw-tools__codex_lens(action="search_files", query="pattern", limit=20)
|
||||
mcp__ccw-tools__codex_lens(action="symbol", file="src/main.py")
|
||||
```
|
||||
|
||||
**Actions**: `init`, `search`, `search_files`, `symbol`, `status`, `update`
|
||||
|
||||
### smart_search
|
||||
|
||||
**When to Use**: Quick search without indexing, natural language queries
|
||||
|
||||
```
|
||||
mcp__ccw-tools__smart_search(query="function main", path=".")
|
||||
mcp__ccw-tools__smart_search(query="def init", mode="exact")
|
||||
mcp__ccw-tools__smart_search(query="authentication logic", mode="semantic")
|
||||
```
|
||||
|
||||
**Modes**: `auto` (default), `exact`, `fuzzy`, `semantic`, `graph`
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
1. **Edit fails 1+ times** → `ccw tool exec edit_file`
|
||||
2. **Still fails** → `ccw tool exec write_file`
|
||||
1. **Edit fails 1+ times** → `mcp__ccw-tools__edit_file`
|
||||
2. **Still fails** → `mcp__ccw-tools__write_file`
|
||||
|
||||
## ⚡ sed Line Operations (Line Mode Alternative)
|
||||
|
||||
**When to Use**: Precise line number control (insert, delete, replace specific lines)
|
||||
|
||||
### Common Operations
|
||||
|
||||
```bash
|
||||
# Insert after line 10
|
||||
sed -i '10a\new line content' file.txt
|
||||
|
||||
# Insert before line 5
|
||||
sed -i '5i\new line content' file.txt
|
||||
|
||||
# Delete line 3
|
||||
sed -i '3d' file.txt
|
||||
|
||||
# Delete lines 5-8
|
||||
sed -i '5,8d' file.txt
|
||||
|
||||
# Replace line 3 content
|
||||
sed -i '3c\replacement line' file.txt
|
||||
|
||||
# Replace lines 3-5 content
|
||||
sed -i '3,5c\single replacement line' file.txt
|
||||
```
|
||||
|
||||
### Operation Reference
|
||||
|
||||
| Operation | Command | Example |
|
||||
|-----------|---------|---------|
|
||||
| Insert after | `Na\text` | `sed -i '10a\new' file` |
|
||||
| Insert before | `Ni\text` | `sed -i '5i\new' file` |
|
||||
| Delete line | `Nd` | `sed -i '3d' file` |
|
||||
| Delete range | `N,Md` | `sed -i '5,8d' file` |
|
||||
| Replace line | `Nc\text` | `sed -i '3c\new' file` |
|
||||
|
||||
**Note**: Use `sed -i` for in-place file modification (works in Git Bash on Windows)
|
||||
|
||||
61
ccw/MCP_QUICKSTART.md
Normal file
61
ccw/MCP_QUICKSTART.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# MCP Server Quick Start
|
||||
|
||||
This is a quick reference for using CCW as an MCP server with Claude Desktop.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
1. Ensure CCW is installed:
|
||||
```bash
|
||||
npm install -g ccw
|
||||
```
|
||||
|
||||
2. Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ccw-tools": {
|
||||
"command": "ccw-mcp",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Restart Claude Desktop
|
||||
|
||||
## Available Tools
|
||||
|
||||
Once configured, Claude Desktop can use these CCW tools:
|
||||
|
||||
- **File Operations**: `edit_file`, `write_file`
|
||||
- **Code Analysis**: `codex_lens`, `smart_search`, `get_modules_by_depth`, `classify_folders`
|
||||
- **Git Integration**: `detect_changed_modules`
|
||||
- **Session Management**: `session_manager`
|
||||
- **UI/Design**: `discover_design_files`, `ui_generate_preview`, `convert_tokens_to_css`
|
||||
- **Documentation**: `generate_module_docs`, `update_module_claude`
|
||||
|
||||
## Example Usage in Claude Desktop
|
||||
|
||||
```
|
||||
"Use edit_file to update the version in package.json"
|
||||
|
||||
"Use codex_lens to analyze the authentication flow"
|
||||
|
||||
"Use get_modules_by_depth to show me the project structure"
|
||||
```
|
||||
|
||||
## Full Documentation
|
||||
|
||||
See [MCP_SERVER.md](./MCP_SERVER.md) for complete documentation including:
|
||||
- Detailed tool descriptions
|
||||
- Configuration options
|
||||
- Troubleshooting guide
|
||||
- Development guidelines
|
||||
|
||||
## Testing
|
||||
|
||||
Run MCP server tests:
|
||||
```bash
|
||||
npm run test:mcp
|
||||
```
|
||||
149
ccw/MCP_SERVER.md
Normal file
149
ccw/MCP_SERVER.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# CCW MCP Server
|
||||
|
||||
The CCW MCP Server exposes CCW tools through the Model Context Protocol, allowing Claude Desktop and other MCP clients to access CCW functionality.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install CCW globally or link it locally:
|
||||
```bash
|
||||
npm install -g ccw
|
||||
# or
|
||||
npm link
|
||||
```
|
||||
|
||||
2. The MCP server executable is available as `ccw-mcp`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Claude Desktop Configuration
|
||||
|
||||
Add this to your Claude Desktop MCP settings file:
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ccw-tools": {
|
||||
"command": "ccw-mcp",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If CCW is not installed globally, use the full path:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ccw-tools": {
|
||||
"command": "node",
|
||||
"args": ["/full/path/to/ccw/bin/ccw-mcp.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Restart Claude Desktop
|
||||
|
||||
After updating the configuration, restart Claude Desktop for the changes to take effect.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server exposes the following CCW tools:
|
||||
|
||||
### File Operations
|
||||
- **edit_file** - Edit files with update or line mode
|
||||
- **write_file** - Create or overwrite files
|
||||
|
||||
### Code Analysis
|
||||
- **codex_lens** - Analyze code execution flow
|
||||
- **get_modules_by_depth** - Get module hierarchy by depth
|
||||
- **classify_folders** - Classify project folders
|
||||
- **detect_changed_modules** - Detect modules with git changes
|
||||
- **smart_search** - Intelligent code search
|
||||
|
||||
### Session Management
|
||||
- **session_manager** - Manage workflow sessions
|
||||
|
||||
### UI/Design Tools
|
||||
- **discover_design_files** - Find design-related files
|
||||
- **ui_generate_preview** - Generate UI previews
|
||||
- **ui_instantiate_prototypes** - Create UI prototypes
|
||||
- **convert_tokens_to_css** - Convert design tokens to CSS
|
||||
|
||||
### Documentation
|
||||
- **generate_module_docs** - Generate module documentation
|
||||
- **update_module_claude** - Update CLAUDE.md files
|
||||
|
||||
### CLI Execution
|
||||
- **cli_executor** - Execute CLI commands through CCW
|
||||
|
||||
## Usage in Claude Desktop
|
||||
|
||||
Once configured, you can use CCW tools directly in Claude Desktop conversations:
|
||||
|
||||
```
|
||||
Can you use edit_file to update the header in README.md?
|
||||
|
||||
Use codex_lens to analyze the authentication flow in src/auth/login.js
|
||||
|
||||
Get the module structure with get_modules_by_depth
|
||||
```
|
||||
|
||||
## Testing the Server
|
||||
|
||||
You can test the MCP server is working by checking the logs in Claude Desktop:
|
||||
|
||||
1. Open Claude Desktop
|
||||
2. Check Developer Tools (Help → Developer Tools)
|
||||
3. Look for `ccw-tools v6.1.4 started` message
|
||||
4. Check Console for any errors
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server not starting
|
||||
- Verify `ccw-mcp` is in your PATH or use full path in config
|
||||
- Check Node.js version (requires >= 16.0.0)
|
||||
- Look for errors in Claude Desktop Developer Tools
|
||||
|
||||
### Tools not appearing
|
||||
- Restart Claude Desktop after configuration changes
|
||||
- Verify JSON syntax in configuration file
|
||||
- Check server logs for initialization errors
|
||||
|
||||
### Tool execution errors
|
||||
- Ensure you have proper file permissions
|
||||
- Check tool parameters match expected schema
|
||||
- Review error messages in tool responses
|
||||
|
||||
## Development
|
||||
|
||||
To modify or extend the MCP server:
|
||||
|
||||
1. Edit `ccw/src/mcp-server/index.js` for server logic
|
||||
2. Add/modify tools in `ccw/src/tools/`
|
||||
3. Register new tools in `ccw/src/tools/index.js`
|
||||
4. Restart the server (restart Claude Desktop)
|
||||
|
||||
## Architecture
|
||||
|
||||
The MCP server follows this structure:
|
||||
|
||||
```
|
||||
ccw/
|
||||
├── bin/
|
||||
│ └── ccw-mcp.js # Executable entry point
|
||||
├── src/
|
||||
│ ├── mcp-server/
|
||||
│ │ └── index.js # MCP server implementation
|
||||
│ └── tools/
|
||||
│ ├── index.js # Tool registry
|
||||
│ ├── edit-file.js # Individual tool implementations
|
||||
│ ├── write-file.js
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
The server uses the `@modelcontextprotocol/sdk` to implement the MCP protocol over stdio transport.
|
||||
7
ccw/bin/ccw-mcp.js
Normal file
7
ccw/bin/ccw-mcp.js
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CCW MCP Server Executable
|
||||
* Entry point for running CCW tools as an MCP server
|
||||
*/
|
||||
|
||||
import '../src/mcp-server/index.js';
|
||||
984
ccw/package-lock.json
generated
984
ccw/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,13 @@
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"ccw": "./bin/ccw.js"
|
||||
"ccw": "./bin/ccw.js",
|
||||
"ccw-mcp": "./bin/ccw-mcp.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test tests/*.test.js",
|
||||
"test:codexlens": "node --test tests/codex-lens*.test.js",
|
||||
"test:mcp": "node --test tests/mcp-server.test.js",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -25,6 +27,7 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"boxen": "^7.1.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.0.0",
|
||||
|
||||
@@ -115,6 +115,12 @@ export function run(argv) {
|
||||
.option('--path <path>', 'File path (for edit_file)')
|
||||
.option('--old <text>', 'Old text to replace (for edit_file)')
|
||||
.option('--new <text>', 'New text (for edit_file)')
|
||||
.option('--action <action>', 'Action to perform (for codex_lens)')
|
||||
.option('--query <query>', 'Search query (for codex_lens)')
|
||||
.option('--limit <n>', 'Max results (for codex_lens)', '20')
|
||||
.option('--file <file>', 'File path for symbol extraction (for codex_lens)')
|
||||
.option('--files <files>', 'Comma-separated file paths (for codex_lens update)')
|
||||
.option('--languages <langs>', 'Comma-separated languages (for codex_lens init)')
|
||||
.action((subcommand, args, options) => toolCommand(subcommand, args, options));
|
||||
|
||||
// Session command
|
||||
|
||||
@@ -70,13 +70,14 @@ async function schemaAction(options) {
|
||||
* Execute a tool with given parameters
|
||||
* @param {string} toolName - Tool name
|
||||
* @param {string|undefined} jsonParams - JSON string of parameters
|
||||
* @param {Object} options - CLI options (--path, --old, --new for edit_file)
|
||||
* @param {Object} options - CLI options
|
||||
*/
|
||||
async function execAction(toolName, jsonParams, options) {
|
||||
if (!toolName) {
|
||||
console.error(chalk.red('Tool name is required'));
|
||||
console.error(chalk.gray('Usage: ccw tool exec <tool_name> \'{"param": "value"}\''));
|
||||
console.error(chalk.gray(' ccw tool exec edit_file --path file.txt --old "old" --new "new"'));
|
||||
console.error(chalk.gray(' ccw tool exec codex_lens --action search --query "pattern"'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ async function execAction(toolName, jsonParams, options) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (toolName === 'edit_file') {
|
||||
// Legacy support for edit_file with --path, --old, --new options
|
||||
// Parameter mode for edit_file
|
||||
if (!options.path || !options.old || !options.new) {
|
||||
console.error(chalk.red('edit_file requires --path, --old, and --new parameters'));
|
||||
console.error(chalk.gray('Usage: ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
|
||||
@@ -109,8 +110,23 @@ async function execAction(toolName, jsonParams, options) {
|
||||
params.path = options.path;
|
||||
params.oldText = options.old;
|
||||
params.newText = options.new;
|
||||
} else if (toolName === 'codex_lens') {
|
||||
// Parameter mode for codex_lens
|
||||
if (!options.action) {
|
||||
console.error(chalk.red('codex_lens requires --action parameter'));
|
||||
console.error(chalk.gray('Usage: ccw tool exec codex_lens --action search --query "pattern" --path .'));
|
||||
console.error(chalk.gray('Actions: init, search, search_files, symbol, status, update, bootstrap, check'));
|
||||
process.exit(1);
|
||||
}
|
||||
params.action = options.action;
|
||||
if (options.path) params.path = options.path;
|
||||
if (options.query) params.query = options.query;
|
||||
if (options.limit) params.limit = parseInt(options.limit, 10);
|
||||
if (options.file) params.file = options.file;
|
||||
if (options.files) params.files = options.files.split(',').map(f => f.trim());
|
||||
if (options.languages) params.languages = options.languages.split(',').map(l => l.trim());
|
||||
} else if (jsonParams) {
|
||||
// Non-JSON string provided but not for edit_file
|
||||
// Non-JSON string provided but not for supported tools
|
||||
console.error(chalk.red('Parameters must be valid JSON'));
|
||||
console.error(chalk.gray(`Usage: ccw tool exec ${toolName} '{"param": "value"}'`));
|
||||
process.exit(1);
|
||||
@@ -157,5 +173,6 @@ export async function toolCommand(subcommand, args, options) {
|
||||
console.log(chalk.gray(' ccw tool schema edit_file'));
|
||||
console.log(chalk.gray(' ccw tool exec <tool_name> \'{"param": "value"}\''));
|
||||
console.log(chalk.gray(' ccw tool exec edit_file --path file.txt --old "old text" --new "new text"'));
|
||||
console.log(chalk.gray(' ccw tool exec codex_lens --action search --query "def main" --path .'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,26 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Install CCW MCP server to project
|
||||
if (pathname === '/api/mcp-install-ccw' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { projectPath } = body;
|
||||
if (!projectPath) {
|
||||
return { error: 'projectPath is required', status: 400 };
|
||||
}
|
||||
|
||||
// Generate CCW MCP server config
|
||||
const ccwMcpConfig = {
|
||||
command: "ccw-mcp",
|
||||
args: []
|
||||
};
|
||||
|
||||
// Use existing addMcpServerToProject to install CCW MCP
|
||||
return addMcpServerToProject(projectPath, 'ccw-mcp', ccwMcpConfig);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Remove MCP server from project
|
||||
if (pathname === '/api/mcp-remove-server' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
|
||||
160
ccw/src/mcp-server/index.js
Normal file
160
ccw/src/mcp-server/index.js
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CCW MCP Server
|
||||
* Exposes CCW tools through the Model Context Protocol
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { getAllToolSchemas, executeTool } from '../tools/index.js';
|
||||
|
||||
const SERVER_NAME = 'ccw-tools';
|
||||
const SERVER_VERSION = '6.1.4';
|
||||
|
||||
// Default enabled tools (core set)
|
||||
const DEFAULT_TOOLS = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
||||
|
||||
/**
|
||||
* Get list of enabled tools from environment or defaults
|
||||
*/
|
||||
function getEnabledTools() {
|
||||
const envTools = process.env.CCW_ENABLED_TOOLS;
|
||||
if (envTools) {
|
||||
// Support "all" to enable all tools
|
||||
if (envTools.toLowerCase() === 'all') {
|
||||
return null; // null means all tools
|
||||
}
|
||||
return envTools.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
return DEFAULT_TOOLS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tools based on enabled list
|
||||
*/
|
||||
function filterTools(tools, enabledList) {
|
||||
if (!enabledList) return tools; // null = all tools
|
||||
return tools.filter(tool => enabledList.includes(tool.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure the MCP server
|
||||
*/
|
||||
function createServer() {
|
||||
const enabledTools = getEnabledTools();
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: SERVER_NAME,
|
||||
version: SERVER_VERSION,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Handler for tools/list - Returns enabled CCW tools
|
||||
*/
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const allTools = getAllToolSchemas();
|
||||
const tools = filterTools(allTools, enabledTools);
|
||||
return { tools };
|
||||
});
|
||||
|
||||
/**
|
||||
* Handler for tools/call - Executes a CCW tool
|
||||
*/
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// Check if tool is enabled
|
||||
if (enabledTools && !enabledTools.includes(name)) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Tool "${name}" is not enabled` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeTool(name, args || {});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formatToolResult(result.result) }],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Tool execution failed: ${error.message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tool result for display
|
||||
* @param {*} result - Tool execution result
|
||||
* @returns {string} - Formatted result string
|
||||
*/
|
||||
function formatToolResult(result) {
|
||||
if (result === null || result === undefined) {
|
||||
return 'Tool completed successfully (no output)';
|
||||
}
|
||||
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof result === 'object') {
|
||||
// Pretty print JSON with indentation
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
return String(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main server execution
|
||||
*/
|
||||
async function main() {
|
||||
const server = createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
|
||||
// Connect server to transport
|
||||
await server.connect(transport);
|
||||
|
||||
// Error handling
|
||||
process.on('SIGINT', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Log server start (to stderr to not interfere with stdio protocol)
|
||||
console.error(`${SERVER_NAME} v${SERVER_VERSION} started`);
|
||||
}
|
||||
|
||||
// Run server
|
||||
main().catch((error) => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -579,20 +579,66 @@ async function createMcpServerWithConfig(name, serverConfig) {
|
||||
showRefreshToast(`Failed to create MCP server: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CCW Tools MCP Installation ==========
|
||||
async function installCcwToolsMcp() {
|
||||
// Define CCW Tools MCP server configuration
|
||||
// Use npx for better cross-platform compatibility (handles PATH issues)
|
||||
const ccwToolsConfig = {
|
||||
|
||||
// Get selected tools from checkboxes
|
||||
function getSelectedCcwTools() {
|
||||
const checkboxes = document.querySelectorAll('.ccw-tool-checkbox:checked');
|
||||
return Array.from(checkboxes).map(cb => cb.dataset.tool);
|
||||
}
|
||||
|
||||
// Select tools by category
|
||||
function selectCcwTools(type) {
|
||||
const checkboxes = document.querySelectorAll('.ccw-tool-checkbox');
|
||||
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
if (type === 'all') {
|
||||
cb.checked = true;
|
||||
} else if (type === 'none') {
|
||||
cb.checked = false;
|
||||
} else if (type === 'core') {
|
||||
cb.checked = coreTools.includes(cb.dataset.tool);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build CCW Tools config with selected tools
|
||||
function buildCcwToolsConfig(selectedTools) {
|
||||
const config = {
|
||||
command: "npx",
|
||||
args: ["-y", "ccw-mcp"]
|
||||
};
|
||||
|
||||
// Add env if not all tools or not default 4 core tools
|
||||
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
||||
const isDefault = selectedTools.length === 4 &&
|
||||
coreTools.every(t => selectedTools.includes(t)) &&
|
||||
selectedTools.every(t => coreTools.includes(t));
|
||||
|
||||
if (selectedTools.length === 15) {
|
||||
config.env = { CCW_ENABLED_TOOLS: 'all' };
|
||||
} else if (!isDefault && selectedTools.length > 0) {
|
||||
config.env = { CCW_ENABLED_TOOLS: selectedTools.join(',') };
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function installCcwToolsMcp() {
|
||||
const selectedTools = getSelectedCcwTools();
|
||||
|
||||
if (selectedTools.length === 0) {
|
||||
showRefreshToast('Please select at least one tool', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
|
||||
try {
|
||||
// Show loading toast
|
||||
showRefreshToast('Installing CCW Tools MCP...', 'info');
|
||||
|
||||
// Use the existing copyMcpServerToProject function
|
||||
const response = await fetch('/api/mcp-copy-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -609,7 +655,7 @@ async function installCcwToolsMcp() {
|
||||
if (result.success) {
|
||||
await loadMcpConfig();
|
||||
renderMcpManager();
|
||||
showRefreshToast('CCW Tools MCP installed successfully', 'success');
|
||||
showRefreshToast(`CCW Tools installed (${selectedTools.length} tools)`, 'success');
|
||||
} else {
|
||||
showRefreshToast(result.error || 'Failed to install CCW Tools MCP', 'error');
|
||||
}
|
||||
@@ -618,3 +664,42 @@ async function installCcwToolsMcp() {
|
||||
showRefreshToast(`Failed to install CCW Tools MCP: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCcwToolsMcp() {
|
||||
const selectedTools = getSelectedCcwTools();
|
||||
|
||||
if (selectedTools.length === 0) {
|
||||
showRefreshToast('Please select at least one tool', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
|
||||
try {
|
||||
showRefreshToast('Updating CCW Tools MCP...', 'info');
|
||||
|
||||
const response = await fetch('/api/mcp-copy-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
projectPath: projectPath,
|
||||
serverName: 'ccw-tools',
|
||||
serverConfig: ccwToolsConfig
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update CCW Tools MCP');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
await loadMcpConfig();
|
||||
renderMcpManager();
|
||||
showRefreshToast(`CCW Tools updated (${selectedTools.length} tools)`, 'success');
|
||||
} else {
|
||||
showRefreshToast(result.error || 'Failed to update CCW Tools MCP', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update CCW Tools MCP:', err);
|
||||
showRefreshToast(`Failed to update CCW Tools MCP: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
// MCP Manager View
|
||||
// Renders the MCP server management interface
|
||||
|
||||
// CCW Tools available for MCP
|
||||
const CCW_MCP_TOOLS = [
|
||||
// Core tools (always recommended)
|
||||
{ name: 'write_file', desc: 'Write/create files', core: true },
|
||||
{ name: 'edit_file', desc: 'Edit/replace content', core: true },
|
||||
{ name: 'codex_lens', desc: 'Code index & search', core: true },
|
||||
{ name: 'smart_search', desc: 'Quick regex/NL search', core: true },
|
||||
// Optional tools
|
||||
{ name: 'session_manager', desc: 'Workflow sessions', core: false },
|
||||
{ name: 'generate_module_docs', desc: 'Generate docs', core: false },
|
||||
{ name: 'update_module_claude', desc: 'Update CLAUDE.md', core: false },
|
||||
{ name: 'cli_executor', desc: 'Gemini/Qwen/Codex CLI', core: false },
|
||||
];
|
||||
|
||||
// Get currently enabled tools from installed config
|
||||
function getCcwEnabledTools() {
|
||||
const currentPath = projectPath.replace(/\//g, '\\');
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
|
||||
if (ccwConfig?.env?.CCW_ENABLED_TOOLS) {
|
||||
const val = ccwConfig.env.CCW_ENABLED_TOOLS;
|
||||
if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name);
|
||||
return val.split(',').map(t => t.trim());
|
||||
}
|
||||
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
|
||||
}
|
||||
|
||||
async function renderMcpManager() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
@@ -36,7 +63,7 @@ async function renderMcpManager() {
|
||||
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
||||
// Check if CCW Tools is already installed
|
||||
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
|
||||
|
||||
const enabledTools = getCcwEnabledTools();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="mcp-manager">
|
||||
@@ -54,7 +81,7 @@ async function renderMcpManager() {
|
||||
${isCcwToolsInstalled ? `
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success">
|
||||
<i data-lucide="check" class="w-3 h-3"></i>
|
||||
Installed
|
||||
${enabledTools.length} tools
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary/20 text-primary">
|
||||
@@ -63,36 +90,35 @@ async function renderMcpManager() {
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mb-3">
|
||||
CCW built-in tools for file editing, code search, session management, and more
|
||||
</p>
|
||||
<div class="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="layers" class="w-3 h-3"></i>
|
||||
15 tools available
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-3 h-3"></i>
|
||||
Native integration
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="shield-check" class="w-3 h-3"></i>
|
||||
Built-in & tested
|
||||
</span>
|
||||
<!-- Tool Selection Grid -->
|
||||
<div class="grid grid-cols-3 sm:grid-cols-5 gap-2 mb-3">
|
||||
${CCW_MCP_TOOLS.map(tool => `
|
||||
<label class="flex items-center gap-1.5 text-xs cursor-pointer hover:bg-muted/50 rounded px-1.5 py-1 transition-colors">
|
||||
<input type="checkbox" class="ccw-tool-checkbox w-3 h-3"
|
||||
data-tool="${tool.name}"
|
||||
${enabledTools.includes(tool.name) ? 'checked' : ''}>
|
||||
<span class="${tool.core ? 'font-medium' : 'text-muted-foreground'}">${tool.desc}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<button class="text-primary hover:underline" onclick="selectCcwTools('core')">Core only</button>
|
||||
<button class="text-primary hover:underline" onclick="selectCcwTools('all')">All</button>
|
||||
<button class="text-muted-foreground hover:underline" onclick="selectCcwTools('none')">None</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
${isCcwToolsInstalled ? `
|
||||
<button class="px-4 py-2 text-sm bg-muted text-muted-foreground rounded-lg cursor-not-allowed" disabled>
|
||||
<i data-lucide="check" class="w-4 h-4 inline mr-1"></i>
|
||||
Installed
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="updateCcwToolsMcp()">
|
||||
Update
|
||||
</button>
|
||||
` : `
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="installCcwToolsMcp()">
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
Install CCW Tools
|
||||
Install
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -383,6 +383,30 @@ async function searchCode(params) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search code and return only file paths
|
||||
* @param {Object} params - Search parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function searchFiles(params) {
|
||||
const { query, path = '.', limit = 20 } = params;
|
||||
|
||||
const args = ['search', query, '--files-only', '--limit', limit.toString(), '--json'];
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
result.files = JSON.parse(result.output);
|
||||
delete result.output;
|
||||
} catch {
|
||||
// Keep raw output if JSON parse fails
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract symbols from a file
|
||||
* @param {Object} params - Parameters
|
||||
@@ -474,6 +498,9 @@ async function execute(params) {
|
||||
case 'search':
|
||||
return searchCode(rest);
|
||||
|
||||
case 'search_files':
|
||||
return searchFiles(rest);
|
||||
|
||||
case 'symbol':
|
||||
return extractSymbols(rest);
|
||||
|
||||
@@ -497,7 +524,7 @@ async function execute(params) {
|
||||
return checkVenvStatus();
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}. Valid actions: init, search, symbol, status, update, bootstrap, check`);
|
||||
throw new Error(`Unknown action: ${action}. Valid actions: init, search, search_files, symbol, status, update, bootstrap, check`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,38 +533,30 @@ async function execute(params) {
|
||||
*/
|
||||
export const codexLensTool = {
|
||||
name: 'codex_lens',
|
||||
description: `Code indexing and semantic search via CodexLens Python package.
|
||||
description: `Code indexing and search.
|
||||
|
||||
Actions:
|
||||
- init: Initialize index for a directory
|
||||
- search: Search code (text or semantic mode)
|
||||
- symbol: Extract symbols from a file
|
||||
- status: Get index status
|
||||
- update: Incrementally update specific files (add/modify/remove)
|
||||
- bootstrap: Force re-install CodexLens venv
|
||||
- check: Check venv readiness
|
||||
|
||||
Features:
|
||||
- Automatic venv bootstrap at ~/.codexlens/venv
|
||||
- SQLite FTS5 full-text search
|
||||
- Tree-sitter symbol extraction
|
||||
- Incremental updates for changed files
|
||||
- Optional semantic search with embeddings`,
|
||||
Usage:
|
||||
codex_lens(action="init", path=".") # Index directory
|
||||
codex_lens(action="search", query="func", path=".") # Search code
|
||||
codex_lens(action="search_files", query="x") # Search, return paths only
|
||||
codex_lens(action="symbol", file="f.py") # Extract symbols
|
||||
codex_lens(action="status") # Index status
|
||||
codex_lens(action="update", files=["a.js"]) # Update specific files`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['init', 'search', 'symbol', 'status', 'update', 'bootstrap', 'check'],
|
||||
enum: ['init', 'search', 'search_files', 'symbol', 'status', 'update', 'bootstrap', 'check'],
|
||||
description: 'Action to perform'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Target path (for init, search, status, update)'
|
||||
description: 'Target path (for init, search, search_files, status, update)'
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (for search action)'
|
||||
description: 'Search query (for search and search_files actions)'
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
@@ -561,7 +580,7 @@ Features:
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum results (for search action)',
|
||||
description: 'Maximum results (for search and search_files actions)',
|
||||
default: 20
|
||||
},
|
||||
format: {
|
||||
|
||||
@@ -391,14 +391,13 @@ async function execute(params) {
|
||||
*/
|
||||
export const editFileTool = {
|
||||
name: 'edit_file',
|
||||
description: `Edit file with two modes:
|
||||
- update: Replace oldText with newText (default). Supports multiple edits via 'edits' array.
|
||||
- line: Position-driven line operations (insert_before, insert_after, replace, delete)
|
||||
description: `Edit file by text replacement or line operations.
|
||||
|
||||
Features:
|
||||
- dryRun: Preview changes without modifying file (returns diff)
|
||||
- Auto line ending adaptation (CRLF/LF)
|
||||
- Fuzzy matching for whitespace differences`,
|
||||
Usage:
|
||||
edit_file(path="f.js", oldText="old", newText="new")
|
||||
edit_file(path="f.js", mode="line", operation="insert_after", line=10, text="new line")
|
||||
|
||||
Options: dryRun=true (preview diff), replaceAll=true (replace all occurrences)`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -722,28 +722,15 @@ async function execute(params) {
|
||||
|
||||
export const sessionManagerTool = {
|
||||
name: 'session_manager',
|
||||
description: `Workflow session lifecycle management tool.
|
||||
|
||||
Operations:
|
||||
- init: Create new session with directory structure
|
||||
- list: List sessions (active, archived, or both)
|
||||
- read: Read file content by content_type
|
||||
- write: Write content to file by content_type
|
||||
- update: Update existing JSON file (shallow merge)
|
||||
- archive: Move session from active to archives
|
||||
- mkdir: Create directories within session
|
||||
- delete: Delete a file within session
|
||||
- stats: Get session statistics (tasks, summaries, plan)
|
||||
|
||||
Content Types:
|
||||
session, plan, task, summary, process, chat, brainstorm,
|
||||
review-dim, review-iter, review-fix, todo, context
|
||||
description: `Workflow session management.
|
||||
|
||||
Usage:
|
||||
ccw tool exec session_manager '{"operation":"list"}'
|
||||
ccw tool exec session_manager '{"operation":"init","session_id":"WFS-test"}'
|
||||
ccw tool exec session_manager '{"operation":"read","session_id":"WFS-test","content_type":"session"}'
|
||||
ccw tool exec session_manager '{"operation":"stats","session_id":"WFS-test"}'`,
|
||||
session_manager(operation="init", type="workflow", description="...")
|
||||
session_manager(operation="list", location="active|archived|both")
|
||||
session_manager(operation="read", sessionId="WFS-xxx", contentType="plan|task|summary")
|
||||
session_manager(operation="write", sessionId="WFS-xxx", contentType="plan", content={...})
|
||||
session_manager(operation="archive", sessionId="WFS-xxx")
|
||||
session_manager(operation="stats", sessionId="WFS-xxx")`,
|
||||
|
||||
parameters: {
|
||||
type: 'object',
|
||||
|
||||
@@ -577,19 +577,14 @@ async function execute(params) {
|
||||
*/
|
||||
export const smartSearchTool = {
|
||||
name: 'smart_search',
|
||||
description: `Unified search with intelligent mode selection.
|
||||
description: `Intelligent code search with multiple modes.
|
||||
|
||||
Modes:
|
||||
- auto: Classify intent and recommend optimal search mode (default)
|
||||
- exact: Precise literal matching via ripgrep
|
||||
- fuzzy: Approximate matching with typo tolerance
|
||||
- semantic: Natural language understanding via LLM/embeddings
|
||||
- graph: Dependency relationship traversal
|
||||
Usage:
|
||||
smart_search(query="function main", path=".") # Auto-select mode
|
||||
smart_search(query="def init", mode="exact") # Exact match
|
||||
smart_search(query="authentication logic", mode="semantic") # NL search
|
||||
|
||||
Features:
|
||||
- Multi-backend search coordination
|
||||
- Result fusion with RRF ranking
|
||||
- Configurable result limits and context`,
|
||||
Modes: auto (default), exact, fuzzy, semantic, graph`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -110,14 +110,10 @@ async function execute(params) {
|
||||
*/
|
||||
export const writeFileTool = {
|
||||
name: 'write_file',
|
||||
description: `Create a new file or overwrite an existing file with content.
|
||||
description: `Write content to file. Auto-creates parent directories.
|
||||
|
||||
Features:
|
||||
- Creates parent directories automatically (configurable)
|
||||
- Optional backup before overwrite
|
||||
- Supports text content with proper encoding
|
||||
|
||||
Use with caution as it will overwrite existing files without warning unless backup is enabled.`,
|
||||
Usage: write_file(path="file.js", content="code here")
|
||||
Options: backup=true (backup before overwrite), encoding="utf8"`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
159
ccw/tests/mcp-server.test.js
Normal file
159
ccw/tests/mcp-server.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Basic MCP server tests
|
||||
* Tests the MCP server functionality with mock requests
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('MCP Server', () => {
|
||||
let serverProcess;
|
||||
|
||||
before(async () => {
|
||||
// Start the MCP server
|
||||
const serverPath = join(__dirname, '../bin/ccw-mcp.js');
|
||||
serverProcess = spawn('node', [serverPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise((resolve) => {
|
||||
serverProcess.stderr.once('data', (data) => {
|
||||
const message = data.toString();
|
||||
if (message.includes('started')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
}
|
||||
});
|
||||
|
||||
it('should respond to tools/list request', async () => {
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/list',
|
||||
params: {}
|
||||
};
|
||||
|
||||
// Send request
|
||||
serverProcess.stdin.write(JSON.stringify(request) + '\n');
|
||||
|
||||
// Wait for response
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout'));
|
||||
}, 5000);
|
||||
|
||||
serverProcess.stdout.once('data', (data) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(response.jsonrpc, '2.0');
|
||||
assert.equal(response.id, 1);
|
||||
assert(response.result);
|
||||
assert(Array.isArray(response.result.tools));
|
||||
assert(response.result.tools.length > 0);
|
||||
|
||||
// Check that essential tools are present
|
||||
const toolNames = response.result.tools.map(t => t.name);
|
||||
assert(toolNames.includes('edit_file'));
|
||||
assert(toolNames.includes('write_file'));
|
||||
assert(toolNames.includes('codex_lens'));
|
||||
});
|
||||
|
||||
it('should respond to tools/call request', async () => {
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_modules_by_depth',
|
||||
arguments: {}
|
||||
}
|
||||
};
|
||||
|
||||
// Send request
|
||||
serverProcess.stdin.write(JSON.stringify(request) + '\n');
|
||||
|
||||
// Wait for response
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout'));
|
||||
}, 5000);
|
||||
|
||||
serverProcess.stdout.once('data', (data) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(response.jsonrpc, '2.0');
|
||||
assert.equal(response.id, 2);
|
||||
assert(response.result);
|
||||
assert(Array.isArray(response.result.content));
|
||||
assert(response.result.content.length > 0);
|
||||
assert.equal(response.result.content[0].type, 'text');
|
||||
});
|
||||
|
||||
it('should handle invalid tool name gracefully', async () => {
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'nonexistent_tool',
|
||||
arguments: {}
|
||||
}
|
||||
};
|
||||
|
||||
// Send request
|
||||
serverProcess.stdin.write(JSON.stringify(request) + '\n');
|
||||
|
||||
// Wait for response
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout'));
|
||||
}, 5000);
|
||||
|
||||
serverProcess.stdout.once('data', (data) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(response.jsonrpc, '2.0');
|
||||
assert.equal(response.id, 3);
|
||||
assert(response.result);
|
||||
assert.equal(response.result.isError, true);
|
||||
assert(response.result.content[0].text.includes('not found'));
|
||||
});
|
||||
});
|
||||
@@ -243,6 +243,7 @@ def init(
|
||||
def search(
|
||||
query: str = typer.Argument(..., help="FTS query to run."),
|
||||
limit: int = typer.Option(20, "--limit", "-n", min=1, max=500, help="Max results."),
|
||||
files_only: bool = typer.Option(False, "--files-only", "-f", help="Return only file paths without content snippets."),
|
||||
use_global: bool = typer.Option(False, "--global", "-g", help="Use global database instead of workspace-local."),
|
||||
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging."),
|
||||
@@ -251,6 +252,7 @@ def search(
|
||||
|
||||
Searches the workspace-local .codexlens/index.db by default.
|
||||
Use --global to search the global database at ~/.codexlens/.
|
||||
Use --files-only to return only matching file paths.
|
||||
"""
|
||||
_configure_logging(verbose)
|
||||
|
||||
@@ -258,12 +260,22 @@ def search(
|
||||
try:
|
||||
store, db_path = _get_store_for_path(Path.cwd(), use_global)
|
||||
store.initialize()
|
||||
results = store.search_fts(query, limit=limit)
|
||||
payload = {"query": query, "count": len(results), "results": results}
|
||||
if json_mode:
|
||||
print_json(success=True, result=payload)
|
||||
|
||||
if files_only:
|
||||
file_paths = store.search_files_only(query, limit=limit)
|
||||
payload = {"query": query, "count": len(file_paths), "files": file_paths}
|
||||
if json_mode:
|
||||
print_json(success=True, result=payload)
|
||||
else:
|
||||
for fp in file_paths:
|
||||
console.print(fp)
|
||||
else:
|
||||
render_search_results(results)
|
||||
results = store.search_fts(query, limit=limit)
|
||||
payload = {"query": query, "count": len(results), "results": results}
|
||||
if json_mode:
|
||||
print_json(success=True, result=payload)
|
||||
else:
|
||||
render_search_results(results)
|
||||
except Exception as exc:
|
||||
if json_mode:
|
||||
print_json(success=False, error=str(exc))
|
||||
|
||||
Reference in New Issue
Block a user