alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
407 lines (323 loc) • 10.7 kB
text/typescript
import { Alepha, z } from "alepha";
import { describe, expect, test } from "vitest";
import {
$tool,
AlephaMcp,
type McpContext,
McpServerProvider,
} from "../index.ts";
// ---------------------------------------------------------------------------------------------------------------------
describe("$tool primitive", () => {
test("should register tool with McpServerProvider", 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);
const tool = provider.getTool("add");
expect(tool).toBeDefined();
expect(tool?.name).toBe("add");
expect(tool?.description).toBe("Add two numbers");
});
test("should use custom name when provided", async () => {
const alepha = Alepha.create();
class Tools {
myTool = $tool({
name: "custom-tool-name",
description: "A custom tool",
handler: async () => "result",
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("custom-tool-name");
expect(tool).toBeDefined();
expect(tool?.name).toBe("custom-tool-name");
});
test("should execute tool and return result", async () => {
const alepha = Alepha.create();
class Calculator {
multiply = $tool({
description: "Multiply 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);
const tool = provider.getTool("multiply");
const result = await tool?.execute({ a: 3, b: 4 });
expect(result).toBe(12);
});
test("should validate params with schema", async () => {
const alepha = Alepha.create();
class Tools {
greet = $tool({
description: "Greet someone",
schema: {
params: z.object({
name: z.text(),
}),
result: z.text(),
},
handler: async ({ params }) => `Hello, ${params.name}!`,
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("greet");
const result = await tool?.execute({ name: "World" });
expect(result).toBe("Hello, World!");
});
test("should throw error for invalid params", async () => {
const alepha = Alepha.create();
class Tools {
typed = $tool({
description: "Typed tool",
schema: {
params: z.object({
count: z.number(),
}),
},
handler: async ({ params }) => params.count * 2,
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("typed");
// Pass invalid type - should throw validation error
await expect(tool?.execute({ count: "not-a-number" })).rejects.toThrow();
});
test("should work without schema", async () => {
const alepha = Alepha.create();
class Tools {
simple = $tool({
description: "Simple tool",
handler: async () => "done",
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("simple");
const result = await tool?.execute({});
expect(result).toBe("done");
});
test("should generate correct descriptor", async () => {
const alepha = Alepha.create();
class Tools {
complex = $tool({
name: "complex-tool",
description: "A complex tool with params",
schema: {
params: z.object({
required: z.text(),
optional: z.number().optional(),
}),
result: z.boolean(),
},
handler: async () => true,
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("complex-tool");
const descriptor = tool?.toDescriptor();
expect(descriptor?.name).toBe("complex-tool");
expect(descriptor?.description).toBe("A complex tool with params");
expect(descriptor?.inputSchema.type).toBe("object");
expect(descriptor?.inputSchema.required).toEqual(["required"]);
expect(descriptor?.inputSchema.properties).toBeDefined();
expect((descriptor?.inputSchema.properties as any)?.required?.type).toBe(
"string",
);
expect((descriptor?.inputSchema.properties as any)?.optional?.type).toBe(
"number",
);
});
test("should handle async handlers", async () => {
const alepha = Alepha.create();
class Tools {
async = $tool({
description: "Async tool",
schema: {
params: z.object({
delay: z.number(),
}),
result: z.text(),
},
handler: async ({ params }) => {
await new Promise((resolve) => setTimeout(resolve, params.delay));
return "completed";
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("async");
const result = await tool?.execute({ delay: 10 });
expect(result).toBe("completed");
});
test("should handle void result", async () => {
const alepha = Alepha.create();
let called = false;
class Tools {
void = $tool({
description: "Void tool",
handler: async () => {
called = true;
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("void");
const result = await tool?.execute({});
expect(called).toBe(true);
expect(result).toBeUndefined();
});
test("should propagate handler errors", async () => {
const alepha = Alepha.create();
class Tools {
failing = $tool({
description: "Failing tool",
handler: async () => {
throw new Error("Tool failed");
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("failing");
await expect(tool?.execute({})).rejects.toThrow("Tool failed");
});
// -----------------------------------------------------------------------------------------------------------------
// Context tests
// -----------------------------------------------------------------------------------------------------------------
test("should receive context in handler", async () => {
const alepha = Alepha.create();
let receivedContext: McpContext | undefined;
class Tools {
contextTool = $tool({
description: "Context tool",
handler: async ({ context }) => {
receivedContext = context;
return "done";
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("contextTool");
const testContext: McpContext = {
headers: { authorization: "Bearer test-token" },
};
await tool?.execute({}, testContext);
expect(receivedContext).toBeDefined();
expect(receivedContext?.headers?.authorization).toBe("Bearer test-token");
});
test("should receive context with custom data", async () => {
const alepha = Alepha.create();
interface AuthContext {
userId: string;
projectId: number;
}
let receivedData: AuthContext | undefined;
class Tools {
authTool = $tool({
description: "Authenticated tool",
handler: async ({ context }) => {
receivedData = context?.data as AuthContext;
return `User: ${receivedData?.userId}`;
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("authTool");
const testContext: McpContext<AuthContext> = {
headers: {},
data: { userId: "user-123", projectId: 42 },
};
const result = await tool?.execute({}, testContext);
expect(result).toBe("User: user-123");
expect(receivedData).toEqual({ userId: "user-123", projectId: 42 });
});
test("should work without context", async () => {
const alepha = Alepha.create();
let contextWasUndefined = false;
class Tools {
noContext = $tool({
description: "No context tool",
handler: async ({ context }) => {
contextWasUndefined = context === undefined;
return "done";
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("noContext");
await tool?.execute({});
expect(contextWasUndefined).toBe(true);
});
test("should use context for authentication in handler", async () => {
const alepha = Alepha.create();
class Tools {
protected = $tool({
description: "Protected tool",
schema: {
result: z.text(),
},
handler: async ({ context }) => {
const authHeader = context?.headers?.authorization;
if (!authHeader?.toString().startsWith("Bearer ")) {
throw new Error("Unauthorized");
}
return "Access granted";
},
});
}
alepha.with(AlephaMcp).with(Tools);
await alepha.start();
const provider = alepha.inject(McpServerProvider);
const tool = provider.getTool("protected");
// Without auth - should fail
await expect(tool?.execute({})).rejects.toThrow("Unauthorized");
// With auth - should succeed
const result = await tool?.execute(
{},
{ headers: { authorization: "Bearer valid-token" } },
);
expect(result).toBe("Access granted");
});
});