Skip to main content
Glama

1MCP Server

clientDataRepository.test.ts22.4 kB
import fs from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js'; import { AUTH_CONFIG } from '@src/constants.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ClientDataRepository } from './clientDataRepository.js'; import { FileStorageService } from './fileStorageService.js'; // Mock logger to avoid console output during tests vi.mock('@src/logger/logger.js', () => ({ default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }, })); describe('ClientDataRepository', () => { let repository: ClientDataRepository; let storage: FileStorageService; let tempDir: string; beforeEach(() => { // Create a temporary directory for testing tempDir = path.join(tmpdir(), `client-data-repo-test-${Date.now()}`); storage = new FileStorageService(tempDir); repository = new ClientDataRepository(storage); }); afterEach(() => { storage.shutdown(); // Clean up temp directory if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } }); describe('save', () => { it('should save OAuth client data with all fields', () => { const clientId = 'cli_test-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'oauth-client-123', client_secret: 'secret-value', client_name: 'Test Application', client_uri: 'https://testapp.example.com', redirect_uris: ['https://testapp.example.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], scope: 'openid profile email', token_endpoint_auth_method: 'client_secret_basic', client_id_issued_at: Math.floor(Date.now() / 1000), client_secret_expires_at: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days }; const ttlMs = 30 * 24 * 60 * 60 * 1000; // 30 days repository.save(clientId, clientData, ttlMs); const retrieved = repository.get(clientId); expect(retrieved).toBeDefined(); expect(retrieved!.client_id).toBe(clientData.client_id); expect(retrieved!.client_secret).toBe(clientData.client_secret); expect(retrieved!.client_name).toBe(clientData.client_name); expect(retrieved!.client_uri).toBe(clientData.client_uri); expect(retrieved!.redirect_uris).toEqual(clientData.redirect_uris); expect(retrieved!.grant_types).toEqual(clientData.grant_types); expect(retrieved!.response_types).toEqual(clientData.response_types); expect(retrieved!.scope).toBe(clientData.scope); expect(retrieved!.token_endpoint_auth_method).toBe(clientData.token_endpoint_auth_method); }); it('should handle client data with minimal required fields', () => { const clientId = 'cli_minimal-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'minimal-client', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; const ttlMs = 24 * 60 * 60 * 1000; // 1 day repository.save(clientId, clientData, ttlMs); const retrieved = repository.get(clientId); expect(retrieved!.client_id).toBe('minimal-client'); expect(retrieved!.redirect_uris).toEqual(['https://app.com/callback']); expect(retrieved!.grant_types).toEqual(['authorization_code']); expect(retrieved!.response_types).toEqual(['code']); }); it('should use client_secret_expires_at when provided', () => { const clientId = 'cli_expiry-client-1234-4abc-89de-123456789012'; const futureTimestamp = Math.floor(Date.now() / 1000) + 60 * 60; // 1 hour from now const clientData: OAuthClientInformationFull = { client_id: 'expiry-test', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], client_secret_expires_at: futureTimestamp, }; const ttlMs = 30 * 24 * 60 * 60 * 1000; // 30 days (should be ignored) repository.save(clientId, clientData, ttlMs); const retrieved = repository.get(clientId); // Should use client_secret_expires_at * 1000 instead of Date.now() + ttlMs expect((retrieved as any).expires).toBe(futureTimestamp * 1000); }); it('should use TTL when client_secret_expires_at is not provided', () => { const clientId = 'cli_ttl-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'ttl-test', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; const ttlMs = 60000; // 1 minute const beforeSave = Date.now(); repository.save(clientId, clientData, ttlMs); const afterSave = Date.now(); const retrieved = repository.get(clientId); const expectedMinExpiry = beforeSave + ttlMs; const expectedMaxExpiry = afterSave + ttlMs; expect((retrieved as any).expires).toBeGreaterThanOrEqual(expectedMinExpiry); expect((retrieved as any).expires).toBeLessThanOrEqual(expectedMaxExpiry); }); it('should use client_id_issued_at when provided', () => { const clientId = 'cli_issued-client-1234-4abc-89de-123456789012'; const issuedTimestamp = Math.floor(Date.now() / 1000) - 60 * 60; // 1 hour ago const clientData: OAuthClientInformationFull = { client_id: 'issued-test', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], client_id_issued_at: issuedTimestamp, }; const ttlMs = 60000; repository.save(clientId, clientData, ttlMs); const retrieved = repository.get(clientId); expect((retrieved as any).createdAt).toBe(issuedTimestamp * 1000); }); it('should handle overwriting existing client data', () => { const clientId = 'cli_overwrite-client-1234-4abc-89de-123456789012'; const originalData: OAuthClientInformationFull = { client_id: 'test-client', client_name: 'Original App', redirect_uris: ['https://original.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; const updatedData: OAuthClientInformationFull = { client_id: 'test-client', client_name: 'Updated App', redirect_uris: ['https://updated.com/callback'], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], }; const ttlMs = 60000; repository.save(clientId, originalData, ttlMs); repository.save(clientId, updatedData, ttlMs); const retrieved = repository.get(clientId); expect(retrieved!.client_name).toBe('Updated App'); expect(retrieved!.redirect_uris).toEqual(['https://updated.com/callback']); expect(retrieved!.grant_types).toEqual(['authorization_code', 'refresh_token']); }); }); describe('get', () => { it('should retrieve existing client data', () => { const clientId = 'cli_get-test-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'get-test-client', client_name: 'Get Test App', redirect_uris: ['https://gettest.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); const retrieved = repository.get(clientId); expect(retrieved).toBeDefined(); expect(retrieved!.client_id).toBe('get-test-client'); expect(retrieved!.client_name).toBe('Get Test App'); }); it('should return null for non-existent client', () => { const result = repository.get('cli_nonexistent-1234-4abc-89de-123456789012'); expect(result).toBeNull(); }); it('should handle malformed client IDs gracefully', () => { const malformedIds = ['', ' ', 'invalid/id', '../../../etc/passwd']; for (const id of malformedIds) { const result = repository.get(id); expect(result).toBeNull(); } }); it('should preserve all OAuth client fields', () => { const clientId = 'cli_full-fields-client-1234-4abc-89de-123456789012'; const fullClientData: OAuthClientInformationFull = { client_id: 'full-test-client', client_secret: 'super-secret-value', client_name: 'Full Featured App', client_uri: 'https://fullapp.example.com', logo_uri: 'https://fullapp.example.com/logo.png', tos_uri: 'https://fullapp.example.com/terms', policy_uri: 'https://fullapp.example.com/privacy', redirect_uris: ['https://fullapp.example.com/callback', 'https://fullapp.example.com/oauth/callback'], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'openid profile email read:data write:data', contacts: ['admin@fullapp.example.com'], token_endpoint_auth_method: 'client_secret_post', client_id_issued_at: Math.floor(Date.now() / 1000), client_secret_expires_at: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365, // 1 year }; repository.save(clientId, fullClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.client_id).toBe(fullClientData.client_id); expect(retrieved!.client_secret).toBe(fullClientData.client_secret); expect(retrieved!.client_name).toBe(fullClientData.client_name); expect(retrieved!.client_uri).toBe(fullClientData.client_uri); expect(retrieved!.logo_uri).toBe(fullClientData.logo_uri); expect(retrieved!.tos_uri).toBe(fullClientData.tos_uri); expect(retrieved!.policy_uri).toBe(fullClientData.policy_uri); expect(retrieved!.redirect_uris).toEqual(fullClientData.redirect_uris); expect(retrieved!.grant_types).toEqual(fullClientData.grant_types); expect(retrieved!.response_types).toEqual(fullClientData.response_types); expect(retrieved!.scope).toBe(fullClientData.scope); expect(retrieved!.contacts).toEqual(fullClientData.contacts); expect(retrieved!.token_endpoint_auth_method).toBe(fullClientData.token_endpoint_auth_method); }); }); describe('delete', () => { it('should delete existing client data', () => { const clientId = 'cli_delete-test-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'delete-test-client', redirect_uris: ['https://deletetest.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); // Verify client exists expect(repository.get(clientId)).toBeDefined(); // Delete client const deleted = repository.delete(clientId); expect(deleted).toBe(true); // Verify client is gone expect(repository.get(clientId)).toBeNull(); }); it('should return false when deleting non-existent client', () => { const deleted = repository.delete('cli_nonexistent-1234-4abc-89de-123456789012'); expect(deleted).toBe(false); }); it('should handle multiple deletions of same client', () => { const clientId = 'cli_multi-delete-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'multi-delete-client', redirect_uris: ['https://multidelete.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); const deleted1 = repository.delete(clientId); expect(deleted1).toBe(true); const deleted2 = repository.delete(clientId); expect(deleted2).toBe(false); }); it('should delete only the specified client', () => { const clientId1 = 'cli_client1-1234-4abc-89de-123456789012'; const clientId2 = 'cli_client2-1234-4abc-89de-123456789012'; const clientData1: OAuthClientInformationFull = { client_id: 'client-1', redirect_uris: ['https://client1.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; const clientData2: OAuthClientInformationFull = { client_id: 'client-2', redirect_uris: ['https://client2.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId1, clientData1, 60000); repository.save(clientId2, clientData2, 60000); repository.delete(clientId1); expect(repository.get(clientId1)).toBeNull(); expect(repository.get(clientId2)).toBeDefined(); }); }); describe('Integration with FileStorageService', () => { it('should use correct file prefix', () => { const clientId = 'cli_file-test-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'file-test-client', redirect_uris: ['https://filetest.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); // Check that file was created with correct prefix const expectedFileName = AUTH_CONFIG.SERVER.SESSION.FILE_PREFIX + clientId + '.json'; const filePath = path.join(tempDir, expectedFileName); expect(fs.existsSync(filePath)).toBe(true); }); it('should survive FileStorageService restart', () => { const clientId = 'cli_restart-test-client-1234-4abc-89de-123456789012'; const clientData: OAuthClientInformationFull = { client_id: 'restart-test-client', client_name: 'Restart Test App', redirect_uris: ['https://restarttest.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); const originalData = repository.get(clientId); // Shutdown and recreate storage service storage.shutdown(); storage = new FileStorageService(tempDir); repository = new ClientDataRepository(storage); // Data should still be accessible const retrievedData = repository.get(clientId); expect(retrievedData).toEqual(originalData); }); it('should handle storage errors gracefully', () => { // This test would need to mock FileStorageService to simulate errors // For now, we verify that the repository doesn't crash on invalid operations const result = repository.get('invalid-client-id'); expect(result).toBeNull(); }); }); describe('OAuth 2.1 Client Types', () => { it('should handle public clients (no secret)', () => { const clientId = 'cli_public-client-1234-4abc-89de-123456789012'; const publicClientData: OAuthClientInformationFull = { client_id: 'public-spa-client', client_name: 'Public SPA Application', redirect_uris: ['http://localhost:3000/callback'], grant_types: ['authorization_code'], response_types: ['code'], token_endpoint_auth_method: 'none', // Public client scope: 'openid profile', }; repository.save(clientId, publicClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.client_secret).toBeUndefined(); expect(retrieved!.token_endpoint_auth_method).toBe('none'); }); it('should handle confidential clients (with secret)', () => { const clientId = 'cli_confidential-client-1234-4abc-89de-123456789012'; const confidentialClientData: OAuthClientInformationFull = { client_id: 'confidential-server-client', client_secret: 'very-secure-server-secret', client_name: 'Confidential Server Application', redirect_uris: ['https://secure-app.example.com/oauth/callback'], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'client_secret_basic', scope: 'admin:read admin:write', }; repository.save(clientId, confidentialClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.client_secret).toBe('very-secure-server-secret'); expect(retrieved!.token_endpoint_auth_method).toBe('client_secret_basic'); expect(retrieved!.grant_types).toContain('refresh_token'); }); it('should handle different authentication methods', () => { const authMethods = ['client_secret_basic', 'client_secret_post', 'client_secret_jwt', 'private_key_jwt', 'none']; for (const method of authMethods) { const clientId = `cli_auth-method-${method}-1234-4abc-89de-123456789012`; const clientData: OAuthClientInformationFull = { client_id: `auth-method-${method}`, redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], token_endpoint_auth_method: method, }; repository.save(clientId, clientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.token_endpoint_auth_method).toBe(method); } }); it('should handle multiple redirect URIs', () => { const clientId = 'cli_multi-redirect-client-1234-4abc-89de-123456789012'; const multiRedirectClientData: OAuthClientInformationFull = { client_id: 'multi-redirect-client', redirect_uris: [ 'https://app.example.com/oauth/callback', 'https://app.example.com/auth/callback', 'myapp://oauth/callback', 'http://localhost:3000/callback', ], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, multiRedirectClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.redirect_uris).toHaveLength(4); expect(retrieved!.redirect_uris).toContain('myapp://oauth/callback'); expect(retrieved!.redirect_uris).toContain('http://localhost:3000/callback'); }); }); describe('Performance and Edge Cases', () => { it('should handle Unicode characters in client data', () => { const clientId = 'cli_unicode-client-1234-4abc-89de-123456789012'; const unicodeClientData: OAuthClientInformationFull = { client_id: 'unicode-测试-🌍', client_name: 'тест приложение - 🚀', client_uri: 'https://تطبيق.example.com', redirect_uris: ['https://тест.example.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], scope: 'читать писать', contacts: ['admin@تطبيق.example.com'], }; repository.save(clientId, unicodeClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.client_id).toBe('unicode-测试-🌍'); expect(retrieved!.client_name).toBe('тест приложение - 🚀'); expect(retrieved!.client_uri).toBe('https://تطبيق.example.com'); expect(retrieved!.scope).toBe('читать писать'); }); it('should handle very long client data', () => { const clientId = 'cli_long-data-client-1234-4abc-89de-123456789012'; const longScope = Array.from({ length: 100 }, (_, i) => `scope${i}`).join(' '); const longClientData: OAuthClientInformationFull = { client_id: 'long-data-client', client_name: 'A'.repeat(500), // Very long name client_uri: 'https://very-long-domain-name-that-goes-on-and-on.example.com', redirect_uris: Array.from({ length: 50 }, (_, i) => `https://app${i}.example.com/callback`), grant_types: ['authorization_code', 'refresh_token', 'client_credentials'], response_types: ['code', 'token', 'id_token'], scope: longScope, contacts: Array.from({ length: 10 }, (_, i) => `admin${i}@example.com`), }; repository.save(clientId, longClientData, 60000); const retrieved = repository.get(clientId); expect(retrieved!.client_name).toHaveLength(500); expect(retrieved!.redirect_uris).toHaveLength(50); expect(retrieved!.scope!.split(' ')).toHaveLength(100); expect(retrieved!.contacts).toHaveLength(10); }); it('should handle creating many clients', () => { const clientIds: string[] = []; const numClients = 10; // Reduced for testing performance for (let i = 0; i < numClients; i++) { const clientId = `cli_mass-client-${i}-1234-4abc-89de-123456789012`; const clientData: OAuthClientInformationFull = { client_id: `mass-client-${i}`, client_name: `Mass Test Client ${i}`, redirect_uris: [`https://client${i}.example.com/callback`], grant_types: ['authorization_code'], response_types: ['code'], }; repository.save(clientId, clientData, 60000); clientIds.push(clientId); } expect(clientIds.length).toBe(numClients); // Verify all clients can be retrieved for (const clientId of clientIds) { const retrieved = repository.get(clientId); expect(retrieved).toBeDefined(); } }); it('should handle complex expiration scenarios', () => { const clientId = 'cli_expiry-scenarios-client-1234-4abc-89de-123456789012'; // Test with future expiration const futureExpiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour const clientData: OAuthClientInformationFull = { client_id: 'expiry-test', redirect_uris: ['https://app.com/callback'], grant_types: ['authorization_code'], response_types: ['code'], client_secret_expires_at: futureExpiry, }; repository.save(clientId, clientData, 60000); const retrieved = repository.get(clientId); // Should use the provided expiration time expect((retrieved as any).expires).toBe(futureExpiry * 1000); }); }); });

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