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
|
### Tool Context Protocols
|
||||||
|
For all tool usage, command syntax, and integration guidelines:
|
||||||
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 Strategy**: @~/.claude/workflows/tool-strategy.md
|
- **Tool Strategy**: @~/.claude/workflows/tool-strategy.md
|
||||||
- **Intelligent Context Strategy**: @~/.claude/workflows/intelligent-tools-strategy.md
|
- **Intelligent Context Strategy**: @~/.claude/workflows/intelligent-tools-strategy.md
|
||||||
- **Context Search Commands**: @~/.claude/workflows/context-search-strategy.md
|
- **Context Search Commands**: @~/.claude/workflows/context-search-strategy.md
|
||||||
|
|||||||
@@ -10,107 +10,61 @@
|
|||||||
- Complex API research → Exa Code Context
|
- Complex API research → Exa Code Context
|
||||||
- Real-time information needs → Exa Web Search
|
- Real-time information needs → Exa Web Search
|
||||||
|
|
||||||
## ⚡ CCW Tool Execution
|
## ⚡ CCW MCP Tools
|
||||||
|
|
||||||
### General Usage (JSON Parameters)
|
**优先使用 MCP 工具** (无需 Shell 转义,直接 JSON 参数)
|
||||||
|
|
||||||
```bash
|
### edit_file
|
||||||
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
|
|
||||||
|
|
||||||
**When to Use**: Edit tool fails 1+ times on same file
|
**When to Use**: Edit tool fails 1+ times on same file
|
||||||
|
|
||||||
```bash
|
```
|
||||||
# Basic edit
|
mcp__ccw-tools__edit_file(path="file.py", oldText="old", newText="new")
|
||||||
ccw tool exec edit_file --path "file.py" --old "old code" --new "new code"
|
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)
|
||||||
# Preview without modifying (dry run)
|
mcp__ccw-tools__edit_file(path="file.py", mode="line", operation="insert_after", line=10, text="new line")
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
**When to Use**: Create new files or overwrite existing content
|
||||||
|
|
||||||
```bash
|
```
|
||||||
# Basic write
|
mcp__ccw-tools__write_file(path="file.txt", content="Hello")
|
||||||
ccw tool exec write_file --path "file.txt" --content "Hello"
|
mcp__ccw-tools__write_file(path="file.txt", content="code with `backticks` and ${vars}", backup=true)
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
### Fallback Strategy
|
||||||
|
|
||||||
1. **Edit fails 1+ times** → `ccw tool exec edit_file`
|
1. **Edit fails 1+ times** → `mcp__ccw-tools__edit_file`
|
||||||
2. **Still fails** → `ccw tool exec write_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",
|
"type": "module",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ccw": "./bin/ccw.js"
|
"ccw": "./bin/ccw.js",
|
||||||
|
"ccw-mcp": "./bin/ccw-mcp.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test tests/*.test.js",
|
"test": "node --test tests/*.test.js",
|
||||||
"test:codexlens": "node --test tests/codex-lens*.test.js",
|
"test:codexlens": "node --test tests/codex-lens*.test.js",
|
||||||
|
"test:mcp": "node --test tests/mcp-server.test.js",
|
||||||
"lint": "eslint src/"
|
"lint": "eslint src/"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"boxen": "^7.1.0",
|
"boxen": "^7.1.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ export function run(argv) {
|
|||||||
.option('--path <path>', 'File path (for edit_file)')
|
.option('--path <path>', 'File path (for edit_file)')
|
||||||
.option('--old <text>', 'Old text to replace (for edit_file)')
|
.option('--old <text>', 'Old text to replace (for edit_file)')
|
||||||
.option('--new <text>', 'New text (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));
|
.action((subcommand, args, options) => toolCommand(subcommand, args, options));
|
||||||
|
|
||||||
// Session command
|
// Session command
|
||||||
|
|||||||
@@ -70,13 +70,14 @@ async function schemaAction(options) {
|
|||||||
* Execute a tool with given parameters
|
* Execute a tool with given parameters
|
||||||
* @param {string} toolName - Tool name
|
* @param {string} toolName - Tool name
|
||||||
* @param {string|undefined} jsonParams - JSON string of parameters
|
* @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) {
|
async function execAction(toolName, jsonParams, options) {
|
||||||
if (!toolName) {
|
if (!toolName) {
|
||||||
console.error(chalk.red('Tool name is required'));
|
console.error(chalk.red('Tool name is required'));
|
||||||
console.error(chalk.gray('Usage: ccw tool exec <tool_name> \'{"param": "value"}\''));
|
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 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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ async function execAction(toolName, jsonParams, options) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else if (toolName === 'edit_file') {
|
} 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) {
|
if (!options.path || !options.old || !options.new) {
|
||||||
console.error(chalk.red('edit_file requires --path, --old, and --new parameters'));
|
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"'));
|
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.path = options.path;
|
||||||
params.oldText = options.old;
|
params.oldText = options.old;
|
||||||
params.newText = options.new;
|
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) {
|
} 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.red('Parameters must be valid JSON'));
|
||||||
console.error(chalk.gray(`Usage: ccw tool exec ${toolName} '{"param": "value"}'`));
|
console.error(chalk.gray(`Usage: ccw tool exec ${toolName} '{"param": "value"}'`));
|
||||||
process.exit(1);
|
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 schema edit_file'));
|
||||||
console.log(chalk.gray(' ccw tool exec <tool_name> \'{"param": "value"}\''));
|
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 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;
|
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
|
// API: Remove MCP server from project
|
||||||
if (pathname === '/api/mcp-remove-server' && req.method === 'POST') {
|
if (pathname === '/api/mcp-remove-server' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
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');
|
showRefreshToast(`Failed to create MCP server: ${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== CCW Tools MCP Installation ==========
|
// ========== CCW Tools MCP Installation ==========
|
||||||
async function installCcwToolsMcp() {
|
|
||||||
// Define CCW Tools MCP server configuration
|
// Get selected tools from checkboxes
|
||||||
// Use npx for better cross-platform compatibility (handles PATH issues)
|
function getSelectedCcwTools() {
|
||||||
const ccwToolsConfig = {
|
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",
|
command: "npx",
|
||||||
args: ["-y", "ccw-mcp"]
|
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 {
|
try {
|
||||||
// Show loading toast
|
|
||||||
showRefreshToast('Installing CCW Tools MCP...', 'info');
|
showRefreshToast('Installing CCW Tools MCP...', 'info');
|
||||||
|
|
||||||
// Use the existing copyMcpServerToProject function
|
|
||||||
const response = await fetch('/api/mcp-copy-server', {
|
const response = await fetch('/api/mcp-copy-server', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -609,7 +655,7 @@ async function installCcwToolsMcp() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadMcpConfig();
|
await loadMcpConfig();
|
||||||
renderMcpManager();
|
renderMcpManager();
|
||||||
showRefreshToast('CCW Tools MCP installed successfully', 'success');
|
showRefreshToast(`CCW Tools installed (${selectedTools.length} tools)`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showRefreshToast(result.error || 'Failed to install CCW Tools MCP', 'error');
|
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');
|
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
|
// MCP Manager View
|
||||||
// Renders the MCP server management interface
|
// 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() {
|
async function renderMcpManager() {
|
||||||
const container = document.getElementById('mainContent');
|
const container = document.getElementById('mainContent');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -36,7 +63,7 @@ async function renderMcpManager() {
|
|||||||
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
||||||
// Check if CCW Tools is already installed
|
// Check if CCW Tools is already installed
|
||||||
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
|
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
|
||||||
|
const enabledTools = getCcwEnabledTools();
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="mcp-manager">
|
<div class="mcp-manager">
|
||||||
@@ -54,7 +81,7 @@ async function renderMcpManager() {
|
|||||||
${isCcwToolsInstalled ? `
|
${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">
|
<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>
|
<i data-lucide="check" class="w-3 h-3"></i>
|
||||||
Installed
|
${enabledTools.length} tools
|
||||||
</span>
|
</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">
|
<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>
|
</span>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground mb-3">
|
<!-- Tool Selection Grid -->
|
||||||
CCW built-in tools for file editing, code search, session management, and more
|
<div class="grid grid-cols-3 sm:grid-cols-5 gap-2 mb-3">
|
||||||
</p>
|
${CCW_MCP_TOOLS.map(tool => `
|
||||||
<div class="flex items-center gap-4 text-xs text-muted-foreground">
|
<label class="flex items-center gap-1.5 text-xs cursor-pointer hover:bg-muted/50 rounded px-1.5 py-1 transition-colors">
|
||||||
<span class="flex items-center gap-1">
|
<input type="checkbox" class="ccw-tool-checkbox w-3 h-3"
|
||||||
<i data-lucide="layers" class="w-3 h-3"></i>
|
data-tool="${tool.name}"
|
||||||
15 tools available
|
${enabledTools.includes(tool.name) ? 'checked' : ''}>
|
||||||
</span>
|
<span class="${tool.core ? 'font-medium' : 'text-muted-foreground'}">${tool.desc}</span>
|
||||||
<span class="flex items-center gap-1">
|
</label>
|
||||||
<i data-lucide="zap" class="w-3 h-3"></i>
|
`).join('')}
|
||||||
Native integration
|
</div>
|
||||||
</span>
|
<div class="flex items-center gap-3 text-xs">
|
||||||
<span class="flex items-center gap-1">
|
<button class="text-primary hover:underline" onclick="selectCcwTools('core')">Core only</button>
|
||||||
<i data-lucide="shield-check" class="w-3 h-3"></i>
|
<button class="text-primary hover:underline" onclick="selectCcwTools('all')">All</button>
|
||||||
Built-in & tested
|
<button class="text-muted-foreground hover:underline" onclick="selectCcwTools('none')">None</button>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
${isCcwToolsInstalled ? `
|
${isCcwToolsInstalled ? `
|
||||||
<button class="px-4 py-2 text-sm bg-muted text-muted-foreground rounded-lg cursor-not-allowed" disabled>
|
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||||
<i data-lucide="check" class="w-4 h-4 inline mr-1"></i>
|
onclick="updateCcwToolsMcp()">
|
||||||
Installed
|
Update
|
||||||
</button>
|
</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"
|
<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()">
|
onclick="installCcwToolsMcp()">
|
||||||
<i data-lucide="download" class="w-4 h-4"></i>
|
<i data-lucide="download" class="w-4 h-4"></i>
|
||||||
Install CCW Tools
|
Install
|
||||||
</button>
|
</button>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -383,6 +383,30 @@ async function searchCode(params) {
|
|||||||
return result;
|
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
|
* Extract symbols from a file
|
||||||
* @param {Object} params - Parameters
|
* @param {Object} params - Parameters
|
||||||
@@ -474,6 +498,9 @@ async function execute(params) {
|
|||||||
case 'search':
|
case 'search':
|
||||||
return searchCode(rest);
|
return searchCode(rest);
|
||||||
|
|
||||||
|
case 'search_files':
|
||||||
|
return searchFiles(rest);
|
||||||
|
|
||||||
case 'symbol':
|
case 'symbol':
|
||||||
return extractSymbols(rest);
|
return extractSymbols(rest);
|
||||||
|
|
||||||
@@ -497,7 +524,7 @@ async function execute(params) {
|
|||||||
return checkVenvStatus();
|
return checkVenvStatus();
|
||||||
|
|
||||||
default:
|
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 = {
|
export const codexLensTool = {
|
||||||
name: 'codex_lens',
|
name: 'codex_lens',
|
||||||
description: `Code indexing and semantic search via CodexLens Python package.
|
description: `Code indexing and search.
|
||||||
|
|
||||||
Actions:
|
Usage:
|
||||||
- init: Initialize index for a directory
|
codex_lens(action="init", path=".") # Index directory
|
||||||
- search: Search code (text or semantic mode)
|
codex_lens(action="search", query="func", path=".") # Search code
|
||||||
- symbol: Extract symbols from a file
|
codex_lens(action="search_files", query="x") # Search, return paths only
|
||||||
- status: Get index status
|
codex_lens(action="symbol", file="f.py") # Extract symbols
|
||||||
- update: Incrementally update specific files (add/modify/remove)
|
codex_lens(action="status") # Index status
|
||||||
- bootstrap: Force re-install CodexLens venv
|
codex_lens(action="update", files=["a.js"]) # Update specific files`,
|
||||||
- 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`,
|
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
action: {
|
action: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['init', 'search', 'symbol', 'status', 'update', 'bootstrap', 'check'],
|
enum: ['init', 'search', 'search_files', 'symbol', 'status', 'update', 'bootstrap', 'check'],
|
||||||
description: 'Action to perform'
|
description: 'Action to perform'
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Target path (for init, search, status, update)'
|
description: 'Target path (for init, search, search_files, status, update)'
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Search query (for search action)'
|
description: 'Search query (for search and search_files actions)'
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -561,7 +580,7 @@ Features:
|
|||||||
},
|
},
|
||||||
limit: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum results (for search action)',
|
description: 'Maximum results (for search and search_files actions)',
|
||||||
default: 20
|
default: 20
|
||||||
},
|
},
|
||||||
format: {
|
format: {
|
||||||
|
|||||||
@@ -391,14 +391,13 @@ async function execute(params) {
|
|||||||
*/
|
*/
|
||||||
export const editFileTool = {
|
export const editFileTool = {
|
||||||
name: 'edit_file',
|
name: 'edit_file',
|
||||||
description: `Edit file with two modes:
|
description: `Edit file by text replacement or line operations.
|
||||||
- update: Replace oldText with newText (default). Supports multiple edits via 'edits' array.
|
|
||||||
- line: Position-driven line operations (insert_before, insert_after, replace, delete)
|
|
||||||
|
|
||||||
Features:
|
Usage:
|
||||||
- dryRun: Preview changes without modifying file (returns diff)
|
edit_file(path="f.js", oldText="old", newText="new")
|
||||||
- Auto line ending adaptation (CRLF/LF)
|
edit_file(path="f.js", mode="line", operation="insert_after", line=10, text="new line")
|
||||||
- Fuzzy matching for whitespace differences`,
|
|
||||||
|
Options: dryRun=true (preview diff), replaceAll=true (replace all occurrences)`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -722,28 +722,15 @@ async function execute(params) {
|
|||||||
|
|
||||||
export const sessionManagerTool = {
|
export const sessionManagerTool = {
|
||||||
name: 'session_manager',
|
name: 'session_manager',
|
||||||
description: `Workflow session lifecycle management tool.
|
description: `Workflow session management.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ccw tool exec session_manager '{"operation":"list"}'
|
session_manager(operation="init", type="workflow", description="...")
|
||||||
ccw tool exec session_manager '{"operation":"init","session_id":"WFS-test"}'
|
session_manager(operation="list", location="active|archived|both")
|
||||||
ccw tool exec session_manager '{"operation":"read","session_id":"WFS-test","content_type":"session"}'
|
session_manager(operation="read", sessionId="WFS-xxx", contentType="plan|task|summary")
|
||||||
ccw tool exec session_manager '{"operation":"stats","session_id":"WFS-test"}'`,
|
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: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@@ -577,19 +577,14 @@ async function execute(params) {
|
|||||||
*/
|
*/
|
||||||
export const smartSearchTool = {
|
export const smartSearchTool = {
|
||||||
name: 'smart_search',
|
name: 'smart_search',
|
||||||
description: `Unified search with intelligent mode selection.
|
description: `Intelligent code search with multiple modes.
|
||||||
|
|
||||||
Modes:
|
Usage:
|
||||||
- auto: Classify intent and recommend optimal search mode (default)
|
smart_search(query="function main", path=".") # Auto-select mode
|
||||||
- exact: Precise literal matching via ripgrep
|
smart_search(query="def init", mode="exact") # Exact match
|
||||||
- fuzzy: Approximate matching with typo tolerance
|
smart_search(query="authentication logic", mode="semantic") # NL search
|
||||||
- semantic: Natural language understanding via LLM/embeddings
|
|
||||||
- graph: Dependency relationship traversal
|
|
||||||
|
|
||||||
Features:
|
Modes: auto (default), exact, fuzzy, semantic, graph`,
|
||||||
- Multi-backend search coordination
|
|
||||||
- Result fusion with RRF ranking
|
|
||||||
- Configurable result limits and context`,
|
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -110,14 +110,10 @@ async function execute(params) {
|
|||||||
*/
|
*/
|
||||||
export const writeFileTool = {
|
export const writeFileTool = {
|
||||||
name: 'write_file',
|
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:
|
Usage: write_file(path="file.js", content="code here")
|
||||||
- Creates parent directories automatically (configurable)
|
Options: backup=true (backup before overwrite), encoding="utf8"`,
|
||||||
- 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.`,
|
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
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(
|
def search(
|
||||||
query: str = typer.Argument(..., help="FTS query to run."),
|
query: str = typer.Argument(..., help="FTS query to run."),
|
||||||
limit: int = typer.Option(20, "--limit", "-n", min=1, max=500, help="Max results."),
|
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."),
|
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."),
|
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging."),
|
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.
|
Searches the workspace-local .codexlens/index.db by default.
|
||||||
Use --global to search the global database at ~/.codexlens/.
|
Use --global to search the global database at ~/.codexlens/.
|
||||||
|
Use --files-only to return only matching file paths.
|
||||||
"""
|
"""
|
||||||
_configure_logging(verbose)
|
_configure_logging(verbose)
|
||||||
|
|
||||||
@@ -258,12 +260,22 @@ def search(
|
|||||||
try:
|
try:
|
||||||
store, db_path = _get_store_for_path(Path.cwd(), use_global)
|
store, db_path = _get_store_for_path(Path.cwd(), use_global)
|
||||||
store.initialize()
|
store.initialize()
|
||||||
results = store.search_fts(query, limit=limit)
|
|
||||||
payload = {"query": query, "count": len(results), "results": results}
|
if files_only:
|
||||||
if json_mode:
|
file_paths = store.search_files_only(query, limit=limit)
|
||||||
print_json(success=True, result=payload)
|
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:
|
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:
|
except Exception as exc:
|
||||||
if json_mode:
|
if json_mode:
|
||||||
print_json(success=False, error=str(exc))
|
print_json(success=False, error=str(exc))
|
||||||
|
|||||||
Reference in New Issue
Block a user