@sideboard-ai/core 0.1.311 → 0.1.313

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.
@@ -5,10 +5,10 @@ import {
5
5
  brightsyMcpServerName,
6
6
  ensureCliTeamTracked,
7
7
  ensureConnectedBrightsyTeamTokens
8
- } from "./chunk-42JGPPVS.js";
8
+ } from "./chunk-V5BTGQ6M.js";
9
9
  import {
10
10
  loadBrightsyConfig
11
- } from "./chunk-PXMLMBMI.js";
11
+ } from "./chunk-E63AM3PW.js";
12
12
  import {
13
13
  httpFetch
14
14
  } from "./chunk-2GPQZZLY.js";
@@ -447,6 +447,9 @@ var brightsyAdapter = {
447
447
 
448
448
  // src/agents/claude.ts
449
449
  import { existsSync as existsSync11 } from "fs";
450
+
451
+ // src/mcp-ui/host.ts
452
+ import { createHash } from "crypto";
450
453
  import { homedir as homedir5 } from "os";
451
454
 
452
455
  // src/agents/orch-mcp-isolation.ts
@@ -458,6 +461,20 @@ import { homedir } from "os";
458
461
  import { isAbsolute, join, relative, resolve } from "path";
459
462
 
460
463
  // src/mcp-ui/meta.ts
464
+ var HTML_MAX = 15e5;
465
+ function matchClaudeMcpTool(toolName, serverNames) {
466
+ const name = toolName.trim();
467
+ if (!name.startsWith("mcp__")) return null;
468
+ const ranked = [...serverNames].filter((s) => s.trim()).sort((a, b) => b.length - a.length);
469
+ for (const server of ranked) {
470
+ const prefix = `mcp__${server}__`;
471
+ if (!name.startsWith(prefix)) continue;
472
+ const tool = name.slice(prefix.length).trim();
473
+ if (!tool) continue;
474
+ return { server, tool };
475
+ }
476
+ return null;
477
+ }
461
478
  function parseMcpServerLaunch(entry) {
462
479
  const command = typeof entry.command === "string" ? entry.command.trim() : "";
463
480
  if (command) {
@@ -489,6 +506,105 @@ function parseMcpServerLaunch(entry) {
489
506
  ...headers ? { headers } : {}
490
507
  };
491
508
  }
509
+ function uiResourceUriFromMeta(meta) {
510
+ const rec = asRecord(meta);
511
+ const nested = asRecord(rec.ui);
512
+ const fromNested = typeof nested.resourceUri === "string" ? nested.resourceUri.trim() : "";
513
+ const legacy = typeof rec["ui/resourceUri"] === "string" ? rec["ui/resourceUri"].trim() : "";
514
+ const uri = fromNested || legacy;
515
+ return uri.startsWith("ui://") ? uri : null;
516
+ }
517
+ function cspFromMeta(meta) {
518
+ const ui = asRecord(asRecord(meta).ui);
519
+ const csp = asRecord(ui.csp);
520
+ const connectDomains = originList(csp.connectDomains);
521
+ const resourceDomains = originList(csp.resourceDomains);
522
+ const frameDomains = originList(csp.frameDomains);
523
+ const baseUriDomains = originList(csp.baseUriDomains);
524
+ if (!connectDomains?.length && !resourceDomains?.length && !frameDomains?.length && !baseUriDomains?.length) {
525
+ return void 0;
526
+ }
527
+ return {
528
+ ...connectDomains?.length ? { connectDomains } : {},
529
+ ...resourceDomains?.length ? { resourceDomains } : {},
530
+ ...frameDomains?.length ? { frameDomains } : {},
531
+ ...baseUriDomains?.length ? { baseUriDomains } : {}
532
+ };
533
+ }
534
+ function htmlFromResourceContent(content) {
535
+ const uri = content.uri?.trim() ?? "";
536
+ if (!uri.startsWith("ui://")) return null;
537
+ const mimeType = content.mimeType?.trim() || "text/html";
538
+ if (!isHtmlMime(mimeType)) return null;
539
+ const html = htmlText(content.text, content.blob);
540
+ if (!html) return null;
541
+ return { uri, mimeType, html };
542
+ }
543
+ function mcpUiContentSecurityPolicy(csp) {
544
+ const resource = (csp?.resourceDomains ?? []).join(" ");
545
+ const connect = (csp?.connectDomains ?? []).join(" ");
546
+ const frames = csp?.frameDomains?.length ? csp.frameDomains.join(" ") : "'none'";
547
+ const base = csp?.baseUriDomains?.length ? csp.baseUriDomains.join(" ") : "'none'";
548
+ const connectSrc = connect ? `'self' ${connect}`.trim() : "'none'";
549
+ return [
550
+ "default-src 'none'",
551
+ `script-src 'self' 'unsafe-inline' ${resource}`.trim(),
552
+ `style-src 'self' 'unsafe-inline' ${resource}`.trim(),
553
+ `img-src 'self' data: ${resource}`.trim(),
554
+ `font-src 'self' ${resource}`.trim(),
555
+ `connect-src ${connectSrc}`,
556
+ `media-src 'self' data: ${resource}`.trim(),
557
+ `frame-src ${frames}`,
558
+ `base-uri ${base}`,
559
+ "object-src 'none'"
560
+ ].join("; ");
561
+ }
562
+ function htmlText(text, blob) {
563
+ if (typeof text === "string" && text.trim()) {
564
+ return text.length <= HTML_MAX ? text : null;
565
+ }
566
+ if (typeof blob !== "string" || !blob.trim()) return null;
567
+ try {
568
+ const decoded = decodeBase64(blob.trim());
569
+ if (!decoded.trim() || decoded.length > HTML_MAX) return null;
570
+ return decoded;
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ function decodeBase64(blob) {
576
+ if (typeof Buffer !== "undefined") return Buffer.from(blob, "base64").toString("utf8");
577
+ const bin = atob(blob);
578
+ const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
579
+ return new TextDecoder().decode(bytes);
580
+ }
581
+ function isHtmlMime(mime) {
582
+ const base = mime.split(";")[0]?.trim().toLowerCase() ?? "";
583
+ return base === "text/html";
584
+ }
585
+ function originList(value) {
586
+ if (!Array.isArray(value)) return void 0;
587
+ const out = [];
588
+ for (const item of value) {
589
+ if (typeof item !== "string") continue;
590
+ const origin = originToken(item);
591
+ if (origin && !out.includes(origin)) out.push(origin);
592
+ }
593
+ return out;
594
+ }
595
+ function originToken(value) {
596
+ const trimmed = value.trim();
597
+ if (/^(https?|wss?):\/\/\*\.[a-z0-9.-]+(?::\d+)?$/i.test(trimmed)) return trimmed;
598
+ try {
599
+ const url = new URL(trimmed);
600
+ if (url.protocol !== "https:" && url.protocol !== "http:" && url.protocol !== "wss:" && url.protocol !== "ws:") {
601
+ return null;
602
+ }
603
+ return url.origin;
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
492
608
  function stringRecord(value) {
493
609
  const rec = asRecord(value);
494
610
  const out = {};
@@ -1620,6 +1736,492 @@ function toCodexUnattendedAppsArgs() {
1620
1736
  ];
1621
1737
  }
1622
1738
 
1739
+ // src/mcp-ui/proxy-http.ts
1740
+ import { randomBytes, randomUUID } from "crypto";
1741
+ import { createServer } from "http";
1742
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
1743
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1744
+
1745
+ // src/mcp-ui/proxy-server.ts
1746
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1747
+ import {
1748
+ CallToolRequestSchema,
1749
+ GetPromptRequestSchema,
1750
+ ListPromptsRequestSchema,
1751
+ ListResourcesRequestSchema,
1752
+ ListResourceTemplatesRequestSchema,
1753
+ ListToolsRequestSchema,
1754
+ ReadResourceRequestSchema
1755
+ } from "@modelcontextprotocol/sdk/types.js";
1756
+
1757
+ // src/mcp-ui/visibility.ts
1758
+ function visibilityFromMeta(meta) {
1759
+ const rec = asRecord2(meta);
1760
+ const ui = asRecord2(rec.ui);
1761
+ const raw = ui.visibility;
1762
+ if (!Array.isArray(raw)) return ["model", "app"];
1763
+ const out = [];
1764
+ for (const item of raw) {
1765
+ if (item !== "model" && item !== "app") continue;
1766
+ if (!out.includes(item)) out.push(item);
1767
+ }
1768
+ return out.length ? out : ["model", "app"];
1769
+ }
1770
+ function toolVisibleTo(visibility, audience) {
1771
+ const vis = visibility?.length ? visibility : ["model", "app"];
1772
+ return vis.includes(audience);
1773
+ }
1774
+ function asRecord2(value) {
1775
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1776
+ return value;
1777
+ }
1778
+
1779
+ // src/mcp-ui/proxy-server.ts
1780
+ function attachMcpUiProxyHandlers(server, session) {
1781
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1782
+ const tools = await session.listTools();
1783
+ return { tools: tools.filter((tool) => toolVisibleTo(tool.visibility, "model")).map(listedTool) };
1784
+ });
1785
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1786
+ const name = request.params.name;
1787
+ const tools = await session.listTools();
1788
+ const tool = tools.find((item) => item.name === name);
1789
+ if (!tool || !toolVisibleTo(tool.visibility, "model")) {
1790
+ throw new Error(`Tool ${name} is not available to the model`);
1791
+ }
1792
+ return await session.callTool(name, request.params.arguments ?? {});
1793
+ });
1794
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1795
+ const resource = await session.readResource(request.params.uri);
1796
+ if (!resource) throw new Error(`Resource ${request.params.uri} was empty`);
1797
+ return {
1798
+ contents: [
1799
+ {
1800
+ uri: resource.uri,
1801
+ ...resource.mimeType ? { mimeType: resource.mimeType } : {},
1802
+ ...resource.text != null ? { text: resource.text } : {},
1803
+ ...resource.blob != null ? { blob: resource.blob } : {}
1804
+ }
1805
+ ]
1806
+ };
1807
+ });
1808
+ server.setRequestHandler(ListResourcesRequestSchema, async () => session.listResources());
1809
+ server.setRequestHandler(
1810
+ ListResourceTemplatesRequestSchema,
1811
+ async () => session.listResourceTemplates()
1812
+ );
1813
+ server.setRequestHandler(ListPromptsRequestSchema, async () => session.listPrompts());
1814
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1815
+ return await session.getPrompt(request.params.name, request.params.arguments);
1816
+ });
1817
+ server.fallbackRequestHandler = async (request) => {
1818
+ return await session.forward(request.method, request.params);
1819
+ };
1820
+ }
1821
+ function listedTool(tool) {
1822
+ return {
1823
+ name: tool.name,
1824
+ ...tool.description ? { description: tool.description } : {},
1825
+ inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
1826
+ _meta: {
1827
+ ui: {
1828
+ ...tool.resourceUri ? { resourceUri: tool.resourceUri } : {},
1829
+ visibility: tool.visibility
1830
+ }
1831
+ }
1832
+ };
1833
+ }
1834
+
1835
+ // src/mcp-ui/proxy-http.ts
1836
+ async function listenMcpUiProxy(opts) {
1837
+ const token = randomBytes(24).toString("hex");
1838
+ const transports = /* @__PURE__ */ new Map();
1839
+ const httpServer = createServer((req, res) => {
1840
+ void handle(req, res);
1841
+ });
1842
+ await new Promise((resolve2, reject) => {
1843
+ httpServer.once("error", reject);
1844
+ httpServer.listen(0, "127.0.0.1", () => resolve2());
1845
+ });
1846
+ const addr = httpServer.address();
1847
+ if (!addr || typeof addr === "string") throw new Error("mcp-ui proxy failed to bind");
1848
+ const origin = `http://127.0.0.1:${addr.port}`;
1849
+ async function handle(req, res) {
1850
+ if (req.headers.authorization !== `Bearer ${token}`) {
1851
+ res.writeHead(401).end();
1852
+ return;
1853
+ }
1854
+ const target = parseMcpUiProxyPath(req.url);
1855
+ if (!target) {
1856
+ res.writeHead(404).end();
1857
+ return;
1858
+ }
1859
+ try {
1860
+ const session = await opts.sessionFor(target.server, target.dirsKey);
1861
+ if (!session) {
1862
+ res.writeHead(404).end();
1863
+ return;
1864
+ }
1865
+ const header = req.headers["mcp-session-id"];
1866
+ const sid = typeof header === "string" ? header : "";
1867
+ const sessionKey = `${target.server}:${target.dirsKey}`;
1868
+ let transport = sid ? transports.get(`${sessionKey}:${sid}`) : void 0;
1869
+ if (!transport) {
1870
+ if (req.method !== "POST") {
1871
+ res.writeHead(400).end();
1872
+ return;
1873
+ }
1874
+ const created = new StreamableHTTPServerTransport({
1875
+ sessionIdGenerator: () => randomUUID(),
1876
+ allowedHosts: ["127.0.0.1", "localhost"],
1877
+ onsessioninitialized: (id) => {
1878
+ transports.set(`${sessionKey}:${id}`, created);
1879
+ },
1880
+ onsessionclosed: (id) => {
1881
+ transports.delete(`${sessionKey}:${id}`);
1882
+ }
1883
+ });
1884
+ transport = created;
1885
+ const mcp = new Server2(
1886
+ { name: `sideboard-ui-proxy-${target.server}`, version: "1.0.0" },
1887
+ { capabilities: session.capabilities() }
1888
+ );
1889
+ attachMcpUiProxyHandlers(mcp, session);
1890
+ await mcp.connect(created);
1891
+ }
1892
+ await transport.handleRequest(req, res);
1893
+ } catch {
1894
+ if (!res.headersSent) res.writeHead(500).end();
1895
+ }
1896
+ }
1897
+ return {
1898
+ urlFor(server, dirsKey) {
1899
+ const base = `${origin}/s/${encodeURIComponent(server)}`;
1900
+ return dirsKey ? `${base}/d/${dirsKey}` : base;
1901
+ },
1902
+ headers: { Authorization: `Bearer ${token}` },
1903
+ async close() {
1904
+ await Promise.all([...transports.values()].map((item) => item.close().catch(() => void 0)));
1905
+ transports.clear();
1906
+ await new Promise((resolve2) => httpServer.close(() => resolve2()));
1907
+ }
1908
+ };
1909
+ }
1910
+ function parseMcpUiProxyPath(url) {
1911
+ const path = (url ?? "").split("?")[0] ?? "";
1912
+ if (!path.startsWith("/s/")) return null;
1913
+ const rest = path.slice(3).replace(/\/$/, "");
1914
+ const split = rest.indexOf("/d/");
1915
+ try {
1916
+ if (split < 0) {
1917
+ const server2 = decodeURIComponent(rest).trim();
1918
+ return server2 ? { server: server2, dirsKey: "" } : null;
1919
+ }
1920
+ const server = decodeURIComponent(rest.slice(0, split)).trim();
1921
+ const dirsKey = rest.slice(split + 3);
1922
+ if (!server || !/^[a-f0-9]{16}$/.test(dirsKey)) return null;
1923
+ return { server, dirsKey };
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ }
1928
+
1929
+ // src/mcp-ui/session.ts
1930
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1931
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1932
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
1933
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1934
+ import { ResultSchema } from "@modelcontextprotocol/sdk/types.js";
1935
+ var REQUEST_TIMEOUT_MS = 2e4;
1936
+ async function orEmpty(run2, fallback) {
1937
+ try {
1938
+ return await run2();
1939
+ } catch {
1940
+ return fallback;
1941
+ }
1942
+ }
1943
+ var UI_CAPABILITIES = {
1944
+ extensions: {
1945
+ "io.modelcontextprotocol/ui": {
1946
+ mimeTypes: ["text/html;profile=mcp-app"]
1947
+ }
1948
+ }
1949
+ };
1950
+ async function openMcpUiSession(launch) {
1951
+ return openMcpUiSessionFromTransport(transportForLaunch(launch));
1952
+ }
1953
+ async function openMcpUiSessionFromTransport(transport) {
1954
+ const client = new Client(
1955
+ { name: "sideboard", version: "1.0.0" },
1956
+ { capabilities: UI_CAPABILITIES }
1957
+ );
1958
+ await client.connect(transport, { timeout: REQUEST_TIMEOUT_MS });
1959
+ return {
1960
+ async listTools() {
1961
+ const listed = await client.listTools(void 0, { timeout: REQUEST_TIMEOUT_MS });
1962
+ return listed.tools.map((tool) => {
1963
+ const meta = tool._meta;
1964
+ return {
1965
+ name: tool.name,
1966
+ ...tool.description ? { description: tool.description } : {},
1967
+ ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {},
1968
+ resourceUri: uiResourceUriFromMeta(meta),
1969
+ visibility: visibilityFromMeta(meta),
1970
+ ...cspFromMeta(meta) ? { csp: cspFromMeta(meta) } : {}
1971
+ };
1972
+ });
1973
+ },
1974
+ listResources: () => orEmpty(
1975
+ () => client.listResources(void 0, { timeout: REQUEST_TIMEOUT_MS }),
1976
+ { resources: [] }
1977
+ ),
1978
+ listResourceTemplates: () => orEmpty(
1979
+ () => client.listResourceTemplates(void 0, { timeout: REQUEST_TIMEOUT_MS }),
1980
+ { resourceTemplates: [] }
1981
+ ),
1982
+ listPrompts: () => orEmpty(
1983
+ () => client.listPrompts(void 0, { timeout: REQUEST_TIMEOUT_MS }),
1984
+ { prompts: [] }
1985
+ ),
1986
+ getPrompt(name, args) {
1987
+ return client.getPrompt({ name, arguments: args }, { timeout: REQUEST_TIMEOUT_MS });
1988
+ },
1989
+ async readResource(uri) {
1990
+ const result = await client.readResource({ uri }, { timeout: REQUEST_TIMEOUT_MS });
1991
+ const content = result.contents?.[0];
1992
+ if (!content) return null;
1993
+ const text = "text" in content && typeof content.text === "string" ? content.text : void 0;
1994
+ const blob = "blob" in content && typeof content.blob === "string" ? content.blob : void 0;
1995
+ return {
1996
+ uri: content.uri,
1997
+ ...content.mimeType ? { mimeType: content.mimeType } : {},
1998
+ ...text != null ? { text } : {},
1999
+ ...blob != null ? { blob } : {},
2000
+ ...cspFromMeta(content._meta) ? { csp: cspFromMeta(content._meta) } : {}
2001
+ };
2002
+ },
2003
+ async callTool(name, args) {
2004
+ return client.callTool({ name, arguments: args }, void 0, { timeout: REQUEST_TIMEOUT_MS });
2005
+ },
2006
+ capabilities() {
2007
+ const caps = client.getServerCapabilities() ?? {};
2008
+ return {
2009
+ tools: caps.tools ?? {},
2010
+ resources: caps.resources ?? {},
2011
+ prompts: caps.prompts ?? {},
2012
+ completions: caps.completions ?? {},
2013
+ ...caps.logging ? { logging: caps.logging } : {}
2014
+ };
2015
+ },
2016
+ forward(method, params) {
2017
+ return client.request(
2018
+ { method, params },
2019
+ ResultSchema,
2020
+ { timeout: REQUEST_TIMEOUT_MS }
2021
+ );
2022
+ },
2023
+ async close() {
2024
+ await client.close();
2025
+ }
2026
+ };
2027
+ }
2028
+ function transportForLaunch(launch) {
2029
+ if (launch.kind === "stdio") {
2030
+ return new StdioClientTransport({
2031
+ command: launch.command,
2032
+ args: launch.args,
2033
+ env: { ...getDefaultEnvironment(), ...launch.env },
2034
+ stderr: "ignore",
2035
+ ...launch.cwd ? { cwd: launch.cwd } : {}
2036
+ });
2037
+ }
2038
+ const url = new URL(launch.url);
2039
+ const requestInit = launch.headers ? { headers: launch.headers } : void 0;
2040
+ if (launch.kind === "sse") {
2041
+ return new SSEClientTransport(url, requestInit ? { requestInit } : void 0);
2042
+ }
2043
+ return new StreamableHTTPClientTransport(url, requestInit ? { requestInit } : void 0);
2044
+ }
2045
+
2046
+ // src/mcp-ui/host.ts
2047
+ var TOOL_TTL_MS = 5 * 60 * 1e3;
2048
+ var McpUiHost = class {
2049
+ constructor(opts) {
2050
+ this.opts = opts;
2051
+ }
2052
+ opts;
2053
+ sessions = /* @__PURE__ */ new Map();
2054
+ toolLists = /* @__PURE__ */ new Map();
2055
+ proxy;
2056
+ proxyDirs = /* @__PURE__ */ new Map();
2057
+ async viewForClaudeTool(toolName, projectDirs) {
2058
+ const entries = this.userEntries(projectDirs);
2059
+ const match = matchClaudeMcpTool(toolName, Object.keys(entries));
2060
+ if (!match) return null;
2061
+ const launch = parseMcpServerLaunch(entries[match.server] ?? {});
2062
+ if (!launch) return null;
2063
+ try {
2064
+ const session = await this.sessionFor(match.server, launch);
2065
+ const tools = await this.toolsFor(match.server, launch, session);
2066
+ const tool = tools.find((item) => item.name === match.tool);
2067
+ if (!tool?.resourceUri) return null;
2068
+ const resource = await session.readResource(tool.resourceUri);
2069
+ if (!resource) return null;
2070
+ const html = htmlFromResourceContent(resource);
2071
+ if (!html) return null;
2072
+ const csp = resource.csp ?? tool.csp;
2073
+ return {
2074
+ server: match.server,
2075
+ tool: match.tool,
2076
+ uri: html.uri,
2077
+ mimeType: html.mimeType,
2078
+ html: html.html,
2079
+ csp: mcpUiContentSecurityPolicy(csp)
2080
+ };
2081
+ } catch (err) {
2082
+ console.error(
2083
+ `[mcp-ui] ${match.server}/${match.tool}:`,
2084
+ err instanceof Error ? err.message : err
2085
+ );
2086
+ return null;
2087
+ }
2088
+ }
2089
+ /**
2090
+ * Point Claude at loopback HTTP so it shares this host's upstream session
2091
+ * and never sees `visibility: ["app"]` tools.
2092
+ */
2093
+ async proxyEntries(entries, projectDirs) {
2094
+ const names = Object.keys(entries).filter((name) => name.trim());
2095
+ if (names.length === 0) return {};
2096
+ const dirsKey = dirsKeyFor(projectDirs);
2097
+ this.proxyDirs.set(dirsKey, projectDirs);
2098
+ try {
2099
+ const listen = await this.ensureProxy();
2100
+ const out = {};
2101
+ for (const name of names) {
2102
+ out[name] = {
2103
+ type: "http",
2104
+ url: listen.urlFor(name, dirsKey),
2105
+ headers: listen.headers
2106
+ };
2107
+ }
2108
+ return out;
2109
+ } catch (err) {
2110
+ console.error("[mcp-ui] proxy:", err instanceof Error ? err.message : err);
2111
+ return { ...entries };
2112
+ }
2113
+ }
2114
+ async callTool(server, tool, args, projectDirs) {
2115
+ const entry = this.userEntries(projectDirs)[server];
2116
+ if (!entry) throw new Error(`MCP server ${server} is not available`);
2117
+ const launch = parseMcpServerLaunch(entry);
2118
+ if (!launch) throw new Error(`MCP server ${server} is not available`);
2119
+ const session = await this.sessionFor(server, launch);
2120
+ const tools = await this.toolsFor(server, launch, session);
2121
+ const info = tools.find((item) => item.name === tool);
2122
+ if (!info || !toolVisibleTo(info.visibility, "app")) {
2123
+ throw new Error(`Tool ${tool} is not available to the app`);
2124
+ }
2125
+ return session.callTool(tool, args);
2126
+ }
2127
+ async readResource(server, uri, projectDirs) {
2128
+ const session = await this.sessionForName(server, projectDirs);
2129
+ if (!session) throw new Error(`MCP server ${server} is not available`);
2130
+ const resource = await session.readResource(uri);
2131
+ if (!resource) throw new Error(`Resource ${uri} was empty`);
2132
+ return {
2133
+ contents: [
2134
+ {
2135
+ uri: resource.uri,
2136
+ ...resource.mimeType ? { mimeType: resource.mimeType } : {},
2137
+ ...resource.text != null ? { text: resource.text } : {},
2138
+ ...resource.blob != null ? { blob: resource.blob } : {}
2139
+ }
2140
+ ]
2141
+ };
2142
+ }
2143
+ async closeAll() {
2144
+ const pending = [...this.sessions.values()];
2145
+ const proxy = this.proxy;
2146
+ this.sessions.clear();
2147
+ this.toolLists.clear();
2148
+ this.proxyDirs.clear();
2149
+ this.proxy = void 0;
2150
+ await Promise.all([
2151
+ ...pending.map((session) => session.then((open) => open.close()).catch(() => void 0)),
2152
+ proxy ? proxy.then((listen) => listen.close()).catch(() => void 0) : Promise.resolve()
2153
+ ]);
2154
+ }
2155
+ ensureProxy() {
2156
+ if (this.proxy) return this.proxy;
2157
+ this.proxy = listenMcpUiProxy({
2158
+ sessionFor: (server, dirsKey) => {
2159
+ if (dirsKey && !this.proxyDirs.has(dirsKey)) return Promise.resolve(null);
2160
+ return this.sessionForName(server, this.proxyDirs.get(dirsKey));
2161
+ }
2162
+ }).catch((err) => {
2163
+ this.proxy = void 0;
2164
+ throw err;
2165
+ });
2166
+ return this.proxy;
2167
+ }
2168
+ userEntries(projectDirs) {
2169
+ const out = {};
2170
+ for (const [name, entry] of Object.entries(this.opts.entries(projectDirs))) {
2171
+ if (!name.trim() || isInjectedOrchMcpName(name) || !entry) continue;
2172
+ out[name] = entry;
2173
+ }
2174
+ return out;
2175
+ }
2176
+ async sessionForName(server, projectDirs) {
2177
+ const entry = this.userEntries(projectDirs)[server];
2178
+ if (!entry) return null;
2179
+ const launch = parseMcpServerLaunch(entry);
2180
+ if (!launch) return null;
2181
+ return this.sessionFor(server, launch);
2182
+ }
2183
+ sessionFor(server, launch) {
2184
+ const key = `${server}
2185
+ ${JSON.stringify(launch)}`;
2186
+ const existing = this.sessions.get(key);
2187
+ if (existing) return existing;
2188
+ const open = this.opts.openSession ?? openMcpUiSession;
2189
+ const pending = open(launch).catch((err) => {
2190
+ this.sessions.delete(key);
2191
+ throw err;
2192
+ });
2193
+ this.sessions.set(key, pending);
2194
+ return pending;
2195
+ }
2196
+ async toolsFor(server, launch, session) {
2197
+ const key = `${server}
2198
+ ${JSON.stringify(launch)}`;
2199
+ const cached = this.toolLists.get(key);
2200
+ if (cached && cached.key === key && Date.now() - cached.at < TOOL_TTL_MS) return cached.tools;
2201
+ const tools = await session.listTools();
2202
+ this.toolLists.set(key, { at: Date.now(), key, tools });
2203
+ return tools;
2204
+ }
2205
+ };
2206
+ var singleton;
2207
+ function getMcpUiHost() {
2208
+ if (!singleton) {
2209
+ singleton = new McpUiHost({
2210
+ entries: (projectDirs) => listUserClaudeMcpServerEntries(homedir5(), projectDirs)
2211
+ });
2212
+ }
2213
+ return singleton;
2214
+ }
2215
+ async function proxyClaudeUserMcp(orchestrator, projectDirs) {
2216
+ if (orchestrator) return {};
2217
+ const dirs = projectDirs.filter((d) => typeof d === "string" && d.length > 0);
2218
+ return getMcpUiHost().proxyEntries(listUserClaudeMcpServerEntries(homedir5(), dirs), dirs);
2219
+ }
2220
+ function dirsKeyFor(projectDirs) {
2221
+ if (!projectDirs?.length) return "";
2222
+ return createHash("sha256").update(projectDirs.join("\0")).digest("hex").slice(0, 16);
2223
+ }
2224
+
1623
2225
  // src/agents/types.ts
1624
2226
  var PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or Approves / Hands off the plan). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any project files except via Sideboard MCP present_plan (writes .context/attachments/plan.md). When you need a clarifying decision (approach forks, auth choice, scope) \u2014 not greetings, check-ins, or an invented task menu: (1) first write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it) \u2014 do not leave the user staring at bare labels; (2) then call Sideboard MCP ask_user with the same options, including a description on every option. If one option is the obvious default, proceed without asking. Sideboard shows questions in the composer and mirrors them in chat. After ask_user, wait for the user's next message with their answers before finalizing the plan. When the plan is ready for approval: (1) call present_plan with the full markdown plan (title + content) so Sideboard saves .context/attachments/plan.md and shows it in chat for Approve / Hand off / Copy; (2) Claude should also call ExitPlanMode after present_plan. Do not skip present_plan \u2014 the plan must be a markdown file, not only chat prose.";
1625
2227
  var PLAN_MODE_OFF_INSTRUCTION = "Plan mode is off. Do not call present_plan or ExitPlanMode \u2014 those only work when the user turns Plan on in the composer. If you need to propose an approach, write it in chat.";
@@ -2264,7 +2866,7 @@ var claudeAdapter = {
2264
2866
  });
2265
2867
  const injectedBrightsyNames = injectedServers.filter((s) => s.name === "brightsy" || s.name.startsWith("brightsy_")).map((s) => s.name);
2266
2868
  const chromeOn = claudeChromeEnabled();
2267
- const userMcpEntries = isOrchestrator ? {} : listUserClaudeMcpServerEntries(homedir5(), [thread.repoPath, thread.worktreePath]);
2869
+ const userMcpEntries = await proxyClaudeUserMcp(isOrchestrator, [thread.repoPath, thread.worktreePath]);
2268
2870
  let allowedTools;
2269
2871
  if (isOrchestrator) {
2270
2872
  allowedTools = [
@@ -2602,7 +3204,7 @@ function unwrapCodexMcpResult(result) {
2602
3204
  return String(result);
2603
3205
  }
2604
3206
  }
2605
- function asRecord2(value) {
3207
+ function asRecord3(value) {
2606
3208
  if (value && typeof value === "object" && !Array.isArray(value)) {
2607
3209
  return value;
2608
3210
  }
@@ -2755,7 +3357,7 @@ var codexAdapter = {
2755
3357
  }
2756
3358
  if (itemType === "mcp_tool_call" && item.id && item.server && item.tool) {
2757
3359
  const name = `mcp__${item.server}__${item.tool}`;
2758
- const input = asRecord2(item.arguments);
3360
+ const input = asRecord3(item.arguments);
2759
3361
  const events = [
2760
3362
  { type: "tool_use", id: item.id, name, input }
2761
3363
  ];
@@ -3056,20 +3658,20 @@ function turnCostUsdFromCursorUsage(before, after) {
3056
3658
  if (!(delta >= 0) || !Number.isFinite(delta)) return void 0;
3057
3659
  return delta / 100;
3058
3660
  }
3059
- function asRecord3(value) {
3661
+ function asRecord4(value) {
3060
3662
  if (value && typeof value === "object" && !Array.isArray(value)) {
3061
3663
  return value;
3062
3664
  }
3063
3665
  return void 0;
3064
3666
  }
3065
3667
  function normalizeCursorToolCall(name, args) {
3066
- const raw = asRecord3(args);
3668
+ const raw = asRecord4(args);
3067
3669
  const toolName = typeof raw?.toolName === "string" && raw.toolName.trim() ? raw.toolName.trim() : null;
3068
- const looksLikeMcp = name === "mcp" || toolName != null && (typeof raw?.providerIdentifier === "string" || asRecord3(raw?.args) != null);
3670
+ const looksLikeMcp = name === "mcp" || toolName != null && (typeof raw?.providerIdentifier === "string" || asRecord4(raw?.args) != null);
3069
3671
  if (looksLikeMcp && toolName) {
3070
3672
  const providerRaw = typeof raw.providerIdentifier === "string" && raw.providerIdentifier.trim() ? raw.providerIdentifier.trim() : "sideboard";
3071
3673
  const provider = providerRaw.replace(/[^a-zA-Z0-9_-]/g, "_");
3072
- const nested = asRecord3(raw.args);
3674
+ const nested = asRecord4(raw.args);
3073
3675
  return {
3074
3676
  name: `mcp__${provider}__${toolName}`,
3075
3677
  input: nested ?? { toolName }
@@ -3080,7 +3682,7 @@ function normalizeCursorToolCall(name, args) {
3080
3682
  function unwrapCursorToolResult(result) {
3081
3683
  if (result == null) return void 0;
3082
3684
  if (typeof result === "string") return result;
3083
- const rec = asRecord3(result);
3685
+ const rec = asRecord4(result);
3084
3686
  if (!rec) {
3085
3687
  try {
3086
3688
  return JSON.stringify(result);
@@ -3091,18 +3693,18 @@ function unwrapCursorToolResult(result) {
3091
3693
  const collectTexts = (items) => {
3092
3694
  const texts = [];
3093
3695
  for (const item of items) {
3094
- const row = asRecord3(item);
3696
+ const row = asRecord4(item);
3095
3697
  if (!row) continue;
3096
3698
  if (typeof row.text === "string") {
3097
3699
  texts.push(row.text);
3098
3700
  continue;
3099
3701
  }
3100
- const nested = asRecord3(row.text);
3702
+ const nested = asRecord4(row.text);
3101
3703
  if (typeof nested?.text === "string") texts.push(nested.text);
3102
3704
  }
3103
3705
  return texts;
3104
3706
  };
3105
- const value = asRecord3(rec.value);
3707
+ const value = asRecord4(rec.value);
3106
3708
  if (value && Array.isArray(value.content)) {
3107
3709
  const texts = collectTexts(value.content);
3108
3710
  if (texts.length) return texts.join("\n");
@@ -3119,10 +3721,10 @@ function unwrapCursorToolResult(result) {
3119
3721
  }
3120
3722
  function cursorToolResultIsError(status, result) {
3121
3723
  if (status === "error") return true;
3122
- const rec = asRecord3(result);
3724
+ const rec = asRecord4(result);
3123
3725
  if (!rec) return false;
3124
3726
  if (rec.status === "error") return true;
3125
- const value = asRecord3(rec.value);
3727
+ const value = asRecord4(rec.value);
3126
3728
  return value?.isError === true;
3127
3729
  }
3128
3730
  function cursorSdkMessageToEvents(msg) {
@@ -3623,7 +4225,7 @@ var FALLBACK_OPENCODE_MODELS = [
3623
4225
  ];
3624
4226
  var cachedOpencodeModels = null;
3625
4227
  var OPENCODE_MODEL_CACHE_MS = 5 * 60 * 1e3;
3626
- function asRecord4(value) {
4228
+ function asRecord5(value) {
3627
4229
  if (value && typeof value === "object" && !Array.isArray(value)) {
3628
4230
  return value;
3629
4231
  }
@@ -3794,7 +4396,7 @@ var opencodeAdapter = {
3794
4396
  const name = part?.tool ?? part?.name ?? obj.tool ?? obj.name ?? "tool";
3795
4397
  const state = part?.state;
3796
4398
  const input = (state?.input && typeof state.input === "object" ? state.input : void 0) ?? part?.input ?? obj.input;
3797
- const metadata = asRecord4(state?.metadata);
4399
+ const metadata = asRecord5(state?.metadata);
3798
4400
  const id = opencodeToolId(
3799
4401
  name,
3800
4402
  part?.callID ?? part?.id ?? obj.id,
@@ -3829,7 +4431,7 @@ var opencodeAdapter = {
3829
4431
  }
3830
4432
  if (obj.type === "subtask_event") {
3831
4433
  const parentId = str(obj.childSessionID) ?? str(obj.childSessionId);
3832
- const part = asRecord4(obj.part);
4434
+ const part = asRecord5(obj.part);
3833
4435
  if (!parentId || !part) return null;
3834
4436
  if (part.type === "text" && str(part.text)) {
3835
4437
  return withTaskParent(
@@ -3840,8 +4442,8 @@ var opencodeAdapter = {
3840
4442
  if (part.type === "tool" || str(part.tool)) {
3841
4443
  const name = str(part.tool) ?? str(part.name) ?? "tool";
3842
4444
  const id = str(part.callID) ?? str(part.id) ?? `${parentId}-${name}`;
3843
- const state = asRecord4(part.state);
3844
- const input = asRecord4(state?.input) ?? asRecord4(part.input);
4445
+ const state = asRecord5(part.state);
4446
+ const input = asRecord5(state?.input) ?? asRecord5(part.input);
3845
4447
  const nested = [
3846
4448
  withEventParentId({ type: "tool_use", id, name, input }, parentId)
3847
4449
  ];