Add per-leaf mcpAllow flag for MCP visibility gating (default-deny)

This commit is contained in:
megaproxy 2026-05-25 21:22:15 +01:00
parent b35a5b282d
commit 6068522ee3
2 changed files with 39 additions and 0 deletions

View file

@ -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);

View file

@ -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++) {