@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/src/index.ts
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { normalizeReturnMode } from "./file-utils"
|
|
2
|
-
import { FileStoreDO } from "./file-store-do"
|
|
3
|
-
import { resolveModelForProvider, resolveProviderAlias } from "./agent-defaults"
|
|
4
|
-
import { handleMcpRequest } from "./mcp-server"
|
|
5
|
-
import { loadEchoPdfConfig } from "./pdf-config"
|
|
6
|
-
import { getRuntimeFileStore } from "./pdf-storage"
|
|
7
|
-
import { listProviderModels } from "./provider-client"
|
|
8
|
-
import { buildToolOutputEnvelope } from "./response-schema"
|
|
9
|
-
import { callTool, listToolSchemas } from "./tool-registry"
|
|
10
|
-
import type { AgentTraceEvent, PdfOperationRequest } from "./pdf-types"
|
|
11
|
-
import type { Env, JsonObject } from "./types"
|
|
12
|
-
|
|
13
|
-
const json = (data: unknown, status = 200): Response =>
|
|
14
|
-
new Response(JSON.stringify(data), {
|
|
15
|
-
status,
|
|
16
|
-
headers: {
|
|
17
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
18
|
-
"Cache-Control": "no-store",
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const toError = (error: unknown): string =>
|
|
23
|
-
error instanceof Error ? error.message : String(error)
|
|
24
|
-
|
|
25
|
-
const errorStatus = (error: unknown): number | null => {
|
|
26
|
-
const status = (error as { status?: unknown })?.status
|
|
27
|
-
return typeof status === "number" && Number.isFinite(status) ? status : null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const errorCode = (error: unknown): string | null => {
|
|
31
|
-
const code = (error as { code?: unknown })?.code
|
|
32
|
-
return typeof code === "string" && code.length > 0 ? code : null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const errorDetails = (error: unknown): unknown => (error as { details?: unknown })?.details
|
|
36
|
-
|
|
37
|
-
const jsonError = (error: unknown, fallbackStatus = 500): Response => {
|
|
38
|
-
const status = errorStatus(error) ?? fallbackStatus
|
|
39
|
-
const code = errorCode(error)
|
|
40
|
-
const details = errorDetails(error)
|
|
41
|
-
return json({ error: toError(error), code, details }, status)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const readJson = async (request: Request): Promise<Record<string, unknown>> => {
|
|
45
|
-
try {
|
|
46
|
-
const body = await request.json()
|
|
47
|
-
if (typeof body === "object" && body !== null && !Array.isArray(body)) {
|
|
48
|
-
return body as Record<string, unknown>
|
|
49
|
-
}
|
|
50
|
-
return {}
|
|
51
|
-
} catch {
|
|
52
|
-
return {}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const asObj = (value: unknown): JsonObject =>
|
|
57
|
-
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
58
|
-
? (value as JsonObject)
|
|
59
|
-
: {}
|
|
60
|
-
|
|
61
|
-
const resolvePublicBaseUrl = (request: Request, configured?: string): string =>
|
|
62
|
-
typeof configured === "string" && configured.length > 0 ? configured : request.url
|
|
63
|
-
|
|
64
|
-
const sanitizeDownloadFilename = (filename: string): string => {
|
|
65
|
-
const cleaned = filename
|
|
66
|
-
.replace(/[\r\n"]/g, "")
|
|
67
|
-
.replace(/[^\x20-\x7E]+/g, "")
|
|
68
|
-
.trim()
|
|
69
|
-
return cleaned.length > 0 ? cleaned : "download.bin"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const isFileGetAuthorized = (request: Request, env: Env, config: { authHeader?: string; authEnv?: string }): boolean => {
|
|
73
|
-
if (!config.authHeader || !config.authEnv) return true
|
|
74
|
-
const required = env[config.authEnv]
|
|
75
|
-
if (typeof required !== "string" || required.length === 0) return true
|
|
76
|
-
return request.headers.get(config.authHeader) === required
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sseResponse = (stream: ReadableStream<Uint8Array>): Response =>
|
|
80
|
-
new Response(stream, {
|
|
81
|
-
headers: {
|
|
82
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
83
|
-
"Cache-Control": "no-store",
|
|
84
|
-
Connection: "keep-alive",
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const encodeSse = (event: string, data: unknown): Uint8Array => {
|
|
89
|
-
const encoder = new TextEncoder()
|
|
90
|
-
return encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const isValidOperation = (value: unknown): value is PdfOperationRequest["operation"] =>
|
|
94
|
-
value === "extract_pages" || value === "ocr_pages" || value === "tables_to_latex"
|
|
95
|
-
|
|
96
|
-
const toPdfOperation = (input: Record<string, unknown>, defaultProvider: string): PdfOperationRequest => ({
|
|
97
|
-
operation: isValidOperation(input.operation) ? input.operation : "extract_pages",
|
|
98
|
-
fileId: typeof input.fileId === "string" ? input.fileId : undefined,
|
|
99
|
-
url: typeof input.url === "string" ? input.url : undefined,
|
|
100
|
-
base64: typeof input.base64 === "string" ? input.base64 : undefined,
|
|
101
|
-
filename: typeof input.filename === "string" ? input.filename : undefined,
|
|
102
|
-
pages: Array.isArray(input.pages) ? input.pages.map((v) => Number(v)) : [],
|
|
103
|
-
renderScale: typeof input.renderScale === "number" ? input.renderScale : undefined,
|
|
104
|
-
provider: typeof input.provider === "string" ? input.provider : defaultProvider,
|
|
105
|
-
model: typeof input.model === "string" ? input.model : "",
|
|
106
|
-
providerApiKeys: typeof input.providerApiKeys === "object" && input.providerApiKeys !== null
|
|
107
|
-
? (input.providerApiKeys as Record<string, string>)
|
|
108
|
-
: undefined,
|
|
109
|
-
returnMode: normalizeReturnMode(input.returnMode),
|
|
110
|
-
prompt: typeof input.prompt === "string" ? input.prompt : undefined,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
const toolNameByOperation: Record<PdfOperationRequest["operation"], string> = {
|
|
114
|
-
extract_pages: "pdf_extract_pages",
|
|
115
|
-
ocr_pages: "pdf_ocr_pages",
|
|
116
|
-
tables_to_latex: "pdf_tables_to_latex",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const operationArgsFromRequest = (request: PdfOperationRequest): JsonObject => {
|
|
120
|
-
const args: JsonObject = {
|
|
121
|
-
pages: request.pages as unknown as JsonObject["pages"],
|
|
122
|
-
}
|
|
123
|
-
if (request.fileId) args.fileId = request.fileId
|
|
124
|
-
if (request.url) args.url = request.url
|
|
125
|
-
if (request.base64) args.base64 = request.base64
|
|
126
|
-
if (request.filename) args.filename = request.filename
|
|
127
|
-
if (typeof request.renderScale === "number") args.renderScale = request.renderScale
|
|
128
|
-
if (request.returnMode) args.returnMode = request.returnMode
|
|
129
|
-
if (request.provider) args.provider = request.provider
|
|
130
|
-
if (request.model) args.model = request.model
|
|
131
|
-
if (request.prompt) args.prompt = request.prompt
|
|
132
|
-
return args
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export default {
|
|
136
|
-
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
137
|
-
const url = new URL(request.url)
|
|
138
|
-
const config = loadEchoPdfConfig(env)
|
|
139
|
-
const runtimeStore = getRuntimeFileStore(env, config)
|
|
140
|
-
const fileStore = runtimeStore.store
|
|
141
|
-
|
|
142
|
-
if (request.method === "GET" && url.pathname === "/health") {
|
|
143
|
-
return json({ ok: true, service: config.service.name, now: new Date().toISOString() })
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (request.method === "GET" && url.pathname === "/config") {
|
|
147
|
-
return json({
|
|
148
|
-
service: config.service,
|
|
149
|
-
agent: config.agent,
|
|
150
|
-
providers: Object.entries(config.providers).map(([alias, provider]) => ({ alias, type: provider.type })),
|
|
151
|
-
capabilities: {
|
|
152
|
-
toolCatalogEndpoint: "/tools/catalog",
|
|
153
|
-
toolCallEndpoint: "/tools/call",
|
|
154
|
-
fileOpsEndpoint: "/api/files/op",
|
|
155
|
-
fileUploadEndpoint: "/api/files/upload",
|
|
156
|
-
fileStatsEndpoint: "/api/files/stats",
|
|
157
|
-
fileCleanupEndpoint: "/api/files/cleanup",
|
|
158
|
-
supportedReturnModes: ["inline", "file_id", "url"],
|
|
159
|
-
},
|
|
160
|
-
mcp: {
|
|
161
|
-
serverName: config.mcp.serverName,
|
|
162
|
-
version: config.mcp.version,
|
|
163
|
-
authHeader: config.mcp.authHeader ?? null,
|
|
164
|
-
},
|
|
165
|
-
fileGet: {
|
|
166
|
-
authHeader: config.service.fileGet?.authHeader ?? null,
|
|
167
|
-
cacheTtlSeconds: config.service.fileGet?.cacheTtlSeconds ?? 300,
|
|
168
|
-
},
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (request.method === "GET" && url.pathname === "/tools/catalog") {
|
|
173
|
-
return json({ tools: listToolSchemas() })
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (request.method === "POST" && url.pathname === "/tools/call") {
|
|
177
|
-
const body = await readJson(request)
|
|
178
|
-
const name = typeof body.name === "string" ? body.name : ""
|
|
179
|
-
if (!name) return json({ error: "Missing required field: name" }, 400)
|
|
180
|
-
try {
|
|
181
|
-
const args = asObj(body.arguments)
|
|
182
|
-
const preferredProvider = resolveProviderAlias(
|
|
183
|
-
config,
|
|
184
|
-
typeof body.provider === "string" ? body.provider : undefined
|
|
185
|
-
)
|
|
186
|
-
const preferredModel = resolveModelForProvider(
|
|
187
|
-
config,
|
|
188
|
-
preferredProvider,
|
|
189
|
-
typeof body.model === "string" ? body.model : undefined
|
|
190
|
-
)
|
|
191
|
-
if (name.startsWith("pdf_")) {
|
|
192
|
-
if (typeof args.provider !== "string" || args.provider.length === 0) {
|
|
193
|
-
args.provider = preferredProvider
|
|
194
|
-
}
|
|
195
|
-
if (typeof args.model !== "string" || args.model.length === 0) {
|
|
196
|
-
args.model = preferredModel
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const result = await callTool(name, args, {
|
|
201
|
-
config,
|
|
202
|
-
env,
|
|
203
|
-
fileStore,
|
|
204
|
-
providerApiKeys: typeof body.providerApiKeys === "object" && body.providerApiKeys !== null
|
|
205
|
-
? (body.providerApiKeys as Record<string, string>)
|
|
206
|
-
: undefined,
|
|
207
|
-
})
|
|
208
|
-
return json(buildToolOutputEnvelope(result, resolvePublicBaseUrl(request, config.service.publicBaseUrl)))
|
|
209
|
-
} catch (error) {
|
|
210
|
-
return jsonError(error, 500)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (request.method === "POST" && url.pathname === "/providers/models") {
|
|
215
|
-
const body = await readJson(request)
|
|
216
|
-
const provider = resolveProviderAlias(config, typeof body.provider === "string" ? body.provider : undefined)
|
|
217
|
-
const runtimeKeys = typeof body.providerApiKeys === "object" && body.providerApiKeys !== null
|
|
218
|
-
? (body.providerApiKeys as Record<string, string>)
|
|
219
|
-
: undefined
|
|
220
|
-
try {
|
|
221
|
-
const models = await listProviderModels(config, env, provider, runtimeKeys)
|
|
222
|
-
return json({ provider, models })
|
|
223
|
-
} catch (error) {
|
|
224
|
-
return json({ error: toError(error) }, 500)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (request.method === "POST" && url.pathname === "/api/agent/run") {
|
|
229
|
-
const body = await readJson(request)
|
|
230
|
-
if (Object.hasOwn(body, "operation") && !isValidOperation(body.operation)) {
|
|
231
|
-
return json({ error: "Invalid operation. Must be one of: extract_pages, ocr_pages, tables_to_latex" }, 400)
|
|
232
|
-
}
|
|
233
|
-
const requestPayload = toPdfOperation(body, config.agent.defaultProvider)
|
|
234
|
-
try {
|
|
235
|
-
const result = await callTool(toolNameByOperation[requestPayload.operation], operationArgsFromRequest(requestPayload), {
|
|
236
|
-
config,
|
|
237
|
-
env,
|
|
238
|
-
fileStore,
|
|
239
|
-
providerApiKeys: requestPayload.providerApiKeys,
|
|
240
|
-
})
|
|
241
|
-
return json(result)
|
|
242
|
-
} catch (error) {
|
|
243
|
-
return jsonError(error, 500)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (request.method === "POST" && url.pathname === "/api/agent/stream") {
|
|
248
|
-
const body = await readJson(request)
|
|
249
|
-
if (Object.hasOwn(body, "operation") && !isValidOperation(body.operation)) {
|
|
250
|
-
return json({ error: "Invalid operation. Must be one of: extract_pages, ocr_pages, tables_to_latex" }, 400)
|
|
251
|
-
}
|
|
252
|
-
const requestPayload = toPdfOperation(body, config.agent.defaultProvider)
|
|
253
|
-
const stream = new TransformStream<Uint8Array, Uint8Array>()
|
|
254
|
-
const writer = stream.writable.getWriter()
|
|
255
|
-
let queue: Promise<void> = Promise.resolve()
|
|
256
|
-
const send = (event: string, data: unknown): void => {
|
|
257
|
-
queue = queue.then(() => writer.write(encodeSse(event, data))).catch(() => undefined)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const run = async (): Promise<void> => {
|
|
261
|
-
try {
|
|
262
|
-
send("meta", { kind: "meta", startedAt: new Date().toISOString(), streaming: true })
|
|
263
|
-
send("io", { kind: "io", direction: "input", content: requestPayload })
|
|
264
|
-
|
|
265
|
-
const result = await callTool(toolNameByOperation[requestPayload.operation], operationArgsFromRequest(requestPayload), {
|
|
266
|
-
config,
|
|
267
|
-
env,
|
|
268
|
-
fileStore,
|
|
269
|
-
providerApiKeys: requestPayload.providerApiKeys,
|
|
270
|
-
trace: (event: AgentTraceEvent) => send("step", event),
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
send("io", { kind: "io", direction: "output", content: "operation completed" })
|
|
274
|
-
send("result", { kind: "result", output: result })
|
|
275
|
-
send("done", { ok: true })
|
|
276
|
-
} catch (error) {
|
|
277
|
-
send("error", { kind: "error", message: toError(error) })
|
|
278
|
-
send("done", { ok: false })
|
|
279
|
-
} finally {
|
|
280
|
-
await queue
|
|
281
|
-
await writer.close()
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
ctx.waitUntil(run())
|
|
285
|
-
return sseResponse(stream.readable)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (request.method === "POST" && url.pathname === "/api/files/op") {
|
|
289
|
-
const body = await readJson(request)
|
|
290
|
-
try {
|
|
291
|
-
const result = await callTool("file_ops", asObj(body), {
|
|
292
|
-
config,
|
|
293
|
-
env,
|
|
294
|
-
fileStore,
|
|
295
|
-
})
|
|
296
|
-
return json(result)
|
|
297
|
-
} catch (error) {
|
|
298
|
-
return jsonError(error, 500)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (request.method === "POST" && url.pathname === "/api/files/upload") {
|
|
303
|
-
try {
|
|
304
|
-
const formData = await request.formData()
|
|
305
|
-
const file = formData.get("file") as {
|
|
306
|
-
readonly name?: string
|
|
307
|
-
readonly type?: string
|
|
308
|
-
arrayBuffer?: () => Promise<ArrayBuffer>
|
|
309
|
-
} | null
|
|
310
|
-
if (!file || typeof file.arrayBuffer !== "function") {
|
|
311
|
-
return json({ error: "Missing file field: file" }, 400)
|
|
312
|
-
}
|
|
313
|
-
const bytes = new Uint8Array(await file.arrayBuffer())
|
|
314
|
-
const stored = await fileStore.put({
|
|
315
|
-
filename: file.name || `upload-${Date.now()}.pdf`,
|
|
316
|
-
mimeType: file.type || "application/pdf",
|
|
317
|
-
bytes,
|
|
318
|
-
})
|
|
319
|
-
return json({ file: stored }, 200)
|
|
320
|
-
} catch (error) {
|
|
321
|
-
return jsonError(error, 500)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (request.method === "GET" && url.pathname === "/api/files/get") {
|
|
326
|
-
const fileGetConfig = config.service.fileGet ?? {}
|
|
327
|
-
if (!isFileGetAuthorized(request, env, fileGetConfig)) {
|
|
328
|
-
return json({ error: "Unauthorized", code: "UNAUTHORIZED" }, 401)
|
|
329
|
-
}
|
|
330
|
-
const fileId = url.searchParams.get("fileId") || ""
|
|
331
|
-
if (!fileId) return json({ error: "Missing fileId" }, 400)
|
|
332
|
-
const file = await fileStore.get(fileId)
|
|
333
|
-
if (!file) return json({ error: "File not found" }, 404)
|
|
334
|
-
const download = url.searchParams.get("download") === "1"
|
|
335
|
-
const headers = new Headers()
|
|
336
|
-
headers.set("Content-Type", file.mimeType)
|
|
337
|
-
const cacheTtl = Number(fileGetConfig.cacheTtlSeconds ?? 300)
|
|
338
|
-
const cacheControl = cacheTtl > 0
|
|
339
|
-
? `public, max-age=${Math.floor(cacheTtl)}, s-maxage=${Math.floor(cacheTtl)}`
|
|
340
|
-
: "no-store"
|
|
341
|
-
headers.set("Cache-Control", cacheControl)
|
|
342
|
-
if (download) {
|
|
343
|
-
headers.set("Content-Disposition", `attachment; filename=\"${sanitizeDownloadFilename(file.filename)}\"`)
|
|
344
|
-
}
|
|
345
|
-
return new Response(file.bytes, { status: 200, headers })
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (request.method === "GET" && url.pathname === "/api/files/stats") {
|
|
349
|
-
try {
|
|
350
|
-
return json(await runtimeStore.stats(), 200)
|
|
351
|
-
} catch (error) {
|
|
352
|
-
return json({ error: toError(error) }, 500)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (request.method === "POST" && url.pathname === "/api/files/cleanup") {
|
|
357
|
-
try {
|
|
358
|
-
return json(await runtimeStore.cleanup(), 200)
|
|
359
|
-
} catch (error) {
|
|
360
|
-
return json({ error: toError(error) }, 500)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (request.method === "POST" && url.pathname === "/mcp") {
|
|
365
|
-
return await handleMcpRequest(request, env, config, fileStore)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (request.method === "GET" && env.ASSETS) {
|
|
369
|
-
const assetReq = url.pathname === "/"
|
|
370
|
-
? new Request(new URL("/index.html", url), request)
|
|
371
|
-
: request
|
|
372
|
-
const asset = await env.ASSETS.fetch(assetReq)
|
|
373
|
-
if (asset.status !== 404) return asset
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return json(
|
|
377
|
-
{
|
|
378
|
-
error: "Not found",
|
|
379
|
-
routes: {
|
|
380
|
-
health: "GET /health",
|
|
381
|
-
config: "GET /config",
|
|
382
|
-
toolsCatalog: "GET /tools/catalog",
|
|
383
|
-
toolCall: "POST /tools/call",
|
|
384
|
-
models: "POST /providers/models",
|
|
385
|
-
run: "POST /api/agent/run",
|
|
386
|
-
stream: "POST /api/agent/stream",
|
|
387
|
-
files: "POST /api/files/op",
|
|
388
|
-
fileUpload: "POST /api/files/upload",
|
|
389
|
-
fileGet: "GET /api/files/get?fileId=<id>",
|
|
390
|
-
fileStats: "GET /api/files/stats",
|
|
391
|
-
fileCleanup: "POST /api/files/cleanup",
|
|
392
|
-
mcp: "POST /mcp",
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
404
|
|
396
|
-
)
|
|
397
|
-
},
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export { FileStoreDO }
|
package/src/mcp-server.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import type { Env, FileStore } from "./types"
|
|
2
|
-
import type { EchoPdfConfig } from "./pdf-types"
|
|
3
|
-
import { buildMcpContent, buildToolOutputEnvelope } from "./response-schema"
|
|
4
|
-
import { callTool, listToolSchemas } from "./tool-registry"
|
|
5
|
-
|
|
6
|
-
interface JsonRpcRequest {
|
|
7
|
-
readonly jsonrpc?: string
|
|
8
|
-
readonly id?: string | number | null
|
|
9
|
-
readonly method?: string
|
|
10
|
-
readonly params?: unknown
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const ok = (id: JsonRpcRequest["id"], result: unknown): Response =>
|
|
14
|
-
new Response(
|
|
15
|
-
JSON.stringify({
|
|
16
|
-
jsonrpc: "2.0",
|
|
17
|
-
id: id ?? null,
|
|
18
|
-
result,
|
|
19
|
-
}),
|
|
20
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const err = (
|
|
24
|
-
id: JsonRpcRequest["id"],
|
|
25
|
-
code: number,
|
|
26
|
-
message: string,
|
|
27
|
-
data?: Record<string, unknown>
|
|
28
|
-
): Response =>
|
|
29
|
-
new Response(
|
|
30
|
-
JSON.stringify({
|
|
31
|
-
jsonrpc: "2.0",
|
|
32
|
-
id: id ?? null,
|
|
33
|
-
error: data ? { code, message, data } : { code, message },
|
|
34
|
-
}),
|
|
35
|
-
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
const asObj = (v: unknown): Record<string, unknown> =>
|
|
39
|
-
typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : {}
|
|
40
|
-
|
|
41
|
-
const maybeAuthorized = (request: Request, env: Env, config: EchoPdfConfig): boolean => {
|
|
42
|
-
if (!config.mcp.authHeader || !config.mcp.authEnv) return true
|
|
43
|
-
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
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const resolvePublicBaseUrl = (request: Request, configured?: string): string =>
|
|
49
|
-
typeof configured === "string" && configured.length > 0 ? configured : request.url
|
|
50
|
-
|
|
51
|
-
const prepareMcpToolArgs = (toolName: string, args: Record<string, unknown>): Record<string, unknown> => {
|
|
52
|
-
if (toolName === "pdf_extract_pages") {
|
|
53
|
-
const mode = typeof args.returnMode === "string" ? args.returnMode : ""
|
|
54
|
-
if (!mode) {
|
|
55
|
-
return { ...args, returnMode: "url" }
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return args
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const handleMcpRequest = async (
|
|
62
|
-
request: Request,
|
|
63
|
-
env: Env,
|
|
64
|
-
config: EchoPdfConfig,
|
|
65
|
-
fileStore: FileStore
|
|
66
|
-
): Promise<Response> => {
|
|
67
|
-
if (!maybeAuthorized(request, env, config)) {
|
|
68
|
-
return new Response("Unauthorized", { status: 401 })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let body: JsonRpcRequest
|
|
72
|
-
try {
|
|
73
|
-
body = (await request.json()) as JsonRpcRequest
|
|
74
|
-
} catch {
|
|
75
|
-
return err(null, -32700, "Parse error")
|
|
76
|
-
}
|
|
77
|
-
if (typeof body !== "object" || body === null) {
|
|
78
|
-
return err(null, -32600, "Invalid Request")
|
|
79
|
-
}
|
|
80
|
-
if (body.jsonrpc !== "2.0") {
|
|
81
|
-
return err(body.id ?? null, -32600, "Invalid Request: jsonrpc must be '2.0'")
|
|
82
|
-
}
|
|
83
|
-
const method = body.method ?? ""
|
|
84
|
-
const id = body.id ?? null
|
|
85
|
-
if (typeof method !== "string" || method.length === 0) {
|
|
86
|
-
return err(id, -32600, "Invalid Request: method is required")
|
|
87
|
-
}
|
|
88
|
-
const params = asObj(body.params)
|
|
89
|
-
|
|
90
|
-
if (method === "initialize") {
|
|
91
|
-
return ok(id, {
|
|
92
|
-
protocolVersion: "2024-11-05",
|
|
93
|
-
serverInfo: {
|
|
94
|
-
name: config.mcp.serverName,
|
|
95
|
-
version: config.mcp.version,
|
|
96
|
-
},
|
|
97
|
-
capabilities: {
|
|
98
|
-
tools: {},
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (method === "tools/list") {
|
|
104
|
-
return ok(id, { tools: listToolSchemas().map((tool) => ({
|
|
105
|
-
name: tool.name,
|
|
106
|
-
description: tool.description,
|
|
107
|
-
inputSchema: tool.inputSchema,
|
|
108
|
-
})) })
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (method !== "tools/call") {
|
|
112
|
-
return err(id, -32601, `Unsupported method: ${method}`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const toolName = typeof params.name === "string" ? params.name : ""
|
|
116
|
-
const args = prepareMcpToolArgs(toolName, asObj(params.arguments))
|
|
117
|
-
if (!toolName) {
|
|
118
|
-
return err(id, -32602, "Invalid params: name is required", {
|
|
119
|
-
code: "INVALID_PARAMS",
|
|
120
|
-
status: 400,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const result = await callTool(toolName, args, {
|
|
126
|
-
config,
|
|
127
|
-
env,
|
|
128
|
-
fileStore,
|
|
129
|
-
})
|
|
130
|
-
const envelope = buildToolOutputEnvelope(result, resolvePublicBaseUrl(request, config.service.publicBaseUrl))
|
|
131
|
-
return ok(id, { content: buildMcpContent(envelope) })
|
|
132
|
-
} catch (error) {
|
|
133
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
134
|
-
const status = (error as { status?: unknown })?.status
|
|
135
|
-
const stableStatus = typeof status === "number" && Number.isFinite(status) ? status : 500
|
|
136
|
-
const code = (error as { code?: unknown })?.code
|
|
137
|
-
const details = (error as { details?: unknown })?.details
|
|
138
|
-
if (message.startsWith("Unknown tool:")) {
|
|
139
|
-
return err(id, -32601, message, {
|
|
140
|
-
code: typeof code === "string" ? code : "TOOL_NOT_FOUND",
|
|
141
|
-
status: 404,
|
|
142
|
-
details,
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
if (stableStatus >= 400 && stableStatus < 500) {
|
|
146
|
-
return err(id, -32602, message, {
|
|
147
|
-
code: typeof code === "string" ? code : "INVALID_PARAMS",
|
|
148
|
-
status: stableStatus,
|
|
149
|
-
details,
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
return err(id, -32000, message, {
|
|
153
|
-
code: typeof code === "string" ? code : "INTERNAL_ERROR",
|
|
154
|
-
status: stableStatus,
|
|
155
|
-
details,
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
}
|