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