@omercnet/paseo-omp 0.3.0-next.95.1 → 0.3.0-next.97.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/provider/host-tools.ts +223 -26
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ProviderMcpServerConfig,
|
|
4
4
|
ProviderSessionConfig,
|
|
5
5
|
} from "@getpaseo/plugin/server/provider";
|
|
6
|
+
import { isValidImagePayload } from "./image";
|
|
6
7
|
import {
|
|
7
8
|
type ConnectedMcpClient,
|
|
8
9
|
type ConnectedMcpTool,
|
|
@@ -37,7 +38,13 @@ const MAX_HOST_TOOL_DESCRIPTION_BYTES = 64 * 1024;
|
|
|
37
38
|
const MAX_HOST_TOOL_SCHEMA_BYTES = 256 * 1024;
|
|
38
39
|
const MAX_HOST_TOOL_CATALOG_BYTES = 768 * 1024;
|
|
39
40
|
const MAX_HOST_TOOL_RESULT_BYTES = 12 * 1024 * 1024;
|
|
41
|
+
const MAX_HOST_TOOL_CONTENT_BLOCKS = 512;
|
|
42
|
+
const MAX_HOST_TOOL_TEXT_BYTES = 1024 * 1024;
|
|
43
|
+
const MAX_HOST_TOOL_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
|
|
40
44
|
const MAX_STRUCTURED_CONTENT_FALLBACK_BYTES = 1024 * 1024;
|
|
45
|
+
const CONTENT_TRUNCATED_NOTICE = "[MCP result content truncated: exceeded size limits]";
|
|
46
|
+
const DETAILS_OMITTED_NOTICE = "[MCP structured content omitted: exceeded size limits]";
|
|
47
|
+
const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
|
|
41
48
|
const MAX_PENDING_HOST_TOOL_CALLS = 64;
|
|
42
49
|
const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
|
|
43
50
|
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
|
|
@@ -272,42 +279,171 @@ async function discoverMcpTools(
|
|
|
272
279
|
throw new OmpPublicError("MCP server tool-list pagination exceeds the supported limit");
|
|
273
280
|
}
|
|
274
281
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
type HostToolContentBlock = OmpHostToolResult["result"]["content"][number];
|
|
283
|
+
|
|
284
|
+
function hostToolContentIsBounded(content: readonly HostToolContentBlock[]): boolean {
|
|
285
|
+
return (
|
|
286
|
+
boundedJsonBytes(
|
|
287
|
+
content,
|
|
288
|
+
MAX_HOST_TOOL_RESULT_BYTES,
|
|
289
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS,
|
|
290
|
+
MAX_HOST_TOOL_IMAGE_DATA_BYTES,
|
|
291
|
+
4_096,
|
|
292
|
+
) !== Number.POSITIVE_INFINITY
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function noticeBlock(text: string): HostToolContentBlock {
|
|
297
|
+
return { type: "text", text };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function appendBoundedNotice(
|
|
301
|
+
content: readonly HostToolContentBlock[],
|
|
302
|
+
notice: string,
|
|
303
|
+
): HostToolContentBlock[] {
|
|
304
|
+
const trailingNotice = content.at(-1)?.text;
|
|
305
|
+
const contentWasTruncated =
|
|
306
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
307
|
+
const prefix = (contentWasTruncated ? content.slice(0, -1) : content).slice(
|
|
308
|
+
0,
|
|
309
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS - 1,
|
|
310
|
+
);
|
|
311
|
+
const markerText =
|
|
312
|
+
notice === DETAILS_OMITTED_NOTICE && contentWasTruncated ? RESULT_TRUNCATED_NOTICE : notice;
|
|
313
|
+
const marker = noticeBlock(markerText);
|
|
314
|
+
while (prefix.length > 0 && !hostToolContentIsBounded([...prefix, marker])) prefix.pop();
|
|
315
|
+
return [...prefix, marker];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function boundedTextPrefix(
|
|
319
|
+
prefix: readonly HostToolContentBlock[],
|
|
320
|
+
block: HostToolContentBlock,
|
|
321
|
+
): HostToolContentBlock | undefined {
|
|
322
|
+
if (block.type !== "text" || typeof block.text !== "string") return;
|
|
323
|
+
const marker = noticeBlock(CONTENT_TRUNCATED_NOTICE);
|
|
324
|
+
let low = 0;
|
|
325
|
+
let high = block.text.length;
|
|
326
|
+
let best = "";
|
|
327
|
+
while (low <= high) {
|
|
328
|
+
const middle = Math.floor((low + high) / 2);
|
|
329
|
+
let text = block.text.slice(0, middle);
|
|
330
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
331
|
+
const candidate = { type: "text", text } satisfies HostToolContentBlock;
|
|
332
|
+
if (hostToolContentIsBounded([...prefix, candidate, marker])) {
|
|
333
|
+
best = text;
|
|
334
|
+
low = middle + 1;
|
|
335
|
+
} else {
|
|
336
|
+
high = middle - 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return best ? { type: "text", text: best } : undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeContent(content: readonly unknown[]): HostToolContentBlock[] {
|
|
343
|
+
const normalized: HostToolContentBlock[] = [];
|
|
344
|
+
for (const value of content) {
|
|
345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
346
|
+
throw new Error("MCP tool returned invalid content");
|
|
347
|
+
}
|
|
348
|
+
const block = value as Record<string, unknown>;
|
|
349
|
+
if (typeof block.type !== "string" || !block.type || utf8Bytes(block.type) > 256) {
|
|
350
|
+
throw new Error("MCP tool returned invalid content");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let candidate: HostToolContentBlock;
|
|
354
|
+
let blockWasTruncated = false;
|
|
355
|
+
if (block.type === "text") {
|
|
356
|
+
if (typeof block.text !== "string") throw new Error("MCP tool returned invalid text content");
|
|
357
|
+
const text = truncateUtf8(block.text, MAX_HOST_TOOL_TEXT_BYTES);
|
|
358
|
+
candidate = { ...block, type: "text", text } as HostToolContentBlock;
|
|
359
|
+
blockWasTruncated = text !== block.text;
|
|
360
|
+
} else if (block.type === "image") {
|
|
361
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") {
|
|
362
|
+
throw new Error("MCP tool returned invalid image content");
|
|
363
|
+
}
|
|
364
|
+
if (block.data.length > MAX_HOST_TOOL_IMAGE_DATA_BYTES) {
|
|
365
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
366
|
+
}
|
|
367
|
+
if (!isValidImagePayload(block.data, block.mimeType, MAX_HOST_TOOL_IMAGE_DATA_BYTES)) {
|
|
368
|
+
throw new Error("MCP tool returned invalid image content");
|
|
369
|
+
}
|
|
370
|
+
candidate = {
|
|
371
|
+
...block,
|
|
372
|
+
type: "image",
|
|
373
|
+
data: block.data,
|
|
374
|
+
mimeType: block.mimeType,
|
|
375
|
+
} as HostToolContentBlock;
|
|
376
|
+
} else {
|
|
377
|
+
candidate = block as HostToolContentBlock;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (normalized.length >= MAX_HOST_TOOL_CONTENT_BLOCKS) {
|
|
381
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
382
|
+
}
|
|
383
|
+
if (!hostToolContentIsBounded([...normalized, candidate])) {
|
|
384
|
+
const prefix = boundedTextPrefix(normalized, candidate);
|
|
385
|
+
return appendBoundedNotice(
|
|
386
|
+
prefix ? [...normalized, prefix] : normalized,
|
|
387
|
+
CONTENT_TRUNCATED_NOTICE,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
normalized.push(candidate);
|
|
391
|
+
if (blockWasTruncated) return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
392
|
+
}
|
|
393
|
+
return normalized;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function structuredContentIsBounded(value: unknown): boolean {
|
|
397
|
+
return (
|
|
279
398
|
boundedJsonBytes(
|
|
280
|
-
|
|
399
|
+
value,
|
|
281
400
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
282
401
|
1_024,
|
|
283
402
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
284
403
|
4_096,
|
|
285
|
-
)
|
|
286
|
-
)
|
|
287
|
-
|
|
404
|
+
) !== Number.POSITIVE_INFINITY
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeResult(result: unknown): OmpHostToolResult["result"] {
|
|
409
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
410
|
+
throw new Error("MCP tool returned an invalid result");
|
|
288
411
|
}
|
|
289
412
|
const record = result as Record<string, unknown>;
|
|
290
413
|
if (Array.isArray(record.content)) {
|
|
291
414
|
const details = record.structuredContent;
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
415
|
+
const detailsAreBounded = details === undefined || structuredContentIsBounded(details);
|
|
416
|
+
let content = normalizeContent(record.content);
|
|
417
|
+
if (content.length === 0 && details !== undefined && detailsAreBounded) {
|
|
418
|
+
content = [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: truncateUtf8(JSON.stringify(details), MAX_STRUCTURED_CONTENT_FALLBACK_BYTES),
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
if (!detailsAreBounded) content = appendBoundedNotice(content, DETAILS_OMITTED_NOTICE);
|
|
426
|
+
|
|
427
|
+
let normalized = parseOmpHostToolAgentResult({
|
|
302
428
|
content,
|
|
303
|
-
...(details !== undefined ? { details } : {}),
|
|
429
|
+
...(details !== undefined && detailsAreBounded ? { details } : {}),
|
|
304
430
|
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
305
431
|
});
|
|
432
|
+
if (!structuredContentIsBounded(normalized)) {
|
|
433
|
+
normalized = parseOmpHostToolAgentResult({
|
|
434
|
+
content: appendBoundedNotice(content, DETAILS_OMITTED_NOTICE),
|
|
435
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return normalized;
|
|
306
439
|
}
|
|
307
440
|
if (Object.hasOwn(record, "toolResult")) {
|
|
441
|
+
const details = record.toolResult;
|
|
442
|
+
const detailsAreBounded = structuredContentIsBounded(details);
|
|
308
443
|
return parseOmpHostToolAgentResult({
|
|
309
|
-
content: [
|
|
310
|
-
details:
|
|
444
|
+
content: [noticeBlock(detailsAreBounded ? "MCP tool completed" : DETAILS_OMITTED_NOTICE)],
|
|
445
|
+
...(detailsAreBounded ? { details } : {}),
|
|
446
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
311
447
|
});
|
|
312
448
|
}
|
|
313
449
|
throw new Error("MCP tool returned an unsupported result");
|
|
@@ -704,12 +840,73 @@ export class OmpHostToolsBridge {
|
|
|
704
840
|
result: OmpHostToolResult,
|
|
705
841
|
): OmpHostToolResult {
|
|
706
842
|
const limit = runtime.maxHostToolFrameBytes ?? 1024 * 1024;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
843
|
+
const fits = (candidate: OmpHostToolResult) => {
|
|
844
|
+
try {
|
|
845
|
+
return Buffer.byteLength(`${JSON.stringify(candidate)}\n`) <= limit;
|
|
846
|
+
} catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
if (fits(result)) return result;
|
|
851
|
+
|
|
852
|
+
const withoutDetails: OmpHostToolResult = {
|
|
853
|
+
...result,
|
|
854
|
+
result: {
|
|
855
|
+
content: appendBoundedNotice(result.result.content, DETAILS_OMITTED_NOTICE),
|
|
856
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
if (result.result.details !== undefined && fits(withoutDetails)) return withoutDetails;
|
|
860
|
+
|
|
861
|
+
const trailingNotice = result.result.content.at(-1)?.text;
|
|
862
|
+
const detailsWereOmitted =
|
|
863
|
+
trailingNotice === DETAILS_OMITTED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
864
|
+
const contentWasTruncated =
|
|
865
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
866
|
+
const marker = noticeBlock(
|
|
867
|
+
result.result.details !== undefined || detailsWereOmitted
|
|
868
|
+
? RESULT_TRUNCATED_NOTICE
|
|
869
|
+
: CONTENT_TRUNCATED_NOTICE,
|
|
870
|
+
);
|
|
871
|
+
const sourceContent =
|
|
872
|
+
detailsWereOmitted || contentWasTruncated
|
|
873
|
+
? result.result.content.slice(0, -1)
|
|
874
|
+
: result.result.content;
|
|
875
|
+
const boundedContent: HostToolContentBlock[] = [];
|
|
876
|
+
const terminalWith = (content: HostToolContentBlock[]): OmpHostToolResult => ({
|
|
877
|
+
...result,
|
|
878
|
+
result: {
|
|
879
|
+
content,
|
|
880
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
for (const block of sourceContent) {
|
|
884
|
+
const candidate = terminalWith([...boundedContent, block, marker]);
|
|
885
|
+
if (fits(candidate)) {
|
|
886
|
+
boundedContent.push(block);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
890
|
+
let low = 0;
|
|
891
|
+
let high = block.text.length;
|
|
892
|
+
let best = "";
|
|
893
|
+
while (low <= high) {
|
|
894
|
+
const middle = Math.floor((low + high) / 2);
|
|
895
|
+
let text = block.text.slice(0, middle);
|
|
896
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
897
|
+
if (fits(terminalWith([...boundedContent, { type: "text", text }, marker]))) {
|
|
898
|
+
best = text;
|
|
899
|
+
low = middle + 1;
|
|
900
|
+
} else {
|
|
901
|
+
high = middle - 1;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (best) boundedContent.push({ type: "text", text: best });
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
711
907
|
}
|
|
712
|
-
|
|
908
|
+
const bounded = terminalWith([...boundedContent, marker]);
|
|
909
|
+
return fits(bounded) ? bounded : errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
|
|
713
910
|
}
|
|
714
911
|
|
|
715
912
|
private sendTerminal(
|