From 6013ba38e5a1ef7deda6c556635aa1eefeb59938 Mon Sep 17 00:00:00 2001 From: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:17:24 +0000 Subject: [PATCH] fix(mcp): derive a real name from hosts with a multi-part suffix `deriveName` dropped only the final label before picking a name, so on a host like mcp.acme.co.uk the leftover "co" won. Every .co.uk / .com.au / .co.za server therefore registered as "co", and a second one silently overwrote the first in each engine's MCP config. Also drop the generic label of a multi-part suffix when a real name still precedes it, leaving single-label TLDs and bare suffix hosts untouched. --- src/mcp.mjs | 11 ++++++++++- test/mcp.test.mjs | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mcp.mjs b/src/mcp.mjs index 6a467a9..254c38f 100644 --- a/src/mcp.mjs +++ b/src/mcp.mjs @@ -11,12 +11,21 @@ export function isRemoteTarget(target) { return /^https?:\/\//i.test(String(target)); } +// Second-level labels that are part of a multi-part public suffix rather than a +// name, as in co.uk / com.au / co.za. Dropping only the TLD would leave these. +const SUFFIX_LABELS = ["co", "com", "net", "org", "gov", "edu", "ac"]; + /** Derive a sane server name from a remote URL's host (e.g. mcp.sentry.dev → sentry). */ export function deriveName(target) { const sanitize = (s) => String(s).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, ""); try { const labels = new URL(target).hostname.split(".").filter(Boolean); - const withoutTld = labels.slice(0, -1); // drop the TLD + let withoutTld = labels.slice(0, -1); // drop the TLD + // ...and the generic label of a multi-part suffix, as long as a real name + // still precedes it (a bare "co.uk" host has nothing better to offer). + if (withoutTld.length > 1 && SUFFIX_LABELS.includes(withoutTld[withoutTld.length - 1])) { + withoutTld = withoutTld.slice(0, -1); + } const meaningful = withoutTld.filter((l) => !["mcp", "www", "api", "app"].includes(l)); const pick = meaningful[meaningful.length - 1] || withoutTld[withoutTld.length - 1] || labels[0]; return sanitize(pick) || "server"; diff --git a/test/mcp.test.mjs b/test/mcp.test.mjs index a1d12ef..f2451e8 100644 --- a/test/mcp.test.mjs +++ b/test/mcp.test.mjs @@ -20,6 +20,19 @@ test("deriveName pulls a sane name from a remote host", () => { assert.equal(deriveName("not a url"), "server"); }); +test("deriveName skips the generic label of a multi-part suffix", () => { + assert.equal(deriveName("https://mcp.acme.co.uk/sse"), "acme"); + assert.equal(deriveName("https://api.example.com.au/mcp"), "example"); + assert.equal(deriveName("https://widgets.co.za/mcp"), "widgets"); + // A .co TLD is still just a TLD, and a bare suffix host keeps its fallback. + assert.equal(deriveName("https://mcp.example.co/sse"), "example"); + assert.equal(deriveName("https://co.uk/mcp"), "co"); +}); + +test("deriveName gives distinct multi-part-suffix hosts distinct names", () => { + assert.notEqual(deriveName("https://mcp.acme.co.uk/sse"), deriveName("https://mcp.widgets.co.za/sse")); +}); + test("isRemoteTarget distinguishes URLs from commands", () => { assert.equal(isRemoteTarget("https://x.dev/mcp"), true); assert.equal(isRemoteTarget("npx"), false);