@infimind-next/ai-hive-mcp 0.2.1 → 0.2.4

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.
@@ -1,16 +1,19 @@
1
1
  import type { BearerTokenProvider } from "./auth-token-provider.js";
2
+ import { type McpClientInfo } from "./client-info.js";
2
3
  type FetchImpl = typeof fetch;
3
4
  type AiHiveApiClientInput = {
4
5
  endpoint: string;
5
6
  tokenProvider: BearerTokenProvider;
6
7
  timeoutMs: number;
7
8
  fetchImpl?: FetchImpl;
9
+ clientInfoProvider?: () => McpClientInfo | undefined;
8
10
  };
9
11
  export declare class AiHiveApiClient {
10
12
  private readonly endpoint;
11
13
  private readonly tokenProvider;
12
14
  private readonly timeoutMs;
13
15
  private readonly fetchImpl;
16
+ private readonly clientInfoProvider?;
14
17
  constructor(input: AiHiveApiClientInput);
15
18
  private withTimeout;
16
19
  private request;
@@ -1,3 +1,4 @@
1
+ import { encodeMcpClientInfo, MCP_CLIENT_INFO_HEADER, } from "./client-info.js";
1
2
  function formatIssue(value) {
2
3
  if (typeof value === "string") {
3
4
  return value;
@@ -46,11 +47,13 @@ export class AiHiveApiClient {
46
47
  tokenProvider;
47
48
  timeoutMs;
48
49
  fetchImpl;
50
+ clientInfoProvider;
49
51
  constructor(input) {
50
52
  this.endpoint = input.endpoint.replace(/\/+$/, "");
51
53
  this.tokenProvider = input.tokenProvider;
52
54
  this.timeoutMs = input.timeoutMs;
53
55
  this.fetchImpl = input.fetchImpl ?? fetch;
56
+ this.clientInfoProvider = input.clientInfoProvider;
54
57
  }
55
58
  async withTimeout(operation) {
56
59
  const controller = new AbortController();
@@ -99,11 +102,15 @@ export class AiHiveApiClient {
99
102
  });
100
103
  }
101
104
  send(path, init, signal, token) {
105
+ const encodedClientInfo = encodeMcpClientInfo(this.clientInfoProvider?.());
102
106
  return this.fetchImpl(`${this.endpoint}${path}`, {
103
107
  ...init,
104
108
  signal,
105
109
  headers: {
106
110
  Authorization: `Bearer ${token}`,
111
+ ...(encodedClientInfo
112
+ ? { [MCP_CLIENT_INFO_HEADER]: encodedClientInfo }
113
+ : {}),
107
114
  ...(init.body ? { "Content-Type": "application/json" } : {}),
108
115
  ...init.headers,
109
116
  },
package/dist/cli.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { McpClientConfig } from "./config.js";
2
2
  import { runServer as runMcpServer } from "./server.js";
3
3
  import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
4
4
  import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
5
- import { listenForWorkBuddyCallback } from "./workbuddy-loopback.js";
5
+ import { listenForWorkBuddyCallback, type WorkBuddyResultRedirects } from "./workbuddy-loopback.js";
6
6
  export type CliDependencies = {
7
7
  stdout(line: string): void;
8
8
  stderr(line: string): void;
@@ -12,3 +12,4 @@ export type CliDependencies = {
12
12
  runServer: typeof runMcpServer;
13
13
  };
14
14
  export declare function runCli(config: McpClientConfig, dependencies?: Partial<CliDependencies>): Promise<number>;
15
+ export declare function createWorkBuddyResultRedirects(webOrigin: string): WorkBuddyResultRedirects;
package/dist/cli.js CHANGED
@@ -49,6 +49,14 @@ function resolveDependencies(config, overrides) {
49
49
  };
50
50
  }
51
51
  async function runAuth(config, dependencies) {
52
+ let resultRedirects;
53
+ try {
54
+ resultRedirects = createWorkBuddyResultRedirects(config.webOrigin);
55
+ }
56
+ catch {
57
+ dependencies.stderr("连接 AI-HIVE 失败,请重试");
58
+ return 1;
59
+ }
52
60
  const store = dependencies.createStore(config.credentialsFile);
53
61
  const oauth = dependencies.createOAuthClient(config.endpoint);
54
62
  const { state, codeVerifier, codeChallenge } = createPkcePair();
@@ -58,6 +66,7 @@ async function runAuth(config, dependencies) {
58
66
  listenerPromise = Promise.resolve(dependencies.listenForCallback({
59
67
  expectedState: state,
60
68
  timeoutMs: AUTH_CALLBACK_DEADLINE_MS,
69
+ resultRedirects,
61
70
  onCode: async (code) => {
62
71
  const payload = await oauth.exchangeAuthorizationCode({
63
72
  code,
@@ -112,6 +121,26 @@ async function runAuth(config, dependencies) {
112
121
  await listener.close().catch(() => undefined);
113
122
  }
114
123
  }
124
+ export function createWorkBuddyResultRedirects(webOrigin) {
125
+ const baseUrl = new URL(webOrigin);
126
+ if ((baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") ||
127
+ baseUrl.username !== "" ||
128
+ baseUrl.password !== "" ||
129
+ baseUrl.search !== "" ||
130
+ baseUrl.hash !== "") {
131
+ throw new Error("Web origin 无效");
132
+ }
133
+ const successUrl = new URL("/welcome?from=workbuddy-auth", baseUrl);
134
+ const failureUrl = new URL("/authorization-failed?from=workbuddy-auth&reason=callback-error", baseUrl);
135
+ if (successUrl.origin !== baseUrl.origin ||
136
+ failureUrl.origin !== baseUrl.origin) {
137
+ throw new Error("授权结果地址不同源");
138
+ }
139
+ return {
140
+ successUrl: successUrl.href,
141
+ failureUrl: failureUrl.href,
142
+ };
143
+ }
115
144
  async function runStatus(config, dependencies) {
116
145
  const store = dependencies.createStore(config.credentialsFile);
117
146
  const deadlineAt = Date.now() + STATUS_DEADLINE_MS;
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const MCP_CLIENT_INFO_HEADER = "x-ai-hive-mcp-client-info";
3
+ export declare const MCP_CLIENT_INFO_HEADER_MAX_BYTES = 1024;
4
+ export type McpClientInfo = {
5
+ name: string;
6
+ version: string;
7
+ };
8
+ export declare function encodeMcpClientInfo(input?: McpClientInfo): string | undefined;
9
+ export declare function readMcpClientInfo(server: McpServer): McpClientInfo | undefined;
@@ -0,0 +1,16 @@
1
+ export const MCP_CLIENT_INFO_HEADER = "x-ai-hive-mcp-client-info";
2
+ export const MCP_CLIENT_INFO_HEADER_MAX_BYTES = 1024;
3
+ export function encodeMcpClientInfo(input) {
4
+ if (!input) {
5
+ return undefined;
6
+ }
7
+ const encoded = Buffer.from(JSON.stringify({ name: input.name, version: input.version }), "utf8").toString("base64url");
8
+ return Buffer.byteLength(encoded, "ascii") <=
9
+ MCP_CLIENT_INFO_HEADER_MAX_BYTES
10
+ ? encoded
11
+ : undefined;
12
+ }
13
+ export function readMcpClientInfo(server) {
14
+ const info = server.server.getClientVersion();
15
+ return info ? { name: info.name, version: info.version } : undefined;
16
+ }
package/dist/index.js CHANGED
File without changes
package/dist/schemas.d.ts CHANGED
@@ -10,34 +10,34 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
10
10
  headers: z.ZodRecord<z.ZodString, z.ZodString>;
11
11
  }, "strict", z.ZodTypeAny, {
12
12
  method: "PUT";
13
- headers: Record<string, string>;
14
13
  url: string;
14
+ headers: Record<string, string>;
15
15
  }, {
16
16
  method: "PUT";
17
- headers: Record<string, string>;
18
17
  url: string;
18
+ headers: Record<string, string>;
19
19
  }>;
20
20
  expiresAt: z.ZodString;
21
21
  }, "strict", z.ZodTypeAny, {
22
+ mimeType: string;
22
23
  mediaId: string;
23
24
  mediaType: "IMAGE" | "VIDEO" | "DOCUMENT";
24
- mimeType: string;
25
25
  sizeBytes: number;
26
26
  upload: {
27
27
  method: "PUT";
28
- headers: Record<string, string>;
29
28
  url: string;
29
+ headers: Record<string, string>;
30
30
  };
31
31
  expiresAt: string;
32
32
  }, {
33
+ mimeType: string;
33
34
  mediaId: string;
34
35
  mediaType: "IMAGE" | "VIDEO" | "DOCUMENT";
35
- mimeType: string;
36
36
  sizeBytes: number;
37
37
  upload: {
38
38
  method: "PUT";
39
- headers: Record<string, string>;
40
39
  url: string;
40
+ headers: Record<string, string>;
41
41
  };
42
42
  expiresAt: string;
43
43
  }>;
@@ -65,12 +65,12 @@ export declare const chatTextShape: {
65
65
  content: z.ZodEffects<z.ZodString, string, string>;
66
66
  mediaIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
67
67
  }, "strict", z.ZodTypeAny, {
68
- role: "system" | "user" | "assistant";
69
68
  content: string;
69
+ role: "user" | "assistant" | "system";
70
70
  mediaIds: string[];
71
71
  }, {
72
- role: "system" | "user" | "assistant";
73
72
  content: string;
73
+ role: "user" | "assistant" | "system";
74
74
  mediaIds?: string[] | undefined;
75
75
  }>, "many">;
76
76
  thinkingEnabled: z.ZodOptional<z.ZodBoolean>;
package/dist/server.d.ts CHANGED
@@ -4,7 +4,7 @@ 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.2.1";
7
+ export declare const SERVER_VERSION = "0.2.4";
8
8
  declare function content(value: unknown): {
9
9
  content: {
10
10
  type: "text";
package/dist/server.js CHANGED
@@ -4,9 +4,10 @@ import { chatTextShape, generateImageShape, generateVideoShape, getGenerationTas
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
+ import { readMcpClientInfo } from "./client-info.js";
7
8
  import { uploadMediaFromPath } from "./local-media.js";
8
9
  import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
9
- export const SERVER_VERSION = "0.2.1";
10
+ export const SERVER_VERSION = "0.2.4";
10
11
  const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
11
12
  function content(value) {
12
13
  return {
@@ -103,6 +104,7 @@ export function createServer(input) {
103
104
  endpoint: input.endpoint,
104
105
  tokenProvider: input.tokenProvider,
105
106
  timeoutMs: input.timeoutMs,
107
+ clientInfoProvider: () => readMcpClientInfo(server),
106
108
  });
107
109
  for (const tool of createToolDefinitions(client)) {
108
110
  server.tool(tool.name, tool.schema, tool.handler);
@@ -3,8 +3,13 @@ export type LoopbackListener = {
3
3
  completion: Promise<void>;
4
4
  close(): Promise<void>;
5
5
  };
6
+ export type WorkBuddyResultRedirects = {
7
+ successUrl: string;
8
+ failureUrl: string;
9
+ };
6
10
  export declare function listenForWorkBuddyCallback(_input: {
7
11
  expectedState: string;
8
12
  timeoutMs: number;
9
13
  onCode(code: string): Promise<void>;
14
+ resultRedirects?: WorkBuddyResultRedirects;
10
15
  }): Promise<LoopbackListener>;
@@ -102,7 +102,12 @@ export function listenForWorkBuddyCallback(_input) {
102
102
  }
103
103
  catch {
104
104
  if (!settled) {
105
- await sendHtmlAndWait(response, 500, failureHtml());
105
+ if (input.resultRedirects) {
106
+ await sendRedirectAndWait(response, input.resultRedirects.failureUrl);
107
+ }
108
+ else {
109
+ await sendHtmlAndWait(response, 500, failureHtml());
110
+ }
106
111
  await finishFailure("WorkBuddy 授权失败,请重新连接");
107
112
  }
108
113
  return;
@@ -111,7 +116,12 @@ export function listenForWorkBuddyCallback(_input) {
111
116
  response.destroy();
112
117
  return;
113
118
  }
114
- await sendHtmlAndWait(response, 200, successHtml());
119
+ if (input.resultRedirects) {
120
+ await sendRedirectAndWait(response, input.resultRedirects.successUrl);
121
+ }
122
+ else {
123
+ await sendHtmlAndWait(response, 200, successHtml());
124
+ }
115
125
  await finishSuccess();
116
126
  }
117
127
  async function handleUnexpectedRequestFailure(response) {
@@ -179,6 +189,18 @@ function sendHtmlAndWait(response, status, body) {
179
189
  response.end(body, resolve);
180
190
  });
181
191
  }
192
+ function sendRedirectAndWait(response, location) {
193
+ return new Promise((resolve) => {
194
+ response.writeHead(302, {
195
+ "Cache-Control": "no-store",
196
+ Connection: "close",
197
+ "Content-Length": "0",
198
+ Location: location,
199
+ "Referrer-Policy": "no-referrer",
200
+ });
201
+ response.end(resolve);
202
+ });
203
+ }
182
204
  function page(title, content) {
183
205
  return `<!doctype html>
184
206
  <html lang="zh-CN">
@@ -191,7 +213,6 @@ function page(title, content) {
191
213
  body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: sans-serif; background: #f8fafc; color: #0f172a; }
192
214
  main { width: min(420px, calc(100% - 48px)); padding: 32px; border: 1px solid #e2e8f0; border-radius: 16px; background: white; box-shadow: 0 16px 40px rgb(15 23 42 / 8%); }
193
215
  h1 { margin-top: 0; }
194
- button { border: 0; border-radius: 8px; padding: 10px 16px; background: #2563eb; color: white; cursor: pointer; }
195
216
  </style>
196
217
  </head>
197
218
  <body>
@@ -202,11 +223,7 @@ function page(title, content) {
202
223
  function successHtml() {
203
224
  return page("授权成功", `<h1>授权成功</h1>
204
225
  <p>请返回 WorkBuddy 继续使用。</p>
205
- <p>页面将尝试自动关闭;如果没有关闭,请手动关闭。</p>
206
- <button type="button" onclick="window.close()">关闭页面</button>
207
- <script>
208
- window.setTimeout(() => window.close(), 300);
209
- </script>`);
226
+ <p>您可以手动关闭此页面。</p>`);
210
227
  }
211
228
  function failureHtml() {
212
229
  return page("授权失败", `<h1>授权失败</h1>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infimind-next/ai-hive-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,6 +12,13 @@
12
12
  "files": [
13
13
  "dist"
14
14
  ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
18
+ "lint": "eslint \"src/**/*.ts\"",
19
+ "prepack": "pnpm run build",
20
+ "test": "vitest run"
21
+ },
15
22
  "dependencies": {
16
23
  "@modelcontextprotocol/sdk": "^1.29.0",
17
24
  "file-type": "^21.3.4",
@@ -22,11 +29,5 @@
22
29
  "typescript": "^5.0.0",
23
30
  "typescript-eslint": "^8.0.0",
24
31
  "vitest": "^3.0.0"
25
- },
26
- "scripts": {
27
- "build": "tsc -p tsconfig.json",
28
- "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
29
- "lint": "eslint \"src/**/*.ts\"",
30
- "test": "vitest run"
31
32
  }
32
- }
33
+ }