@echofiles/echo-pdf 0.4.0 → 0.4.2
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 +80 -0
- package/bin/echo-pdf.js +9 -164
- package/bin/lib/http.js +72 -0
- package/bin/lib/mcp-stdio.js +99 -0
- package/dist/agent-defaults.d.ts +3 -0
- package/dist/agent-defaults.js +18 -0
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +24 -0
- package/dist/core/index.d.ts +50 -0
- package/dist/core/index.js +7 -0
- package/dist/file-ops.d.ts +11 -0
- package/dist/file-ops.js +36 -0
- package/dist/file-store-do.d.ts +36 -0
- package/dist/file-store-do.js +298 -0
- package/dist/file-utils.d.ts +6 -0
- package/dist/file-utils.js +36 -0
- package/dist/http-error.d.ts +9 -0
- package/dist/http-error.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.js +127 -0
- package/dist/pdf-agent.d.ts +18 -0
- package/dist/pdf-agent.js +217 -0
- package/dist/pdf-config.d.ts +4 -0
- package/dist/pdf-config.js +130 -0
- package/dist/pdf-storage.d.ts +8 -0
- package/dist/pdf-storage.js +86 -0
- package/dist/pdf-types.d.ts +79 -0
- package/dist/pdf-types.js +1 -0
- package/dist/pdfium-engine.d.ts +9 -0
- package/dist/pdfium-engine.js +180 -0
- package/dist/provider-client.d.ts +12 -0
- package/dist/provider-client.js +134 -0
- package/dist/provider-keys.d.ts +10 -0
- package/dist/provider-keys.js +27 -0
- package/dist/r2-file-store.d.ts +20 -0
- package/dist/r2-file-store.js +176 -0
- package/dist/response-schema.d.ts +15 -0
- package/dist/response-schema.js +159 -0
- package/dist/tool-registry.d.ts +16 -0
- package/dist/tool-registry.js +175 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/worker.d.ts +7 -0
- package/dist/worker.js +366 -0
- package/package.json +22 -4
- package/wrangler.toml +1 -1
- package/src/agent-defaults.ts +0 -25
- package/src/file-ops.ts +0 -50
- package/src/file-store-do.ts +0 -349
- package/src/file-utils.ts +0 -43
- package/src/http-error.ts +0 -21
- package/src/index.ts +0 -400
- package/src/mcp-server.ts +0 -158
- package/src/pdf-agent.ts +0 -252
- package/src/pdf-config.ts +0 -143
- package/src/pdf-storage.ts +0 -109
- package/src/pdf-types.ts +0 -85
- package/src/pdfium-engine.ts +0 -207
- package/src/provider-client.ts +0 -176
- package/src/provider-keys.ts +0 -44
- package/src/r2-file-store.ts +0 -195
- package/src/response-schema.ts +0 -182
- package/src/tool-registry.ts +0 -203
- package/src/types.ts +0 -40
- package/src/wasm.d.ts +0 -4
package/README.md
CHANGED
|
@@ -13,6 +13,80 @@
|
|
|
13
13
|
- CLI
|
|
14
14
|
- HTTP API
|
|
15
15
|
|
|
16
|
+
## Using echo-pdf as a library
|
|
17
|
+
|
|
18
|
+
`@echofiles/echo-pdf` 支持直接作为库导入,面向下游复用 `pdf_extract_pages / pdf_ocr_pages / pdf_tables_to_latex / file_ops` 工具实现。
|
|
19
|
+
|
|
20
|
+
### Public entrypoints(semver 稳定)
|
|
21
|
+
|
|
22
|
+
- `@echofiles/echo-pdf`:core API(推荐)
|
|
23
|
+
- `@echofiles/echo-pdf/core`:与根入口等价的 core API
|
|
24
|
+
- `@echofiles/echo-pdf/worker`:Worker 路由入口(给 Wrangler/Worker 集成用)
|
|
25
|
+
|
|
26
|
+
仅以上 `exports` 子路径视为公开 API。`src/*`、`dist/*` 等深路径导入不受兼容性承诺保护,可能在次版本中变动。
|
|
27
|
+
|
|
28
|
+
### Runtime expectations
|
|
29
|
+
|
|
30
|
+
- Node.js: `>=20`(与 `package.json#engines` 一致)
|
|
31
|
+
- 需要 ESM `import` 能力与标准 `fetch`(Node 20+ 原生支持)
|
|
32
|
+
- 建议使用支持 package `exports` 的现代 bundler/runtime(Vite、Webpack 5、Rspack、esbuild、Wrangler 等)
|
|
33
|
+
- TypeScript 消费方建议:`module=NodeNext` + `moduleResolution=NodeNext`
|
|
34
|
+
|
|
35
|
+
### Clean project import smoke
|
|
36
|
+
|
|
37
|
+
下面这段命令与仓库中的集成测试保持一致,可在全新目录验证 npm 包“可直接 import”:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
tmpdir="$(mktemp -d)"
|
|
41
|
+
cd "$tmpdir"
|
|
42
|
+
npm init -y
|
|
43
|
+
npm i /path/to/echofiles-echo-pdf-<version>.tgz
|
|
44
|
+
node --input-type=module -e "await import('@echofiles/echo-pdf'); await import('@echofiles/echo-pdf/core'); await import('@echofiles/echo-pdf/worker'); console.log('ok')"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Example
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { callTool, listToolSchemas } from "@echofiles/echo-pdf"
|
|
51
|
+
import configJson from "./echo-pdf.config.json" with { type: "json" }
|
|
52
|
+
|
|
53
|
+
const fileStore = {
|
|
54
|
+
async put(input) {
|
|
55
|
+
const id = crypto.randomUUID()
|
|
56
|
+
const record = { ...input, id, sizeBytes: input.bytes.byteLength, createdAt: new Date().toISOString() }
|
|
57
|
+
memory.set(id, record)
|
|
58
|
+
return record
|
|
59
|
+
},
|
|
60
|
+
async get(id) {
|
|
61
|
+
return memory.get(id) ?? null
|
|
62
|
+
},
|
|
63
|
+
async list() {
|
|
64
|
+
return [...memory.values()]
|
|
65
|
+
},
|
|
66
|
+
async delete(id) {
|
|
67
|
+
return memory.delete(id)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const memory = new Map()
|
|
72
|
+
const env = {}
|
|
73
|
+
|
|
74
|
+
console.log(listToolSchemas().map((tool) => tool.name))
|
|
75
|
+
|
|
76
|
+
const result = await callTool(
|
|
77
|
+
"pdf_extract_pages",
|
|
78
|
+
{ fileId: "<FILE_ID>", pages: [1], returnMode: "inline" },
|
|
79
|
+
{ config: configJson, env, fileStore }
|
|
80
|
+
)
|
|
81
|
+
console.log(result)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
版本策略:
|
|
85
|
+
|
|
86
|
+
- `exports` 列出的入口及其导出符号按 semver 管理
|
|
87
|
+
- 对公开 API 的破坏性变更只会在 major 版本发布
|
|
88
|
+
- 新增导出、参数扩展(向后兼容)会在 minor/patch 发布
|
|
89
|
+
|
|
16
90
|
## 1. 服务地址
|
|
17
91
|
|
|
18
92
|
请先确定你的线上地址(Worker 域名)。文档里用:
|
|
@@ -144,6 +218,7 @@ echo-pdf setup add cursor
|
|
|
144
218
|
echo-pdf setup add cline
|
|
145
219
|
echo-pdf setup add windsurf
|
|
146
220
|
echo-pdf setup add json
|
|
221
|
+
echo-pdf setup add claude-desktop --mode stdio
|
|
147
222
|
```
|
|
148
223
|
|
|
149
224
|
`setup add` 输出的是配置片段,把它合并到对应客户端的 MCP 配置文件。
|
|
@@ -283,6 +358,11 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/tools/call \
|
|
|
283
358
|
- `ECHO_PDF_MCP_KEY`(可选,启用 MCP 鉴权)
|
|
284
359
|
- `ECHO_PDF_WORKER_NAME`(CLI 默认 URL 推导)
|
|
285
360
|
|
|
361
|
+
鉴权注意:
|
|
362
|
+
|
|
363
|
+
- 如果配置了 `authHeader/authEnv` 但未注入对应 secret,服务会返回配置错误(fail-closed),不会默认放行。
|
|
364
|
+
- 仅开发调试场景可显式设置 `ECHO_PDF_ALLOW_MISSING_AUTH_SECRET=1` 临时放行“缺 secret”的请求。
|
|
365
|
+
|
|
286
366
|
## 7. 本地开发与测试
|
|
287
367
|
|
|
288
368
|
安装与开发:
|
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
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EchoPdfConfig } from "./pdf-types.js";
|
|
2
|
+
export declare const resolveProviderAlias: (config: EchoPdfConfig, requestedProvider?: string) => string;
|
|
3
|
+
export declare const resolveModelForProvider: (config: EchoPdfConfig, _providerAlias: string, requestedModel?: string) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const normalize = (value) => value.trim();
|
|
2
|
+
export const resolveProviderAlias = (config, requestedProvider) => {
|
|
3
|
+
const raw = normalize(requestedProvider ?? "");
|
|
4
|
+
if (raw.length === 0)
|
|
5
|
+
return config.agent.defaultProvider;
|
|
6
|
+
if (config.providers[raw])
|
|
7
|
+
return raw;
|
|
8
|
+
const fromType = Object.entries(config.providers).find(([, provider]) => provider.type === raw)?.[0];
|
|
9
|
+
if (fromType)
|
|
10
|
+
return fromType;
|
|
11
|
+
throw new Error(`Provider "${raw}" not configured`);
|
|
12
|
+
};
|
|
13
|
+
export const resolveModelForProvider = (config, _providerAlias, requestedModel) => {
|
|
14
|
+
const explicit = normalize(requestedModel ?? "");
|
|
15
|
+
if (explicit.length > 0)
|
|
16
|
+
return explicit;
|
|
17
|
+
return normalize(config.agent.defaultModel ?? "");
|
|
18
|
+
};
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Env } from "./types.js";
|
|
2
|
+
export interface AuthCheckOptions {
|
|
3
|
+
readonly authHeader?: string;
|
|
4
|
+
readonly authEnv?: string;
|
|
5
|
+
readonly allowMissingSecret?: boolean;
|
|
6
|
+
readonly misconfiguredCode: string;
|
|
7
|
+
readonly unauthorizedCode: string;
|
|
8
|
+
readonly contextName: string;
|
|
9
|
+
}
|
|
10
|
+
export type AuthCheckResult = {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const checkHeaderAuth: (request: Request, env: Env, options: AuthCheckOptions) => AuthCheckResult;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const checkHeaderAuth = (request, env, options) => {
|
|
2
|
+
if (!options.authHeader || !options.authEnv)
|
|
3
|
+
return { ok: true };
|
|
4
|
+
const required = env[options.authEnv];
|
|
5
|
+
if (typeof required !== "string" || required.length === 0) {
|
|
6
|
+
if (options.allowMissingSecret === true)
|
|
7
|
+
return { ok: true };
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
status: 500,
|
|
11
|
+
code: options.misconfiguredCode,
|
|
12
|
+
message: `${options.contextName} auth is configured but env "${options.authEnv}" is missing`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (request.headers.get(options.authHeader) !== required) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
status: 401,
|
|
19
|
+
code: options.unauthorizedCode,
|
|
20
|
+
message: "Unauthorized",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { ok: true };
|
|
24
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { callTool, listToolSchemas } from "../tool-registry.js";
|
|
2
|
+
export type { ToolRuntimeContext } from "../tool-registry.js";
|
|
3
|
+
export type { ToolSchema } from "../pdf-types.js";
|
|
4
|
+
export type { Env, FileStore, JsonObject } from "../types.js";
|
|
5
|
+
import type { ReturnMode } from "../types.js";
|
|
6
|
+
export interface PdfExtractPagesArgs {
|
|
7
|
+
readonly fileId?: string;
|
|
8
|
+
readonly url?: string;
|
|
9
|
+
readonly base64?: string;
|
|
10
|
+
readonly filename?: string;
|
|
11
|
+
readonly pages: ReadonlyArray<number>;
|
|
12
|
+
readonly renderScale?: number;
|
|
13
|
+
readonly returnMode?: ReturnMode;
|
|
14
|
+
}
|
|
15
|
+
export interface PdfOcrPagesArgs {
|
|
16
|
+
readonly fileId?: string;
|
|
17
|
+
readonly url?: string;
|
|
18
|
+
readonly base64?: string;
|
|
19
|
+
readonly filename?: string;
|
|
20
|
+
readonly pages: ReadonlyArray<number>;
|
|
21
|
+
readonly renderScale?: number;
|
|
22
|
+
readonly provider?: string;
|
|
23
|
+
readonly model?: string;
|
|
24
|
+
readonly prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PdfTablesToLatexArgs {
|
|
27
|
+
readonly fileId?: string;
|
|
28
|
+
readonly url?: string;
|
|
29
|
+
readonly base64?: string;
|
|
30
|
+
readonly filename?: string;
|
|
31
|
+
readonly pages: ReadonlyArray<number>;
|
|
32
|
+
readonly renderScale?: number;
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly prompt?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface FileOpsArgs {
|
|
38
|
+
readonly op: "list" | "read" | "delete" | "put";
|
|
39
|
+
readonly fileId?: string;
|
|
40
|
+
readonly includeBase64?: boolean;
|
|
41
|
+
readonly text?: string;
|
|
42
|
+
readonly filename?: string;
|
|
43
|
+
readonly mimeType?: string;
|
|
44
|
+
readonly base64?: string;
|
|
45
|
+
readonly returnMode?: ReturnMode;
|
|
46
|
+
}
|
|
47
|
+
export declare const pdf_extract_pages: (args: PdfExtractPagesArgs, ctx: import("../tool-registry.js").ToolRuntimeContext) => Promise<unknown>;
|
|
48
|
+
export declare const pdf_ocr_pages: (args: PdfOcrPagesArgs, ctx: import("../tool-registry.js").ToolRuntimeContext) => Promise<unknown>;
|
|
49
|
+
export declare const pdf_tables_to_latex: (args: PdfTablesToLatexArgs, ctx: import("../tool-registry.js").ToolRuntimeContext) => Promise<unknown>;
|
|
50
|
+
export declare const file_ops: (args: FileOpsArgs, ctx: import("../tool-registry.js").ToolRuntimeContext) => Promise<unknown>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { callTool, listToolSchemas } from "../tool-registry.js";
|
|
2
|
+
import { callTool } from "../tool-registry.js";
|
|
3
|
+
const asJsonObject = (value) => value;
|
|
4
|
+
export const pdf_extract_pages = async (args, ctx) => callTool("pdf_extract_pages", asJsonObject(args), ctx);
|
|
5
|
+
export const pdf_ocr_pages = async (args, ctx) => callTool("pdf_ocr_pages", asJsonObject(args), ctx);
|
|
6
|
+
export const pdf_tables_to_latex = async (args, ctx) => callTool("pdf_tables_to_latex", asJsonObject(args), ctx);
|
|
7
|
+
export const file_ops = async (args, ctx) => callTool("file_ops", asJsonObject(args), ctx);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FileStore, ReturnMode } from "./types.js";
|
|
2
|
+
export declare const runFileOp: (fileStore: FileStore, input: {
|
|
3
|
+
readonly op: "list" | "read" | "delete" | "put";
|
|
4
|
+
readonly fileId?: string;
|
|
5
|
+
readonly includeBase64?: boolean;
|
|
6
|
+
readonly text?: string;
|
|
7
|
+
readonly filename?: string;
|
|
8
|
+
readonly mimeType?: string;
|
|
9
|
+
readonly base64?: string;
|
|
10
|
+
readonly returnMode?: ReturnMode;
|
|
11
|
+
}) => Promise<unknown>;
|
package/dist/file-ops.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fromBase64, normalizeReturnMode, toInlineFilePayload } from "./file-utils.js";
|
|
2
|
+
export const runFileOp = async (fileStore, input) => {
|
|
3
|
+
if (input.op === "list") {
|
|
4
|
+
return { files: await fileStore.list() };
|
|
5
|
+
}
|
|
6
|
+
if (input.op === "put") {
|
|
7
|
+
const bytes = input.base64 ? fromBase64(input.base64) : new TextEncoder().encode(input.text ?? "");
|
|
8
|
+
const meta = await fileStore.put({
|
|
9
|
+
filename: input.filename ?? `file-${Date.now()}.txt`,
|
|
10
|
+
mimeType: input.mimeType ?? "text/plain; charset=utf-8",
|
|
11
|
+
bytes,
|
|
12
|
+
});
|
|
13
|
+
const returnMode = normalizeReturnMode(input.returnMode);
|
|
14
|
+
if (returnMode === "file_id")
|
|
15
|
+
return { returnMode, file: meta };
|
|
16
|
+
if (returnMode === "url")
|
|
17
|
+
return { returnMode, file: meta, url: `/api/files/get?fileId=${encodeURIComponent(meta.id)}` };
|
|
18
|
+
const stored = await fileStore.get(meta.id);
|
|
19
|
+
if (!stored)
|
|
20
|
+
throw new Error(`File not found after put: ${meta.id}`);
|
|
21
|
+
return {
|
|
22
|
+
returnMode,
|
|
23
|
+
...toInlineFilePayload(stored, true),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!input.fileId) {
|
|
27
|
+
throw new Error("fileId is required");
|
|
28
|
+
}
|
|
29
|
+
if (input.op === "delete") {
|
|
30
|
+
return { deleted: await fileStore.delete(input.fileId), fileId: input.fileId };
|
|
31
|
+
}
|
|
32
|
+
const file = await fileStore.get(input.fileId);
|
|
33
|
+
if (!file)
|
|
34
|
+
throw new Error(`File not found: ${input.fileId}`);
|
|
35
|
+
return toInlineFilePayload(file, Boolean(input.includeBase64));
|
|
36
|
+
};
|