feat: centrally mount and dispatch agent MCP services
This commit is contained in:
@@ -43,10 +43,50 @@ func (p *Panel) Handler() http.Handler {
|
||||
mux.HandleFunc("/panel/api/audits", p.protected(p.Runner.AuditsJSON))
|
||||
mux.HandleFunc("/panel/api/exec", p.protected(p.Runner.PanelExec))
|
||||
mux.HandleFunc("/panel/api/key/rotate", p.protected(p.Runner.RotateAPIKey))
|
||||
mux.HandleFunc("/panel/api/mcp/services", p.protected(p.mcpServices))
|
||||
mux.HandleFunc("/panel/api/mcp/service", p.protected(p.deleteMCPService))
|
||||
mux.HandleFunc("/panel/api/mcp/list", p.protected(p.listAgentMCP))
|
||||
mux.HandleFunc("/panel/api/mcp/call", p.protected(p.callAgentMCP))
|
||||
mux.HandleFunc("/", p.index)
|
||||
return mux
|
||||
}
|
||||
|
||||
type mcpPanelRunner interface {
|
||||
MCPServicesJSON(http.ResponseWriter, *http.Request)
|
||||
DeleteMCPService(http.ResponseWriter, *http.Request)
|
||||
ListAgentMCPJSON(http.ResponseWriter, *http.Request)
|
||||
CallAgentMCPJSON(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func (p *Panel) mcpServices(w http.ResponseWriter, r *http.Request) {
|
||||
if v, ok := p.Runner.(mcpPanelRunner); ok {
|
||||
v.MCPServicesJSON(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "MCP API unavailable", 501)
|
||||
}
|
||||
func (p *Panel) deleteMCPService(w http.ResponseWriter, r *http.Request) {
|
||||
if v, ok := p.Runner.(mcpPanelRunner); ok {
|
||||
v.DeleteMCPService(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "MCP API unavailable", 501)
|
||||
}
|
||||
func (p *Panel) listAgentMCP(w http.ResponseWriter, r *http.Request) {
|
||||
if v, ok := p.Runner.(mcpPanelRunner); ok {
|
||||
v.ListAgentMCPJSON(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "MCP API unavailable", 501)
|
||||
}
|
||||
func (p *Panel) callAgentMCP(w http.ResponseWriter, r *http.Request) {
|
||||
if v, ok := p.Runner.(mcpPanelRunner); ok {
|
||||
v.CallAgentMCPJSON(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "MCP API unavailable", 501)
|
||||
}
|
||||
|
||||
func (p *Panel) handleDevices(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodDelete {
|
||||
p.Runner.DeleteDevice(w, r)
|
||||
|
||||
@@ -40,3 +40,6 @@ header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted
|
||||
.badge-docker-text.no{background:rgba(148,163,184,.15);color:var(--text-muted);border:1px solid var(--border)}
|
||||
.device-card.offline{opacity:.62;filter:saturate(.45)}
|
||||
.device-card.offline .btn-revoke{opacity:1}
|
||||
|
||||
.mcp-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}.mcp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}.mcp-item{border:1px solid var(--line,#30343b);border-radius:10px;padding:14px;background:var(--card,#11151a)}.mcp-item h3{margin:0 0 6px;font-size:15px}.mcp-meta{font-size:12px;opacity:.72;line-height:1.7}.mcp-actions{display:flex;gap:8px;margin-top:10px}.mcp-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mcp-form .full{grid-column:1/-1}.mcp-form label{display:flex;flex-direction:column;gap:5px;font-size:12px;opacity:.9}.mcp-form input,.mcp-form select,.mcp-form textarea{width:100%;box-sizing:border-box}.mcp-form textarea{min-height:72px;font-family:monospace}.mcp-nodes{display:flex;flex-wrap:wrap;gap:8px}.mcp-nodes label{flex-direction:row;align-items:center}.mcp-status{font-size:12px;margin-top:8px;min-height:18px}
|
||||
@media(max-width:700px){.mcp-form{grid-template-columns:1fr}.mcp-form .full{grid-column:auto}.mcp-toolbar{align-items:flex-start;flex-direction:column}}
|
||||
|
||||
@@ -109,3 +109,22 @@ $("#logout").onclick=async()=>{
|
||||
location.href="/panel/login"
|
||||
};
|
||||
loadDevices().then(loadAudits).catch(console.error);
|
||||
|
||||
let mcpEditingId="";
|
||||
async function loadMCPServices(){
|
||||
const services=await api("/mcp/services");
|
||||
const devices=(await api("/devices")).filter(d=>!d.is_local);
|
||||
$("#mcpNodes").innerHTML=devices.map(d=>`<label><input type="checkbox" value="${esc(d.name)}">${esc(d.name)}</label>`).join("");
|
||||
$("#mcpList").innerHTML=services.map(s=>`<div class="mcp-item"><h3>${esc(s.name)} <small>${esc(s.id)}</small></h3><div class="mcp-meta">${esc(s.transport)} · ${s.scope==='custom'?'指定节点':'全部节点'} · ${s.enabled?'启用':'停用'}${s.url?' · '+esc(s.url):s.command?' · '+esc(s.command):''}</div><div class="mcp-actions"><button type="button" onclick="editMCP('${esc(s.id)}')">编辑</button><button type="button" onclick="deleteMCP('${esc(s.id)}')">删除</button></div></div>`).join("")||'<div class="mcp-meta">暂无 MCP 服务配置。</div>';
|
||||
}
|
||||
function resetMCPForm(){
|
||||
mcpEditingId=""; $("#mcpEditor").hidden=false; $("#mcpId").disabled=false; ["mcpId","mcpName","mcpCommand","mcpUrl"].forEach(id=>$("#"+id).value=""); $("#mcpTransport").value="stdio"; $("#mcpEnabled").value="1"; $("#mcpArgs").value="[]"; $("#mcpHeaders").value="{}"; $("#mcpEnv").value="{}"; $("#mcpScope").value="all"; $("#mcpStatus").textContent=""; $("#mcpNodes").querySelectorAll("input").forEach(x=>x.checked=false);
|
||||
}
|
||||
async function editMCP(id){
|
||||
const services=await api("/mcp/services"); const s=services.find(x=>x.id===id); if(!s)return; resetMCPForm(); mcpEditingId=id; $("#mcpId").value=s.id; $("#mcpId").disabled=true; $("#mcpName").value=s.name||""; $("#mcpTransport").value=s.transport; $("#mcpEnabled").value=s.enabled?"1":"0"; $("#mcpCommand").value=s.command||""; $("#mcpUrl").value=s.url||""; $("#mcpArgs").value=s.args_json||"[]"; $("#mcpHeaders").value=s.headers_json||"{}"; $("#mcpEnv").value=s.env_json||"{}"; $("#mcpScope").value=s.scope||"all"; let nodes=[];try{nodes=JSON.parse(s.target_nodes_json||"[]")}catch(e){};$("#mcpNodes").querySelectorAll("input").forEach(x=>x.checked=nodes.includes(x.value));
|
||||
}
|
||||
async function deleteMCP(id){if(!confirm(`确认删除 MCP 服务 [${id}]?`))return;try{await api("/mcp/service?id="+encodeURIComponent(id),{method:"DELETE"});await loadMCPServices()}catch(e){alert("删除失败: "+e.message)}}
|
||||
$("#mcpNew").onclick=resetMCPForm; $("#mcpCancel").onclick=()=>$("#mcpEditor").hidden=true;
|
||||
$("#mcpSave").onclick=async()=>{try{const scope=$("#mcpScope").value;const body={id:$("#mcpId").value.trim(),name:$("#mcpName").value.trim(),transport:$("#mcpTransport").value,command:$("#mcpCommand").value.trim(),url:$("#mcpUrl").value.trim(),scope,enabled:$("#mcpEnabled").value==="1",args:JSON.parse($("#mcpArgs").value||"[]"),headers:JSON.parse($("#mcpHeaders").value||"{}"),env:JSON.parse($("#mcpEnv").value||"{}"),target_nodes:[...$("#mcpNodes").querySelectorAll("input:checked")].map(x=>x.value)};if(!body.id||!body.name)throw Error("服务标识和显示名称不能为空");await api("/mcp/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});$("#mcpStatus").textContent="已保存并向在线节点同步。";await loadMCPServices()}catch(e){$("#mcpStatus").textContent="保存失败: "+e.message}};
|
||||
$("#mcpTransport").onchange=()=>{const stdio=$("#mcpTransport").value==="stdio";$("#mcpCommand").parentElement.style.display=stdio?"":"none";$("#mcpArgs").parentElement.style.display=stdio?"":"none";$("#mcpUrl").parentElement.style.display=stdio?"none":""};
|
||||
loadMCPServices().catch(console.error);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><script>(function(){try{var t=localStorage.getItem("vpc_theme");if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme:light)").matches?"light":"dark";document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme="dark"}})();</script><title>VPS-Commander</title><link rel="stylesheet" href="/static/app.css"></head><body><header><div class="brand-wrap"><img src="/static/icon.png" alt="logo" class="brand-logo"><div><strong>VPS-Commander</strong><span>Private Infrastructure Console</span></div></div><div class="header-actions"><button id="themeToggle" type="button" class="btn-icon" title="切换主题" aria-label="切换主题"><svg class="theme-svg svg-sun" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg><svg class="theme-svg svg-moon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></button><button id="logout" type="button">退出</button></div></header><main><section class="grid"><article class="card wide"><div class="cardhead"><h2>设备看板</h2><button id="refresh" type="button">刷新</button></div><div id="devices" class="devices"></div></article><article class="card"><h2>Actions 向导</h2><p>当前 Hub 的 OpenAPI Schema:</p><div class="actions"><button id="copySchema" type="button">复制 Schema 地址</button><button id="copyPrompt" type="button">复制 System Prompt</button><button id="rotateKey" type="button">轮换 API Key</button></div><pre id="schemaHint">/openapi.json</pre><pre id="newKey"></pre></article><article class="card wide"><div class="cardhead"><h2>快速控制台</h2><select id="device"></select></div><textarea id="command" placeholder="输入 Shell 命令,例如 uname -a"></textarea><div><button id="run" type="button">执行</button></div><pre id="output"></pre></article><article class="card wide"><div class="cardhead"><h2>审计日志</h2><button id="auditRefresh" type="button">刷新</button></div><div class="tablewrap"><table><thead><tr><th>时间</th><th>设备</th><th>动作</th><th>命令/路径</th><th>退出码</th><th>耗时</th><th>来源</th></tr></thead><tbody id="audits"></tbody></table></div></article></section></main><script src="/static/app.js"></script></body></html>
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><script>(function(){try{var t=localStorage.getItem("vpc_theme");if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme:light)").matches?"light":"dark";document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme="dark"}})();</script><title>VPS-Commander</title><link rel="stylesheet" href="/static/app.css"></head><body><header><div class="brand-wrap"><img src="/static/icon.png" alt="logo" class="brand-logo"><div><strong>VPS-Commander</strong><span>Private Infrastructure Console</span></div></div><div class="header-actions"><button id="themeToggle" type="button" class="btn-icon" title="切换主题" aria-label="切换主题"><svg class="theme-svg svg-sun" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg><svg class="theme-svg svg-moon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></button><button id="logout" type="button">退出</button></div></header><main><section class="grid"><article class="card wide"><div class="cardhead"><h2>设备看板</h2><button id="refresh" type="button">刷新</button></div><div id="devices" class="devices"></div></article><article class="card"><h2>Actions 向导</h2><p>当前 Hub 的 OpenAPI Schema:</p><div class="actions"><button id="copySchema" type="button">复制 Schema 地址</button><button id="copyPrompt" type="button">复制 System Prompt</button><button id="rotateKey" type="button">轮换 API Key</button></div><pre id="schemaHint">/openapi.json</pre><pre id="newKey"></pre></article><article class="card wide"><div class="mcp-toolbar"><h2>MCP 挂载</h2><button id="mcpNew" type="button">新增 MCP</button></div><div id="mcpList" class="mcp-grid"></div><div id="mcpEditor" hidden><div class="mcp-form"><label>服务标识<input id="mcpId" placeholder="docker"></label><label>显示名称<input id="mcpName" placeholder="Docker 容器管理"></label><label>传输类型<select id="mcpTransport"><option value="stdio">命令行 / stdio</option><option value="http">HTTP / Streamable HTTP</option><option value="sse">SSE</option></select></label><label>启用<select id="mcpEnabled"><option value="1">启用</option><option value="0">停用</option></select></label><label class="full">Command<input id="mcpCommand" placeholder="docker-mcp"></label><label class="full">Arguments(JSON 数组)<textarea id="mcpArgs">[]</textarea></label><label class="full">URL<input id="mcpUrl" placeholder="http://127.0.0.1:8080/mcp"></label><label class="full">Headers(JSON 对象,可选)<textarea id="mcpHeaders">{}</textarea></label><label class="full">环境变量(JSON 对象,可选)<textarea id="mcpEnv">{}</textarea></label><label>生效范围<select id="mcpScope"><option value="all">全部节点</option><option value="custom">指定节点</option></select></label><div><span>指定节点</span><div id="mcpNodes" class="mcp-nodes"></div></div></div><div class="mcp-actions"><button id="mcpSave" type="button">保存并热更新</button><button id="mcpCancel" type="button">取消</button></div><div id="mcpStatus" class="mcp-status"></div></div></article>
|
||||
<article class="card wide"><div class="cardhead"><h2>快速控制台</h2><select id="device"></select></div><textarea id="command" placeholder="输入 Shell 命令,例如 uname -a"></textarea><div><button id="run" type="button">执行</button></div><pre id="output"></pre></article><article class="card wide"><div class="cardhead"><h2>审计日志</h2><button id="auditRefresh" type="button">刷新</button></div><div class="tablewrap"><table><thead><tr><th>时间</th><th>设备</th><th>动作</th><th>命令/路径</th><th>退出码</th><th>耗时</th><th>来源</th></tr></thead><tbody id="audits"></tbody></table></div></article></section></main><script src="/static/app.js"></script></body></html>
|
||||
|
||||
Reference in New Issue
Block a user