Skip to main content
Glama
shell.environment.test.ts10.9 kB
import {describe, it, expect, beforeAll} from 'vitest'; import {spawn, SpawnOptions} from 'child_process'; import {Client} from '@modelcontextprotocol/sdk/client/index.js'; import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'; import {fileURLToPath} from 'url'; import {dirname, join} from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const indexPath = join(__dirname, 'index.ts'); /** * Detect available shells on the current system */ function getAvailableShells(): Array<{name: string; command: string; args: string[]}> { const shells: Array<{name: string; command: string; args: string[]}> = []; if (process.platform === 'win32') { // Windows shells shells.push( {name: 'cmd', command: 'cmd.exe', args: ['/c']}, {name: 'powershell', command: 'powershell.exe', args: ['-Command']}, {name: 'pwsh', command: 'pwsh.exe', args: ['-Command']} ); // Git Bash (if available) try { const gitBashPaths = [ 'C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe' ]; for (const path of gitBashPaths) { try { require('fs').accessSync(path); shells.push({name: 'git-bash', command: path, args: ['-c']}); break; } catch { // Path doesn't exist } } } catch { // Git Bash not available } } else { // Unix shells shells.push( {name: 'sh', command: '/bin/sh', args: ['-c']}, {name: 'bash', command: '/bin/bash', args: ['-c']} ); // Optional shells (check if they exist) const optionalShells = [ {name: 'zsh', command: '/bin/zsh', args: ['-c']}, {name: 'dash', command: '/bin/dash', args: ['-c']}, {name: 'fish', command: '/usr/bin/fish', args: ['-c']} ]; for (const shell of optionalShells) { try { require('fs').accessSync(shell.command); shells.push(shell); } catch { // Shell not available } } } return shells; } /** * Start the MCP server through a specific shell */ async function startServerThroughShell( shellCommand: string, shellArgs: string[], env: Record<string, string> = {} ): Promise<{client: Client; close: () => Promise<void>}> { const command = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const args = ['-y', 'tsx', indexPath]; // Construct the full command for the shell const fullCommand = `${command} ${args.join(' ')}`; const finalArgs = [...shellArgs, fullCommand]; const transport = new StdioClientTransport({ command: shellCommand, args: finalArgs, env: { ...process.env, ...env, PDFDANCER_DOCS_BASE_URL: 'https://docusaurus-cloudflare-search.michael-lahr-0b0.workers.dev/' } }); const client = new Client( { name: 'shell-test-client', version: '1.0.0' }, { capabilities: {} } ); await client.connect(transport); return { client, close: async () => { await client.close(); // Give processes time to clean up await new Promise(resolve => setTimeout(resolve, 100)); } }; } describe('Shell Environment Tests', () => { let availableShells: Array<{name: string; command: string; args: string[]}>; beforeAll(() => { availableShells = getAvailableShells(); console.log( `Detected shells: ${availableShells.map(s => s.name).join(', ')}` ); }); describe('Server startup across different shells', () => { for (const shell of getAvailableShells()) { it(`should start and respond in ${shell.name}`, async () => { try { const {client, close} = await startServerThroughShell( shell.command, shell.args ); try { // Test basic connectivity const tools = await client.listTools(); expect(tools.tools).toBeDefined(); expect(tools.tools.length).toBe(4); } finally { await close(); } } catch (error) { // Skip if shell is not actually available if ( error instanceof Error && (error.message.includes('ENOENT') || error.message.includes('not found')) ) { console.log(`Skipping ${shell.name}: not available`); return; } throw error; } }, 30000); } }); describe('Environment variable handling', () => { it('should handle different locale settings', async () => { const shell = availableShells[0]; const {client, close} = await startServerThroughShell( shell.command, shell.args, { LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8' } ); try { const result = await client.callTool({ name: 'version', arguments: {} }); expect(result.content).toBeDefined(); } finally { await close(); } }, 30000); it('should handle ASCII locale', async () => { const shell = availableShells[0]; const {client, close} = await startServerThroughShell( shell.command, shell.args, { LANG: 'C', LC_ALL: 'C' } ); try { const result = await client.callTool({ name: 'version', arguments: {} }); expect(result.content).toBeDefined(); } finally { await close(); } }, 30000); it('should handle custom PDFDANCER_DOCS_BASE_URL', async () => { const shell = availableShells[0]; const {client, close} = await startServerThroughShell( shell.command, shell.args, { PDFDANCER_DOCS_BASE_URL: 'https://docusaurus-cloudflare-search.michael-lahr-0b0.workers.dev/' } ); try { const tools = await client.listTools(); expect(tools.tools.length).toBe(4); } finally { await close(); } }, 30000); }); describe('Non-interactive shell behavior', () => { it('should work without TTY', async () => { const shell = availableShells[0]; // Spawn without a TTY const transport = new StdioClientTransport({ command: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['-y', 'tsx', indexPath], env: { ...process.env, TERM: undefined, // Remove terminal info PDFDANCER_DOCS_BASE_URL: 'https://docusaurus-cloudflare-search.michael-lahr-0b0.workers.dev/' } }); const client = new Client( { name: 'no-tty-client', version: '1.0.0' }, { capabilities: {} } ); try { await client.connect(transport); const tools = await client.listTools(); expect(tools.tools.length).toBe(4); await client.close(); } catch (error) { await client.close(); throw error; } }, 30000); }); describe('Path and working directory handling', () => { it('should work regardless of current working directory', async () => { const shell = availableShells[0]; const {client, close} = await startServerThroughShell( shell.command, shell.args, { PWD: '/tmp' } ); try { const result = await client.callTool({ name: 'version', arguments: {} }); expect(result.content).toBeDefined(); } finally { await close(); } }, 30000); }); describe('Special character handling in shell', () => { it('should handle queries with special shell characters', async () => { const shell = availableShells[0]; const {client, close} = await startServerThroughShell( shell.command, shell.args ); try { // Test queries that might break shell escaping const specialQueries = [ 'page & document', 'page | filter', 'page; test', 'page $(echo test)', 'page `echo test`', 'page "quoted"', "page 'single'", 'page\\escaped' ]; for (const query of specialQueries) { try { await client.callTool({ name: 'search-docs', arguments: {query} }); // Should not throw or execute shell commands } catch (error) { // Network errors are acceptable if ( error instanceof Error && !error.message.includes('fetch failed') && !error.message.includes('Request to') ) { throw error; } } } } finally { await close(); } }, 60000); }); });

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/MenschMachine/pdfdancer-mcp'

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