@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 +44 -0
- package/bin/echo-pdf.js +91 -22
- 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
|
@@ -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
|
-
|
|
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
|
|
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") {
|