@infimind-next/ai-hive-mcp 0.2.5 → 0.3.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 +48 -0
- package/dist/api-client.d.ts +2 -0
- package/dist/api-client.js +66 -13
- package/dist/local-media.js +13 -0
- package/dist/schemas.d.ts +22 -12
- package/dist/schemas.js +48 -10
- package/dist/server.d.ts +3 -2
- package/dist/server.js +112 -6
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @infimind-next/ai-hive-mcp
|
|
2
|
+
|
|
3
|
+
蜂巢 AI 的本地 MCP 服务,让你在自己的 MCP 客户端里调用文本、图片和视频模型。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
在 MCP 客户端中加入以下配置,把 `<APIKEY>` 换成你在蜂巢 AI 网页端「MCP 接入」里生成的 API Key:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"ai-hive": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@infimind-next/ai-hive-mcp@0.3.0", "--key", "<APIKEY>"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
要求 Node.js >= 20.19.0。本包通过标准 MCP stdio 与客户端通信,仅保证与支持 MCP 的客户端配合使用。
|
|
21
|
+
|
|
22
|
+
## 工具
|
|
23
|
+
|
|
24
|
+
- `get_user_info`:查询当前用户和余额
|
|
25
|
+
- `list_models`:按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格
|
|
26
|
+
- `get_model`:查询单个模型的参数 schema、媒体要求、计费因素和调用示例
|
|
27
|
+
- `upload_media_from_path`:上传本地图片、视频或音频,返回 mediaId
|
|
28
|
+
- `chat_text`:文本对话
|
|
29
|
+
- `generate_image`:图片预检或正式生成
|
|
30
|
+
- `generate_video`:视频预检或正式生成
|
|
31
|
+
- `get_generation_task`:查询生成任务进度和结果
|
|
32
|
+
|
|
33
|
+
## 调用流程
|
|
34
|
+
|
|
35
|
+
图片和视频生成固定按 5 步走:
|
|
36
|
+
|
|
37
|
+
1. 用 `list_models` 选择模型,记录 `publicModelId`。
|
|
38
|
+
2. 用 `get_model` 查看参数契约,`params` 的字段名、类型、枚举值和默认值逐项来自返回的 `paramsSchema`。
|
|
39
|
+
3. 调用 `generate_image` 或 `generate_video` 时传 `dryRun: true` 预检,核对返回的 `estimatedReservationMicros` 估价(单位:人民币 micros,1 元 = 1,000,000 micros)。
|
|
40
|
+
4. 确认费用后去掉 `dryRun` 正式生成,`params` 替换为预检返回的 `resolvedParams`,并传入预检返回的 `pricingSnapshot` 和本次调用稳定唯一的 `clientRequestId`。
|
|
41
|
+
5. 用 `get_generation_task` 查询任务进度和结果,不重复创建任务。
|
|
42
|
+
|
|
43
|
+
## 重试与价格变化
|
|
44
|
+
|
|
45
|
+
- 传输中断或超时后重试:复用同一个 `clientRequestId` 和相同业务参数,不会重复扣费。
|
|
46
|
+
- 修正参数、余额或媒体问题后重试:必须使用新的 `clientRequestId`。
|
|
47
|
+
- 正式生成提示价格已更新:保持业务参数和原 `clientRequestId` 不变,重新预检获取最新 `pricingSnapshot` 后重试。
|
|
48
|
+
- 返回 `REQUEST_OUTCOME_UNKNOWN`:停止自动重试,联系管理员核查。
|
package/dist/api-client.d.ts
CHANGED
|
@@ -21,11 +21,13 @@ export declare class AiHiveApiClient {
|
|
|
21
21
|
private readResponse;
|
|
22
22
|
getUserInfo(): Promise<unknown>;
|
|
23
23
|
listModels(input: Record<string, unknown>): Promise<unknown>;
|
|
24
|
+
getModel(publicModelId: string, routingMode?: string): Promise<unknown>;
|
|
24
25
|
createMediaUpload(input: unknown): Promise<unknown>;
|
|
25
26
|
completeMediaUpload(mediaId: string): Promise<unknown>;
|
|
26
27
|
chatText(input: unknown): Promise<unknown>;
|
|
27
28
|
generateImage(input: unknown): Promise<unknown>;
|
|
28
29
|
generateVideo(input: unknown): Promise<unknown>;
|
|
30
|
+
private requestGeneration;
|
|
29
31
|
getGenerationTask(input: {
|
|
30
32
|
taskId: string;
|
|
31
33
|
}): Promise<unknown>;
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { encodeMcpClientInfo, MCP_CLIENT_INFO_HEADER, } from "./client-info.js";
|
|
2
|
+
class AiHiveApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "AiHiveApiError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const GET_MODEL_UNSUPPORTED_MESSAGE = "当前 AI-HIVE 服务端版本不支持 get_model,请先升级服务端后再试";
|
|
11
|
+
const DRY_RUN_UNSUPPORTED_MESSAGE = "当前 AI-HIVE 服务端版本不支持 dryRun,请先升级服务端后再试";
|
|
2
12
|
function formatIssue(value) {
|
|
3
13
|
if (typeof value === "string") {
|
|
4
14
|
return value;
|
|
@@ -34,6 +44,15 @@ function errorMessage(body) {
|
|
|
34
44
|
function timeoutError() {
|
|
35
45
|
return new Error("请求超时,请稍后重试");
|
|
36
46
|
}
|
|
47
|
+
function redactInternalPricing(value) {
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.map(redactInternalPricing);
|
|
50
|
+
}
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => key === "originalMicros" ? [] : [[key, redactInternalPricing(item)]]));
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
37
56
|
async function discardResponseBody(response) {
|
|
38
57
|
try {
|
|
39
58
|
await response.body?.cancel();
|
|
@@ -119,18 +138,39 @@ export class AiHiveApiClient {
|
|
|
119
138
|
async readResponse(response) {
|
|
120
139
|
const body = await response.json().catch(() => ({}));
|
|
121
140
|
if (!response.ok) {
|
|
122
|
-
throw new
|
|
141
|
+
throw new AiHiveApiError(response.status, errorMessage(body));
|
|
123
142
|
}
|
|
124
143
|
return body;
|
|
125
144
|
}
|
|
126
145
|
getUserInfo() {
|
|
127
146
|
return this.request("/mcp/user-info");
|
|
128
147
|
}
|
|
129
|
-
listModels(input) {
|
|
130
|
-
const query =
|
|
131
|
-
|
|
148
|
+
async listModels(input) {
|
|
149
|
+
const query = new URLSearchParams();
|
|
150
|
+
if (typeof input.modelType === "string") {
|
|
151
|
+
query.set("modelType", input.modelType);
|
|
152
|
+
}
|
|
153
|
+
if (typeof input.detail === "string") {
|
|
154
|
+
query.set("detail", input.detail);
|
|
155
|
+
}
|
|
156
|
+
const serialized = query.toString();
|
|
157
|
+
return redactInternalPricing(await this.request(`/mcp/models${serialized ? `?${serialized}` : ""}`));
|
|
158
|
+
}
|
|
159
|
+
// getModel 与 listModels 的 detail 参数依赖服务端新部署; 旧服务端会 404 或被
|
|
160
|
+
// strict 入参校验拒绝, 所以 npm 0.3.0 必须在服务端升级之后发布.
|
|
161
|
+
async getModel(publicModelId, routingMode) {
|
|
162
|
+
const query = routingMode
|
|
163
|
+
? `?${new URLSearchParams({ routingMode }).toString()}`
|
|
132
164
|
: "";
|
|
133
|
-
|
|
165
|
+
try {
|
|
166
|
+
return redactInternalPricing(await this.request(`/mcp/models/${encodeURIComponent(publicModelId)}${query}`));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof AiHiveApiError && error.status === 404) {
|
|
170
|
+
throw new Error(GET_MODEL_UNSUPPORTED_MESSAGE);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
134
174
|
}
|
|
135
175
|
createMediaUpload(input) {
|
|
136
176
|
return this.request("/mcp/media/upload-token", {
|
|
@@ -150,16 +190,29 @@ export class AiHiveApiClient {
|
|
|
150
190
|
});
|
|
151
191
|
}
|
|
152
192
|
generateImage(input) {
|
|
153
|
-
return this.
|
|
154
|
-
method: "POST",
|
|
155
|
-
body: JSON.stringify(input),
|
|
156
|
-
});
|
|
193
|
+
return this.requestGeneration("/mcp/generation/image", input);
|
|
157
194
|
}
|
|
158
195
|
generateVideo(input) {
|
|
159
|
-
return this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
return this.requestGeneration("/mcp/generation/video", input);
|
|
197
|
+
}
|
|
198
|
+
async requestGeneration(path, input) {
|
|
199
|
+
try {
|
|
200
|
+
return await this.request(path, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: JSON.stringify(input),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (typeof input === "object" &&
|
|
207
|
+
input !== null &&
|
|
208
|
+
input.dryRun === true &&
|
|
209
|
+
error instanceof AiHiveApiError &&
|
|
210
|
+
error.status === 400 &&
|
|
211
|
+
/\bdryRun\b/.test(error.message)) {
|
|
212
|
+
throw new Error(DRY_RUN_UNSUPPORTED_MESSAGE);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
163
216
|
}
|
|
164
217
|
getGenerationTask(input) {
|
|
165
218
|
return this.request(`/mcp/generation/tasks/${encodeURIComponent(input.taskId)}`);
|
package/dist/local-media.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mcpCreateMediaUploadOutputSchema, mcpUploadMediaFromPathInputSchema, }
|
|
|
5
5
|
import { fileTypeFromBuffer } from "file-type";
|
|
6
6
|
const FILE_HEAD_BYTES = 4100;
|
|
7
7
|
const SMALL_MEDIA_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
const IMAGE_LIMIT_BYTES = 30 * 1024 * 1024;
|
|
8
9
|
const AUDIO_LIMIT_BYTES = 15 * 1024 * 1024;
|
|
9
10
|
const VIDEO_LIMIT_BYTES = 100 * 1024 * 1024;
|
|
10
11
|
const EXTENSION_MIME = new Map([
|
|
@@ -12,6 +13,12 @@ const EXTENSION_MIME = new Map([
|
|
|
12
13
|
[".jpg", "image/jpeg"],
|
|
13
14
|
[".jpeg", "image/jpeg"],
|
|
14
15
|
[".webp", "image/webp"],
|
|
16
|
+
[".bmp", "image/bmp"],
|
|
17
|
+
[".gif", "image/gif"],
|
|
18
|
+
[".tif", "image/tiff"],
|
|
19
|
+
[".tiff", "image/tiff"],
|
|
20
|
+
[".heic", "image/heic"],
|
|
21
|
+
[".heif", "image/heif"],
|
|
15
22
|
[".mp3", "audio/mpeg"],
|
|
16
23
|
[".wav", "audio/wav"],
|
|
17
24
|
[".mp4", "video/mp4"],
|
|
@@ -47,6 +54,9 @@ function mediaSizeLimit(contentType) {
|
|
|
47
54
|
if (contentType.startsWith("audio/")) {
|
|
48
55
|
return AUDIO_LIMIT_BYTES;
|
|
49
56
|
}
|
|
57
|
+
if (contentType.startsWith("image/")) {
|
|
58
|
+
return IMAGE_LIMIT_BYTES;
|
|
59
|
+
}
|
|
50
60
|
return SMALL_MEDIA_LIMIT_BYTES;
|
|
51
61
|
}
|
|
52
62
|
function mediaTypeLabel(contentType) {
|
|
@@ -56,6 +66,9 @@ function mediaTypeLabel(contentType) {
|
|
|
56
66
|
if (contentType.startsWith("audio/")) {
|
|
57
67
|
return "音频";
|
|
58
68
|
}
|
|
69
|
+
if (contentType.startsWith("image/")) {
|
|
70
|
+
return "图片";
|
|
71
|
+
}
|
|
59
72
|
return "图片/文档";
|
|
60
73
|
}
|
|
61
74
|
function assertFileSizeAllowed(sizeBytes, contentType) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ export declare const mcpUploadMediaFromPathInputSchema: z.ZodObject<{
|
|
|
56
56
|
}>;
|
|
57
57
|
export declare const listModelsShape: {
|
|
58
58
|
modelType: z.ZodOptional<z.ZodEnum<["TEXT", "IMAGE", "VIDEO"]>>;
|
|
59
|
+
detail: z.ZodOptional<z.ZodEnum<["lite", "full"]>>;
|
|
60
|
+
};
|
|
61
|
+
export declare const getModelShape: {
|
|
62
|
+
publicModelId: z.ZodString;
|
|
63
|
+
routingMode: z.ZodOptional<z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>>;
|
|
59
64
|
};
|
|
60
65
|
export declare const chatTextShape: {
|
|
61
66
|
publicModelId: z.ZodString;
|
|
@@ -63,7 +68,7 @@ export declare const chatTextShape: {
|
|
|
63
68
|
messages: z.ZodArray<z.ZodObject<{
|
|
64
69
|
role: z.ZodEnum<["system", "user", "assistant"]>;
|
|
65
70
|
content: z.ZodEffects<z.ZodString, string, string>;
|
|
66
|
-
mediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
71
|
+
mediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
67
72
|
}, "strict", z.ZodTypeAny, {
|
|
68
73
|
content: string;
|
|
69
74
|
role: "user" | "assistant" | "system";
|
|
@@ -71,31 +76,36 @@ export declare const chatTextShape: {
|
|
|
71
76
|
}, {
|
|
72
77
|
content: string;
|
|
73
78
|
role: "user" | "assistant" | "system";
|
|
74
|
-
mediaIds?: string[] | undefined;
|
|
79
|
+
mediaIds?: string[] | null | undefined;
|
|
75
80
|
}>, "many">;
|
|
76
81
|
thinkingEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
-
|
|
82
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
83
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
78
84
|
};
|
|
79
85
|
export declare const generateImageShape: {
|
|
80
86
|
publicModelId: z.ZodString;
|
|
81
87
|
routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
|
|
82
88
|
prompt: z.ZodEffects<z.ZodString, string, string>;
|
|
83
|
-
batchSize: z.ZodDefault<z.ZodNumber
|
|
84
|
-
imageMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
85
|
-
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
86
|
-
|
|
89
|
+
batchSize: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodNumber, z.ZodNull]>, number, number | null>>;
|
|
90
|
+
imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
91
|
+
params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
|
|
92
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
93
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
94
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
87
95
|
};
|
|
88
96
|
export declare const generateVideoShape: {
|
|
89
97
|
publicModelId: z.ZodString;
|
|
90
98
|
routingMode: z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>;
|
|
91
99
|
prompt: z.ZodEffects<z.ZodString, string, string>;
|
|
92
|
-
imageMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
93
|
-
videoMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
94
|
-
audioMediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
100
|
+
imageMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
101
|
+
videoMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
102
|
+
audioMediaIds: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodNull]>, string[], string[] | null>>;
|
|
95
103
|
firstFrameMediaId: z.ZodOptional<z.ZodString>;
|
|
96
104
|
lastFrameMediaId: z.ZodOptional<z.ZodString>;
|
|
97
|
-
params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
98
|
-
|
|
105
|
+
params: z.ZodDefault<z.ZodEffects<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodNull]>, Record<string, unknown>, Record<string, unknown> | null>>;
|
|
106
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
107
|
+
clientRequestId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
108
|
+
pricingSnapshot: z.ZodOptional<z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>, "many">, z.ZodString, z.ZodNull]>>;
|
|
99
109
|
};
|
|
100
110
|
export declare const getGenerationTaskShape: {
|
|
101
111
|
taskId: z.ZodString;
|
package/dist/schemas.js
CHANGED
|
@@ -11,7 +11,24 @@ const routingModeSchema = z.enum([
|
|
|
11
11
|
"SUCCESS_FIRST",
|
|
12
12
|
]);
|
|
13
13
|
const pricingSnapshotSchema = z.record(z.unknown());
|
|
14
|
-
|
|
14
|
+
// 宿主常把可选数组/对象字段塞成 null, 归一到默认值避免服务端收到非法请求.
|
|
15
|
+
const nullableMediaIdsSchema = z
|
|
16
|
+
.union([z.array(mediaIdSchema), z.null()])
|
|
17
|
+
.transform((value) => value ?? [])
|
|
18
|
+
.default([]);
|
|
19
|
+
const paramsSchema = z
|
|
20
|
+
.union([z.record(z.unknown()), z.null()])
|
|
21
|
+
.transform((value) => value ?? {})
|
|
22
|
+
.default({});
|
|
23
|
+
// SDK 在 handler 之前先用 zod 校验入参, schema 必须放行畸形快照, server 层才有机会代为归一化.
|
|
24
|
+
const pricingSnapshotInputSchema = z
|
|
25
|
+
.union([
|
|
26
|
+
pricingSnapshotSchema,
|
|
27
|
+
z.array(pricingSnapshotSchema.nullable()),
|
|
28
|
+
z.string(),
|
|
29
|
+
z.null(),
|
|
30
|
+
])
|
|
31
|
+
.optional();
|
|
15
32
|
export const mcpCreateMediaUploadOutputSchema = z
|
|
16
33
|
.object({
|
|
17
34
|
mediaId: mediaIdSchema,
|
|
@@ -41,39 +58,60 @@ const messageSchema = z
|
|
|
41
58
|
.object({
|
|
42
59
|
role: z.enum(["system", "user", "assistant"]),
|
|
43
60
|
content: nonBlankStringSchema,
|
|
44
|
-
mediaIds:
|
|
61
|
+
mediaIds: nullableMediaIdsSchema,
|
|
45
62
|
})
|
|
46
63
|
.strict();
|
|
64
|
+
// 幂等键供付费调用去重, 空白值等同于没传, 所以沿用 nonBlank 校验但保持 optional.
|
|
65
|
+
const clientRequestIdSchema = z
|
|
66
|
+
.string()
|
|
67
|
+
.max(100)
|
|
68
|
+
.refine((value) => value.trim().length > 0, {
|
|
69
|
+
message: "Expected non-blank string",
|
|
70
|
+
})
|
|
71
|
+
.optional();
|
|
47
72
|
export const listModelsShape = {
|
|
48
73
|
modelType: z.enum(["TEXT", "IMAGE", "VIDEO"]).optional(),
|
|
74
|
+
detail: z.enum(["lite", "full"]).optional(),
|
|
75
|
+
};
|
|
76
|
+
export const getModelShape = {
|
|
77
|
+
publicModelId: z.string().min(1),
|
|
78
|
+
routingMode: routingModeSchema.optional(),
|
|
49
79
|
};
|
|
50
80
|
export const chatTextShape = {
|
|
51
81
|
publicModelId: z.string().min(1),
|
|
52
82
|
routingMode: routingModeSchema,
|
|
53
83
|
messages: z.array(messageSchema).min(1),
|
|
54
84
|
thinkingEnabled: z.boolean().optional(),
|
|
55
|
-
|
|
85
|
+
clientRequestId: clientRequestIdSchema,
|
|
86
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
56
87
|
};
|
|
57
88
|
export const generateImageShape = {
|
|
58
89
|
publicModelId: z.string().min(1),
|
|
59
90
|
routingMode: routingModeSchema,
|
|
60
91
|
prompt: nonBlankStringSchema,
|
|
61
|
-
batchSize: z
|
|
62
|
-
|
|
92
|
+
batchSize: z
|
|
93
|
+
.union([z.number().int().min(1).max(10), z.null()])
|
|
94
|
+
.transform((value) => value ?? 1)
|
|
95
|
+
.default(1),
|
|
96
|
+
imageMediaIds: nullableMediaIdsSchema,
|
|
63
97
|
params: paramsSchema,
|
|
64
|
-
|
|
98
|
+
dryRun: z.boolean().default(false),
|
|
99
|
+
clientRequestId: clientRequestIdSchema,
|
|
100
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
65
101
|
};
|
|
66
102
|
export const generateVideoShape = {
|
|
67
103
|
publicModelId: z.string().min(1),
|
|
68
104
|
routingMode: routingModeSchema,
|
|
69
105
|
prompt: nonBlankStringSchema,
|
|
70
|
-
imageMediaIds:
|
|
71
|
-
videoMediaIds:
|
|
72
|
-
audioMediaIds:
|
|
106
|
+
imageMediaIds: nullableMediaIdsSchema,
|
|
107
|
+
videoMediaIds: nullableMediaIdsSchema,
|
|
108
|
+
audioMediaIds: nullableMediaIdsSchema,
|
|
73
109
|
firstFrameMediaId: mediaIdSchema.optional(),
|
|
74
110
|
lastFrameMediaId: mediaIdSchema.optional(),
|
|
75
111
|
params: paramsSchema,
|
|
76
|
-
|
|
112
|
+
dryRun: z.boolean().default(false),
|
|
113
|
+
clientRequestId: clientRequestIdSchema,
|
|
114
|
+
pricingSnapshot: pricingSnapshotInputSchema,
|
|
77
115
|
};
|
|
78
116
|
export const getGenerationTaskShape = {
|
|
79
117
|
taskId: z.string().min(1),
|
package/dist/server.d.ts
CHANGED
|
@@ -4,16 +4,17 @@ import { AiHiveApiClient } from "./api-client.js";
|
|
|
4
4
|
import { type BearerTokenProvider } from "./auth-token-provider.js";
|
|
5
5
|
import type { McpClientConfig } from "./config.js";
|
|
6
6
|
import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
|
|
7
|
-
export declare const SERVER_VERSION = "0.
|
|
7
|
+
export declare const SERVER_VERSION = "0.3.0";
|
|
8
8
|
declare function content(value: unknown): {
|
|
9
9
|
content: {
|
|
10
10
|
type: "text";
|
|
11
11
|
text: string;
|
|
12
12
|
}[];
|
|
13
13
|
};
|
|
14
|
-
type ApiClientLike = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "createMediaUpload" | "completeMediaUpload" | "chatText" | "generateImage" | "generateVideo" | "getGenerationTask" | "putUpload">;
|
|
14
|
+
type ApiClientLike = Pick<AiHiveApiClient, "getUserInfo" | "listModels" | "getModel" | "createMediaUpload" | "completeMediaUpload" | "chatText" | "generateImage" | "generateVideo" | "getGenerationTask" | "putUpload">;
|
|
15
15
|
export type ToolDefinition = {
|
|
16
16
|
name: string;
|
|
17
|
+
description: string;
|
|
17
18
|
schema: z.ZodRawShape;
|
|
18
19
|
handler: (args: Record<string, unknown>) => Promise<ReturnType<typeof content>>;
|
|
19
20
|
};
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, listModelsShape, } from "./schemas.js";
|
|
3
|
+
import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, } from "./schemas.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { AiHiveApiClient } from "./api-client.js";
|
|
6
6
|
import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
|
|
7
7
|
import { readMcpClientInfo } from "./client-info.js";
|
|
8
8
|
import { uploadMediaFromPath } from "./local-media.js";
|
|
9
9
|
import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
|
|
10
|
-
export const SERVER_VERSION = "0.
|
|
10
|
+
export const SERVER_VERSION = "0.3.0";
|
|
11
11
|
const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
|
|
12
|
+
const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 list_models 返回的快照对象,也可以省略该字段由客户端自动获取";
|
|
13
|
+
const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
14
|
+
const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId 或 routingMode 与请求不一致,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
|
|
15
|
+
const MODEL_UNAVAILABLE_MESSAGE = "模型当前不可用或不支持该路由模式,请重新调用 list_models 获取最新模型信息";
|
|
12
16
|
function content(value) {
|
|
13
17
|
return {
|
|
14
18
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
@@ -19,20 +23,111 @@ function rejectDeprecatedAttachmentIds(args) {
|
|
|
19
23
|
throw new Error(DEPRECATED_ATTACHMENT_IDS_MESSAGE);
|
|
20
24
|
}
|
|
21
25
|
}
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function pickSnapshotForRequest(snapshots, publicModelId, routingMode) {
|
|
30
|
+
return snapshots.find((item) => isPlainObject(item) &&
|
|
31
|
+
item.publicModelId === publicModelId &&
|
|
32
|
+
item.routingMode === routingMode);
|
|
33
|
+
}
|
|
34
|
+
// LLM 回传 pricingSnapshot 时经常漏传或变形(数组/JSON 字符串/null), 由客户端代为归一化,
|
|
35
|
+
// 保证进入付费 API 的请求一定携带服务端期望的单个快照对象.
|
|
36
|
+
async function resolvePricingSnapshot(client, modelType, args) {
|
|
37
|
+
const requestArgs = { ...args };
|
|
38
|
+
if (requestArgs.dryRun === false) {
|
|
39
|
+
delete requestArgs.dryRun;
|
|
40
|
+
}
|
|
41
|
+
let snapshot = requestArgs.pricingSnapshot;
|
|
42
|
+
const isDryRun = args.dryRun === true;
|
|
43
|
+
const withoutPricingSnapshot = () => {
|
|
44
|
+
const request = { ...requestArgs };
|
|
45
|
+
delete request.pricingSnapshot;
|
|
46
|
+
return request;
|
|
47
|
+
};
|
|
48
|
+
const resolveCurrentSnapshot = async () => {
|
|
49
|
+
const models = await client().listModels({ modelType });
|
|
50
|
+
const model = Array.isArray(models)
|
|
51
|
+
? models.find((item) => isPlainObject(item) && item.publicModelId === args.publicModelId)
|
|
52
|
+
: undefined;
|
|
53
|
+
const snapshots = isPlainObject(model) ? model.pricingSnapshot : undefined;
|
|
54
|
+
const matched = Array.isArray(snapshots)
|
|
55
|
+
? pickSnapshotForRequest(snapshots, args.publicModelId, args.routingMode)
|
|
56
|
+
: undefined;
|
|
57
|
+
if (!matched) {
|
|
58
|
+
throw new Error(MODEL_UNAVAILABLE_MESSAGE);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...requestArgs,
|
|
62
|
+
pricingSnapshot: matched,
|
|
63
|
+
pricingSnapshotAutoResolved: true,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
if (snapshot === undefined || snapshot === null) {
|
|
67
|
+
return isDryRun ? withoutPricingSnapshot() : resolveCurrentSnapshot();
|
|
68
|
+
}
|
|
69
|
+
if (typeof snapshot === "string") {
|
|
70
|
+
try {
|
|
71
|
+
snapshot = JSON.parse(snapshot);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(SNAPSHOT_PARSE_FAILURE_MESSAGE);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (snapshot === undefined || snapshot === null) {
|
|
78
|
+
return isDryRun ? withoutPricingSnapshot() : resolveCurrentSnapshot();
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(snapshot)) {
|
|
81
|
+
const matched = pickSnapshotForRequest(snapshot, args.publicModelId, args.routingMode);
|
|
82
|
+
if (!matched) {
|
|
83
|
+
throw new Error(SNAPSHOT_ARRAY_MISMATCH_MESSAGE);
|
|
84
|
+
}
|
|
85
|
+
return { ...requestArgs, pricingSnapshot: matched };
|
|
86
|
+
}
|
|
87
|
+
if (isPlainObject(snapshot)) {
|
|
88
|
+
// 单对象快照同样可能传错模型或档位, 与数组分支一致校验, 避免按错误价格计费;
|
|
89
|
+
// 键缺失时放行, 由服务端 strict schema 兜底.
|
|
90
|
+
if ((typeof snapshot.publicModelId === "string" &&
|
|
91
|
+
snapshot.publicModelId !== args.publicModelId) ||
|
|
92
|
+
(typeof snapshot.routingMode === "string" &&
|
|
93
|
+
snapshot.routingMode !== args.routingMode)) {
|
|
94
|
+
throw new Error(SNAPSHOT_IDENTITY_MISMATCH_MESSAGE);
|
|
95
|
+
}
|
|
96
|
+
return { ...requestArgs, pricingSnapshot: snapshot };
|
|
97
|
+
}
|
|
98
|
+
// 数字/布尔等原始类型同样是畸形回传, 不能原样透传给付费 API.
|
|
99
|
+
throw new Error(SNAPSHOT_PARSE_FAILURE_MESSAGE);
|
|
100
|
+
}
|
|
101
|
+
// 幂等键语义写在每个付费工具的描述里, 让宿主在选型阶段就能看到重试规则.
|
|
102
|
+
const CLIENT_REQUEST_ID_NOTE = "付费调用应传入稳定 UUID 作为 clientRequestId 幂等键;仅传输中断、REQUEST_IN_PROGRESS 或刷新过期 pricingSnapshot 时复用原值和相同业务参数。余额、参数、媒体或已缓存的模型调用失败在修正后必须使用新的 clientRequestId;REQUEST_OUTCOME_UNKNOWN 必须停止自动重试并人工核查。";
|
|
22
103
|
export function createToolDefinitions(client) {
|
|
23
104
|
return [
|
|
24
105
|
{
|
|
25
106
|
name: "get_user_info",
|
|
107
|
+
description: "查询当前用户和余额。",
|
|
26
108
|
schema: {},
|
|
27
109
|
handler: async () => content(await client().getUserInfo()),
|
|
28
110
|
},
|
|
29
111
|
{
|
|
30
112
|
name: "list_models",
|
|
113
|
+
description: "按 TEXT、IMAGE 或 VIDEO 查询可用模型与公开价格。detail 为 lite 时只返回各模型各路由的售价摘要(维度、单位、计费方式、内含数量及 saleMicros),用于快速选型;detail 为 full 时返回完整公开详情。saleMicros 是客户实际结算价,两种结果都不包含内部参考成本。",
|
|
31
114
|
schema: listModelsShape,
|
|
32
115
|
handler: async (args) => content(await client().listModels(args)),
|
|
33
116
|
},
|
|
117
|
+
{
|
|
118
|
+
name: "get_model",
|
|
119
|
+
description: "按 publicModelId 查询单个模型的参数 schema、媒体要求、计费因素和示例;传 routingMode 可获取线路专属契约。",
|
|
120
|
+
schema: getModelShape,
|
|
121
|
+
handler: async (args) => {
|
|
122
|
+
const publicModelId = String(args.publicModelId);
|
|
123
|
+
return content(typeof args.routingMode === "string"
|
|
124
|
+
? await client().getModel(publicModelId, args.routingMode)
|
|
125
|
+
: await client().getModel(publicModelId));
|
|
126
|
+
},
|
|
127
|
+
},
|
|
34
128
|
{
|
|
35
129
|
name: "upload_media_from_path",
|
|
130
|
+
description: "上传本地图片、视频、音频或文档并返回 mediaId。",
|
|
36
131
|
schema: {
|
|
37
132
|
path: z.string().min(1),
|
|
38
133
|
filename: z.string().min(1).optional(),
|
|
@@ -42,24 +137,35 @@ export function createToolDefinitions(client) {
|
|
|
42
137
|
},
|
|
43
138
|
{
|
|
44
139
|
name: "chat_text",
|
|
140
|
+
description: `使用指定模型生成文本。${CLIENT_REQUEST_ID_NOTE}`,
|
|
45
141
|
schema: chatTextShape,
|
|
46
142
|
handler: async (args) => {
|
|
47
143
|
rejectDeprecatedAttachmentIds(args);
|
|
48
|
-
|
|
144
|
+
const body = await resolvePricingSnapshot(client, "TEXT", args);
|
|
145
|
+
return content(await client().chatText(body));
|
|
49
146
|
},
|
|
50
147
|
},
|
|
51
148
|
{
|
|
52
149
|
name: "generate_image",
|
|
150
|
+
description: `创建图片生成任务;dryRun=true 时只校验参数、媒体和预估费用,不创建任务且可省略 pricingSnapshot 与 clientRequestId。${CLIENT_REQUEST_ID_NOTE}`,
|
|
53
151
|
schema: generateImageShape,
|
|
54
|
-
handler: async (args) =>
|
|
152
|
+
handler: async (args) => {
|
|
153
|
+
const body = await resolvePricingSnapshot(client, "IMAGE", args);
|
|
154
|
+
return content(await client().generateImage(body));
|
|
155
|
+
},
|
|
55
156
|
},
|
|
56
157
|
{
|
|
57
158
|
name: "generate_video",
|
|
159
|
+
description: `创建视频生成任务;dryRun=true 时只校验参数、媒体和预估费用,不创建任务且可省略 pricingSnapshot 与 clientRequestId。${CLIENT_REQUEST_ID_NOTE}`,
|
|
58
160
|
schema: generateVideoShape,
|
|
59
|
-
handler: async (args) =>
|
|
161
|
+
handler: async (args) => {
|
|
162
|
+
const body = await resolvePricingSnapshot(client, "VIDEO", args);
|
|
163
|
+
return content(await client().generateVideo(body));
|
|
164
|
+
},
|
|
60
165
|
},
|
|
61
166
|
{
|
|
62
167
|
name: "get_generation_task",
|
|
168
|
+
description: "查询图片或视频任务状态和结果。",
|
|
63
169
|
schema: getGenerationTaskShape,
|
|
64
170
|
handler: async (args) => content(await client().getGenerationTask({
|
|
65
171
|
taskId: String(args.taskId),
|
|
@@ -107,7 +213,7 @@ export function createServer(input) {
|
|
|
107
213
|
clientInfoProvider: () => readMcpClientInfo(server),
|
|
108
214
|
});
|
|
109
215
|
for (const tool of createToolDefinitions(client)) {
|
|
110
|
-
server.tool(tool.name, tool.schema, tool.handler);
|
|
216
|
+
server.tool(tool.name, tool.description, tool.schema, tool.handler);
|
|
111
217
|
}
|
|
112
218
|
return server;
|
|
113
219
|
}
|