Skip to main content
Glama

Peekaboo MCP

by steipete
automation-tools.test.tsβ€’9.31 kB
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest"; import { spawn, ChildProcess } from "child_process"; import * as path from "path"; import { rm } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; const SERVER_PATH = path.resolve(process.cwd(), "dist/index.js"); // Simple JSON-RPC client for testing class JSONRPCClient { private proc: ChildProcess; private messageId = 1; private responseHandlers = new Map<number, (response: any) => void>(); private buffer = ""; constructor() { this.proc = spawn("node", [SERVER_PATH], { stdio: ["pipe", "pipe", "inherit"], }); this.proc.stdout?.on("data", (data) => { this.buffer += data.toString(); this.processBuffer(); }); } private processBuffer() { const lines = this.buffer.split("\n"); this.buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { try { const message = JSON.parse(line); if (message.id && this.responseHandlers.has(message.id)) { const handler = this.responseHandlers.get(message.id)!; this.responseHandlers.delete(message.id); handler(message); } } catch (e) { // Ignore non-JSON lines } } } } async request(method: string, params: any = {}): Promise<any> { const id = this.messageId++; const request = { jsonrpc: "2.0", id, method, params, }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.responseHandlers.delete(id); reject(new Error(`Request timeout for method: ${method}`)); }, 10000); // Increased timeout to 10 seconds this.responseHandlers.set(id, (response) => { clearTimeout(timeout); if (response.error) { reject(response.error); } else { resolve(response.result); } }); this.proc.stdin?.write(JSON.stringify(request) + "\n"); }); } close() { this.proc.kill(); } } describe("Automation Tools", () => { let client: JSONRPCClient; beforeAll(() => { client = new JSONRPCClient(); }); afterAll(() => { client.close(); }); describe("tools/list", () => { it("should list all automation tools", async () => { const result = await client.request("tools/list"); expect(result.tools).toBeInstanceOf(Array); const toolNames = result.tools.map((t: any) => t.name); // Verify all new spec v3 tools are present expect(toolNames).toContain("see"); expect(toolNames).toContain("click"); expect(toolNames).toContain("type"); expect(toolNames).toContain("scroll"); expect(toolNames).toContain("hotkey"); expect(toolNames).toContain("swipe"); expect(toolNames).toContain("run"); expect(toolNames).toContain("sleep"); // Also verify existing tools are still there expect(toolNames).toContain("image"); expect(toolNames).toContain("analyze"); expect(toolNames).toContain("list"); }); it("should have proper descriptions for new tools", async () => { const result = await client.request("tools/list"); const tools = result.tools; const seeToolInfo = tools.find((t: any) => t.name === "see"); expect(seeToolInfo).toBeDefined(); expect(seeToolInfo.title).toContain("UI"); expect(seeToolInfo.description).toContain("screenshot"); expect(seeToolInfo.description).toContain("element"); const clickToolInfo = tools.find((t: any) => t.name === "click"); expect(clickToolInfo).toBeDefined(); expect(clickToolInfo.title).toContain("Click"); expect(clickToolInfo.description).toContain("element"); const typeToolInfo = tools.find((t: any) => t.name === "type"); expect(typeToolInfo).toBeDefined(); expect(typeToolInfo.title).toContain("Type"); expect(typeToolInfo.description).toContain("text"); }); }); describe("tool schemas", () => { it("should have valid schemas for all new tools", async () => { const result = await client.request("tools/list"); const tools = result.tools; const expectedSchemas = { see: ["app_target", "path", "session", "annotate"], click: ["query", "on", "coords", "session", "wait_for", "double", "right"], type: ["text", "on", "session", "clear", "delay", "press_return", "tab", "escape", "delete"], scroll: ["direction", "amount", "on", "session", "delay", "smooth"], hotkey: ["keys", "hold_duration"], swipe: ["from", "to", "duration", "steps"], run: ["script_path", "output", "no_fail_fast", "verbose"], sleep: ["duration"] }; for (const [toolName, expectedProps] of Object.entries(expectedSchemas)) { const toolInfo = tools.find((t: any) => t.name === toolName); expect(toolInfo).toBeDefined(); expect(toolInfo.inputSchema).toBeDefined(); expect(toolInfo.inputSchema.type).toBe("object"); const properties = Object.keys(toolInfo.inputSchema.properties || {}); for (const prop of expectedProps) { expect(properties).toContain(prop); } } }); it("should have correct required fields", async () => { const result = await client.request("tools/list"); const tools = result.tools; // Check required fields for key tools const typeToolInfo = tools.find((t: any) => t.name === "type"); // Text is optional for type tool - you can use special keys without text expect(typeToolInfo.inputSchema.required).toBeDefined(); expect(Array.isArray(typeToolInfo.inputSchema.required)).toBe(true); const scrollToolInfo = tools.find((t: any) => t.name === "scroll"); expect(scrollToolInfo.inputSchema.required).toContain("direction"); const hotkeyToolInfo = tools.find((t: any) => t.name === "hotkey"); expect(hotkeyToolInfo.inputSchema.required).toContain("keys"); const sleepToolInfo = tools.find((t: any) => t.name === "sleep"); expect(sleepToolInfo.inputSchema.required).toContain("duration"); }); }); describe("tool execution", () => { it("should execute sleep tool", async () => { const startTime = Date.now(); const result = await client.request("tools/call", { name: "sleep", arguments: { duration: 200 } }); const duration = Date.now() - startTime; expect(result.content).toBeInstanceOf(Array); expect(result.content[0].type).toBe("text"); expect(result.content[0].text).toContain("Paused"); expect(duration).toBeGreaterThanOrEqual(200); expect(result.isError).toBeFalsy(); }); it("should validate tool arguments", async () => { // Test missing required argument - use sleep tool which requires duration const result = await client.request("tools/call", { name: "sleep", arguments: {} // Missing required 'duration' field }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Invalid arguments"); }); it("should handle see tool with minimal arguments", async () => { const result = await client.request("tools/call", { name: "see", arguments: {} }); // The tool might fail due to permissions, but should at least be callable expect(result.content).toBeInstanceOf(Array); expect(result.content[0].type).toBe("text"); // If it succeeds, verify the response format if (!result.isError) { expect(result._meta).toBeDefined(); expect(result._meta.session_id).toBeTruthy(); } }); }); describe("click tool validation", () => { beforeEach(async () => { // Clean up any existing sessions to ensure consistent test behavior const sessionDir = join(homedir(), ".peekaboo/session"); try { await rm(sessionDir, { recursive: true, force: true }); } catch (error) { // Directory might not exist, which is fine } }); it("should require at least one target parameter", async () => { const result = await client.request("tools/call", { name: "click", arguments: {} // Missing query, on, or coords }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Must specify either"); }); it("should accept different target types", async () => { const targets = [ { query: "Button", wait_for: 100 }, // Reduce wait time for tests { on: "B1", wait_for: 100 }, { coords: "100,200", wait_for: 100 } ]; for (const target of targets) { const result = await client.request("tools/call", { name: "click", arguments: target }); // Should at least parse the arguments correctly expect(result.content).toBeInstanceOf(Array); // Will fail due to missing session or element, which is expected if (result.isError) { expect(result.content[0].text).toMatch(/session|Session|No actionable element found|UI element not found/); } } }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server