Skip to main content
Glama

OpenAPI MCP Server

MIT License
2,130
150
  • Apple
import { describe, it, expect, beforeEach, vi } from "vitest" import type { OpenAPIMCPServerConfig } from "../src/config" import type { AuthProvider } from "../src/auth-provider" vi.mock("@modelcontextprotocol/sdk/server/index.js") vi.mock("@modelcontextprotocol/sdk/server/transport.js") vi.mock("@modelcontextprotocol/sdk/types.js") vi.mock("../src/tools-manager") vi.mock("../src/api-client") import { Server } from "@modelcontextprotocol/sdk/server/index.js" import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js" // Create a dummy interface that implements the Transport interface interface ServerTransport extends Transport { start(): Promise<void> send(message: any): Promise<void> close(): Promise<void> } import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { ToolsManager } from "../src/tools-manager" import { ApiClient } from "../src/api-client" import { OpenAPIServer } from "../src/server" const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "http://localhost", openApiSpec: "spec.yaml", headers: { Authorization: "Bearer token" }, transportType: "stdio", httpPort: 3000, httpHost: "127.0.0.1", endpointPath: "/mcp", toolsMode: "all", specInputMethod: "file", } describe("OpenAPIServer", () => { let server: OpenAPIServer let mockServer: { setRequestHandler: Mock; connect: Mock } let mockToolsManager: { initialize: Mock getAllTools: Mock findTool: Mock getToolsWithIds: Mock getSpecLoader: Mock getOpenApiSpec: Mock } let mockApiClient: { executeApiCall: Mock; setTools: Mock; setOpenApiSpec: Mock } type Mock = ReturnType<typeof vi.fn> beforeEach(() => { vi.clearAllMocks() const mockSetRequestHandler = vi.fn() const mockConnect = vi.fn() vi.mocked(Server).mockImplementation( () => ({ setRequestHandler: mockSetRequestHandler, connect: mockConnect, }) as any, ) vi.mocked(ToolsManager).mockImplementation( () => ({ initialize: vi.fn(), getAllTools: vi.fn().mockReturnValue([]), findTool: vi.fn(), getToolsWithIds: vi.fn().mockReturnValue([]), getSpecLoader: vi.fn().mockReturnValue({}), // Return a mock spec loader object getOpenApiSpec: vi.fn(), }) as any, ) vi.mocked(ApiClient).mockImplementation( () => ({ executeApiCall: vi.fn(), setTools: vi.fn(), setOpenApiSpec: vi.fn(), }) as any, ) server = new OpenAPIServer(config) // @ts-expect-error: access private for test mockServer = server.server // @ts-expect-error: access private for test mockToolsManager = server.toolsManager // @ts-expect-error: access private for test mockApiClient = server.apiClient }) it("should construct with config", () => { expect(server).toBeInstanceOf(OpenAPIServer) }) it("should register tool listing handler", () => { expect(mockServer.setRequestHandler).toHaveBeenCalledWith( ListToolsRequestSchema, expect.any(Function), ) }) it("should handle tool listing", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[0][1] vi.mocked(mockToolsManager.getAllTools).mockReturnValue([{ name: "tool1" }]) const result = await handler() expect(result.tools).toEqual([{ name: "tool1" }]) }) describe("Tool Execution", () => { it("should handle tool execution success", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::ping", tool: { name: "ping" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ ok: true }) const req = { params: { id: "get::ping", arguments: { foo: "bar" } } } const result = await handler(req) expect(result.content[0].text).toContain("ok") }) it("should explicitly pass request arguments to API client", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "post::users", tool: { name: "create-user" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ id: 123, name: "John" }) const testArguments = { name: "John Doe", email: "john@example.com", age: 30, metadata: { role: "admin", department: "engineering" }, } const req = { params: { id: "post::users", arguments: testArguments } } await handler(req) // Verify that executeApiCall was called with the exact arguments from the request expect(mockApiClient.executeApiCall).toHaveBeenCalledWith("post::users", testArguments) expect(mockApiClient.executeApiCall).toHaveBeenCalledTimes(1) }) it("should handle empty arguments object", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::status", tool: { name: "get-status" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ status: "ok" }) const req = { params: { id: "get::status", arguments: {} } } await handler(req) // Verify that executeApiCall was called with empty arguments expect(mockApiClient.executeApiCall).toHaveBeenCalledWith("get::status", {}) }) it("should handle undefined arguments", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::health", tool: { name: "health-check" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ healthy: true }) const req = { params: { id: "get::health" } } // No arguments property await handler(req) // Verify that executeApiCall was called with empty object when arguments is undefined expect(mockApiClient.executeApiCall).toHaveBeenCalledWith("get::health", {}) }) it("should handle complex nested arguments", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "put::users-123", tool: { name: "update-user" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ updated: true }) const complexArguments = { user: { profile: { name: "Jane Smith", contact: { email: "jane@example.com", phone: "+1-555-0123", }, }, preferences: { notifications: ["email", "sms"], theme: "dark", }, }, metadata: { lastModified: "2023-12-01T10:00:00Z", version: 2, }, } const req = { params: { id: "put::users-123", arguments: complexArguments } } await handler(req) // Verify that executeApiCall preserves complex argument structure expect(mockApiClient.executeApiCall).toHaveBeenCalledWith("put::users-123", complexArguments) }) it("should handle tool not found", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue(undefined) vi.mocked(mockToolsManager.getAllTools).mockReturnValue([{ name: "ping" }]) const req = { params: { id: "not-exist", arguments: {} } } await expect(handler(req)).rejects.toThrow("Tool not found: not-exist") }) it("should handle tool execution error", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::ping", tool: { name: "ping" }, }) vi.mocked(mockApiClient.executeApiCall).mockRejectedValue(new Error("fail")) const req = { params: { id: "get::ping", arguments: {} } } const result = await handler(req) expect(result.isError).toBe(true) expect(result.content[0].text).toContain("fail") }) it("should handle non-Error exceptions during tool execution", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::ping", tool: { name: "ping" }, }) vi.mocked(mockApiClient.executeApiCall).mockRejectedValue("string error") const req = { params: { id: "get::ping", arguments: {} } } // Non-Error exceptions should be re-thrown await expect(handler(req)).rejects.toBe("string error") }) it("should require tool ID or name", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] const req = { params: { arguments: {} } } // No id or name await expect(handler(req)).rejects.toThrow("Tool ID or name is required") }) it("should handle tool lookup by name", async () => { const handler = vi.mocked(mockServer.setRequestHandler).mock.calls[1][1] vi.mocked(mockToolsManager.findTool).mockReturnValue({ toolId: "get::ping", tool: { name: "ping" }, }) vi.mocked(mockApiClient.executeApiCall).mockResolvedValue({ pong: true }) const req = { params: { name: "ping", arguments: { test: "value" } } } await handler(req) expect(mockToolsManager.findTool).toHaveBeenCalledWith("ping") expect(mockApiClient.executeApiCall).toHaveBeenCalledWith("get::ping", { test: "value" }) }) }) describe("Server Lifecycle", () => { it("should start the server successfully", async () => { vi.mocked(mockToolsManager.initialize).mockResolvedValue(undefined) vi.mocked(mockServer.connect).mockResolvedValue(undefined) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await expect(server.start(transport)).resolves.toBeUndefined() expect(mockToolsManager.initialize).toHaveBeenCalled() expect(mockServer.connect).toHaveBeenCalledWith(transport) }) it("should handle ToolsManager initialization failure", async () => { const initError = new Error("Failed to load OpenAPI spec") vi.mocked(mockToolsManager.initialize).mockRejectedValue(initError) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await expect(server.start(transport)).rejects.toThrow("Failed to load OpenAPI spec") // Verify that server.connect was not called when initialization fails expect(mockServer.connect).not.toHaveBeenCalled() expect(mockApiClient.setTools).not.toHaveBeenCalled() }) it("should handle ToolsManager initialization failure with network error", async () => { const networkError = new Error("ENOTFOUND api.example.com") networkError.name = "NetworkError" vi.mocked(mockToolsManager.initialize).mockRejectedValue(networkError) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await expect(server.start(transport)).rejects.toThrow("ENOTFOUND api.example.com") expect(mockServer.connect).not.toHaveBeenCalled() }) it("should handle Server.connect() failure", async () => { vi.mocked(mockToolsManager.initialize).mockResolvedValue(undefined) const connectError = new Error("Transport connection failed") vi.mocked(mockServer.connect).mockRejectedValue(connectError) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await expect(server.start(transport)).rejects.toThrow("Transport connection failed") // Verify that initialization completed but connection failed expect(mockToolsManager.initialize).toHaveBeenCalled() expect(mockApiClient.setTools).toHaveBeenCalled() expect(mockServer.connect).toHaveBeenCalledWith(transport) }) it("should handle Server.connect() failure with timeout", async () => { vi.mocked(mockToolsManager.initialize).mockResolvedValue(undefined) const timeoutError = new Error("Connection timeout") timeoutError.name = "TimeoutError" vi.mocked(mockServer.connect).mockRejectedValue(timeoutError) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await expect(server.start(transport)).rejects.toThrow("Connection timeout") expect(mockToolsManager.initialize).toHaveBeenCalled() expect(mockServer.connect).toHaveBeenCalledWith(transport) }) it("should provide tools with their IDs to the API client when starting the server", async () => { vi.mocked(mockToolsManager.initialize).mockResolvedValue(undefined) vi.mocked(mockServer.connect).mockResolvedValue(undefined) // Mock the tools with their IDs const mockToolsWithIds = [ ["GET::users", { name: "list-users" }], ["POST::users", { name: "create-user" }], ] vi.mocked(mockToolsManager.getToolsWithIds).mockReturnValue(mockToolsWithIds) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await server.start(transport) // Verify that getToolsWithIds is called expect(mockToolsManager.getToolsWithIds).toHaveBeenCalled() // Verify that setTools is called with a map containing the correct tool IDs and tools const expectedToolsMap = new Map([ ["GET::users", { name: "list-users" }], ["POST::users", { name: "create-user" }], ]) expect(mockApiClient.setTools).toHaveBeenCalledWith(expectedToolsMap) }) it("should handle empty tools list during startup", async () => { vi.mocked(mockToolsManager.initialize).mockResolvedValue(undefined) vi.mocked(mockServer.connect).mockResolvedValue(undefined) vi.mocked(mockToolsManager.getToolsWithIds).mockReturnValue([]) const transport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as ServerTransport await server.start(transport) // Verify that setTools is called with an empty map expect(mockApiClient.setTools).toHaveBeenCalledWith(new Map()) }) // Note: The OpenAPIServer class doesn't currently have a close/stop method // This test documents the expected behavior if such a method were to be added it("should document expected close/stop lifecycle behavior", () => { // Currently, OpenAPIServer doesn't have a close() or stop() method // If added in the future, it should: // 1. Close any open connections // 2. Clean up resources // 3. Potentially call transport.close() if available expect(typeof (server as any).close).toBe("undefined") expect(typeof (server as any).stop).toBe("undefined") // This test serves as documentation for future implementation // When close/stop methods are added, they should be tested here }) }) it("should advertise tools capabilities in initialization response", () => { // Verify the server was constructed with the correct capabilities expect(Server).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ capabilities: { tools: { list: true, execute: true, }, }, }), ) }) describe("AuthProvider Integration", () => { it("should use AuthProvider when provided in config", () => { const mockAuthProvider: AuthProvider = { getAuthHeaders: vi.fn().mockResolvedValue({ Authorization: "Bearer token" }), handleAuthError: vi.fn().mockResolvedValue(false), } const configWithAuthProvider: OpenAPIMCPServerConfig = { ...config, authProvider: mockAuthProvider, } new OpenAPIServer(configWithAuthProvider) // Verify ApiClient was constructed with the AuthProvider expect(ApiClient).toHaveBeenCalledWith(config.apiBaseUrl, mockAuthProvider, expect.anything()) }) it("should use StaticAuthProvider with headers when no AuthProvider provided", () => { const configWithHeaders: OpenAPIMCPServerConfig = { ...config, headers: { "X-API-Key": "test-key" }, } new OpenAPIServer(configWithHeaders) // Verify ApiClient was constructed with a StaticAuthProvider expect(ApiClient).toHaveBeenCalledWith( config.apiBaseUrl, expect.objectContaining({ getAuthHeaders: expect.any(Function), handleAuthError: expect.any(Function), }), expect.anything(), ) }) it("should prefer AuthProvider over headers when both are provided", () => { const mockAuthProvider: AuthProvider = { getAuthHeaders: vi.fn().mockResolvedValue({ Authorization: "Bearer token" }), handleAuthError: vi.fn().mockResolvedValue(false), } const configWithBoth: OpenAPIMCPServerConfig = { ...config, headers: { "X-API-Key": "should-be-ignored" }, authProvider: mockAuthProvider, } new OpenAPIServer(configWithBoth) // Verify ApiClient was constructed with the AuthProvider, not the headers expect(ApiClient).toHaveBeenCalledWith(config.apiBaseUrl, mockAuthProvider, expect.anything()) }) }) })

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/ivo-toby/mcp-openapi-server'

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