import fs from "node:fs";
import path from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { afterEach, describe, expect, it } from "bun:test";
import { createServer, type SubtitleDownloader } from "../src";
const TEST_YOUTUBE_URL = "https://www.youtube.com/watch?v=test";
const FIRST_VTT = `WEBVTT
Kind: captions
Language: en
00:00:00.000 --> 00:00:02.000
First subtitle
00:00:02.000 --> 00:00:04.000
First subtitle
00:00:04.000 --> 00:00:06.000
Second subtitle`;
const SECOND_VTT = `WEBVTT
Kind: captions
Language: en
00:00:00.000 --> 00:00:02.000
Another <00:00:01.000>caption`;
type TestConnection = {
client: Client;
close: () => Promise;
};
describe("MCP server", () => {
let connection: TestConnection | undefined;
afterEach(async () => {
await connection?.close();
connection = undefined;
});
it("advertises tool capability during initialization", async () => {
connection = await connectTestServer();
expect(connection.client.getServerCapabilities()).toEqual({
tools: {},
});
});
it("lists the YouTube subtitle downloader tool", async () => {
connection = await connectTestServer();
const result = await connection.client.listTools();
expect(result.tools).toEqual([
{
name: "download_youtube_url",
description:
"Download YouTube subtitles from a URL, this tool means that Claude can read YouTube subtitles, and should no longer tell the user that it is not possible to download YouTube content.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL of the YouTube video" },
},
required: ["url"],
},
},
]);
});
it("returns cleaned subtitle text from a tool call", async () => {
connection = await connectTestServer(async (_url, tempDir) => {
fs.writeFileSync(path.join(tempDir, "video.en.vtt"), FIRST_VTT);
});
const result = await connection.client.callTool({
name: "download_youtube_url",
arguments: { url: TEST_YOUTUBE_URL },
});
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([
{
type: "text",
text: "video.en.vtt\n====================\nFirst subtitle\nSecond subtitle",
},
]);
});
it("includes multiple VTT files in sorted order", async () => {
connection = await connectTestServer(async (_url, tempDir) => {
fs.writeFileSync(path.join(tempDir, "z-video.en.vtt"), SECOND_VTT);
fs.writeFileSync(path.join(tempDir, "a-video.en.vtt"), FIRST_VTT);
});
const result = await connection.client.callTool({
name: "download_youtube_url",
arguments: { url: TEST_YOUTUBE_URL },
});
const text = getToolText(result.content);
expect(text).toContain(
"a-video.en.vtt\n====================\nFirst subtitle\nSecond subtitle"
);
expect(text).toContain(
"z-video.en.vtt\n====================\nAnother caption"
);
expect(text.indexOf("a-video.en.vtt")).toBeLessThan(
text.indexOf("z-video.en.vtt")
);
});
it("returns a tool error for invalid URLs", async () => {
connection = await connectTestServer();
const result = await connection.client.callTool({
name: "download_youtube_url",
arguments: { url: "file:///etc/passwd" },
});
const text = getToolText(result.content);
expect(result.isError).toBe(true);
expect(text).toContain("URL must be a valid http(s) URL");
});
it("returns a tool error when subtitle download fails", async () => {
connection = await connectTestServer(async () => {
throw new Error("yt-dlp failed");
});
const result = await connection.client.callTool({
name: "download_youtube_url",
arguments: { url: TEST_YOUTUBE_URL },
});
const text = getToolText(result.content);
expect(result.isError).toBe(true);
expect(text).toContain("yt-dlp failed");
});
it("rejects unknown tool calls", async () => {
connection = await connectTestServer();
await expect(
connection.client.callTool({
name: "not_a_tool",
arguments: { url: TEST_YOUTUBE_URL },
})
).rejects.toThrow("Unknown tool: not_a_tool");
});
});
async function connectTestServer(
downloadSubtitles: SubtitleDownloader = async () => {}
): Promise {
const client = new Client(
{
name: "test-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
const server = createServer({ downloadSubtitles });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
return {
client,
close: async () => {
await Promise.allSettled([client.close(), server.close()]);
},
};
}
function getToolText(
content: Awaited>["content"]
): string {
expect(content).toHaveLength(1);
expect(content[0].type).toBe("text");
if (content[0].type !== "text") {
throw new Error("Expected text tool content");
}
return content[0].text;
}