From 6068522ee3a7304e1c835d489185ce4952650510 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Mon, 25 May 2026 21:22:15 +0100 Subject: [PATCH] Add per-leaf mcpAllow flag for MCP visibility gating (default-deny) --- src/lib/layout/tree.test.ts | 22 ++++++++++++++++++++++ src/lib/layout/tree.ts | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/lib/layout/tree.test.ts b/src/lib/layout/tree.test.ts index 542964c..58a755a 100644 --- a/src/lib/layout/tree.test.ts +++ b/src/lib/layout/tree.test.ts @@ -12,6 +12,7 @@ import { setLeafShell, changeLabel, toggleBroadcast, + toggleMcpAllow, adjustFontSize, adjustAllFontSizes, resolveFontSize, @@ -363,6 +364,27 @@ describe("toggleBroadcast", () => { }); }); +describe("toggleMcpAllow", () => { + it("default-undefined toggles to true", () => { + const leaf = newLeaf(); + expect(leaf.mcpAllow).toBeUndefined(); + const on = toggleMcpAllow(leaf, leaf.id) as LeafNode; + expect(on.mcpAllow).toBe(true); + }); + + it("true toggles to false", () => { + const leaf = newLeaf({ mcpAllow: true }); + const off = toggleMcpAllow(leaf, leaf.id) as LeafNode; + expect(off.mcpAllow).toBe(false); + }); + + it("MUST NOT swap the leaf id (metadata-only, no PTY respawn)", () => { + const leaf = newLeaf(); + const next = toggleMcpAllow(leaf, leaf.id) as LeafNode; + expect(next.id).toBe(leaf.id); + }); +}); + describe("resolveFontSize", () => { it("returns the default when offset is undefined or 0", () => { expect(resolveFontSize(undefined)).toBe(DEFAULT_FONT_SIZE); diff --git a/src/lib/layout/tree.ts b/src/lib/layout/tree.ts index e068bf6..ea70fb2 100644 --- a/src/lib/layout/tree.ts +++ b/src/lib/layout/tree.ts @@ -44,6 +44,13 @@ export interface LeafNode { * later doesn't require migrating saved workspaces. */ fontSizeOffset?: number; + /** + * If true, this pane is visible to the MCP server (Claude can list it, + * read its scrollback, etc.). Default-DENY: when undefined or false, the + * MCP surface filters this pane out entirely. Toggled via the per-pane + * MCP chip in the toolbar. + */ + mcpAllow?: boolean; } /** Base xterm.js font size in px. Per-leaf offset adds on top of this. */ @@ -262,6 +269,15 @@ export function toggleBroadcast(root: TreeNode, leafId: NodeId): TreeNode { }); } +/** Toggle a leaf's mcpAllow flag. Metadata-only — does NOT swap the id. + * Drives whether the MCP server includes this pane in its surface. */ +export function toggleMcpAllow(root: TreeNode, leafId: NodeId): TreeNode { + return replaceById(root, leafId, (node) => { + if (node.kind !== "leaf") return node; + return { ...node, mcpAllow: !node.mcpAllow }; + }); +} + /** Compute the actual pixel font size from a leaf's offset, clamped to * [MIN_FONT_SIZE, MAX_FONT_SIZE]. */ export function resolveFontSize(offset: number | undefined): number { @@ -351,6 +367,7 @@ export function reshapeToPreset( if (src.label !== undefined) slot.label = src.label; if (src.broadcast !== undefined) slot.broadcast = src.broadcast; if (src.fontSizeOffset !== undefined) slot.fontSizeOffset = src.fontSizeOffset; + if (src.mcpAllow !== undefined) slot.mcpAllow = src.mcpAllow; } for (let i = slots.length; i < existingLeaves.length; i++) {