@skyphusion/search-mcp 0.5.1 → 0.7.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 +17 -0
- package/package.json +5 -4
- package/public/ask-widget.js +33 -6
package/README.md
CHANGED
|
@@ -89,6 +89,23 @@ wrangler secret put MCP_TOKEN -c wrangler.mcp.toml
|
|
|
89
89
|
wrangler secret put TURNSTILE_SECRET # optional; skips verification when unset
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Service binding RPC
|
|
93
|
+
|
|
94
|
+
`POST /ask` stays the browser route (CORS, Turnstile, per-IP rate limit). A Worker on the same account can retrieve chunks without that route by binding to the named entrypoint `SearchEntrypoint`, exported from `src/index.ts` (the public query Worker and the rockenhaus Worker, which share that file). The MCP Worker does not export it.
|
|
95
|
+
|
|
96
|
+
The entrypoint is not a URL. Internet `fetch` still hits the default export only. The caller selects the class in its own wrangler config. This Worker's wrangler bindings do not change. `entrypoint` is the caller's service-binding field ([RPC named entrypoints](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints)):
|
|
97
|
+
|
|
98
|
+
```toml
|
|
99
|
+
[[services]]
|
|
100
|
+
binding = "SEARCH_RPC"
|
|
101
|
+
service = "skyphusion-search" # or rockenhaus-search; a self-host uses its own script name
|
|
102
|
+
entrypoint = "SearchEntrypoint"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`await env.SEARCH_RPC.search(question)` returns `{ chunks }` from the same `SEARCH` binding `/ask` uses. The retrieval options match `/ask` (hybrid, 6 results, query rewrite on). Nothing is generated: the call is `SEARCH.search`, not `chatCompletions`. The question must be a non-empty string of at most 2000 characters after trim, the same bound as `/ask`. A bad call throws `Error` with one of these messages: `empty_question`, `question_too_long`, `rate_limited`, `search_unavailable`.
|
|
106
|
+
|
|
107
|
+
`ASK_LIMITER` still applies. The key is the fixed string `ask:rpc` for every binding caller. There is no Origin check and no Turnstile check: the method has no HTTP request, and only a Worker whose config names this entrypoint receives a stub.
|
|
108
|
+
|
|
92
109
|
### MCP client wiring
|
|
93
110
|
|
|
94
111
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyphusion/search-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Bearer-gated MCP server and streaming query Worker over Cloudflare AI Search, plus a git-to-R2 corpus sync toolkit.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Conrad Rockenhaus <conrad@skyphusion.org>",
|
|
@@ -72,12 +72,13 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@cloudflare/workers-types": "^5.20260713.1",
|
|
74
74
|
"@types/node": "^26.2.0",
|
|
75
|
-
"@vitest/coverage-istanbul": "^
|
|
75
|
+
"@vitest/coverage-istanbul": "^5.0.3",
|
|
76
76
|
"typescript": "^7.0.2",
|
|
77
|
-
"vitest": "^
|
|
77
|
+
"vitest": "^5.0.3",
|
|
78
78
|
"wrangler": "^4.110.0"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|
|
81
|
-
"undici": "7.29.0"
|
|
81
|
+
"undici": "7.29.0",
|
|
82
|
+
"sharp": "0.35.4"
|
|
82
83
|
}
|
|
83
84
|
}
|
package/public/ask-widget.js
CHANGED
|
@@ -193,18 +193,40 @@
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function handleEvent(block) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
var
|
|
196
|
+
// One SSE event block: an optional "event:" name plus one or more "data:" lines,
|
|
197
|
+
// which the SSE spec joins with "\n". An error event throws so the request's
|
|
198
|
+
// catch shows it: an upstream failure must never read as an empty corpus.
|
|
199
|
+
var name = "";
|
|
200
|
+
var lines = [];
|
|
201
|
+
block.split("\n").forEach(function (line) {
|
|
202
|
+
var f = line.match(/^(event|data):\s?(.*)$/);
|
|
203
|
+
if (!f) return;
|
|
204
|
+
if (f[1] === "event") name = f[2].trim();
|
|
205
|
+
else lines.push(f[2]);
|
|
206
|
+
});
|
|
207
|
+
if (!lines.length) {
|
|
208
|
+
if (name === "error") throw new Error("stream error");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
var payload = lines.join("\n").trim();
|
|
200
212
|
if (payload === "[DONE]") return;
|
|
201
213
|
var data;
|
|
202
214
|
try {
|
|
203
215
|
data = JSON.parse(payload);
|
|
204
216
|
} catch (e) {
|
|
217
|
+
if (name === "error") throw new Error("stream error");
|
|
205
218
|
return;
|
|
206
219
|
}
|
|
207
|
-
if (
|
|
220
|
+
if (name === "error" || (data && data.error)) {
|
|
221
|
+
var err = data && data.error;
|
|
222
|
+
throw new Error(
|
|
223
|
+
(typeof err === "string" && err) ||
|
|
224
|
+
(err && err.message) ||
|
|
225
|
+
(data && data.message) ||
|
|
226
|
+
"stream error",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (name === "chunks") {
|
|
208
230
|
renderSources(data);
|
|
209
231
|
return;
|
|
210
232
|
}
|
|
@@ -246,7 +268,12 @@
|
|
|
246
268
|
var buf = "";
|
|
247
269
|
function pump() {
|
|
248
270
|
return reader.read().then(function (r) {
|
|
249
|
-
if (r.done)
|
|
271
|
+
if (r.done) {
|
|
272
|
+
// Flush what is left: the final event may lack its trailing blank line.
|
|
273
|
+
buf += decoder.decode();
|
|
274
|
+
if (buf.trim()) handleEvent(buf);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
250
277
|
buf += decoder.decode(r.value, { stream: true });
|
|
251
278
|
var parts = buf.split("\n\n");
|
|
252
279
|
buf = parts.pop();
|