mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 22:09:37 +08:00
feat: expand mcp transports and skill execution
This commit is contained in:
@@ -312,6 +312,9 @@ const resources = {
|
||||
configMCPInstallDoneTitle: 'MCP package installed',
|
||||
configMCPInstallDoneMessage: 'Installed {{package}} and resolved binary {{bin}}.',
|
||||
configMCPInstallDoneFallback: 'MCP package installed.',
|
||||
configMCPArgsEnterHint: 'Type one argument and press Enter',
|
||||
configMCPCommandMissing: 'MCP command is unavailable.',
|
||||
configMCPInstallSuggested: 'Suggested package: {{pkg}}',
|
||||
configMCPDiscoveredTools: 'Discovered MCP Tools',
|
||||
configMCPDiscoveredToolsCount: '{{count}} discovered',
|
||||
configNoMCPDiscoveredTools: 'No MCP tools discovered yet.',
|
||||
@@ -495,6 +498,7 @@ const resources = {
|
||||
dingtalk: 'DingTalk',
|
||||
filesystem: 'Filesystem',
|
||||
working_dir: 'Working Directory',
|
||||
url: 'URL',
|
||||
timeout: 'Timeout',
|
||||
auto_install_missing: 'Auto-install Missing',
|
||||
sandbox: 'Sandbox',
|
||||
@@ -833,6 +837,9 @@ const resources = {
|
||||
configMCPInstallDoneTitle: 'MCP 包安装完成',
|
||||
configMCPInstallDoneMessage: '已安装 {{package}},并解析到可执行文件 {{bin}}。',
|
||||
configMCPInstallDoneFallback: 'MCP 包已安装。',
|
||||
configMCPArgsEnterHint: '输入一个参数后按回车添加',
|
||||
configMCPCommandMissing: 'MCP 命令不可用。',
|
||||
configMCPInstallSuggested: '建议安装包:{{pkg}}',
|
||||
configMCPDiscoveredTools: '已发现的 MCP 工具',
|
||||
configMCPDiscoveredToolsCount: '已发现 {{count}} 个',
|
||||
configNoMCPDiscoveredTools: '暂未发现 MCP 工具。',
|
||||
@@ -1016,6 +1023,7 @@ const resources = {
|
||||
dingtalk: '钉钉',
|
||||
filesystem: '文件系统',
|
||||
working_dir: '工作目录',
|
||||
url: '地址',
|
||||
timeout: '超时',
|
||||
auto_install_missing: '自动安装缺失依赖',
|
||||
sandbox: '沙箱',
|
||||
|
||||
@@ -23,6 +23,8 @@ const MCP: React.FC = () => {
|
||||
const ui = useUI();
|
||||
const [newMCPServerName, setNewMCPServerName] = useState('');
|
||||
const [mcpTools, setMcpTools] = useState<Array<{ name: string; description?: string; mcp?: { server?: string; remote_tool?: string } }>>([]);
|
||||
const [mcpServerChecks, setMcpServerChecks] = useState<Array<{ name: string; status?: string; message?: string; package?: string; installer?: string; installable?: boolean; resolved?: string }>>([]);
|
||||
const [argInputs, setArgInputs] = useState<Record<string, string>>({});
|
||||
const [baseline, setBaseline] = useState<any>(null);
|
||||
|
||||
const currentPayload = useMemo(() => cfg || {}, [cfg]);
|
||||
@@ -49,9 +51,13 @@ const MCP: React.FC = () => {
|
||||
const data = await r.json();
|
||||
if (!cancelled) {
|
||||
setMcpTools(Array.isArray(data?.mcp_tools) ? data.mcp_tools : []);
|
||||
setMcpServerChecks(Array.isArray(data?.mcp_server_checks) ? data.mcp_server_checks : []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setMcpTools([]);
|
||||
if (!cancelled) {
|
||||
setMcpTools([]);
|
||||
setMcpServerChecks([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +73,23 @@ const MCP: React.FC = () => {
|
||||
setCfg((v) => setPath(v, `tools.mcp.servers.${name}.${field}`, value));
|
||||
}
|
||||
|
||||
function addMCPArg(name: string, rawValue: string) {
|
||||
const value = rawValue.trim();
|
||||
if (!value) return;
|
||||
const current = ((((cfg as any)?.tools?.mcp?.servers?.[name]?.args) || []) as any[])
|
||||
.map((x) => String(x).trim())
|
||||
.filter(Boolean);
|
||||
updateMCPServerField(name, 'args', [...current, value]);
|
||||
setArgInputs((prev) => ({ ...prev, [name]: '' }));
|
||||
}
|
||||
|
||||
function removeMCPArg(name: string, index: number) {
|
||||
const current = ((((cfg as any)?.tools?.mcp?.servers?.[name]?.args) || []) as any[])
|
||||
.map((x) => String(x).trim())
|
||||
.filter(Boolean);
|
||||
updateMCPServerField(name, 'args', current.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
function addMCPServer() {
|
||||
const name = newMCPServerName.trim();
|
||||
if (!name) return;
|
||||
@@ -83,9 +106,11 @@ const MCP: React.FC = () => {
|
||||
next.tools.mcp.servers[name] = {
|
||||
enabled: true,
|
||||
transport: 'stdio',
|
||||
url: '',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
permission: 'workspace',
|
||||
working_dir: '',
|
||||
description: '',
|
||||
package: '',
|
||||
@@ -94,6 +119,7 @@ const MCP: React.FC = () => {
|
||||
return next;
|
||||
});
|
||||
setNewMCPServerName('');
|
||||
setArgInputs((prev) => ({ ...prev, [name]: '' }));
|
||||
}
|
||||
|
||||
async function removeMCPServer(name: string) {
|
||||
@@ -111,21 +137,32 @@ const MCP: React.FC = () => {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setArgInputs((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function inferMCPPackage(server: any): string {
|
||||
if (typeof server?.package === 'string' && server.package.trim()) return server.package.trim();
|
||||
const command = String(server?.command || '').trim();
|
||||
const args = Array.isArray(server?.args) ? server.args.map((x: any) => String(x).trim()).filter(Boolean) : [];
|
||||
if (command === 'npx' || command.endsWith('/npx')) {
|
||||
const pkg = args.find((arg: string) => !arg.startsWith('-'));
|
||||
return pkg || '';
|
||||
function inferMCPInstallSpec(server: any): { installer: string; packageName: string } {
|
||||
if (typeof server?.installer === 'string' && server.installer.trim() && typeof server?.package === 'string' && server.package.trim()) {
|
||||
return { installer: server.installer.trim(), packageName: server.package.trim() };
|
||||
}
|
||||
return '';
|
||||
if (typeof server?.package === 'string' && server.package.trim()) {
|
||||
return { installer: 'npm', packageName: server.package.trim() };
|
||||
}
|
||||
const command = String(server?.command || '').trim().split('/').pop() || '';
|
||||
const args = Array.isArray(server?.args) ? server.args.map((x: any) => String(x).trim()).filter(Boolean) : [];
|
||||
const pkg = args.find((arg: string) => !arg.startsWith('-')) || '';
|
||||
if (command === 'npx') return { installer: 'npm', packageName: pkg };
|
||||
if (command === 'uvx') return { installer: 'uv', packageName: pkg };
|
||||
if (command === 'bunx') return { installer: 'bun', packageName: pkg };
|
||||
return { installer: 'npm', packageName: '' };
|
||||
}
|
||||
|
||||
async function installMCPServerPackage(name: string, server: any) {
|
||||
const defaultPkg = inferMCPPackage(server);
|
||||
const inferred = inferMCPInstallSpec(server);
|
||||
const defaultPkg = inferred.packageName;
|
||||
const pkg = await ui.promptDialog({
|
||||
title: t('configMCPInstallTitle'),
|
||||
message: t('configMCPInstallMessage', { name }),
|
||||
@@ -141,7 +178,7 @@ const MCP: React.FC = () => {
|
||||
const r = await fetch(`/webui/api/mcp/install${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ package: packageName }),
|
||||
body: JSON.stringify({ package: packageName, installer: inferred.installer }),
|
||||
});
|
||||
const text = await r.text();
|
||||
if (!r.ok) {
|
||||
@@ -172,6 +209,14 @@ const MCP: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function installMCPServerCheckPackage(check: { name: string; package?: string; installer?: string }) {
|
||||
const server = (((cfg as any)?.tools?.mcp?.servers?.[check.name]) || {}) as any;
|
||||
if (check.package && !String(server?.package || '').trim()) {
|
||||
updateMCPServerField(check.name, 'package', check.package);
|
||||
}
|
||||
await installMCPServerPackage(check.name, { ...server, package: check.package || server?.package, installer: check.installer });
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
const payload = cfg;
|
||||
@@ -255,22 +300,86 @@ const MCP: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries((((cfg as any)?.tools?.mcp?.servers) || {}) as Record<string, any>).map(([name, server]) => (
|
||||
{Object.entries((((cfg as any)?.tools?.mcp?.servers) || {}) as Record<string, any>).map(([name, server]) => {
|
||||
const transport = String(server?.transport || 'stdio');
|
||||
const isStdio = transport === 'stdio';
|
||||
const usesURL = transport === 'http' || transport === 'streamable_http' || transport === 'sse';
|
||||
return (
|
||||
<div key={name} className="grid grid-cols-1 md:grid-cols-12 gap-2 rounded-xl border border-zinc-800 bg-zinc-900/30 p-2 text-xs">
|
||||
<div className="md:col-span-2 font-mono text-zinc-300 flex items-center">{name}</div>
|
||||
<label className="md:col-span-1 flex items-center gap-2 text-zinc-300">
|
||||
<input type="checkbox" checked={!!server?.enabled} onChange={(e)=>updateMCPServerField(name, 'enabled', e.target.checked)} />
|
||||
{t('enable')}
|
||||
</label>
|
||||
<input value={String(server?.command || '')} onChange={(e)=>updateMCPServerField(name, 'command', e.target.value)} placeholder={t('configLabels.command')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.working_dir || '')} onChange={(e)=>updateMCPServerField(name, 'working_dir', e.target.value)} placeholder={t('configLabels.working_dir')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={Array.isArray(server?.args) ? server.args.join(',') : ''} onChange={(e)=>updateMCPServerField(name, 'args', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.args')}${t('configCommaSeparatedHint')}`} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.package || '')} onChange={(e)=>updateMCPServerField(name, 'package', e.target.value)} placeholder={t('configLabels.package')} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.description || '')} onChange={(e)=>updateMCPServerField(name, 'description', e.target.value)} placeholder={t('configLabels.description')} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<button onClick={()=>installMCPServerPackage(name, server)} className="md:col-span-1 px-2 py-1 rounded bg-emerald-900/60 hover:bg-emerald-800 text-emerald-100">{t('install')}</button>
|
||||
<select value={transport} onChange={(e)=>updateMCPServerField(name, 'transport', e.target.value)} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800">
|
||||
<option value="stdio">stdio</option>
|
||||
<option value="http">http</option>
|
||||
<option value="streamable_http">streamable_http</option>
|
||||
<option value="sse">sse</option>
|
||||
</select>
|
||||
{isStdio && (
|
||||
<>
|
||||
<input value={String(server?.command || '')} onChange={(e)=>updateMCPServerField(name, 'command', e.target.value)} placeholder={t('configLabels.command')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<select value={String(server?.permission || 'workspace')} onChange={(e)=>updateMCPServerField(name, 'permission', e.target.value)} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800">
|
||||
<option value="workspace">workspace</option>
|
||||
<option value="full">full</option>
|
||||
</select>
|
||||
<input value={String(server?.working_dir || '')} onChange={(e)=>updateMCPServerField(name, 'working_dir', e.target.value)} placeholder={t('configLabels.working_dir')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<div className="md:col-span-2 rounded-lg bg-zinc-950/70 border border-zinc-800 p-2 space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(Array.isArray(server?.args) ? server.args : []).map((arg: any, index: number) => (
|
||||
<span key={`${name}-arg-${index}`} className="inline-flex items-center gap-2 rounded-md bg-zinc-800 px-2 py-1 text-[11px] text-zinc-200">
|
||||
<span className="font-mono">{String(arg)}</span>
|
||||
<button type="button" onClick={() => removeMCPArg(name, index)} className="text-zinc-400 hover:text-zinc-100">x</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
value={argInputs[name] || ''}
|
||||
onChange={(e) => setArgInputs((prev) => ({ ...prev, [name]: e.target.value }))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addMCPArg(name, argInputs[name] || '');
|
||||
}
|
||||
}}
|
||||
onBlur={() => addMCPArg(name, argInputs[name] || '')}
|
||||
placeholder={t('configMCPArgsEnterHint')}
|
||||
className="w-full px-2 py-1 rounded-lg bg-zinc-900/80 border border-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
<input value={String(server?.package || '')} onChange={(e)=>updateMCPServerField(name, 'package', e.target.value)} placeholder={t('configLabels.package')} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
</>
|
||||
)}
|
||||
{usesURL && (
|
||||
<input value={String(server?.url || '')} onChange={(e)=>updateMCPServerField(name, 'url', e.target.value)} placeholder={t('configLabels.url')} className="md:col-span-5 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
)}
|
||||
<input value={String(server?.description || '')} onChange={(e)=>updateMCPServerField(name, 'description', e.target.value)} placeholder={t('configLabels.description')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
{isStdio && (
|
||||
<button onClick={()=>installMCPServerPackage(name, server)} className="md:col-span-1 px-2 py-1 rounded bg-emerald-900/60 hover:bg-emerald-800 text-emerald-100">{t('install')}</button>
|
||||
)}
|
||||
<button onClick={()=>removeMCPServer(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">{t('delete')}</button>
|
||||
{(() => {
|
||||
const check = mcpServerChecks.find((item) => item.name === name);
|
||||
if (!check || check.status === 'ok' || check.status === 'disabled' || check.status === 'not_applicable') return null;
|
||||
return (
|
||||
<div className="md:col-span-12 rounded-lg border border-amber-800/60 bg-amber-950/30 px-3 py-2 text-xs text-amber-100 flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<div>{check.message || t('configMCPCommandMissing')}</div>
|
||||
{check.package && (
|
||||
<div className="text-amber-300/80">{t('configMCPInstallSuggested', { pkg: check.package })} {check.installer ? `(${check.installer})` : ''}</div>
|
||||
)}
|
||||
</div>
|
||||
{check.installable && (
|
||||
<button onClick={() => installMCPServerCheckPackage(check)} className="px-2 py-1 rounded bg-amber-700 hover:bg-amber-600 text-white">
|
||||
{t('install')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
)})}
|
||||
{Object.keys((((cfg as any)?.tools?.mcp?.servers) || {}) as Record<string, any>).length === 0 && (
|
||||
<div className="text-xs text-zinc-500">{t('configNoMCPServers')}</div>
|
||||
)}
|
||||
|
||||
@@ -390,6 +390,9 @@ const SubagentProfiles: React.FC = () => {
|
||||
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded"
|
||||
placeholder="read_file, list_files, memory_search"
|
||||
/>
|
||||
<div className="mt-1 text-[11px] text-zinc-500">
|
||||
<span className="font-mono text-zinc-400">skill_exec</span> is inherited automatically and does not need to be listed here.
|
||||
</div>
|
||||
{groups.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{groups.map((g) => (
|
||||
|
||||
@@ -96,6 +96,13 @@ type RegistrySubagent = {
|
||||
prompt_file_found?: boolean;
|
||||
memory_namespace?: string;
|
||||
tool_allowlist?: string[];
|
||||
inherited_tools?: string[];
|
||||
effective_tools?: string[];
|
||||
tool_visibility?: {
|
||||
mode?: string;
|
||||
inherited_tool_count?: number;
|
||||
effective_tool_count?: number;
|
||||
};
|
||||
routing_keywords?: string[];
|
||||
};
|
||||
|
||||
@@ -539,6 +546,7 @@ const Subagents: React.FC = () => {
|
||||
|
||||
const localMainStats = taskStats[normalizeTitle(localRoot.agent_id, 'main')] || { total: 0, running: 0, failed: 0, waiting: 0, latestStatus: '', latestUpdated: 0, active: [] };
|
||||
const localMainTask = recentTaskByAgent[normalizeTitle(localRoot.agent_id, 'main')];
|
||||
const localMainRegistry = registryItems.find((item) => item.agent_id === localRoot.agent_id);
|
||||
localBranchStats.running += localMainStats.running;
|
||||
localBranchStats.failed += localMainStats.failed;
|
||||
const localMainCard: GraphCardSpec = {
|
||||
@@ -557,8 +565,10 @@ const Subagents: React.FC = () => {
|
||||
`children=${localChildren.length + remoteClusters.length}`,
|
||||
`total=${localMainStats.total} running=${localMainStats.running}`,
|
||||
`waiting=${localMainStats.waiting} failed=${localMainStats.failed}`,
|
||||
`notify=${normalizeTitle(registryItems.find((item) => item.agent_id === localRoot.agent_id)?.notify_main_policy, 'final_only')}`,
|
||||
`notify=${normalizeTitle(localMainRegistry?.notify_main_policy, 'final_only')}`,
|
||||
`transport=${normalizeTitle(localRoot.transport, 'local')} type=${normalizeTitle(localRoot.type, 'router')}`,
|
||||
`tools=${normalizeTitle(localMainRegistry?.tool_visibility?.mode, 'allowlist')} visible=${localMainRegistry?.tool_visibility?.effective_tool_count ?? 0} inherited=${localMainRegistry?.tool_visibility?.inherited_tool_count ?? 0}`,
|
||||
(localMainRegistry?.inherited_tools || []).length ? `inherits: ${(localMainRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -',
|
||||
localMainStats.active[0] ? `task: ${localMainStats.active[0].title}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: localMainStats.running > 0 ? 'bg-emerald-500' : localMainStats.latestStatus === 'failed' ? 'bg-red-500' : 'bg-amber-400',
|
||||
@@ -575,6 +585,7 @@ const Subagents: React.FC = () => {
|
||||
const childY = childStartY;
|
||||
const stats = taskStats[normalizeTitle(child.agent_id, '')] || { total: 0, running: 0, failed: 0, waiting: 0, latestStatus: '', latestUpdated: 0, active: [] };
|
||||
const task = recentTaskByAgent[normalizeTitle(child.agent_id, '')];
|
||||
const childRegistry = registryItems.find((item) => item.agent_id === child.agent_id);
|
||||
localBranchStats.running += stats.running;
|
||||
localBranchStats.failed += stats.failed;
|
||||
cards.push({
|
||||
@@ -592,8 +603,10 @@ const Subagents: React.FC = () => {
|
||||
meta: [
|
||||
`total=${stats.total} running=${stats.running}`,
|
||||
`waiting=${stats.waiting} failed=${stats.failed}`,
|
||||
`notify=${normalizeTitle(registryItems.find((item) => item.agent_id === child.agent_id)?.notify_main_policy, 'final_only')}`,
|
||||
`notify=${normalizeTitle(childRegistry?.notify_main_policy, 'final_only')}`,
|
||||
`transport=${normalizeTitle(child.transport, 'local')} type=${normalizeTitle(child.type, 'worker')}`,
|
||||
`tools=${normalizeTitle(childRegistry?.tool_visibility?.mode, 'allowlist')} visible=${childRegistry?.tool_visibility?.effective_tool_count ?? 0} inherited=${childRegistry?.tool_visibility?.inherited_tool_count ?? 0}`,
|
||||
(childRegistry?.inherited_tools || []).length ? `inherits: ${(childRegistry?.inherited_tools || []).join(', ')}` : 'inherits: -',
|
||||
stats.active[0] ? `task: ${stats.active[0].title}` : task ? `last: ${summarizeTask(task.task, task.label)}` : t('noLiveTasks'),
|
||||
],
|
||||
accent: stats.running > 0 ? 'bg-emerald-500' : stats.latestStatus === 'failed' ? 'bg-red-500' : 'bg-sky-400',
|
||||
|
||||
Reference in New Issue
Block a user