Skip to main content
Glama

OpenAPI MCP Server

api-client.test.ts33 kB
import { describe, it, expect, vi, beforeEach } from "vitest" import { ApiClient } from "../src/api-client.js" import { StaticAuthProvider } from "../src/auth-provider.js" import { OpenAPISpecLoader } from "../src/openapi-loader.js" import { Tool } from "@modelcontextprotocol/sdk/types.js" describe("ApiClient Dynamic Meta-Tools", () => { let apiClient: ApiClient let mockAxios: any let mockSpecLoader: OpenAPISpecLoader beforeEach(() => { mockAxios = { create: vi.fn().mockReturnThis(), request: vi.fn(), get: vi.fn(), post: vi.fn(), } mockSpecLoader = new OpenAPISpecLoader() apiClient = new ApiClient("https://api.example.com", new StaticAuthProvider(), mockSpecLoader) // Mock the axios instance ;(apiClient as any).axiosInstance = mockAxios }) describe("LIST-API-ENDPOINTS", () => { it("should handle LIST-API-ENDPOINTS meta-tool without making HTTP request", async () => { const openApiSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/users": { get: { summary: "Get users", description: "Retrieve all users", responses: {}, }, post: { summary: "Create user", description: "Create a new user", responses: {}, }, }, "/users/{id}": { get: { summary: "Get user by ID", description: "Retrieve a specific user", responses: {}, }, }, }, } apiClient.setOpenApiSpec(openApiSpec) const result = await apiClient.executeApiCall("LIST-API-ENDPOINTS", {}) expect(result).toEqual({ endpoints: [ { method: "GET", path: "/users", summary: "Get users", description: "Retrieve all users", operationId: "", tags: [], }, { method: "POST", path: "/users", summary: "Create user", description: "Create a new user", operationId: "", tags: [], }, { method: "GET", path: "/users/{id}", summary: "Get user by ID", description: "Retrieve a specific user", operationId: "", tags: [], }, ], total: 3, note: "Use INVOKE-API-ENDPOINT to call specific endpoints with the path parameter", }) // Verify no HTTP request was made expect(mockAxios.request).not.toHaveBeenCalled() expect(mockAxios.get).not.toHaveBeenCalled() expect(mockAxios.post).not.toHaveBeenCalled() }) it("should work without OpenAPI spec using fallback", async () => { const tools = new Map<string, Tool>([ [ "GET::users", { name: "Get Users", description: "List all users", inputSchema: { type: "object", properties: {} }, }, ], [ "POST::users", { name: "Create User", description: "Create a user", inputSchema: { type: "object", properties: {} }, }, ], ]) apiClient.setTools(tools) const result = await apiClient.executeApiCall("LIST-API-ENDPOINTS", {}) expect(result.endpoints).toHaveLength(2) expect(result.note).toContain("Limited endpoint information") }) }) describe("GET-API-ENDPOINT-SCHEMA", () => { it("should handle GET-API-ENDPOINT-SCHEMA meta-tool", async () => { const openApiSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/users": { get: { summary: "Get users", description: "Retrieve all users", parameters: [ { name: "page", in: "query", schema: { type: "integer" }, }, ], responses: {}, }, }, }, } apiClient.setOpenApiSpec(openApiSpec as any) const result = await apiClient.executeApiCall("GET-API-ENDPOINT-SCHEMA", { endpoint: "/users", }) expect(result.path).toBe("/users") expect(result.operations).toHaveLength(1) expect(result.operations[0].method).toBe("GET") expect(result.operations[0].summary).toBe("Get users") }) }) describe("INVOKE-API-ENDPOINT", () => { it("should handle INVOKE-API-ENDPOINT meta-tool with direct HTTP request", async () => { const openApiSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/users": { get: { summary: "Get users", }, }, }, } apiClient.setOpenApiSpec(openApiSpec as any) mockAxios.request.mockResolvedValue({ data: [{ id: 1, name: "John" }] }) const result = await apiClient.executeApiCall("INVOKE-API-ENDPOINT", { endpoint: "/users", method: "GET", params: { page: 1 }, }) expect(result).toEqual([{ id: 1, name: "John" }]) expect(mockAxios.request).toHaveBeenCalledWith({ method: "get", url: "/users", headers: {}, params: { page: 1 }, }) }) }) it("should provide specific error messages for GET-API-ENDPOINT-SCHEMA with missing endpoint", async () => { const tools = new Map([ [ "GET-API-ENDPOINT-SCHEMA", { name: "get-api-endpoint-schema", description: "Get schema", inputSchema: { type: "object" as const, properties: {} }, } as any, ], ]) apiClient.setTools(tools) await expect(apiClient.executeApiCall("GET-API-ENDPOINT-SCHEMA", {})).rejects.toThrow( "Missing required parameter 'endpoint' for tool 'GET-API-ENDPOINT-SCHEMA'", ) }) it("should provide specific error messages for INVOKE-API-ENDPOINT with missing endpoint", async () => { const tools = new Map([ [ "INVOKE-API-ENDPOINT", { name: "invoke-api-endpoint", description: "Invoke endpoint", inputSchema: { type: "object" as const, properties: {} }, } as any, ], ]) apiClient.setTools(tools) await expect(apiClient.executeApiCall("INVOKE-API-ENDPOINT", {})).rejects.toThrow( "Missing required parameter 'endpoint' for tool 'INVOKE-API-ENDPOINT'", ) }) it("should provide specific error messages for GET-API-ENDPOINT-SCHEMA with invalid endpoint", async () => { const tools = new Map([ [ "GET-API-ENDPOINT-SCHEMA", { name: "get-api-endpoint-schema", description: "Get schema", inputSchema: { type: "object" as const, properties: {} }, } as any, ], ]) apiClient.setTools(tools) // Mock OpenAPI spec with no matching endpoint const mockSpec = { openapi: "3.0.0", info: { title: "Test", version: "1.0.0" }, paths: {}, } as any apiClient.setOpenApiSpec(mockSpec) await expect( apiClient.executeApiCall("GET-API-ENDPOINT-SCHEMA", { endpoint: "/invalid" }), ).rejects.toThrow("No endpoint found for path '/invalid' in tool 'GET-API-ENDPOINT-SCHEMA'") }) }) describe("ApiClient request body handling", () => { it("sends an empty JSON object when POST tools only include header parameters", async () => { const mockSpecLoader = new OpenAPISpecLoader() const apiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const toolId = "POST::qry-alerts-qry-alerts-pst" const tools = new Map<string, Tool>([ [ toolId, { name: "qry-alerts-qry-alerts-pst", description: "Query alerts", inputSchema: { type: "object" as const, properties: { authorization: { type: "string" as const, "x-parameter-location": "header", }, }, }, } as Tool, ], ]) apiClient.setTools(tools) let capturedConfig: any const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { ok: true } }) }) ;(apiClient as any).axiosInstance = mockAxios await apiClient.executeApiCall(toolId, { authorization: "Bearer secure_token_123" }) expect(capturedConfig).toBeDefined() expect(capturedConfig.method).toBe("post") expect(capturedConfig.headers.authorization).toBe("Bearer secure_token_123") expect(capturedConfig.data).toEqual({}) }) }) // Regression test for Issue #33: Path parameter replacement bug describe("Issue #33 Regression Test", () => { it("should correctly replace path parameters without affecting similar text in path segments", async () => { // This test specifically addresses the bug described in issue #33: // Original bug: /inputs/{input} with input=00000 would result in /00000s/input // Expected behavior: /inputs/{input} with input=00000 should result in /inputs/00000 const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) // Create a mock OpenAPI spec with the problematic path structure const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/inputs/{input}": { get: { operationId: "getInput", parameters: [ { name: "input", in: "path", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } // Set the spec and generate tools mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) // Mock axios to capture the actual request URL let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios // Execute the API call with the problematic parameter value from issue #33 const toolId = "GET::inputs__---input" await mockApiClient.executeApiCall(toolId, { input: "00000" }) // Verify the URL was correctly constructed expect(capturedConfig).toBeDefined() expect(capturedConfig.url).toBe("/inputs/00000") // Explicitly verify the bug is NOT present expect(capturedConfig.url).not.toBe("/00000s/input") }) it("should handle multiple path parameters without substring replacement issues", async () => { // Additional test to ensure the fix works with multiple parameters const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/users/{userId}/posts/{postId}": { get: { operationId: "getUserPost", parameters: [ { name: "userId", in: "path", required: true, schema: { type: "string" as const }, }, { name: "postId", in: "path", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::users__---userId__posts__---postId" await mockApiClient.executeApiCall(toolId, { userId: "123", postId: "456" }) expect(capturedConfig.url).toBe("/users/123/posts/456") }) }) /* * Issue #33 Fix: Path Parameter Replacement Bug * * The bug was in the tool ID generation and path parameter replacement: * * OLD BEHAVIOR: * - Path: /inputs/{input} with parameter input=00000 * - Tool ID generation removed braces: /inputs/input * - Parameter replacement: /inputs/input -> /00000s/input (WRONG!) * * NEW BEHAVIOR: * - Path: /inputs/{input} with parameter input=00000 * - Tool ID generation transforms braces to markers: /inputs/---input * - Parameter replacement: /inputs/---input -> /inputs/00000 (CORRECT!) * * The fix transforms {param} to ---param in tool IDs to preserve parameter * location information, then updates the parameter replacement logic to * handle these markers correctly. */ // Tests for Issue #33 and PR #38 review comment edge cases describe("PR #38 Review Comment Fixes", () => { describe("Parameter Matching Precision in API Client", () => { it("should not partially match parameter names that are substrings of path segments", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) // Test case where parameter names could cause substring collisions const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/users/{userid}/info/{user}": { get: { operationId: "getUserInfo", parameters: [ { name: "userid", in: "path", required: true, schema: { type: "string" as const }, }, { name: "user", in: "path", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios // This should NOT cause substring replacement issues const toolId = "GET::api__users__---userid__info__---user" await mockApiClient.executeApiCall(toolId, { userid: "456", user: "123" }) expect(capturedConfig.url).toBe("/api/users/456/info/123") // Verify no partial matches occurred expect(capturedConfig.url).not.toContain("456id") // Would indicate partial match of "user" in "userid" expect(capturedConfig.url).not.toContain("123id") }) it("should handle parameters with similar names without cross-contamination", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/{id}/data/{idNum}": { get: { operationId: "getIdData", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" as const }, }, { name: "idNum", in: "path", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__---id__data__---idNum" await mockApiClient.executeApiCall(toolId, { id: "ABC", idNum: "789" }) expect(capturedConfig.url).toBe("/api/ABC/data/789") // Ensure no cross-contamination between similar parameter names expect(capturedConfig.url).not.toContain("ABCNum") expect(capturedConfig.url).not.toContain("789Num") }) it("should properly handle parameter replacement with double underscore boundaries", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/v1/{param}/nested/{param2}": { get: { operationId: "getNestedParam", parameters: [ { name: "param", in: "path", required: true, schema: { type: "string" as const }, }, { name: "param2", in: "path", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__v1__---param__nested__---param2" await mockApiClient.executeApiCall(toolId, { param: "VALUE1", param2: "VALUE2" }) expect(capturedConfig.url).toBe("/api/v1/VALUE1/nested/VALUE2") // Verify boundaries are respected and no partial replacement occurs expect(capturedConfig.url).not.toContain("VALUE12") // param2 should not be affected by param replacement }) }) describe("Sanitization Edge Cases", () => { it("should handle paths with consecutive hyphens correctly in API calls", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) // Create a spec with a path that has consecutive hyphens that should be preserved const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/resource---name/items": { get: { operationId: "getResourceItems", responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios // The tool ID should preserve the triple hyphens properly const toolId = "GET::api__resource---name__items" await mockApiClient.executeApiCall(toolId, {}) expect(capturedConfig.url).toBe("/api/resource---name/items") }) }) }) // Tests for Issue #50: Support for header parameters describe("Issue #50: Header Parameter Support", () => { it("should send header parameters in request headers for GET requests", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/data": { get: { operationId: "getData", parameters: [ { name: "authorization", in: "header", required: true, schema: { type: "string" as const }, }, { name: "x-api-key", in: "header", required: false, schema: { type: "string" as const }, }, { name: "page", in: "query", required: false, schema: { type: "integer" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__data" await mockApiClient.executeApiCall(toolId, { authorization: "Bearer token123", "x-api-key": "secret-key", page: 1, }) // Verify header parameters are in headers expect(capturedConfig.headers).toEqual({ authorization: "Bearer token123", "x-api-key": "secret-key", }) // Verify query parameters are in params (not headers) expect(capturedConfig.params).toEqual({ page: 1 }) // Verify header parameters are NOT in params expect(capturedConfig.params.authorization).toBeUndefined() expect(capturedConfig.params["x-api-key"]).toBeUndefined() }) it("should send header parameters in request headers for POST requests", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/resources": { post: { operationId: "createResource", parameters: [ { name: "x-request-id", in: "header", required: true, schema: { type: "string" as const }, }, ], requestBody: { content: { "application/json": { schema: { type: "object" as const, properties: { name: { type: "string" as const }, value: { type: "string" as const }, }, }, }, }, }, responses: { "201": { description: "Created" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { id: 1, name: "test" } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "POST::api__resources" await mockApiClient.executeApiCall(toolId, { "x-request-id": "req-12345", name: "test-resource", value: "test-value", }) // Verify header parameter is in headers expect(capturedConfig.headers).toEqual({ "x-request-id": "req-12345", }) // Verify body parameters are in data (not headers) expect(capturedConfig.data).toEqual({ name: "test-resource", value: "test-value", }) // Verify header parameter is NOT in data expect(capturedConfig.data["x-request-id"]).toBeUndefined() }) it("should handle query parameters in POST requests (Issue #44)", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/upload": { post: { operationId: "uploadFile", parameters: [ { name: "overwrite", in: "query", required: false, schema: { type: "boolean" as const }, description: "Whether to overwrite existing file", }, { name: "notify", in: "query", required: false, schema: { type: "boolean" as const }, description: "Whether to send notifications", }, ], requestBody: { required: true, content: { "application/json": { schema: { type: "object" as const, properties: { filename: { type: "string" as const }, data: { type: "string" as const }, }, }, }, }, }, responses: { "200": { description: "Upload successful" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: { success: true } }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "POST::upload" await mockApiClient.executeApiCall(toolId, { overwrite: true, notify: false, filename: "test.txt", data: "file content here", }) // Verify query parameters are in params (query string) expect(capturedConfig.params).toEqual({ overwrite: true, notify: false, }) // Verify body parameters are in data (request body) expect(capturedConfig.data).toEqual({ filename: "test.txt", data: "file content here", }) // Verify query parameters are NOT in body expect(capturedConfig.data.overwrite).toBeUndefined() expect(capturedConfig.data.notify).toBeUndefined() // Verify body parameters are NOT in query string expect(capturedConfig.params.filename).toBeUndefined() expect(capturedConfig.params.data).toBeUndefined() }) it("should handle mixed path, query, and header parameters", async () => { const mockSpecLoader = new OpenAPISpecLoader() const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/users/{userId}/posts": { get: { operationId: "getUserPosts", parameters: [ { name: "userId", in: "path", required: true, schema: { type: "string" as const }, }, { name: "authorization", in: "header", required: true, schema: { type: "string" as const }, }, { name: "limit", in: "query", required: false, schema: { type: "integer" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: [] }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__users__---userId__posts" await mockApiClient.executeApiCall(toolId, { userId: "user-123", authorization: "Bearer xyz789", limit: 10, }) // Verify path parameter is in URL expect(capturedConfig.url).toBe("/api/users/user-123/posts") // Verify header parameter is in headers expect(capturedConfig.headers).toEqual({ authorization: "Bearer xyz789", }) // Verify query parameter is in params expect(capturedConfig.params).toEqual({ limit: 10 }) // Verify no parameters leaked into wrong locations expect(capturedConfig.params.userId).toBeUndefined() expect(capturedConfig.params.authorization).toBeUndefined() expect(capturedConfig.headers.limit).toBeUndefined() }) it("should merge header parameters with auth headers", async () => { const mockSpecLoader = new OpenAPISpecLoader() const authHeaders = { Authorization: "Bearer auth-token" } const mockApiClient = new ApiClient( "https://api.example.com", new StaticAuthProvider(authHeaders), mockSpecLoader, ) const testSpec = { openapi: "3.0.0", info: { title: "Test API", version: "1.0.0" }, paths: { "/api/data": { get: { operationId: "getData", parameters: [ { name: "x-custom-header", in: "header", required: true, schema: { type: "string" as const }, }, ], responses: { "200": { description: "Success" } }, }, }, }, } mockApiClient.setOpenApiSpec(testSpec as any) const tools = mockSpecLoader.parseOpenAPISpec(testSpec as any) mockApiClient.setTools(tools) let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: {} }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__data" await mockApiClient.executeApiCall(toolId, { "x-custom-header": "custom-value", }) // Verify both auth headers and custom header parameters are present expect(capturedConfig.headers).toEqual({ Authorization: "Bearer auth-token", "x-custom-header": "custom-value", }) }) it("should handle header parameters without tool definition (fallback mode)", async () => { // This test ensures the fallback behavior doesn't break when tool definition is unavailable const mockApiClient = new ApiClient("https://api.example.com", new StaticAuthProvider()) // Don't set tools, forcing fallback behavior let capturedConfig: any = null const mockAxios = vi.fn().mockImplementation((config) => { capturedConfig = config return Promise.resolve({ data: {} }) }) ;(mockApiClient as any).axiosInstance = mockAxios const toolId = "GET::api__data" await mockApiClient.executeApiCall(toolId, { page: 1, }) // In fallback mode without tool definition, parameters go to query/body as before expect(capturedConfig.params).toEqual({ page: 1 }) }) })

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