Skip to content

Commit 6013ba3

Browse files
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.
1 parent 4abd6e5 commit 6013ba3

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

src/mcp.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@ export function isRemoteTarget(target) {
1111
return /^https?:\/\//i.test(String(target));
1212
}
1313

14+
// Second-level labels that are part of a multi-part public suffix rather than a
15+
// name, as in co.uk / com.au / co.za. Dropping only the TLD would leave these.
16+
const SUFFIX_LABELS = ["co", "com", "net", "org", "gov", "edu", "ac"];
17+
1418
/** Derive a sane server name from a remote URL's host (e.g. mcp.sentry.dev → sentry). */
1519
export function deriveName(target) {
1620
const sanitize = (s) => String(s).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
1721
try {
1822
const labels = new URL(target).hostname.split(".").filter(Boolean);
19-
const withoutTld = labels.slice(0, -1); // drop the TLD
23+
let withoutTld = labels.slice(0, -1); // drop the TLD
24+
// ...and the generic label of a multi-part suffix, as long as a real name
25+
// still precedes it (a bare "co.uk" host has nothing better to offer).
26+
if (withoutTld.length > 1 && SUFFIX_LABELS.includes(withoutTld[withoutTld.length - 1])) {
27+
withoutTld = withoutTld.slice(0, -1);
28+
}
2029
const meaningful = withoutTld.filter((l) => !["mcp", "www", "api", "app"].includes(l));
2130
const pick = meaningful[meaningful.length - 1] || withoutTld[withoutTld.length - 1] || labels[0];
2231
return sanitize(pick) || "server";

test/mcp.test.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ test("deriveName pulls a sane name from a remote host", () => {
2020
assert.equal(deriveName("not a url"), "server");
2121
});
2222

23+
test("deriveName skips the generic label of a multi-part suffix", () => {
24+
assert.equal(deriveName("https://mcp.acme.co.uk/sse"), "acme");
25+
assert.equal(deriveName("https://api.example.com.au/mcp"), "example");
26+
assert.equal(deriveName("https://widgets.co.za/mcp"), "widgets");
27+
// A .co TLD is still just a TLD, and a bare suffix host keeps its fallback.
28+
assert.equal(deriveName("https://mcp.example.co/sse"), "example");
29+
assert.equal(deriveName("https://co.uk/mcp"), "co");
30+
});
31+
32+
test("deriveName gives distinct multi-part-suffix hosts distinct names", () => {
33+
assert.notEqual(deriveName("https://mcp.acme.co.uk/sse"), deriveName("https://mcp.widgets.co.za/sse"));
34+
});
35+
2336
test("isRemoteTarget distinguishes URLs from commands", () => {
2437
assert.equal(isRemoteTarget("https://x.dev/mcp"), true);
2538
assert.equal(isRemoteTarget("npx"), false);

0 commit comments

Comments
 (0)