feat(mcp): 添加 OpenCode 客户端配置(第二位)

OpenCode 用 opencode.json(非 mcp.json),根键 mcp(非 mcpServers),
远程端点 type: remote(非 streamable-http),带 $schema 与 enabled 字段。
来源:2026 opencode.ai 官方文档。同步更新骨架屏形状数组(0..7)。
This commit is contained in:
xfy 2026-07-28 16:34:15 +08:00
parent 9d7f01a4ce
commit 3f3bf411ca
3 changed files with 43 additions and 9 deletions

View File

@ -305,8 +305,9 @@ pub async fn get_mcp_client_configs(token: String) -> Result<McpClientConfigs, S
&token,
);
// (标题, 内容, 语言)JSON 配置用 json 语法高亮CLI 一行命令用 bash。
let entries: [(&str, String, &str); 6] = [
let entries: [(&str, String, &str); 7] = [
("Oh-My-Pi~/.pi/agent/mcp.json 或 .pi/mcp.json", c.omp_json, "json"),
("OpenCode~/.config/opencode/opencode.json 或项目根 opencode.json", c.opencode_json, "json"),
("Claude Code.mcp.json / ~/.claude.json", c.claude_code_json, "json"),
("Cursor~/.cursor/mcp.json", c.cursor_json, "json"),
("Clinecline_mcp_settings.json", c.cline_json, "json"),

View File

@ -1,12 +1,11 @@
//! MCP 客户端配置片段生成。
//!
//! 管理员签发令牌后,需要把「如何把这个博客接入某个 AI 客户端」的配置粘进去。
//! 不同客户端的配置文件格式不同,这里生成 4 种可直接复制粘贴的片段,全部指向
//! 不同客户端的配置文件格式不同,这里生成多种可直接复制粘贴的片段,全部指向
//! 同一个 `/mcp` 端点、携带同一个 `Authorization: Bearer` 头。
//!
//! 形状来源:`docs/mcp-research.md` §"Client-config output format",各客户端官方文档
//! Claude Code / Cursor / Cline)核实。所有 JSON 都是 `serde_json` 构造再 pretty-print
//! 保证格式合法(不会手抖写错逗号/引号)。
//! Claude Code / Cursor / Cline / Oh-My-Pi / OpenCode)核实。所有 JSON 都是 `serde_json`
//! 构造再 pretty-print保证格式合法(不会手抖写错逗号/引号)。
use serde::Serialize;
@ -29,6 +28,10 @@ pub struct ClientConfigs {
/// Oh-My-Pi`~/.pi/agent/mcp.json` 全局 / `.pi/mcp.json` 项目级)。与其它客户端的
/// 关键差异:字段名是 `transport`(而非 `type`),值仍为 `streamable-http`。
pub omp_json: String,
/// OpenCode`opencode.json` 全局 `~/.config/opencode/opencode.json` / 项目根)。
/// 关键差异schema 根键是 `mcp`(非 `mcpServers`),远程端点用 `type: "remote"`
///(非 `streamable-http`),并带 `$schema` 与 `enabled` 字段2026 opencode.ai 官方文档)。
pub opencode_json: String,
/// 通用原始 JSON一个 server entry 的纯净形式,供其它兼容客户端粘贴。
pub generic_json: String,
/// Claude Code CLI 一行命令:`claude mcp add --transport http <name> <url> --header ...`。
@ -100,6 +103,20 @@ pub fn generate_client_configs(base_url: &str, token: &str) -> ClientConfigs {
}
});
// --- OpenCode根键 mcp非 mcpServersremote 端点用 type: "remote"(非 streamable-http ---
// 带 $schema 与 enabled 字段opencode.ai 官方文档要求)。
let opencode_json = serde_json::json!({
"$schema": "https://opencode.ai/config.json",
"mcp": {
SERVER_NAME: {
"type": "remote",
"url": mcp_url,
"enabled": true,
"headers": { "Authorization": auth_header }
}
}
});
// --- 通用:单个 server entry 的纯净形式 ---
let generic_json = serde_json::json!({
"type": "streamable-http",
@ -118,6 +135,7 @@ pub fn generate_client_configs(base_url: &str, token: &str) -> ClientConfigs {
cursor_json: pretty_json(&cursor_json),
cline_json: pretty_json(&cline_json),
omp_json: pretty_json(&omp_json),
opencode_json: pretty_json(&opencode_json),
generic_json: pretty_json(&generic_json),
claude_cli,
}
@ -208,6 +226,21 @@ mod tests {
assert_eq!(entry["headers"]["Authorization"], format!("Bearer {TOKEN}"));
}
#[test]
fn opencode_json_uses_mcp_root_key_and_remote_type() {
let cfg = generate_client_configs(BASE, TOKEN);
let v: serde_json::Value = serde_json::from_str(&cfg.opencode_json).unwrap();
// 关键差异:根键是 mcp非 mcpServers
assert!(v.get("mcpServers").is_none(), "opencode 配置不应含 mcpServers 键");
let entry = &v["mcp"]["yggdrasil"];
// 远程端点用 type: "remote"(非 streamable-http
assert_eq!(entry["type"], "remote");
assert_eq!(entry["enabled"], true);
assert_eq!(v["$schema"], "https://opencode.ai/config.json");
assert_eq!(entry["url"], "https://rua.plus/mcp");
assert_eq!(entry["headers"]["Authorization"], format!("Bearer {TOKEN}"));
}
#[test]
fn claude_cli_one_liner_contains_url_and_header() {
let cfg = generate_client_configs(BASE, TOKEN);
@ -219,8 +252,7 @@ mod tests {
#[test]
fn all_json_is_pretty_indented() {
let cfg = generate_client_configs(BASE, TOKEN);
// pretty JSON 至少含一个换行 + 缩进(非单行紧凑形式)。
for s in [&cfg.claude_code_json, &cfg.cursor_json, &cfg.cline_json, &cfg.omp_json, &cfg.generic_json] {
for s in [&cfg.claude_code_json, &cfg.cursor_json, &cfg.cline_json, &cfg.omp_json, &cfg.opencode_json, &cfg.generic_json] {
assert!(s.contains('\n'), "JSON 应是 pretty-printed: {s}");
assert!(s.contains(" "), "JSON 应含 2 空格缩进: {s}");
}

View File

@ -53,11 +53,12 @@ const LIFETIME_OPTIONS: &[(TokenLifetime, &str)] = &[
/// 配置骨架屏的尺寸变化:(标题宽度 px, 代码块高度 px)。
///
/// 按 6 个真实配置片段的长短差异给出不同占位尺寸,让骨架屏更像即将出现的真实内容
/// 按 7 个真实配置片段的长短差异给出不同占位尺寸,让骨架屏更像即将出现的真实内容
/// 而非千篇一律的等高块。
#[cfg(target_arch = "wasm32")]
const CONFIG_SKELETON_SHAPES: &[(&str, &str)] = &[
("width: 320px;", "height: 152px;"), // Oh-My-Pi JSON
("width: 280px;", "height: 168px;"), // OpenCode JSON
("width: 260px;", "height: 168px;"), // Claude Code JSON
("width: 200px;", "height: 136px;"), // Cursor JSON
("width: 220px;", "height: 168px;"), // Cline JSON
@ -552,7 +553,7 @@ fn ConfigCard() -> Element {
if loading() {
div { class: "flex flex-col gap-4",
for i in 0..6 {
for i in 0..7 {
div { key: "{i}", class: "flex flex-col gap-2",
div { class: "flex items-center justify-between",
SkeletonBox {