zhihu-mcp-proxy 1.1.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.
Files changed (3) hide show
  1. package/README.md +74 -0
  2. package/bin.js +208 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # zhihu-mcp-proxy
2
+
3
+ MCP server for Zhihu Search — bridges Zhihu's official [MCP-over-SSE API](https://developer.zhihu.com/) to stdio.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx zhihu-mcp-proxy
9
+ ```
10
+
11
+ Requires `ZHIHU_ACCESS_SECRET` environment variable.
12
+
13
+ ## Configuration
14
+
15
+ ### Claude Desktop / OpenClaw / any MCP client
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "zhihu-search": {
21
+ "command": "npx",
22
+ "args": ["zhihu-mcp-proxy"],
23
+ "env": {
24
+ "ZHIHU_ACCESS_SECRET": "your_access_secret_here"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### OpenClaw `openclaw.json`
32
+
33
+ ```json
34
+ {
35
+ "mcp": {
36
+ "servers": {
37
+ "zhihu-search": {
38
+ "command": "npx",
39
+ "args": ["zhihu-mcp-proxy"],
40
+ "env": {
41
+ "ZHIHU_ACCESS_SECRET": "your_access_secret_here"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Tool
50
+
51
+ ### `zhihu_search`
52
+
53
+ | Parameter | Type | Required | Description |
54
+ |-----------|--------|----------|------------------------------------|
55
+ | `query` | string | ✅ | Search keywords, 2-100 characters |
56
+ | `count` | number | ❌ | Number of results (1-10, default 10) |
57
+
58
+ Returns structured XML with titles, authors, content snippets, and ranking scores.
59
+
60
+ ## How It Works
61
+
62
+ 1. On startup, connects to Zhihu's SSE endpoint with your access_secret
63
+ 2. Receives the `endpoint` event with a session-specific message URL
64
+ 3. Initializes the MCP session over that channel
65
+ 4. Exposes `zhihu_search` as a local stdio MCP tool
66
+ 5. Forwards tool calls to Zhihu and returns results
67
+
68
+ ## Getting an Access Secret
69
+
70
+ Apply at [Zhihu Open Platform](https://developer.zhihu.com/).
71
+
72
+ ## License
73
+
74
+ MIT
package/bin.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * zhihu-mcp-proxy
5
+ *
6
+ * Zhihu Search MCP Proxy — bridges Zhihu's MCP-over-SSE endpoint
7
+ * to a local stdio MCP server that OpenClaw can consume.
8
+ *
9
+ * Environment variables:
10
+ * ZHIHU_ACCESS_SECRET (required) — Bearer token for Zhihu MCP API
11
+ */
12
+
13
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
14
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
15
+ const { z } = require("zod");
16
+ const { EventSource } = require("eventsource");
17
+
18
+ const ZHIHU_SSE_URL = "https://developer.zhihu.com/api/mcp/zhihu_search/v1/sse";
19
+ const ZHIHU_MSG_BASE = "https://developer.zhihu.com/api/mcp/zhihu_search/v1/message";
20
+ const SSE_CONNECT_TIMEOUT = 15_000;
21
+
22
+ // ── Zhihu SSE client ────────────────────────────────────────────────
23
+
24
+ class ZhihuSSEClient {
25
+ constructor(accessSecret) {
26
+ this.accessSecret = accessSecret;
27
+ this.es = null;
28
+ this.messageUrl = null;
29
+ this.rpcId = 0;
30
+ this.pending = new Map();
31
+ this.initialized = false;
32
+ }
33
+
34
+ async connect() {
35
+ return new Promise((resolve, reject) => {
36
+ let settled = false;
37
+ const connectTimer = setTimeout(() => {
38
+ if (!settled) {
39
+ settled = true;
40
+ reject(new Error("SSE connection timeout — did not receive endpoint event within 15s"));
41
+ }
42
+ }, SSE_CONNECT_TIMEOUT);
43
+
44
+ const es = new EventSource(ZHIHU_SSE_URL, {
45
+ headers: {
46
+ Authorization: `Bearer ${this.accessSecret}`,
47
+ },
48
+ });
49
+
50
+ es.addEventListener("endpoint", (e) => {
51
+ const path = e.data;
52
+ if (path && !settled) {
53
+ clearTimeout(connectTimer);
54
+ settled = true;
55
+ this.messageUrl = new URL(path, ZHIHU_MSG_BASE).href;
56
+ this.es = es;
57
+ resolve();
58
+ }
59
+ });
60
+
61
+ es.addEventListener("message", (e) => {
62
+ try {
63
+ const msg = JSON.parse(e.data);
64
+ this._handleMessage(msg);
65
+ } catch {
66
+ // ignore non-JSON
67
+ }
68
+ });
69
+
70
+ es.addEventListener("error", () => {
71
+ if (!settled) {
72
+ clearTimeout(connectTimer);
73
+ settled = true;
74
+ reject(new Error("SSE connection failed — check ZHIHU_ACCESS_SECRET or network"));
75
+ }
76
+ });
77
+ });
78
+ }
79
+
80
+ async initialize() {
81
+ const result = await this._send("initialize", {
82
+ protocolVersion: "2024-11-05",
83
+ clientInfo: { name: "zhihu-mcp-proxy", version: "1.0.0" },
84
+ capabilities: {},
85
+ });
86
+ this.initialized = true;
87
+ return result;
88
+ }
89
+
90
+ async callTool(name, args) {
91
+ return this._send("tools/call", { name, arguments: args });
92
+ }
93
+
94
+ _send(method, params) {
95
+ return new Promise((resolve, reject) => {
96
+ const id = ++this.rpcId;
97
+ const timer = setTimeout(() => {
98
+ this.pending.delete(id);
99
+ reject(new Error(`RPC timeout for ${method} (id=${id})`));
100
+ }, 30_000);
101
+
102
+ this.pending.set(id, { resolve, reject, timer });
103
+
104
+ const body = JSON.stringify({
105
+ jsonrpc: "2.0",
106
+ id,
107
+ method,
108
+ params,
109
+ });
110
+
111
+ fetch(this.messageUrl, {
112
+ method: "POST",
113
+ headers: {
114
+ Authorization: `Bearer ${this.accessSecret}`,
115
+ "Content-Type": "application/json",
116
+ },
117
+ body,
118
+ }).catch((err) => {
119
+ clearTimeout(timer);
120
+ this.pending.delete(id);
121
+ reject(err);
122
+ });
123
+ });
124
+ }
125
+
126
+ _handleMessage(msg) {
127
+ const { id, result, error } = msg;
128
+ if (id == null) return;
129
+ const entry = this.pending.get(id);
130
+ if (!entry) return;
131
+ clearTimeout(entry.timer);
132
+ this.pending.delete(id);
133
+ if (error) {
134
+ entry.reject(new Error(error.message || JSON.stringify(error)));
135
+ } else {
136
+ entry.resolve(result);
137
+ }
138
+ }
139
+
140
+ close() {
141
+ if (this.es) {
142
+ this.es.close();
143
+ this.es = null;
144
+ }
145
+ }
146
+ }
147
+
148
+ // ── MCP Server ──────────────────────────────────────────────────────
149
+
150
+ async function main() {
151
+ const accessSecret = process.env.ZHIHU_ACCESS_SECRET;
152
+ if (!accessSecret) {
153
+ console.error("Error: ZHIHU_ACCESS_SECRET environment variable is required");
154
+ process.exit(1);
155
+ }
156
+
157
+ const client = new ZhihuSSEClient(accessSecret);
158
+ await client.connect();
159
+ await client.initialize();
160
+
161
+ const server = new McpServer({
162
+ name: "zhihu-search",
163
+ version: "1.0.0",
164
+ });
165
+
166
+ server.tool(
167
+ "zhihu_search",
168
+ "Search Zhihu for articles, answers, and posts. Returns structured XML results with titles, authors, and content snippets.",
169
+ {
170
+ query: z.string().min(2).max(100).describe("Search keywords, 2-100 characters"),
171
+ count: z.number().min(1).max(10).optional().describe("Number of results to return (1-10, default 10)"),
172
+ },
173
+ async ({ query, count }) => {
174
+ try {
175
+ const result = await client.callTool("zhihu_search", {
176
+ query,
177
+ count: count ?? 10,
178
+ });
179
+ if (result?.content) {
180
+ return { content: result.content };
181
+ }
182
+ return {
183
+ content: [{ type: "text", text: JSON.stringify(result) }],
184
+ };
185
+ } catch (err) {
186
+ return {
187
+ content: [{ type: "text", text: `Error searching Zhihu: ${err.message}` }],
188
+ isError: true,
189
+ };
190
+ }
191
+ }
192
+ );
193
+
194
+ const transport = new StdioServerTransport();
195
+ await server.connect(transport);
196
+
197
+ const cleanup = () => {
198
+ client.close();
199
+ process.exit(0);
200
+ };
201
+ process.on("SIGINT", cleanup);
202
+ process.on("SIGTERM", cleanup);
203
+ }
204
+
205
+ main().catch((err) => {
206
+ console.error("Fatal:", err.message);
207
+ process.exit(1);
208
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "zhihu-mcp-proxy",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Zhihu Search — bridges Zhihu's official MCP-over-SSE API to stdio for npx usage",
5
+ "main": "bin.js",
6
+ "bin": {
7
+ "zhihu-mcp-proxy": "./bin.js"
8
+ },
9
+ "files": [
10
+ "bin.js",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "mcp-server",
19
+ "zhihu",
20
+ "zhihu-search",
21
+ "search",
22
+ "model-context-protocol"
23
+ ],
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.12.1",
27
+ "eventsource": "^3.0.6"
28
+ }
29
+ }