Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
71 changes: 71 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
});
38 changes: 33 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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 <path>] <command> [args...]');
console.error('Usage: envmcp [--env-file <path>] [--env-stdin] <command> [args...]');
console.error('\nOptions:');
console.error(' -e, --env-file <path> Specify a custom path to the environment file');
console.error(' --env-stdin Read environment variables from stdin');
console.error('\nArguments:');
console.error(' <command> 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;

Expand All @@ -27,13 +30,31 @@ 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;
break;
}
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;
Expand All @@ -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);
}

Expand Down
57 changes: 53 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
const content = fs.readFileSync(filePath, 'utf8');
function parseEnvContent(content: string): Record<string, string> {
const result: Record<string, string> = {};

// Split by newlines and process each line
Expand Down Expand Up @@ -96,6 +95,16 @@ function parseEnvFile(filePath: string): Record<string, string> {
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<string, string> {
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
Expand Down Expand Up @@ -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<boolean> {
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
Expand Down