@echofiles/echo-pdf 0.3.1 → 0.4.1

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
@@ -48,6 +48,13 @@ npm i -g @echofiles/echo-pdf
48
48
  echo-pdf init --service-url https://echo-pdf.echofilesai.workers.dev
49
49
  ```
50
50
 
51
+ 本地一键启动服务(daemon):
52
+
53
+ ```bash
54
+ echo-pdf dev --port 8788
55
+ echo-pdf init --service-url http://127.0.0.1:8788
56
+ ```
57
+
51
58
  配置 API Key(仅保存在本机 CLI 配置,不会上报到服务端存储):
52
59
 
53
60
  ```bash
@@ -115,6 +122,20 @@ echo-pdf mcp call --tool pdf_extract_pages --args '{
115
122
  }'
116
123
  ```
117
124
 
125
+ ### 3.1.3 stdio MCP(支持本地文件路径)
126
+
127
+ stdio 模式会把本地 `path/filePath` 自动上传为 `fileId` 后再调用远端工具。
128
+
129
+ ```bash
130
+ echo-pdf mcp stdio
131
+ ```
132
+
133
+ 生成 Claude Desktop/Cursor 等可用的 stdio 配置片段:
134
+
135
+ ```bash
136
+ echo-pdf setup add claude-desktop --mode stdio
137
+ ```
138
+
118
139
  ### 3.2 给客户端生成 MCP 配置片段
119
140
 
120
141
  ```bash
@@ -123,6 +144,7 @@ echo-pdf setup add cursor
123
144
  echo-pdf setup add cline
124
145
  echo-pdf setup add windsurf
125
146
  echo-pdf setup add json
147
+ echo-pdf setup add claude-desktop --mode stdio
126
148
  ```
127
149
 
128
150
  `setup add` 输出的是配置片段,把它合并到对应客户端的 MCP 配置文件。
@@ -172,6 +194,12 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/api/files/upload \
172
194
 
173
195
  返回中会拿到 `file.id`。
174
196
 
197
+ CLI 等价命令:
198
+
199
+ ```bash
200
+ echo-pdf file upload ./sample.pdf
201
+ ```
202
+
175
203
  ### 5.2 提取页面图片
176
204
 
177
205
  ```bash
@@ -185,6 +213,18 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/tools/call \
185
213
  }'
186
214
  ```
187
215
 
216
+ CLI(支持直接传本地路径):
217
+
218
+ ```bash
219
+ echo-pdf call --tool pdf_extract_pages --args '{"path":"./sample.pdf","pages":[1],"returnMode":"url"}'
220
+ ```
221
+
222
+ 下载产物:
223
+
224
+ ```bash
225
+ echo-pdf file get --file-id <FILE_ID> --out ./output.bin
226
+ ```
227
+
188
228
  ### 5.3 OCR
189
229
 
190
230
  ```bash
@@ -244,6 +284,10 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/tools/call \
244
284
  - `ECHO_PDF_MCP_KEY`(可选,启用 MCP 鉴权)
245
285
  - `ECHO_PDF_WORKER_NAME`(CLI 默认 URL 推导)
246
286
 
287
+ 鉴权注意:
288
+
289
+ - 如果配置了 `authHeader/authEnv` 但未注入对应 secret,服务会返回配置错误(fail-closed),不会默认放行。
290
+
247
291
  ## 7. 本地开发与测试
248
292
 
249
293
  安装与开发:
package/bin/echo-pdf.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process"
2
3
  import fs from "node:fs"
3
4
  import os from "node:os"
4
5
  import path from "node:path"
5
6
  import { fileURLToPath } from "node:url"
7
+ import { downloadFile, postJson, uploadFile, withUploadedLocalFile } from "./lib/http.js"
8
+ import { runMcpStdio } from "./lib/mcp-stdio.js"
6
9
 
7
10
  const CONFIG_DIR = path.join(os.homedir(), ".config", "echo-pdf-cli")
8
11
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
@@ -162,25 +165,6 @@ const buildProviderApiKeys = (config, profileName) => {
162
165
  return providerApiKeys
163
166
  }
164
167
 
165
- const postJson = async (url, payload, extraHeaders = {}) => {
166
- const response = await fetch(url, {
167
- method: "POST",
168
- headers: { "Content-Type": "application/json", ...extraHeaders },
169
- body: JSON.stringify(payload),
170
- })
171
- const text = await response.text()
172
- let data
173
- try {
174
- data = JSON.parse(text)
175
- } catch {
176
- data = { raw: text }
177
- }
178
- if (!response.ok) {
179
- throw new Error(`${response.status} ${JSON.stringify(data)}`)
180
- }
181
- return data
182
- }
183
-
184
168
  const print = (data) => {
185
169
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
186
170
  }
@@ -208,6 +192,32 @@ const buildMcpRequest = (id, method, params = {}) => ({
208
192
  params,
209
193
  })
210
194
 
195
+ const runDevServer = (port, host) => {
196
+ const wranglerBin = path.resolve(__dirname, "../node_modules/.bin/wrangler")
197
+ const wranglerArgs = ["dev", "--port", String(port), "--ip", host]
198
+ const cmd = fs.existsSync(wranglerBin) ? wranglerBin : "npx"
199
+ const args = fs.existsSync(wranglerBin) ? wranglerArgs : ["-y", "wrangler", ...wranglerArgs]
200
+ const child = spawn(cmd, args, {
201
+ stdio: "inherit",
202
+ env: process.env,
203
+ cwd: process.cwd(),
204
+ })
205
+ child.on("exit", (code, signal) => {
206
+ if (signal) process.kill(process.pid, signal)
207
+ process.exit(code ?? 0)
208
+ })
209
+ }
210
+
211
+ const runMcpStdioCommand = async () => {
212
+ const config = loadConfig()
213
+ await runMcpStdio({
214
+ serviceUrl: config.serviceUrl,
215
+ headers: buildMcpHeaders(),
216
+ postJson,
217
+ withUploadedLocalFile,
218
+ })
219
+ }
220
+
211
221
  const parseConfigValue = (raw, type = "auto") => {
212
222
  if (type === "string") return String(raw)
213
223
  if (type === "number") {
@@ -302,6 +312,7 @@ const usage = () => {
302
312
  process.stdout.write(`echo-pdf CLI\n\n`)
303
313
  process.stdout.write(`Commands:\n`)
304
314
  process.stdout.write(` init [--service-url URL]\n`)
315
+ process.stdout.write(` dev [--port 8788] [--host 127.0.0.1]\n`)
305
316
  process.stdout.write(` provider set --provider <${PROVIDER_SET_NAMES.join("|")}> --api-key <KEY> [--profile name]\n`)
306
317
  process.stdout.write(` provider use --provider <${PROVIDER_ALIASES.join("|")}> [--profile name]\n`)
307
318
  process.stdout.write(` provider list [--profile name]\n`)
@@ -312,13 +323,29 @@ const usage = () => {
312
323
  process.stdout.write(` model list [--profile name]\n`)
313
324
  process.stdout.write(` tools\n`)
314
325
  process.stdout.write(` call --tool <name> --args '<json>' [--provider alias] [--model model] [--profile name]\n`)
326
+ process.stdout.write(` file upload <local.pdf>\n`)
327
+ process.stdout.write(` file get --file-id <id> --out <path>\n`)
315
328
  process.stdout.write(` mcp initialize\n`)
316
329
  process.stdout.write(` mcp tools\n`)
317
330
  process.stdout.write(` mcp call --tool <name> --args '<json>'\n`)
331
+ process.stdout.write(` mcp stdio\n`)
318
332
  process.stdout.write(` setup add <claude-desktop|claude-code|cursor|cline|windsurf|gemini|json>\n`)
319
333
  }
320
334
 
321
- const setupSnippet = (tool, serviceUrl) => {
335
+ const setupSnippet = (tool, serviceUrl, mode = "http") => {
336
+ if (mode === "stdio") {
337
+ return {
338
+ mcpServers: {
339
+ "echo-pdf": {
340
+ command: "echo-pdf",
341
+ args: ["mcp", "stdio"],
342
+ env: {
343
+ ECHO_PDF_SERVICE_URL: serviceUrl,
344
+ },
345
+ },
346
+ },
347
+ }
348
+ }
322
349
  const transport = {
323
350
  type: "streamable-http",
324
351
  url: `${serviceUrl}/mcp`,
@@ -405,6 +432,14 @@ const main = async () => {
405
432
  return
406
433
  }
407
434
 
435
+ if (command === "dev") {
436
+ const port = typeof flags.port === "string" ? Number(flags.port) : 8788
437
+ const host = typeof flags.host === "string" ? flags.host : "127.0.0.1"
438
+ if (!Number.isFinite(port) || port <= 0) throw new Error("dev --port must be positive number")
439
+ runDevServer(Math.floor(port), host)
440
+ return
441
+ }
442
+
408
443
  if (command === "provider" && subcommand === "set") {
409
444
  const providerAlias = resolveProviderAliasInput(flags.provider)
410
445
  const apiKey = flags["api-key"]
@@ -538,15 +573,42 @@ const main = async () => {
538
573
  const tool = flags.tool
539
574
  if (typeof tool !== "string") throw new Error("call requires --tool")
540
575
  const args = typeof flags.args === "string" ? JSON.parse(flags.args) : {}
576
+ const preparedArgs = await withUploadedLocalFile(config.serviceUrl, tool, args)
541
577
  const provider = resolveProviderAlias(profile, flags.provider)
542
578
  const model = typeof flags.model === "string" ? flags.model : resolveDefaultModel(profile, provider)
543
579
  const providerApiKeys = buildProviderApiKeys(config, profileName)
544
- const payload = buildToolCallRequest({ tool, args, provider, model, providerApiKeys })
580
+ const payload = buildToolCallRequest({ tool, args: preparedArgs, provider, model, providerApiKeys })
545
581
  const data = await postJson(`${config.serviceUrl}/tools/call`, payload)
546
582
  print(data)
547
583
  return
548
584
  }
549
585
 
586
+ if (command === "file") {
587
+ const action = rest[0] || ""
588
+ const config = loadConfig()
589
+ if (action === "upload") {
590
+ const filePath = rest[1]
591
+ if (!filePath) throw new Error("file upload requires a path")
592
+ const data = await uploadFile(config.serviceUrl, filePath)
593
+ print({
594
+ fileId: data?.file?.id || "",
595
+ filename: data?.file?.filename || path.basename(filePath),
596
+ sizeBytes: data?.file?.sizeBytes || 0,
597
+ file: data?.file || null,
598
+ })
599
+ return
600
+ }
601
+ if (action === "get") {
602
+ const fileId = typeof flags["file-id"] === "string" ? flags["file-id"] : ""
603
+ const out = typeof flags.out === "string" ? flags.out : ""
604
+ if (!fileId || !out) throw new Error("file get requires --file-id and --out")
605
+ const savedTo = await downloadFile(config.serviceUrl, fileId, out)
606
+ print({ ok: true, fileId, savedTo })
607
+ return
608
+ }
609
+ throw new Error("file command supports: upload|get")
610
+ }
611
+
550
612
  if (command === "mcp" && subcommand === "initialize") {
551
613
  const config = loadConfig()
552
614
  const data = await postJson(`${config.serviceUrl}/mcp`, buildMcpRequest(1, "initialize"), buildMcpHeaders())
@@ -575,11 +637,18 @@ const main = async () => {
575
637
  return
576
638
  }
577
639
 
640
+ if (command === "mcp" && subcommand === "stdio") {
641
+ await runMcpStdioCommand()
642
+ return
643
+ }
644
+
578
645
  if (command === "setup" && subcommand === "add") {
579
646
  const tool = rest[0]
580
647
  if (!tool) throw new Error("setup add requires tool name")
581
648
  const config = loadConfig()
582
- print(setupSnippet(tool, config.serviceUrl))
649
+ const mode = typeof flags.mode === "string" ? flags.mode : "http"
650
+ if (!["http", "stdio"].includes(mode)) throw new Error("setup add --mode must be http|stdio")
651
+ print(setupSnippet(tool, config.serviceUrl, mode))
583
652
  return
584
653
  }
585
654
 
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ export const postJson = async (url, payload, extraHeaders = {}) => {
5
+ const response = await fetch(url, {
6
+ method: "POST",
7
+ headers: { "Content-Type": "application/json", ...extraHeaders },
8
+ body: JSON.stringify(payload),
9
+ })
10
+ const text = await response.text()
11
+ let data
12
+ try {
13
+ data = JSON.parse(text)
14
+ } catch {
15
+ data = { raw: text }
16
+ }
17
+ if (!response.ok) {
18
+ throw new Error(`${response.status} ${JSON.stringify(data)}`)
19
+ }
20
+ return data
21
+ }
22
+
23
+ export const uploadFile = async (serviceUrl, filePath) => {
24
+ const absPath = path.resolve(process.cwd(), filePath)
25
+ const bytes = fs.readFileSync(absPath)
26
+ const filename = path.basename(absPath)
27
+ const form = new FormData()
28
+ form.append("file", new Blob([bytes]), filename)
29
+ const response = await fetch(`${serviceUrl}/api/files/upload`, { method: "POST", body: form })
30
+ const text = await response.text()
31
+ let data
32
+ try {
33
+ data = JSON.parse(text)
34
+ } catch {
35
+ data = { raw: text }
36
+ }
37
+ if (!response.ok) {
38
+ throw new Error(`${response.status} ${JSON.stringify(data)}`)
39
+ }
40
+ return data
41
+ }
42
+
43
+ export const downloadFile = async (serviceUrl, fileId, outputPath) => {
44
+ const response = await fetch(`${serviceUrl}/api/files/get?fileId=${encodeURIComponent(fileId)}&download=1`)
45
+ if (!response.ok) {
46
+ const text = await response.text()
47
+ throw new Error(`${response.status} ${text}`)
48
+ }
49
+ const bytes = Buffer.from(await response.arrayBuffer())
50
+ const absOut = path.resolve(process.cwd(), outputPath)
51
+ fs.mkdirSync(path.dirname(absOut), { recursive: true })
52
+ fs.writeFileSync(absOut, bytes)
53
+ return absOut
54
+ }
55
+
56
+ export const withUploadedLocalFile = async (serviceUrl, tool, args) => {
57
+ const nextArgs = { ...(args || {}) }
58
+ if (tool.startsWith("pdf_")) {
59
+ const localPath = typeof nextArgs.path === "string"
60
+ ? nextArgs.path
61
+ : (typeof nextArgs.filePath === "string" ? nextArgs.filePath : "")
62
+ if (localPath && !nextArgs.fileId && !nextArgs.url && !nextArgs.base64) {
63
+ const upload = await uploadFile(serviceUrl, localPath)
64
+ const fileId = upload?.file?.id
65
+ if (!fileId) throw new Error(`upload failed for local path: ${localPath}`)
66
+ nextArgs.fileId = fileId
67
+ delete nextArgs.path
68
+ delete nextArgs.filePath
69
+ }
70
+ }
71
+ return nextArgs
72
+ }
@@ -0,0 +1,99 @@
1
+ const mcpReadLoop = (onMessage, onError) => {
2
+ let buffer = Buffer.alloc(0)
3
+ let expectedLength = null
4
+ process.stdin.on("data", (chunk) => {
5
+ buffer = Buffer.concat([buffer, chunk])
6
+ while (true) {
7
+ if (expectedLength === null) {
8
+ const headerEnd = buffer.indexOf("\r\n\r\n")
9
+ if (headerEnd === -1) break
10
+ const headerRaw = buffer.slice(0, headerEnd).toString("utf-8")
11
+ const lines = headerRaw.split("\r\n")
12
+ const cl = lines.find((line) => line.toLowerCase().startsWith("content-length:"))
13
+ if (!cl) {
14
+ onError(new Error("Missing Content-Length"))
15
+ buffer = buffer.slice(headerEnd + 4)
16
+ continue
17
+ }
18
+ expectedLength = Number(cl.split(":")[1]?.trim() || "0")
19
+ buffer = buffer.slice(headerEnd + 4)
20
+ }
21
+ if (!Number.isFinite(expectedLength) || expectedLength < 0) {
22
+ onError(new Error("Invalid Content-Length"))
23
+ expectedLength = null
24
+ continue
25
+ }
26
+ if (buffer.length < expectedLength) break
27
+ const body = buffer.slice(0, expectedLength).toString("utf-8")
28
+ buffer = buffer.slice(expectedLength)
29
+ expectedLength = null
30
+ try {
31
+ const maybePromise = onMessage(JSON.parse(body))
32
+ if (maybePromise && typeof maybePromise.then === "function") {
33
+ maybePromise.catch(onError)
34
+ }
35
+ } catch (error) {
36
+ onError(error)
37
+ }
38
+ }
39
+ })
40
+ }
41
+
42
+ const mcpWrite = (obj) => {
43
+ const body = Buffer.from(JSON.stringify(obj))
44
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`)
45
+ process.stdout.write(header)
46
+ process.stdout.write(body)
47
+ }
48
+
49
+ export const runMcpStdio = async (deps) => {
50
+ const {
51
+ serviceUrl,
52
+ headers,
53
+ postJson,
54
+ withUploadedLocalFile,
55
+ } = deps
56
+ mcpReadLoop(async (msg) => {
57
+ const method = msg?.method
58
+ const id = Object.hasOwn(msg || {}, "id") ? msg.id : null
59
+ if (msg?.jsonrpc !== "2.0" || typeof method !== "string") {
60
+ mcpWrite({ jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } })
61
+ return
62
+ }
63
+ if (method === "notifications/initialized") return
64
+ if (method === "initialize" || method === "tools/list") {
65
+ const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
66
+ mcpWrite(data)
67
+ return
68
+ }
69
+ if (method === "tools/call") {
70
+ try {
71
+ const tool = String(msg?.params?.name || "")
72
+ const args = (msg?.params?.arguments && typeof msg.params.arguments === "object")
73
+ ? msg.params.arguments
74
+ : {}
75
+ const preparedArgs = await withUploadedLocalFile(serviceUrl, tool, args)
76
+ const payload = {
77
+ ...msg,
78
+ params: {
79
+ ...(msg.params || {}),
80
+ arguments: preparedArgs,
81
+ },
82
+ }
83
+ const data = await postJson(`${serviceUrl}/mcp`, payload, headers)
84
+ mcpWrite(data)
85
+ } catch (error) {
86
+ mcpWrite({
87
+ jsonrpc: "2.0",
88
+ id,
89
+ error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
90
+ })
91
+ }
92
+ return
93
+ }
94
+ const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
95
+ mcpWrite(data)
96
+ }, (error) => {
97
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
98
+ })
99
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echofiles/echo-pdf",
3
3
  "description": "MCP-first PDF agent on Cloudflare Workers with CLI and web demo.",
4
- "version": "0.3.1",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/src/index.ts CHANGED
@@ -69,11 +69,25 @@ const sanitizeDownloadFilename = (filename: string): string => {
69
69
  return cleaned.length > 0 ? cleaned : "download.bin"
70
70
  }
71
71
 
72
- const isFileGetAuthorized = (request: Request, env: Env, config: { authHeader?: string; authEnv?: string }): boolean => {
73
- if (!config.authHeader || !config.authEnv) return true
72
+ const fileGetAuthState = (
73
+ request: Request,
74
+ env: Env,
75
+ config: { authHeader?: string; authEnv?: string }
76
+ ): { ok: true } | { ok: false; status: number; code: string; message: string } => {
77
+ if (!config.authHeader || !config.authEnv) return { ok: true }
74
78
  const required = env[config.authEnv]
75
- if (typeof required !== "string" || required.length === 0) return true
76
- return request.headers.get(config.authHeader) === required
79
+ if (typeof required !== "string" || required.length === 0) {
80
+ return {
81
+ ok: false,
82
+ status: 500,
83
+ code: "AUTH_MISCONFIGURED",
84
+ message: `file get auth is configured but env "${config.authEnv}" is missing`,
85
+ }
86
+ }
87
+ if (request.headers.get(config.authHeader) !== required) {
88
+ return { ok: false, status: 401, code: "UNAUTHORIZED", message: "Unauthorized" }
89
+ }
90
+ return { ok: true }
77
91
  }
78
92
 
79
93
  const sseResponse = (stream: ReadableStream<Uint8Array>): Response =>
@@ -188,7 +202,7 @@ export default {
188
202
  preferredProvider,
189
203
  typeof body.model === "string" ? body.model : undefined
190
204
  )
191
- if (name.startsWith("pdf_")) {
205
+ if (name === "pdf_ocr_pages" || name === "pdf_tables_to_latex") {
192
206
  if (typeof args.provider !== "string" || args.provider.length === 0) {
193
207
  args.provider = preferredProvider
194
208
  }
@@ -221,7 +235,7 @@ export default {
221
235
  const models = await listProviderModels(config, env, provider, runtimeKeys)
222
236
  return json({ provider, models })
223
237
  } catch (error) {
224
- return json({ error: toError(error) }, 500)
238
+ return jsonError(error, 500)
225
239
  }
226
240
  }
227
241
 
@@ -324,8 +338,9 @@ export default {
324
338
 
325
339
  if (request.method === "GET" && url.pathname === "/api/files/get") {
326
340
  const fileGetConfig = config.service.fileGet ?? {}
327
- if (!isFileGetAuthorized(request, env, fileGetConfig)) {
328
- return json({ error: "Unauthorized", code: "UNAUTHORIZED" }, 401)
341
+ const auth = fileGetAuthState(request, env, fileGetConfig)
342
+ if (!auth.ok) {
343
+ return json({ error: auth.message, code: auth.code }, auth.status)
329
344
  }
330
345
  const fileId = url.searchParams.get("fileId") || ""
331
346
  if (!fileId) return json({ error: "Missing fileId" }, 400)
package/src/mcp-server.ts CHANGED
@@ -38,11 +38,20 @@ const err = (
38
38
  const asObj = (v: unknown): Record<string, unknown> =>
39
39
  typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : {}
40
40
 
41
- const maybeAuthorized = (request: Request, env: Env, config: EchoPdfConfig): boolean => {
42
- if (!config.mcp.authHeader || !config.mcp.authEnv) return true
41
+ const authState = (
42
+ request: Request,
43
+ env: Env,
44
+ config: EchoPdfConfig
45
+ ): { ok: true } | { ok: false; status: number; message: string } => {
46
+ if (!config.mcp.authHeader || !config.mcp.authEnv) return { ok: true }
43
47
  const required = env[config.mcp.authEnv]
44
- if (typeof required !== "string" || required.length === 0) return true
45
- return request.headers.get(config.mcp.authHeader) === required
48
+ if (typeof required !== "string" || required.length === 0) {
49
+ return { ok: false, status: 500, message: `MCP auth is configured but env "${config.mcp.authEnv}" is missing` }
50
+ }
51
+ if (request.headers.get(config.mcp.authHeader) !== required) {
52
+ return { ok: false, status: 401, message: "Unauthorized" }
53
+ }
54
+ return { ok: true }
46
55
  }
47
56
 
48
57
  const resolvePublicBaseUrl = (request: Request, configured?: string): string =>
@@ -64,8 +73,9 @@ export const handleMcpRequest = async (
64
73
  config: EchoPdfConfig,
65
74
  fileStore: FileStore
66
75
  ): Promise<Response> => {
67
- if (!maybeAuthorized(request, env, config)) {
68
- return new Response("Unauthorized", { status: 401 })
76
+ const auth = authState(request, env, config)
77
+ if (!auth.ok) {
78
+ return new Response(auth.message, { status: auth.status })
69
79
  }
70
80
 
71
81
  let body: JsonRpcRequest
@@ -85,6 +95,9 @@ export const handleMcpRequest = async (
85
95
  if (typeof method !== "string" || method.length === 0) {
86
96
  return err(id, -32600, "Invalid Request: method is required")
87
97
  }
98
+ if (method.startsWith("notifications/")) {
99
+ return new Response(null, { status: 204 })
100
+ }
88
101
  const params = asObj(body.params)
89
102
 
90
103
  if (method === "initialize") {