@echofiles/echo-pdf 0.4.0 → 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 +5 -0
- package/bin/echo-pdf.js +9 -164
- package/bin/lib/http.js +72 -0
- package/bin/lib/mcp-stdio.js +99 -0
- package/package.json +1 -1
- package/src/index.ts +23 -8
- package/src/mcp-server.ts +19 -6
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ echo-pdf setup add cursor
|
|
|
144
144
|
echo-pdf setup add cline
|
|
145
145
|
echo-pdf setup add windsurf
|
|
146
146
|
echo-pdf setup add json
|
|
147
|
+
echo-pdf setup add claude-desktop --mode stdio
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
`setup add` 输出的是配置片段,把它合并到对应客户端的 MCP 配置文件。
|
|
@@ -283,6 +284,10 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/tools/call \
|
|
|
283
284
|
- `ECHO_PDF_MCP_KEY`(可选,启用 MCP 鉴权)
|
|
284
285
|
- `ECHO_PDF_WORKER_NAME`(CLI 默认 URL 推导)
|
|
285
286
|
|
|
287
|
+
鉴权注意:
|
|
288
|
+
|
|
289
|
+
- 如果配置了 `authHeader/authEnv` 但未注入对应 secret,服务会返回配置错误(fail-closed),不会默认放行。
|
|
290
|
+
|
|
286
291
|
## 7. 本地开发与测试
|
|
287
292
|
|
|
288
293
|
安装与开发:
|
package/bin/echo-pdf.js
CHANGED
|
@@ -4,6 +4,8 @@ import fs from "node:fs"
|
|
|
4
4
|
import os from "node:os"
|
|
5
5
|
import path from "node:path"
|
|
6
6
|
import { fileURLToPath } from "node:url"
|
|
7
|
+
import { downloadFile, postJson, uploadFile, withUploadedLocalFile } from "./lib/http.js"
|
|
8
|
+
import { runMcpStdio } from "./lib/mcp-stdio.js"
|
|
7
9
|
|
|
8
10
|
const CONFIG_DIR = path.join(os.homedir(), ".config", "echo-pdf-cli")
|
|
9
11
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
@@ -163,25 +165,6 @@ const buildProviderApiKeys = (config, profileName) => {
|
|
|
163
165
|
return providerApiKeys
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
const postJson = async (url, payload, extraHeaders = {}) => {
|
|
167
|
-
const response = await fetch(url, {
|
|
168
|
-
method: "POST",
|
|
169
|
-
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
170
|
-
body: JSON.stringify(payload),
|
|
171
|
-
})
|
|
172
|
-
const text = await response.text()
|
|
173
|
-
let data
|
|
174
|
-
try {
|
|
175
|
-
data = JSON.parse(text)
|
|
176
|
-
} catch {
|
|
177
|
-
data = { raw: text }
|
|
178
|
-
}
|
|
179
|
-
if (!response.ok) {
|
|
180
|
-
throw new Error(`${response.status} ${JSON.stringify(data)}`)
|
|
181
|
-
}
|
|
182
|
-
return data
|
|
183
|
-
}
|
|
184
|
-
|
|
185
168
|
const print = (data) => {
|
|
186
169
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
|
|
187
170
|
}
|
|
@@ -209,57 +192,6 @@ const buildMcpRequest = (id, method, params = {}) => ({
|
|
|
209
192
|
params,
|
|
210
193
|
})
|
|
211
194
|
|
|
212
|
-
const uploadFile = async (serviceUrl, filePath) => {
|
|
213
|
-
const absPath = path.resolve(process.cwd(), filePath)
|
|
214
|
-
const bytes = fs.readFileSync(absPath)
|
|
215
|
-
const filename = path.basename(absPath)
|
|
216
|
-
const form = new FormData()
|
|
217
|
-
form.append("file", new Blob([bytes]), filename)
|
|
218
|
-
const response = await fetch(`${serviceUrl}/api/files/upload`, { method: "POST", body: form })
|
|
219
|
-
const text = await response.text()
|
|
220
|
-
let data
|
|
221
|
-
try {
|
|
222
|
-
data = JSON.parse(text)
|
|
223
|
-
} catch {
|
|
224
|
-
data = { raw: text }
|
|
225
|
-
}
|
|
226
|
-
if (!response.ok) {
|
|
227
|
-
throw new Error(`${response.status} ${JSON.stringify(data)}`)
|
|
228
|
-
}
|
|
229
|
-
return data
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const downloadFile = async (serviceUrl, fileId, outputPath) => {
|
|
233
|
-
const response = await fetch(`${serviceUrl}/api/files/get?fileId=${encodeURIComponent(fileId)}&download=1`)
|
|
234
|
-
if (!response.ok) {
|
|
235
|
-
const text = await response.text()
|
|
236
|
-
throw new Error(`${response.status} ${text}`)
|
|
237
|
-
}
|
|
238
|
-
const bytes = Buffer.from(await response.arrayBuffer())
|
|
239
|
-
const absOut = path.resolve(process.cwd(), outputPath)
|
|
240
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true })
|
|
241
|
-
fs.writeFileSync(absOut, bytes)
|
|
242
|
-
return absOut
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const withUploadedLocalFile = async (serviceUrl, tool, args) => {
|
|
246
|
-
const nextArgs = { ...(args || {}) }
|
|
247
|
-
if (tool.startsWith("pdf_")) {
|
|
248
|
-
const localPath = typeof nextArgs.path === "string"
|
|
249
|
-
? nextArgs.path
|
|
250
|
-
: (typeof nextArgs.filePath === "string" ? nextArgs.filePath : "")
|
|
251
|
-
if (localPath && !nextArgs.fileId && !nextArgs.url && !nextArgs.base64) {
|
|
252
|
-
const upload = await uploadFile(serviceUrl, localPath)
|
|
253
|
-
const fileId = upload?.file?.id
|
|
254
|
-
if (!fileId) throw new Error(`upload failed for local path: ${localPath}`)
|
|
255
|
-
nextArgs.fileId = fileId
|
|
256
|
-
delete nextArgs.path
|
|
257
|
-
delete nextArgs.filePath
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return nextArgs
|
|
261
|
-
}
|
|
262
|
-
|
|
263
195
|
const runDevServer = (port, host) => {
|
|
264
196
|
const wranglerBin = path.resolve(__dirname, "../node_modules/.bin/wrangler")
|
|
265
197
|
const wranglerArgs = ["dev", "--port", String(port), "--ip", host]
|
|
@@ -276,100 +208,13 @@ const runDevServer = (port, host) => {
|
|
|
276
208
|
})
|
|
277
209
|
}
|
|
278
210
|
|
|
279
|
-
const
|
|
280
|
-
let buffer = Buffer.alloc(0)
|
|
281
|
-
let expectedLength = null
|
|
282
|
-
process.stdin.on("data", (chunk) => {
|
|
283
|
-
buffer = Buffer.concat([buffer, chunk])
|
|
284
|
-
while (true) {
|
|
285
|
-
if (expectedLength === null) {
|
|
286
|
-
const headerEnd = buffer.indexOf("\r\n\r\n")
|
|
287
|
-
if (headerEnd === -1) break
|
|
288
|
-
const headerRaw = buffer.slice(0, headerEnd).toString("utf-8")
|
|
289
|
-
const lines = headerRaw.split("\r\n")
|
|
290
|
-
const cl = lines.find((line) => line.toLowerCase().startsWith("content-length:"))
|
|
291
|
-
if (!cl) {
|
|
292
|
-
onError(new Error("Missing Content-Length"))
|
|
293
|
-
buffer = buffer.slice(headerEnd + 4)
|
|
294
|
-
continue
|
|
295
|
-
}
|
|
296
|
-
expectedLength = Number(cl.split(":")[1]?.trim() || "0")
|
|
297
|
-
buffer = buffer.slice(headerEnd + 4)
|
|
298
|
-
}
|
|
299
|
-
if (!Number.isFinite(expectedLength) || expectedLength < 0) {
|
|
300
|
-
onError(new Error("Invalid Content-Length"))
|
|
301
|
-
expectedLength = null
|
|
302
|
-
continue
|
|
303
|
-
}
|
|
304
|
-
if (buffer.length < expectedLength) break
|
|
305
|
-
const body = buffer.slice(0, expectedLength).toString("utf-8")
|
|
306
|
-
buffer = buffer.slice(expectedLength)
|
|
307
|
-
expectedLength = null
|
|
308
|
-
try {
|
|
309
|
-
const maybePromise = onMessage(JSON.parse(body))
|
|
310
|
-
if (maybePromise && typeof maybePromise.then === "function") {
|
|
311
|
-
maybePromise.catch(onError)
|
|
312
|
-
}
|
|
313
|
-
} catch (error) {
|
|
314
|
-
onError(error)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const mcpWrite = (obj) => {
|
|
321
|
-
const body = Buffer.from(JSON.stringify(obj))
|
|
322
|
-
const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`)
|
|
323
|
-
process.stdout.write(header)
|
|
324
|
-
process.stdout.write(body)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const runMcpStdio = async () => {
|
|
211
|
+
const runMcpStdioCommand = async () => {
|
|
328
212
|
const config = loadConfig()
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (msg?.jsonrpc !== "2.0" || typeof method !== "string") {
|
|
335
|
-
mcpWrite({ jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } })
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
if (method === "notifications/initialized") return
|
|
339
|
-
if (method === "initialize" || method === "tools/list") {
|
|
340
|
-
const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
|
|
341
|
-
mcpWrite(data)
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
if (method === "tools/call") {
|
|
345
|
-
try {
|
|
346
|
-
const tool = String(msg?.params?.name || "")
|
|
347
|
-
const args = (msg?.params?.arguments && typeof msg.params.arguments === "object")
|
|
348
|
-
? msg.params.arguments
|
|
349
|
-
: {}
|
|
350
|
-
const preparedArgs = await withUploadedLocalFile(serviceUrl, tool, args)
|
|
351
|
-
const payload = {
|
|
352
|
-
...msg,
|
|
353
|
-
params: {
|
|
354
|
-
...(msg.params || {}),
|
|
355
|
-
arguments: preparedArgs,
|
|
356
|
-
},
|
|
357
|
-
}
|
|
358
|
-
const data = await postJson(`${serviceUrl}/mcp`, payload, headers)
|
|
359
|
-
mcpWrite(data)
|
|
360
|
-
} catch (error) {
|
|
361
|
-
mcpWrite({
|
|
362
|
-
jsonrpc: "2.0",
|
|
363
|
-
id,
|
|
364
|
-
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
|
|
370
|
-
mcpWrite(data)
|
|
371
|
-
}, (error) => {
|
|
372
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
213
|
+
await runMcpStdio({
|
|
214
|
+
serviceUrl: config.serviceUrl,
|
|
215
|
+
headers: buildMcpHeaders(),
|
|
216
|
+
postJson,
|
|
217
|
+
withUploadedLocalFile,
|
|
373
218
|
})
|
|
374
219
|
}
|
|
375
220
|
|
|
@@ -793,7 +638,7 @@ const main = async () => {
|
|
|
793
638
|
}
|
|
794
639
|
|
|
795
640
|
if (command === "mcp" && subcommand === "stdio") {
|
|
796
|
-
await
|
|
641
|
+
await runMcpStdioCommand()
|
|
797
642
|
return
|
|
798
643
|
}
|
|
799
644
|
|
package/bin/lib/http.js
ADDED
|
@@ -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
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
|
|
73
|
-
|
|
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)
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
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
|
|
42
|
-
|
|
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)
|
|
45
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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") {
|