@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 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.5.1",
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": "^4.1.10",
75
+ "@vitest/coverage-istanbul": "^5.0.3",
76
76
  "typescript": "^7.0.2",
77
- "vitest": "^4.1.10",
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
  }
@@ -193,18 +193,40 @@
193
193
  }
194
194
 
195
195
  function handleEvent(block) {
196
- var isChunks = /(^|\n)event:\s*chunks/.test(block);
197
- var m = block.match(/(^|\n)data:\s?(.*)$/s);
198
- if (!m) return;
199
- var payload = m[2].trim();
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 (isChunks) {
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) return;
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();