@infimind-next/ai-hive-mcp 0.3.3 → 0.4.1
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 +78 -12
- package/dist/api-client.d.ts +15 -0
- package/dist/api-client.js +89 -9
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +34 -0
- package/dist/config.d.ts +19 -1
- package/dist/config.js +104 -7
- package/dist/index.js +9 -1
- package/dist/managed-config.d.ts +75 -0
- package/dist/managed-config.js +71 -0
- package/dist/managed-download.d.ts +13 -0
- package/dist/managed-download.js +138 -0
- package/dist/managed-files.d.ts +35 -0
- package/dist/managed-files.js +240 -0
- package/dist/managed-images.d.ts +70 -0
- package/dist/managed-images.js +449 -0
- package/dist/managed-server.d.ts +15 -0
- package/dist/managed-server.js +195 -0
- package/dist/managed-token-provider.d.ts +11 -0
- package/dist/managed-token-provider.js +31 -0
- package/dist/rest-commands.d.ts +14 -0
- package/dist/rest-commands.js +164 -0
- package/dist/schemas.d.ts +118 -10
- package/dist/schemas.js +79 -0
- package/dist/server.d.ts +4 -3
- package/dist/server.js +116 -34
- package/dist/setup/detect.d.ts +9 -0
- package/dist/setup/detect.js +34 -0
- package/dist/setup/merge.d.ts +5 -0
- package/dist/setup/merge.js +142 -0
- package/dist/setup/run-setup.d.ts +20 -0
- package/dist/setup/run-setup.js +332 -0
- package/dist/setup/skills.d.ts +8 -0
- package/dist/setup/skills.js +35 -0
- package/dist/setup/targets.d.ts +14 -0
- package/dist/setup/targets.js +46 -0
- package/package.json +11 -9
- package/skills/ai-hive-billing/SKILL.md +33 -0
- package/skills/ai-hive-image-video-generation/SKILL.md +41 -0
- package/skills/ai-hive-media-upload/SKILL.md +33 -0
- package/skills/ai-hive-model-selection/SKILL.md +50 -0
- package/skills/ai-hive-text-generation/SKILL.md +39 -0
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toolResultContent } from "./user-notices.js";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, } from "./schemas.js";
|
|
4
|
+
import { billingQueryInputSchema, listBillingRecordsInputSchema, getBillingRecordInputSchema, billingQueryShape, listBillingRecordsShape, getBillingRecordShape, chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, listGenerationTasksShape, } from "./schemas.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
|
|
7
7
|
import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
|
|
@@ -9,7 +9,7 @@ import { readMcpClientInfo } from "./client-info.js";
|
|
|
9
9
|
import { createUpdateNotifier } from "./update-notifier.js";
|
|
10
10
|
import { uploadMediaFromPath } from "./local-media.js";
|
|
11
11
|
import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
|
|
12
|
-
export const SERVER_VERSION = "0.
|
|
12
|
+
export const SERVER_VERSION = "0.4.1";
|
|
13
13
|
const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
|
|
14
14
|
const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 list_models 返回的快照对象,也可以省略该字段由客户端自动获取";
|
|
15
15
|
const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
@@ -17,7 +17,7 @@ const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId
|
|
|
17
17
|
const MODEL_UNAVAILABLE_MESSAGE = "模型当前不可用或不支持该路由模式,请重新调用 list_models 获取最新模型信息";
|
|
18
18
|
// 此文案与 @ai-hive/contracts 的 USER_INFO_TOOL_* 常量保持一致; 独立发布的客户端不引入运行依赖.
|
|
19
19
|
const USER_INFO_TOOL_TITLE = "查看蜂巢账户信息";
|
|
20
|
-
const USER_INFO_TOOL_DESCRIPTION = "
|
|
20
|
+
const USER_INFO_TOOL_DESCRIPTION = "查看你的蜂巢账户资料、可用积分和充值入口。";
|
|
21
21
|
function content(value) {
|
|
22
22
|
const result = Array.isArray(value)
|
|
23
23
|
? { models: value }
|
|
@@ -130,14 +130,16 @@ export function createToolDefinitions(client) {
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
name: "list_models",
|
|
133
|
-
|
|
133
|
+
title: "查询可用模型与价格",
|
|
134
|
+
description: "按 TEXT、IMAGE 或 VIDEO 查询可用模型与价格,售价单位是蜂蜜。detail=lite 用于选型,detail=full 返回完整契约。",
|
|
134
135
|
schema: listModelsShape,
|
|
135
136
|
annotations: { readOnlyHint: false },
|
|
136
137
|
handler: async (args) => content(await client().listModels(args, { announcements: true })),
|
|
137
138
|
},
|
|
138
139
|
{
|
|
139
140
|
name: "get_model",
|
|
140
|
-
|
|
141
|
+
title: "查询模型调用契约",
|
|
142
|
+
description: "按 publicModelId 查询模型的参数结构、媒体要求、计费因素和调用示例。调用或生成前应以此结果为准;传 routingMode 可只取该线路的契约。",
|
|
141
143
|
schema: getModelShape,
|
|
142
144
|
annotations: { readOnlyHint: false },
|
|
143
145
|
handler: async (args) => {
|
|
@@ -147,18 +149,20 @@ export function createToolDefinitions(client) {
|
|
|
147
149
|
},
|
|
148
150
|
{
|
|
149
151
|
name: "upload_media_from_path",
|
|
150
|
-
|
|
151
|
-
description: "
|
|
152
|
+
title: "上传本地媒体",
|
|
153
|
+
description: "上传本机图片、视频、音频或文档并返回 mediaId。图片理解和生成前的素材都先走这一步。",
|
|
152
154
|
schema: {
|
|
153
155
|
path: z.string().min(1),
|
|
154
156
|
filename: z.string().min(1).optional(),
|
|
155
157
|
contentType: z.string().min(1).optional(),
|
|
156
158
|
},
|
|
159
|
+
annotations: { readOnlyHint: false },
|
|
157
160
|
handler: async (args) => content(await uploadMediaFromPath(client(), args)),
|
|
158
161
|
},
|
|
159
162
|
{
|
|
160
163
|
name: "create_media_upload",
|
|
161
|
-
|
|
164
|
+
title: "创建媒体上传",
|
|
165
|
+
description: "为媒体文件创建短期上传地址。传 filename、contentType、sizeBytes,再按返回的 method 和 headers 把文件原始字节 PUT 到该地址。",
|
|
162
166
|
schema: {
|
|
163
167
|
filename: z.string().min(1),
|
|
164
168
|
contentType: z.string().min(1),
|
|
@@ -169,7 +173,8 @@ export function createToolDefinitions(client) {
|
|
|
169
173
|
},
|
|
170
174
|
{
|
|
171
175
|
name: "complete_media_upload",
|
|
172
|
-
|
|
176
|
+
title: "确认媒体上传完成",
|
|
177
|
+
description: "原始字节 PUT 完成后校验对象大小和类型,把 mediaId 标记为可用于模型调用。",
|
|
173
178
|
schema: { mediaId: z.string().min(1) },
|
|
174
179
|
annotations: { readOnlyHint: false },
|
|
175
180
|
handler: async (args) => content(await client().completeMediaUpload(String(args.mediaId), {
|
|
@@ -178,7 +183,8 @@ export function createToolDefinitions(client) {
|
|
|
178
183
|
},
|
|
179
184
|
{
|
|
180
185
|
name: "chat_text",
|
|
181
|
-
|
|
186
|
+
title: "调用文本模型",
|
|
187
|
+
description: "用指定模型生成文本,是付费调用。",
|
|
182
188
|
schema: chatTextShape,
|
|
183
189
|
annotations: { readOnlyHint: false },
|
|
184
190
|
handler: async (args) => {
|
|
@@ -189,7 +195,8 @@ export function createToolDefinitions(client) {
|
|
|
189
195
|
},
|
|
190
196
|
{
|
|
191
197
|
name: "generate_image",
|
|
192
|
-
|
|
198
|
+
title: "生成图片",
|
|
199
|
+
description: "用指定模型生成图片,是付费调用;dryRun=true 可先检查输入并查看预估费用(单位是蜂蜜)。",
|
|
193
200
|
schema: generateImageShape,
|
|
194
201
|
annotations: { readOnlyHint: false },
|
|
195
202
|
handler: async (args) => {
|
|
@@ -199,7 +206,8 @@ export function createToolDefinitions(client) {
|
|
|
199
206
|
},
|
|
200
207
|
{
|
|
201
208
|
name: "generate_video",
|
|
202
|
-
|
|
209
|
+
title: "生成视频",
|
|
210
|
+
description: "用指定模型生成视频,是付费调用;dryRun=true 可先检查参数、素材和预估费用(单位是蜂蜜)。缺素材可以估价,但不能正式生成。",
|
|
203
211
|
schema: generateVideoShape,
|
|
204
212
|
annotations: { readOnlyHint: false },
|
|
205
213
|
handler: async (args) => {
|
|
@@ -209,15 +217,16 @@ export function createToolDefinitions(client) {
|
|
|
209
217
|
},
|
|
210
218
|
{
|
|
211
219
|
name: "get_material_mode",
|
|
212
|
-
|
|
213
|
-
description: "
|
|
220
|
+
title: "查询素材认证资格",
|
|
221
|
+
description: "查询素材认证资格和授权页面。授权必须由用户本人完成;未授权时把 actionUrl 交给用户阅读并同意,不要代替用户操作。",
|
|
214
222
|
schema: {},
|
|
223
|
+
annotations: { readOnlyHint: false },
|
|
215
224
|
handler: async () => content(await client().getMaterialMode({ announcements: true })),
|
|
216
225
|
},
|
|
217
226
|
{
|
|
218
227
|
name: "quote_video_materials",
|
|
219
|
-
|
|
220
|
-
description: "
|
|
228
|
+
title: "查询认证素材报价",
|
|
229
|
+
description: "为认证视频素材报价并冻结素材,返回的 id 作为 materialQuoteId 传给 generate_video;此工具可选,费用变化时先向用户确认。",
|
|
221
230
|
schema: {
|
|
222
231
|
publicModelId: generateVideoShape.publicModelId,
|
|
223
232
|
routingMode: generateVideoShape.routingMode,
|
|
@@ -230,6 +239,7 @@ export function createToolDefinitions(client) {
|
|
|
230
239
|
pricingSnapshot: generateVideoShape.pricingSnapshot,
|
|
231
240
|
params: generateVideoShape.params,
|
|
232
241
|
},
|
|
242
|
+
annotations: { readOnlyHint: false },
|
|
233
243
|
handler: async (args) => {
|
|
234
244
|
const body = await resolvePricingSnapshot(client, "VIDEO", {
|
|
235
245
|
...args,
|
|
@@ -239,9 +249,83 @@ export function createToolDefinitions(client) {
|
|
|
239
249
|
return content(await client().quoteVideoMaterials(body, { announcements: true }));
|
|
240
250
|
},
|
|
241
251
|
},
|
|
252
|
+
{
|
|
253
|
+
name: "get_billing_summary",
|
|
254
|
+
title: "查询账单汇总",
|
|
255
|
+
description: "查询当前账户在网页、API、MCP 等全部入口的实际结算消费,数值单位是蜂蜜;预占和释放单列,不算消费。不填时间默认北京时间今日,自选时间按 [from,to) 查询。查指定生成批次传 taskIds,并覆盖全部结算日期;积分差不能代替账单。任务缺失或结算未完成时,合计不是最终费用。",
|
|
256
|
+
schema: billingQueryShape,
|
|
257
|
+
inputSchema: z.object(billingQueryShape).strict(),
|
|
258
|
+
annotations: {
|
|
259
|
+
readOnlyHint: true,
|
|
260
|
+
idempotentHint: true,
|
|
261
|
+
destructiveHint: false,
|
|
262
|
+
openWorldHint: false,
|
|
263
|
+
},
|
|
264
|
+
handler: async (args) => {
|
|
265
|
+
const api = client();
|
|
266
|
+
if (!api.getBillingSummary)
|
|
267
|
+
throw new Error("当前客户端暂不支持账单查询,请更新客户端后重试");
|
|
268
|
+
return content(await api.getBillingSummary(billingQueryInputSchema.parse(args)));
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "list_billing_records",
|
|
273
|
+
title: "查询账单记录",
|
|
274
|
+
description: "分页查询当前账户全部入口的账单记录,数值单位是蜂蜜;实际结算消费与预占、释放分别展示,预占和释放不算消费。时间为 [from,to),不填默认北京时间今日;用 taskIds 查指定批次,积分差不能代替账单。",
|
|
275
|
+
schema: listBillingRecordsShape,
|
|
276
|
+
inputSchema: z.object(listBillingRecordsShape).strict(),
|
|
277
|
+
annotations: {
|
|
278
|
+
readOnlyHint: true,
|
|
279
|
+
idempotentHint: true,
|
|
280
|
+
destructiveHint: false,
|
|
281
|
+
openWorldHint: false,
|
|
282
|
+
},
|
|
283
|
+
handler: async (args) => {
|
|
284
|
+
const api = client();
|
|
285
|
+
if (!api.listBillingRecords)
|
|
286
|
+
throw new Error("当前客户端暂不支持账单查询,请更新客户端后重试");
|
|
287
|
+
return content(await api.listBillingRecords(listBillingRecordsInputSchema.parse(args)));
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "get_billing_record",
|
|
292
|
+
title: "查询账单详情",
|
|
293
|
+
description: "按 recordId 查询当前账户的账单详情,数值单位是蜂蜜;区分实际结算消费与预占、释放,预占和释放不算消费。记录 ID 来自 list_billing_records。",
|
|
294
|
+
schema: getBillingRecordShape,
|
|
295
|
+
inputSchema: getBillingRecordInputSchema,
|
|
296
|
+
annotations: {
|
|
297
|
+
readOnlyHint: true,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
destructiveHint: false,
|
|
300
|
+
openWorldHint: false,
|
|
301
|
+
},
|
|
302
|
+
handler: async (args) => {
|
|
303
|
+
const api = client();
|
|
304
|
+
if (!api.getBillingRecord)
|
|
305
|
+
throw new Error("当前客户端暂不支持账单查询,请更新客户端后重试");
|
|
306
|
+
return content(await api.getBillingRecord(getBillingRecordInputSchema.parse(args)));
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "list_generation_tasks",
|
|
311
|
+
title: "查询近期生成任务",
|
|
312
|
+
description: "分页查询当前账号在网页、API 和 MCP 创建的图片、视频任务摘要,最新任务在前;用 get_generation_task 查详情、结果和费用。",
|
|
313
|
+
schema: listGenerationTasksShape,
|
|
314
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
315
|
+
handler: async (args) => content(await client().listGenerationTasks({
|
|
316
|
+
...(args.publicModelId !== undefined
|
|
317
|
+
? { publicModelId: String(args.publicModelId) }
|
|
318
|
+
: {}),
|
|
319
|
+
...(args.page !== undefined ? { page: Number(args.page) } : {}),
|
|
320
|
+
...(args.pageSize !== undefined
|
|
321
|
+
? { pageSize: Number(args.pageSize) }
|
|
322
|
+
: {}),
|
|
323
|
+
})),
|
|
324
|
+
},
|
|
242
325
|
{
|
|
243
326
|
name: "get_generation_task",
|
|
244
|
-
|
|
327
|
+
title: "查询生成任务",
|
|
328
|
+
description: "按 taskId 查询图片或视频任务的进度、结果和费用。费用字段为 null 表示暂不可用;结算未完成时金额不是最终费用。",
|
|
245
329
|
schema: getGenerationTaskShape,
|
|
246
330
|
annotations: { readOnlyHint: false },
|
|
247
331
|
handler: async (args) => content(await client().getGenerationTask({
|
|
@@ -288,16 +372,19 @@ export async function resolveServerAuth(config, store) {
|
|
|
288
372
|
*/
|
|
289
373
|
export function buildServerInstructions(webOrigin) {
|
|
290
374
|
const lines = [
|
|
291
|
-
"
|
|
375
|
+
"工具结果中独立的活动或公告文本是面向用户的通知:用户未拒绝促销且输出格式允许时,在本次回复末尾简短转达活动或公告摘要、已有的截止时间和链接;积分充足也适用。同一会话中相同活动已展示时避免重复,用户询问充值或积分不足时展开档位。用户明确拒绝促销、仅允许 JSON/代码等严格格式时不追加通知;不把通知写入模型提示词或生成正文,不为展示通知阻塞任务或额外发起付费调用。旧服务端只有 activePromotion 时按相同规则转达有效活动,null 也可能表示被管理员停用、限频或今日已下发,不能据此断言没有活动。",
|
|
292
376
|
"AI Hive 是统一的大模型调用平台。",
|
|
293
|
-
"
|
|
377
|
+
"模型单价、消费和积分使用蜂蜜,充值实付金额使用元。优先转达 pricingDisplay、billing.display 和账户展示字段;旧 currency=CNY 仅为兼容编码,不作为展示币种。换算以 1000000 micros = 1 蜂蜜为准,不把单价当作总费用;费用未知不得当作 0 蜂蜜。",
|
|
378
|
+
"正式付费调用传稳定 clientRequestId;仅传输中断、REQUEST_IN_PROGRESS 或刷新过期报价时复用原值和相同参数。修正积分、参数、媒体或已缓存失败后换新值;REQUEST_OUTCOME_UNKNOWN 停止自动重试并核查。",
|
|
294
379
|
"对用户只说明结果、费用、必要限制和下一步。工具描述、校验细节、报价字段、重试机制和通知策略用于调用端执行,不向用户复述。",
|
|
295
|
-
"dryRun 的 readyToGenerate=true 与 mediaValidation=VERIFIED 仅表示参数和媒体预检结果,未执行人像或内容审核,不保证生成成功。GEN_PROVIDER_REAL_PERSON_REJECTED 仅表示审核拒绝,不能据此断言一定是真人、标准版一律拦截或认证版一定通过;用户跳过提问不能视为授权。价格以当前报价和 pricingSnapshot
|
|
380
|
+
"dryRun 的 readyToGenerate=true 与 mediaValidation=VERIFIED 仅表示参数和媒体预检结果,未执行人像或内容审核,不保证生成成功。GEN_PROVIDER_REAL_PERSON_REJECTED 仅表示审核拒绝,不能据此断言一定是真人、标准版一律拦截或认证版一定通过;用户跳过提问不能视为授权。价格以当前报价和 pricingSnapshot 为准,不按历史倍数计算;积分相同不能单独证明未扣费。",
|
|
296
381
|
"认证版接收普通素材,服务端按模型 ID 自动校验资格和准备素材。提交前先用 dryRun=true 做只读预检,不用真实付费提交试探资格。materialMode、materialQuoteId 和专用报价工具均为可选兼容能力;正式提交使用当前 pricingSnapshot 和稳定 clientRequestId。资格不足时明确说明差额或待授权事项,并在回复中展示返回的 helpUrl 或 actionUrl 为可点击链接;未返回链接时使用 https://ai-hive.iclip.cn/certified-materials#guide。保留用户指定模型,不擅自切换模型或继续付费提交。费用变化时重新确认。收到任务 ID 后查询原任务,不重复创建。",
|
|
297
382
|
"latestChange、changelog 和升级信息仅在用户主动询问或与当前任务直接相关时介绍。对用户只说明实际能力、费用、限制、结果和必要操作,不转述字段名、错误码、报价 ID、幂等机制、版本适配或内部排查过程;技术字段和 recoveryAction 供调用端处理,不能替代用户操作说明。用户明确排查接入问题时可以解释所需技术细节。",
|
|
298
|
-
"可用 get_user_info
|
|
299
|
-
"
|
|
300
|
-
"
|
|
383
|
+
"可用 get_user_info 查询蜂蜜积分、官网地址和充值入口。按 get_user_info 返回的 rechargeGuide 说明账户支付方式,不主动介绍账户未拥有的额度。不能仅凭钱包积分为 0 拒绝调用。只有实际调用返回积分不足时,才引导用户打开 rechargeUrl 充值后重试。",
|
|
384
|
+
"积分不足时将返回的 rechargeUrl 展示为可点击链接;充值完成后等待用户确认继续,再按 recoveryAction 恢复原任务,不得自动发起付费重试。保留原模型、参数和费用确认,已返回任务 ID 时只查询原任务。",
|
|
385
|
+
"pricingSnapshot 支持对象、数组或 JSON 字符串;数组按 publicModelId 和 routingMode 唯一匹配,不能取第一项或猜测价格。缺少或无法匹配时先读取 get_model,使用当前匹配快照;费用变化后重新取得用户确认。",
|
|
386
|
+
"先用 list_models(detail=lite) 选型,再用 get_model 读取所选模型契约。上传前可省略媒体做 dryRun:缺素材不等于参数错误,费用未知不得当作 0 蜂蜜或上限。用户接受方案后上传,再带真实媒体预检;同参数同费用不重复确认,费用或条件变化才再确认。正式调用原样回传最后一次预检的 pricingSnapshot,费用数值保留字符串;仅 materialQuoteId 可自动准备,不能据此省略 pricingSnapshot。",
|
|
387
|
+
"充值提醒只依据本次返回的 activePromotion;有值时用 title、headline 和 promoTiers 转达本场「限时特惠 · 充值加赠」,实付金额与入账积分沿用服务端结果,使用「距本场结束」说明 endsAt,并提供 rechargeUrl 的「立即充值」入口。模型调用返回 null 时不追加活动提醒;get_user_info 返回 null 时不推断具体原因。缓存或幂等重放中的 endsAt 已过期时,先重新查询 get_user_info,以新结果为准,不沿用旧截止时间或推算后续活动。",
|
|
301
388
|
"调用 chat_text、generate_image、generate_video 前,用 get_model 确认参数契约;正式调用必须传稳定的 clientRequestId 幂等键,修正参数后换新值。",
|
|
302
389
|
"chat_text 同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。",
|
|
303
390
|
];
|
|
@@ -338,17 +425,12 @@ export function createServer(input) {
|
|
|
338
425
|
throw error;
|
|
339
426
|
}
|
|
340
427
|
};
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}, handler);
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
server.tool(tool.name, tool.description, tool.schema, handler);
|
|
351
|
-
}
|
|
428
|
+
server.registerTool(tool.name, {
|
|
429
|
+
title: tool.title,
|
|
430
|
+
description: tool.description,
|
|
431
|
+
inputSchema: tool.inputSchema ?? tool.schema,
|
|
432
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
433
|
+
}, handler);
|
|
352
434
|
}
|
|
353
435
|
return server;
|
|
354
436
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClientTarget } from "./targets.js";
|
|
2
|
+
export type PathExists = (path: string) => Promise<boolean>;
|
|
3
|
+
export type DetectInput = {
|
|
4
|
+
home: string;
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
env: Record<string, string | undefined>;
|
|
7
|
+
exists: PathExists;
|
|
8
|
+
};
|
|
9
|
+
export declare function detectTarget(target: ClientTarget, input: DetectInput): Promise<boolean>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export async function detectTarget(target, input) {
|
|
3
|
+
switch (target.id) {
|
|
4
|
+
case "claude-code":
|
|
5
|
+
return ((await input.exists(join(input.home, ".claude"))) ||
|
|
6
|
+
(await executableOnPath("claude", input)));
|
|
7
|
+
case "cursor":
|
|
8
|
+
return ((await input.exists(join(input.home, ".cursor"))) ||
|
|
9
|
+
(await input.exists(join(input.home, "Library/Application Support/Cursor"))));
|
|
10
|
+
case "codex":
|
|
11
|
+
return ((await input.exists(join(input.home, ".codex"))) ||
|
|
12
|
+
(await executableOnPath("codex", input)));
|
|
13
|
+
case "claude-desktop":
|
|
14
|
+
return (target.configPath !== null &&
|
|
15
|
+
(await input.exists(join(target.configPath, ".."))));
|
|
16
|
+
case "opencode":
|
|
17
|
+
return ((await input.exists(join(input.home, ".config/opencode"))) ||
|
|
18
|
+
(await executableOnPath("opencode", input)));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function executableOnPath(name, input) {
|
|
22
|
+
const pathValue = input.env.PATH ?? "";
|
|
23
|
+
const extensions = input.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
|
|
24
|
+
for (const directory of pathValue.split(input.platform === "win32" ? ";" : ":")) {
|
|
25
|
+
if (!directory)
|
|
26
|
+
continue;
|
|
27
|
+
for (const extension of extensions) {
|
|
28
|
+
if (await input.exists(join(directory, `${name}${extension}`))) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function mergeJsonMcpServers(existing: string | null, name: string, server: unknown): string;
|
|
2
|
+
export declare function mergeOpenCodeServer(existing: string | null, name: string, server: unknown): string;
|
|
3
|
+
export declare function readJsonServerEnv(existing: string | null, field: "mcpServers" | "mcp", name: string): Record<string, string>;
|
|
4
|
+
export declare function upsertCodexServer(existing: string | null, name: string, packageSpec: string, env: Record<string, string>): string;
|
|
5
|
+
export declare function readCodexEnv(lines: string[], targetTable: string): Record<string, string>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// 所有写入都做"读旧值 -> 只改自己的键 -> 整体写回", 避免覆盖用户已有的 MCP 配置.
|
|
2
|
+
export function mergeJsonMcpServers(existing, name, server) {
|
|
3
|
+
const document = parseJsonObject(existing);
|
|
4
|
+
const servers = readRecord(document, "mcpServers");
|
|
5
|
+
return serialize({
|
|
6
|
+
...document,
|
|
7
|
+
mcpServers: { ...servers, [name]: server },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function mergeOpenCodeServer(existing, name, server) {
|
|
11
|
+
const document = parseJsonObject(existing);
|
|
12
|
+
const servers = readRecord(document, "mcp");
|
|
13
|
+
return serialize({
|
|
14
|
+
...document,
|
|
15
|
+
$schema: typeof document.$schema === "string"
|
|
16
|
+
? document.$schema
|
|
17
|
+
: "https://opencode.ai/config.json",
|
|
18
|
+
mcp: { ...servers, [name]: server },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// 重跑时无密钥也要保留用户已有的 env, 否则一次没导出环境变量的执行就会把配置降级.
|
|
22
|
+
export function readJsonServerEnv(existing, field, name) {
|
|
23
|
+
const document = parseJsonObject(existing);
|
|
24
|
+
const servers = readRecord(document, field);
|
|
25
|
+
const server = servers[name];
|
|
26
|
+
if (typeof server !== "object" || server === null || Array.isArray(server)) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
const env = server[field === "mcp" ? "environment" : "env"];
|
|
30
|
+
return readStringRecord(env);
|
|
31
|
+
}
|
|
32
|
+
export function upsertCodexServer(existing, name, packageSpec, env) {
|
|
33
|
+
const source = existing ?? "";
|
|
34
|
+
const lines = source.split("\n");
|
|
35
|
+
const targetTable = `mcp_servers.${name}`;
|
|
36
|
+
const start = lines.findIndex((line) => tableName(line) === targetTable);
|
|
37
|
+
if (start === -1) {
|
|
38
|
+
const prefix = source.length === 0 || source.endsWith("\n") ? "" : "\n";
|
|
39
|
+
const separator = source.trim().length === 0 ? "" : "\n";
|
|
40
|
+
return `${source}${prefix}${separator}${codexBlock(name, packageSpec, env)}\n`;
|
|
41
|
+
}
|
|
42
|
+
let end = start + 1;
|
|
43
|
+
while (end < lines.length) {
|
|
44
|
+
const table = tableName(lines[end]);
|
|
45
|
+
// 只吞掉同名表及其子表; [mcp_servers.ai_hive_backup] 是另一张表, 必须保留.
|
|
46
|
+
if (table !== null &&
|
|
47
|
+
table !== targetTable &&
|
|
48
|
+
!table.startsWith(`${targetTable}.`)) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
end += 1;
|
|
52
|
+
}
|
|
53
|
+
const blockLines = lines.slice(start, end);
|
|
54
|
+
const mergedEnv = {
|
|
55
|
+
...readCodexEnv(blockLines, targetTable),
|
|
56
|
+
...env,
|
|
57
|
+
};
|
|
58
|
+
const before = lines.slice(0, start).join("\n");
|
|
59
|
+
const after = lines.slice(end).join("\n");
|
|
60
|
+
return ([before, codexBlock(name, packageSpec, mergedEnv), after]
|
|
61
|
+
.filter((part) => part.trim().length > 0)
|
|
62
|
+
.join("\n") + "\n");
|
|
63
|
+
}
|
|
64
|
+
export function readCodexEnv(lines, targetTable) {
|
|
65
|
+
const envTable = `${targetTable}.env`;
|
|
66
|
+
const env = {};
|
|
67
|
+
let inside = false;
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const table = tableName(line);
|
|
70
|
+
if (table !== null) {
|
|
71
|
+
inside = table === envTable;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!inside)
|
|
75
|
+
continue;
|
|
76
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"(.*)"\s*$/.exec(line.trim());
|
|
77
|
+
if (match) {
|
|
78
|
+
env[match[1]] = match[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
83
|
+
function codexBlock(name, packageSpec, env) {
|
|
84
|
+
const lines = [
|
|
85
|
+
`[mcp_servers.${name}]`,
|
|
86
|
+
'command = "npx"',
|
|
87
|
+
`args = ["-y", ${JSON.stringify(packageSpec)}]`,
|
|
88
|
+
];
|
|
89
|
+
const entries = Object.entries(env);
|
|
90
|
+
if (entries.length > 0) {
|
|
91
|
+
lines.push("", `[mcp_servers.${name}.env]`);
|
|
92
|
+
for (const [key, value] of entries) {
|
|
93
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
98
|
+
// 表头可能带行尾注释; 先剥注释再比较, 避免漏掉合法写法导致追加重复表.
|
|
99
|
+
function tableName(line) {
|
|
100
|
+
const stripped = line.split("#")[0].trim();
|
|
101
|
+
const match = /^\[([^\]]+)\]$/.exec(stripped);
|
|
102
|
+
return match ? match[1].trim() : null;
|
|
103
|
+
}
|
|
104
|
+
function readRecord(document, field) {
|
|
105
|
+
const current = document[field];
|
|
106
|
+
if (current === undefined) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
if (typeof current !== "object" ||
|
|
110
|
+
current === null ||
|
|
111
|
+
Array.isArray(current)) {
|
|
112
|
+
throw new Error(field === "mcpServers"
|
|
113
|
+
? "配置文件里的 mcpServers 不是对象,请手动添加配置"
|
|
114
|
+
: "配置文件里的 mcp 不是对象,请手动添加配置");
|
|
115
|
+
}
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
function readStringRecord(value) {
|
|
119
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => typeof item === "string" ? [[key, item]] : []));
|
|
123
|
+
}
|
|
124
|
+
function parseJsonObject(existing) {
|
|
125
|
+
if (existing === null || existing.trim().length === 0) {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
let value;
|
|
129
|
+
try {
|
|
130
|
+
value = JSON.parse(existing);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error("配置文件不是有效 JSON,请手动添加配置");
|
|
134
|
+
}
|
|
135
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
136
|
+
throw new Error("配置文件顶层不是对象,请手动添加配置");
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function serialize(value) {
|
|
141
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { McpClientConfig } from "../config.js";
|
|
2
|
+
export declare const MCP_SERVER_NAME = "ai-hive";
|
|
3
|
+
export declare const MCP_CODEX_SERVER_NAME = "ai_hive";
|
|
4
|
+
export declare const MCP_PACKAGE_SPEC = "@infimind-next/ai-hive-mcp@latest";
|
|
5
|
+
type CommandRunner = (command: string, args: string[]) => Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
export type SetupDependencies = {
|
|
9
|
+
home: string;
|
|
10
|
+
platform: NodeJS.Platform;
|
|
11
|
+
env: Record<string, string | undefined>;
|
|
12
|
+
skillsRoot: string;
|
|
13
|
+
stdout(line: string): void;
|
|
14
|
+
stderr(line: string): void;
|
|
15
|
+
runCommand?: CommandRunner;
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
};
|
|
18
|
+
type SetupConfig = Pick<McpClientConfig, "key" | "endpoint" | "endpointExplicit" | "webOrigin" | "webOriginExplicit" | "credentialsFile" | "timeoutMs" | "setup">;
|
|
19
|
+
export declare function runSetup(config: SetupConfig, dependencies: SetupDependencies): Promise<number>;
|
|
20
|
+
export {};
|