@chibitek/chibi 0.1.155 → 0.1.157
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/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-BkYbog6D.js} +1 -1
- package/dist/assets/{arc-DU5fSmKU.js → arc-QwTuMsxp.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-GdicO2PQ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-Dkr29TFF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-Dz_3dstx.js} +1 -1
- package/dist/assets/channel-D2y0i2ps.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-CGsfu-PI.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-CDqI37_6.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-DnmSuyTb.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-Dh7tXcJT.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-D_SM1ZrY.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-DmeMuTsI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-R1UT9b5f.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-DpPfU1yO.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-DlihfTkM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-BlTV6lSC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-Br1kPFWw.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-BqPp3ZMd.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-DFfs_k8-.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-Ch3xIJA7.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DIHUD87N.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-CAgQQdeF.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-9HVKodrI.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-DU2kDq_h.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-D-qTAhHD.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-C31SnI9P.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-BHrdiBVd.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DWyljRo5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-DyY59XPp.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-BevMIeAy.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-Ct18WHf4.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-yhixO1N3.js} +1 -1
- package/dist/assets/{index-CG1OvRgO.js → index-BEJ8EKQt.js} +1 -1
- package/dist/assets/index-DGWeJcck.css +1 -0
- package/dist/assets/{index-D_8SD_0K.js → index-ziyH_uer.js} +170 -168
- package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-CYa-r_S5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-CnxwM7gY.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-RKQfXBrp.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-CSlRDIF6.js} +1 -1
- package/dist/assets/{linear-DTGq6qn5.js → linear-Ch19KVyt.js} +1 -1
- package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-Bek7I_t6.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-B_LiP8su.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-DhzQeiZf.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-D-BYNKHS.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-DnCm0heP.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-COqgYnhn.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP-Dvt72ajf.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-CisC9nbp.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-BO8xqvxa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-DLk1jBzf.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-C_wutSE7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-hqo1UlEi.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-C_DJldPg.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-CsidVXMA.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-Bs-IIqn_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-CXEUyxaj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-B9qvmm4P.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +81 -81
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +2354 -1729
- package/dist-server/openmausbot.js +676 -643
- package/dist-server/pair-cli.js +676 -643
- package/dist-server/server/app-installers.js +107 -7
- package/dist-server/server/cli-help.js +74 -0
- package/dist-server/server/cli.js +32 -74
- package/dist-server/server/device-routing.js +60 -0
- package/dist-server/server/drivers/claude.js +13 -11
- package/dist-server/server/drivers/local-inject.js +3 -4
- package/dist-server/server/engine-tiers.js +5 -14
- package/dist-server/server/index.js +84 -104
- package/dist-server/server/personal-goal-runtime.js +86 -0
- package/dist-server/server/personal-goal-turn.js +125 -0
- package/dist-server/server/personal-goals.js +195 -0
- package/dist-server/server/routes/local-models.js +3 -2
- package/dist-server/server/skill-fetch.js +14 -0
- package/dist-server/shared/engine-choices.js +15 -0
- package/dist-server/shared/engine-tiers.js +74 -2
- package/dist-server/shared/local-models.js +1 -3
- package/dist-server/shared/personal-goal.js +43 -0
- package/dist-server/vps-container-mcp.js +81 -81
- package/package.json +1 -1
- package/dist/assets/channel-GBaPOaz-.js +0 -1
- package/dist/assets/index-C8YP7Zxj.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
- package/dist-server/connector-proxy.js +0 -205
- package/dist-server/server/composio.js +0 -1051
- package/dist-server/server/connector-proxy.js +0 -249
- package/dist-server/server/mcp-public-registry.js +0 -325
- package/dist-server/shared/connector-availability.js +0 -12
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// Harness-owned Composio MCP bridge.
|
|
2
|
-
//
|
|
3
|
-
// Provider CLIs only see this stdio server. Ordinary MCP traffic is relayed
|
|
4
|
-
// to the configured Composio Session, but connection requests are converted
|
|
5
|
-
// into first-class OpenMausBot chat cards. The agent never authors an auth
|
|
6
|
-
// URL and credentials never pass through its transcript.
|
|
7
|
-
//
|
|
8
|
-
// stdout is the MCP transport. Never log there.
|
|
9
|
-
import readline from "node:readline";
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
const UPSTREAM = process.env.OMB_CONNECTOR_UPSTREAM_URL ?? "";
|
|
12
|
-
const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
|
|
13
|
-
const BOT_ID = process.env.OMB_BOT_ID ?? "";
|
|
14
|
-
const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
|
|
15
|
-
const TOKEN = process.env.OMB_CONNECTOR_TOKEN ?? process.env.OMB_COMMS_TOKEN ?? "";
|
|
16
|
-
const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
|
|
17
|
-
const INITIALIZE_RELAY_TIMEOUT_MS = 1_000;
|
|
18
|
-
const RELAY_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
-
function parsedHeaders() {
|
|
20
|
-
try {
|
|
21
|
-
const value = JSON.parse(process.env.OMB_CONNECTOR_UPSTREAM_HEADERS ?? "{}");
|
|
22
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
-
return {};
|
|
24
|
-
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const upstreamHeaders = parsedHeaders();
|
|
31
|
-
let upstreamSessionId = "";
|
|
32
|
-
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
33
|
-
function textResult(id, text, isError = false) {
|
|
34
|
-
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) } };
|
|
35
|
-
}
|
|
36
|
-
function jsonRpcError(id, message) {
|
|
37
|
-
return { jsonrpc: "2.0", id, error: { code: -32000, message } };
|
|
38
|
-
}
|
|
39
|
-
function initializeResult(id, protocolVersion) {
|
|
40
|
-
return {
|
|
41
|
-
jsonrpc: "2.0",
|
|
42
|
-
id,
|
|
43
|
-
result: {
|
|
44
|
-
protocolVersion: typeof protocolVersion === "string" && protocolVersion ? protocolVersion : "2024-11-05",
|
|
45
|
-
capabilities: { tools: {} },
|
|
46
|
-
serverInfo: { name: "openmausbot-connectors", version: "1" },
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async function readBounded(response) {
|
|
51
|
-
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
52
|
-
if (declared > MAX_RESPONSE_BYTES)
|
|
53
|
-
throw new Error("connector response exceeded 20 MB");
|
|
54
|
-
if (!response.body)
|
|
55
|
-
return "";
|
|
56
|
-
const reader = response.body.getReader();
|
|
57
|
-
const decoder = new TextDecoder();
|
|
58
|
-
let size = 0;
|
|
59
|
-
let text = "";
|
|
60
|
-
for (;;) {
|
|
61
|
-
const { done, value } = await reader.read();
|
|
62
|
-
if (done)
|
|
63
|
-
break;
|
|
64
|
-
size += value.byteLength;
|
|
65
|
-
if (size > MAX_RESPONSE_BYTES) {
|
|
66
|
-
await reader.cancel();
|
|
67
|
-
throw new Error("connector response exceeded 20 MB");
|
|
68
|
-
}
|
|
69
|
-
text += decoder.decode(value, { stream: true });
|
|
70
|
-
}
|
|
71
|
-
return text + decoder.decode();
|
|
72
|
-
}
|
|
73
|
-
function parseUpstream(text, id) {
|
|
74
|
-
const trimmed = text.trim();
|
|
75
|
-
if (!trimmed)
|
|
76
|
-
return null;
|
|
77
|
-
if (trimmed.startsWith("{"))
|
|
78
|
-
return JSON.parse(trimmed);
|
|
79
|
-
const frames = trimmed
|
|
80
|
-
.split(/\r?\n/)
|
|
81
|
-
.filter((line) => line.startsWith("data:"))
|
|
82
|
-
.map((line) => line.slice(5).trim())
|
|
83
|
-
.filter((line) => line && line !== "[DONE]")
|
|
84
|
-
.flatMap((line) => {
|
|
85
|
-
try {
|
|
86
|
-
return [JSON.parse(line)];
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return frames.findLast((frame) => frame.id === id) ?? frames.at(-1) ?? null;
|
|
93
|
-
}
|
|
94
|
-
async function relay(message, timeoutMs = RELAY_TIMEOUT_MS) {
|
|
95
|
-
if (!UPSTREAM)
|
|
96
|
-
throw new Error("connected apps are unavailable");
|
|
97
|
-
const response = await fetch(UPSTREAM, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: {
|
|
100
|
-
"content-type": "application/json",
|
|
101
|
-
accept: "application/json, text/event-stream",
|
|
102
|
-
...upstreamHeaders,
|
|
103
|
-
...(upstreamSessionId ? { "mcp-session-id": upstreamSessionId } : {}),
|
|
104
|
-
},
|
|
105
|
-
body: JSON.stringify(message),
|
|
106
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
107
|
-
});
|
|
108
|
-
const nextSession = response.headers.get("mcp-session-id");
|
|
109
|
-
if (nextSession)
|
|
110
|
-
upstreamSessionId = nextSession;
|
|
111
|
-
if (!response.ok)
|
|
112
|
-
throw new Error(`connector service returned HTTP ${response.status}`);
|
|
113
|
-
return parseUpstream(await readBounded(response), message.id);
|
|
114
|
-
}
|
|
115
|
-
function connectorAdds(args) {
|
|
116
|
-
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
117
|
-
return [];
|
|
118
|
-
const toolkits = args.toolkits;
|
|
119
|
-
if (!Array.isArray(toolkits))
|
|
120
|
-
return [];
|
|
121
|
-
const seen = new Set();
|
|
122
|
-
const requests = [];
|
|
123
|
-
for (const item of toolkits) {
|
|
124
|
-
if (typeof item === "string") {
|
|
125
|
-
const slug = item.trim().toLowerCase();
|
|
126
|
-
const key = JSON.stringify([slug, ""]);
|
|
127
|
-
if (!seen.has(key)) {
|
|
128
|
-
seen.add(key);
|
|
129
|
-
requests.push({ slug });
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
134
|
-
continue;
|
|
135
|
-
const row = item;
|
|
136
|
-
const action = String(row.action ?? "add").toLowerCase();
|
|
137
|
-
if (!["add", "connect", "initiate"].includes(action))
|
|
138
|
-
continue;
|
|
139
|
-
const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
|
|
140
|
-
if (typeof slug !== "string")
|
|
141
|
-
continue;
|
|
142
|
-
const normalized = slug.trim().toLowerCase();
|
|
143
|
-
const alias = (typeof row.alias === "string" ? row.alias : typeof row.account === "string" ? row.account : "").trim();
|
|
144
|
-
const key = JSON.stringify([normalized, alias.toLowerCase()]);
|
|
145
|
-
if (seen.has(key))
|
|
146
|
-
continue;
|
|
147
|
-
seen.add(key);
|
|
148
|
-
requests.push({ slug: normalized, ...(alias ? { alias } : {}) });
|
|
149
|
-
}
|
|
150
|
-
return requests;
|
|
151
|
-
}
|
|
152
|
-
async function showConnectorCards(items) {
|
|
153
|
-
const response = await fetch(`${HARNESS}/api/internal/connectors/request`, {
|
|
154
|
-
method: "POST",
|
|
155
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
|
|
156
|
-
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID, items, resumeKey: randomUUID() }),
|
|
157
|
-
signal: AbortSignal.timeout(30_000),
|
|
158
|
-
});
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
const body = (await response.json().catch(() => ({})));
|
|
161
|
-
throw new Error(String(body.error ?? `could not show connection card (HTTP ${response.status})`));
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
async function handle(message) {
|
|
165
|
-
const id = message.id;
|
|
166
|
-
const method = String(message.method ?? "");
|
|
167
|
-
// OpenCode (and other MCP clients) mark a stdio server failed unless
|
|
168
|
-
// initialize returns capabilities/serverInfo. Relaying that handshake to
|
|
169
|
-
// Composio can time out, return a newer protocolVersion, or throw when the
|
|
170
|
-
// upstream URL never reached the child env — all of which previously
|
|
171
|
-
// surfaced as a tools/call-shaped {content,isError} payload.
|
|
172
|
-
if (method === "notifications/initialized" || method === "initialized") {
|
|
173
|
-
if (UPSTREAM)
|
|
174
|
-
void relay(message).catch(() => { });
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (method === "initialize") {
|
|
178
|
-
if (UPSTREAM) {
|
|
179
|
-
try {
|
|
180
|
-
// Capture the upstream session id when the service is healthy, but
|
|
181
|
-
// never let a stalled provider prevent the local MCP client from
|
|
182
|
-
// mounting the connector tools. The client sends initialized only
|
|
183
|
-
// after this bounded attempt and the local initialize response.
|
|
184
|
-
await relay(message, INITIALIZE_RELAY_TIMEOUT_MS);
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
// Best-effort session setup. The client still needs a valid result.
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (id !== undefined) {
|
|
191
|
-
const params = (message.params ?? {});
|
|
192
|
-
send(initializeResult(id, params.protocolVersion));
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (method === "tools/call") {
|
|
197
|
-
const params = (message.params ?? {});
|
|
198
|
-
const name = String(params.name ?? "");
|
|
199
|
-
const requests = /MANAGE_CONNECTIONS$/i.test(name) ? connectorAdds(params.arguments) : [];
|
|
200
|
-
if (requests.length) {
|
|
201
|
-
await showConnectorCards(requests);
|
|
202
|
-
const labels = requests.map((r) => (r.alias ? `${r.slug} (${r.alias})` : r.slug)).join(", ");
|
|
203
|
-
send(textResult(id, `Atomic showed the user a secure connection card for ${labels}. End this turn now. The app will continue the task automatically after the connection finishes.`));
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (/WAIT_FOR_CONNECTIONS$/i.test(name)) {
|
|
207
|
-
send(textResult(id, "Atomic is handling connection completion and will continue the task automatically."));
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
const response = await relay(message);
|
|
213
|
-
if (response && id !== undefined)
|
|
214
|
-
send(response);
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
if (id === undefined)
|
|
218
|
-
return;
|
|
219
|
-
const messageText = error instanceof Error ? error.message : String(error);
|
|
220
|
-
if (method === "tools/call")
|
|
221
|
-
send(textResult(id, messageText, true));
|
|
222
|
-
else
|
|
223
|
-
send(jsonRpcError(id, messageText));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const input = readline.createInterface({ input: process.stdin, terminal: false });
|
|
227
|
-
input.on("line", (line) => {
|
|
228
|
-
const trimmed = line.trim();
|
|
229
|
-
if (!trimmed)
|
|
230
|
-
return;
|
|
231
|
-
let message;
|
|
232
|
-
try {
|
|
233
|
-
message = JSON.parse(trimmed);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
void handle(message).catch((error) => {
|
|
239
|
-
if (message.id === undefined)
|
|
240
|
-
return;
|
|
241
|
-
const method = String(message.method ?? "");
|
|
242
|
-
const messageText = error instanceof Error ? error.message : String(error);
|
|
243
|
-
if (method === "tools/call")
|
|
244
|
-
send(textResult(message.id, messageText, true));
|
|
245
|
-
else
|
|
246
|
-
send(jsonRpcError(message.id, messageText));
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
input.on("close", () => process.exit(0));
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
// The Plugins window's MCP Marketplace, backed by the official MCP Registry
|
|
2
|
-
// (https://registry.modelcontextprotocol.io, public, no key). This module
|
|
3
|
-
// fetches and normalizes registry pages and turns one entry, or a featured
|
|
4
|
-
// service from shared/mcp-featured.ts, into a config.json `mcpServers` entry.
|
|
5
|
-
// Adding goes through the same rules as every other MCP server: the entry is
|
|
6
|
-
// saved switched off, secrets are kept in its env or headers like any other
|
|
7
|
-
// server's, and nothing here logs a value.
|
|
8
|
-
import { featuredMcpIcon, featuredMcpServer, MCP_FEATURED } from "../shared/mcp-featured.js";
|
|
9
|
-
import { slugMcpName } from "./mcp-registry.js";
|
|
10
|
-
export const MCP_REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
11
|
-
/** The OAuth bridge Atomic already runs remote servers through (see
|
|
12
|
-
* electron/chibitek-defaults.mjs MCP_REMOTE_PACKAGE; a test keeps them equal).
|
|
13
|
-
* Pinned so its saved sign-ins survive a new mcp-remote release. */
|
|
14
|
-
export const MCP_REMOTE_PACKAGE = "mcp-remote@0.14.3";
|
|
15
|
-
const CACHE_MS = 60 * 60 * 1000;
|
|
16
|
-
const CACHE_MAX = 200;
|
|
17
|
-
const TIMEOUT_MS = 8_000;
|
|
18
|
-
const PAGE_SIZE = 30;
|
|
19
|
-
const OFFICIAL = "io.modelcontextprotocol.registry/official";
|
|
20
|
-
export const REGISTRY_OFFLINE_MESSAGE = "The MCP Registry could not be reached. Check your internet connection and try again.";
|
|
21
|
-
export class RegistryUnavailableError extends Error {
|
|
22
|
-
constructor() {
|
|
23
|
-
super(REGISTRY_OFFLINE_MESSAGE);
|
|
24
|
-
this.name = "RegistryUnavailableError";
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function record(value) {
|
|
28
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
29
|
-
}
|
|
30
|
-
function text(value) {
|
|
31
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
32
|
-
}
|
|
33
|
-
function httpsUrl(value) {
|
|
34
|
-
const raw = text(value);
|
|
35
|
-
if (!raw)
|
|
36
|
-
return undefined;
|
|
37
|
-
try {
|
|
38
|
-
const url = new URL(raw);
|
|
39
|
-
return url.protocol === "https:" ? url.toString() : undefined;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const featuredByRegistryName = new Map(MCP_FEATURED.filter((server) => server.registryName).map((server) => [server.registryName, server]));
|
|
46
|
-
/** "io.github.acme/thing" -> "acme", "com.notion/mcp" -> "notion.com". */
|
|
47
|
-
export function publisherFor(name) {
|
|
48
|
-
const namespace = name.split("/")[0] ?? name;
|
|
49
|
-
if (namespace.startsWith("io.github."))
|
|
50
|
-
return namespace.slice("io.github.".length);
|
|
51
|
-
return namespace.split(".").reverse().join(".");
|
|
52
|
-
}
|
|
53
|
-
/** "notion-mcp-server" -> "Notion Mcp Server" when the entry has no title. */
|
|
54
|
-
function titleFor(name) {
|
|
55
|
-
const leaf = name.split("/").pop() ?? name;
|
|
56
|
-
return leaf.split(/[-_.]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ") || name;
|
|
57
|
-
}
|
|
58
|
-
function githubOwner(repositoryUrl) {
|
|
59
|
-
if (!repositoryUrl)
|
|
60
|
-
return undefined;
|
|
61
|
-
const match = /^https:\/\/github\.com\/([A-Za-z0-9-]{1,39})(?:\/|$)/.exec(repositoryUrl);
|
|
62
|
-
return match?.[1];
|
|
63
|
-
}
|
|
64
|
-
function input(kind, raw) {
|
|
65
|
-
const item = record(raw);
|
|
66
|
-
const name = text(item.name);
|
|
67
|
-
if (!name)
|
|
68
|
-
return null;
|
|
69
|
-
const description = text(item.description);
|
|
70
|
-
return {
|
|
71
|
-
kind,
|
|
72
|
-
name,
|
|
73
|
-
...(description ? { description: description.slice(0, 300) } : {}),
|
|
74
|
-
required: item.isRequired === true,
|
|
75
|
-
secret: item.isSecret === true,
|
|
76
|
-
...(kind === "header" && text(item.value) ? { template: text(item.value) } : {}),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function remotes(raw) {
|
|
80
|
-
if (!Array.isArray(raw))
|
|
81
|
-
return [];
|
|
82
|
-
return raw.flatMap((entry) => {
|
|
83
|
-
const item = record(entry);
|
|
84
|
-
const url = httpsUrl(item.url);
|
|
85
|
-
// A URL with {placeholders} needs per-person values Atomic cannot guess.
|
|
86
|
-
if (!url || /[{}]|%7B|%7D/i.test(url))
|
|
87
|
-
return [];
|
|
88
|
-
const type = item.type === "sse" ? "sse" : item.type === "streamable-http" ? "http" : null;
|
|
89
|
-
if (!type)
|
|
90
|
-
return [];
|
|
91
|
-
const headers = Array.isArray(item.headers) ? item.headers.map((header) => input("header", header)).filter((header) => header !== null) : [];
|
|
92
|
-
return [{ type, url, headers }];
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function packages(raw) {
|
|
96
|
-
if (!Array.isArray(raw))
|
|
97
|
-
return [];
|
|
98
|
-
return raw.flatMap((entry) => {
|
|
99
|
-
const item = record(entry);
|
|
100
|
-
const registryType = text(item.registryType);
|
|
101
|
-
const identifier = text(item.identifier);
|
|
102
|
-
if (!registryType || !identifier)
|
|
103
|
-
return [];
|
|
104
|
-
const env = Array.isArray(item.environmentVariables)
|
|
105
|
-
? item.environmentVariables.map((variable) => input("env", variable)).filter((variable) => variable !== null)
|
|
106
|
-
: [];
|
|
107
|
-
return [{
|
|
108
|
-
registryType,
|
|
109
|
-
identifier,
|
|
110
|
-
...(text(item.version) ? { version: text(item.version) } : {}),
|
|
111
|
-
...(text(record(item.transport).type) ? { transport: text(record(item.transport).type) } : {}),
|
|
112
|
-
...(text(item.runtimeHint) ? { runtimeHint: text(item.runtimeHint) } : {}),
|
|
113
|
-
env,
|
|
114
|
-
}];
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
const SAFE_PACKAGE = /^(@[a-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*$/;
|
|
118
|
-
const SAFE_VERSION = /^[\w.+-]{1,64}$/;
|
|
119
|
-
/** Remote first (nothing to install), then npm, then PyPI. A remote whose
|
|
120
|
-
* headers are all optional is assumed to sign in with OAuth, so it runs
|
|
121
|
-
* through the mcp-remote bridge; one that requires a header (an API key) is
|
|
122
|
-
* kept as a plain URL entry with that header. */
|
|
123
|
-
function installPlan(server) {
|
|
124
|
-
const remote = server.remotes.find((item) => item.type === "http") ?? server.remotes.find((item) => item.type === "sse");
|
|
125
|
-
if (remote)
|
|
126
|
-
return { kind: "remote", remote, oauth: !remote.headers.some((header) => header.required) };
|
|
127
|
-
const stdio = (item) => !item.transport || item.transport === "stdio";
|
|
128
|
-
const npm = server.packages.find((item) => item.registryType === "npm" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
|
|
129
|
-
if (npm)
|
|
130
|
-
return { kind: "npm", pkg: npm };
|
|
131
|
-
const pypi = server.packages.find((item) => item.registryType === "pypi" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
|
|
132
|
-
if (pypi)
|
|
133
|
-
return { kind: "pypi", pkg: pypi };
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
function planInputs(plan) {
|
|
137
|
-
if (!plan)
|
|
138
|
-
return [];
|
|
139
|
-
const list = plan.kind === "remote"
|
|
140
|
-
? plan.oauth ? [] : plan.remote.headers.map(({ template: _template, ...header }) => header)
|
|
141
|
-
: plan.pkg.env;
|
|
142
|
-
return [...list].sort((a, b) => Number(b.required) - Number(a.required));
|
|
143
|
-
}
|
|
144
|
-
/** One registry list entry to the shape the renderer shows, or null when it
|
|
145
|
-
* is not the latest version or not usable at all. */
|
|
146
|
-
export function normalizeRegistryEntry(raw) {
|
|
147
|
-
const entry = record(raw);
|
|
148
|
-
const server = record(entry.server);
|
|
149
|
-
const meta = record(record(entry._meta)[OFFICIAL]);
|
|
150
|
-
if (meta.isLatest === false)
|
|
151
|
-
return null;
|
|
152
|
-
if (typeof meta.status === "string" && meta.status !== "active")
|
|
153
|
-
return null;
|
|
154
|
-
const name = text(server.name);
|
|
155
|
-
if (!name)
|
|
156
|
-
return null;
|
|
157
|
-
const repositoryUrl = httpsUrl(record(server.repository).url);
|
|
158
|
-
const websiteUrl = httpsUrl(server.websiteUrl);
|
|
159
|
-
const featured = featuredByRegistryName.get(name);
|
|
160
|
-
const icons = Array.isArray(server.icons) ? server.icons : [];
|
|
161
|
-
const registryIcon = icons.map((icon) => httpsUrl(record(icon).src)).find(Boolean);
|
|
162
|
-
const owner = githubOwner(repositoryUrl);
|
|
163
|
-
const iconUrl = featured
|
|
164
|
-
? featuredMcpIcon(featured.id)
|
|
165
|
-
: registryIcon ?? (owner ? `https://github.com/${owner}.png?size=64` : undefined);
|
|
166
|
-
const normalized = {
|
|
167
|
-
id: name,
|
|
168
|
-
name,
|
|
169
|
-
title: text(server.title) ?? featured?.name ?? titleFor(name),
|
|
170
|
-
description: (text(server.description) ?? "").slice(0, 400),
|
|
171
|
-
publisher: publisherFor(name),
|
|
172
|
-
version: text(server.version) ?? "",
|
|
173
|
-
...(repositoryUrl ? { repositoryUrl } : {}),
|
|
174
|
-
...(websiteUrl ? { websiteUrl } : {}),
|
|
175
|
-
remotes: remotes(server.remotes),
|
|
176
|
-
packages: packages(server.packages),
|
|
177
|
-
...(iconUrl ? { iconUrl } : {}),
|
|
178
|
-
...(featured ? { featured: featured.id } : {}),
|
|
179
|
-
};
|
|
180
|
-
const plan = installPlan(normalized);
|
|
181
|
-
return { ...normalized, install: plan?.kind ?? null, inputs: planInputs(plan) };
|
|
182
|
-
}
|
|
183
|
-
/** Normalize a page: latest versions only, one card per server name. */
|
|
184
|
-
export function normalizeRegistryPage(body) {
|
|
185
|
-
const page = record(body);
|
|
186
|
-
const seen = new Set();
|
|
187
|
-
const servers = [];
|
|
188
|
-
for (const raw of Array.isArray(page.servers) ? page.servers : []) {
|
|
189
|
-
const server = normalizeRegistryEntry(raw);
|
|
190
|
-
if (!server || seen.has(server.id))
|
|
191
|
-
continue;
|
|
192
|
-
seen.add(server.id);
|
|
193
|
-
servers.push(server);
|
|
194
|
-
}
|
|
195
|
-
const nextCursor = text(record(page.metadata).nextCursor);
|
|
196
|
-
return { servers, ...(nextCursor ? { nextCursor } : {}) };
|
|
197
|
-
}
|
|
198
|
-
/** A registry client with a one-hour in-memory cache. Every failure (offline,
|
|
199
|
-
* timeout, a registry error) becomes RegistryUnavailableError. */
|
|
200
|
-
export function createMcpPublicRegistry(options = {}) {
|
|
201
|
-
const doFetch = options.fetch ?? fetch;
|
|
202
|
-
const base = options.base ?? MCP_REGISTRY_BASE;
|
|
203
|
-
const now = options.now ?? Date.now;
|
|
204
|
-
const cache = new Map();
|
|
205
|
-
async function getJson(url) {
|
|
206
|
-
const hit = cache.get(url);
|
|
207
|
-
if (hit && now() - hit.at < CACHE_MS)
|
|
208
|
-
return hit.value;
|
|
209
|
-
let response;
|
|
210
|
-
try {
|
|
211
|
-
response = await doFetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
throw new RegistryUnavailableError();
|
|
215
|
-
}
|
|
216
|
-
if (response.status === 404)
|
|
217
|
-
return null;
|
|
218
|
-
if (!response.ok)
|
|
219
|
-
throw new RegistryUnavailableError();
|
|
220
|
-
let value;
|
|
221
|
-
try {
|
|
222
|
-
value = await response.json();
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
throw new RegistryUnavailableError();
|
|
226
|
-
}
|
|
227
|
-
if (cache.size >= CACHE_MAX)
|
|
228
|
-
cache.delete(cache.keys().next().value);
|
|
229
|
-
cache.set(url, { at: now(), value });
|
|
230
|
-
return value;
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
async search({ search, cursor }) {
|
|
234
|
-
const params = new URLSearchParams({ limit: String(PAGE_SIZE), version: "latest" });
|
|
235
|
-
const query = search?.trim().slice(0, 100);
|
|
236
|
-
if (query)
|
|
237
|
-
params.set("search", query);
|
|
238
|
-
if (cursor)
|
|
239
|
-
params.set("cursor", cursor.slice(0, 300));
|
|
240
|
-
return normalizeRegistryPage(await getJson(`${base}/v0/servers?${params}`));
|
|
241
|
-
},
|
|
242
|
-
async get(id) {
|
|
243
|
-
const body = await getJson(`${base}/v0/servers/${encodeURIComponent(id)}/versions/latest`);
|
|
244
|
-
return body === null ? null : normalizeRegistryEntry(body);
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
/** A free config name from the server's own name: "notion", then "notion-2". */
|
|
249
|
-
export function freeMcpName(preferred, taken) {
|
|
250
|
-
const base = slugMcpName(preferred) || "mcp-server";
|
|
251
|
-
if (!taken(base))
|
|
252
|
-
return base;
|
|
253
|
-
for (let n = 2; n < 100; n += 1) {
|
|
254
|
-
const candidate = `${base.slice(0, 32 - String(n).length - 1)}-${n}`;
|
|
255
|
-
if (!taken(candidate))
|
|
256
|
-
return candidate;
|
|
257
|
-
}
|
|
258
|
-
return `${base.slice(0, 23)}-${Date.now().toString(36).slice(-8)}`;
|
|
259
|
-
}
|
|
260
|
-
function bridgeEntry(url) {
|
|
261
|
-
return { command: "npx", args: ["-y", MCP_REMOTE_PACKAGE, url], env: {} };
|
|
262
|
-
}
|
|
263
|
-
function cleanValues(values) {
|
|
264
|
-
const out = {};
|
|
265
|
-
for (const [key, value] of Object.entries(record(values))) {
|
|
266
|
-
if (typeof value === "string" && value.trim())
|
|
267
|
-
out[key] = value.trim();
|
|
268
|
-
}
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
/** The config entry a featured service becomes: its official URL through the
|
|
272
|
-
* OAuth bridge. */
|
|
273
|
-
export function planFeaturedAdd(featured, taken) {
|
|
274
|
-
return { ok: true, name: freeMcpName(featured.id, taken), entry: bridgeEntry(featured.url), kind: "remote", oauth: true };
|
|
275
|
-
}
|
|
276
|
-
/** The config entry a registry server becomes, with the person's values for
|
|
277
|
-
* its inputs. Required inputs that are missing are refused with the list, so
|
|
278
|
-
* the renderer can ask for them. */
|
|
279
|
-
export function planRegistryAdd(server, rawValues, taken) {
|
|
280
|
-
const values = cleanValues(rawValues);
|
|
281
|
-
const plan = installPlan(server);
|
|
282
|
-
if (!plan) {
|
|
283
|
-
const docker = server.packages.some((item) => item.registryType === "oci" || item.registryType === "docker");
|
|
284
|
-
return {
|
|
285
|
-
ok: false,
|
|
286
|
-
status: 400,
|
|
287
|
-
code: "unsupported",
|
|
288
|
-
error: docker
|
|
289
|
-
? `${server.title} only ships as a Docker image, which Atomic can't add for you yet. Add it by hand under MCP servers.`
|
|
290
|
-
: `${server.title} doesn't offer a remote address or an npm or PyPI package, so Atomic can't add it for you. Add it by hand under MCP servers.`,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
const inputs = planInputs(plan);
|
|
294
|
-
const missing = inputs.filter((item) => item.required && !values[item.name]);
|
|
295
|
-
if (missing.length) {
|
|
296
|
-
return { ok: false, status: 400, code: "needs_input", inputs, error: `${server.title} needs ${missing.map((item) => item.name).join(", ")} before it can be added.` };
|
|
297
|
-
}
|
|
298
|
-
const name = freeMcpName(server.featured ?? server.name.split("/").pop() ?? server.title, taken);
|
|
299
|
-
if (plan.kind === "remote") {
|
|
300
|
-
if (plan.oauth)
|
|
301
|
-
return { ok: true, name, entry: bridgeEntry(plan.remote.url), kind: "remote", oauth: true };
|
|
302
|
-
const headers = {};
|
|
303
|
-
for (const header of plan.remote.headers) {
|
|
304
|
-
const value = values[header.name];
|
|
305
|
-
if (!value)
|
|
306
|
-
continue;
|
|
307
|
-
// "Bearer {api_key}" -> "Bearer <value>" unless the value already is the whole header.
|
|
308
|
-
headers[header.name] = header.template && /\{[^}]+\}/.test(header.template) && !/\s/.test(value)
|
|
309
|
-
? header.template.replace(/\{[^}]+\}/, value)
|
|
310
|
-
: value;
|
|
311
|
-
}
|
|
312
|
-
return { ok: true, name, entry: { type: plan.remote.type, url: plan.remote.url, headers }, kind: "remote", oauth: false };
|
|
313
|
-
}
|
|
314
|
-
const env = {};
|
|
315
|
-
for (const variable of plan.pkg.env) {
|
|
316
|
-
if (values[variable.name])
|
|
317
|
-
env[variable.name] = values[variable.name];
|
|
318
|
-
}
|
|
319
|
-
const version = plan.pkg.version && SAFE_VERSION.test(plan.pkg.version) ? plan.pkg.version : undefined;
|
|
320
|
-
if (plan.kind === "npm") {
|
|
321
|
-
return { ok: true, name, entry: { command: "npx", args: ["-y", version ? `${plan.pkg.identifier}@${version}` : plan.pkg.identifier], env }, kind: "npm", oauth: false };
|
|
322
|
-
}
|
|
323
|
-
return { ok: true, name, entry: { command: "uvx", args: [plan.pkg.identifier], env }, kind: "pypi", oauth: false };
|
|
324
|
-
}
|
|
325
|
-
export { featuredMcpServer };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const MANAGED_CUSTOM_AUTH_TOOLKITS = new Set(["twitter", "x"]);
|
|
2
|
-
/**
|
|
3
|
-
* Some Composio toolkits no longer include provider-managed credentials.
|
|
4
|
-
* The official OpenMausBot broker cannot offer those connections until its
|
|
5
|
-
* project owns the corresponding OAuth app. Self-hosted Composio projects can
|
|
6
|
-
* still supply their own auth config, so keep this restriction mode-specific.
|
|
7
|
-
*/
|
|
8
|
-
export function managedConnectorUnavailableReason(mode, slug) {
|
|
9
|
-
if (mode !== "managed" || !MANAGED_CUSTOM_AUTH_TOOLKITS.has(slug.trim().toLowerCase()))
|
|
10
|
-
return null;
|
|
11
|
-
return "Twitter/X needs your own X Developer app and Composio auth config. Use self-hosted connected apps for now.";
|
|
12
|
-
}
|