mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: 实现 MCP 工具集中式路径验证,增强安全性和可配置性
- 新增 path-validator.ts:参考 MCP filesystem 服务器设计的集中式路径验证器
- 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 环境变量配置
- 多层路径验证:绝对路径解析 → 沙箱检查 → 符号链接验证
- 向后兼容:未设置环境变量时回退到 process.cwd()
- 更新所有 MCP 工具使用集中式路径验证:
- write-file.ts: 使用 validatePath()
- edit-file.ts: 使用 validatePath({ mustExist: true })
- read-file.ts: 使用 validatePath() + getProjectRoot()
- smart-search.ts: 使用 getProjectRoot()
- core-memory.ts: 使用 getProjectRoot()
- MCP 服务器启动时输出项目根目录和允许目录信息
- MCP 管理界面增强:
- CCW Tools 安装卡片新增路径设置 UI
- 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 配置
- 添加"使用当前项目"快捷按钮
- 支持 Claude 和 Codex 两种模式
- 添加中英文国际化翻译
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -927,9 +927,28 @@ function selectCcwTools(type) {
|
||||
});
|
||||
}
|
||||
|
||||
// Get CCW path settings from input fields
|
||||
function getCcwPathConfig() {
|
||||
const projectRootInput = document.querySelector('.ccw-project-root-input');
|
||||
const allowedDirsInput = document.querySelector('.ccw-allowed-dirs-input');
|
||||
return {
|
||||
projectRoot: projectRootInput?.value || '',
|
||||
allowedDirs: allowedDirsInput?.value || ''
|
||||
};
|
||||
}
|
||||
|
||||
// Set CCW_PROJECT_ROOT to current project path
|
||||
function setCcwProjectRootToCurrent() {
|
||||
const input = document.querySelector('.ccw-project-root-input');
|
||||
if (input && projectPath) {
|
||||
input.value = projectPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Build CCW Tools config with selected tools
|
||||
// Uses isWindowsPlatform from state.js to generate platform-appropriate commands
|
||||
function buildCcwToolsConfig(selectedTools) {
|
||||
function buildCcwToolsConfig(selectedTools, pathConfig = {}) {
|
||||
const { projectRoot, allowedDirs } = pathConfig;
|
||||
// Windows requires 'cmd /c' wrapper to execute npx
|
||||
// Other platforms (macOS, Linux) can run npx directly
|
||||
const config = isWindowsPlatform
|
||||
@@ -948,12 +967,30 @@ function buildCcwToolsConfig(selectedTools) {
|
||||
coreTools.every(t => selectedTools.includes(t)) &&
|
||||
selectedTools.every(t => coreTools.includes(t));
|
||||
|
||||
// Initialize env if needed
|
||||
if (selectedTools.length === 15) {
|
||||
config.env = { CCW_ENABLED_TOOLS: 'all' };
|
||||
} else if (!isDefault && selectedTools.length > 0) {
|
||||
config.env = { CCW_ENABLED_TOOLS: selectedTools.join(',') };
|
||||
}
|
||||
|
||||
// Add path settings if provided
|
||||
if (!config.env) {
|
||||
config.env = {};
|
||||
}
|
||||
|
||||
if (projectRoot && projectRoot.trim()) {
|
||||
config.env.CCW_PROJECT_ROOT = projectRoot.trim();
|
||||
}
|
||||
if (allowedDirs && allowedDirs.trim()) {
|
||||
config.env.CCW_ALLOWED_DIRS = allowedDirs.trim();
|
||||
}
|
||||
|
||||
// Remove env object if empty
|
||||
if (config.env && Object.keys(config.env).length === 0) {
|
||||
delete config.env;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -965,7 +1002,8 @@ async function installCcwToolsMcp(scope = 'workspace') {
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
const pathConfig = getCcwPathConfig();
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools, pathConfig);
|
||||
|
||||
try {
|
||||
const scopeLabel = scope === 'global' ? 'globally' : 'to workspace';
|
||||
@@ -1032,7 +1070,8 @@ async function updateCcwToolsMcp(scope = 'workspace') {
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
const pathConfig = getCcwPathConfig();
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools, pathConfig);
|
||||
|
||||
try {
|
||||
const scopeLabel = scope === 'global' ? 'globally' : 'in workspace';
|
||||
@@ -1126,7 +1165,8 @@ async function installCcwToolsMcpToCodex() {
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
const pathConfig = getCcwPathConfig();
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools, pathConfig);
|
||||
|
||||
try {
|
||||
const isUpdate = codexMcpServers && codexMcpServers['ccw-tools'];
|
||||
@@ -1176,3 +1216,4 @@ window.openMcpCreateModal = openMcpCreateModal;
|
||||
window.toggleProjectConfigType = toggleProjectConfigType;
|
||||
window.getPreferredProjectConfigType = getPreferredProjectConfigType;
|
||||
window.setPreferredProjectConfigType = setPreferredProjectConfigType;
|
||||
window.setCcwProjectRootToCurrent = setCcwProjectRootToCurrent;
|
||||
|
||||
@@ -290,6 +290,8 @@ const i18n = {
|
||||
'codexlens.modelDeleted': 'Model deleted',
|
||||
'codexlens.modelDeleteFailed': 'Model deletion failed',
|
||||
'codexlens.deleteModelConfirm': 'Are you sure you want to delete model',
|
||||
'codexlens.modelListError': 'Failed to load models',
|
||||
'codexlens.noModelsAvailable': 'No models available',
|
||||
|
||||
// CodexLens Indexing Progress
|
||||
'codexlens.indexing': 'Indexing',
|
||||
@@ -609,6 +611,12 @@ const i18n = {
|
||||
'mcp.claudeMode': 'Claude Mode',
|
||||
'mcp.codexMode': 'Codex Mode',
|
||||
|
||||
// CCW Tools Path Settings
|
||||
'mcp.pathSettings': 'Path Settings',
|
||||
'mcp.useCurrentDir': 'Use current directory',
|
||||
'mcp.useCurrentProject': 'Use current project',
|
||||
'mcp.allowedDirsPlaceholder': 'Comma-separated paths (optional)',
|
||||
|
||||
// Codex MCP
|
||||
'mcp.codex.globalServers': 'Codex Global MCP Servers',
|
||||
'mcp.codex.newServer': 'New Server',
|
||||
@@ -1617,6 +1625,8 @@ const i18n = {
|
||||
'codexlens.modelDeleted': '模型已删除',
|
||||
'codexlens.modelDeleteFailed': '模型删除失败',
|
||||
'codexlens.deleteModelConfirm': '确定要删除模型',
|
||||
'codexlens.modelListError': '加载模型列表失败',
|
||||
'codexlens.noModelsAvailable': '没有可用模型',
|
||||
|
||||
// CodexLens 索引进度
|
||||
'codexlens.indexing': '索引中',
|
||||
@@ -1914,6 +1924,12 @@ const i18n = {
|
||||
'mcp.claudeMode': 'Claude 模式',
|
||||
'mcp.codexMode': 'Codex 模式',
|
||||
|
||||
// CCW Tools Path Settings
|
||||
'mcp.pathSettings': '路径设置',
|
||||
'mcp.useCurrentDir': '使用当前目录',
|
||||
'mcp.useCurrentProject': '使用当前项目',
|
||||
'mcp.allowedDirsPlaceholder': '逗号分隔的路径列表(可选)',
|
||||
|
||||
// Codex MCP
|
||||
'mcp.codex.globalServers': 'Codex 全局 MCP 服务器',
|
||||
'mcp.codex.newServer': '新建服务器',
|
||||
|
||||
@@ -415,9 +415,23 @@ async function loadModelList() {
|
||||
var response = await fetch('/api/codexlens/models');
|
||||
var result = await response.json();
|
||||
|
||||
if (!result.success || !result.result || !result.result.models) {
|
||||
if (!result.success) {
|
||||
// Check if the error is specifically about fastembed not being installed
|
||||
var errorMsg = result.error || '';
|
||||
if (errorMsg.includes('fastembed not installed') || errorMsg.includes('Semantic')) {
|
||||
container.innerHTML =
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.semanticNotInstalled') + '</div>';
|
||||
} else {
|
||||
// Show actual error message for other failures
|
||||
container.innerHTML =
|
||||
'<div class="text-sm text-error">' + t('codexlens.modelListError') + ': ' + (errorMsg || t('common.unknownError')) + '</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result || !result.result.models) {
|
||||
container.innerHTML =
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.semanticNotInstalled') + '</div>';
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.noModelsAvailable') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,22 @@ function getCcwEnabledToolsCodex() {
|
||||
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
|
||||
}
|
||||
|
||||
// Get current CCW_PROJECT_ROOT from config
|
||||
function getCcwProjectRoot() {
|
||||
const currentPath = projectPath;
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
|
||||
return ccwConfig?.env?.CCW_PROJECT_ROOT || '';
|
||||
}
|
||||
|
||||
// Get current CCW_ALLOWED_DIRS from config
|
||||
function getCcwAllowedDirs() {
|
||||
const currentPath = projectPath;
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
|
||||
return ccwConfig?.env?.CCW_ALLOWED_DIRS || '';
|
||||
}
|
||||
|
||||
async function renderMcpManager() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
@@ -232,6 +248,34 @@ async function renderMcpManager() {
|
||||
<button class="text-primary hover:underline" onclick="selectCcwToolsCodex('all')">All</button>
|
||||
<button class="text-muted-foreground hover:underline" onclick="selectCcwToolsCodex('none')">None</button>
|
||||
</div>
|
||||
<!-- Path Settings -->
|
||||
<div class="ccw-path-settings mt-3 pt-3 border-t border-border/50">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="folder-root" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-xs font-medium text-muted-foreground">${t('mcp.pathSettings')}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
|
||||
<input type="text"
|
||||
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder="${projectPath || t('mcp.useCurrentDir')}"
|
||||
value="${getCcwProjectRoot()}">
|
||||
<button class="p-1 text-muted-foreground hover:text-foreground"
|
||||
onclick="setCcwProjectRootToCurrent()"
|
||||
title="${t('mcp.useCurrentProject')}">
|
||||
<i data-lucide="locate-fixed" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
|
||||
<input type="text"
|
||||
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
||||
value="${getCcwAllowedDirs()}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
@@ -418,6 +462,34 @@ async function renderMcpManager() {
|
||||
<button class="text-primary hover:underline" onclick="selectCcwTools('all')">All</button>
|
||||
<button class="text-muted-foreground hover:underline" onclick="selectCcwTools('none')">None</button>
|
||||
</div>
|
||||
<!-- Path Settings -->
|
||||
<div class="ccw-path-settings mt-3 pt-3 border-t border-border/50">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="folder-root" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-xs font-medium text-muted-foreground">${t('mcp.pathSettings')}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
|
||||
<input type="text"
|
||||
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder="${projectPath || t('mcp.useCurrentDir')}"
|
||||
value="${getCcwProjectRoot()}">
|
||||
<button class="p-1 text-muted-foreground hover:text-foreground"
|
||||
onclick="setCcwProjectRootToCurrent()"
|
||||
title="${t('mcp.useCurrentProject')}">
|
||||
<i data-lucide="locate-fixed" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
|
||||
<input type="text"
|
||||
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder="${t('mcp.allowedDirsPlaceholder')}"
|
||||
value="${getCcwAllowedDirs()}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user