Skip to main content
Glama

Financial Modeling Prep MCP Server

Apache 2.0
17
59
  • Linux
  • Apple
TransportConfigurationValidation.test.ts23.9 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { readFile, writeFile, mkdir, rm } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; import { validateServerJsonSchema } from '../utils/versionSync.js'; describe('TransportConfigurationValidation', () => { let testDir: string; beforeEach(async () => { testDir = join(tmpdir(), `transport-config-test-${Date.now()}`); await mkdir(testDir, { recursive: true }); }); afterEach(async () => { await rm(testDir, { recursive: true, force: true }); }); /** * Creates a base server.json with specified transport configuration. * @param transportConfig - Transport configuration to use. * @param packageOverrides - Optional package-level overrides. */ async function createServerJsonWithTransport( transportConfig: any, packageOverrides: any = {} ): Promise<void> { const serverJson = { $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', name: 'io.github.imbenrabi/financial-modeling-prep-mcp-server', description: 'MCP server for Financial Modeling Prep API with 250+ financial data tools', version: '2.5.0', status: 'active', packages: [ { registry_type: 'npm', registry_base_url: 'https://registry.npmjs.org', identifier: 'financial-modeling-prep-mcp-server', version: '2.5.0', runtime_hint: 'npx', transport: transportConfig, ...packageOverrides } ], repository: { url: 'https://github.com/imbenrabi/Financial-Modeling-Prep-MCP-Server', source: 'github', id: '988409529' }, website_url: 'https://github.com/imbenrabi/Financial-Modeling-Prep-MCP-Server' }; await writeFile( join(testDir, 'server.json'), JSON.stringify(serverJson, null, 2), 'utf-8' ); } describe('HTTP Transport Configuration', () => { it('should validate streamable-http transport with URL', async () => { const transportConfig = { type: 'streamable-http', url: 'https://financial-modeling-prep-mcp-server-production.up.railway.app/mcp' }; await createServerJsonWithTransport(transportConfig); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const transport = serverJson.packages[0].transport; expect(transport.type).toBe('streamable-http'); expect(transport.url).toBeDefined(); expect(transport.url).toMatch(/^https:\/\//); }); it('should validate streamable-http transport with headers', async () => { const transportConfig = { type: 'streamable-http', url: 'https://example.com/mcp', headers: [ { name: 'Authorization', value: 'Bearer token' }, { name: 'Content-Type', value: 'application/json' } ] }; await createServerJsonWithTransport(transportConfig); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const transport = serverJson.packages[0].transport; expect(transport.headers).toBeDefined(); expect(Array.isArray(transport.headers)).toBe(true); expect(transport.headers).toHaveLength(2); expect(transport.headers[0].name).toBe('Authorization'); expect(transport.headers[1].name).toBe('Content-Type'); }); it('should validate SSE transport configuration', async () => { const transportConfig = { type: 'sse', url: 'https://example.com/sse-endpoint' }; await createServerJsonWithTransport(transportConfig); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const transport = serverJson.packages[0].transport; expect(transport.type).toBe('sse'); expect(transport.url).toBeDefined(); expect(transport.url).toMatch(/^https:\/\//); }); it('should validate URL format requirements', async () => { const validUrls = [ 'https://example.com/mcp', 'https://api.example.com:8080/mcp', 'https://subdomain.example.com/path/to/mcp', 'http://localhost:3000/mcp' // Local development ]; for (const url of validUrls) { const transportConfig = { type: 'streamable-http', url }; await createServerJsonWithTransport(transportConfig); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const transport = serverJson.packages[0].transport; expect(() => new URL(transport.url)).not.toThrow(); } }); }); describe('STDIO Transport Configuration', () => { it('should validate stdio transport without URL', async () => { const transportConfig = { type: 'stdio' }; await createServerJsonWithTransport(transportConfig); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const transport = serverJson.packages[0].transport; expect(transport.type).toBe('stdio'); expect(transport.url).toBeUndefined(); }); it('should validate stdio transport with runtime arguments', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { package_arguments: [ { type: 'named', name: '--fmp-token', description: 'Financial Modeling Prep API access token', is_required: false, format: 'string' }, { type: 'named', name: '--port', description: 'Port number for HTTP server mode', is_required: false, format: 'number' } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.transport.type).toBe('stdio'); expect(pkg.package_arguments).toBeDefined(); expect(pkg.package_arguments).toHaveLength(2); }); }); describe('Runtime Arguments Validation', () => { it('should validate named arguments structure', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { package_arguments: [ { type: 'named', name: '--fmp-token', description: 'Financial Modeling Prep API access token', is_required: true, format: 'string' }, { type: 'named', name: '--debug', description: 'Enable debug mode', is_required: false, format: 'boolean' }, { type: 'named', name: '--port', description: 'Port number', is_required: false, format: 'number' } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const args = serverJson.packages[0].package_arguments; expect(args).toHaveLength(3); const tokenArg = args.find((arg: any) => arg.name === '--fmp-token'); expect(tokenArg.type).toBe('named'); expect(tokenArg.is_required).toBe(true); expect(tokenArg.format).toBe('string'); const debugArg = args.find((arg: any) => arg.name === '--debug'); expect(debugArg.format).toBe('boolean'); expect(debugArg.is_required).toBe(false); const portArg = args.find((arg: any) => arg.name === '--port'); expect(portArg.format).toBe('number'); }); it('should validate positional arguments structure', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { package_arguments: [ { type: 'positional', name: 'config-file', description: 'Path to configuration file', is_required: true, format: 'string' }, { type: 'positional', name: 'output-dir', description: 'Output directory', is_required: false, format: 'string' } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const args = serverJson.packages[0].package_arguments; expect(args).toHaveLength(2); const configArg = args.find((arg: any) => arg.name === 'config-file'); expect(configArg.type).toBe('positional'); expect(configArg.is_required).toBe(true); const outputArg = args.find((arg: any) => arg.name === 'output-dir'); expect(outputArg.type).toBe('positional'); expect(outputArg.is_required).toBe(false); }); it('should validate argument format types', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { package_arguments: [ { type: 'named', name: '--string-arg', description: 'String argument', format: 'string' }, { type: 'named', name: '--number-arg', description: 'Number argument', format: 'number' }, { type: 'named', name: '--boolean-arg', description: 'Boolean argument', format: 'boolean' }, { type: 'named', name: '--array-arg', description: 'Array argument', format: 'array' } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const args = serverJson.packages[0].package_arguments; const formats = args.map((arg: any) => arg.format); expect(formats).toContain('string'); expect(formats).toContain('number'); expect(formats).toContain('boolean'); expect(formats).toContain('array'); }); }); describe('Environment Variables Configuration', () => { it('should validate environment variables structure', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { environment_variables: [ { name: 'FMP_ACCESS_TOKEN', description: 'Financial Modeling Prep API access token', is_required: true, format: 'string', is_secret: true }, { name: 'DEBUG_MODE', description: 'Enable debug logging', is_required: false, format: 'boolean', is_secret: false }, { name: 'PORT', description: 'Server port number', is_required: false, format: 'number', is_secret: false } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const envVars = serverJson.packages[0].environment_variables; expect(envVars).toHaveLength(3); const tokenVar = envVars.find((env: any) => env.name === 'FMP_ACCESS_TOKEN'); expect(tokenVar.is_required).toBe(true); expect(tokenVar.is_secret).toBe(true); expect(tokenVar.format).toBe('string'); const debugVar = envVars.find((env: any) => env.name === 'DEBUG_MODE'); expect(debugVar.format).toBe('boolean'); expect(debugVar.is_secret).toBe(false); const portVar = envVars.find((env: any) => env.name === 'PORT'); expect(portVar.format).toBe('number'); }); it('should validate secret environment variables', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { environment_variables: [ { name: 'API_KEY', description: 'Secret API key', is_secret: true, format: 'string' }, { name: 'DATABASE_PASSWORD', description: 'Database password', is_secret: true, format: 'string' }, { name: 'PUBLIC_CONFIG', description: 'Public configuration', is_secret: false, format: 'string' } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const envVars = serverJson.packages[0].environment_variables; const secretVars = envVars.filter((env: any) => env.is_secret === true); const publicVars = envVars.filter((env: any) => env.is_secret === false); expect(secretVars).toHaveLength(2); expect(publicVars).toHaveLength(1); expect(secretVars.map((env: any) => env.name)).toContain('API_KEY'); expect(secretVars.map((env: any) => env.name)).toContain('DATABASE_PASSWORD'); expect(publicVars[0].name).toBe('PUBLIC_CONFIG'); }); }); describe('Runtime Hints Validation', () => { it('should validate NPX runtime hint', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { runtime_hint: 'npx' }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.runtime_hint).toBe('npx'); }); it('should validate UVX runtime hint', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { registry_type: 'pypi', runtime_hint: 'uvx' }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.runtime_hint).toBe('uvx'); expect(pkg.registry_type).toBe('pypi'); }); it('should validate Docker runtime hint', async () => { const transportConfig = { type: 'streamable-http', url: 'http://localhost:8080/mcp' }; const packageOverrides = { registry_type: 'oci', runtime_hint: 'docker' }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.runtime_hint).toBe('docker'); expect(pkg.registry_type).toBe('oci'); }); it('should validate custom runtime hints', async () => { const customHints = ['node', 'python', 'java', 'go']; for (const hint of customHints) { const transportConfig = { type: 'stdio' }; const packageOverrides = { runtime_hint: hint }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.runtime_hint).toBe(hint); } }); }); describe('Transport and Arguments Integration', () => { it('should validate HTTP transport with runtime arguments', async () => { const transportConfig = { type: 'streamable-http', url: 'https://example.com/mcp' }; const packageOverrides = { package_arguments: [ { type: 'named', name: '--token', description: 'API token for authentication', is_required: true, format: 'string' } ], environment_variables: [ { name: 'API_TOKEN', description: 'API token from environment', is_required: false, format: 'string', is_secret: true } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.transport.type).toBe('streamable-http'); expect(pkg.transport.url).toBeDefined(); expect(pkg.package_arguments).toHaveLength(1); expect(pkg.environment_variables).toHaveLength(1); }); it('should validate stdio transport with comprehensive configuration', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { runtime_hint: 'npx', package_arguments: [ { type: 'named', name: '--fmp-token', description: 'FMP API token', is_required: false, format: 'string' }, { type: 'named', name: '--port', description: 'HTTP server port', is_required: false, format: 'number' }, { type: 'named', name: '--dynamic-tool-discovery', description: 'Enable dynamic tool discovery', is_required: false, format: 'boolean' }, { type: 'named', name: '--fmp-tool-sets', description: 'Comma-separated tool sets', is_required: false, format: 'string' } ], environment_variables: [ { name: 'FMP_ACCESS_TOKEN', description: 'FMP API access token', is_required: false, format: 'string', is_secret: true }, { name: 'PORT', description: 'HTTP server port', is_required: false, format: 'number', is_secret: false }, { name: 'DYNAMIC_TOOL_DISCOVERY', description: 'Enable dynamic tool discovery', is_required: false, format: 'boolean', is_secret: false }, { name: 'FMP_TOOL_SETS', description: 'Comma-separated tool sets', is_required: false, format: 'string', is_secret: false } ] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.transport.type).toBe('stdio'); expect(pkg.runtime_hint).toBe('npx'); expect(pkg.package_arguments).toHaveLength(4); expect(pkg.environment_variables).toHaveLength(4); // Validate specific arguments match our server's actual configuration const tokenArg = pkg.package_arguments.find((arg: any) => arg.name === '--fmp-token'); expect(tokenArg).toBeDefined(); const portArg = pkg.package_arguments.find((arg: any) => arg.name === '--port'); expect(portArg).toBeDefined(); const dynamicArg = pkg.package_arguments.find((arg: any) => arg.name === '--dynamic-tool-discovery'); expect(dynamicArg).toBeDefined(); const toolSetsArg = pkg.package_arguments.find((arg: any) => arg.name === '--fmp-tool-sets'); expect(toolSetsArg).toBeDefined(); }); }); describe('Multiple Transport Support', () => { it('should validate server with both HTTP and stdio transports', async () => { const serverJson = { $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', name: 'io.github.imbenrabi/financial-modeling-prep-mcp-server', description: 'MCP server with multiple transport options', version: '2.5.0', packages: [ { registry_type: 'npm', identifier: 'financial-modeling-prep-mcp-server', version: '2.5.0', runtime_hint: 'npx', transport: { type: 'streamable-http', url: 'https://example.com/mcp' } } ], remotes: [ { type: 'streamable-http', url: 'https://alternative.example.com/mcp' }, { type: 'sse', url: 'https://sse.example.com/events' } ] }; await writeFile( join(testDir, 'server.json'), JSON.stringify(serverJson, null, 2), 'utf-8' ); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); const parsedServerJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); expect(parsedServerJson.packages[0].transport.type).toBe('streamable-http'); expect(parsedServerJson.remotes).toHaveLength(2); expect(parsedServerJson.remotes[0].type).toBe('streamable-http'); expect(parsedServerJson.remotes[1].type).toBe('sse'); }); }); describe('Transport Configuration Edge Cases', () => { it('should handle missing transport configuration', async () => { const serverJson = { $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', name: 'io.github.imbenrabi/financial-modeling-prep-mcp-server', description: 'MCP server without transport', version: '2.5.0', packages: [ { registry_type: 'npm', identifier: 'financial-modeling-prep-mcp-server', version: '2.5.0' // No transport specified } ] }; await writeFile( join(testDir, 'server.json'), JSON.stringify(serverJson, null, 2), 'utf-8' ); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); // Transport is optional }); it('should validate empty arguments and environment variables arrays', async () => { const transportConfig = { type: 'stdio' }; const packageOverrides = { package_arguments: [], environment_variables: [] }; await createServerJsonWithTransport(transportConfig, packageOverrides); const result = await validateServerJsonSchema(testDir); expect(result.isValid).toBe(true); const serverJson = JSON.parse(await readFile(join(testDir, 'server.json'), 'utf-8')); const pkg = serverJson.packages[0]; expect(pkg.package_arguments).toHaveLength(0); expect(pkg.environment_variables).toHaveLength(0); }); }); });

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/imbenrabi/Financial-Modeling-Prep-MCP-Server'

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