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:
catlog22
2025-12-13 09:14:57 +08:00
parent 15122b9ebb
commit d4e59770d0
20 changed files with 1829 additions and 200 deletions

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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

View File

@@ -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 .'));
}
}

View File

@@ -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
View 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);
});

View File

@@ -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');
}
}

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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: {

View File

@@ -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: {

View 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'));
});
});

View File

@@ -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))