openmausbot 0.1.92 → 0.1.94
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/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -1,137 +1,118 @@
|
|
|
1
|
-
// pi-mcp-extension
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// that exactly.
|
|
14
|
-
import { Type, type TObjectOptions, type TSchema, type TSchemaOptions } from "typebox";
|
|
15
|
-
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
16
|
-
import { readFileSync } from "node:fs";
|
|
17
|
-
|
|
18
|
-
interface McpServerDef {
|
|
19
|
-
command: string;
|
|
20
|
-
args?: string[];
|
|
21
|
-
env?: Record<string, string>;
|
|
22
|
-
/** "local-computer" marks the user's real host desktop: every tool on such
|
|
23
|
-
* a server is gated behind a permission card before it executes. */
|
|
24
|
-
scope?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface McpConfig {
|
|
28
|
-
mcpServers?: Record<string, McpServerDef>;
|
|
1
|
+
// server/drivers/pi-mcp-extension.ts
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// shared/tool-scope.ts
|
|
7
|
+
var SERVER_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
8
|
+
var CONTROL_CHARACTER = /[\x00-\x1f\x7f]/;
|
|
9
|
+
var MAX_SELECTORS = 256;
|
|
10
|
+
var MAX_SELECTOR_LENGTH = 1024;
|
|
11
|
+
function validName(name) {
|
|
12
|
+
return typeof name === "string" && name.trim().length > 0 && name.length <= MAX_SELECTOR_LENGTH && !CONTROL_CHARACTER.test(name) && !name.includes("*");
|
|
29
13
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
function selectorIdentity(selector) {
|
|
15
|
+
if (selector.length > MAX_SELECTOR_LENGTH || CONTROL_CHARACTER.test(selector)) return;
|
|
16
|
+
if (selector.startsWith("native:")) {
|
|
17
|
+
const name = selector.slice(7);
|
|
18
|
+
if (name === "*" || validName(name)) return { kind: "native", name };
|
|
19
|
+
} else if (selector.startsWith("mcp:")) {
|
|
20
|
+
const separator = selector.indexOf(":", 4);
|
|
21
|
+
if (separator < 0) return;
|
|
22
|
+
const server = selector.slice(4, separator);
|
|
23
|
+
const name = selector.slice(separator + 1);
|
|
24
|
+
if (SERVER_NAME.test(server) && (name === "*" || validName(name))) {
|
|
25
|
+
return { kind: "mcp", server, name };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
35
28
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
function parseToolScope(value) {
|
|
30
|
+
if (value === void 0) return { ok: true, scope: void 0 };
|
|
31
|
+
const invalid = { ok: false, error: "Tool selection must contain only valid allow and deny lists." };
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
|
|
33
|
+
const prototype = Object.getPrototypeOf(value);
|
|
34
|
+
if (prototype !== Object.prototype && prototype !== null) return invalid;
|
|
35
|
+
const scope = {};
|
|
36
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
37
|
+
if (key !== "allow" && key !== "deny") return invalid;
|
|
38
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
39
|
+
if (!descriptor || !("value" in descriptor)) return invalid;
|
|
40
|
+
const list = descriptor.value;
|
|
41
|
+
if (!Array.isArray(list) || list.length > MAX_SELECTORS) return invalid;
|
|
42
|
+
if (!Array.from(list).every((entry) => typeof entry === "string" && selectorIdentity(entry))) return invalid;
|
|
43
|
+
scope[key] = [...new Set(list)];
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, scope };
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
data: unknown;
|
|
45
|
-
mimeType: unknown;
|
|
47
|
+
function matches(selector, tool) {
|
|
48
|
+
const selected = selectorIdentity(selector);
|
|
49
|
+
return selected?.kind === tool.kind && (selected.kind !== "mcp" || tool.kind === "mcp" && selected.server === tool.server) && (selected.name === "*" || selected.name === tool.name);
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
interface PiToolContext {
|
|
51
|
-
ui: {
|
|
52
|
-
confirm(title: string, message: string): Promise<boolean>;
|
|
53
|
-
};
|
|
51
|
+
function permits(scope, tool) {
|
|
52
|
+
if (!validName(tool.name) || tool.kind === "mcp" && !SERVER_NAME.test(tool.server)) return false;
|
|
53
|
+
return (scope?.allow === void 0 || scope.allow.some((selector) => matches(selector, tool))) && !scope?.deny?.some((selector) => matches(selector, tool));
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
label: string;
|
|
59
|
-
description: string;
|
|
60
|
-
parameters: TSchema;
|
|
61
|
-
execute(
|
|
62
|
-
toolCallId: string,
|
|
63
|
-
params: unknown,
|
|
64
|
-
signal: AbortSignal | undefined,
|
|
65
|
-
onUpdate: unknown,
|
|
66
|
-
context: PiToolContext,
|
|
67
|
-
): Promise<{ content: PiToolContent[]; details: Record<string, never> }>;
|
|
55
|
+
function allowsTool(scope, tool) {
|
|
56
|
+
const parsed = parseToolScope(scope);
|
|
57
|
+
return parsed.ok && permits(parsed.scope, tool);
|
|
68
58
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
function canUseMcpServer(scope, server, names) {
|
|
60
|
+
const parsed = parseToolScope(scope);
|
|
61
|
+
if (!parsed.ok || !SERVER_NAME.test(server)) return false;
|
|
62
|
+
if (names !== void 0) {
|
|
63
|
+
return names.some((name) => permits(parsed.scope, { kind: "mcp", server, name }));
|
|
64
|
+
}
|
|
65
|
+
if (parsed.scope?.deny?.includes(`mcp:${server}:*`)) return false;
|
|
66
|
+
if (parsed.scope?.allow === void 0) return true;
|
|
67
|
+
return parsed.scope.allow.some((selector) => {
|
|
68
|
+
const selected = selectorIdentity(selector);
|
|
69
|
+
return selected?.kind === "mcp" && selected.server === server && (selected.name === "*" || permits(parsed.scope, selected));
|
|
70
|
+
});
|
|
76
71
|
}
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
94
|
-
|
|
95
|
-
constructor(def: McpServerDef) {
|
|
73
|
+
// server/drivers/pi-mcp-extension.ts
|
|
74
|
+
var MCP_STARTUP_TIMEOUT_MS = 8e3;
|
|
75
|
+
var MCP_TOOL_TIMEOUT_MS = 10 * 6e4;
|
|
76
|
+
var MCP_MAX_LIST_PAGES = 100;
|
|
77
|
+
var MCP_MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
78
|
+
var TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
|
|
79
|
+
var TOOL_OUTPUT_MAX_LINES = 2e3;
|
|
80
|
+
var TOOL_NAME_MAX_LENGTH = 64;
|
|
81
|
+
var StdioMcp = class {
|
|
82
|
+
child;
|
|
83
|
+
buf = "";
|
|
84
|
+
nextId = 1;
|
|
85
|
+
disposed = false;
|
|
86
|
+
pending = /* @__PURE__ */ new Map();
|
|
87
|
+
constructor(def) {
|
|
96
88
|
this.child = spawn(def.command, def.args ?? [], {
|
|
97
89
|
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
-
env: { ...process.env, ...def.env }
|
|
90
|
+
env: { ...process.env, ...def.env }
|
|
99
91
|
});
|
|
100
92
|
this.child.stderr.on("data", () => {
|
|
101
|
-
/* best-effort drain so a chatty server never blocks */
|
|
102
93
|
});
|
|
103
94
|
this.child.stdout.setEncoding("utf8");
|
|
104
|
-
this.child.stdout.on("data", (chunk
|
|
95
|
+
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
105
96
|
this.child.on("error", (err) => this.closeWithError(err));
|
|
106
|
-
// A server that starts and then dies emits `exit`, never `error`; without
|
|
107
|
-
// settling on it, an in-flight init/listTools would hang the extension
|
|
108
|
-
// load for the whole turn.
|
|
109
97
|
this.child.on("exit", (code) => this.closeWithError(new Error(`MCP server exited (code ${code ?? "?"})`)));
|
|
110
|
-
// A write to a dead child errors asynchronously on the stream; an
|
|
111
|
-
// unhandled stream error would kill the pi process (the same hazard
|
|
112
|
-
// spawnCli documents for driver-spawned children in procs.ts).
|
|
113
98
|
this.child.stdin.on("error", () => this.closeWithError(new Error("MCP server stdin closed")));
|
|
114
99
|
}
|
|
115
|
-
|
|
116
|
-
private failAll(err: Error): void {
|
|
100
|
+
failAll(err) {
|
|
117
101
|
for (const [, p] of this.pending) p.reject(err);
|
|
118
102
|
this.pending.clear();
|
|
119
103
|
}
|
|
120
|
-
|
|
121
|
-
private closeWithError(err: Error): void {
|
|
104
|
+
closeWithError(err) {
|
|
122
105
|
if (this.disposed) return;
|
|
123
106
|
this.disposed = true;
|
|
124
107
|
this.failAll(err);
|
|
125
108
|
try {
|
|
126
109
|
this.child.kill();
|
|
127
110
|
} catch {
|
|
128
|
-
/* already gone */
|
|
129
111
|
}
|
|
130
112
|
}
|
|
131
|
-
|
|
132
|
-
private onData(chunk: string): void {
|
|
113
|
+
onData(chunk) {
|
|
133
114
|
this.buf += chunk;
|
|
134
|
-
let nl
|
|
115
|
+
let nl;
|
|
135
116
|
while ((nl = this.buf.indexOf("\n")) !== -1) {
|
|
136
117
|
const line = this.buf.slice(0, nl).trim();
|
|
137
118
|
this.buf = this.buf.slice(nl + 1);
|
|
@@ -140,22 +121,20 @@ export class StdioMcp {
|
|
|
140
121
|
this.closeWithError(new Error(`MCP frame exceeded ${MCP_MAX_FRAME_BYTES} bytes`));
|
|
141
122
|
return;
|
|
142
123
|
}
|
|
143
|
-
let msg
|
|
124
|
+
let msg;
|
|
144
125
|
try {
|
|
145
126
|
msg = JSON.parse(line);
|
|
146
127
|
} catch {
|
|
147
128
|
continue;
|
|
148
129
|
}
|
|
149
130
|
if (typeof msg.id === "number" && this.pending.has(msg.id)) {
|
|
150
|
-
const p = this.pending.get(msg.id)
|
|
131
|
+
const p = this.pending.get(msg.id);
|
|
151
132
|
this.pending.delete(msg.id);
|
|
152
133
|
if (msg.error) p.reject(new Error(msg.error.message ?? "MCP error"));
|
|
153
134
|
else p.resolve(msg.result);
|
|
154
135
|
continue;
|
|
155
136
|
}
|
|
156
|
-
|
|
157
|
-
// explicitly instead of leaving a conforming server waiting forever.
|
|
158
|
-
if (msg.id !== undefined && typeof msg.method === "string") {
|
|
137
|
+
if (msg.id !== void 0 && typeof msg.method === "string") {
|
|
159
138
|
try {
|
|
160
139
|
this.write({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "Client method not supported" } });
|
|
161
140
|
} catch (err) {
|
|
@@ -163,44 +142,38 @@ export class StdioMcp {
|
|
|
163
142
|
return;
|
|
164
143
|
}
|
|
165
144
|
}
|
|
166
|
-
// Notifications (e.g. notifications/tools/list_changed) are intentionally
|
|
167
|
-
// ignored for this single-shot mount.
|
|
168
145
|
}
|
|
169
146
|
if (Buffer.byteLength(this.buf, "utf8") > MCP_MAX_FRAME_BYTES) {
|
|
170
147
|
this.closeWithError(new Error(`MCP frame exceeded ${MCP_MAX_FRAME_BYTES} bytes without a newline`));
|
|
171
148
|
}
|
|
172
149
|
}
|
|
173
|
-
|
|
174
|
-
private write(frame: unknown): void {
|
|
150
|
+
write(frame) {
|
|
175
151
|
if (this.disposed || this.child.stdin.destroyed || !this.child.stdin.writable) {
|
|
176
152
|
throw new Error("MCP server stdin is closed");
|
|
177
153
|
}
|
|
178
154
|
this.child.stdin.write(JSON.stringify(frame) + "\n");
|
|
179
155
|
}
|
|
180
|
-
|
|
181
|
-
private call(method: string, params: unknown, timeoutMs: number, signal?: AbortSignal): Promise<unknown> {
|
|
156
|
+
call(method, params, timeoutMs, signal) {
|
|
182
157
|
if (this.disposed) return Promise.reject(new Error("MCP client is closed"));
|
|
183
158
|
if (signal?.aborted) return Promise.reject(new Error(`MCP ${method} aborted`));
|
|
184
|
-
|
|
185
159
|
const id = this.nextId++;
|
|
186
|
-
return new Promise
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
187
161
|
let settled = false;
|
|
188
162
|
const cleanup = () => {
|
|
189
163
|
clearTimeout(timer);
|
|
190
164
|
signal?.removeEventListener("abort", onAbort);
|
|
191
165
|
};
|
|
192
|
-
const settle = (fn
|
|
166
|
+
const settle = (fn) => {
|
|
193
167
|
if (settled) return;
|
|
194
168
|
settled = true;
|
|
195
169
|
cleanup();
|
|
196
170
|
fn();
|
|
197
171
|
};
|
|
198
|
-
const cancel = (reason
|
|
172
|
+
const cancel = (reason) => {
|
|
199
173
|
this.pending.delete(id);
|
|
200
174
|
try {
|
|
201
175
|
this.notify("notifications/cancelled", { requestId: id, reason });
|
|
202
176
|
} catch {
|
|
203
|
-
/* the transport may already be gone */
|
|
204
177
|
}
|
|
205
178
|
settle(() => reject(new Error(reason)));
|
|
206
179
|
};
|
|
@@ -210,7 +183,7 @@ export class StdioMcp {
|
|
|
210
183
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
211
184
|
this.pending.set(id, {
|
|
212
185
|
resolve: (value) => settle(() => resolve(value)),
|
|
213
|
-
reject: (err) => settle(() => reject(err))
|
|
186
|
+
reject: (err) => settle(() => reject(err))
|
|
214
187
|
});
|
|
215
188
|
try {
|
|
216
189
|
this.write({ jsonrpc: "2.0", id, method, params });
|
|
@@ -220,38 +193,35 @@ export class StdioMcp {
|
|
|
220
193
|
}
|
|
221
194
|
});
|
|
222
195
|
}
|
|
223
|
-
|
|
224
|
-
private notify(method: string, params?: unknown): void {
|
|
196
|
+
notify(method, params) {
|
|
225
197
|
this.write({ jsonrpc: "2.0", method, params });
|
|
226
198
|
}
|
|
227
|
-
|
|
228
|
-
async init(): Promise<void> {
|
|
199
|
+
async init() {
|
|
229
200
|
await this.call(
|
|
230
201
|
"initialize",
|
|
231
202
|
{
|
|
232
203
|
protocolVersion: "2024-11-05",
|
|
233
204
|
capabilities: {},
|
|
234
|
-
clientInfo: { name: "openmausbot-pi", version: "1" }
|
|
205
|
+
clientInfo: { name: "openmausbot-pi", version: "1" }
|
|
235
206
|
},
|
|
236
|
-
MCP_STARTUP_TIMEOUT_MS
|
|
207
|
+
MCP_STARTUP_TIMEOUT_MS
|
|
237
208
|
);
|
|
238
209
|
this.notify("notifications/initialized");
|
|
239
210
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
const seenCursors = new Set<string>();
|
|
211
|
+
async listTools() {
|
|
212
|
+
const tools = [];
|
|
213
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
244
214
|
const deadline = Date.now() + MCP_STARTUP_TIMEOUT_MS;
|
|
245
|
-
let cursor
|
|
215
|
+
let cursor;
|
|
246
216
|
for (let page = 0; page < MCP_MAX_LIST_PAGES; page += 1) {
|
|
247
217
|
const remainingMs = Math.max(1, deadline - Date.now());
|
|
248
|
-
const res =
|
|
218
|
+
const res = await this.call(
|
|
249
219
|
"tools/list",
|
|
250
220
|
cursor ? { cursor } : {},
|
|
251
|
-
remainingMs
|
|
252
|
-
)
|
|
221
|
+
remainingMs
|
|
222
|
+
);
|
|
253
223
|
if (!Array.isArray(res?.tools)) throw new Error("MCP tools/list returned no tools array");
|
|
254
|
-
tools.push(...
|
|
224
|
+
tools.push(...res.tools);
|
|
255
225
|
if (typeof res.nextCursor !== "string" || !res.nextCursor) return tools;
|
|
256
226
|
if (seenCursors.has(res.nextCursor)) throw new Error("MCP tools/list repeated a pagination cursor");
|
|
257
227
|
seenCursors.add(res.nextCursor);
|
|
@@ -259,84 +229,39 @@ export class StdioMcp {
|
|
|
259
229
|
}
|
|
260
230
|
throw new Error(`MCP tools/list exceeded ${MCP_MAX_LIST_PAGES} pages`);
|
|
261
231
|
}
|
|
262
|
-
|
|
263
232
|
/** tools/call → Pi content, preserving screenshots and bounding text so a
|
|
264
233
|
* remote server cannot flood the model context. */
|
|
265
|
-
async callTool(name
|
|
266
|
-
const res =
|
|
234
|
+
async callTool(name, args, signal) {
|
|
235
|
+
const res = await this.call(
|
|
267
236
|
"tools/call",
|
|
268
237
|
{ name, arguments: args ?? {} },
|
|
269
238
|
MCP_TOOL_TIMEOUT_MS,
|
|
270
|
-
signal
|
|
271
|
-
)
|
|
239
|
+
signal
|
|
240
|
+
);
|
|
272
241
|
const rawContent = Array.isArray(res?.content) ? res.content : [];
|
|
273
|
-
const text = rawContent
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
.filter(
|
|
279
|
-
(c): c is McpImageContent =>
|
|
280
|
-
Boolean(c) &&
|
|
281
|
-
(c as { type?: unknown }).type === "image" &&
|
|
282
|
-
typeof (c as McpImageContent).data === "string" &&
|
|
283
|
-
typeof (c as McpImageContent).mimeType === "string",
|
|
284
|
-
)
|
|
285
|
-
.map((c) => ({ type: "image" as const, data: String(c.data), mimeType: String(c.mimeType) }));
|
|
286
|
-
const unsupported = rawContent.length - rawContent.filter((c) => (c as { type?: unknown })?.type === "text").length - images.length;
|
|
242
|
+
const text = rawContent.filter((c) => Boolean(c) && c.type === "text").map((c) => String(c.text ?? "")).join("\n");
|
|
243
|
+
const images = rawContent.filter(
|
|
244
|
+
(c) => Boolean(c) && c.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string"
|
|
245
|
+
).map((c) => ({ type: "image", data: String(c.data), mimeType: String(c.mimeType) }));
|
|
246
|
+
const unsupported = rawContent.length - rawContent.filter((c) => c?.type === "text").length - images.length;
|
|
287
247
|
let boundedText = truncateToolText(text || (images.length ? "" : "(empty result)"));
|
|
288
248
|
if (unsupported > 0) boundedText += `${boundedText ? "\n" : ""}[${unsupported} unsupported MCP content item(s) omitted]`;
|
|
289
249
|
return {
|
|
290
|
-
content: [...
|
|
291
|
-
isError: Boolean(res?.isError)
|
|
250
|
+
content: [...boundedText ? [{ type: "text", text: boundedText }] : [], ...images],
|
|
251
|
+
isError: Boolean(res?.isError)
|
|
292
252
|
};
|
|
293
253
|
}
|
|
294
|
-
|
|
295
|
-
dispose(): void {
|
|
254
|
+
dispose() {
|
|
296
255
|
if (this.disposed) return;
|
|
297
256
|
this.disposed = true;
|
|
298
257
|
this.failAll(new Error("MCP client disposed"));
|
|
299
258
|
try {
|
|
300
259
|
this.child.kill();
|
|
301
260
|
} catch {
|
|
302
|
-
/* already gone */
|
|
303
261
|
}
|
|
304
262
|
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
interface JsonSchema {
|
|
308
|
-
type?: string | string[];
|
|
309
|
-
title?: unknown;
|
|
310
|
-
description?: unknown;
|
|
311
|
-
default?: unknown;
|
|
312
|
-
const?: unknown;
|
|
313
|
-
enum?: unknown[];
|
|
314
|
-
nullable?: unknown;
|
|
315
|
-
anyOf?: unknown[];
|
|
316
|
-
oneOf?: unknown[];
|
|
317
|
-
allOf?: unknown[];
|
|
318
|
-
items?: unknown | unknown[];
|
|
319
|
-
prefixItems?: unknown[];
|
|
320
|
-
properties?: Record<string, unknown>;
|
|
321
|
-
required?: unknown;
|
|
322
|
-
additionalProperties?: unknown;
|
|
323
|
-
minimum?: unknown;
|
|
324
|
-
maximum?: unknown;
|
|
325
|
-
exclusiveMinimum?: unknown;
|
|
326
|
-
exclusiveMaximum?: unknown;
|
|
327
|
-
multipleOf?: unknown;
|
|
328
|
-
minLength?: unknown;
|
|
329
|
-
maxLength?: unknown;
|
|
330
|
-
pattern?: unknown;
|
|
331
|
-
format?: unknown;
|
|
332
|
-
minItems?: unknown;
|
|
333
|
-
maxItems?: unknown;
|
|
334
|
-
uniqueItems?: unknown;
|
|
335
|
-
minProperties?: unknown;
|
|
336
|
-
maxProperties?: unknown;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const SCHEMA_OPTION_KEYS = [
|
|
263
|
+
};
|
|
264
|
+
var SCHEMA_OPTION_KEYS = [
|
|
340
265
|
"title",
|
|
341
266
|
"description",
|
|
342
267
|
"default",
|
|
@@ -353,64 +278,54 @@ const SCHEMA_OPTION_KEYS = [
|
|
|
353
278
|
"maxItems",
|
|
354
279
|
"uniqueItems",
|
|
355
280
|
"minProperties",
|
|
356
|
-
"maxProperties"
|
|
357
|
-
]
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const options: TSchemaOptions = {};
|
|
281
|
+
"maxProperties"
|
|
282
|
+
];
|
|
283
|
+
function schemaOptions(schema) {
|
|
284
|
+
const options = {};
|
|
361
285
|
for (const key of SCHEMA_OPTION_KEYS) {
|
|
362
286
|
const value = schema[key];
|
|
363
|
-
if (value !==
|
|
287
|
+
if (value !== void 0) Reflect.set(options, key, value);
|
|
364
288
|
}
|
|
365
289
|
return options;
|
|
366
290
|
}
|
|
367
|
-
|
|
368
|
-
function isSchemaObject(value: unknown): value is JsonSchema {
|
|
291
|
+
function isSchemaObject(value) {
|
|
369
292
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
370
293
|
}
|
|
371
|
-
|
|
372
|
-
function primitiveLiteral(value: unknown): TSchema | null {
|
|
294
|
+
function primitiveLiteral(value) {
|
|
373
295
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
374
296
|
return Type.Literal(value);
|
|
375
297
|
}
|
|
376
298
|
if (value === null) return Type.Null();
|
|
377
299
|
return null;
|
|
378
300
|
}
|
|
379
|
-
|
|
380
|
-
function withNullable(schema: TSchema, nullable: unknown): TSchema {
|
|
301
|
+
function withNullable(schema, nullable) {
|
|
381
302
|
return nullable === true ? Type.Union([schema, Type.Null()]) : schema;
|
|
382
303
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const visit = (candidate: unknown, mode: "root" | "all" | "choice") => {
|
|
304
|
+
function collectObjectShape(schema) {
|
|
305
|
+
const properties = {};
|
|
306
|
+
const required = /* @__PURE__ */ new Set();
|
|
307
|
+
const visit = (candidate, mode) => {
|
|
389
308
|
if (!isSchemaObject(candidate)) return;
|
|
390
309
|
for (const branch of candidate.allOf ?? []) visit(branch, mode === "choice" ? "choice" : "all");
|
|
391
|
-
for (const branch of [...
|
|
310
|
+
for (const branch of [...candidate.anyOf ?? [], ...candidate.oneOf ?? []]) visit(branch, "choice");
|
|
392
311
|
for (const [key, value] of Object.entries(candidate.properties ?? {})) {
|
|
393
|
-
|
|
394
|
-
// still retained so the model knows which arguments exist.
|
|
395
|
-
if (mode === "root" || properties[key] === undefined) properties[key] = value;
|
|
312
|
+
if (mode === "root" || properties[key] === void 0) properties[key] = value;
|
|
396
313
|
}
|
|
397
314
|
if (mode !== "choice" && Array.isArray(candidate.required)) {
|
|
398
315
|
for (const key of candidate.required) if (typeof key === "string") required.add(key);
|
|
399
316
|
}
|
|
400
317
|
};
|
|
401
|
-
|
|
402
318
|
visit(schema, "root");
|
|
403
319
|
return { properties, required };
|
|
404
320
|
}
|
|
405
|
-
|
|
406
|
-
function objectSchema(schema: JsonSchema, root = false): TSchema {
|
|
321
|
+
function objectSchema(schema, root = false) {
|
|
407
322
|
const { properties, required } = collectObjectShape(schema);
|
|
408
|
-
const converted
|
|
323
|
+
const converted = {};
|
|
409
324
|
for (const [key, value] of Object.entries(properties)) {
|
|
410
325
|
const nested = nestedTypebox(value);
|
|
411
326
|
converted[key] = required.has(key) ? nested : Type.Optional(nested);
|
|
412
327
|
}
|
|
413
|
-
const options
|
|
328
|
+
const options = schemaOptions(schema);
|
|
414
329
|
if (typeof schema.additionalProperties === "boolean") {
|
|
415
330
|
options.additionalProperties = schema.additionalProperties;
|
|
416
331
|
} else if (isSchemaObject(schema.additionalProperties)) {
|
|
@@ -419,34 +334,26 @@ function objectSchema(schema: JsonSchema, root = false): TSchema {
|
|
|
419
334
|
const object = Type.Object(converted, options);
|
|
420
335
|
return root ? object : withNullable(object, schema.nullable);
|
|
421
336
|
}
|
|
422
|
-
|
|
423
|
-
function nestedTypebox(schema: unknown): TSchema {
|
|
337
|
+
function nestedTypebox(schema) {
|
|
424
338
|
if (!isSchemaObject(schema)) return Type.Any();
|
|
425
339
|
const options = schemaOptions(schema);
|
|
426
|
-
|
|
427
340
|
const literal = primitiveLiteral(schema.const);
|
|
428
341
|
if (literal) return withNullable(literal, schema.nullable);
|
|
429
|
-
|
|
430
342
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
431
343
|
if (schema.enum.every((value) => typeof value === "string")) {
|
|
432
|
-
// `{type:"string", enum:[...]}` works across Google/OpenAI/Anthropic;
|
|
433
|
-
// Type.Union(Type.Literal(...)) does not work with Google's tool API.
|
|
434
344
|
return withNullable(Type.String({ ...options, enum: schema.enum }), schema.nullable);
|
|
435
345
|
}
|
|
436
|
-
const literals = schema.enum.map(primitiveLiteral).filter((value)
|
|
346
|
+
const literals = schema.enum.map(primitiveLiteral).filter((value) => value !== null);
|
|
437
347
|
if (literals.length === schema.enum.length) {
|
|
438
348
|
const union = literals.length === 1 ? literals[0] : Type.Union(literals, options);
|
|
439
349
|
return withNullable(union, schema.nullable);
|
|
440
350
|
}
|
|
441
351
|
}
|
|
442
|
-
|
|
443
352
|
if (Array.isArray(schema.type)) {
|
|
444
353
|
const variants = schema.type.map((type) => nestedTypebox({ ...schema, type, nullable: false }));
|
|
445
354
|
return variants.length === 1 ? variants[0] : Type.Union(variants, options);
|
|
446
355
|
}
|
|
447
|
-
|
|
448
356
|
if (schema.type === "object" || schema.properties) return objectSchema(schema);
|
|
449
|
-
|
|
450
357
|
const choice = schema.anyOf ?? schema.oneOf;
|
|
451
358
|
if (Array.isArray(choice) && choice.length > 0) {
|
|
452
359
|
const variants = choice.map(nestedTypebox);
|
|
@@ -456,7 +363,6 @@ function nestedTypebox(schema: unknown): TSchema {
|
|
|
456
363
|
const variants = schema.allOf.map(nestedTypebox);
|
|
457
364
|
return withNullable(variants.length === 1 ? variants[0] : Type.Intersect(variants, options), schema.nullable);
|
|
458
365
|
}
|
|
459
|
-
|
|
460
366
|
switch (schema.type) {
|
|
461
367
|
case "string":
|
|
462
368
|
return withNullable(Type.String(options), schema.nullable);
|
|
@@ -469,34 +375,22 @@ function nestedTypebox(schema: unknown): TSchema {
|
|
|
469
375
|
case "null":
|
|
470
376
|
return Type.Null();
|
|
471
377
|
case "array": {
|
|
472
|
-
const tupleItems = schema.prefixItems ?? (Array.isArray(schema.items) ? schema.items :
|
|
473
|
-
const value = tupleItems
|
|
474
|
-
? Type.Tuple(tupleItems.map(nestedTypebox), options)
|
|
475
|
-
: Type.Array(schema.items ? nestedTypebox(schema.items) : Type.Any(), options);
|
|
378
|
+
const tupleItems = schema.prefixItems ?? (Array.isArray(schema.items) ? schema.items : void 0);
|
|
379
|
+
const value = tupleItems ? Type.Tuple(tupleItems.map(nestedTypebox), options) : Type.Array(schema.items ? nestedTypebox(schema.items) : Type.Any(), options);
|
|
476
380
|
return withNullable(value, schema.nullable);
|
|
477
381
|
}
|
|
478
382
|
default:
|
|
479
383
|
return Type.Any();
|
|
480
384
|
}
|
|
481
385
|
}
|
|
482
|
-
|
|
483
|
-
/** Best-effort JSON Schema → TypeBox. MCP tool parameters must always expose
|
|
484
|
-
* an object at the root; Pi/provider tool serialization rejects a root schema
|
|
485
|
-
* without `type: "object"`. Nested schemas retain unions, nullability and
|
|
486
|
-
* constraints instead of being incorrectly rewritten as objects. */
|
|
487
|
-
export function toTypebox(schema: unknown): TSchema {
|
|
386
|
+
function toTypebox(schema) {
|
|
488
387
|
return objectSchema(isSchemaObject(schema) ? schema : {}, true);
|
|
489
388
|
}
|
|
490
|
-
|
|
491
|
-
/** pi tool names are lowercase snake identifiers; MCP tool names are not
|
|
492
|
-
* (COMPOSIO_SEARCH_TOOLS, browser_navigate, mcp__x…). Normalize and prefix
|
|
493
|
-
* with the server so two servers can never collide. */
|
|
494
|
-
function sanitizeToolName(server: string, tool: string): string {
|
|
389
|
+
function sanitizeToolName(server, tool) {
|
|
495
390
|
const raw = `${server}_${tool}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
496
391
|
return raw.slice(0, TOOL_NAME_MAX_LENGTH) || "mcp_tool";
|
|
497
392
|
}
|
|
498
|
-
|
|
499
|
-
export function allocateToolName(server: string, tool: string, used: Set<string>): string {
|
|
393
|
+
function allocateToolName(server, tool, used) {
|
|
500
394
|
const base = sanitizeToolName(server, tool);
|
|
501
395
|
let candidate = base;
|
|
502
396
|
for (let suffixNumber = 2; used.has(candidate); suffixNumber += 1) {
|
|
@@ -505,28 +399,25 @@ export function allocateToolName(server: string, tool: string, used: Set<string>
|
|
|
505
399
|
}
|
|
506
400
|
return candidate;
|
|
507
401
|
}
|
|
508
|
-
|
|
509
|
-
export function truncateToolText(text: string): string {
|
|
402
|
+
function truncateToolText(text) {
|
|
510
403
|
const lines = text.split("\n");
|
|
511
404
|
const lineLimited = lines.slice(0, TOOL_OUTPUT_MAX_LINES).join("\n");
|
|
512
405
|
const bytes = Buffer.from(lineLimited, "utf8");
|
|
513
406
|
let byteEnd = Math.min(bytes.length, TOOL_OUTPUT_MAX_BYTES);
|
|
514
|
-
|
|
515
|
-
// middle of a UTF-8 code point.
|
|
516
|
-
while (byteEnd > 0 && byteEnd < bytes.length && (bytes[byteEnd] & 0xc0) === 0x80) byteEnd -= 1;
|
|
407
|
+
while (byteEnd > 0 && byteEnd < bytes.length && (bytes[byteEnd] & 192) === 128) byteEnd -= 1;
|
|
517
408
|
const content = bytes.subarray(0, byteEnd).toString("utf8");
|
|
518
409
|
const truncatedByLines = lines.length > TOOL_OUTPUT_MAX_LINES;
|
|
519
410
|
const truncatedByBytes = bytes.length > TOOL_OUTPUT_MAX_BYTES;
|
|
520
411
|
if (!truncatedByLines && !truncatedByBytes) return content;
|
|
521
412
|
const reasons = [
|
|
522
|
-
...
|
|
523
|
-
...
|
|
413
|
+
...truncatedByLines ? [`${TOOL_OUTPUT_MAX_LINES}-line limit`] : [],
|
|
414
|
+
...truncatedByBytes ? [`${TOOL_OUTPUT_MAX_BYTES / 1024}KB limit`] : []
|
|
524
415
|
];
|
|
525
|
-
return `${content}
|
|
526
|
-
}
|
|
416
|
+
return `${content}
|
|
527
417
|
|
|
528
|
-
|
|
529
|
-
|
|
418
|
+
[MCP output truncated: ${reasons.join(" and ")}. Refine the request for less output.]`;
|
|
419
|
+
}
|
|
420
|
+
function summarizeParams(params) {
|
|
530
421
|
try {
|
|
531
422
|
const s = JSON.stringify(params ?? {});
|
|
532
423
|
return s === "{}" ? "" : s.slice(0, 300);
|
|
@@ -534,54 +425,111 @@ function summarizeParams(params: unknown): string {
|
|
|
534
425
|
return "";
|
|
535
426
|
}
|
|
536
427
|
}
|
|
537
|
-
|
|
538
|
-
export default async function (pi: PiExtensionApi): Promise<void> {
|
|
428
|
+
async function pi_mcp_extension_default(pi) {
|
|
539
429
|
const configPath = process.env.OMB_MCP_CONFIG;
|
|
540
430
|
if (!configPath) return;
|
|
541
|
-
|
|
542
|
-
let config: McpConfig;
|
|
431
|
+
let config;
|
|
543
432
|
try {
|
|
544
|
-
config = JSON.parse(readFileSync(configPath, "utf8"))
|
|
433
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
545
434
|
} catch {
|
|
546
|
-
|
|
435
|
+
throw new Error("OpenMausBot Pi MCP configuration could not be read");
|
|
436
|
+
}
|
|
437
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("OpenMausBot Pi MCP configuration is invalid");
|
|
438
|
+
const parsed = parseToolScope(config.toolScope);
|
|
439
|
+
if (!parsed.ok) throw new Error(parsed.error);
|
|
440
|
+
const scope = parsed.scope;
|
|
441
|
+
if (scope !== void 0 && (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function")) throw new Error("Pi tool selection enforcement APIs are unavailable");
|
|
442
|
+
const used = /* @__PURE__ */ new Set();
|
|
443
|
+
const identities = /* @__PURE__ */ new Map();
|
|
444
|
+
let enforcementFailed = false;
|
|
445
|
+
const clients = [];
|
|
446
|
+
const serverEntries = Object.entries(config.mcpServers ?? {}).filter(([name]) => scope === void 0 || canUseMcpServer(scope, name));
|
|
447
|
+
if (scope !== void 0) {
|
|
448
|
+
const allowed = (name) => allowsTool(scope, identities.get(name) ?? { kind: "native", name });
|
|
449
|
+
const intersect = () => {
|
|
450
|
+
if (enforcementFailed) throw new Error("Pi tool selection enforcement is unavailable");
|
|
451
|
+
try {
|
|
452
|
+
const active = pi.getActiveTools();
|
|
453
|
+
if (!Array.isArray(active) || active.some((name) => typeof name !== "string")) throw new Error("Pi tool selection enforcement returned an invalid catalog");
|
|
454
|
+
const requested = new Set(active.filter(allowed));
|
|
455
|
+
pi.setActiveTools([...requested]);
|
|
456
|
+
const selected = pi.getActiveTools();
|
|
457
|
+
if (!Array.isArray(selected) || selected.some((name) => !requested.has(name))) throw new Error("Pi tool selection enforcement did not apply the active-tool restriction");
|
|
458
|
+
if (config.scopeReadyPath) writeFileSync(config.scopeReadyPath, JSON.stringify({ ok: true, toolScope: scope }), { mode: 384 });
|
|
459
|
+
return selected;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
enforcementFailed = true;
|
|
462
|
+
if (config.scopeReadyPath) {
|
|
463
|
+
try {
|
|
464
|
+
writeFileSync(config.scopeReadyPath, JSON.stringify({ ok: false }), { mode: 384 });
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
for (const event of ["session_start", "session_switch", "model_select", "before_agent_start", "context"]) pi.on(event, () => {
|
|
472
|
+
intersect();
|
|
473
|
+
});
|
|
474
|
+
pi.on("tool_call", (event) => !enforcementFailed && typeof event?.toolName === "string" && allowed(event.toolName) ? void 0 : { block: true, reason: "Tool selection excludes this tool" });
|
|
475
|
+
pi.on("before_provider_request", (event, context) => {
|
|
476
|
+
let selected = /* @__PURE__ */ new Set();
|
|
477
|
+
try {
|
|
478
|
+
selected = new Set(intersect());
|
|
479
|
+
} catch {
|
|
480
|
+
try {
|
|
481
|
+
context?.abort?.();
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const declared = (name) => selected.has(name) && allowed(name);
|
|
486
|
+
const payload = event?.payload;
|
|
487
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
488
|
+
const body = payload;
|
|
489
|
+
if (Array.isArray(body.tools)) body.tools = body.tools.flatMap((value) => {
|
|
490
|
+
if (!value || typeof value !== "object") return [];
|
|
491
|
+
const tool = value;
|
|
492
|
+
if (Array.isArray(tool.functionDeclarations)) {
|
|
493
|
+
const functionDeclarations = tool.functionDeclarations.filter((declaration) => typeof declaration.name === "string" && declared(declaration.name));
|
|
494
|
+
return functionDeclarations.length ? [{ ...tool, functionDeclarations }] : [];
|
|
495
|
+
}
|
|
496
|
+
const name = tool.function?.name ?? tool.name;
|
|
497
|
+
return typeof name === "string" && declared(name) ? [tool] : [];
|
|
498
|
+
});
|
|
499
|
+
return payload;
|
|
500
|
+
});
|
|
547
501
|
}
|
|
548
|
-
|
|
549
|
-
const used = new Set<string>();
|
|
550
|
-
const clients: StdioMcp[] = [];
|
|
551
|
-
const serverEntries = Object.entries(config.mcpServers ?? {});
|
|
552
|
-
|
|
553
|
-
// Mount independent servers concurrently so one slow integration cannot
|
|
554
|
-
// consume the startup timeout once per server. Registration remains in
|
|
555
|
-
// config order below for deterministic tool names and collision suffixes.
|
|
556
502
|
const mounts = await Promise.all(
|
|
557
503
|
serverEntries.map(async ([serverName, def]) => {
|
|
558
504
|
if (!def || typeof def.command !== "string" || !def.command.trim()) return { serverName };
|
|
559
|
-
let client
|
|
505
|
+
let client;
|
|
560
506
|
try {
|
|
561
507
|
client = new StdioMcp(def);
|
|
562
508
|
await client.init();
|
|
563
509
|
return { serverName, def, client, tools: await client.listTools() };
|
|
564
510
|
} catch (err) {
|
|
565
511
|
const message = err instanceof Error ? err.message : String(err);
|
|
566
|
-
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: ${message}
|
|
512
|
+
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: ${message}
|
|
513
|
+
`);
|
|
567
514
|
client?.dispose();
|
|
568
515
|
return { serverName };
|
|
569
516
|
}
|
|
570
|
-
})
|
|
517
|
+
})
|
|
571
518
|
);
|
|
572
|
-
|
|
573
519
|
for (const mount of mounts) {
|
|
574
520
|
if (!mount.client || !mount.def || !mount.tools) continue;
|
|
575
521
|
const { serverName, def, client, tools } = mount;
|
|
576
|
-
const gated = def.scope === "local-computer";
|
|
522
|
+
const gated = def.scope === "local-computer" || def.scope === "custom" && config.approvalMode !== "full";
|
|
577
523
|
let registered = 0;
|
|
578
|
-
|
|
579
524
|
for (const tool of tools) {
|
|
580
525
|
if (!tool || typeof tool.name !== "string" || !tool.name.trim()) {
|
|
581
|
-
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: skipped a tool with no valid name
|
|
526
|
+
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}: skipped a tool with no valid name
|
|
527
|
+
`);
|
|
582
528
|
continue;
|
|
583
529
|
}
|
|
584
530
|
const toolName = tool.name;
|
|
531
|
+
const identity = { kind: "mcp", server: serverName, name: toolName };
|
|
532
|
+
if (scope !== void 0 && !allowsTool(scope, identity)) continue;
|
|
585
533
|
const name = allocateToolName(serverName, toolName, used);
|
|
586
534
|
try {
|
|
587
535
|
const parameters = toTypebox(tool.inputSchema);
|
|
@@ -591,14 +539,12 @@ export default async function (pi: PiExtensionApi): Promise<void> {
|
|
|
591
539
|
description: typeof tool.description === "string" ? tool.description : `${toolName} (MCP tool from ${serverName})`,
|
|
592
540
|
parameters,
|
|
593
541
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
594
|
-
|
|
595
|
-
// (ctx.ui.confirm → extension_ui_request → Allow/Deny card). This
|
|
596
|
-
// mirrors ACP's session/request_permission and Codex's elicitation.
|
|
542
|
+
if (scope !== void 0 && (enforcementFailed || !allowsTool(scope, identity))) throw new Error("Tool selection excludes this tool");
|
|
597
543
|
if (gated) {
|
|
598
544
|
const detail = summarizeParams(params);
|
|
599
545
|
const allowed = await ctx.ui.confirm(
|
|
600
|
-
`Allow ${toolName} on your computer?`,
|
|
601
|
-
detail || `Run ${serverName}:${toolName}
|
|
546
|
+
def.scope === "local-computer" ? `Allow ${toolName} on your computer?` : `Allow ${serverName}:${toolName}?`,
|
|
547
|
+
detail || `Run ${serverName}:${toolName}`
|
|
602
548
|
);
|
|
603
549
|
if (!allowed) {
|
|
604
550
|
return { content: [{ type: "text", text: "Blocked by the user." }], details: {} };
|
|
@@ -606,33 +552,33 @@ export default async function (pi: PiExtensionApi): Promise<void> {
|
|
|
606
552
|
}
|
|
607
553
|
const res = await client.callTool(toolName, params, signal);
|
|
608
554
|
if (res.isError) {
|
|
609
|
-
const message = res.content
|
|
610
|
-
.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
611
|
-
.map((item) => item.text)
|
|
612
|
-
.join("\n");
|
|
613
|
-
// Pi marks a custom tool as failed only when execute throws;
|
|
614
|
-
// returning error-looking text incorrectly produces isError=false.
|
|
555
|
+
const message = res.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
615
556
|
throw new Error(message || `MCP tool ${serverName}:${toolName} returned an error`);
|
|
616
557
|
}
|
|
617
558
|
return { content: res.content, details: {} };
|
|
618
|
-
}
|
|
559
|
+
}
|
|
619
560
|
});
|
|
620
561
|
used.add(name);
|
|
562
|
+
identities.set(name, identity);
|
|
621
563
|
registered += 1;
|
|
622
564
|
} catch (err) {
|
|
623
|
-
// One malformed tool must not dispose the client behind tools that
|
|
624
|
-
// were already registered from the same server.
|
|
625
565
|
const message = err instanceof Error ? err.message : String(err);
|
|
626
|
-
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}:${toolName}: ${message}
|
|
566
|
+
process.stderr.write(`[openmausbot-pi-mcp] ${serverName}:${toolName}: ${message}
|
|
567
|
+
`);
|
|
627
568
|
}
|
|
628
569
|
}
|
|
629
|
-
|
|
630
570
|
if (registered > 0) clients.push(client);
|
|
631
571
|
else client.dispose();
|
|
632
572
|
}
|
|
633
|
-
|
|
634
573
|
pi.on("session_shutdown", () => {
|
|
635
574
|
for (const c of clients) c.dispose();
|
|
636
575
|
clients.length = 0;
|
|
637
576
|
});
|
|
638
577
|
}
|
|
578
|
+
export {
|
|
579
|
+
StdioMcp,
|
|
580
|
+
allocateToolName,
|
|
581
|
+
pi_mcp_extension_default as default,
|
|
582
|
+
toTypebox,
|
|
583
|
+
truncateToolText
|
|
584
|
+
};
|