From b593b9d0264678c55dbf924dde8ed75b809812ea Mon Sep 17 00:00:00 2001 From: asemabdallah Date: Sun, 11 Oct 2026 02:28:19 +0200 Subject: [PATCH] fix(mcp): handle links inside SVG in the document bridge --- apps/mcp/src/renderer.ts | 9 +++- apps/mcp/tests/renderer.test.ts | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/apps/mcp/src/renderer.ts b/apps/mcp/src/renderer.ts index 8e1400b..d6637c9 100644 --- a/apps/mcp/src/renderer.ts +++ b/apps/mcp/src/renderer.ts @@ -12,9 +12,14 @@ window.openLink = function(url) { }; document.addEventListener('click', function(e) { var a = e.target.closest('a[href]'); - if (a && a.href.startsWith('http')) { + if (!a) return; + // SVG elements expose href as an SVGAnimatedString, not a string. + var url = typeof a.href === 'string' + ? a.href + : new URL(a.href.baseVal, document.baseURI).href; + if (url.startsWith('http')) { e.preventDefault(); - window.parent.postMessage({ type: 'open-link', url: a.href }, '*'); + window.parent.postMessage({ type: 'open-link', url: url }, '*'); } }); function reportHeight() { diff --git a/apps/mcp/tests/renderer.test.ts b/apps/mcp/tests/renderer.test.ts index e77a943..7b47ebd 100644 --- a/apps/mcp/tests/renderer.test.ts +++ b/apps/mcp/tests/renderer.test.ts @@ -37,3 +37,82 @@ describe("assembleDocument", () => { expect(doc).toContain("ResizeObserver"); }); }); + +describe("bridge link handling", () => { + // Run the bridge script against minimal stubs and return its click handler + // plus the messages it posts to the parent frame. + function loadBridge() { + const doc = assembleDocument(""); + const script = doc.slice( + doc.lastIndexOf("") + ); + let onClick: ((e: unknown) => void) | undefined; + const posted: unknown[] = []; + const document = { + baseURI: "about:srcdoc", + documentElement: { scrollHeight: 0 }, + body: {}, + getElementById: () => null, + addEventListener: (type: string, fn: (e: unknown) => void) => { + if (type === "click") onClick = fn; + }, + }; + const window = { + parent: { postMessage: (msg: unknown) => posted.push(msg) }, + addEventListener: () => {}, + }; + new Function( + "document", + "window", + "ResizeObserver", + "setInterval", + "setTimeout", + script + )( + document, + window, + class { + observe() {} + }, + () => 0, + () => 0 + ); + return { click: onClick!, posted }; + } + + function clickOn(href: unknown) { + const event = { + target: { closest: () => ({ href }) }, + defaultPrevented: false, + }; + return Object.assign(event, { + preventDefault: () => { + event.defaultPrevented = true; + }, + }); + } + + it("forwards html anchors to the parent", () => { + const { click, posted } = loadBridge(); + const event = clickOn("https://example.com/a"); + click(event); + expect(event.defaultPrevented).toBe(true); + expect(posted).toEqual([ + { type: "open-link", url: "https://example.com/a" }, + ]); + }); + + it("forwards svg anchors, whose href is an SVGAnimatedString", () => { + const { click, posted } = loadBridge(); + const event = clickOn({ + baseVal: "https://example.com/b", + animVal: "https://example.com/b", + }); + click(event); + expect(event.defaultPrevented).toBe(true); + expect(posted).toEqual([ + { type: "open-link", url: "https://example.com/b" }, + ]); + }); +});