Skip to main content
Glama
dynamic-routing-tests.ts16.7 kB
import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; import { launchServer, findAvailablePort, ServerInstance, TransportMode, HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; import { Request, Response } from "express"; const MOCK_TOKEN_DEFAULT = 'glpat-mock-token-default'; const MOCK_TOKEN_HEADER = 'glpat-mock-token-header'; describe('Dynamic Routing and Authentication Scenarios', () => { const originalToken = process.env.GITLAB_TOKEN_TEST; before(() => { process.env.GITLAB_TOKEN_TEST = 'mock-token-for-launcher'; }); after(() => { if (originalToken) { process.env.GITLAB_TOKEN_TEST = originalToken; } else { delete process.env.GITLAB_TOKEN_TEST; } }); // Scenario 1: remote=off, dynamic=off describe('Scenario 1: Remote Auth OFF, Dynamic URL OFF', () => { let mcpServer: ServerInstance; let mcpUrl: string; let mockServer: MockGitLabServer; const originalProjectId = process.env.TEST_PROJECT_ID; before(async () => { // Ensure GITLAB_TOKEN_TEST matches what we expect for this scenario // to avoid launchServer overwriting GITLAB_PERSONAL_ACCESS_TOKEN with a different value process.env.GITLAB_TOKEN_TEST = MOCK_TOKEN_DEFAULT; process.env.TEST_PROJECT_ID = '1'; const mockPort = await findMockServerPort(9021); mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN_DEFAULT] }); await mockServer.start(); const mcpPort = await findAvailablePort(3021); mcpServer = await launchServer({ mode: TransportMode.STREAMABLE_HTTP, port: mcpPort, env: { GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`, GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN_DEFAULT, REMOTE_AUTHORIZATION: "false", ENABLE_DYNAMIC_API_URL: "false", GITLAB_TOKEN_TEST: MOCK_TOKEN_DEFAULT, }, }); mcpUrl = `http://${HOST}:${mcpPort}/mcp`; }); after(async () => { if (originalProjectId) { process.env.TEST_PROJECT_ID = originalProjectId; } else { delete process.env.TEST_PROJECT_ID; } if (mcpServer) mcpServer.kill(); if (mockServer) await mockServer.stop(); }); test('should ignore headers and use startup config', async () => { mockServer.addMockHandler('get', '/projects/1', (req: Request, res: Response) => { // index.ts uses Authorization header by default unless GITLAB_IS_OLD is set assert.strictEqual(req.headers['authorization'], `Bearer ${MOCK_TOKEN_DEFAULT}`); res.json({ id: 1, default_branch: 'main' }); }); const client = new CustomHeaderClient({ headers: { 'authorization': `Bearer ${MOCK_TOKEN_HEADER}`, // This should be ignored 'X-GitLab-API-URL': 'http://localhost:9999/api/v4', // This should be ignored } }); await client.connect(mcpUrl); const result = await client.callTool('get_project', { project_id: "1" }); assert.ok(result, 'Should get a result from the tool call'); await client.disconnect(); }); }); // Scenario 2: remote=on, dynamic=off describe('Scenario 2: Remote Auth ON, Dynamic URL OFF', () => { let mcpServer: ServerInstance; let mcpUrl: string; let mockServer: MockGitLabServer; before(async () => { const mockPort = await findMockServerPort(9022); mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN_HEADER] }); await mockServer.start(); const mcpPort = await findAvailablePort(3022); mcpServer = await launchServer({ mode: TransportMode.STREAMABLE_HTTP, port: mcpPort, env: { GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`, REMOTE_AUTHORIZATION: "true", ENABLE_DYNAMIC_API_URL: "false", }, }); mcpUrl = `http://${HOST}:${mcpPort}/mcp`; }); after(async () => { if (mcpServer) mcpServer.kill(); if (mockServer) await mockServer.stop(); }); test('should use token from header and ignore dynamic URL', async () => { mockServer.addMockHandler('get', '/projects/1', (req: Request, res: Response) => { assert.strictEqual(req.headers['authorization'], `Bearer ${MOCK_TOKEN_HEADER}`); res.json({ id: 1, default_branch: 'main' }); }); const client = new CustomHeaderClient({ headers: { 'authorization': `Bearer ${MOCK_TOKEN_HEADER}`, 'X-GitLab-API-URL': 'http://localhost:9999/api/v4', // This should be ignored } }); await client.connect(mcpUrl); const result = await client.callTool('get_project', { project_id: "1" }); assert.ok(result, 'Should get a result from the tool call'); await client.disconnect(); }); }); // Scenario 3: remote=off, dynamic=on - should be an error describe('Scenario 3: Remote Auth OFF, Dynamic URL ON (Error Case)', () => { test('should fail to start with an error', async () => { await assert.rejects( launchServer({ mode: TransportMode.STREAMABLE_HTTP, port: await findAvailablePort(3023), env: { REMOTE_AUTHORIZATION: "false", ENABLE_DYNAMIC_API_URL: "true", GITLAB_TOKEN_TEST: "mock-token", // Required to bypass launcher check }, }), (err: Error) => { // The server process exits with code 1, which launchServer catches and throws as a generic error // We can't easily see the stderr output here without modifying launchServer, // so we accept the exit code 1 error as success for this negative test. return err.message.includes('Server process exited with code 1'); } ); }); }); // Scenario 4: remote=on, dynamic=on describe('Scenario 4: Remote Auth ON, Dynamic URL ON', () => { let mcpServer: ServerInstance; let mcpUrl: string; let defaultMockServer: MockGitLabServer; let headerMockServer: MockGitLabServer; before(async () => { const defaultPort = await findMockServerPort(9024); defaultMockServer = new MockGitLabServer({ port: defaultPort, validTokens: [MOCK_TOKEN_DEFAULT, MOCK_TOKEN_HEADER] }); await defaultMockServer.start(); const headerPort = await findMockServerPort(9025); headerMockServer = new MockGitLabServer({ port: headerPort, validTokens: [MOCK_TOKEN_DEFAULT, MOCK_TOKEN_HEADER] }); await headerMockServer.start(); const mcpPort = await findAvailablePort(3024); mcpServer = await launchServer({ mode: TransportMode.STREAMABLE_HTTP, port: mcpPort, env: { GITLAB_API_URL: `${defaultMockServer.getUrl()}/api/v4`, GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN_DEFAULT, REMOTE_AUTHORIZATION: "true", ENABLE_DYNAMIC_API_URL: "true", }, }); mcpUrl = `http://${HOST}:${mcpPort}/mcp`; }); after(async () => { if (mcpServer) mcpServer.kill(); if (defaultMockServer) await defaultMockServer.stop(); if (headerMockServer) await headerMockServer.stop(); }); test('should use default URL and token when no headers are provided', async () => { defaultMockServer.addMockHandler('get', '/projects/1', (req: Request, res: Response) => { assert.strictEqual(req.headers['private-token'], MOCK_TOKEN_DEFAULT); res.json(createMockProject(1, 'default-server')); }); const client = new CustomHeaderClient({ headers: { 'private-token': MOCK_TOKEN_DEFAULT } }); await client.connect(mcpUrl); const result = await client.callTool('get_project', { project_id: "1" }); const resultContent = JSON.parse((result.content[0] as any).text); assert.strictEqual(resultContent.description, 'default-server'); await client.disconnect(); }); test('should use custom URL from header and default token', async () => { headerMockServer.addMockHandler('get', '/projects/2', (req: Request, res: Response) => { assert.strictEqual(req.headers['private-token'], MOCK_TOKEN_DEFAULT); res.json(createMockProject(2, 'header-server')); }); const client = new CustomHeaderClient({ headers: { 'private-token': MOCK_TOKEN_DEFAULT, 'X-GitLab-API-URL': `${headerMockServer.getUrl()}/api/v4`, } }); await client.connect(mcpUrl); const result = await client.callTool('get_project', { project_id: "2" }); const resultContent = JSON.parse((result.content[0] as any).text); assert.strictEqual(resultContent.description, 'header-server'); await client.disconnect(); }); test('should use custom URL and token from headers', async () => { headerMockServer.addMockHandler('get', '/projects/3', (req: Request, res: Response) => { assert.strictEqual(req.headers['authorization'], `Bearer ${MOCK_TOKEN_HEADER}`); res.json(createMockProject(3, 'header-server-with-header-token')); }); const client = new CustomHeaderClient({ headers: { 'authorization': `Bearer ${MOCK_TOKEN_HEADER}`, 'X-GitLab-API-URL': `${headerMockServer.getUrl()}/api/v4`, } }); await client.connect(mcpUrl); const result = await client.callTool('get_project', { project_id: "3" }); const resultContent = JSON.parse((result.content[0] as any).text); assert.strictEqual(resultContent.description, 'header-server-with-header-token'); await client.disconnect(); }); test('should work with multiple tool calls', async () => { const client = new CustomHeaderClient({ headers: { 'authorization': `Bearer ${MOCK_TOKEN_HEADER}`, 'X-GitLab-API-URL': `${headerMockServer.getUrl()}/api/v4`, } }); await client.connect(mcpUrl); await validateToolCalls(client, headerMockServer, MOCK_TOKEN_HEADER); await client.disconnect(); }); }); }); // Helper functions to create schema-compliant mock objects function createMockUser() { return { id: 1, username: 'mock_user', name: 'Mock User', state: 'active', avatar_url: 'https://example.com/avatar.png', web_url: 'https://example.com/mock_user' }; } function createMockProject(id: number, description: string = 'Mock Project') { return { id, name: `Project ${id}`, path_with_namespace: `group/project-${id}`, description, visibility: 'private', web_url: `https://gitlab.example.com/group/project-${id}`, created_at: '2024-01-01T00:00:00Z', last_activity_at: '2024-01-01T00:00:00Z', default_branch: 'main', namespace: { id: 1, name: 'Group', path: 'group', kind: 'group', full_path: 'group', web_url: 'https://gitlab.example.com/group' } }; } function createMockIssue(id: number, projectId: number) { return { id, iid: id, project_id: projectId, title: `Issue ${id}`, description: 'Description', state: 'opened', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', closed_at: null, web_url: `https://gitlab.example.com/group/project-${projectId}/issues/${id}`, author: createMockUser(), assignees: [], labels: [], milestone: null, user_notes_count: 0, upvotes: 0, downvotes: 0, confidential: false }; } function createMockMergeRequest(id: number, projectId: number) { return { id, iid: id, project_id: projectId, title: `MR ${id}`, description: 'Description', state: 'opened', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', merged_at: null, closed_at: null, merge_commit_sha: null, web_url: `https://gitlab.example.com/group/project-${projectId}/merge_requests/${id}`, author: createMockUser(), source_branch: 'feature', target_branch: 'main', draft: false, work_in_progress: false, merge_status: 'can_be_merged' }; } function createMockPipeline(id: number, projectId: number) { return { id, project_id: projectId, sha: 'sha123', ref: 'main', status: 'success', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', web_url: `https://gitlab.example.com/group/project-${projectId}/pipelines/${id}`, user: createMockUser() }; } function createMockCommit(id: string) { return { id, short_id: id.substring(0, 8), title: 'Commit message', author_name: 'Mock User', author_email: 'mock@example.com', authored_date: '2024-01-01T00:00:00Z', committer_name: 'Mock User', committer_email: 'mock@example.com', committed_date: '2024-01-01T00:00:00Z', message: 'Commit message', parent_ids: [], web_url: `https://gitlab.example.com/commit/${id}` }; } function createMockLabel(id: number) { return { id, name: `Label ${id}`, color: '#FF0000', text_color: '#FFFFFF', description: 'Label description', open_issues_count: 0, closed_issues_count: 0, open_merge_requests_count: 0, subscribed: false, priority: null, is_project_label: true }; } function createMockTreeItem(id: string) { return { id, name: 'file.txt', type: 'blob', path: 'file.txt', mode: '100644' }; } async function validateToolCalls(client: CustomHeaderClient, mockServer: MockGitLabServer, expectedToken: string) { const toolsToTest = [ { name: 'get_project', params: { project_id: '1' } }, { name: 'list_issues', params: { project_id: '1' } }, { name: 'get_merge_request', params: { project_id: '1', merge_request_iid: '1' } }, { name: 'list_merge_requests', params: { project_id: '1' } }, { name: 'get_repository_tree', params: { project_id: '1' } }, { name: 'list_labels', params: { project_id: '1' } }, { name: 'list_pipelines', params: { project_id: '1' } }, { name: 'list_commits', params: { project_id: '1' } }, ]; for (const tool of toolsToTest) { mockServer.clearCustomHandlers(); let mockPath = ''; let mockResponse: any; switch (tool.name) { case 'get_project': mockPath = '/projects/1'; mockResponse = createMockProject(1, 'mock-response'); break; case 'list_issues': mockPath = '/projects/1/issues'; mockResponse = [createMockIssue(1, 1)]; break; case 'get_merge_request': mockPath = '/projects/1/merge_requests/1'; mockResponse = createMockMergeRequest(1, 1); break; case 'list_merge_requests': mockPath = '/projects/1/merge_requests'; mockResponse = [createMockMergeRequest(1, 1)]; break; case 'get_repository_tree': mockPath = '/projects/1/repository/tree'; mockResponse = [createMockTreeItem('blob1')]; break; case 'list_labels': mockPath = '/projects/1/labels'; mockResponse = [createMockLabel(1)]; break; case 'list_pipelines': mockPath = '/projects/1/pipelines'; mockResponse = [createMockPipeline(1, 1)]; break; case 'list_commits': mockPath = '/projects/1/repository/commits'; mockResponse = [createMockCommit('sha1')]; break; default: throw new Error(`Unknown tool: ${tool.name}`); } mockServer.addMockHandler('get', mockPath, (req: Request, res: Response) => { if (req.headers['authorization']) { assert.strictEqual(req.headers['authorization'], `Bearer ${expectedToken}`); } else { assert.strictEqual(req.headers['private-token'], expectedToken); } res.json(mockResponse); }); const result = await client.callTool(tool.name, tool.params); const resultContent = JSON.parse((result.content[0] as any).text); // Basic validation that we got the expected object back if (Array.isArray(mockResponse)) { assert.ok(Array.isArray(resultContent)); assert.strictEqual(resultContent.length, mockResponse.length); // Check ID of first item if (resultContent[0].id) { assert.strictEqual(String(resultContent[0].id), String(mockResponse[0].id)); } } else { assert.strictEqual(String(resultContent.id), String(mockResponse.id)); if (tool.name === 'get_project') { assert.strictEqual(resultContent.description, 'mock-response'); } } } }

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/zereight/gitlab-mcp'

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