@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { StoragePolicy } from "./pdf-types.js";
|
|
2
|
+
import type { DurableObjectNamespace, DurableObjectState, StoredFileMeta, StoredFileRecord } from "./types.js";
|
|
3
|
+
interface StoreStats {
|
|
4
|
+
readonly fileCount: number;
|
|
5
|
+
readonly totalBytes: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class FileStoreDO {
|
|
8
|
+
private readonly state;
|
|
9
|
+
constructor(state: DurableObjectState);
|
|
10
|
+
fetch(request: Request): Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
export declare class DurableObjectFileStore {
|
|
13
|
+
private readonly namespace;
|
|
14
|
+
private readonly policy;
|
|
15
|
+
constructor(namespace: DurableObjectNamespace, policy: StoragePolicy);
|
|
16
|
+
private stub;
|
|
17
|
+
put(input: {
|
|
18
|
+
readonly filename: string;
|
|
19
|
+
readonly mimeType: string;
|
|
20
|
+
readonly bytes: Uint8Array;
|
|
21
|
+
}): Promise<StoredFileMeta>;
|
|
22
|
+
get(fileId: string): Promise<StoredFileRecord | null>;
|
|
23
|
+
list(): Promise<ReadonlyArray<StoredFileMeta>>;
|
|
24
|
+
delete(fileId: string): Promise<boolean>;
|
|
25
|
+
stats(): Promise<{
|
|
26
|
+
policy: StoragePolicy;
|
|
27
|
+
stats: StoreStats;
|
|
28
|
+
}>;
|
|
29
|
+
cleanup(): Promise<{
|
|
30
|
+
policy: StoragePolicy;
|
|
31
|
+
deletedExpired: number;
|
|
32
|
+
deletedEvicted: number;
|
|
33
|
+
stats: StoreStats;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from "./file-utils.js";
|
|
2
|
+
const json = (data, status = 200) => new Response(JSON.stringify(data), {
|
|
3
|
+
status,
|
|
4
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
5
|
+
});
|
|
6
|
+
const readJson = async (request) => {
|
|
7
|
+
try {
|
|
8
|
+
const body = await request.json();
|
|
9
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) {
|
|
10
|
+
return body;
|
|
11
|
+
}
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const defaultPolicy = () => ({
|
|
19
|
+
maxFileBytes: 1_200_000,
|
|
20
|
+
maxTotalBytes: 52_428_800,
|
|
21
|
+
ttlHours: 24,
|
|
22
|
+
cleanupBatchSize: 50,
|
|
23
|
+
});
|
|
24
|
+
const parsePolicy = (input) => {
|
|
25
|
+
const raw = typeof input === "object" && input !== null && !Array.isArray(input)
|
|
26
|
+
? input
|
|
27
|
+
: {};
|
|
28
|
+
const fallback = defaultPolicy();
|
|
29
|
+
const maxFileBytes = Number(raw.maxFileBytes ?? fallback.maxFileBytes);
|
|
30
|
+
const maxTotalBytes = Number(raw.maxTotalBytes ?? fallback.maxTotalBytes);
|
|
31
|
+
const ttlHours = Number(raw.ttlHours ?? fallback.ttlHours);
|
|
32
|
+
const cleanupBatchSize = Number(raw.cleanupBatchSize ?? fallback.cleanupBatchSize);
|
|
33
|
+
return {
|
|
34
|
+
maxFileBytes: Number.isFinite(maxFileBytes) && maxFileBytes > 0 ? Math.floor(maxFileBytes) : fallback.maxFileBytes,
|
|
35
|
+
maxTotalBytes: Number.isFinite(maxTotalBytes) && maxTotalBytes > 0 ? Math.floor(maxTotalBytes) : fallback.maxTotalBytes,
|
|
36
|
+
ttlHours: Number.isFinite(ttlHours) && ttlHours > 0 ? ttlHours : fallback.ttlHours,
|
|
37
|
+
cleanupBatchSize: Number.isFinite(cleanupBatchSize) && cleanupBatchSize > 0 ? Math.floor(cleanupBatchSize) : fallback.cleanupBatchSize,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const toMeta = (value) => ({
|
|
41
|
+
id: value.id,
|
|
42
|
+
filename: value.filename,
|
|
43
|
+
mimeType: value.mimeType,
|
|
44
|
+
sizeBytes: value.sizeBytes,
|
|
45
|
+
createdAt: value.createdAt,
|
|
46
|
+
});
|
|
47
|
+
const listStoredValues = async (state) => {
|
|
48
|
+
const listed = await state.storage.list({ prefix: "file:" });
|
|
49
|
+
return [...listed.values()];
|
|
50
|
+
};
|
|
51
|
+
const computeStats = (files) => ({
|
|
52
|
+
fileCount: files.length,
|
|
53
|
+
totalBytes: files.reduce((sum, file) => sum + file.sizeBytes, 0),
|
|
54
|
+
});
|
|
55
|
+
const isExpired = (createdAt, ttlHours) => {
|
|
56
|
+
const createdMs = Date.parse(createdAt);
|
|
57
|
+
if (!Number.isFinite(createdMs))
|
|
58
|
+
return false;
|
|
59
|
+
return Date.now() - createdMs > ttlHours * 60 * 60 * 1000;
|
|
60
|
+
};
|
|
61
|
+
const deleteFiles = async (state, files) => {
|
|
62
|
+
let deleted = 0;
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const ok = await state.storage.delete(`file:${file.id}`);
|
|
65
|
+
if (ok)
|
|
66
|
+
deleted += 1;
|
|
67
|
+
}
|
|
68
|
+
return deleted;
|
|
69
|
+
};
|
|
70
|
+
export class FileStoreDO {
|
|
71
|
+
state;
|
|
72
|
+
constructor(state) {
|
|
73
|
+
this.state = state;
|
|
74
|
+
}
|
|
75
|
+
async fetch(request) {
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
if (request.method === "POST" && url.pathname === "/put") {
|
|
78
|
+
const body = await readJson(request);
|
|
79
|
+
const policy = parsePolicy(body.policy);
|
|
80
|
+
const filename = typeof body.filename === "string" ? body.filename : `file-${Date.now()}`;
|
|
81
|
+
const mimeType = typeof body.mimeType === "string" ? body.mimeType : "application/octet-stream";
|
|
82
|
+
const bytesBase64 = typeof body.bytesBase64 === "string" ? body.bytesBase64 : "";
|
|
83
|
+
const bytes = fromBase64(bytesBase64);
|
|
84
|
+
if (bytes.byteLength > policy.maxFileBytes) {
|
|
85
|
+
return json({
|
|
86
|
+
error: `file too large: ${bytes.byteLength} bytes exceeds maxFileBytes ${policy.maxFileBytes}`,
|
|
87
|
+
code: "FILE_TOO_LARGE",
|
|
88
|
+
policy,
|
|
89
|
+
}, 413);
|
|
90
|
+
}
|
|
91
|
+
let files = await listStoredValues(this.state);
|
|
92
|
+
const expired = files.filter((file) => isExpired(file.createdAt, policy.ttlHours));
|
|
93
|
+
if (expired.length > 0) {
|
|
94
|
+
await deleteFiles(this.state, expired);
|
|
95
|
+
files = await listStoredValues(this.state);
|
|
96
|
+
}
|
|
97
|
+
let stats = computeStats(files);
|
|
98
|
+
const projected = stats.totalBytes + bytes.byteLength;
|
|
99
|
+
if (projected > policy.maxTotalBytes) {
|
|
100
|
+
const needFree = projected - policy.maxTotalBytes;
|
|
101
|
+
const candidates = [...files]
|
|
102
|
+
.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt))
|
|
103
|
+
.slice(0, policy.cleanupBatchSize);
|
|
104
|
+
let freed = 0;
|
|
105
|
+
const evictList = [];
|
|
106
|
+
for (const file of candidates) {
|
|
107
|
+
evictList.push(file);
|
|
108
|
+
freed += file.sizeBytes;
|
|
109
|
+
if (freed >= needFree)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (evictList.length > 0) {
|
|
113
|
+
await deleteFiles(this.state, evictList);
|
|
114
|
+
files = await listStoredValues(this.state);
|
|
115
|
+
stats = computeStats(files);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (stats.totalBytes + bytes.byteLength > policy.maxTotalBytes) {
|
|
119
|
+
return json({
|
|
120
|
+
error: `storage quota exceeded: total ${stats.totalBytes} + incoming ${bytes.byteLength} > maxTotalBytes ${policy.maxTotalBytes}`,
|
|
121
|
+
code: "STORAGE_QUOTA_EXCEEDED",
|
|
122
|
+
policy,
|
|
123
|
+
stats,
|
|
124
|
+
}, 507);
|
|
125
|
+
}
|
|
126
|
+
const id = crypto.randomUUID();
|
|
127
|
+
const value = {
|
|
128
|
+
id,
|
|
129
|
+
filename,
|
|
130
|
+
mimeType,
|
|
131
|
+
sizeBytes: bytes.byteLength,
|
|
132
|
+
createdAt: new Date().toISOString(),
|
|
133
|
+
bytesBase64,
|
|
134
|
+
};
|
|
135
|
+
await this.state.storage.put(`file:${id}`, value);
|
|
136
|
+
return json({ file: toMeta(value), policy });
|
|
137
|
+
}
|
|
138
|
+
if (request.method === "GET" && url.pathname === "/get") {
|
|
139
|
+
const fileId = url.searchParams.get("fileId");
|
|
140
|
+
if (!fileId)
|
|
141
|
+
return json({ error: "Missing fileId" }, 400);
|
|
142
|
+
const value = await this.state.storage.get(`file:${fileId}`);
|
|
143
|
+
if (!value)
|
|
144
|
+
return json({ file: null });
|
|
145
|
+
return json({ file: value });
|
|
146
|
+
}
|
|
147
|
+
if (request.method === "GET" && url.pathname === "/list") {
|
|
148
|
+
const listed = await this.state.storage.list({ prefix: "file:" });
|
|
149
|
+
const files = [...listed.values()].map(toMeta);
|
|
150
|
+
return json({ files });
|
|
151
|
+
}
|
|
152
|
+
if (request.method === "POST" && url.pathname === "/delete") {
|
|
153
|
+
const body = await readJson(request);
|
|
154
|
+
const fileId = typeof body.fileId === "string" ? body.fileId : "";
|
|
155
|
+
if (!fileId)
|
|
156
|
+
return json({ error: "Missing fileId" }, 400);
|
|
157
|
+
const key = `file:${fileId}`;
|
|
158
|
+
const existing = await this.state.storage.get(key);
|
|
159
|
+
if (!existing)
|
|
160
|
+
return json({ deleted: false });
|
|
161
|
+
await this.state.storage.delete(key);
|
|
162
|
+
return json({ deleted: true });
|
|
163
|
+
}
|
|
164
|
+
if (request.method === "GET" && url.pathname === "/stats") {
|
|
165
|
+
let policyInput;
|
|
166
|
+
const encoded = url.searchParams.get("policy");
|
|
167
|
+
if (encoded) {
|
|
168
|
+
try {
|
|
169
|
+
policyInput = JSON.parse(encoded);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
policyInput = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const policy = parsePolicy(policyInput);
|
|
176
|
+
const files = await listStoredValues(this.state);
|
|
177
|
+
const stats = computeStats(files);
|
|
178
|
+
return json({ policy, stats });
|
|
179
|
+
}
|
|
180
|
+
if (request.method === "POST" && url.pathname === "/cleanup") {
|
|
181
|
+
const body = await readJson(request);
|
|
182
|
+
const policy = parsePolicy(body.policy);
|
|
183
|
+
const files = await listStoredValues(this.state);
|
|
184
|
+
const expired = files.filter((file) => isExpired(file.createdAt, policy.ttlHours));
|
|
185
|
+
const deletedExpired = await deleteFiles(this.state, expired);
|
|
186
|
+
const afterExpired = await listStoredValues(this.state);
|
|
187
|
+
let stats = computeStats(afterExpired);
|
|
188
|
+
let deletedEvicted = 0;
|
|
189
|
+
if (stats.totalBytes > policy.maxTotalBytes) {
|
|
190
|
+
const sorted = [...afterExpired].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
191
|
+
const evictList = [];
|
|
192
|
+
for (const file of sorted) {
|
|
193
|
+
evictList.push(file);
|
|
194
|
+
const projected = stats.totalBytes - evictList.reduce((sum, item) => sum + item.sizeBytes, 0);
|
|
195
|
+
if (projected <= policy.maxTotalBytes)
|
|
196
|
+
break;
|
|
197
|
+
if (evictList.length >= policy.cleanupBatchSize)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
deletedEvicted = await deleteFiles(this.state, evictList);
|
|
201
|
+
stats = computeStats(await listStoredValues(this.state));
|
|
202
|
+
}
|
|
203
|
+
return json({
|
|
204
|
+
policy,
|
|
205
|
+
deletedExpired,
|
|
206
|
+
deletedEvicted,
|
|
207
|
+
stats,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return json({ error: "Not found" }, 404);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export class DurableObjectFileStore {
|
|
214
|
+
namespace;
|
|
215
|
+
policy;
|
|
216
|
+
constructor(namespace, policy) {
|
|
217
|
+
this.namespace = namespace;
|
|
218
|
+
this.policy = policy;
|
|
219
|
+
}
|
|
220
|
+
stub() {
|
|
221
|
+
return this.namespace.get(this.namespace.idFromName("echo-pdf-file-store"));
|
|
222
|
+
}
|
|
223
|
+
async put(input) {
|
|
224
|
+
const response = await this.stub().fetch("https://do/put", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
filename: input.filename,
|
|
229
|
+
mimeType: input.mimeType,
|
|
230
|
+
bytesBase64: toBase64(input.bytes),
|
|
231
|
+
policy: this.policy,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
const payload = (await response.json());
|
|
235
|
+
if (!response.ok || !payload.file) {
|
|
236
|
+
const details = payload;
|
|
237
|
+
const error = new Error(payload.error ?? "DO put failed");
|
|
238
|
+
error.status = response.status;
|
|
239
|
+
error.code = typeof details.code === "string" ? details.code : undefined;
|
|
240
|
+
error.details = { policy: details.policy, stats: details.stats };
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
return payload.file;
|
|
244
|
+
}
|
|
245
|
+
async get(fileId) {
|
|
246
|
+
const response = await this.stub().fetch(`https://do/get?fileId=${encodeURIComponent(fileId)}`);
|
|
247
|
+
const payload = (await response.json());
|
|
248
|
+
if (!response.ok)
|
|
249
|
+
throw new Error("DO get failed");
|
|
250
|
+
if (!payload.file)
|
|
251
|
+
return null;
|
|
252
|
+
return {
|
|
253
|
+
id: payload.file.id,
|
|
254
|
+
filename: payload.file.filename,
|
|
255
|
+
mimeType: payload.file.mimeType,
|
|
256
|
+
sizeBytes: payload.file.sizeBytes,
|
|
257
|
+
createdAt: payload.file.createdAt,
|
|
258
|
+
bytes: fromBase64(payload.file.bytesBase64),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async list() {
|
|
262
|
+
const response = await this.stub().fetch("https://do/list");
|
|
263
|
+
const payload = (await response.json());
|
|
264
|
+
if (!response.ok)
|
|
265
|
+
throw new Error("DO list failed");
|
|
266
|
+
return payload.files ?? [];
|
|
267
|
+
}
|
|
268
|
+
async delete(fileId) {
|
|
269
|
+
const response = await this.stub().fetch("https://do/delete", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify({ fileId }),
|
|
273
|
+
});
|
|
274
|
+
const payload = (await response.json());
|
|
275
|
+
if (!response.ok)
|
|
276
|
+
throw new Error("DO delete failed");
|
|
277
|
+
return payload.deleted === true;
|
|
278
|
+
}
|
|
279
|
+
async stats() {
|
|
280
|
+
const policyEncoded = encodeURIComponent(JSON.stringify(this.policy));
|
|
281
|
+
const response = await this.stub().fetch(`https://do/stats?policy=${policyEncoded}`);
|
|
282
|
+
const payload = (await response.json());
|
|
283
|
+
if (!response.ok)
|
|
284
|
+
throw new Error("DO stats failed");
|
|
285
|
+
return payload;
|
|
286
|
+
}
|
|
287
|
+
async cleanup() {
|
|
288
|
+
const response = await this.stub().fetch("https://do/cleanup", {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: { "Content-Type": "application/json" },
|
|
291
|
+
body: JSON.stringify({ policy: this.policy }),
|
|
292
|
+
});
|
|
293
|
+
const payload = (await response.json());
|
|
294
|
+
if (!response.ok)
|
|
295
|
+
throw new Error("DO cleanup failed");
|
|
296
|
+
return payload;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReturnMode, StoredFileRecord } from "./types.js";
|
|
2
|
+
export declare const fromBase64: (value: string) => Uint8Array;
|
|
3
|
+
export declare const toBase64: (bytes: Uint8Array) => string;
|
|
4
|
+
export declare const toDataUrl: (bytes: Uint8Array, mimeType: string) => string;
|
|
5
|
+
export declare const normalizeReturnMode: (value: unknown) => ReturnMode;
|
|
6
|
+
export declare const toInlineFilePayload: (file: StoredFileRecord, includeBase64: boolean) => Record<string, unknown>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const fromBase64 = (value) => {
|
|
2
|
+
const raw = atob(value.replace(/^data:.*;base64,/, ""));
|
|
3
|
+
const out = new Uint8Array(raw.length);
|
|
4
|
+
for (let i = 0; i < raw.length; i++) {
|
|
5
|
+
out[i] = raw.charCodeAt(i);
|
|
6
|
+
}
|
|
7
|
+
return out;
|
|
8
|
+
};
|
|
9
|
+
export const toBase64 = (bytes) => {
|
|
10
|
+
let binary = "";
|
|
11
|
+
const chunkSize = 0x8000;
|
|
12
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
13
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
14
|
+
binary += String.fromCharCode(...chunk);
|
|
15
|
+
}
|
|
16
|
+
return btoa(binary);
|
|
17
|
+
};
|
|
18
|
+
export const toDataUrl = (bytes, mimeType) => `data:${mimeType};base64,${toBase64(bytes)}`;
|
|
19
|
+
export const normalizeReturnMode = (value) => {
|
|
20
|
+
if (value === "file_id" || value === "url" || value === "inline") {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return "inline";
|
|
24
|
+
};
|
|
25
|
+
export const toInlineFilePayload = (file, includeBase64) => ({
|
|
26
|
+
file: {
|
|
27
|
+
id: file.id,
|
|
28
|
+
filename: file.filename,
|
|
29
|
+
mimeType: file.mimeType,
|
|
30
|
+
sizeBytes: file.sizeBytes,
|
|
31
|
+
createdAt: file.createdAt,
|
|
32
|
+
},
|
|
33
|
+
dataUrl: file.mimeType.startsWith("image/") ? toDataUrl(file.bytes, file.mimeType) : undefined,
|
|
34
|
+
base64: includeBase64 ? toBase64(file.bytes) : undefined,
|
|
35
|
+
text: file.mimeType.startsWith("text/") ? new TextDecoder().decode(file.bytes) : undefined,
|
|
36
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class HttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
constructor(status: number, code: string, message: string, details?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare const badRequest: (code: string, message: string, details?: unknown) => HttpError;
|
|
8
|
+
export declare const notFound: (code: string, message: string, details?: unknown) => HttpError;
|
|
9
|
+
export declare const unprocessable: (code: string, message: string, details?: unknown) => HttpError;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(status, code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const badRequest = (code, message, details) => new HttpError(400, code, message, details);
|
|
13
|
+
export const notFound = (code, message, details) => new HttpError(404, code, message, details);
|
|
14
|
+
export const unprocessable = (code, message, details) => new HttpError(422, code, message, details);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core/index.js";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { checkHeaderAuth } from "./auth.js";
|
|
2
|
+
import { buildMcpContent, buildToolOutputEnvelope } from "./response-schema.js";
|
|
3
|
+
import { callTool, listToolSchemas } from "./tool-registry.js";
|
|
4
|
+
const ok = (id, result) => new Response(JSON.stringify({
|
|
5
|
+
jsonrpc: "2.0",
|
|
6
|
+
id: id ?? null,
|
|
7
|
+
result,
|
|
8
|
+
}), { headers: { "Content-Type": "application/json" } });
|
|
9
|
+
const err = (id, code, message, data) => new Response(JSON.stringify({
|
|
10
|
+
jsonrpc: "2.0",
|
|
11
|
+
id: id ?? null,
|
|
12
|
+
error: data ? { code, message, data } : { code, message },
|
|
13
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
14
|
+
const asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
15
|
+
const resolvePublicBaseUrl = (request, configured) => typeof configured === "string" && configured.length > 0 ? configured : request.url;
|
|
16
|
+
const prepareMcpToolArgs = (toolName, args) => {
|
|
17
|
+
if (toolName === "pdf_extract_pages") {
|
|
18
|
+
const mode = typeof args.returnMode === "string" ? args.returnMode : "";
|
|
19
|
+
if (!mode) {
|
|
20
|
+
return { ...args, returnMode: "url" };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return args;
|
|
24
|
+
};
|
|
25
|
+
export const handleMcpRequest = async (request, env, config, fileStore) => {
|
|
26
|
+
const auth = checkHeaderAuth(request, env, {
|
|
27
|
+
authHeader: config.mcp.authHeader,
|
|
28
|
+
authEnv: config.mcp.authEnv,
|
|
29
|
+
allowMissingSecret: env.ECHO_PDF_ALLOW_MISSING_AUTH_SECRET === "1",
|
|
30
|
+
misconfiguredCode: "AUTH_MISCONFIGURED",
|
|
31
|
+
unauthorizedCode: "UNAUTHORIZED",
|
|
32
|
+
contextName: "MCP",
|
|
33
|
+
});
|
|
34
|
+
if (!auth.ok) {
|
|
35
|
+
return new Response(JSON.stringify({ error: auth.message, code: auth.code }), {
|
|
36
|
+
status: auth.status,
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
let body;
|
|
41
|
+
try {
|
|
42
|
+
body = (await request.json());
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return err(null, -32700, "Parse error");
|
|
46
|
+
}
|
|
47
|
+
if (typeof body !== "object" || body === null) {
|
|
48
|
+
return err(null, -32600, "Invalid Request");
|
|
49
|
+
}
|
|
50
|
+
if (body.jsonrpc !== "2.0") {
|
|
51
|
+
return err(body.id ?? null, -32600, "Invalid Request: jsonrpc must be '2.0'");
|
|
52
|
+
}
|
|
53
|
+
const method = body.method ?? "";
|
|
54
|
+
const id = body.id ?? null;
|
|
55
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
56
|
+
return err(id, -32600, "Invalid Request: method is required");
|
|
57
|
+
}
|
|
58
|
+
if (method.startsWith("notifications/")) {
|
|
59
|
+
return new Response(null, { status: 204 });
|
|
60
|
+
}
|
|
61
|
+
const params = asObj(body.params);
|
|
62
|
+
if (method === "initialize") {
|
|
63
|
+
return ok(id, {
|
|
64
|
+
protocolVersion: "2024-11-05",
|
|
65
|
+
serverInfo: {
|
|
66
|
+
name: config.mcp.serverName,
|
|
67
|
+
version: config.mcp.version,
|
|
68
|
+
},
|
|
69
|
+
capabilities: {
|
|
70
|
+
tools: {},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (method === "tools/list") {
|
|
75
|
+
return ok(id, { tools: listToolSchemas().map((tool) => ({
|
|
76
|
+
name: tool.name,
|
|
77
|
+
description: tool.description,
|
|
78
|
+
inputSchema: tool.inputSchema,
|
|
79
|
+
})) });
|
|
80
|
+
}
|
|
81
|
+
if (method !== "tools/call") {
|
|
82
|
+
return err(id, -32601, `Unsupported method: ${method}`);
|
|
83
|
+
}
|
|
84
|
+
const toolName = typeof params.name === "string" ? params.name : "";
|
|
85
|
+
const args = prepareMcpToolArgs(toolName, asObj(params.arguments));
|
|
86
|
+
if (!toolName) {
|
|
87
|
+
return err(id, -32602, "Invalid params: name is required", {
|
|
88
|
+
code: "INVALID_PARAMS",
|
|
89
|
+
status: 400,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const result = await callTool(toolName, args, {
|
|
94
|
+
config,
|
|
95
|
+
env,
|
|
96
|
+
fileStore,
|
|
97
|
+
});
|
|
98
|
+
const envelope = buildToolOutputEnvelope(result, resolvePublicBaseUrl(request, config.service.publicBaseUrl));
|
|
99
|
+
return ok(id, { content: buildMcpContent(envelope) });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
const status = error?.status;
|
|
104
|
+
const stableStatus = typeof status === "number" && Number.isFinite(status) ? status : 500;
|
|
105
|
+
const code = error?.code;
|
|
106
|
+
const details = error?.details;
|
|
107
|
+
if (message.startsWith("Unknown tool:")) {
|
|
108
|
+
return err(id, -32601, message, {
|
|
109
|
+
code: typeof code === "string" ? code : "TOOL_NOT_FOUND",
|
|
110
|
+
status: 404,
|
|
111
|
+
details,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (stableStatus >= 400 && stableStatus < 500) {
|
|
115
|
+
return err(id, -32602, message, {
|
|
116
|
+
code: typeof code === "string" ? code : "INVALID_PARAMS",
|
|
117
|
+
status: stableStatus,
|
|
118
|
+
details,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return err(id, -32000, message, {
|
|
122
|
+
code: typeof code === "string" ? code : "INTERNAL_ERROR",
|
|
123
|
+
status: stableStatus,
|
|
124
|
+
details,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Env, FileStore } from "./types.js";
|
|
2
|
+
import type { AgentTraceEvent, EchoPdfConfig, PdfOperationRequest } from "./pdf-types.js";
|
|
3
|
+
interface RuntimeOptions {
|
|
4
|
+
readonly trace?: (event: AgentTraceEvent) => void;
|
|
5
|
+
readonly fileStore: FileStore;
|
|
6
|
+
}
|
|
7
|
+
export declare const ingestPdfFromPayload: (config: EchoPdfConfig, input: {
|
|
8
|
+
readonly fileId?: string;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly base64?: string;
|
|
11
|
+
readonly filename?: string;
|
|
12
|
+
}, opts: RuntimeOptions) => Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
bytes: Uint8Array;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const runPdfAgent: (config: EchoPdfConfig, env: Env, request: PdfOperationRequest, opts: RuntimeOptions) => Promise<unknown>;
|
|
18
|
+
export {};
|