Skip to main content
Glama

OpenAPI MCP Server

MIT License
2,130
150
  • Apple
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" import { ToolsManager } from "../src/tools-manager" import { OpenAPISpecLoader, ExtendedTool } from "../src/openapi-loader" import { Tool } from "@modelcontextprotocol/sdk/types.js" import { parseToolId as parseToolIdUtil } from "../src/utils/tool-id.js" // Mock dependencies vi.mock("../src/openapi-loader", () => { return { OpenAPISpecLoader: vi.fn().mockImplementation(() => ({ loadOpenAPISpec: vi.fn(), parseOpenAPISpec: vi.fn(), })), } }) describe("ToolsManager", () => { let toolsManager: ToolsManager let mockConfig: any let mockSpecLoader: any beforeEach(() => { mockConfig = { name: "test-server", version: "1.0.0", apiBaseUrl: "http://example.com/api", openApiSpec: "http://example.com/openapi.json", } toolsManager = new ToolsManager(mockConfig) mockSpecLoader = (toolsManager as any).specLoader }) afterEach(() => { vi.clearAllMocks() }) describe("initialize", () => { it("should load and parse the OpenAPI spec", async () => { const mockSpec = { paths: {} } const mockTools = new Map([ ["GET::users", { name: "List Users", description: "Get all users" } as Tool], ["POST::users", { name: "Create User", description: "Create a new user" } as Tool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue(mockSpec) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) await toolsManager.initialize() expect(mockSpecLoader.loadOpenAPISpec).toHaveBeenCalledWith( mockConfig.openApiSpec, undefined, undefined, ) expect(mockSpecLoader.parseOpenAPISpec).toHaveBeenCalledWith(mockSpec) expect((toolsManager as any).tools).toEqual(mockTools) }) it("should load dynamic meta-tools when toolsMode is dynamic", async () => { // Configure for dynamic mode ;(toolsManager as any).config.toolsMode = "dynamic" const spyLoad = mockSpecLoader.loadOpenAPISpec.mockResolvedValue({} as any) await toolsManager.initialize() // parseOpenAPISpec should not be called expect(mockSpecLoader.parseOpenAPISpec).not.toHaveBeenCalled() const tools = toolsManager.getAllTools().map((t) => t.name) expect(tools).toEqual([ "list-api-endpoints", "get-api-endpoint-schema", "invoke-api-endpoint", ]) }) describe("toolsMode: explicit", () => { it("should only load tools explicitly listed in includeTools when toolsMode is explicit", async () => { // Setup raw tools const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users" } as ExtendedTool, ], [ "POST::users", { name: "createUser", httpMethod: "POST", resourceName: "users" } as ExtendedTool, ], [ "GET::orders", { name: "getOrders", httpMethod: "GET", resourceName: "orders" } as ExtendedTool, ], [ "DELETE::orders-id", { name: "deleteOrder", httpMethod: "DELETE", resourceName: "orders" } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) // Configure for explicit mode with specific tools ;(toolsManager as any).config.toolsMode = "explicit" ;(toolsManager as any).config.includeTools = ["GET::users", "POST::users"] await toolsManager.initialize() // Should only include explicitly listed tools, ignoring other filters const resultToolIds = Array.from((toolsManager as any).tools.keys()) expect(resultToolIds).toEqual(["GET::users", "POST::users"]) }) it("should load no tools when toolsMode is explicit but includeTools is empty", async () => { const mockTools = new Map([ ["GET::users", { name: "getUsers" } as ExtendedTool], ["POST::users", { name: "createUser" } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "explicit" ;(toolsManager as any).config.includeTools = [] await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual([]) }) it("should ignore other filters when toolsMode is explicit", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["public"], } as ExtendedTool, ], [ "POST::orders", { name: "createOrder", httpMethod: "POST", resourceName: "orders", tags: ["admin"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "explicit" ;(toolsManager as any).config.includeTools = ["POST::orders"] // Explicitly include POST::orders ;(toolsManager as any).config.includeOperations = ["get"] // This should be ignored ;(toolsManager as any).config.includeResources = ["users"] // This should be ignored ;(toolsManager as any).config.includeTags = ["public"] // This should be ignored await toolsManager.initialize() // Should only include POST::orders despite other filters that would exclude it expect(Array.from((toolsManager as any).tools.keys())).toEqual(["POST::orders"]) }) it("should handle tool names in includeTools for explicit mode", async () => { const mockTools = new Map([ ["GET::users", { name: "getUsers", httpMethod: "GET" } as ExtendedTool], ["POST::users", { name: "createUser", httpMethod: "POST" } as ExtendedTool], ["GET::orders", { name: "getOrders", httpMethod: "GET" } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "explicit" ;(toolsManager as any).config.includeTools = ["getUsers", "createUser"] // Using tool names instead of IDs await toolsManager.initialize() const resultToolIds = Array.from((toolsManager as any).tools.keys()) expect(resultToolIds).toEqual(["GET::users", "POST::users"]) }) }) describe("Resource Name Extraction Logic", () => { it("should handle complex path examples for resource filtering", async () => { const mockTools = new Map([ // Simple resource paths ["GET::users", { name: "getUsers", resourceName: "users" } as ExtendedTool], ["GET::orders", { name: "getOrders", resourceName: "orders" } as ExtendedTool], // Complex nested paths - resource should be the last non-parameter segment [ "GET::api__v1__user__profile__settings", { name: "userProfileSettings", resourceName: "settings" } as ExtendedTool, ], [ "POST::api__v2__organizations__id__members", { name: "addOrgMember", resourceName: "members" } as ExtendedTool, ], [ "PUT::service__users__authority__groups__id", { name: "updateAuthorityGroup", resourceName: "groups" } as ExtendedTool, ], // Paths with hyphens and underscores [ "GET::user_profile__data", { name: "userProfileData", resourceName: "data" } as ExtendedTool, ], [ "POST::api__v1__user__management", { name: "manageUser", resourceName: "management" } as ExtendedTool, ], // Edge cases ["GET::health", { name: "healthCheck", resourceName: "health" } as ExtendedTool], [ "GET::api__status__check", { name: "statusCheck", resourceName: "check" } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" // Test filtering by different resource names const testCases = [ { filter: ["users"], expected: ["GET::users"] }, { filter: ["settings"], expected: ["GET::api__v1__user__profile__settings"] }, { filter: ["members"], expected: ["POST::api__v2__organizations__id__members"] }, { filter: ["groups"], expected: ["PUT::service__users__authority__groups__id"] }, { filter: ["data"], expected: ["GET::user_profile__data"] }, { filter: ["management"], expected: ["POST::api__v1__user__management"] }, { filter: ["health"], expected: ["GET::health"] }, { filter: ["check"], expected: ["GET::api__status__check"] }, { filter: ["users", "data"], expected: ["GET::users", "GET::user_profile__data"] }, ] for (const testCase of testCases) { // Reset tools manager for each test const freshToolsManager = new ToolsManager({ ...mockConfig, toolsMode: "all", includeResources: testCase.filter, }) ;(freshToolsManager as any).specLoader = mockSpecLoader await freshToolsManager.initialize() const resultToolIds = Array.from((freshToolsManager as any).tools.keys()) expect(resultToolIds.sort()).toEqual(testCase.expected.sort()) } }) it("should handle resource names with special characters and case variations", async () => { const mockTools = new Map([ [ "GET::api__user_profiles", { name: "userProfiles", resourceName: "user_profiles" } as ExtendedTool, ], ["GET::api__UserData", { name: "getUserData", resourceName: "UserData" } as ExtendedTool], [ "GET::api__ADMIN_PANEL", { name: "adminPanel", resourceName: "ADMIN_PANEL" } as ExtendedTool, ], [ "GET::api__kebab-case-resource", { name: "kebabResource", resourceName: "kebab-case-resource" } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeResources = [ "user_profiles", "userdata", "admin_panel", ] // Mixed case filters await toolsManager.initialize() const resultToolIds = Array.from((toolsManager as any).tools.keys()) // Should match case-insensitively expect(resultToolIds.sort()).toEqual( ["GET::api__user_profiles", "GET::api__UserData", "GET::api__ADMIN_PANEL"].sort(), ) }) }) describe("Filter Order of Application", () => { it("should apply filters in the correct order: includeTools -> includeOperations -> includeResources -> includeTags", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["public", "read"], } as ExtendedTool, ], [ "POST::users", { name: "createUser", httpMethod: "POST", resourceName: "users", tags: ["admin", "write"], } as ExtendedTool, ], [ "GET::orders", { name: "getOrders", httpMethod: "GET", resourceName: "orders", tags: ["public", "read"], } as ExtendedTool, ], [ "DELETE::orders-id", { name: "deleteOrder", httpMethod: "DELETE", resourceName: "orders", tags: ["admin", "write"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" // Apply all filters - should work as AND operation ;(toolsManager as any).config.includeOperations = ["get", "post"] // Excludes DELETE ;(toolsManager as any).config.includeResources = ["users"] // Excludes orders ;(toolsManager as any).config.includeTags = ["public"] // Excludes admin-only tools await toolsManager.initialize() // Only GET::users should match all criteria: // - Operation: GET (✓) or POST (✗ - has admin tag, not public) // - Resource: users (✓) // - Tags: public (✓) const resultToolIds = Array.from((toolsManager as any).tools.keys()) expect(resultToolIds).toEqual(["GET::users"]) }) it("should document filter precedence with includeTools taking highest priority", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["public"], } as ExtendedTool, ], [ "DELETE::orders-id", { name: "deleteOrder", httpMethod: "DELETE", resourceName: "orders", tags: ["admin"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" // includeTools should override other filters ;(toolsManager as any).config.includeTools = ["DELETE::orders-id"] // Explicitly include this tool ;(toolsManager as any).config.includeOperations = ["get"] // Would normally exclude DELETE ;(toolsManager as any).config.includeResources = ["users"] // Would normally exclude orders ;(toolsManager as any).config.includeTags = ["public"] // Would normally exclude admin await toolsManager.initialize() // DELETE::orders-id should be included despite other filters because it's in includeTools const resultToolIds = Array.from((toolsManager as any).tools.keys()) expect(resultToolIds).toEqual(["DELETE::orders-id"]) }) it("should handle empty filter arrays correctly", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["public"], } as ExtendedTool, ], [ "POST::orders", { name: "createOrder", httpMethod: "POST", resourceName: "orders", tags: ["admin"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" // Empty arrays should not filter anything ;(toolsManager as any).config.includeTools = [] ;(toolsManager as any).config.includeOperations = [] ;(toolsManager as any).config.includeResources = [] ;(toolsManager as any).config.includeTags = [] await toolsManager.initialize() // All tools should be included const resultToolIds = Array.from((toolsManager as any).tools.keys()) expect(resultToolIds.sort()).toEqual(["GET::users", "POST::orders"]) }) }) it("should filter tools by includeTools list", async () => { // Setup raw tools const mockTools = new Map([ ["GET::foo", { name: "foo" } as Tool], ["GET::bar", { name: "bar" } as Tool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeTools = ["GET::bar"] await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::bar"]) }) it("should filter tools by includeOperations list", async () => { const mockTools = new Map([ ["GET::1", { name: "g1", httpMethod: "GET" } as ExtendedTool], ["POST::1", { name: "p1", httpMethod: "POST" } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeOperations = ["get"] await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::1"]) }) it("should filter tools by includeResources list", async () => { const mockTools = new Map([ ["GET::users", { name: "u", resourceName: "users" } as ExtendedTool], ["GET::orders-id", { name: "o", resourceName: "orders" } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeResources = ["users"] await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::users"]) }) it("should filter tools by includeTags list", async () => { const mockTools = new Map([ ["GET::a", { name: "a", tags: ["x"] } as ExtendedTool], ["GET::b", { name: "b", tags: ["y"] } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeTags = ["x"] await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::a"]) }) it("should filter tools by includeTags list case-insensitively", async () => { const mockTools = new Map([ ["GET::a", { name: "a", tags: ["USERS"] } as ExtendedTool], ["GET::b", { name: "b", tags: ["products"] } as ExtendedTool], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeTags = ["users"] // lowercase, will match uppercase "USERS" await toolsManager.initialize() expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::a"]) }) it("should filter tools by multiple criteria simultaneously", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["users", "public"], } as ExtendedTool, ], [ "POST::users", { name: "createUser", httpMethod: "POST", resourceName: "users", tags: ["users", "admin"], } as ExtendedTool, ], [ "GET::orders", { name: "getOrders", httpMethod: "GET", resourceName: "orders", tags: ["orders"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeOperations = ["get"] ;(toolsManager as any).config.includeResources = ["users"] ;(toolsManager as any).config.includeTags = ["public"] await toolsManager.initialize() // Should only include GET::users because it matches all criteria expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::users"]) }) it("should handle tools with missing metadata gracefully", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["users"], } as ExtendedTool, ], [ "POST::unknown", { name: "unknownTool", // Missing httpMethod, resourceName, tags } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeOperations = ["get"] await toolsManager.initialize() // Should only include GET::users because POST::unknown has no httpMethod expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::users"]) }) it("should filter by resource names case-insensitively", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", resourceName: "Users", // uppercase } as ExtendedTool, ], [ "GET::orders", { name: "getOrders", resourceName: "orders", // lowercase } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeResources = ["users"] // lowercase filter await toolsManager.initialize() // Should match "Users" resource case-insensitively expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::users"]) }) describe("Edge Cases and Error Handling", () => { it("should handle undefined or null filter arrays", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: "GET", resourceName: "users", tags: ["public"], } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" // Set filters to undefined/null ;(toolsManager as any).config.includeTools = undefined ;(toolsManager as any).config.includeOperations = null ;(toolsManager as any).config.includeResources = undefined ;(toolsManager as any).config.includeTags = null await toolsManager.initialize() // Should include all tools when filters are undefined/null expect(Array.from((toolsManager as any).tools.keys())).toEqual(["GET::users"]) }) it("should handle tools with empty or undefined tags arrays", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", tags: [], inputSchema: { type: "object", properties: {} }, } as ExtendedTool, ], [ "POST::orders", { name: "createOrder", tags: undefined, inputSchema: { type: "object", properties: {} }, } as ExtendedTool, ], [ "PUT::products", { name: "updateProduct", inputSchema: { type: "object", properties: {} }, } as ExtendedTool, ], // No tags property ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeTags = ["public"] await toolsManager.initialize() // No tools should match since none have the "public" tag expect(Array.from((toolsManager as any).tools.keys())).toEqual([]) }) it("should handle malformed tool metadata gracefully", async () => { const mockTools = new Map([ [ "GET::users", { name: "getUsers", httpMethod: 123, resourceName: null, tags: "not-an-array", inputSchema: { type: "object", properties: {} }, } as any, ], [ "POST::orders", { name: "createOrder", httpMethod: "", resourceName: "", tags: [], inputSchema: { type: "object", properties: {} }, } as ExtendedTool, ], ]) mockSpecLoader.loadOpenAPISpec.mockResolvedValue({ paths: {} } as any) mockSpecLoader.parseOpenAPISpec.mockReturnValue(mockTools) ;(toolsManager as any).config.toolsMode = "all" ;(toolsManager as any).config.includeOperations = ["post"] await toolsManager.initialize() // Should handle malformed data gracefully and only include valid tools expect(Array.from((toolsManager as any).tools.keys())).toEqual([]) }) }) }) describe("getAllTools - return all tools", () => { it("should return all tools", async () => { const mockTools = new Map([ ["GET::users", { name: "List Users" } as Tool], ["POST::users", { name: "Create User" } as Tool], ]) // Set up the tools map vi.spyOn(toolsManager as any, "tools", "get").mockReturnValue(mockTools) const tools = toolsManager.getAllTools() expect(tools).toEqual([{ name: "List Users" }, { name: "Create User" }]) }) }) describe("getToolsWithIds", () => { it("should return all tools with their IDs", async () => { const mockTools = new Map([ ["GET::users", { name: "List Users" } as Tool], ["POST::users", { name: "Create User" } as Tool], ]) // Set up the tools map vi.spyOn(toolsManager as any, "tools", "get").mockReturnValue(mockTools) const toolsWithIds = toolsManager.getToolsWithIds() expect(toolsWithIds).toEqual([ ["GET::users", { name: "List Users" }], ["POST::users", { name: "Create User" }], ]) }) }) describe("findTool", () => { it("should find a tool by ID", () => { const mockTools = new Map([ ["GET::users", { name: "List Users" } as Tool], ["POST::users", { name: "Create User" } as Tool], ]) // Set up the tools map vi.spyOn(toolsManager as any, "tools", "get").mockReturnValue(mockTools) const result = toolsManager.findTool("GET::users") expect(result).toEqual({ toolId: "GET::users", tool: { name: "List Users" }, }) }) it("should find a tool by name", () => { const mockTools = new Map([ ["GET::users", { name: "List Users" } as Tool], ["POST::users", { name: "Create User" } as Tool], ]) // Set up the tools map vi.spyOn(toolsManager as any, "tools", "get").mockReturnValue(mockTools) const result = toolsManager.findTool("Create User") expect(result).toEqual({ toolId: "POST::users", tool: { name: "Create User" }, }) }) it("should return undefined if tool is not found", () => { const mockTools = new Map([["GET::users", { name: "List Users" } as Tool]]) // Set up the tools map vi.spyOn(toolsManager as any, "tools", "get").mockReturnValue(mockTools) const result = toolsManager.findTool("nonexistent") expect(result).toBeUndefined() }) }) describe("parseToolId", () => { it("should parse a tool ID into method and path", () => { const result = toolsManager.parseToolId("GET::users__active") expect(result).toEqual({ method: "GET", path: "/users/active", }) }) it("should handle complex paths with hyphens", () => { const result = toolsManager.parseToolId("POST::api__v1__user-profile__update") expect(result).toEqual({ method: "POST", path: "/api/v1/user-profile/update", }) }) it("should handle paths with underscores", () => { const result = toolsManager.parseToolId("GET::user_profile__user_id") expect(result).toEqual({ method: "GET", path: "/user_profile/user_id", }) }) it("should handle paths with special characters (encoded)", () => { const specialPath = "/user_profile/{user_id}/data-2024_06" const pathPart = "user_profile__user_id__data-2024_06" const toolId = `GET::${pathPart}` const result = toolsManager.parseToolId(toolId) expect(result).toEqual({ method: "GET", path: "/user_profile/user_id/data-2024_06", }) }) it("should round-trip encode and decode toolId for any path", () => { const paths = [ "/user_profile/{user_id}", "/api/v1/user-profile_update", "/foo-bar_baz/123", "/complex/path_with-mixed_chars/and123", ] for (const path of paths) { const method = "GET" // Simulate the toolId generation process with new format const cleanPath = path .replace(/^\//, "") .replace(/\{([^}]+)\}/g, "$1") .replace(/\//g, "__") // Use double underscores now const toolId = `${method}::${cleanPath}` const { method: parsedMethod, path: parsedPath } = toolsManager.parseToolId(toolId) expect(parsedMethod).toBe(method) // The parsed path should reconstruct the original API path structure const expectedPath = "/" + cleanPath.replace(/__/g, "/") expect(parsedPath).toBe(expectedPath) } }) it("should handle legitimate hyphens in path segments correctly", () => { // Test the enhanced hyphen handling - now much simpler with double underscores const testCases = [ { toolId: "GET::api__resource-name__items", expected: { method: "GET", path: "/api/resource-name/items" }, }, { toolId: "POST::user-profile__data", expected: { method: "POST", path: "/user-profile/data" }, }, { toolId: "PUT::api__v1__user-management-system", expected: { method: "PUT", path: "/api/v1/user-management-system" }, }, { toolId: "DELETE::complex-path__with-multiple-hyphens", expected: { method: "DELETE", path: "/complex-path/with-multiple-hyphens" }, }, ] for (const testCase of testCases) { const result = toolsManager.parseToolId(testCase.toolId) expect(result).toEqual(testCase.expected) } }) it("REGRESSION: should resolve original toolId ambiguity issue with underscores and hyphens", () => { // This test validates that the original issue is resolved: // The new double underscore format eliminates all ambiguity const problematicPaths = [ // Original problematic case: path with underscores and hyphens "/user_profile-data", "/api_v1-user-management", "/service_users-authority_groups", // Edge cases that could cause confusion "/user-profile_data", "/api-v1_user_management", "/complex_path-with-mixed_separators", ] for (const originalPath of problematicPaths) { const method = "POST" // Step 1: Simulate toolId generation (as done in openapi-loader.ts) const cleanPath = originalPath .replace(/^\//, "") // Remove leading slash .replace(/\{([^}]+)\}/g, "$1") // Remove curly braces from path params .replace(/\//g, "__") // Convert slashes to double underscores const toolId = `${method}::${cleanPath}` // Step 2: Parse the toolId back (as done in tools-manager.ts and api-client.ts) const { method: parsedMethod, path: parsedPath } = toolsManager.parseToolId(toolId) // Step 3: Validate the round-trip is unambiguous expect(parsedMethod).toBe(method) // The parsed path should reconstruct the original API path structure const expectedPath = "/" + cleanPath.replace(/__/g, "/") expect(parsedPath).toBe(expectedPath) // Step 4: Validate that the toolId format is unambiguous // The :: separator ensures we can always split correctly expect(toolId).toContain("::") expect(toolId.split("::")).toHaveLength(2) expect(toolId.split("::")[0]).toBe(method) expect(toolId.split("::")[1]).toBe(cleanPath) } }) it("REGRESSION: demonstrates that old format would have been ambiguous", () => { // This test demonstrates why the old format was problematic // and validates that the new format resolves the ambiguity const problematicPaths = [ "/user_profile-data", // Could be confused with "/user/profile-data" "/api-v1_user", // Could be confused with "/api-v1/user" "/service_users-groups", // Could be confused with "/service/users-groups" ] for (const path of problematicPaths) { // With the OLD format (using single hyphen separator): // The toolId would be: "GET-user_profile-data" // When parsing, it's ambiguous where the method ends and path begins // because both method separator and path parts use hyphens // With the NEW format (using :: separator and __ for paths): const method = "GET" const cleanPath = path.replace(/^\//, "").replace(/\//g, "__") const newFormatToolId = `${method}::${cleanPath}` // The new format is unambiguous because :: only appears once as separator // and __ is used for path separators (extremely rare in real APIs) expect(newFormatToolId.split("::")).toHaveLength(2) expect(newFormatToolId.split("::")[0]).toBe(method) expect(newFormatToolId.split("::")[1]).toBe(cleanPath) // Parsing is now deterministic and reconstructs the original API path const { method: parsedMethod, path: parsedPath } = toolsManager.parseToolId(newFormatToolId) expect(parsedMethod).toBe(method) expect(parsedPath).toBe("/" + cleanPath.replace(/__/g, "/")) // The old format would have been: "GET-user_profile-data" // Which could be parsed as: // - method="GET", path="/user_profile-data" (incorrect - doesn't reconstruct API path) // - method="GET-user", path="/profile-data" (incorrect) // - method="GET-user_profile", path="/data" (incorrect) // The new format eliminates this ambiguity completely } }) it("should use centralized parseToolId utility consistently", () => { // Verify that ToolsManager.parseToolId uses the same utility as ApiClient // This ensures consistency across modules const testToolId = "GET::api__v1__users__id__profile" const result = toolsManager.parseToolId(testToolId) // Test the utility directly to ensure consistency const utilityResult = parseToolIdUtil(testToolId) expect(result).toEqual(utilityResult) expect(result).toEqual({ method: "GET", path: "/api/v1/users/id/profile", }) }) }) })

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