diff --git a/frontend/src/hooks/useCloudData.ts b/frontend/src/hooks/useCloudData.ts index f4ddf03..203d89d 100644 --- a/frontend/src/hooks/useCloudData.ts +++ b/frontend/src/hooks/useCloudData.ts @@ -287,9 +287,13 @@ export const useCloudData = () => { const callServerTool = useCallback( async (serverName: string, toolName: string, args: Record) => { try { - const data = await apiPost(`/cloud/servers/${serverName}/tools/${toolName}/call`, { - arguments: args, - }); + // URL-encode server and tool names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") + const data = await apiPost( + `/cloud/servers/${encodeURIComponent(serverName)}/tools/${encodeURIComponent(toolName)}/call`, + { + arguments: args, + }, + ); if (data && data.success) { return data.data; diff --git a/frontend/src/services/promptService.ts b/frontend/src/services/promptService.ts index 6b0504b..6ad917a 100644 --- a/frontend/src/services/promptService.ts +++ b/frontend/src/services/promptService.ts @@ -59,8 +59,9 @@ export const getPrompt = async ( server?: string, ): Promise => { try { + // URL-encode server and prompt names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") const response = await apiPost( - `/mcp/${server}/prompts/${encodeURIComponent(request.promptName)}`, + `/mcp/${encodeURIComponent(server || '')}/prompts/${encodeURIComponent(request.promptName)}`, { name: request.promptName, arguments: request.arguments, @@ -94,9 +95,13 @@ export const togglePrompt = async ( enabled: boolean, ): Promise<{ success: boolean; error?: string }> => { try { - const response = await apiPost(`/servers/${serverName}/prompts/${promptName}/toggle`, { - enabled, - }); + // URL-encode server and prompt names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") + const response = await apiPost( + `/servers/${encodeURIComponent(serverName)}/prompts/${encodeURIComponent(promptName)}/toggle`, + { + enabled, + }, + ); return { success: response.success, @@ -120,8 +125,9 @@ export const updatePromptDescription = async ( description: string, ): Promise<{ success: boolean; error?: string }> => { try { + // URL-encode server and prompt names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") const response = await apiPut( - `/servers/${serverName}/prompts/${promptName}/description`, + `/servers/${encodeURIComponent(serverName)}/prompts/${encodeURIComponent(promptName)}/description`, { description }, { headers: { diff --git a/frontend/src/services/toolService.ts b/frontend/src/services/toolService.ts index 6cd19b5..e480899 100644 --- a/frontend/src/services/toolService.ts +++ b/frontend/src/services/toolService.ts @@ -25,7 +25,10 @@ export const callTool = async ( ): Promise => { try { // Construct the URL with optional server parameter - const url = server ? `/tools/${server}/${request.toolName}` : '/tools/call'; + // URL-encode server and tool names to handle slashes in names (e.g., "com.atlassian/atlassian-mcp-server") + const url = server + ? `/tools/${encodeURIComponent(server)}/${encodeURIComponent(request.toolName)}` + : '/tools/call'; const response = await apiPost(url, request.arguments, { headers: { @@ -62,8 +65,9 @@ export const toggleTool = async ( enabled: boolean, ): Promise<{ success: boolean; error?: string }> => { try { + // URL-encode server and tool names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") const response = await apiPost( - `/servers/${serverName}/tools/${toolName}/toggle`, + `/servers/${encodeURIComponent(serverName)}/tools/${encodeURIComponent(toolName)}/toggle`, { enabled }, { headers: { @@ -94,8 +98,9 @@ export const updateToolDescription = async ( description: string, ): Promise<{ success: boolean; error?: string }> => { try { + // URL-encode server and tool names to handle slashes (e.g., "com.atlassian/atlassian-mcp-server") const response = await apiPut( - `/servers/${serverName}/tools/${toolName}/description`, + `/servers/${encodeURIComponent(serverName)}/tools/${encodeURIComponent(toolName)}/description`, { description }, { headers: { diff --git a/src/controllers/cloudController.ts b/src/controllers/cloudController.ts index 2e6b8da..fa9709a 100644 --- a/src/controllers/cloudController.ts +++ b/src/controllers/cloudController.ts @@ -207,7 +207,8 @@ export const getCloudServersByTag = async (req: Request, res: Response): Promise // Get tools for a specific cloud server export const getCloudServerToolsList = async (req: Request, res: Response): Promise => { try { - const { serverName } = req.params; + // Decode URL-encoded parameter to handle slashes in server name + const serverName = decodeURIComponent(req.params.serverName); if (!serverName) { res.status(400).json({ success: false, @@ -236,7 +237,9 @@ export const getCloudServerToolsList = async (req: Request, res: Response): Prom // Call a tool on a cloud server export const callCloudTool = async (req: Request, res: Response): Promise => { try { - const { serverName, toolName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/tool names + const serverName = decodeURIComponent(req.params.serverName); + const toolName = decodeURIComponent(req.params.toolName); const { arguments: args } = req.body; if (!serverName) { diff --git a/src/controllers/openApiController.ts b/src/controllers/openApiController.ts index 71d6568..3aacf62 100644 --- a/src/controllers/openApiController.ts +++ b/src/controllers/openApiController.ts @@ -167,7 +167,9 @@ export const getOpenAPIStats = async (req: Request, res: Response): Promise => { try { - const { serverName, toolName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/tool names + const serverName = decodeURIComponent(req.params.serverName); + const toolName = decodeURIComponent(req.params.toolName); // Import handleCallToolRequest function const { handleCallToolRequest } = await import('../services/mcpService.js'); diff --git a/src/controllers/promptController.ts b/src/controllers/promptController.ts index 054cb38..8dd5347 100644 --- a/src/controllers/promptController.ts +++ b/src/controllers/promptController.ts @@ -7,7 +7,9 @@ import { handleGetPromptRequest } from '../services/mcpService.js'; */ export const getPrompt = async (req: Request, res: Response): Promise => { try { - const { serverName, promptName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/prompt names + const serverName = decodeURIComponent(req.params.serverName); + const promptName = decodeURIComponent(req.params.promptName); if (!serverName || !promptName) { res.status(400).json({ success: false, diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index e6ca1b0..c36413c 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -375,7 +375,9 @@ export const toggleServer = async (req: Request, res: Response): Promise = // Toggle tool status for a specific server export const toggleTool = async (req: Request, res: Response): Promise => { try { - const { serverName, toolName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/tool names + const serverName = decodeURIComponent(req.params.serverName); + const toolName = decodeURIComponent(req.params.toolName); const { enabled } = req.body; if (!serverName || !toolName) { @@ -437,7 +439,9 @@ export const toggleTool = async (req: Request, res: Response): Promise => // Update tool description for a specific server export const updateToolDescription = async (req: Request, res: Response): Promise => { try { - const { serverName, toolName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/tool names + const serverName = decodeURIComponent(req.params.serverName); + const toolName = decodeURIComponent(req.params.toolName); const { description } = req.body; if (!serverName || !toolName) { @@ -747,7 +751,9 @@ export const updateSystemConfig = (req: Request, res: Response): void => { // Toggle prompt status for a specific server export const togglePrompt = async (req: Request, res: Response): Promise => { try { - const { serverName, promptName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/prompt names + const serverName = decodeURIComponent(req.params.serverName); + const promptName = decodeURIComponent(req.params.promptName); const { enabled } = req.body; if (!serverName || !promptName) { @@ -809,7 +815,9 @@ export const togglePrompt = async (req: Request, res: Response): Promise = // Update prompt description for a specific server export const updatePromptDescription = async (req: Request, res: Response): Promise => { try { - const { serverName, promptName } = req.params; + // Decode URL-encoded parameters to handle slashes in server/prompt names + const serverName = decodeURIComponent(req.params.serverName); + const promptName = decodeURIComponent(req.params.promptName); const { description } = req.body; if (!serverName || !promptName) { diff --git a/src/services/openApiGeneratorService.ts b/src/services/openApiGeneratorService.ts index 21d29fe..d429a7a 100644 --- a/src/services/openApiGeneratorService.ts +++ b/src/services/openApiGeneratorService.ts @@ -225,13 +225,22 @@ export async function generateOpenAPISpec( // Generate paths from tools const paths: OpenAPIV3.PathsObject = {}; + const separator = getNameSeparator(); for (const { tool, serverName } of allTools) { const operation = generateOperationFromTool(tool, serverName); const { requestBody } = convertToolSchemaToOpenAPI(tool); - // Create path for the tool - const pathName = `/tools/${serverName}/${tool.name}`; + // Extract the tool name without server prefix + // Tool names are in format: serverName + separator + toolName + const prefix = `${serverName}${separator}`; + const toolNameOnly = tool.name.startsWith(prefix) + ? tool.name.substring(prefix.length) + : tool.name; + + // Create path for the tool with URL-encoded server and tool names + // This handles cases where names contain slashes (e.g., "com.atlassian/atlassian-mcp-server") + const pathName = `/tools/${encodeURIComponent(serverName)}/${encodeURIComponent(toolNameOnly)}`; const method = requestBody ? 'post' : 'get'; if (!paths[pathName]) { diff --git a/tests/controllers/openApiController.test.ts b/tests/controllers/openApiController.test.ts index 404b1f3..d245c12 100644 --- a/tests/controllers/openApiController.test.ts +++ b/tests/controllers/openApiController.test.ts @@ -299,4 +299,16 @@ describe('OpenAPI Granular Endpoints', () => { const group = mockGetGroupByIdOrName('nonexistent'); expect(group).toBeNull(); }); + + test('should decode URL-encoded server and tool names with slashes', () => { + // Test that URL-encoded names with slashes are properly decoded + const encodedServerName = 'com.atlassian%2Fatlassian-mcp-server'; + const encodedToolName = 'atlassianUserInfo'; + + const decodedServerName = decodeURIComponent(encodedServerName); + const decodedToolName = decodeURIComponent(encodedToolName); + + expect(decodedServerName).toBe('com.atlassian/atlassian-mcp-server'); + expect(decodedToolName).toBe('atlassianUserInfo'); + }); }); \ No newline at end of file diff --git a/tests/services/openApiGeneratorService.test.ts b/tests/services/openApiGeneratorService.test.ts index 36646e5..4b3baa3 100644 --- a/tests/services/openApiGeneratorService.test.ts +++ b/tests/services/openApiGeneratorService.test.ts @@ -65,6 +65,27 @@ describe('OpenAPI Generator Service', () => { expect(spec).toHaveProperty('paths'); expect(typeof spec.paths).toBe('object'); }); + + it('should URL-encode server and tool names with slashes in paths', async () => { + const spec = await generateOpenAPISpec(); + + // Check if any paths contain URL-encoded values + // Paths with slashes in server/tool names should be encoded + const paths = Object.keys(spec.paths); + + // If there are any servers with slashes, verify encoding + // e.g., "com.atlassian/atlassian-mcp-server" should become "com.atlassian%2Fatlassian-mcp-server" + for (const path of paths) { + // Path should not have unencoded slashes in the middle segments + // Valid format: /tools/{encoded-server}/{encoded-tool} + const pathSegments = path.split('/').filter((s) => s.length > 0); + if (pathSegments[0] === 'tools' && pathSegments.length >= 3) { + // The server name (segment 1) and tool name (segment 2+) should not create extra segments + // If properly encoded, there should be exactly 3 segments: ['tools', serverName, toolName] + expect(pathSegments.length).toBe(3); + } + } + }); }); describe('getToolStats', () => {