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.
- package/README.md +74 -0
- package/bin.js +208 -0
- 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
|
+
}
|