Skip to main content
Glama

1MCP Server

http-auth.test.ts17.7 kB
import { ConfigBuilder, ProtocolValidator, TestProcessManager } from '@test/e2e/utils/index.js'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; describe('HTTP Transport Authentication E2E', () => { let processManager: TestProcessManager; let configBuilder: ConfigBuilder; let configPath: string; let httpPort: number; let baseUrl: string; beforeEach(async () => { processManager = new TestProcessManager(); configBuilder = new ConfigBuilder(); // Use a random port for testing httpPort = 3000 + Math.floor(Math.random() * 1000); baseUrl = `http://localhost:${httpPort}`; }); afterEach(async () => { await processManager.cleanup(); configBuilder.cleanup(); }); it('should configure HTTP transport with authentication', async () => { const fixturesPath = join(__dirname, '../fixtures'); configPath = configBuilder .enableHttpTransport(httpPort) .enableAuth('test-client-id', 'test-client-secret') .addStdioServer('echo-server', 'node', [join(fixturesPath, 'echo-server.js')], ['test', 'echo']) .writeToFile(); const config = configBuilder.build(); expect(configPath).toBeDefined(); expect(configPath.endsWith('.json')).toBe(true); expect(config.transport?.http?.port).toBe(httpPort); expect(config.auth?.enabled).toBe(true); expect(config.servers).toHaveLength(1); }); it('should handle OAuth 2.1 authentication configuration', async () => { const authConfig = configBuilder.enableHttpTransport(httpPort).enableAuth('oauth-client', 'oauth-secret').build(); expect(baseUrl).toBe(`http://localhost:${httpPort}`); expect(authConfig.transport?.http?.port).toBe(httpPort); expect(authConfig.auth?.clientId).toBe('oauth-client'); expect(authConfig.auth?.clientSecret).toBe('oauth-secret'); expect(authConfig.auth?.enabled).toBe(true); }); it('should validate HTTP authentication request patterns', async () => { // Test authentication-related request validation const authRequests = [ { jsonrpc: '2.0', id: 1, method: 'auth/login', params: { username: 'test', password: 'secret' }, }, { jsonrpc: '2.0', id: 2, method: 'auth/refresh', params: { refresh_token: 'refresh_token_value' }, }, { jsonrpc: '2.0', id: 3, method: 'auth/logout', params: { token: 'access_token_value' }, }, ]; authRequests.forEach((request) => { const validation = ProtocolValidator.validateRequest(request); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); }); }); it('should handle HTTP server configuration', async () => { const httpConfig = configBuilder .enableHttpTransport(httpPort) .enableAuth('http-client', 'http-secret') .addHttpServer('external-api', 'http://external-api.example.com', ['external', 'api']) .build(); expect(httpConfig.transport?.http?.port).toBe(httpPort); expect(httpConfig.servers).toHaveLength(1); expect(httpConfig.servers[0].transport).toBe('http'); expect(httpConfig.servers[0].endpoint).toBe('http://external-api.example.com'); }); it('should handle mixed transport configuration', async () => { const fixturesPath = join(__dirname, '../fixtures'); const mixedConfig = configBuilder .enableStdioTransport() .enableHttpTransport(httpPort) .enableAuth('mixed-client', 'mixed-secret') .addStdioServer('local-server', 'node', [join(fixturesPath, 'echo-server.js')], ['local']) .addHttpServer('remote-server', 'https://api.example.com', ['remote']) .build(); expect(mixedConfig.transport?.stdio).toBe(true); expect(mixedConfig.transport?.http?.port).toBe(httpPort); expect(mixedConfig.servers).toHaveLength(2); const stdioServer = mixedConfig.servers.find((s) => s.transport === 'stdio'); const httpServer = mixedConfig.servers.find((s) => s.transport === 'http'); expect(stdioServer?.name).toBe('local-server'); expect(httpServer?.name).toBe('remote-server'); }); it('should validate authentication error responses', async () => { // Test authentication-specific error codes const authErrors = [ { code: -32001, message: 'Authentication required' }, { code: -32002, message: 'Invalid credentials' }, { code: -32003, message: 'Token expired' }, { code: -32004, message: 'Insufficient permissions' }, { code: -32005, message: 'Rate limit exceeded' }, ]; authErrors.forEach((error) => { const validation = ProtocolValidator.validateError(error); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); }); }); it('should validate token-based authentication patterns', async () => { // Test different token types and validation const tokenPatterns = [ { type: 'bearer', token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...' }, { type: 'api_key', token: 'ak_1234567890abcdef' }, { type: 'oauth2', token: 'oauth2_access_token_value' }, { type: 'basic', token: 'Basic dXNlcjpwYXNzd29yZA==' }, ]; tokenPatterns.forEach((pattern) => { const authHeader = pattern.type === 'basic' ? pattern.token : `${pattern.type.charAt(0).toUpperCase() + pattern.type.slice(1)} ${pattern.token}`; expect(authHeader).toBeDefined(); expect(authHeader.length).toBeGreaterThan(0); }); }); it('should handle authentication flow simulation', async () => { // Simulate authentication flow steps const authFlow = [ { step: 'request_auth', method: 'auth/request', params: { client_id: 'test' } }, { step: 'provide_credentials', method: 'auth/login', params: { username: 'user', password: 'pass' } }, { step: 'receive_token', method: 'auth/token', params: { grant_type: 'password' } }, { step: 'use_token', method: 'tools/list', params: {}, headers: { authorization: 'Bearer token' } }, { step: 'refresh_token', method: 'auth/refresh', params: { refresh_token: 'refresh' } }, { step: 'logout', method: 'auth/logout', params: { token: 'access_token' } }, ]; authFlow.forEach((step, index) => { const request = { jsonrpc: '2.0', id: index + 1, method: step.method, params: step.params, }; const validation = ProtocolValidator.validateRequest(request); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); }); }); it('should complete OAuth 2.1 flow with MCP specification compliance', async () => { const fixturesPath = join(__dirname, '../fixtures'); // Generate PKCE code verifier and challenge const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateRandomState(); const clientId = 'test-oauth-client'; const clientSecret = 'test-oauth-secret'; const redirectUri = `${baseUrl}/oauth/callback`; // Configure server with OAuth enabled configPath = configBuilder .enableHttpTransport(httpPort) .enableAuth(clientId, clientSecret) .addStdioServer('echo-server', 'node', [join(fixturesPath, 'echo-server.js')], ['test', 'echo']) .writeToFile(); // Start the MCP agent as a process using the built version const agentProcess = await processManager.startProcess('mcp-agent', { command: 'node', args: [ 'build/index.js', 'serve', '--config', configPath, '--transport', 'http', '--port', httpPort.toString(), '--enable-auth', ], timeout: 30000, startupTimeout: 10000, // Increased startup timeout }); expect(agentProcess.pid).toBeGreaterThan(0); // Wait for server to be ready with health check let serverReady = false; let attempts = 0; const maxAttempts = 15; // Increased max attempts while (!serverReady && attempts < maxAttempts) { attempts++; await new Promise((resolve) => setTimeout(resolve, 200)); try { const healthResponse = await fetch(`${baseUrl}/health`, { method: 'GET', signal: AbortSignal.timeout(3000), // Increased timeout }); if (healthResponse.ok) { serverReady = true; console.log(`Server ready after ${attempts} attempts`); } else { console.log(`Health check attempt ${attempts}: HTTP ${healthResponse.status}`); } } catch (error) { console.log(`Health check attempt ${attempts}: ${error instanceof Error ? error.message : String(error)}`); } } if (!serverReady) { console.warn('Server may not be fully ready, continuing with limited testing'); } // Step 1: Test well-known OAuth authorization server endpoint (only if server is ready) if (serverReady) { try { const authServerResponse = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`, { signal: AbortSignal.timeout(5000), }); if (authServerResponse.ok) { const authServerMetadata = await authServerResponse.json(); // Verify required OAuth 2.1 authorization server metadata fields expect(authServerMetadata).toHaveProperty('issuer'); expect(authServerMetadata).toHaveProperty('authorization_endpoint'); expect(authServerMetadata).toHaveProperty('token_endpoint'); expect(authServerMetadata).toHaveProperty('response_types_supported'); expect(authServerMetadata).toHaveProperty('grant_types_supported'); expect(authServerMetadata).toHaveProperty('code_challenge_methods_supported'); // Verify specific values (allow both localhost and 127.0.0.1) expect(authServerMetadata.issuer).toMatch(/http:\/\/(localhost|127\.0\.0\.1):\d+\//); expect(authServerMetadata.authorization_endpoint).toContain('/authorize'); expect(authServerMetadata.token_endpoint).toContain('/token'); expect(authServerMetadata.response_types_supported).toContain('code'); expect(authServerMetadata.grant_types_supported).toContain('authorization_code'); expect(authServerMetadata.code_challenge_methods_supported).toContain('S256'); } else { console.warn( 'OAuth authorization server metadata endpoint not available, status:', authServerResponse.status, ); } } catch (error) { console.warn( 'Could not test OAuth authorization server metadata:', error instanceof Error ? error.message : String(error), ); } } // Step 2: Test well-known OAuth protected resource endpoint (only if server is ready) if (serverReady) { try { const protectedResourceResponse = await fetch(`${baseUrl}/.well-known/oauth-protected-resource`, { signal: AbortSignal.timeout(5000), }); if (protectedResourceResponse.ok) { const protectedResourceMetadata = await protectedResourceResponse.json(); // Verify required OAuth protected resource metadata fields expect(protectedResourceMetadata).toHaveProperty('resource'); expect(protectedResourceMetadata).toHaveProperty('authorization_servers'); expect(protectedResourceMetadata).toHaveProperty('scopes_supported'); // Verify specific values (allow both localhost and 127.0.0.1) expect(protectedResourceMetadata.resource).toMatch(/http:\/\/(localhost|127\.0\.0\.1):\d+\//); expect(protectedResourceMetadata.authorization_servers).toEqual( expect.arrayContaining([expect.stringMatching(/http:\/\/(localhost|127\.0\.0\.1):\d+\//)]), ); expect(protectedResourceMetadata.scopes_supported).toEqual(expect.arrayContaining(['tag:echo', 'tag:test'])); } else { console.warn( 'OAuth protected resource metadata endpoint not available, status:', protectedResourceResponse.status, ); } } catch (error) { console.warn( 'Could not test OAuth protected resource metadata:', error instanceof Error ? error.message : String(error), ); } } // Step 2.5: Test invalid OAuth well-known endpoints return 404 (only if server is ready) if (serverReady) { try { const invalidPaths = [ `${baseUrl}/.well-known/oauth-protected-resource/mcp`, `${baseUrl}/.well-known/oauth-authorization-server/mcp`, ]; for (const invalidPath of invalidPaths) { // Test GET request returns 404 const getResponse = await fetch(invalidPath, { signal: AbortSignal.timeout(5000), }); expect(getResponse.status).toBe(404); // Test OPTIONS request - SDK might handle this differently const optionsResponse = await fetch(invalidPath, { method: 'OPTIONS', signal: AbortSignal.timeout(5000), }); // OPTIONS may return 204 (No Content) for CORS preflight or 404, both are acceptable expect([204, 404]).toContain(optionsResponse.status); // Only verify error response format for GET requests (when status is 404) if (getResponse.status === 404) { const responseText = await getResponse.text(); // Check if it's JSON or HTML if (responseText.startsWith('{')) { const errorData = JSON.parse(responseText); expect(errorData).toMatchObject({ error: expect.any(String), }); } else { // If it's HTML (e.g., error page), just verify the status code expect(responseText).toBeTruthy(); } } } } catch (error) { console.warn('Could not test invalid OAuth paths:', error instanceof Error ? error.message : String(error)); } } // Step 3: Test WWW-Authenticate header on unauthorized access to protected endpoints (only if server is ready) if (serverReady) { try { const unauthorizedResponse = await fetch(`${baseUrl}/sse`, { headers: { Accept: 'text/event-stream' }, signal: AbortSignal.timeout(5000), }); if (unauthorizedResponse.status === 401) { const wwwAuth = unauthorizedResponse.headers.get('WWW-Authenticate'); expect(wwwAuth).toBeTruthy(); expect(wwwAuth).toMatch(/^Bearer/); // Should contain resource_metadata parameter per MCP SDK implementation expect(wwwAuth).toContain('resource_metadata='); } else { console.warn('Expected 401 for unauthorized access, got:', unauthorizedResponse.status); } } catch (error) { console.warn('Could not test WWW-Authenticate header:', error instanceof Error ? error.message : String(error)); } } // Step 4-8: Complete OAuth flow test (structure validation) // Test OAuth authorization request structure const authUrl = new URL(`${baseUrl}/oauth/authorize`); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', clientId); authUrl.searchParams.set('redirect_uri', redirectUri); authUrl.searchParams.set('scope', 'test echo'); authUrl.searchParams.set('state', state); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); // Validate that OAuth URL is properly constructed expect(authUrl.toString()).toContain('response_type=code'); expect(authUrl.toString()).toContain('code_challenge='); expect(authUrl.toString()).toContain('code_challenge_method=S256'); expect(authUrl.toString()).toContain('state='); // Test token exchange request structure const tokenData = new URLSearchParams(); tokenData.set('grant_type', 'authorization_code'); tokenData.set('client_id', clientId); tokenData.set('client_secret', clientSecret); tokenData.set('code', 'mock-auth-code'); tokenData.set('redirect_uri', redirectUri); tokenData.set('code_verifier', codeVerifier); // Validate token request structure expect(tokenData.get('grant_type')).toBe('authorization_code'); expect(tokenData.get('code_verifier')).toBe(codeVerifier); expect(tokenData.get('client_id')).toBe(clientId); // Validate PKCE implementation expect(codeVerifier.length).toBeGreaterThanOrEqual(43); expect(codeChallenge.length).toBeGreaterThan(0); expect(codeChallenge).not.toBe(codeVerifier); // Challenge should be different from verifier // Clean up the process await processManager.stopProcess('mcp-agent'); }); }); // PKCE utility functions function generateCodeVerifier(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; let result = ''; for (let i = 0; i < 128; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } function generateCodeChallenge(verifier: string): string { // Use Node.js crypto module for proper SHA256 hashing const crypto = require('crypto'); const hash = crypto.createHash('sha256').update(verifier).digest(); return hash.toString('base64url'); // base64url encoding per OAuth 2.1 spec } function generateRandomState(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); }

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/1mcp-app/agent'

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