diff --git a/package-lock.json b/package-lock.json index f540bac..8820e62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "envmcp", - "version": "0.2.0", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "envmcp", - "version": "0.2.0", + "version": "0.2.3", "license": "MIT", "bin": { "envmcp": "dist/cli.js" diff --git a/package.json b/package.json index 29aa3b4..03bf9a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "envmcp", - "version": "0.2.1", + "name": "@kdbx/envmcp", + "version": "0.2.3", "description": "A lightweight way to use environment variables in your Cursor MCP server definitions.", "main": "dist/index.js", "bin": { diff --git a/src/cli.test.ts b/src/cli.test.ts index 61545fb..972d922 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -2,6 +2,7 @@ import { execFile } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; +import { spawn } from 'child_process'; describe('envmcp CLI', () => { const defaultEnvFile = path.join(process.cwd(), '.env.mcp'); @@ -94,4 +95,74 @@ describe('envmcp CLI', () => { done(); }); }, 10000); + + it('should read environment variables from stdin when --env-stdin is specified', (done) => { + const child = spawn(process.execPath, [ + path.join(process.cwd(), 'dist/cli.js'), + '--env-stdin', + 'node', + '-p', + 'process.env.STDIN_VAR' + ]); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + if (code !== 0) { + console.error('Test error - stderr:', stderr); + done.fail(new Error(`Process exited with code ${code}`)); + return; + } + expect(stdout.trim()).toBe('stdin_value'); + done(); + }); + + // Write environment variables to stdin + child.stdin.write('STDIN_VAR=stdin_value\n'); + child.stdin.end(); + }, 10000); + + it('should error when --env-file and --env-stdin are both specified', (done) => { + const args = [ + path.join(process.cwd(), 'dist/cli.js'), + '--env-file', + customEnvFile, + '--env-stdin', + 'node', + '-p', + 'process.env.TEST_VAR' + ]; + + execFile(process.execPath, args, (error, stdout, stderr) => { + expect(error).not.toBeNull(); + expect(stderr).toContain('--env-file and --env-stdin are mutually exclusive'); + done(); + }); + }, 10000); + + it('should error when --env-stdin is specified multiple times', (done) => { + const args = [ + path.join(process.cwd(), 'dist/cli.js'), + '--env-stdin', + '--env-stdin', + 'node', + '-p', + 'process.env.TEST_VAR' + ]; + + execFile(process.execPath, args, (error, stdout, stderr) => { + expect(error).not.toBeNull(); + expect(stderr).toContain('--env-stdin option specified more than once'); + done(); + }); + }, 10000); }); diff --git a/src/cli.ts b/src/cli.ts index cf3614f..6749f48 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,20 +1,23 @@ #!/usr/bin/env node -import { loadEnvironmentVariablesFromFile, executeCommand } from './index'; +import { loadEnvironmentVariablesFromFile, loadEnvironmentVariablesFromStdin, executeCommand } from './index'; function printUsage() { - console.error('Usage: envmcp [--env-file ] [args...]'); + console.error('Usage: envmcp [--env-file ] [--env-stdin] [args...]'); console.error('\nOptions:'); console.error(' -e, --env-file Specify a custom path to the environment file'); + console.error(' --env-stdin Read environment variables from stdin'); console.error('\nArguments:'); console.error(' The command to execute'); console.error(' [args...] Arguments for the command'); console.error('\nNote: All options must precede the command.'); + console.error('Note: --env-file and --env-stdin are mutually exclusive.'); } -function main() { +async function main() { const argv = process.argv.slice(2); let customEnvFilePath: string | undefined = undefined; + let useStdin = false; let commandIndex = 0; let error = false; @@ -27,6 +30,11 @@ function main() { error = true; break; } + if (useStdin) { + console.error('Error: --env-file and --env-stdin are mutually exclusive.'); + error = true; + break; + } if (commandIndex + 1 >= argv.length || argv[commandIndex + 1].startsWith('-')) { console.error(`Error: ${arg} option requires a path.`); error = true; @@ -34,6 +42,19 @@ function main() { } customEnvFilePath = argv[commandIndex + 1]; commandIndex += 2; + } else if (arg === '--env-stdin') { + if (useStdin) { + console.error('Error: --env-stdin option specified more than once.'); + error = true; + break; + } + if (customEnvFilePath !== undefined) { + console.error('Error: --env-file and --env-stdin are mutually exclusive.'); + error = true; + break; + } + useStdin = true; + commandIndex += 1; } else { // First non-option argument marks the start of the command break; @@ -53,8 +74,15 @@ function main() { const command = commandArgs[0]; const actualArgsForCommand = commandArgs.slice(1); - // Load environment variables from .env.mcp file - if (!loadEnvironmentVariablesFromFile(customEnvFilePath)) { + // Load environment variables from .env.mcp file or stdin + let envLoaded = false; + if (useStdin) { + envLoaded = await loadEnvironmentVariablesFromStdin(); + } else { + envLoaded = loadEnvironmentVariablesFromFile(customEnvFilePath); + } + + if (!envLoaded) { process.exit(1); } diff --git a/src/index.ts b/src/index.ts index 15e9261..63b4af4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,12 +59,11 @@ export function findEnvFilePath(startDir: string = process.cwd()): string | unde } /** - * Parses an environment file and returns its contents as key-value pairs - * @param filePath Path to the environment file + * Parses environment content (from file or stdin) and returns key-value pairs + * @param content The content string to parse * @returns Object containing environment variables */ -function parseEnvFile(filePath: string): Record { - const content = fs.readFileSync(filePath, 'utf8'); +function parseEnvContent(content: string): Record { const result: Record = {}; // Split by newlines and process each line @@ -96,6 +95,16 @@ function parseEnvFile(filePath: string): Record { return result; } +/** + * Parses an environment file and returns its contents as key-value pairs + * @param filePath Path to the environment file + * @returns Object containing environment variables + */ +function parseEnvFile(filePath: string): Record { + const content = fs.readFileSync(filePath, 'utf8'); + return parseEnvContent(content); +} + /** * Loads environment variables from a .env.mcp file * @param filePath Optional custom path to the env file @@ -124,6 +133,46 @@ export function loadEnvironmentVariablesFromFile(filePath?: string): boolean { } } +/** + * Reads environment variables from stdin + * @returns Promise that resolves to true if environment variables were loaded, false otherwise + */ +export function loadEnvironmentVariablesFromStdin(): Promise { + return new Promise((resolve) => { + let stdinContent = ''; + + process.stdin.setEncoding('utf8'); + + process.stdin.on('readable', () => { + let chunk; + while ((chunk = process.stdin.read()) !== null) { + stdinContent += chunk; + } + }); + + process.stdin.on('end', () => { + try { + const envVars = parseEnvContent(stdinContent); + + // Add variables to process.env + Object.entries(envVars).forEach(([key, value]) => { + process.env[key] = value; + }); + + resolve(true); + } catch (error) { + reportError(`Error parsing environment variables from stdin:`, error); + resolve(false); + } + }); + + process.stdin.on('error', (error) => { + reportError(`Error reading from stdin:`, error); + resolve(false); + }); + }); +} + /** * Executes a command with the given arguments * @param command The command to execute