Skip to main content
Glama
external-server.test.ts22.6 kB
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" import * as http from "http" // Mock the MCP SDK modules before importing our modules vi.mock("@modelcontextprotocol/sdk/server/index.js", () => ({ Server: vi.fn().mockImplementation(() => ({ setRequestHandler: vi.fn(), connect: vi.fn().mockResolvedValue(undefined), })), })) vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: vi.fn().mockImplementation(() => ({ start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), })), })) vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ ListToolsRequestSchema: { method: "tools/list" }, CallToolRequestSchema: { method: "tools/call" }, })) // Import after mocks are set up import { OpenAPIServer } from "../src/server" import { StreamableHttpServerTransport } from "../src/transport/StreamableHttpServerTransport" import type { OpenAPIMCPServerConfig } from "../src/config" import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js" /** * External Server Tests * * These tests verify that the package works correctly when used as a library, * following the usage pattern documented in the README: * * ```typescript * import { OpenAPIServer } from "@ivotoby/openapi-mcp-server" * import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" * * const config = { * name: "my-api-server", * version: "1.0.0", * apiBaseUrl: "https://api.example.com", * openApiSpec: "https://api.example.com/openapi.json", * specInputMethod: "url" as const, * headers: { Authorization: "Bearer your-token" }, * transportType: "stdio" as const, * toolsMode: "all" as const, * } * * const server = new OpenAPIServer(config) * const transport = new StdioServerTransport() * await server.start(transport) * ``` */ describe("External Server - OpenAPIServer", () => { let server: OpenAPIServer | null = null afterEach(() => { server = null vi.clearAllMocks() }) describe("Basic instantiation", () => { it("should create OpenAPIServer with minimal config", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", } server = new OpenAPIServer(config) expect(server).toBeDefined() expect(server).toBeInstanceOf(OpenAPIServer) }) it("should create OpenAPIServer with headers", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", headers: { Authorization: "Bearer test-token", "X-API-Key": "test-api-key", }, transportType: "stdio", toolsMode: "all", } server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should create OpenAPIServer with all toolsMode options", () => { const baseConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, transportType: "stdio" as const, } // toolsMode: "all" const serverAll = new OpenAPIServer({ ...baseConfig, toolsMode: "all" as const }) expect(serverAll).toBeDefined() // toolsMode: "dynamic" const serverDynamic = new OpenAPIServer({ ...baseConfig, toolsMode: "dynamic" as const }) expect(serverDynamic).toBeDefined() // toolsMode: "explicit" const serverExplicit = new OpenAPIServer({ ...baseConfig, toolsMode: "explicit" as const, includeTools: ["GET::users"], }) expect(serverExplicit).toBeDefined() }) it("should create OpenAPIServer with specInputMethod variations", () => { const baseConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", transportType: "stdio" as const, toolsMode: "all" as const, } // specInputMethod: "url" const serverUrl = new OpenAPIServer({ ...baseConfig, openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, }) expect(serverUrl).toBeDefined() // specInputMethod: "file" const serverFile = new OpenAPIServer({ ...baseConfig, openApiSpec: "./openapi.json", specInputMethod: "file" as const, }) expect(serverFile).toBeDefined() // specInputMethod: "inline" const serverInline = new OpenAPIServer({ ...baseConfig, openApiSpec: "inline", specInputMethod: "inline" as const, inlineSpecContent: JSON.stringify({ openapi: "3.0.0", info: { title: "Test", version: "1.0.0" }, paths: {}, }), }) expect(serverInline).toBeDefined() }) }) describe("Transport configuration", () => { it("should accept stdio transport type in config", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", } server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should accept http transport type in config", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "http", httpPort: 3000, httpHost: "127.0.0.1", endpointPath: "/mcp", toolsMode: "all", } server = new OpenAPIServer(config) expect(server).toBeDefined() }) }) describe("Filtering options", () => { it("should accept includeTools filter", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "explicit", includeTools: ["GET::users", "POST::users"], } server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should accept includeTags filter", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", includeTags: ["users", "admin"], } server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should accept includeResources filter", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", includeResources: ["/users", "/orders"], } server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should accept includeOperations filter", () => { const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", includeOperations: ["get", "post"], } server = new OpenAPIServer(config) expect(server).toBeDefined() }) }) }) /** * Tests for the documented external server pattern with mock transport */ describe("External Server - Start with Transport", () => { let server: OpenAPIServer | null = null let mockTransport: Transport beforeEach(() => { mockTransport = { start: vi.fn().mockResolvedValue(undefined), send: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } as unknown as Transport vi.clearAllMocks() }) afterEach(() => { server = null }) it("should start server with custom transport (documented pattern)", async () => { // This tests the exact pattern from the README const config: OpenAPIMCPServerConfig = { name: "my-api-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", headers: { Authorization: "Bearer your-token", "X-API-Key": "your-api-key", }, transportType: "stdio", toolsMode: "all", } server = new OpenAPIServer(config) // Mock the toolsManager.initialize to not actually load specs // @ts-expect-error: accessing private member for testing server.toolsManager.initialize = vi.fn().mockResolvedValue(undefined) // @ts-expect-error: accessing private member for testing server.toolsManager.getToolsWithIds = vi.fn().mockReturnValue([]) // @ts-expect-error: accessing private member for testing server.toolsManager.getOpenApiSpec = vi.fn().mockReturnValue(null) await server.start(mockTransport) // Verify initialize was called // @ts-expect-error: accessing private member for testing expect(server.toolsManager.initialize).toHaveBeenCalled() }) it("should work with StdioServerTransport mock", async () => { const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js") const config: OpenAPIMCPServerConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "stdio", toolsMode: "all", } server = new OpenAPIServer(config) const transport = new StdioServerTransport() // @ts-expect-error: accessing private member for testing server.toolsManager.initialize = vi.fn().mockResolvedValue(undefined) // @ts-expect-error: accessing private member for testing server.toolsManager.getToolsWithIds = vi.fn().mockReturnValue([]) // @ts-expect-error: accessing private member for testing server.toolsManager.getOpenApiSpec = vi.fn().mockReturnValue(null) await server.start(transport) // @ts-expect-error: accessing private member for testing expect(server.toolsManager.initialize).toHaveBeenCalled() }) }) /** * Integration tests for external server with real HTTP transport */ describe("External Server - HTTP Transport Integration", () => { let server: OpenAPIServer | null = null let transport: StreamableHttpServerTransport | null = null let externalServer: http.Server | null = null const TEST_PORT = 3457 const TEST_HOST = "127.0.0.1" const MCP_ENDPOINT = "/mcp" const waitForClose = (ms: number = 100): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)) afterEach(async () => { if (transport) { try { await transport.close() } catch { // Ignore close errors } transport = null } if (externalServer) { await new Promise<void>((resolve) => { externalServer!.close(() => resolve()) }).catch(() => {}) externalServer = null } server = null await waitForClose(50) }) it("should start server with StreamableHttpServerTransport", async () => { const config: OpenAPIMCPServerConfig = { name: "http-test-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "http", httpPort: TEST_PORT, httpHost: TEST_HOST, endpointPath: MCP_ENDPOINT, toolsMode: "dynamic", // Use dynamic to avoid loading actual specs } server = new OpenAPIServer(config) transport = new StreamableHttpServerTransport(TEST_PORT, TEST_HOST, MCP_ENDPOINT) // Mock the toolsManager to avoid network calls // @ts-expect-error: accessing private member for testing server.toolsManager.initialize = vi.fn().mockResolvedValue(undefined) // @ts-expect-error: accessing private member for testing server.toolsManager.getToolsWithIds = vi.fn().mockReturnValue([]) // @ts-expect-error: accessing private member for testing server.toolsManager.getOpenApiSpec = vi.fn().mockReturnValue(null) // Start the transport manually since we're mocking SDK's connect() which normally calls transport.start() await transport.start() await server.start(transport) // Verify server is running by checking health endpoint const response = await makeRequest(`http://${TEST_HOST}:${TEST_PORT}/health`) expect(response.statusCode).toBe(200) const body = JSON.parse(response.body) as { status: string } expect(body.status).toBe("healthy") }) it("should handle MCP initialize request when used as library", async () => { const config: OpenAPIMCPServerConfig = { name: "mcp-library-test", version: "2.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "http", httpPort: TEST_PORT, httpHost: TEST_HOST, endpointPath: MCP_ENDPOINT, toolsMode: "dynamic", } // Create external server before passing to transport externalServer = http.createServer() externalServer.listen(TEST_PORT, TEST_HOST) server = new OpenAPIServer(config) transport = new StreamableHttpServerTransport( TEST_PORT, TEST_HOST, MCP_ENDPOINT, externalServer, ) // @ts-expect-error: accessing private member for testing server.toolsManager.initialize = vi.fn().mockResolvedValue(undefined) // @ts-expect-error: accessing private member for testing server.toolsManager.getToolsWithIds = vi.fn().mockReturnValue([]) // @ts-expect-error: accessing private member for testing server.toolsManager.getOpenApiSpec = vi.fn().mockReturnValue(null) await server.start(transport) // Test MCP endpoint responds correctly const response = await makeRequest(`http://${TEST_HOST}:${TEST_PORT}${MCP_ENDPOINT}`, { method: "POST", headers: { "Content-Type": "text/plain" }, body: "{}", }) // Should reject non-JSON content type expect(response.statusCode).toBe(415) }) it("should work with external http.Server passed to transport", async () => { const config: OpenAPIMCPServerConfig = { name: "external-server-test", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url", transportType: "http", httpPort: TEST_PORT, httpHost: TEST_HOST, endpointPath: MCP_ENDPOINT, toolsMode: "dynamic", } // Create external server with custom routes let customRouteHit = false externalServer = http.createServer((req, res) => { if (req.url === "/api/custom") { customRouteHit = true res.writeHead(200, { "Content-Type": "application/json" }) res.end(JSON.stringify({ custom: true })) return } // Other requests will be handled by the MCP handler attached later. }) externalServer.listen(TEST_PORT, TEST_HOST) server = new OpenAPIServer(config) transport = new StreamableHttpServerTransport( TEST_PORT, TEST_HOST, MCP_ENDPOINT, externalServer, ) // @ts-expect-error: accessing private member for testing server.toolsManager.initialize = vi.fn().mockResolvedValue(undefined) // @ts-expect-error: accessing private member for testing server.toolsManager.getToolsWithIds = vi.fn().mockReturnValue([]) // @ts-expect-error: accessing private member for testing server.toolsManager.getOpenApiSpec = vi.fn().mockReturnValue(null) await server.start(transport) // Custom route should work const customResponse = await makeRequest(`http://${TEST_HOST}:${TEST_PORT}/api/custom`) expect(customResponse.statusCode).toBe(200) expect(customRouteHit).toBe(true) // Health endpoint should also work const healthResponse = await makeRequest(`http://${TEST_HOST}:${TEST_PORT}/health`) expect(healthResponse.statusCode).toBe(200) }) }) /** * Tests for module exports - verify all expected exports are available */ describe("External Server - Module Exports", () => { it("should export OpenAPIServer class", async () => { const { OpenAPIServer } = await import("../src/index") expect(OpenAPIServer).toBeDefined() expect(typeof OpenAPIServer).toBe("function") }) it("should export StreamableHttpServerTransport class", async () => { const { StreamableHttpServerTransport } = await import("../src/index") expect(StreamableHttpServerTransport).toBeDefined() expect(typeof StreamableHttpServerTransport).toBe("function") }) it("should export ApiClient class", async () => { const { ApiClient } = await import("../src/index") expect(ApiClient).toBeDefined() expect(typeof ApiClient).toBe("function") }) it("should export ToolsManager class", async () => { const { ToolsManager } = await import("../src/index") expect(ToolsManager).toBeDefined() expect(typeof ToolsManager).toBe("function") }) it("should export config utilities", async () => { const { loadConfig, parseHeaders } = await import("../src/index") expect(loadConfig).toBeDefined() expect(parseHeaders).toBeDefined() expect(typeof loadConfig).toBe("function") expect(typeof parseHeaders).toBe("function") }) it("should export auth provider classes", async () => { const { StaticAuthProvider } = await import("../src/index") expect(StaticAuthProvider).toBeDefined() expect(typeof StaticAuthProvider).toBe("function") }) it("should export OpenAPISpecLoader", async () => { const { OpenAPISpecLoader } = await import("../src/index") expect(OpenAPISpecLoader).toBeDefined() expect(typeof OpenAPISpecLoader).toBe("function") }) }) /** * Tests for documented usage examples from README */ describe("External Server - README Examples", () => { it("should work with the basic external server example pattern", () => { // This mirrors the basic-library-usage example const config = { name: "my-api-mcp-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, headers: { Authorization: "Bearer your-api-token", "X-API-Key": "your-api-key", "User-Agent": "MyApp/1.0.0", }, transportType: "stdio" as const, toolsMode: "all" as const, } const server = new OpenAPIServer(config) expect(server).toBeDefined() // Verify the config was accepted without errors // @ts-expect-error: accessing private member for testing expect(server.toolsManager).toBeDefined() // @ts-expect-error: accessing private member for testing expect(server.apiClient).toBeDefined() }) it("should work with dynamic tools mode", () => { const config = { name: "dynamic-tools-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, transportType: "stdio" as const, toolsMode: "dynamic" as const, } const server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should work with explicit tools mode", () => { const config = { name: "explicit-tools-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, transportType: "stdio" as const, toolsMode: "explicit" as const, includeTools: ["GET::users", "POST::users"], } const server = new OpenAPIServer(config) expect(server).toBeDefined() }) it("should work with HTTP transport configuration", () => { const config = { name: "http-server", version: "1.0.0", apiBaseUrl: "https://api.example.com", openApiSpec: "https://api.example.com/openapi.json", specInputMethod: "url" as const, transportType: "http" as const, httpPort: 3000, httpHost: "127.0.0.1", endpointPath: "/mcp", toolsMode: "all" as const, } const server = new OpenAPIServer(config) expect(server).toBeDefined() }) }) /** * Helper function to make HTTP requests */ function makeRequest( url: string, options: { method?: string headers?: Record<string, string> body?: string } = {}, ): Promise<{ statusCode: number; body: string; headers: http.IncomingHttpHeaders }> { return new Promise((resolve, reject) => { const urlObj = new URL(url) const reqOptions: http.RequestOptions = { hostname: urlObj.hostname, port: urlObj.port, path: urlObj.pathname, method: options.method || "GET", headers: options.headers || {}, } const req = http.request(reqOptions, (res) => { let body = "" res.on("data", (chunk: Buffer) => { body += chunk.toString() }) res.on("end", () => { resolve({ statusCode: res.statusCode || 0, body, headers: res.headers, }) }) }) req.on("error", reject) req.setTimeout(5000, () => { req.destroy() reject(new Error("Request timeout")) }) if (options.body) { req.write(options.body) } req.end() }) }

Latest Blog Posts

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