dsh-skill-mcp-market 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,7 +161,7 @@ DSH 本来就有技能运行时和 MCP 运行时,缺的只是市场这一层
161
161
  | `skill_market_install` | 装技能:`github`(可带 `@ref`)/ `skills.sh`(页面 URL 或裸 `owner/repo`)/ `clawhub` / `skillsmp` / `trusttools` / `browse.sh` / `lobehub-agent` / `skillhub` / `url` / `local` / `zip` |
162
162
  | `skill_market_list` | 列已装技能(含 `SKILL.md` 路径) |
163
163
  | `skill_market_uninstall` | 卸载(移到 `market/trash/`) |
164
- | `mcp_market_search` | 搜 MCP:官方注册表(实时)+ 已安装列表;目录不可达时返回降级列表与 `catalogHint` |
164
+ | `mcp_market_search` | 搜 MCP:`source: "registry"`(官方注册表,默认)或 `"lobehub"`(LobeHub 市场);目录不可达时返回降级列表与 `catalogHint` |
165
165
  | `mcp_market_install` | 装 MCP:`slug`(官方注册表名称,如 `io.github.Async23/chrome-devtools-mcp`)/ 手工定义 / `importJson`(Claude·Cursor 的 `mcpServers`),装完立即探活 |
166
166
  | `mcp_market_list` | 列已装 MCP(transport、enabled、mounted、已发布工具名) |
167
167
  | `mcp_market_set_enabled` | 启停(断开即撤销工具,启用即重连) |
@@ -173,6 +173,7 @@ DSH 本来就有技能运行时和 MCP 运行时,缺的只是市场这一层
173
173
  | 源 | 说明 |
174
174
  |---|---|
175
175
  | **MCP 官方注册表** | `registry.modelcontextprotocol.io`,免费、实时的 MCP 目录,自带真实启动方式(npm / streamable-http) |
176
+ | **LobeHub MCP 市场** | `market.lobehub.com`,第二个 MCP 市场(面板里单独一档)。搜索走官方 `@lobehub/market-cli`,首次自动注册客户端;卡片带作者/分类/安装次数;安装取其公开 manifest 的真实启动方式 |
176
177
  | `skills.sh` | 技能索引,支持页面 URL 与裸 `owner/repo`;搜索接口要求 ≥2 字符,空搜索回退到首页推荐 |
177
178
  | `skillsmp` | 带反爬(Cloudflare),走 python→curl→fetch 三级取数 |
178
179
  | `browse.sh` | 启动预热 + 内存缓存,冷启动超时不缓存空结果 |
@@ -211,7 +212,9 @@ dsh-skill-mcp-market/
211
212
  - 技能发现是**一层深**:只认 `<root>/<name>/SKILL.md` 和 `<root>/<name>.md`。
212
213
  - MCP 定义是标准写法,能否真跑由安装时的探活结果决定,不预先保证;注册表里标记为必填的环境变量会在安装时提示。
213
214
  - 市场不伪造下载量/热度:上游没给就显示 0。
214
- - LobeHub 页面会间歇返回 `402 payment_required`,所以它只作为次要源;MCP 目录不可达时按「上次缓存 → 内置条目」降级,并在面板上提示可直接按名称安装。
215
+ - 两个 MCP 市场**各自独立**,面板一次只看一个(和技能页签一样,不做合并):官方注册表偏结构化启动方式,LobeHub 偏卡片信息与分类。
216
+ - LobeHub 需要注册一次市场客户端(`npx -y @lobehub/market-cli register --base-url https://market.lobehub.com --name "DSH Market" --source dsh`),插件首次使用会自动完成;其搜索分页上限 40 条。
217
+ - MCP 目录不可达时按「上次缓存 → 内置条目」降级,并在面板上提示可直接按名称安装。
215
218
 
216
219
  ## 开发
217
220
 
package/client.js CHANGED
@@ -142,6 +142,7 @@ window.__ModuleLoader__.load({
142
142
  const [detail, setDetail] = useState(null);
143
143
  const [draft, setDraft] = useState({});
144
144
  const [source, setSource] = useState('clawhub');
145
+ const [mcpSource, setMcpSource] = useState('registry');
145
146
  const [size, setSize] = useState(24);
146
147
  const [sort, setSort] = useState('default');
147
148
  const [importPath, setImportPath] = useState('');
@@ -201,7 +202,10 @@ window.__ModuleLoader__.load({
201
202
  setBusy('mcp.search');
202
203
  setError('');
203
204
  try {
204
- const d = await call('mcp.search', { query: text });
205
+ const d = await call('mcp.search', {
206
+ query: text,
207
+ source: (options && options.source) || mcpSource,
208
+ });
205
209
  setMcps(d.items || []);
206
210
  // The host reports catalogue degradation separately from "no match", so both the
207
211
  // reason and the still-usable path (install by registry name) reach the user.
@@ -468,7 +472,10 @@ window.__ModuleLoader__.load({
468
472
  h('div', { style: S.desc }, item.description || ''),
469
473
  h('div', { style: S.meta },
470
474
  h('span', null, item.transport || 'stdio'),
471
- item.source ? h('span', null, `· ${item.source}`) : null,
475
+ h('span', null, `· ${item.sourceLabel || (item.source === 'lobehub' ? 'LobeHub' : 'MCP 官方注册表')}`),
476
+ item.owner ? h('span', null, `· ${item.owner}`) : null,
477
+ item.category ? h('span', null, `· ${item.category}`) : null,
478
+ item.installs ? h('span', null, `· ${Number(item.installs).toLocaleString()} 次安装`) : null,
472
479
  (item.envRequired || []).length ? h('span', null, `· 需要 ${item.envRequired.join(', ')}`) : null),
473
480
  // "name" is the registry identifier users may need to copy into a manual install.
474
481
  h('div', { style: S.meta }, h('span', null, item.slug)),
@@ -563,7 +570,15 @@ window.__ModuleLoader__.load({
563
570
  searchSkills(query, { source: key, size });
564
571
  },
565
572
  }, key === 'all' ? '全部来源' : key))
566
- : h('span', { style: S.status }, '来源:MCP 官方注册表(实时)'),
573
+ : ['registry', 'lobehub'].map((key) =>
574
+ h('button', {
575
+ key,
576
+ style: S.chip(mcpSource === key),
577
+ onClick: () => {
578
+ setMcpSource(key);
579
+ searchMcps(query, { source: key });
580
+ },
581
+ }, key === 'registry' ? 'MCP 官方注册表' : 'LobeHub')),
567
582
  h('div', { style: { flex: 1 } }),
568
583
  h('select', {
569
584
  style: { ...S.input, flex: '0 0 120px', height: 30 },
package/index.js CHANGED
@@ -21,7 +21,8 @@ import { installSkill, listInstalledSkills, uninstallSkill, updateSkill } from "
21
21
  import { createMcpManager, missingEnv, normalizeServerName } from "./src/mcp.js";
22
22
  import { fallbackCards, registryDefinitionByName, searchMcpRegistry } from "./src/mcp-registry.js";
23
23
  import { createMarketSkillProvider, readSkillBody } from "./src/provider.js";
24
- import { lobehubMcpConfig, makeCache, searchLobeHubMcp, searchSkills, warmMarketCache } from "./src/sources.js";
24
+ import { makeCache, searchSkills, warmMarketCache } from "./src/sources.js";
25
+ import { lobehubManifestDefinition, lobehubRegistered, searchLobeHubMarket } from "./src/lobehub-market.js";
25
26
  import { setUserEnvironmentVariable } from "./src/env.js";
26
27
 
27
28
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -88,7 +89,42 @@ function shortError(error) {
88
89
  * only one of them is worth acting on. Installation never depends on this function — a
89
90
  * `slug` resolves on its own — so a degraded list never blocks a real install.
90
91
  */
91
- async function loadMcpCatalog(cache, query = "") {
92
+ /**
93
+ * The LobeHub marketplace, as its own source: it is a separate catalogue with its own
94
+ * identifiers, categories and install manifests, so the panel switches between the two
95
+ * markets instead of blending them.
96
+ */
97
+ async function loadLobeHubCatalog(cache, query = "") {
98
+ const key = `lobehub:list:${String(query || "").trim().toLowerCase() || "all"}:60`;
99
+ try {
100
+ const result = await searchLobeHubMarket({ query, pageSize: 40, sort: query ? "" : "installCount", cache });
101
+ if (result.items.length > 0) {
102
+ await cache.put(key, result.items);
103
+ return { items: result.items, hint: "", live: true, categories: result.categories, source: "lobehub" };
104
+ }
105
+ return {
106
+ items: [],
107
+ hint: String(query || "").trim()
108
+ ? `LobeHub 里没有匹配「${String(query).trim()}」的连接器,换个关键词(单个词更好)试试。`
109
+ : "LobeHub 目录暂时没有返回条目,稍后再试。",
110
+ live: true,
111
+ categories: result.categories,
112
+ source: "lobehub",
113
+ };
114
+ } catch (error) {
115
+ const stale = await cache.get(key, 7 * 24 * 3600 * 1000);
116
+ if (Array.isArray(stale) && stale.length > 0) {
117
+ return { items: stale, hint: `LobeHub 暂时不可达(${shortError(error)}),下面是上次取到的列表。`, live: false, categories: [], source: "lobehub" };
118
+ }
119
+ const registerHint = lobehubRegistered()
120
+ ? ""
121
+ : "(还没有注册市场客户端,可运行 npx -y @lobehub/market-cli register --base-url https://market.lobehub.com --name \"DSH Market\" --source dsh)";
122
+ return { items: [], hint: `LobeHub 暂时不可达:${shortError(error)}${registerHint}`, live: false, categories: [], source: "lobehub" };
123
+ }
124
+ }
125
+
126
+ async function loadMcpCatalog(cache, query = "", source = "registry") {
127
+ if (source === "lobehub") return loadLobeHubCatalog(cache, query);
92
128
  const key = `registry:v2:${String(query || "").trim().toLowerCase() || "all"}:60`;
93
129
  const problems = [];
94
130
  let answered = false;
@@ -101,20 +137,6 @@ async function loadMcpCatalog(cache, query = "") {
101
137
  problems.push(`官方注册表:${shortError(error)}`);
102
138
  }
103
139
 
104
- try {
105
- const result = await searchLobeHubMcp({ query, pageSize: 60, cache });
106
- answered = true;
107
- if (result.items.length > 0) {
108
- return {
109
- items: result.items,
110
- hint: problems.length > 0 ? `官方注册表暂时不可用(${problems[0]}),下面来自 LobeHub 目录。` : "",
111
- live: true,
112
- };
113
- }
114
- } catch (error) {
115
- problems.push(`LobeHub:${shortError(error)}`);
116
- }
117
-
118
140
  // Both catalogues answered and neither had a match: that is an empty search result, not
119
141
  // an outage, and it must not be dressed up as a degraded catalogue.
120
142
  if (answered && problems.length === 0) {
@@ -151,21 +173,13 @@ function applyEnvValues(envValues) {
151
173
 
152
174
  /** Resolve one LobeHub slug into a runnable MCP definition by reading its page. */
153
175
  async function resolveLobeHubDefinition(slug, serverName) {
154
- const config = await lobehubMcpConfig(slug);
155
- const name = normalizeServerName(serverName || config.name || slug);
176
+ // A LobeHub identifier resolves through its public marketplace manifest, which declares the
177
+ // real command/args (or remote URL) plus the environment variables the server expects.
178
+ const definition = await lobehubManifestDefinition(slug);
156
179
  return {
157
- serverName: name,
158
- title: slug,
159
- description: "",
160
- transport: config.url ? "streamable-http" : "stdio",
161
- command: config.command || "",
162
- args: config.args || [],
163
- env: config.env || {},
164
- url: config.url || "",
165
- headers: config.headers || {},
166
- source: "lobehub",
180
+ ...definition,
181
+ serverName: normalizeServerName(serverName || definition.title || slug),
167
182
  slug,
168
- homepage: `https://lobehub.com/mcp/${slug}`,
169
183
  };
170
184
  }
171
185
 
@@ -188,9 +202,9 @@ async function resolveMcpDefinition(slug, serverName) {
188
202
  } catch (error) {
189
203
  const detail = registryError ? `注册表查询失败:${shortError(registryError)}` : "注册表里没有这个名称";
190
204
  throw new Error(
191
- `无法解析 MCP 连接器 "${slug}"(${detail})。LobeHub 目录已改为付费接口(HTTP 402)。` +
192
- `请改用 MCP 官方注册表名称,例如 mcp_market_install({slug:"io.github.Async23/chrome-devtools-mcp"}),` +
193
- `或直接给 command/args(stdio)或 url(streamable-http)手动安装。原始错误:${shortError(error)}`,
205
+ `无法解析 MCP 连接器 "${slug}"(${detail};LobeHub manifest 也失败:${shortError(error)})。` +
206
+ `可改用 MCP 官方注册表名称(例如 io.github.Async23/chrome-devtools-mcp)或 LobeHub 标识符(例如 microsoft-playwright-mcp),` +
207
+ `也可以直接给 command/args(stdio)或 url(streamable-http)手动安装。`,
194
208
  );
195
209
  }
196
210
  }
@@ -500,12 +514,19 @@ export async function apply(ctx, config = {}) {
500
514
  query: { type: "string" },
501
515
  category: { type: "string" },
502
516
  includeInstalled: { type: "boolean", description: "Include installed connectors. Defaults to true." },
517
+ source: {
518
+ type: "string",
519
+ enum: ["registry", "lobehub"],
520
+ description:
521
+ "Which MCP market to search: `registry` (official Model Context Protocol registry, default) or `lobehub` (LobeHub marketplace, richer cards with author/install counts).",
522
+ },
503
523
  },
504
524
  additionalProperties: false,
505
525
  },
506
526
  handler: async (args) => {
507
527
  const query = String(args.query || "").trim();
508
- const catalog = await loadMcpCatalog(marketCache, query);
528
+ const wanted = String(args.source || "registry") === "lobehub" ? "lobehub" : "registry";
529
+ const catalog = await loadMcpCatalog(marketCache, query, wanted);
509
530
  const rows = catalog.live
510
531
  ? catalog.items
511
532
  : catalog.items.filter((item) =>
@@ -825,7 +846,8 @@ export async function apply(ctx, config = {}) {
825
846
  return mcp.update(body.serverName, body.patch || {});
826
847
  case "mcp.search": {
827
848
  const query = String(body.query || "").trim();
828
- const catalog = await loadMcpCatalog(marketCache, query);
849
+ const wanted = String(body.source || "registry") === "lobehub" ? "lobehub" : "registry";
850
+ const catalog = await loadMcpCatalog(marketCache, query, wanted);
829
851
  // A live registry search is already filtered server-side; fallback and cached lists
830
852
  // are not, so only those get a local filter.
831
853
  const rows = catalog.live
@@ -849,6 +871,8 @@ export async function apply(ctx, config = {}) {
849
871
  source: item.source || "registry",
850
872
  })),
851
873
  hint: catalog.hint,
874
+ categories: catalog.categories || [],
875
+ source: wanted,
852
876
  };
853
877
  }
854
878
  case "mcp.install": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-mcp-market",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "DSH 技能市场与 MCP 连接器市场:浏览实时技能源,一键安装 SKILL.md 技能与 MCP 连接器,装完即刻可用、无需重启。SKILL & MCP marketplace for DeepSeek Harness — browse live sources, install skills and MCP connectors, effective immediately.",
6
6
  "license": "MIT",
@@ -0,0 +1,211 @@
1
+ // The second MCP market: LobeHub's plugin marketplace.
2
+ //
3
+ // LobeHub used to be reachable only by scraping its pages, which is why this plugin kept it
4
+ // as a last-resort fallback. It now ships a documented marketplace API, used through its own
5
+ // CLI (`@lobehub/market-cli`), which handles the OAuth client registration for us:
6
+ //
7
+ // npx -y @lobehub/market-cli register --base-url https://market.lobehub.com --name "DSH Market" --source dsh
8
+ // npx -y @lobehub/market-cli mcp search --q "playwright" --sort installCount --output json
9
+ // npx -y @lobehub/market-cli mcp view <identifier> --output json
10
+ //
11
+ // Search results carry name/author/description/category/installCount/ratingAverage, i.e. the
12
+ // same fields the reference desktop app renders. The per-plugin manifest (command, args, url,
13
+ // env schema) is served anonymously at /api/v1/plugins/<identifier>/manifest, so installs do
14
+ // not need the CLI at all.
15
+ import { execFile } from "node:child_process";
16
+ import { existsSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { getJson } from "./http.js";
20
+
21
+ /** Marketplace origin; overridable for self-hosted or test instances. */
22
+ export const LOBEHUB_MARKET_BASE = process.env.LOBEHUB_MARKET_BASE_URL || "https://market.lobehub.com";
23
+
24
+ /** Where the CLI keeps the registered client credentials. */
25
+ export function lobehubCredentialsPath() {
26
+ return join(homedir(), ".lobehub-market", "credentials.json");
27
+ }
28
+
29
+ export function lobehubRegistered() {
30
+ try {
31
+ return existsSync(lobehubCredentialsPath());
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /** Quote one argv item for a `cmd /c` command line. */
38
+ function quoteForCmd(value) {
39
+ const text = String(value);
40
+ if (!/[\s"&|<>^]/.test(text)) return text;
41
+ return `"${text.replace(/"/g, '\\"')}"`;
42
+ }
43
+
44
+ function runCli(args, timeoutMs = 90000) {
45
+ // Windows cannot execFile a `.cmd` shim without a shell (EINVAL), so the CLI runs through
46
+ // cmd.exe there with an explicitly quoted command line.
47
+ const isWindows = process.platform === "win32";
48
+ const file = isWindows ? process.env.ComSpec || "cmd.exe" : "npx";
49
+ const argv = isWindows
50
+ ? ["/d", "/s", "/c", ["npx", "-y", "@lobehub/market-cli", ...args].map(quoteForCmd).join(" ")]
51
+ : ["-y", "@lobehub/market-cli", ...args];
52
+ return new Promise((resolve, reject) => {
53
+ execFile(
54
+ file,
55
+ argv,
56
+ {
57
+ timeout: timeoutMs,
58
+ maxBuffer: 16 * 1024 * 1024,
59
+ windowsHide: true,
60
+ env: { ...process.env, MARKET_BASE_URL: LOBEHUB_MARKET_BASE, NO_COLOR: "1", npm_config_yes: "true" },
61
+ },
62
+ (error, stdout, stderr) => {
63
+ if (error) {
64
+ const detail = String(stderr || error.message || "")
65
+ .split(/\r?\n/)
66
+ .filter((line) => !/^npm notice/.test(line.trim()))
67
+ .join(" ")
68
+ .replace(/\s+/g, " ")
69
+ .slice(0, 300);
70
+ const wrapped = new Error(`LobeHub 市场命令失败:${detail}`);
71
+ wrapped.code = error.code;
72
+ reject(wrapped);
73
+ } else {
74
+ resolve(String(stdout || ""));
75
+ }
76
+ },
77
+ );
78
+ });
79
+ }
80
+
81
+ /** Register this plugin as a marketplace client once; the CLI stores the credentials itself. */
82
+ export async function registerLobeHubClient(name = "DSH Market") {
83
+ await runCli(["register", "--base-url", LOBEHUB_MARKET_BASE, "--name", name, "--source", "dsh"], 150000);
84
+ return lobehubCredentialsPath();
85
+ }
86
+
87
+ function parseJsonOutput(text) {
88
+ const start = String(text).indexOf("{");
89
+ if (start < 0) throw new Error(`LobeHub 市场返回了非 JSON 内容:${String(text).slice(0, 120)}`);
90
+ return JSON.parse(String(text).slice(start));
91
+ }
92
+
93
+ function mapItem(raw) {
94
+ const connectionType = String(raw.connectionType || "").toLowerCase();
95
+ const transport = connectionType === "local" || connectionType === "stdio"
96
+ ? "stdio"
97
+ : raw.haveCloudEndpoint || connectionType === "remote" || connectionType === "http"
98
+ ? "streamable-http"
99
+ : "stdio";
100
+ return {
101
+ id: `lobehub:${raw.identifier}`,
102
+ slug: raw.identifier,
103
+ name: raw.name || raw.identifier,
104
+ description: raw.description || "",
105
+ category: raw.category || "",
106
+ transport,
107
+ envRequired: Array.isArray(raw.envRequired) ? raw.envRequired : [],
108
+ installable: Boolean(raw.identifier),
109
+ homepage: raw.homepage || raw.github?.url || `https://market.lobehub.com/s/plugins/${raw.identifier}`,
110
+ owner: raw.author || "",
111
+ installs: Number(raw.installCount) || 0,
112
+ rating: Number(raw.ratingAverage) || 0,
113
+ ratingCount: Number(raw.ratingCount) || 0,
114
+ stars: Number(raw.github?.stars) || 0,
115
+ featured: Boolean(raw.isFeatured),
116
+ validated: Boolean(raw.isValidated),
117
+ source: "lobehub",
118
+ sourceLabel: "LobeHub",
119
+ manifestUrl: raw.manifestUrl || "",
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Search the LobeHub marketplace. Registered credentials are reused; a first run without any
125
+ * registers this plugin automatically so the tab is never dead on arrival.
126
+ */
127
+ export async function searchLobeHubMarket({ query = "", pageSize = 24, category = "", sort = "", page = 1, cache } = {}) {
128
+ // The API rejects `--page-size 60` with "Bad Request"; 40 answers reliably.
129
+ const size = Math.max(1, Math.min(40, Number(pageSize) || 24));
130
+ const pageNumber = Math.max(1, Number(page) || 1);
131
+ const key = `lobehub_market:${String(query).toLowerCase()}:${category}:${sort}:${pageNumber}:${size}`;
132
+ if (cache) {
133
+ const hit = await cache.get(key, 10 * 60 * 1000);
134
+ if (hit) return hit;
135
+ }
136
+ const hasQuery = Boolean(String(query).trim());
137
+ const args = ["mcp", "search", "--output", "json", "--page", String(pageNumber), "--page-size", String(size)];
138
+ if (hasQuery) args.push("--q", String(query).trim());
139
+ if (category) args.push("--category", category);
140
+ // `--sort` without a query is rejected ("Bad Request"), so browsering keeps the API order
141
+ // and the requested ordering is applied to the returned page locally.
142
+ if (sort && hasQuery) args.push("--sort", sort);
143
+
144
+ let text;
145
+ try {
146
+ text = await runCli(args);
147
+ } catch (error) {
148
+ // A missing or rejected registration is the one failure worth repairing automatically.
149
+ if (lobehubRegistered()) throw error;
150
+ await registerLobeHubClient();
151
+ text = await runCli(args, 120000);
152
+ }
153
+ const data = parseJsonOutput(text);
154
+ const items = (data.items || []).map(mapItem);
155
+ if (sort === "installCount" && !hasQuery) items.sort((a, b) => (b.installs || 0) - (a.installs || 0));
156
+ const result = {
157
+ source: "lobehub",
158
+ sourceLabel: "LobeHub",
159
+ items,
160
+ total: Number(data.totalCount) || items.length,
161
+ categories: Array.isArray(data.categories) ? data.categories : [],
162
+ page: Number(data.currentPage) || 1,
163
+ pageSize: Number(data.pageSize) || size,
164
+ totalPages: Number(data.totalPages) || 1,
165
+ };
166
+ if (cache) await cache.put(key, result);
167
+ return result;
168
+ }
169
+
170
+ /** Turn one manifest deployment option into the plugin's MCP definition shape. */
171
+ function definitionFromManifest(manifest, identifier) {
172
+ const options = Array.isArray(manifest.deploymentOptions) ? manifest.deploymentOptions : [];
173
+ const withCommand = options.find((option) => option.connection && option.connection.command);
174
+ const withUrl = options.find((option) => option.connection && option.connection.url);
175
+ const chosen = withCommand || withUrl || options[0] || {};
176
+ const connection = chosen.connection || {};
177
+ const schema = connection.configSchema && connection.configSchema.properties ? connection.configSchema.properties : {};
178
+ const env = {};
179
+ for (const [name, spec] of Object.entries(connection.env || schema)) {
180
+ env[name] = typeof spec === "string" ? spec : String(spec?.default ?? spec?.example ?? "");
181
+ }
182
+ const isHttp = Boolean(connection.url);
183
+ return {
184
+ title: manifest.name || identifier,
185
+ description: manifest.description || "",
186
+ transport: isHttp ? "streamable-http" : "stdio",
187
+ command: isHttp ? "" : connection.command || "",
188
+ args: Array.isArray(connection.args) ? connection.args : [],
189
+ url: connection.url || "",
190
+ env,
191
+ headers: connection.headers && typeof connection.headers === "object" ? connection.headers : {},
192
+ homepage: manifest.homepage || "",
193
+ category: manifest.category || "",
194
+ source: "lobehub",
195
+ slug: identifier,
196
+ envHints: Object.fromEntries(
197
+ Object.entries(connection.env || schema).map(([name, spec]) => [name, String((spec && (spec.description || spec.title)) || "")]),
198
+ ),
199
+ };
200
+ }
201
+
202
+ /** Read one plugin manifest (public endpoint) and reduce it to a runnable definition. */
203
+ export async function lobehubManifestDefinition(identifier) {
204
+ const url = `${LOBEHUB_MARKET_BASE}/api/v1/plugins/${encodeURIComponent(identifier)}/manifest`;
205
+ const manifest = await getJson(url, { timeoutMs: 25000 });
206
+ const definition = definitionFromManifest(manifest, identifier);
207
+ if (!definition.command && !definition.url) {
208
+ throw new Error(`LobeHub 条目 "${identifier}" 没有声明可用的启动方式(既无命令也无远程地址)`);
209
+ }
210
+ return definition;
211
+ }