release(secrets-mcp): 0.6.0 - local gateway onboarding and target_exec
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 4m1s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Has been skipped

This commit is contained in:
voson
2026-04-12 15:48:22 +08:00
parent 34093b0e23
commit cb5865b958
19 changed files with 3515 additions and 453 deletions

View File

@@ -0,0 +1,157 @@
use std::sync::Arc;
use axum::Router;
use axum::extract::State;
use axum::response::{Html, IntoResponse};
use axum::routing::{get, post};
use crate::cache::SharedCache;
use crate::config::LocalConfig;
use crate::remote::RemoteClient;
#[derive(Clone)]
pub struct AppState {
pub config: LocalConfig,
pub cache: SharedCache,
pub remote: Arc<RemoteClient>,
}
async fn index(State(state): State<AppState>) -> impl IntoResponse {
Html(format!(
r#"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>secrets-mcp-local onboarding</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 920px; margin: 24px auto; padding: 0 16px; line-height: 1.5; }}
code, pre {{ background: #f6f8fa; border-radius: 6px; }}
code {{ padding: 2px 6px; }}
pre {{ padding: 12px; overflow-x: auto; }}
.card {{ border: 1px solid #d0d7de; border-radius: 12px; padding: 16px; margin: 16px 0; }}
.row {{ display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }}
button, a.button {{ border: 1px solid #1f2328; background: #1f2328; color: white; padding: 8px 14px; border-radius: 8px; text-decoration: none; cursor: pointer; }}
a.secondary, button.secondary {{ background: white; color: #1f2328; }}
iframe {{ width: 100%; min-height: 420px; border: 1px solid #d0d7de; border-radius: 12px; }}
.muted {{ color: #57606a; }}
</style>
</head>
<body>
<h1>secrets-mcp-local</h1>
<p class="muted">本地 MCP 地址:<code>http://{bind}/mcp</code></p>
<p class="muted">远端服务地址:<code>{remote}</code></p>
<div class="card">
<h2>当前状态</h2>
<pre id="status">loading...</pre>
<div class="row">
<button id="start-bind">开始绑定</button>
<button id="poll-bind" class="secondary">检查授权结果</button>
<a class="button secondary" href="/unlock" target="_blank" rel="noreferrer">打开解锁页</a>
<button id="refresh" class="secondary">刷新状态</button>
</div>
</div>
<div class="card">
<h2>步骤 1远端授权</h2>
<p id="approve-hint" class="muted">点击“开始绑定”后,这里会显示授权地址。</p>
<div id="approve-actions" class="row"></div>
</div>
<div class="card">
<h2>步骤 2本地解锁</h2>
<p class="muted">授权完成后,本页会自动切换到解锁阶段。你也可以直接在下方完成解锁。</p>
<iframe id="unlock-frame" src="/unlock"></iframe>
</div>
<div class="card">
<h2>接入 Cursor</h2>
<p>把 MCP 地址配置为 <code>http://{bind}/mcp</code>。在未就绪时AI 只会看到 bootstrap 工具;完成授权和解锁后会自动暴露业务工具。</p>
</div>
<script>
const statusEl = document.getElementById('status');
const approveHint = document.getElementById('approve-hint');
const approveActions = document.getElementById('approve-actions');
const unlockFrame = document.getElementById('unlock-frame');
function renderApprove(info) {{
approveActions.innerHTML = '';
if (!info?.approve_url) return;
approveHint.textContent = '请先在浏览器完成远端授权,然后回到这里等待自动进入解锁状态。';
const link = document.createElement('a');
link.href = info.approve_url;
link.target = '_blank';
link.rel = 'noreferrer';
link.className = 'button';
link.textContent = '打开远端授权页';
approveActions.appendChild(link);
}}
async function refreshStatus() {{
const res = await fetch('/local/status');
const data = await res.json();
statusEl.textContent = JSON.stringify(data, null, 2);
if (data.pending_bind) renderApprove(data.pending_bind);
if (data.state === 'ready') {{
approveHint.textContent = '本地 MCP 已 ready可以返回 Cursor 正常使用。';
}} else if (data.state === 'pendingUnlock') {{
approveHint.textContent = '远端授权已完成,继续在下方完成本地解锁。';
}}
return data;
}}
async function startBind() {{
const res = await fetch('/local/bind/start', {{ method: 'POST' }});
const data = await res.json();
statusEl.textContent = JSON.stringify(data, null, 2);
renderApprove(data);
}}
async function pollBind() {{
const res = await fetch('/local/bind/exchange', {{
method: 'POST',
headers: {{ 'content-type': 'application/json' }},
body: JSON.stringify({{}})
}});
const data = await res.json();
statusEl.textContent = JSON.stringify(data, null, 2);
await refreshStatus();
if (res.ok && data.status === 'bound') {{
unlockFrame.src = '/unlock';
}}
}}
document.getElementById('start-bind').onclick = startBind;
document.getElementById('poll-bind').onclick = pollBind;
document.getElementById('refresh').onclick = refreshStatus;
window.addEventListener('message', (event) => {{
if (event?.data?.type === 'secrets-mcp-local-ready') refreshStatus();
}});
refreshStatus();
setInterval(refreshStatus, 3000);
</script>
</body>
</html>"#,
bind = state.config.bind,
remote = state.config.remote_base_url,
))
}
pub fn router(state: AppState) -> Router {
Router::new()
.route("/", get(index))
.route("/mcp", axum::routing::any(crate::mcp::handle_mcp))
.route("/local/bind/start", post(crate::bind::bind_start))
.route("/local/bind/exchange", post(crate::bind::bind_exchange))
.route("/local/unbind", post(crate::bind::unbind))
.route("/unlock", get(crate::unlock::unlock_page))
.route(
"/local/unlock/complete",
post(crate::unlock::unlock_complete),
)
.route("/local/lock", post(crate::unlock::lock))
.route("/local/status", get(crate::unlock::status))
.layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024))
.with_state(state)
}