@yyaylt/zhipu-web-search-mcp-server 1.0.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSearch.js","sourceRoot":"","sources":["../../src/schemas/webSearch.ts"],"names":[],"mappings":"AAAA,iCAAiC;AAEjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,EACd,eAAe,EACf,aAAa,EACd,MAAM,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC;SACnB,GAAG,CAAC,EAAE,EAAE,kBAAkB,CAAC;SAC3B,QAAQ,CAAC,sBAAsB,CAAC;IAEnC,aAAa,EAAE,CAAC;SACb,UAAU,CAAC,cAAc,CAAC;SAC1B,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC;SAC7B,QAAQ,CACP,4FAA4F,CAC7F;IAEH,aAAa,EAAE,CAAC;SACb,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,8CAA8C,CAC/C;IAEH,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CACP,0HAA0H,CAC3H;IAEH,oBAAoB,EAAE,CAAC;SACpB,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,+EAA+E,CAChF;IAEH,qBAAqB,EAAE,CAAC;SACrB,UAAU,CAAC,eAAe,CAAC;SAC3B,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC;SACjC,QAAQ,CACP,sFAAsF,CACvF;IAEH,YAAY,EAAE,CAAC;SACZ,UAAU,CAAC,aAAa,CAAC;SACzB,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC;SAC7B,QAAQ,CACP,6DAA6D,CAC9D;IAEH,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,sCAAsC,CAAC;CACpD,CAAC;KACD,MAAM,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { WebSearchApiRequest, WebSearchResponse } from "../types.js";
2
+ /**
3
+ * 调用智谱 Web Search API。
4
+ * @param apiKey 智谱 API Key
5
+ * @param params 搜索请求参数
6
+ * @returns 搜索响应
7
+ */
8
+ export declare function callWebSearch(apiKey: string, params: WebSearchApiRequest): Promise<WebSearchResponse>;
9
+ /**
10
+ * 将 API 异常转换为可读、可操作的中文错误提示。
11
+ * 兜底分支不向客户端返回原始错误内容,仅将详细错误写入 stderr 供本地排查,
12
+ * 避免向 MCP 客户端泄露内部路径、服务地址等敏感信息。
13
+ */
14
+ export declare function handleApiError(error: unknown): string;
15
+ //# sourceMappingURL=webSearchApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSearchApi.d.ts","sourceRoot":"","sources":["../../src/services/webSearchApi.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAkB5B;AASD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA+BrD"}
@@ -0,0 +1,80 @@
1
+ // Web Search API 客户端服务,负责与智谱开放平台通信
2
+ import axios from "axios";
3
+ import { API_BASE_URL, REQUEST_TIMEOUT_MS } from "../constants.js";
4
+ /**
5
+ * 调用智谱 Web Search API。
6
+ * @param apiKey 智谱 API Key
7
+ * @param params 搜索请求参数
8
+ * @returns 搜索响应
9
+ */
10
+ export async function callWebSearch(apiKey, params) {
11
+ try {
12
+ const response = await axios.post(API_BASE_URL, params, {
13
+ timeout: REQUEST_TIMEOUT_MS,
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ "Accept": "application/json",
17
+ "Authorization": `Bearer ${apiKey}`
18
+ }
19
+ });
20
+ return response.data;
21
+ }
22
+ catch (error) {
23
+ throw error;
24
+ }
25
+ }
26
+ // 错误码到建议操作的映射(智谱官方错误码)
27
+ const ERROR_SUGGESTIONS = {
28
+ "1701": "网络搜索并发已达上限,请稍后重试或减少并发请求。",
29
+ "1702": "系统未找到可用的搜索引擎服务,请检查配置或联系管理员。",
30
+ "1703": "搜索引擎未返回有效数据,请调整查询条件后重试。"
31
+ };
32
+ /**
33
+ * 将 API 异常转换为可读、可操作的中文错误提示。
34
+ * 兜底分支不向客户端返回原始错误内容,仅将详细错误写入 stderr 供本地排查,
35
+ * 避免向 MCP 客户端泄露内部路径、服务地址等敏感信息。
36
+ */
37
+ export function handleApiError(error) {
38
+ if (axios.isAxiosError(error)) {
39
+ const axiosError = error;
40
+ if (axiosError.response) {
41
+ const status = axiosError.response.status;
42
+ const body = axiosError.response.data;
43
+ // 尝试解析智谱业务错误码
44
+ const bizCode = extractBizCode(body);
45
+ if (bizCode && ERROR_SUGGESTIONS[bizCode]) {
46
+ return `搜索失败(错误码 ${bizCode}):${ERROR_SUGGESTIONS[bizCode]}`;
47
+ }
48
+ if (status === 401) {
49
+ return "认证失败:请检查 ZHIPU_API_KEY 环境变量是否正确设置。";
50
+ }
51
+ if (status === 429) {
52
+ return "请求过于频繁:网络搜索并发已达上限,请稍后重试。";
53
+ }
54
+ return `API 请求失败,HTTP 状态码 ${status}。`;
55
+ }
56
+ if (axiosError.code === "ECONNABORTED") {
57
+ return "请求超时:网络搜索服务响应过慢,请稍后重试。";
58
+ }
59
+ if (axiosError.code === "ECONNREFUSED") {
60
+ return "网络错误:无法连接到智谱开放平台,请检查网络。";
61
+ }
62
+ }
63
+ // 详细错误仅写入 stderr(stdio 服务器日志通道,供开发者本地排查),不返回给客户端
64
+ console.error("智谱 Web Search 调用发生未知错误:", error);
65
+ return `发生未知错误,请稍后重试或联系管理员。`;
66
+ }
67
+ // 从错误响应体中提取业务错误码
68
+ function extractBizCode(body) {
69
+ if (body &&
70
+ typeof body === "object" &&
71
+ "error" in body &&
72
+ body.error &&
73
+ typeof body.error === "object" &&
74
+ "code" in body.error) {
75
+ const code = body.error.code;
76
+ return typeof code === "string" ? code : undefined;
77
+ }
78
+ return undefined;
79
+ }
80
+ //# sourceMappingURL=webSearchApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSearchApi.js","sourceRoot":"","sources":["../../src/services/webSearchApi.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGnE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,MAA2B;IAE3B,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAC/B,YAAY,EACZ,MAAM,EACN;YACE,OAAO,EAAE,kBAAkB;YAC3B,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,QAAQ,EAAE,kBAAkB;gBAC5B,eAAe,EAAE,UAAU,MAAM,EAAE;aACpC;SACF,CACF,CAAC;QACF,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,uBAAuB;AACvB,MAAM,iBAAiB,GAA2B;IAChD,MAAM,EAAE,0BAA0B;IAClC,MAAM,EAAE,6BAA6B;IACrC,MAAM,EAAE,yBAAyB;CAClC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,UAAU,GAAG,KAAsC,CAAC;QAC1D,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAe,CAAC;YAEjD,cAAc;YACd,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1C,OAAO,YAAY,OAAO,KAAK,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,CAAC;YAED,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;gBACnB,OAAO,oCAAoC,CAAC;YAC9C,CAAC;YACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;gBACnB,OAAO,0BAA0B,CAAC;YACpC,CAAC;YACD,OAAO,qBAAqB,MAAM,GAAG,CAAC;QACxC,CAAC;QACD,IAAI,UAAU,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACvC,OAAO,wBAAwB,CAAC;QAClC,CAAC;QACD,IAAI,UAAU,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACvC,OAAO,yBAAyB,CAAC;QACnC,CAAC;IACH,CAAC;IACD,iDAAiD;IACjD,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;IAChD,OAAO,qBAAqB,CAAC;AAC/B,CAAC;AAED,iBAAiB;AACjB,SAAS,cAAc,CAAC,IAAa;IACnC,IACE,IAAI;QACJ,OAAO,IAAI,KAAK,QAAQ;QACxB,OAAO,IAAI,IAAI;QACf,IAAI,CAAC,KAAK;QACV,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;QAC9B,MAAM,IAAI,IAAI,CAAC,KAAK,EACpB,CAAC;QACD,MAAM,IAAI,GAAI,IAAI,CAAC,KAA2B,CAAC,IAAI,CAAC;QACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,47 @@
1
+ export interface WebSearchResultItem {
2
+ title: string;
3
+ content: string;
4
+ link: string;
5
+ media: string;
6
+ icon: string;
7
+ refer: string;
8
+ publish_date: string;
9
+ }
10
+ export interface WebSearchIntentItem {
11
+ query: string;
12
+ intent: "SEARCH_ALL" | "SEARCH_NONE" | "SEARCH_ALWAYS";
13
+ keywords: string;
14
+ }
15
+ export interface WebSearchResponse {
16
+ id: string;
17
+ created: number;
18
+ request_id: string;
19
+ search_intent: WebSearchIntentItem[];
20
+ search_result: WebSearchResultItem[];
21
+ }
22
+ export interface WebSearchError {
23
+ error: {
24
+ code: string;
25
+ message: string;
26
+ };
27
+ }
28
+ export interface WebSearchApiRequest {
29
+ search_query: string;
30
+ search_engine: string;
31
+ search_intent: boolean;
32
+ count?: number;
33
+ search_domain_filter?: string;
34
+ search_recency_filter?: string;
35
+ content_size?: "medium" | "high";
36
+ request_id?: string;
37
+ user_id?: string;
38
+ }
39
+ export type WebSearchToolOutput = {
40
+ id: string;
41
+ created: number;
42
+ request_id: string;
43
+ search_intent: WebSearchIntentItem[];
44
+ count: number;
45
+ results: WebSearchResultItem[];
46
+ };
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,GAAG,aAAa,GAAG,eAAe,CAAC;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,aAAa,EAAE,mBAAmB,EAAE,CAAC;CACtC;AAGD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAGD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // 智谱 Web Search API 请求/响应类型定义
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8BAA8B"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@yyaylt/zhipu-web-search-mcp-server",
3
+ "version": "1.0.1",
4
+ "description": "MCP server for ZHIPU AI Web Search API integration",
5
+ "keywords": [
6
+ "zhipu",
7
+ "web-search",
8
+ "mcp-server"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://gitee.com/yyaylt/zhipu-web-search-mcp-server.git"
13
+ },
14
+ "license": "Apache-2.0",
15
+ "author": "lost_fish",
16
+ "type": "module",
17
+ "main": "dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "directories": {
20
+ "doc": "docs"
21
+ },
22
+ "scripts": {
23
+ "start": "node dist/index.js",
24
+ "dev": "tsx watch src/index.ts",
25
+ "build": "tsc",
26
+ "clean": "rimraf dist",
27
+ "test": "node --test --import tsx \"tests/**/*.test.ts\""
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.0",
31
+ "axios": "^1.7.9",
32
+ "zod": "^3.24.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.0",
36
+ "rimraf": "^6.0.1",
37
+ "tsx": "^4.19.2",
38
+ "typescript": "^5.7.2"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "files": [
44
+ "dist/**/*",
45
+ "src/**/*",
46
+ "README.md",
47
+ "LICENSE",
48
+ "package.json"
49
+ ]
50
+ }
@@ -0,0 +1,40 @@
1
+ // 智谱 Web Search API 相关常量定义
2
+
3
+ // API 基础地址(智谱开放平台)
4
+ export const API_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/web_search";
5
+
6
+ // 响应内容的最大字符数,防止单次返回内容过大
7
+ export const CHARACTER_LIMIT = 25000;
8
+
9
+ // 支持的搜索引擎编码
10
+ export const SEARCH_ENGINES = {
11
+ STORE: "search_std",
12
+ PRO: "search_pro",
13
+ PRO_SOGOU: "search_pro_sogou",
14
+ PRO_QUARK: "search_pro_quark"
15
+ } as const;
16
+
17
+ // 时间范围过滤可选值
18
+ export const RECENCY_FILTERS = {
19
+ ONE_DAY: "oneDay",
20
+ ONE_WEEK: "oneWeek",
21
+ ONE_MONTH: "oneMonth",
22
+ ONE_YEAR: "oneYear",
23
+ NO_LIMIT: "noLimit"
24
+ } as const;
25
+
26
+ // 内容长度选项
27
+ export const CONTENT_SIZES = {
28
+ MEDIUM: "medium",
29
+ HIGH: "high"
30
+ } as const;
31
+
32
+ // 各搜索引擎支持的 count 枚举约束
33
+ export const COUNT_BY_ENGINE: Record<string, number[]> = {
34
+ [SEARCH_ENGINES.STORE]: [10, 20, 30, 40, 50],
35
+ [SEARCH_ENGINES.PRO]: [10, 20, 30, 40, 50],
36
+ [SEARCH_ENGINES.PRO_SOGOU]: [10, 20, 30, 40, 50]
37
+ };
38
+
39
+ // 请求超时时间(毫秒)
40
+ export const REQUEST_TIMEOUT_MS = 30000;
package/src/index.ts ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 智谱 Web Search MCP Server
4
+ *
5
+ * 基于智谱开放平台 Web Search API(/paas/v4/web_search)实现的 MCP 服务器。
6
+ * 通过标准输入输出(stdio)与客户端通信,提供网络搜索能力。
7
+ *
8
+ * 环境变量:
9
+ * ZHIPU_API_KEY - 智谱 API Key(必填),从 https://bigmodel.cn/usercenter/proj-mgmt/apikeys 获取
10
+ */
11
+
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { callWebSearch, handleApiError } from "./services/webSearchApi.js";
15
+ import { WebSearchInputSchema } from "./schemas/webSearch.js";
16
+ import { CHARACTER_LIMIT } from "./constants.js";
17
+ import type { WebSearchToolOutput } from "./types.js";
18
+
19
+ // 创建 MCP 服务器实例
20
+ const server = new McpServer({
21
+ name: "zhipu-web-search-mcp-server",
22
+ version: "1.0.0"
23
+ });
24
+
25
+ // 将搜索结果格式化为 Markdown 文本
26
+ function formatResultsMarkdown(output: WebSearchToolOutput): string {
27
+ const lines: string[] = [];
28
+
29
+ if (output.search_intent && output.search_intent.length > 0) {
30
+ const intent = output.search_intent[0];
31
+ lines.push(`## 搜索意图识别`);
32
+ lines.push(`- 原始查询:${intent.query}`);
33
+ lines.push(`- 识别意图:${intent.intent}`);
34
+ if (intent.keywords) {
35
+ lines.push(`- 改写关键词:${intent.keywords}`);
36
+ }
37
+ lines.push("");
38
+ }
39
+
40
+ if (output.results.length === 0) {
41
+ lines.push("未找到相关搜索结果。");
42
+ return lines.join("\n");
43
+ }
44
+
45
+ lines.push(`# 搜索结果 (${output.count})`);
46
+ lines.push("");
47
+ output.results.forEach((item, index) => {
48
+ lines.push(`## ${index + 1}. ${item.title}`);
49
+ if (item.publish_date) {
50
+ lines.push(`**发布时间:** ${item.publish_date}`);
51
+ }
52
+ if (item.media) {
53
+ lines.push(`**来源:** ${item.media}`);
54
+ }
55
+ if (item.link) {
56
+ lines.push(`**链接:** ${item.link}`);
57
+ }
58
+ lines.push("");
59
+ lines.push(item.content || "(无摘要)");
60
+ lines.push("");
61
+ });
62
+ return lines.join("\n");
63
+ }
64
+
65
+ // 注册网络搜索工具
66
+ server.registerTool(
67
+ "zhipu_web_search",
68
+ {
69
+ title: "智谱网络搜索",
70
+ description: `通过智谱开放平台 Web Search API 执行网络搜索,返回网页标题、摘要、链接、网站名称等信息。
71
+
72
+ 支持四种搜索引擎:search_std(智谱基础版)、search_pro(智谱高阶版)、search_pro_sogou(搜狗)、search_pro_quark(夸克搜索)。
73
+
74
+ Args:
75
+ - search_query (string): 需要搜索的内容,建议不超过 70 个字符
76
+ - search_engine (string): 搜索引擎编码(默认 search_std)
77
+ - search_intent (boolean): 是否执行搜索意图识别(默认 false)
78
+ - count (number): 返回结果条数,1-50(默认 10)
79
+ - search_domain_filter (string): 白名单域名过滤,仅返回指定域名内容(可选)
80
+ - search_recency_filter (string): 时间范围过滤:oneDay/oneWeek/oneMonth/oneYear/noLimit(默认 noLimit)
81
+ - content_size (string): 返回内容长短:medium/high(默认 medium)
82
+ - user_id (string): 终端用户唯一 ID,长度 6-128 字符(可选)
83
+
84
+ Returns:
85
+ Markdown 文本:搜索意图识别结果 + 搜索结果列表(标题、发布时间、来源、链接、摘要)
86
+ 同时返回结构化 JSON 数据。
87
+
88
+ Examples:
89
+ - "帮我搜索今天的天气" -> search_query="今天北京天气"
90
+ - "搜索 GPT-5 相关新闻" -> search_query="GPT-5 新闻", search_recency_filter="oneWeek"
91
+ - "在夸克上搜索 Python 教程" -> search_query="Python 教程", search_engine="search_pro_quark"
92
+
93
+ Error Handling:
94
+ - 1701:网络搜索并发已达上限,稍后重试
95
+ - 1702:系统未找到可用的搜索引擎服务
96
+ - 1703:搜索引擎未返回有效数据,请调整查询条件`,
97
+ inputSchema: WebSearchInputSchema,
98
+ annotations: {
99
+ readOnlyHint: true,
100
+ destructiveHint: false,
101
+ idempotentHint: true,
102
+ openWorldHint: true
103
+ }
104
+ },
105
+ async (params) => {
106
+ const apiKey = process.env.ZHIPU_API_KEY;
107
+ if (!apiKey) {
108
+ return {
109
+ isError: true,
110
+ content: [
111
+ {
112
+ type: "text" as const,
113
+ text: "缺少 ZHIPU_API_KEY 环境变量。请在运行前通过环境变量设置智谱 API Key,获取地址:https://bigmodel.cn/usercenter/proj-mgmt/apikeys"
114
+ }
115
+ ]
116
+ };
117
+ }
118
+
119
+ try {
120
+ const data = await callWebSearch(apiKey, {
121
+ search_query: params.search_query,
122
+ search_engine: params.search_engine,
123
+ search_intent: params.search_intent,
124
+ count: params.count,
125
+ ...(params.search_domain_filter
126
+ ? { search_domain_filter: params.search_domain_filter }
127
+ : {}),
128
+ search_recency_filter: params.search_recency_filter,
129
+ content_size: params.content_size,
130
+ ...(params.user_id ? { user_id: params.user_id } : {})
131
+ });
132
+
133
+ const results = data.search_result ?? [];
134
+ const output: WebSearchToolOutput = {
135
+ id: data.id,
136
+ created: data.created,
137
+ request_id: data.request_id,
138
+ search_intent: data.search_intent ?? [],
139
+ count: results.length,
140
+ results
141
+ };
142
+
143
+ let text = formatResultsMarkdown(output);
144
+
145
+ // 超长截断,防止响应过大
146
+ if (text.length > CHARACTER_LIMIT) {
147
+ const keepItems = Math.max(1, Math.floor(results.length / 2));
148
+ const truncated = {
149
+ ...output,
150
+ results: results.slice(0, keepItems)
151
+ };
152
+ text = `${formatResultsMarkdown(truncated)}\n\n> 响应因过长被截断(原 ${results.length} 条),可减少 count 或增加关键词精度以查看更多结果。`;
153
+ }
154
+
155
+ return {
156
+ content: [{ type: "text" as const, text }],
157
+ structuredContent: output
158
+ };
159
+ } catch (error) {
160
+ return {
161
+ isError: true,
162
+ content: [
163
+ {
164
+ type: "text" as const,
165
+ text: handleApiError(error)
166
+ }
167
+ ]
168
+ };
169
+ }
170
+ }
171
+ );
172
+
173
+ // 主函数:通过 stdio 启动服务器
174
+ async function main() {
175
+ if (!process.env.ZHIPU_API_KEY) {
176
+ console.error("ERROR: ZHIPU_API_KEY 环境变量是必需的");
177
+ process.exit(1);
178
+ }
179
+
180
+ const transport = new StdioServerTransport();
181
+ await server.connect(transport);
182
+ console.error("智谱 Web Search MCP server 正在运行 (stdio)");
183
+ }
184
+
185
+ main().catch((error) => {
186
+ console.error("服务器启动失败:", error);
187
+ process.exit(1);
188
+ });
@@ -0,0 +1,72 @@
1
+ // Web Search 工具的 Zod 输入校验 schema
2
+
3
+ import { z } from "zod";
4
+ import {
5
+ SEARCH_ENGINES,
6
+ RECENCY_FILTERS,
7
+ CONTENT_SIZES
8
+ } from "../constants.js";
9
+
10
+ export const WebSearchInputSchema = z
11
+ .object({
12
+ search_query: z
13
+ .string()
14
+ .min(1, "搜索关键词不能为空")
15
+ .max(70, "搜索关键词不能超过 70 个字符")
16
+ .describe("需要搜索的内容,建议不超过 70 个字符"),
17
+
18
+ search_engine: z
19
+ .nativeEnum(SEARCH_ENGINES)
20
+ .default(SEARCH_ENGINES.STORE)
21
+ .describe(
22
+ "要调用的搜索引擎编码:search_std(智谱基础版)、search_pro(智谱高阶版)、search_pro_sogou(搜狗)、search_pro_quark(夸克搜索)"
23
+ ),
24
+
25
+ search_intent: z
26
+ .boolean()
27
+ .default(false)
28
+ .describe(
29
+ "是否执行搜索意图识别。true:先识别是否有搜索意图再搜索;false:跳过识别直接搜索"
30
+ ),
31
+
32
+ count: z
33
+ .number()
34
+ .int()
35
+ .min(1)
36
+ .max(50)
37
+ .default(10)
38
+ .describe(
39
+ "返回结果的条数,范围 1-50。注意:search_pro_sogou 仅支持枚举值 10/20/30/40/50,且同时指定 search_domain_filter 和 search_recency_filter 时 count 不生效"
40
+ ),
41
+
42
+ search_domain_filter: z
43
+ .string()
44
+ .optional()
45
+ .describe(
46
+ "限定搜索结果范围的白名单域名(如 www.example.com)。仅 search_std、search_pro、search_pro_sogou 支持"
47
+ ),
48
+
49
+ search_recency_filter: z
50
+ .nativeEnum(RECENCY_FILTERS)
51
+ .default(RECENCY_FILTERS.NO_LIMIT)
52
+ .describe(
53
+ "搜索指定时间范围内的网页。可选值:oneDay(一天内)、oneWeek(一周内)、oneMonth(一个月内)、oneYear(一年内)、noLimit(不限,默认)"
54
+ ),
55
+
56
+ content_size: z
57
+ .nativeEnum(CONTENT_SIZES)
58
+ .default(CONTENT_SIZES.MEDIUM)
59
+ .describe(
60
+ "控制返回网页内容长短:medium(摘要信息,适合常规问答)、high(最大化上下文,信息量更大,适合需要细节的场景)"
61
+ ),
62
+
63
+ user_id: z
64
+ .string()
65
+ .min(6)
66
+ .max(128)
67
+ .optional()
68
+ .describe("终端用户的唯一 ID,用于平台对非法活动的干预,长度 6-128 个字符")
69
+ })
70
+ .strict();
71
+
72
+ export type WebSearchInput = z.infer<typeof WebSearchInputSchema>;
@@ -0,0 +1,95 @@
1
+ // Web Search API 客户端服务,负责与智谱开放平台通信
2
+
3
+ import axios, { AxiosError } from "axios";
4
+ import { API_BASE_URL, REQUEST_TIMEOUT_MS } from "../constants.js";
5
+ import type { WebSearchApiRequest, WebSearchResponse } from "../types.js";
6
+
7
+ /**
8
+ * 调用智谱 Web Search API。
9
+ * @param apiKey 智谱 API Key
10
+ * @param params 搜索请求参数
11
+ * @returns 搜索响应
12
+ */
13
+ export async function callWebSearch(
14
+ apiKey: string,
15
+ params: WebSearchApiRequest
16
+ ): Promise<WebSearchResponse> {
17
+ try {
18
+ const response = await axios.post<WebSearchResponse>(
19
+ API_BASE_URL,
20
+ params,
21
+ {
22
+ timeout: REQUEST_TIMEOUT_MS,
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ "Accept": "application/json",
26
+ "Authorization": `Bearer ${apiKey}`
27
+ }
28
+ }
29
+ );
30
+ return response.data;
31
+ } catch (error) {
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ // 错误码到建议操作的映射(智谱官方错误码)
37
+ const ERROR_SUGGESTIONS: Record<string, string> = {
38
+ "1701": "网络搜索并发已达上限,请稍后重试或减少并发请求。",
39
+ "1702": "系统未找到可用的搜索引擎服务,请检查配置或联系管理员。",
40
+ "1703": "搜索引擎未返回有效数据,请调整查询条件后重试。"
41
+ };
42
+
43
+ /**
44
+ * 将 API 异常转换为可读、可操作的中文错误提示。
45
+ * 兜底分支不向客户端返回原始错误内容,仅将详细错误写入 stderr 供本地排查,
46
+ * 避免向 MCP 客户端泄露内部路径、服务地址等敏感信息。
47
+ */
48
+ export function handleApiError(error: unknown): string {
49
+ if (axios.isAxiosError(error)) {
50
+ const axiosError = error as AxiosError<WebSearchResponse>;
51
+ if (axiosError.response) {
52
+ const status = axiosError.response.status;
53
+ const body = axiosError.response.data as unknown;
54
+
55
+ // 尝试解析智谱业务错误码
56
+ const bizCode = extractBizCode(body);
57
+ if (bizCode && ERROR_SUGGESTIONS[bizCode]) {
58
+ return `搜索失败(错误码 ${bizCode}):${ERROR_SUGGESTIONS[bizCode]}`;
59
+ }
60
+
61
+ if (status === 401) {
62
+ return "认证失败:请检查 ZHIPU_API_KEY 环境变量是否正确设置。";
63
+ }
64
+ if (status === 429) {
65
+ return "请求过于频繁:网络搜索并发已达上限,请稍后重试。";
66
+ }
67
+ return `API 请求失败,HTTP 状态码 ${status}。`;
68
+ }
69
+ if (axiosError.code === "ECONNABORTED") {
70
+ return "请求超时:网络搜索服务响应过慢,请稍后重试。";
71
+ }
72
+ if (axiosError.code === "ECONNREFUSED") {
73
+ return "网络错误:无法连接到智谱开放平台,请检查网络。";
74
+ }
75
+ }
76
+ // 详细错误仅写入 stderr(stdio 服务器日志通道,供开发者本地排查),不返回给客户端
77
+ console.error("智谱 Web Search 调用发生未知错误:", error);
78
+ return `发生未知错误,请稍后重试或联系管理员。`;
79
+ }
80
+
81
+ // 从错误响应体中提取业务错误码
82
+ function extractBizCode(body: unknown): string | undefined {
83
+ if (
84
+ body &&
85
+ typeof body === "object" &&
86
+ "error" in body &&
87
+ body.error &&
88
+ typeof body.error === "object" &&
89
+ "code" in body.error
90
+ ) {
91
+ const code = (body.error as { code: unknown }).code;
92
+ return typeof code === "string" ? code : undefined;
93
+ }
94
+ return undefined;
95
+ }
package/src/types.ts ADDED
@@ -0,0 +1,59 @@
1
+ // 智谱 Web Search API 请求/响应类型定义
2
+
3
+ // 搜索结果条目
4
+ export interface WebSearchResultItem {
5
+ title: string;
6
+ content: string;
7
+ link: string;
8
+ media: string;
9
+ icon: string;
10
+ refer: string;
11
+ publish_date: string;
12
+ }
13
+
14
+ // 搜索意图识别结果
15
+ export interface WebSearchIntentItem {
16
+ query: string;
17
+ intent: "SEARCH_ALL" | "SEARCH_NONE" | "SEARCH_ALWAYS";
18
+ keywords: string;
19
+ }
20
+
21
+ // API 成功响应
22
+ export interface WebSearchResponse {
23
+ id: string;
24
+ created: number;
25
+ request_id: string;
26
+ search_intent: WebSearchIntentItem[];
27
+ search_result: WebSearchResultItem[];
28
+ }
29
+
30
+ // API 错误响应
31
+ export interface WebSearchError {
32
+ error: {
33
+ code: string;
34
+ message: string;
35
+ };
36
+ }
37
+
38
+ // 发送给 API 的请求参数
39
+ export interface WebSearchApiRequest {
40
+ search_query: string;
41
+ search_engine: string;
42
+ search_intent: boolean;
43
+ count?: number;
44
+ search_domain_filter?: string;
45
+ search_recency_filter?: string;
46
+ content_size?: "medium" | "high";
47
+ request_id?: string;
48
+ user_id?: string;
49
+ }
50
+
51
+ // 工具返回值(结构化输出)
52
+ export type WebSearchToolOutput = {
53
+ id: string;
54
+ created: number;
55
+ request_id: string;
56
+ search_intent: WebSearchIntentItem[];
57
+ count: number;
58
+ results: WebSearchResultItem[];
59
+ };