@omercnet/paseo-omp 2.0.0-next.248.1 → 2.0.0-next.251.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
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const OMP_OPERATIONAL_FAILURES = [
|
|
2
2
|
{ category: "session-open", stage: "startup" },
|
|
3
3
|
{ category: "session-open", stage: "catalog" },
|
|
4
|
+
{ category: "session-open", stage: "optional-mcp" },
|
|
4
5
|
{ category: "replay-recovery", stage: "persisted-replay" },
|
|
5
6
|
{ category: "replay-recovery", stage: "runtime-recovery" },
|
|
6
7
|
{ category: "replay-recovery", stage: "rewind" },
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "2.0.0-next.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "2.0.0-next.251.1"; // x-release-please-version
|
|
@@ -58,6 +58,10 @@ const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
|
|
|
58
58
|
const MAX_PENDING_HOST_TOOL_CALLS = 64;
|
|
59
59
|
const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
|
|
60
60
|
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
|
|
61
|
+
// Cold npx-style servers can take several seconds; 10 s leaves headroom under the 20 s ceiling.
|
|
62
|
+
const OPTIONAL_MCP_SERVER_TIMEOUT_MS = 10_000;
|
|
63
|
+
const MAX_DEGRADED_NOTICE_NAMES = 5;
|
|
64
|
+
const MAX_DEGRADED_NAME_BYTES = 64;
|
|
61
65
|
const DEFAULT_MCP_CALL_LIFETIME_MS = 5 * 60 * 1000;
|
|
62
66
|
const MAX_DIRECT_BROWSER_CALLS = 4;
|
|
63
67
|
const BROWSER_CALL_TIMEOUT_MS = 20_000;
|
|
@@ -85,6 +89,8 @@ const DEFAULT_CALL_SCHEDULER: OmpHostToolScheduler = {
|
|
|
85
89
|
|
|
86
90
|
export interface OmpHostToolsOpenOptions {
|
|
87
91
|
connectMcp?: OmpMcpConnector;
|
|
92
|
+
/** Per-server cap for non-Paseo servers. Defaults to 10 s. */
|
|
93
|
+
optionalServerTimeoutMs?: number;
|
|
88
94
|
signal?: AbortSignal;
|
|
89
95
|
initializationTimeoutMs?: number;
|
|
90
96
|
callTimeoutMs?: number;
|
|
@@ -92,6 +98,33 @@ export interface OmpHostToolsOpenOptions {
|
|
|
92
98
|
reportOperationalFailure?: OmpOperationalFailureReporter;
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
export type OmpDegradedMcpServer = {
|
|
102
|
+
name: string;
|
|
103
|
+
reason: "unavailable" | "timed-out" | "invalid-tools";
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const DEGRADED_REASON_TEXT: Record<OmpDegradedMcpServer["reason"], string> = {
|
|
107
|
+
unavailable: "unavailable",
|
|
108
|
+
"timed-out": "timed out",
|
|
109
|
+
"invalid-tools": "invalid tool list",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Fixed reason vocabulary and bounded names only: never raw error text. */
|
|
113
|
+
export function describeDegradedMcpServers(degraded: readonly OmpDegradedMcpServer[]): string {
|
|
114
|
+
const shown = degraded.slice(0, MAX_DEGRADED_NOTICE_NAMES).map((server) => {
|
|
115
|
+
const name = boundedText(
|
|
116
|
+
server.name.replace(/[^\p{L}\p{N}_.:@/ -]/gu, ""),
|
|
117
|
+
MAX_DEGRADED_NAME_BYTES,
|
|
118
|
+
"server",
|
|
119
|
+
);
|
|
120
|
+
return `${name} (${DEGRADED_REASON_TEXT[server.reason]})`;
|
|
121
|
+
});
|
|
122
|
+
const more = degraded.length - shown.length;
|
|
123
|
+
const list = more > 0 ? `${shown.join(", ")}, and ${more} more` : shown.join(", ");
|
|
124
|
+
const noun = degraded.length === 1 ? "server was" : "servers were";
|
|
125
|
+
return `${degraded.length} MCP ${noun} skipped: ${list}. Their tools are missing from this session. Reopen the agent after fixing them.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
type ClassifiedServer = {
|
|
96
129
|
name: string;
|
|
97
130
|
config: ProviderMcpServerConfig;
|
|
@@ -508,6 +541,7 @@ export class OmpHostToolsBridge {
|
|
|
508
541
|
private readonly callTimeoutMs: number,
|
|
509
542
|
private readonly callScheduler: OmpHostToolScheduler,
|
|
510
543
|
private readonly reportOperationalFailure: OmpOperationalFailureReporter,
|
|
544
|
+
readonly degraded: readonly OmpDegradedMcpServer[],
|
|
511
545
|
) {
|
|
512
546
|
this.labels = new Map(definitions.map(({ name, label }) => [name, label ?? name]));
|
|
513
547
|
}
|
|
@@ -535,89 +569,143 @@ export class OmpHostToolsBridge {
|
|
|
535
569
|
const connections: OmpMcpConnection[] = [];
|
|
536
570
|
const definitions: OmpHostToolDefinition[] = [];
|
|
537
571
|
const targets = new Map<string, ToolTarget>();
|
|
572
|
+
const degraded: OmpDegradedMcpServer[] = [];
|
|
573
|
+
const report = (failure: OmpOperationalFailure) => {
|
|
574
|
+
try {
|
|
575
|
+
options.reportOperationalFailure?.(failure);
|
|
576
|
+
} catch {
|
|
577
|
+
// Diagnostics must never affect session open.
|
|
578
|
+
}
|
|
579
|
+
};
|
|
538
580
|
try {
|
|
539
581
|
for (const server of servers) {
|
|
540
582
|
initialization.signal.throwIfAborted();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
583
|
+
// Only Paseo-owned servers are required. Any other server that fails is skipped whole.
|
|
584
|
+
const optional = !server.internal;
|
|
585
|
+
const timeout = optional ? new AbortController() : undefined;
|
|
586
|
+
const timer = timeout
|
|
587
|
+
? setTimeout(
|
|
588
|
+
() => timeout.abort(),
|
|
589
|
+
options.optionalServerTimeoutMs ?? OPTIONAL_MCP_SERVER_TIMEOUT_MS,
|
|
590
|
+
)
|
|
591
|
+
: undefined;
|
|
592
|
+
const signal = timeout
|
|
593
|
+
? AbortSignal.any([initialization.signal, timeout.signal])
|
|
594
|
+
: initialization.signal;
|
|
595
|
+
const connectionCount = connections.length;
|
|
596
|
+
const definitionCount = definitions.length;
|
|
597
|
+
const added: string[] = [];
|
|
598
|
+
let reason: OmpDegradedMcpServer["reason"] = "unavailable";
|
|
548
599
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
600
|
+
const connecting = connectMcp(server.name, server.config, config.cwd, signal);
|
|
601
|
+
let connection: OmpMcpConnection;
|
|
602
|
+
try {
|
|
603
|
+
connection =
|
|
604
|
+
connectMcp === connectMcpServer
|
|
605
|
+
? await connecting
|
|
606
|
+
: await abortable(connecting, signal);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (connectMcp !== connectMcpServer && signal.aborted) {
|
|
609
|
+
const cleanup = connecting.then(
|
|
610
|
+
(lateConnection) => lateConnection.close(),
|
|
611
|
+
(rejection) =>
|
|
612
|
+
rejection instanceof OmpCleanupFailure ? rejection.cleanup : undefined,
|
|
613
|
+
);
|
|
614
|
+
throw new OmpCleanupFailure(
|
|
615
|
+
"OMP MCP connection initialization was interrupted",
|
|
616
|
+
cleanup,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
connections.push(connection);
|
|
622
|
+
const tools = (await discoverMcpTools(connection, signal)).sort((left, right) =>
|
|
623
|
+
left.name.localeCompare(right.name),
|
|
624
|
+
);
|
|
625
|
+
reason = "invalid-tools";
|
|
626
|
+
const namespace = namespaces.get(server.name);
|
|
627
|
+
if (!namespace) throw new Error("MCP server namespace is unavailable");
|
|
628
|
+
for (const tool of tools) {
|
|
629
|
+
if (
|
|
630
|
+
!tool.name ||
|
|
631
|
+
utf8Bytes(tool.name) > MAX_HOST_TOOL_NAME_BYTES ||
|
|
632
|
+
utf8Bytes(tool.description ?? "") > MAX_HOST_TOOL_DESCRIPTION_BYTES ||
|
|
633
|
+
!tool.inputSchema ||
|
|
634
|
+
typeof tool.inputSchema !== "object" ||
|
|
635
|
+
Array.isArray(tool.inputSchema) ||
|
|
636
|
+
boundedJsonBytes(tool.inputSchema, MAX_HOST_TOOL_SCHEMA_BYTES) ===
|
|
637
|
+
Number.POSITIVE_INFINITY
|
|
638
|
+
) {
|
|
639
|
+
throw new Error("MCP server exposed an invalid host tool definition");
|
|
640
|
+
}
|
|
641
|
+
if (definitions.length >= MAX_HOST_TOOLS) {
|
|
642
|
+
throw new Error("MCP servers exposed too many host tools");
|
|
643
|
+
}
|
|
644
|
+
let name = exposedToolName(server, namespace, tool.name);
|
|
645
|
+
if (targets.has(name)) {
|
|
646
|
+
const suffix = digest(`${server.name}\0${tool.name}`);
|
|
647
|
+
name = `${name.slice(0, MAX_HOST_TOOL_NAME_BYTES - suffix.length - 1)}_${suffix}`;
|
|
648
|
+
}
|
|
649
|
+
if (targets.has(name)) throw new Error("MCP host tool names collide");
|
|
650
|
+
const normalizedServerName = safeName(server.name, "server");
|
|
651
|
+
const normalizedToolName = safeName(tool.name, "tool");
|
|
652
|
+
const friendlyToolName = normalizedToolName.startsWith(`${normalizedServerName}_`)
|
|
653
|
+
? normalizedToolName.slice(normalizedServerName.length + 1)
|
|
654
|
+
: tool.name;
|
|
655
|
+
const fallbackLabel = `${humanizeName(server.name)}: ${humanizeName(friendlyToolName)}`;
|
|
656
|
+
definitions.push({
|
|
657
|
+
name,
|
|
658
|
+
label: boundedText(tool.title ?? fallbackLabel, MAX_HOST_TOOL_LABEL_BYTES, name),
|
|
659
|
+
description: boundedText(
|
|
660
|
+
tool.description ?? `MCP tool from ${server.name}`,
|
|
661
|
+
MAX_HOST_TOOL_DESCRIPTION_BYTES,
|
|
662
|
+
"MCP tool",
|
|
663
|
+
),
|
|
664
|
+
loadMode:
|
|
665
|
+
server.internal || server.config.alwaysLoad === true ? "essential" : "discoverable",
|
|
666
|
+
parameters: structuredClone(tool.inputSchema),
|
|
667
|
+
});
|
|
668
|
+
targets.set(name, { toolName: tool.name, connection });
|
|
669
|
+
added.push(name);
|
|
563
670
|
}
|
|
564
|
-
throw error;
|
|
565
|
-
}
|
|
566
|
-
connections.push(connection);
|
|
567
|
-
const tools = (await discoverMcpTools(connection, initialization.signal)).sort(
|
|
568
|
-
(left, right) => left.name.localeCompare(right.name),
|
|
569
|
-
);
|
|
570
|
-
const namespace = namespaces.get(server.name);
|
|
571
|
-
if (!namespace) throw new Error("MCP server namespace is unavailable");
|
|
572
|
-
for (const tool of tools) {
|
|
573
671
|
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
boundedJsonBytes(tool.inputSchema, MAX_HOST_TOOL_SCHEMA_BYTES) ===
|
|
581
|
-
Number.POSITIVE_INFINITY
|
|
672
|
+
boundedJsonBytes(
|
|
673
|
+
definitions,
|
|
674
|
+
MAX_HOST_TOOL_CATALOG_BYTES,
|
|
675
|
+
MAX_HOST_TOOLS,
|
|
676
|
+
64 * 1024,
|
|
677
|
+
) === Number.POSITIVE_INFINITY
|
|
582
678
|
) {
|
|
583
|
-
throw new
|
|
584
|
-
}
|
|
585
|
-
if (definitions.length >= MAX_HOST_TOOLS) {
|
|
586
|
-
throw new Error("MCP servers exposed too many host tools");
|
|
679
|
+
throw new OmpPublicError("OMP host tool catalog exceeds the RPC frame limit");
|
|
587
680
|
}
|
|
588
|
-
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (!optional || initialization.signal.aborted) throw error;
|
|
683
|
+
if (error instanceof OmpCleanupFailure) {
|
|
684
|
+
try {
|
|
685
|
+
await abortable(error.cleanup, initialization.signal);
|
|
686
|
+
} catch {
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
} else if (connections.length > connectionCount) {
|
|
690
|
+
const failed = connections.pop() as OmpMcpConnection;
|
|
691
|
+
const closing = Promise.resolve().then(() => failed.close());
|
|
692
|
+
try {
|
|
693
|
+
await abortable(closing, initialization.signal);
|
|
694
|
+
} catch {
|
|
695
|
+
throw new OmpCleanupFailure("OMP MCP host tool cleanup failed", closing);
|
|
696
|
+
}
|
|
592
697
|
}
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
: tool.name;
|
|
599
|
-
const fallbackLabel = `${humanizeName(server.name)}: ${humanizeName(friendlyToolName)}`;
|
|
600
|
-
definitions.push({
|
|
601
|
-
name,
|
|
602
|
-
label: boundedText(tool.title ?? fallbackLabel, MAX_HOST_TOOL_LABEL_BYTES, name),
|
|
603
|
-
description: boundedText(
|
|
604
|
-
tool.description ?? `MCP tool from ${server.name}`,
|
|
605
|
-
MAX_HOST_TOOL_DESCRIPTION_BYTES,
|
|
606
|
-
"MCP tool",
|
|
607
|
-
),
|
|
608
|
-
loadMode:
|
|
609
|
-
server.internal || server.config.alwaysLoad === true ? "essential" : "discoverable",
|
|
610
|
-
parameters: structuredClone(tool.inputSchema),
|
|
698
|
+
definitions.length = definitionCount;
|
|
699
|
+
for (const name of added) targets.delete(name);
|
|
700
|
+
degraded.push({
|
|
701
|
+
name: server.name,
|
|
702
|
+
reason: timeout?.signal.aborted ? "timed-out" : reason,
|
|
611
703
|
});
|
|
612
|
-
|
|
704
|
+
report({ category: "session-open", stage: "optional-mcp" });
|
|
705
|
+
} finally {
|
|
706
|
+
clearTimeout(timer);
|
|
613
707
|
}
|
|
614
708
|
}
|
|
615
|
-
if (
|
|
616
|
-
boundedJsonBytes(definitions, MAX_HOST_TOOL_CATALOG_BYTES, MAX_HOST_TOOLS, 64 * 1024) ===
|
|
617
|
-
Number.POSITIVE_INFINITY
|
|
618
|
-
) {
|
|
619
|
-
throw new OmpPublicError("OMP host tool catalog exceeds the RPC frame limit");
|
|
620
|
-
}
|
|
621
709
|
return new OmpHostToolsBridge(
|
|
622
710
|
connections,
|
|
623
711
|
definitions,
|
|
@@ -625,6 +713,7 @@ export class OmpHostToolsBridge {
|
|
|
625
713
|
callTimeoutMs,
|
|
626
714
|
callScheduler,
|
|
627
715
|
options.reportOperationalFailure ?? (() => {}),
|
|
716
|
+
degraded,
|
|
628
717
|
);
|
|
629
718
|
} catch (error) {
|
|
630
719
|
const cleanupTasks = [
|
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
type OmpRecoveryOptions,
|
|
25
25
|
withCommittedOmpSelection,
|
|
26
26
|
} from "./config-normalization";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
describeDegradedMcpServers,
|
|
29
|
+
type OmpDegradedMcpServer,
|
|
30
|
+
OmpHostToolsBridge,
|
|
31
|
+
type OmpMcpConnector,
|
|
32
|
+
validateOmpHostToolConfig,
|
|
33
|
+
} from "./host-tools";
|
|
28
34
|
import { isOmpImageMimeType, OmpImageMaterializer } from "./image";
|
|
29
35
|
import type { OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
|
|
30
36
|
import { buildOmpSpawnRequest, type OmpStartOptions } from "./omp-rpc-environment";
|
|
@@ -331,6 +337,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
331
337
|
private readonly permissions: OmpSessionPermissions;
|
|
332
338
|
private unsupportedThinkingNoticePending = false;
|
|
333
339
|
private unsupportedThinkingNoticePublished = false;
|
|
340
|
+
private degradedMcpServers: readonly OmpDegradedMcpServer[] = [];
|
|
334
341
|
|
|
335
342
|
private constructor(
|
|
336
343
|
id: string,
|
|
@@ -685,6 +692,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
685
692
|
scheduler,
|
|
686
693
|
);
|
|
687
694
|
session.unsupportedThinkingNoticePending = unsupportedThinkingLevel;
|
|
695
|
+
session.degradedMcpServers = hostTools.degraded;
|
|
688
696
|
|
|
689
697
|
if (bootstrapConfigRevision !== reconciledConfigRevision) {
|
|
690
698
|
session.configRefreshDirty = true;
|
|
@@ -744,6 +752,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
744
752
|
...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
|
|
745
753
|
});
|
|
746
754
|
this.publishUnsupportedThinkingNotice();
|
|
755
|
+
this.publishDegradedMcpNotice();
|
|
747
756
|
this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
|
|
748
757
|
if (this.replayHistoryOnOpen) await this.replayHistory(true);
|
|
749
758
|
this.publishCommands(this.commandCatalog);
|
|
@@ -767,6 +776,19 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
767
776
|
},
|
|
768
777
|
});
|
|
769
778
|
}
|
|
779
|
+
private publishDegradedMcpNotice(): void {
|
|
780
|
+
if (this.degradedMcpServers.length === 0) return;
|
|
781
|
+
this.emit({
|
|
782
|
+
type: "session.notice",
|
|
783
|
+
sessionId: this.id,
|
|
784
|
+
notice: {
|
|
785
|
+
id: "omp:mcp-servers-unavailable",
|
|
786
|
+
severity: "warning",
|
|
787
|
+
title: "MCP servers unavailable",
|
|
788
|
+
description: describeDegradedMcpServers(this.degradedMcpServers),
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
}
|
|
770
792
|
|
|
771
793
|
private async replayHistory(preferPersistedTranscript = false): Promise<void> {
|
|
772
794
|
if (!this.runtime.canReplayHistory) {
|