mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-18 04:29:39 +08:00
- 新增 MCP 环境变量 key 识别与风险提示 - 在新增 MCP 表单展示 env 用途、占位值和 Docker 边界提醒 - 在 inspect_mcp_draft 输出脱敏 envHints 供 AI 解释参数
288 lines
10 KiB
TypeScript
288 lines
10 KiB
TypeScript
import { splitShellLikeCommand } from './mcpCommandDraft';
|
||
|
||
export type MCPEnvHintCategory = 'secret' | 'endpoint' | 'proxy' | 'path' | 'runtime' | 'generic';
|
||
|
||
export interface MCPEnvHintItem {
|
||
key: string;
|
||
category: MCPEnvHintCategory;
|
||
label: string;
|
||
detail: string;
|
||
valueHint: string;
|
||
sensitive: boolean;
|
||
known: boolean;
|
||
empty: boolean;
|
||
placeholder: boolean;
|
||
}
|
||
|
||
export interface MCPEnvHintProfile {
|
||
envVarCount: number;
|
||
secretLikeCount: number;
|
||
endpointLikeCount: number;
|
||
items: MCPEnvHintItem[];
|
||
warnings: string[];
|
||
nextActions: string[];
|
||
}
|
||
|
||
interface KnownEnvHint {
|
||
category: MCPEnvHintCategory;
|
||
label: string;
|
||
detail: string;
|
||
valueHint: string;
|
||
sensitive?: boolean;
|
||
}
|
||
|
||
const KNOWN_ENV_HINTS: Record<string, KnownEnvHint> = {
|
||
GITHUB_TOKEN: {
|
||
category: 'secret',
|
||
label: 'GitHub Token',
|
||
detail: '通常给 GitHub MCP 读取仓库、Issue、PR 或 Actions 使用。',
|
||
valueHint: '填 GitHub Personal Access Token,按 MCP README 要求授予最小权限。',
|
||
sensitive: true,
|
||
},
|
||
GITLAB_TOKEN: {
|
||
category: 'secret',
|
||
label: 'GitLab Token',
|
||
detail: '通常给 GitLab MCP 访问项目、Merge Request 或 CI 使用。',
|
||
valueHint: '填 GitLab Access Token,并限制到需要访问的项目范围。',
|
||
sensitive: true,
|
||
},
|
||
OPENAI_API_KEY: {
|
||
category: 'secret',
|
||
label: 'OpenAI API Key',
|
||
detail: '给依赖 OpenAI API 的 MCP 服务调用模型或 embedding 接口。',
|
||
valueHint: '填真实 API Key;不要写到 command、args 或聊天消息里。',
|
||
sensitive: true,
|
||
},
|
||
ANTHROPIC_API_KEY: {
|
||
category: 'secret',
|
||
label: 'Anthropic API Key',
|
||
detail: '给依赖 Anthropic Claude API 的 MCP 服务使用。',
|
||
valueHint: '填真实 API Key;确认服务确实需要该变量后再配置。',
|
||
sensitive: true,
|
||
},
|
||
GEMINI_API_KEY: {
|
||
category: 'secret',
|
||
label: 'Gemini API Key',
|
||
detail: '给依赖 Google Gemini API 的 MCP 服务使用。',
|
||
valueHint: '填真实 API Key;也有服务会要求 GOOGLE_API_KEY。',
|
||
sensitive: true,
|
||
},
|
||
GOOGLE_API_KEY: {
|
||
category: 'secret',
|
||
label: 'Google API Key',
|
||
detail: '给 Google/Gemini/Maps/Search 类 MCP 服务使用。',
|
||
valueHint: '填真实 API Key,并确认 README 要求的是 GOOGLE_API_KEY 还是 GEMINI_API_KEY。',
|
||
sensitive: true,
|
||
},
|
||
SLACK_BOT_TOKEN: {
|
||
category: 'secret',
|
||
label: 'Slack Bot Token',
|
||
detail: '给 Slack MCP 读取频道、消息或发送通知使用。',
|
||
valueHint: '填 xoxb- 开头的 Bot Token,并控制 workspace 权限。',
|
||
sensitive: true,
|
||
},
|
||
NOTION_API_KEY: {
|
||
category: 'secret',
|
||
label: 'Notion API Key',
|
||
detail: '给 Notion MCP 访问页面、数据库或 workspace 内容使用。',
|
||
valueHint: '填 Notion integration secret,并只授权需要的页面。',
|
||
sensitive: true,
|
||
},
|
||
DATABASE_URL: {
|
||
category: 'endpoint',
|
||
label: '数据库连接串',
|
||
detail: '给 MCP 服务自己连接数据库使用;这会把数据库连接信息交给该 MCP 进程。',
|
||
valueHint: '只在确实要让该 MCP 直连数据库时填写,优先考虑使用 GoNavi MCP 避免密码外泄。',
|
||
sensitive: true,
|
||
},
|
||
HTTP_PROXY: {
|
||
category: 'proxy',
|
||
label: 'HTTP 代理',
|
||
detail: '让 MCP 进程访问 HTTP 资源时走指定代理。',
|
||
valueHint: '填 http://host:port;如果代理带账号密码,按敏感变量处理。',
|
||
},
|
||
HTTPS_PROXY: {
|
||
category: 'proxy',
|
||
label: 'HTTPS 代理',
|
||
detail: '让 MCP 进程访问 HTTPS 资源时走指定代理。',
|
||
valueHint: '填 http://host:port 或 https://host:port。',
|
||
},
|
||
NO_PROXY: {
|
||
category: 'proxy',
|
||
label: '代理绕过列表',
|
||
detail: '指定哪些域名或地址不走代理。',
|
||
valueHint: '逗号分隔,例如 localhost,127.0.0.1,.corp.local。',
|
||
},
|
||
DOCKER_HOST: {
|
||
category: 'runtime',
|
||
label: 'Docker Daemon 地址',
|
||
detail: '给 docker CLI 指定连接哪个 Docker Engine。',
|
||
valueHint: 'Windows 常见为 npipe:////./pipe/docker_engine;远端 Docker 请确认安全边界。',
|
||
},
|
||
GONAVI_MCP_HTTP_TOKEN: {
|
||
category: 'secret',
|
||
label: 'GoNavi MCP HTTP Token',
|
||
detail: '给远程 MCP HTTP 服务开启 Bearer Token 鉴权时使用。',
|
||
valueHint: '填高熵随机 token;不要复用数据库密码或模型 API Key。',
|
||
sensitive: true,
|
||
},
|
||
NODE_ENV: {
|
||
category: 'runtime',
|
||
label: 'Node 运行环境',
|
||
detail: '影响部分 Node MCP 服务的日志、调试或生产模式。',
|
||
valueHint: '通常填 production、development 或 README 指定值。',
|
||
},
|
||
LOG_LEVEL: {
|
||
category: 'runtime',
|
||
label: '日志级别',
|
||
detail: '控制 MCP 服务输出多少日志。',
|
||
valueHint: '常见值为 debug、info、warn、error;排障时可临时调高。',
|
||
},
|
||
};
|
||
|
||
const SECRET_KEY_RE = /(TOKEN|API[_-]?KEY|SECRET|PASSWORD|PASS|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|DATABASE_URL|DSN)/iu;
|
||
const ENDPOINT_KEY_RE = /(URL|URI|ENDPOINT|BASE[_-]?URL|HOST|ADDR|ADDRESS)/iu;
|
||
const PROXY_KEY_RE = /PROXY/iu;
|
||
const PATH_KEY_RE = /(PATH|DIR|ROOT|HOME|FILE|CONFIG)/iu;
|
||
const RUNTIME_KEY_RE = /^(NODE_ENV|LOG_LEVEL|DEBUG|ENV|TZ)$/iu;
|
||
|
||
const PLACEHOLDER_VALUE_RE = /^(\*+|\.{3}|<[^>]+>|your[-_ ].*|change[_-]?me|replace[_-]?me|xxx+|todo|token|api[_-]?key)$/iu;
|
||
|
||
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||
|
||
const normalizeEnvKey = (key: string): string => toTrimmedString(key).toUpperCase();
|
||
|
||
const normalizeCommandName = (command: string): string => {
|
||
const { tokens } = splitShellLikeCommand(command);
|
||
const raw = toTrimmedString(tokens[0] || command);
|
||
return (raw.split(/[\\/]/u).pop() || raw)
|
||
.replace(/\.(exe|cmd|bat|ps1)$/iu, '')
|
||
.toLowerCase();
|
||
};
|
||
|
||
const inferEnvHint = (key: string): KnownEnvHint => {
|
||
if (SECRET_KEY_RE.test(key)) {
|
||
return {
|
||
category: 'secret',
|
||
label: '密钥 / Token',
|
||
detail: '变量名看起来像密钥、Token、密码或连接串。',
|
||
valueHint: '填真实值,但只保存在本机 MCP 配置里;不要放到 command、args 或聊天内容。',
|
||
sensitive: true,
|
||
};
|
||
}
|
||
if (PROXY_KEY_RE.test(key)) {
|
||
return {
|
||
category: 'proxy',
|
||
label: '代理配置',
|
||
detail: '变量名看起来像网络代理设置。',
|
||
valueHint: '按 README 或企业代理格式填写,例如 http://127.0.0.1:7890。',
|
||
};
|
||
}
|
||
if (ENDPOINT_KEY_RE.test(key)) {
|
||
return {
|
||
category: 'endpoint',
|
||
label: '服务地址',
|
||
detail: '变量名看起来像服务地址、接口地址或主机配置。',
|
||
valueHint: '填写 MCP Server 要访问的 URL、host 或 endpoint。',
|
||
};
|
||
}
|
||
if (PATH_KEY_RE.test(key)) {
|
||
return {
|
||
category: 'path',
|
||
label: '路径 / 配置文件',
|
||
detail: '变量名看起来像本地路径、目录或配置文件位置。',
|
||
valueHint: '填写本机 MCP 进程能访问的绝对路径;Windows 路径建议保留盘符。',
|
||
};
|
||
}
|
||
if (RUNTIME_KEY_RE.test(key)) {
|
||
return {
|
||
category: 'runtime',
|
||
label: '运行时开关',
|
||
detail: '变量名看起来像运行环境、日志或调试开关。',
|
||
valueHint: '按 README 指定的枚举值填写。',
|
||
};
|
||
}
|
||
return {
|
||
category: 'generic',
|
||
label: '自定义配置',
|
||
detail: '未命中内置变量库,按 MCP README 对应字段说明填写。',
|
||
valueHint: '确认变量名大小写和 README 完全一致。',
|
||
};
|
||
};
|
||
|
||
const isPlaceholderValue = (value: string): boolean => {
|
||
const text = toTrimmedString(value);
|
||
if (!text) {
|
||
return false;
|
||
}
|
||
return PLACEHOLDER_VALUE_RE.test(text) || text.includes('...');
|
||
};
|
||
|
||
const buildEnvHintItem = ([key, value]: [string, string]): MCPEnvHintItem => {
|
||
const normalizedKey = normalizeEnvKey(key);
|
||
const knownHint = KNOWN_ENV_HINTS[normalizedKey];
|
||
const hint = knownHint || inferEnvHint(normalizedKey);
|
||
return {
|
||
key: normalizedKey,
|
||
category: hint.category,
|
||
label: hint.label,
|
||
detail: hint.detail,
|
||
valueHint: hint.valueHint,
|
||
sensitive: hint.sensitive === true || SECRET_KEY_RE.test(normalizedKey),
|
||
known: Boolean(knownHint),
|
||
empty: toTrimmedString(value) === '',
|
||
placeholder: isPlaceholderValue(value),
|
||
};
|
||
};
|
||
|
||
export const buildMCPEnvHintProfile = (
|
||
command: string,
|
||
args: string[] | undefined,
|
||
env: Record<string, string> | undefined,
|
||
): MCPEnvHintProfile | null => {
|
||
const items = Object.entries(env || {})
|
||
.sort(([left], [right]) => normalizeEnvKey(left).localeCompare(normalizeEnvKey(right)))
|
||
.map(buildEnvHintItem);
|
||
|
||
if (items.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const warnings: string[] = [];
|
||
const nextActions: string[] = [];
|
||
const secretLikeCount = items.filter((item) => item.sensitive).length;
|
||
const endpointLikeCount = items.filter((item) => item.category === 'endpoint').length;
|
||
const emptyItems = items.filter((item) => item.empty);
|
||
const placeholderItems = items.filter((item) => item.placeholder);
|
||
const dockerCommand = normalizeCommandName(command) === 'docker';
|
||
const dockerEnvForwarded = (args || []).some((arg) => ['-e', '--env'].includes(toTrimmedString(arg).toLowerCase()) || toTrimmedString(arg).startsWith('-e='));
|
||
|
||
if (emptyItems.length > 0) {
|
||
warnings.push(`${emptyItems.length} 个环境变量值为空,测试前需要补齐或删除。`);
|
||
nextActions.push(`补齐 ${emptyItems.map((item) => item.key).slice(0, 3).join('、')} 的值,或删除不需要的变量。`);
|
||
}
|
||
if (placeholderItems.length > 0) {
|
||
warnings.push(`${placeholderItems.length} 个环境变量看起来仍是示例占位值。`);
|
||
nextActions.push(`把 ${placeholderItems.map((item) => item.key).slice(0, 3).join('、')} 替换成真实值后再测试工具发现。`);
|
||
}
|
||
if (dockerCommand && items.length > 0 && !dockerEnvForwarded) {
|
||
warnings.push('command=docker 时,这里的环境变量只传给 docker CLI,不会自动进入容器。');
|
||
nextActions.push('如果容器内 MCP 需要这些变量,请在 args 里按 README 增加 -e KEY=VALUE 或 --env KEY=VALUE。');
|
||
}
|
||
if (secretLikeCount > 0) {
|
||
nextActions.push('密钥类变量只保存在本机配置;不要把真实值发到聊天、Issue 或截图里。');
|
||
}
|
||
if (nextActions.length === 0) {
|
||
nextActions.push('环境变量 key 已可识别;测试失败时优先核对 README 要求的变量名大小写。');
|
||
}
|
||
|
||
return {
|
||
envVarCount: items.length,
|
||
secretLikeCount,
|
||
endpointLikeCount,
|
||
items,
|
||
warnings,
|
||
nextActions,
|
||
};
|
||
};
|