UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

451 lines (393 loc) 12.1 kB
import { Alepha, z } from "alepha"; import { describe, expect, test } from "vitest"; import { $prompt, $resource, $tool, AlephaMcp, MCP_PROTOCOL_VERSION, McpServerProvider, } from "../index.ts"; // --------------------------------------------------------------------------------------------------------------------- describe("MCP Integration", () => { test("full workflow: initialize, list tools, call tool", async () => { const alepha = Alepha.create(); class Calculator { add = $tool({ description: "Add two numbers", schema: { params: z.object({ a: z.number(), b: z.number(), }), result: z.number(), }, handler: async ({ params }) => params.a + params.b, }); } alepha.with(AlephaMcp).with(Calculator); await alepha.start(); const provider = alepha.inject(McpServerProvider); // Step 1: Initialize const initResponse = await provider.handleMessage({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "test", version: "1.0.0" }, }, }); expect(initResponse?.result).toBeDefined(); const initResult = initResponse!.result as { capabilities: { tools?: object }; }; expect(initResult.capabilities.tools).toBeDefined(); // Step 2: Send initialized notification const notifResponse = await provider.handleMessage({ jsonrpc: "2.0", method: "notifications/initialized", }); expect(notifResponse).toBeNull(); // Step 3: List tools const listResponse = await provider.handleMessage({ jsonrpc: "2.0", id: 2, method: "tools/list", }); const tools = (listResponse!.result as { tools: Array<{ name: string }> }) .tools; expect(tools).toHaveLength(1); expect(tools[0].name).toBe("add"); // Step 4: Call tool const callResponse = await provider.handleMessage({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "add", arguments: { a: 10, b: 20 }, }, }); const result = callResponse!.result as { content: Array<{ text: string }> }; expect(result.content[0].text).toBe("30"); }); test("full workflow: initialize, list resources, read resource", async () => { const alepha = Alepha.create(); let readCount = 0; class Files { readme = $resource({ uri: "file:///README.md", name: "README", description: "Project readme file", mimeType: "text/markdown", handler: async () => { readCount++; return { text: `# Project\n\nRead count: ${readCount}` }; }, }); } alepha.with(AlephaMcp).with(Files); await alepha.start(); const provider = alepha.inject(McpServerProvider); // Initialize await provider.handleMessage({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "test", version: "1.0.0" }, }, }); // List resources const listResponse = await provider.handleMessage({ jsonrpc: "2.0", id: 2, method: "resources/list", }); const resources = ( listResponse!.result as { resources: Array<{ uri: string; name: string }>; } ).resources; expect(resources).toHaveLength(1); expect(resources[0].uri).toBe("file:///README.md"); expect(resources[0].name).toBe("README"); // Read resource twice const read1 = await provider.handleMessage({ jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "file:///README.md" }, }); const content1 = (read1!.result as { contents: Array<{ text: string }> }) .contents[0]; expect(content1.text).toContain("Read count: 1"); const read2 = await provider.handleMessage({ jsonrpc: "2.0", id: 4, method: "resources/read", params: { uri: "file:///README.md" }, }); const content2 = (read2!.result as { contents: Array<{ text: string }> }) .contents[0]; expect(content2.text).toContain("Read count: 2"); }); test("full workflow: initialize, list prompts, get prompt", async () => { const alepha = Alepha.create(); class Prompts { codeReview = $prompt({ name: "code-review", description: "Request a code review for the given code", args: z.object({ language: z.text({ description: "Programming language" }), code: z.text({ description: "Code to review" }), focus: z.text({ description: "Focus areas for review" }).optional(), }), handler: async ({ args }) => { const messages = [ { role: "user" as const, content: `Please review this ${args.language} code:\n\n\`\`\`${args.language}\n${args.code}\n\`\`\``, }, ]; if (args.focus) { messages.push({ role: "user" as const, content: `Please focus on: ${args.focus}`, }); } return messages; }, }); } alepha.with(AlephaMcp).with(Prompts); await alepha.start(); const provider = alepha.inject(McpServerProvider); // Initialize await provider.handleMessage({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "test", version: "1.0.0" }, }, }); // List prompts const listResponse = await provider.handleMessage({ jsonrpc: "2.0", id: 2, method: "prompts/list", }); const prompts = ( listResponse!.result as { prompts: Array<{ name: string; arguments?: Array<{ name: string; required?: boolean }>; }>; } ).prompts; expect(prompts).toHaveLength(1); expect(prompts[0].name).toBe("code-review"); expect(prompts[0].arguments).toHaveLength(3); // Get prompt without optional arg const get1 = await provider.handleMessage({ jsonrpc: "2.0", id: 3, method: "prompts/get", params: { name: "code-review", arguments: { language: "TypeScript", code: "const x = 1;", }, }, }); const messages1 = ( get1!.result as { messages: Array<{ role: string; content: { text: string } }>; } ).messages; expect(messages1).toHaveLength(1); expect(messages1[0].content.text).toContain("TypeScript"); // Get prompt with optional arg const get2 = await provider.handleMessage({ jsonrpc: "2.0", id: 4, method: "prompts/get", params: { name: "code-review", arguments: { language: "JavaScript", code: "let y = 2;", focus: "performance", }, }, }); const messages2 = ( get2!.result as { messages: Array<{ content: { text: string } }> } ).messages; expect(messages2).toHaveLength(2); expect(messages2[1].content.text).toContain("performance"); }); test("combined server with all capabilities", async () => { const alepha = Alepha.create(); class MyMcpServer { // Tools calculator = $tool({ description: "Basic calculator", schema: { params: z.object({ op: z.enum(["add", "sub", "mul", "div"]), a: z.number(), b: z.number(), }), result: z.number(), }, handler: async ({ params }) => { switch (params.op) { case "add": return params.a + params.b; case "sub": return params.a - params.b; case "mul": return params.a * params.b; case "div": return params.a / params.b; } }, }); echo = $tool({ description: "Echo the input", schema: { params: z.object({ message: z.text() }), result: z.text(), }, handler: async ({ params }) => params.message, }); // Resources version = $resource({ uri: "app://version", name: "Version", mimeType: "application/json", handler: async () => ({ text: JSON.stringify({ version: "1.0.0", build: "123" }), }), }); // Prompts greeting = $prompt({ description: "Generate a greeting", args: z.object({ name: z.text() }), handler: async ({ args }) => [ { role: "user", content: `Say hello to ${args.name}` }, ], }); } alepha.with(AlephaMcp).with(MyMcpServer); await alepha.start(); const provider = alepha.inject(McpServerProvider); // Check all capabilities const init = await provider.handleMessage({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "test", version: "1.0.0" }, }, }); const caps = ( init!.result as { capabilities: { tools?: object; resources?: object; prompts?: object }; } ).capabilities; expect(caps.tools).toBeDefined(); expect(caps.resources).toBeDefined(); expect(caps.prompts).toBeDefined(); // Use calculator tool const calc = await provider.handleMessage({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "calculator", arguments: { op: "mul", a: 7, b: 8 } }, }); expect( (calc!.result as { content: Array<{ text: string }> }).content[0].text, ).toBe("56"); // Read version resource const ver = await provider.handleMessage({ jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "app://version" }, }); const versionData = JSON.parse( (ver!.result as { contents: Array<{ text: string }> }).contents[0].text, ); expect(versionData.version).toBe("1.0.0"); // Get greeting prompt const greet = await provider.handleMessage({ jsonrpc: "2.0", id: 4, method: "prompts/get", params: { name: "greeting", arguments: { name: "Alice" } }, }); expect( (greet!.result as { messages: Array<{ content: { text: string } }> }) .messages[0].content.text, ).toContain("Alice"); }); test("error handling across multiple requests", async () => { const alepha = Alepha.create(); let callCount = 0; class Tools { flaky = $tool({ description: "Fails every other call", handler: async () => { callCount++; if (callCount % 2 === 0) { throw new Error("Flaky failure"); } return "success"; }, }); } alepha.with(AlephaMcp).with(Tools); await alepha.start(); const provider = alepha.inject(McpServerProvider); // First call - success const r1 = await provider.handleMessage({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "flaky", arguments: {} }, }); expect( (r1!.result as { content: Array<{ text: string }> }).content[0].text, ).toBe("success"); // Second call - failure const r2 = await provider.handleMessage({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "flaky", arguments: {} }, }); expect((r2!.result as { isError?: boolean }).isError).toBe(true); // Third call - success (server should still work) const r3 = await provider.handleMessage({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "flaky", arguments: {} }, }); expect( (r3!.result as { content: Array<{ text: string }> }).content[0].text, ).toBe("success"); }); });