Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 78 additions & 4 deletions src/mcp-server/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ export async function formatResult(
const contentType = response?.headers.get("content-type") ?? "";
let content: CallToolResult["content"] = [];

if (contentType.search(/\bjson\b/g)) {
content = [{ type: "text", text: JSON.stringify(value) }];
const normalizedValue = normalizeLaunchDarklyLinks(value, response?.url);

if (/\bjson\b/g.test(contentType)) {
content = [{ type: "text", text: JSON.stringify(normalizedValue) }];
} else if (
contentType.startsWith("text/event-stream")
&& isAsyncIterable(value)
&& isAsyncIterable(normalizedValue)
) {
content = await consumeSSE(value);
content = await consumeSSE(normalizedValue);
} else if (contentType.startsWith("text/") && typeof value === "string") {
content = [{ type: "text", text: value }];
} else if (isBinaryData(value) && contentType.startsWith("image/")) {
Expand All @@ -78,6 +80,78 @@ export async function formatResult(
return { content };
}

const canonicalLDHosts = new Set([
"app.launchdarkly.com",
"app.launchdarkly.us",
]);

function normalizeLaunchDarklyLinks(
value: unknown,
responseURL?: string,
): unknown {
if (!responseURL) {
return value;
}

let origin: string;
try {
origin = new URL(responseURL).origin;
} catch {
return value;
}

return deepMap(value, (key, val) => {
if (key !== "href" || typeof val !== "string") {
return val;
}

if (val.startsWith("/")) {
try {
return new URL(val, origin).toString();
} catch {
return val;
}
}

try {
const parsed = new URL(val);
if (canonicalLDHosts.has(parsed.host) && parsed.origin !== origin) {
return `${origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
// Leave invalid URL-ish strings unchanged.
}

return val;
});
}

function deepMap(
value: unknown,
transform: (key: string | null, value: unknown) => unknown,
key: string | null = null,
): unknown {
const transformed = transform(key, value);

if (Array.isArray(transformed)) {
return transformed.map((item) => deepMap(item, transform));
}

if (
transformed !== null
&& typeof transformed === "object"
&& Object.getPrototypeOf(transformed) === Object.prototype
) {
const output: Record<string, unknown> = {};
for (const [k, v] of Object.entries(transformed as Record<string, unknown>)) {
output[k] = deepMap(v, transform, k);
}
return output;
}

return transformed;
}

async function consumeSSE(
value: AsyncIterable<unknown>,
): Promise<CallToolResult["content"]> {
Expand Down
76 changes: 76 additions & 0 deletions tests/tools.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";
import { formatResult } from "../src/mcp-server/tools.js";

describe("formatResult link normalization", () => {
test("rewrites app.launchdarkly.com href to response origin", async () => {
const origin = "https://random-domain.example.test";
const input = {
_links: {
site: {
href: "https://app.launchdarkly.com/default/production/features/flag-a",
},
},
};

const response = {
headers: new Headers({ "content-type": "application/json" }),
url: `${origin}/api/v2/flags/default/flag-a`,
};

const result = await formatResult(input, { response });
const text = result.content[0];
expect(text?.type).toBe("text");
const parsed = JSON.parse(text.text);
expect(parsed._links.site.href).toBe(
`${origin}/default/production/features/flag-a`,
);
});

test("rewrites app.launchdarkly.us href to response origin", async () => {
const origin = "https://federal-alt.example.test";
const input = {
_links: {
site: {
href: "https://app.launchdarkly.us/default/production/features/flag-c",
},
},
};

const response = {
headers: new Headers({ "content-type": "application/json" }),
url: `${origin}/api/v2/flags/default/flag-c`,
};

const result = await formatResult(input, { response });
const text = result.content[0];
expect(text?.type).toBe("text");
const parsed = JSON.parse(text.text);
expect(parsed._links.site.href).toBe(
`${origin}/default/production/features/flag-c`,
);
});

test("expands relative href against response origin", async () => {
const origin = "https://another-random.example.test";
const input = {
_links: {
site: {
href: "/default/production/features/flag-b",
},
},
};

const response = {
headers: new Headers({ "content-type": "application/json" }),
url: `${origin}/api/v2/flags/default/flag-b`,
};

const result = await formatResult(input, { response });
const text = result.content[0];
expect(text?.type).toBe("text");
const parsed = JSON.parse(text.text);
expect(parsed._links.site.href).toBe(
`${origin}/default/production/features/flag-b`,
);
});
});