Download src/mcp/glm-tools.ts from bolikoto/zcode-api: direct link, hf CLI and curl.
- Browser
- Download file 7.48 kB
-
https://huggingface.co/spaces/bolikoto/zcode-api/resolve/main/src/mcp/glm-tools.ts
- Command line
-
hf download hf://spaces/bolikoto/zcode-api/src/mcp/glm-tools.ts
-
curl -L -o glm-tools.ts https://huggingface.co/spaces/bolikoto/zcode-api/resolve/main/src/mcp/glm-tools.ts
7.48 kB
| /** | |
| * GLM MCP server registry + tool-call wrappers for the three official remote | |
| * MCP servers (web_search_prime / web_reader / zread). All three use the same | |
| * GLM API key as the LLM upstream, and their endpoints are derived from the | |
| * active provider (see `glmMcpEndpoint`). | |
| * | |
| * Used by the Responses handler to: | |
| * - intercept `web_search` / `web_search_preview` hosted tools and route | |
| * them to GLM's `web_search_prime` MCP tool, | |
| * - (P2, off by default) inject `web_reader` / `zread` as function tools the | |
| * model can call directly. | |
| * | |
| * Each wrapper decodes the MCP result into a typed shape. `web_search_prime` is | |
| * the trickiest: its `content[0].text` is a *doubly-encoded* JSON string (a | |
| * stringified JSON array). The wrapper does `JSON.parse` once to unwrap the | |
| * array, leaving consumers with a clean `WebSearchHit[]`. | |
| */ | |
| import { McpClient, glmMcpEndpoint, type GlmMcpServerId, type McpClientOptions } from "./client.js"; | |
| import type { ProviderId } from "../provider/types.js"; | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // Result types | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface WebSearchHit { | |
| title: string; | |
| link: string; | |
| content: string; | |
| /** GLM's per-result reference id (e.g. "ref_1"). */ | |
| refer?: string; | |
| } | |
| export interface WebPageContent { | |
| /** Decoded page content (markdown or text, per `return_format`). */ | |
| text: string; | |
| /** Raw MCP content blocks (preserved for future field extraction). */ | |
| raw: Array<{ type: string; text?: string }>; | |
| } | |
| export interface ZreadSearchResult { | |
| // GLM zread returns free-form text content; keep it loose. | |
| text: string; | |
| } | |
| export interface ZreadRepoStructure { | |
| text: string; | |
| } | |
| export interface ZreadFileContent { | |
| text: string; | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // Pool: one McpClient per (provider, server) β reuses the session. | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface GlmMcpPoolOptions { | |
| provider: ProviderId; | |
| apiKey: string; | |
| fetchImpl?: typeof fetch; | |
| timeoutMs?: number; | |
| } | |
| /** | |
| * Lazily-initialised pool of MCP clients keyed by server id. Reuses sessions | |
| * across calls (GLM sessions are stateful and expensive to re-establish). | |
| * Call `close()` on shutdown to reset all sessions (best-effort). | |
| */ | |
| export class GlmMcpPool { | |
| private readonly opts: GlmMcpPoolOptions; | |
| private readonly clients = new Map<GlmMcpServerId, McpClient>(); | |
| constructor(opts: GlmMcpPoolOptions) { | |
| this.opts = opts; | |
| } | |
| private client(server: GlmMcpServerId): McpClient { | |
| let c = this.clients.get(server); | |
| if (!c) { | |
| const clientOpts: McpClientOptions = { | |
| url: glmMcpEndpoint(this.opts.provider, server), | |
| apiKey: this.opts.apiKey, | |
| fetchImpl: this.opts.fetchImpl, | |
| timeoutMs: this.opts.timeoutMs, | |
| }; | |
| c = new McpClient(clientOpts); | |
| this.clients.set(server, c); | |
| } | |
| return c; | |
| } | |
| /** Reset all cached sessions (e.g. on credential rotation). */ | |
| reset(): void { | |
| for (const c of this.clients.values()) c.reset(); | |
| } | |
| // ββ web_search_prime ββ | |
| /** | |
| * Search the web via GLM's MCP. `arguments` mirror GLM's inputSchema | |
| * (verified by live `tools/list`): | |
| * - `search_query` (required) | |
| * - `search_domain_filter`, `search_recency_filter`, `content_size`, `location` | |
| */ | |
| async webSearch(args: { | |
| search_query: string; | |
| search_domain_filter?: string; | |
| search_recency_filter?: "oneDay" | "oneWeek" | "oneMonth" | "oneYear" | "noLimit"; | |
| content_size?: "medium" | "high"; | |
| location?: "cn" | "us"; | |
| }): Promise<WebSearchHit[]> { | |
| const result = await this.client("web_search_prime").callTool("web_search_prime", args as Record<string, unknown>); | |
| return decodeWebSearchResult(result.content); | |
| } | |
| // ββ web_reader ββ | |
| /** Fetch a URL's content via GLM's MCP `webReader` tool. */ | |
| async webRead(args: { | |
| url: string; | |
| return_format?: "markdown" | "text"; | |
| no_cache?: boolean; | |
| retain_images?: boolean; | |
| no_gfm?: boolean; | |
| with_images_summary?: boolean; | |
| with_links_summary?: boolean; | |
| timeout?: number; | |
| }): Promise<WebPageContent> { | |
| const result = await this.client("web_reader").callTool("webReader", args as Record<string, unknown>); | |
| return { text: joinText(result.content), raw: result.content as Array<{ type: string; text?: string }> }; | |
| } | |
| // ββ zread ββ | |
| /** Search a GitHub repo's docs/issues/commits via GLM's `zread` MCP. */ | |
| async zreadSearchDoc(args: { repo_name: string; query: string; language?: "zh" | "en" }): Promise<ZreadSearchResult> { | |
| const result = await this.client("zread").callTool("search_doc", args as Record<string, unknown>); | |
| return { text: joinText(result.content) }; | |
| } | |
| /** Get a GitHub repo's directory structure via GLM's `zread` MCP. */ | |
| async zreadGetRepoStructure(args: { repo_name: string; dir_path?: string }): Promise<ZreadRepoStructure> { | |
| const result = await this.client("zread").callTool("get_repo_structure", args as Record<string, unknown>); | |
| return { text: joinText(result.content) }; | |
| } | |
| /** Read a file in a GitHub repo via GLM's `zread` MCP. */ | |
| async zreadReadFile(args: { repo_name: string; file_path: string }): Promise<ZreadFileContent> { | |
| const result = await this.client("zread").callTool("read_file", args as Record<string, unknown>); | |
| return { text: joinText(result.content) }; | |
| } | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| // Decoders | |
| // βββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Decode `web_search_prime`'s doubly-encoded result. The MCP `content[0].text` | |
| * field holds a JSON-stringified array: `"[{\"title\":...,\"link\":..., ...}]"`. | |
| * Caller-friendly: returns `[]` on any parse failure rather than throwing. | |
| */ | |
| export function decodeWebSearchResult(contentBlocks: Array<{ type: string; text?: string }>): WebSearchHit[] { | |
| const text = contentBlocks.find((c) => c.type === "text")?.text; | |
| if (!text) return []; | |
| // First unwrap: text β JSON array string. | |
| let arrayStr: string; | |
| try { | |
| // The text field itself may be a quoted JSON string ("[...]" β [...]) | |
| const unquoted = JSON.parse(text); | |
| arrayStr = typeof unquoted === "string" ? unquoted : text; | |
| } catch { | |
| arrayStr = text; | |
| } | |
| let arr: unknown; | |
| try { | |
| arr = JSON.parse(arrayStr); | |
| } catch { | |
| return []; | |
| } | |
| if (!Array.isArray(arr)) return []; | |
| return arr.filter((h): h is WebSearchHit => { | |
| if (typeof h !== "object" || h === null) return false; | |
| const hit = h as Record<string, unknown>; | |
| return typeof hit.title === "string" && typeof hit.link === "string"; | |
| }); | |
| } | |
| /** Concatenate all `text` blocks in an MCP result into a single string. */ | |
| function joinText(content: Array<{ type: string; text?: string }>): string { | |
| return content | |
| .filter((c) => c.type === "text" && typeof c.text === "string") | |
| .map((c) => c.text as string) | |
| .join("\n"); | |
| } | |