Implement .env configuration

This commit is contained in:
Jason Thistlethwaite
2026-04-27 22:52:27 +00:00
parent 16feb51b12
commit 324084d419
8 changed files with 259 additions and 9 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ VOIPMS_API_USERNAME=
VOIPMS_API_PASSWORD= VOIPMS_API_PASSWORD=
DEFAULT_TIMEZONE=-4 VOIPMS_TIMEZONE=-4
# The default "from" number to be used for sending texts, placing calls, etc # The default "from" number to be used for sending texts, placing calls, etc
MY_DEFAULT_DID= MY_DEFAULT_DID=
+12 -3
View File
@@ -44,6 +44,15 @@ export VOIPMS_API_USERNAME='you@example.com'
export VOIPMS_API_PASSWORD='your-api-password' export VOIPMS_API_PASSWORD='your-api-password'
``` ```
It also loads a project-root `.env` file when present:
```bash
cp .env-example .env
```
Values already exported in the process environment, and values passed with CLI
options, take precedence over `.env` values.
Credentials can also be passed directly: Credentials can also be passed directly:
```bash ```bash
@@ -51,6 +60,7 @@ Credentials can also be passed directly:
``` ```
The endpoint can be overridden with `VOIPMS_API_ENDPOINT` or `--endpoint`. The endpoint can be overridden with `VOIPMS_API_ENDPOINT` or `--endpoint`.
CDR timezone defaults can be set with `VOIPMS_TIMEZONE`.
## CLI Overview ## CLI Overview
@@ -261,9 +271,8 @@ be run behind a trusted network, firewall, VPN, or tunnel.
Start it with: Start it with:
```bash ```bash
export VOIPMS_API_USERNAME='you@example.com' cp .env-example .env
export VOIPMS_API_PASSWORD='your-api-password' # Edit .env and optionally add MCP_AUTH_TOKEN='choose-a-long-random-token'
export MCP_AUTH_TOKEN='choose-a-long-random-token'
./bin/serve-mcp.sh ./bin/serve-mcp.sh
``` ```
+42
View File
@@ -5,6 +5,48 @@ HOST="${MCP_HOST:-0.0.0.0}"
PORT="${MCP_PORT:-8787}" PORT="${MCP_PORT:-8787}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
load_env_file() {
local env_file="$1"
local line key value first last comment_start
[[ -r "${env_file}" ]] || return 0
while IFS= read -r line || [[ -n "${line}" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "${line}" || "${line}" == \#* || "${line}" != *=* ]] && continue
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
[[ -v "${key}" ]] && continue
first="${value:0:1}"
last="${value: -1}"
if [[ "${#value}" -ge 2 && ( ( "${first}" == "'" && "${last}" == "'" ) || ( "${first}" == '"' && "${last}" == '"' ) ) ]]; then
value="${value:1:${#value}-2}"
else
comment_start="${value%%" #"*}"
if [[ "${comment_start}" != "${value}" ]]; then
value="${comment_start}"
value="${value%"${value##*[![:space:]]}"}"
fi
fi
export "${key}=${value}"
done < "${env_file}"
}
load_env_file "${ROOT}/.env"
HOST="${MCP_HOST:-${HOST}}"
PORT="${MCP_PORT:-${PORT}}"
if [[ "${MCP_ALLOW_UNAUTHENTICATED:-}" != "1" && -z "${MCP_AUTH_TOKEN:-}" ]]; then if [[ "${MCP_ALLOW_UNAUTHENTICATED:-}" != "1" && -z "${MCP_AUTH_TOKEN:-}" ]]; then
MCP_AUTH_TOKEN="$(php -r 'echo bin2hex(random_bytes(24));')" MCP_AUTH_TOKEN="$(php -r 'echo bin2hex(random_bytes(24));')"
export MCP_AUTH_TOKEN export MCP_AUTH_TOKEN
+5 -3
View File
@@ -9,13 +9,15 @@ so use a firewall, VPN, or private network when exposing it beyond this machine.
## Start The Server ## Start The Server
```bash ```bash
export VOIPMS_API_USERNAME='you@example.com' cp .env-example .env
export VOIPMS_API_PASSWORD='your-api-password' # Edit .env and optionally add MCP_AUTH_TOKEN='choose-a-long-random-token'
export MCP_AUTH_TOKEN='choose-a-long-random-token'
./bin/serve-mcp.sh ./bin/serve-mcp.sh
``` ```
The launcher reads the project-root `.env` file when present. Already exported
environment variables take precedence over `.env` values.
Defaults: Defaults:
```text ```text
+5 -1
View File
@@ -3,10 +3,14 @@
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/../src/VoipMsClient.php'; require_once __DIR__ . '/../src/VoipMsClient.php';
require_once __DIR__ . '/../src/EnvLoader.php';
use VoipMs\EnvLoader;
use VoipMs\VoipMsClient; use VoipMs\VoipMsClient;
use VoipMs\VoipMsResponse; use VoipMs\VoipMsResponse;
EnvLoader::load(__DIR__ . '/../.env');
const MCP_PROTOCOL_VERSION = '2025-06-18'; const MCP_PROTOCOL_VERSION = '2025-06-18';
const MCP_SESSION_ID = 'voipms-test-session'; const MCP_SESSION_ID = 'voipms-test-session';
@@ -229,7 +233,7 @@ function callTool(string $name, array $arguments): array
'voipms_get_recent_cdr' => responsePayload($client->getCdr( 'voipms_get_recent_cdr' => responsePayload($client->getCdr(
requiredStringArg($arguments, 'date_from'), requiredStringArg($arguments, 'date_from'),
requiredStringArg($arguments, 'date_to'), requiredStringArg($arguments, 'date_to'),
stringArg($arguments, 'timezone', getenv('VOIPMS_TIMEZONE') ?: '-4'), stringArg($arguments, 'timezone', getenv('VOIPMS_TIMEZONE') ?: getenv('DEFAULT_TIMEZONE') ?: '-4'),
cdrFilters($arguments), cdrFilters($arguments),
)), )),
'voipms_add_number_to_phonebook_group' => addNumberToPhonebookGroupTool($client, $arguments), 'voipms_add_number_to_phonebook_group' => addNumberToPhonebookGroupTool($client, $arguments),
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace VoipMs;
final class EnvLoader
{
public static function load(string $path): void
{
if (!is_file($path) || !is_readable($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
[$key, $value] = self::parseLine($line);
if ($key === '' || self::isSet($key)) {
continue;
}
putenv("{$key}={$value}");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
/**
* @return array{0: string, 1: string}
*/
private static function parseLine(string $line): array
{
if (!str_contains($line, '=')) {
return ['', ''];
}
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $key)) {
return ['', ''];
}
return [$key, self::normalizeValue(trim($value))];
}
private static function normalizeValue(string $value): string
{
if ($value === '') {
return '';
}
$first = $value[0];
$last = $value[strlen($value) - 1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) {
$value = substr($value, 1, -1);
return $first === '"' ? stripcslashes($value) : $value;
}
$commentStart = strpos($value, ' #');
if ($commentStart !== false) {
$value = substr($value, 0, $commentStart);
}
return rtrim($value);
}
private static function isSet(string $key): bool
{
return getenv($key) !== false
|| array_key_exists($key, $_ENV)
|| array_key_exists($key, $_SERVER);
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/EnvLoader.php';
use VoipMs\EnvLoader;
/**
* @param callable(): void $test
*/
function runTest(string $name, callable $test): void
{
try {
$test();
echo "PASS {$name}\n";
} catch (Throwable $exception) {
fwrite(STDERR, "FAIL {$name}: {$exception->getMessage()}\n");
exit(1);
}
}
function assertSameValue(mixed $expected, mixed $actual, string $message): void
{
if ($expected !== $actual) {
throw new RuntimeException($message . ' Expected ' . var_export($expected, true) . ', got ' . var_export($actual, true) . '.');
}
}
function clearEnvKey(string $key): void
{
putenv($key);
unset($_ENV[$key], $_SERVER[$key]);
}
/**
* @param list<string> $lines
*/
function withTempEnvFile(array $lines): string
{
$path = tempnam(sys_get_temp_dir(), 'voipms-env-');
if ($path === false) {
throw new RuntimeException('Unable to create temp env file.');
}
file_put_contents($path, implode("\n", $lines) . "\n");
return $path;
}
runTest('loads simple and quoted .env values', function (): void {
foreach (['VOIPMS_TEST_SIMPLE', 'VOIPMS_TEST_SINGLE', 'VOIPMS_TEST_DOUBLE', 'VOIPMS_TEST_EMPTY'] as $key) {
clearEnvKey($key);
}
$path = withTempEnvFile([
'# comment',
'',
'VOIPMS_TEST_SIMPLE=value',
"VOIPMS_TEST_SINGLE='single quoted value'",
'VOIPMS_TEST_DOUBLE="double quoted value"',
'VOIPMS_TEST_EMPTY=',
]);
try {
EnvLoader::load($path);
assertSameValue('value', getenv('VOIPMS_TEST_SIMPLE'), 'Simple value should be available through getenv().');
assertSameValue('single quoted value', $_ENV['VOIPMS_TEST_SINGLE'] ?? null, 'Single quoted value should be available through $_ENV.');
assertSameValue('double quoted value', $_SERVER['VOIPMS_TEST_DOUBLE'] ?? null, 'Double quoted value should be available through $_SERVER.');
assertSameValue('', getenv('VOIPMS_TEST_EMPTY'), 'Empty value should be loaded.');
} finally {
unlink($path);
foreach (['VOIPMS_TEST_SIMPLE', 'VOIPMS_TEST_SINGLE', 'VOIPMS_TEST_DOUBLE', 'VOIPMS_TEST_EMPTY'] as $key) {
clearEnvKey($key);
}
}
});
runTest('does not override existing process environment', function (): void {
clearEnvKey('VOIPMS_TEST_OVERRIDE');
putenv('VOIPMS_TEST_OVERRIDE=from-process');
$_ENV['VOIPMS_TEST_OVERRIDE'] = 'from-process';
$_SERVER['VOIPMS_TEST_OVERRIDE'] = 'from-process';
$path = withTempEnvFile([
'VOIPMS_TEST_OVERRIDE=from-file',
]);
try {
EnvLoader::load($path);
assertSameValue('from-process', getenv('VOIPMS_TEST_OVERRIDE'), 'Existing getenv() value should win.');
assertSameValue('from-process', $_ENV['VOIPMS_TEST_OVERRIDE'] ?? null, 'Existing $_ENV value should win.');
assertSameValue('from-process', $_SERVER['VOIPMS_TEST_OVERRIDE'] ?? null, 'Existing $_SERVER value should win.');
} finally {
unlink($path);
clearEnvKey('VOIPMS_TEST_OVERRIDE');
}
});
runTest('ignores missing .env files', function (): void {
EnvLoader::load(sys_get_temp_dir() . '/voipms-missing-env-file');
});
+5 -1
View File
@@ -5,9 +5,13 @@ declare(strict_types=1);
require_once __DIR__ . '/src/VoipMsResponse.php'; require_once __DIR__ . '/src/VoipMsResponse.php';
require_once __DIR__ . '/src/VoipMsClient.php'; require_once __DIR__ . '/src/VoipMsClient.php';
require_once __DIR__ . '/src/EnvLoader.php';
use VoipMs\EnvLoader;
use VoipMs\VoipMsClient; use VoipMs\VoipMsClient;
EnvLoader::load(__DIR__ . '/.env');
/** /**
* @return array<string, array{category: string, description: string, params?: list<string>, examples?: list<string>}> * @return array<string, array{category: string, description: string, params?: list<string>, examples?: list<string>}>
*/ */
@@ -624,7 +628,7 @@ function parseCdrArguments(array $args): array
$params['date_to'] = $positionals[1]; $params['date_to'] = $positionals[1];
} }
$envTimezone = getenv('VOIPMS_TIMEZONE'); $envTimezone = getenv('VOIPMS_TIMEZONE') ?: getenv('DEFAULT_TIMEZONE');
if (!isset($params['timezone']) && is_string($envTimezone) && $envTimezone !== '') { if (!isset($params['timezone']) && is_string($envTimezone) && $envTimezone !== '') {
$params['timezone'] = $envTimezone; $params['timezone'] = $envTimezone;
} }