Implement .env configuration
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ VOIPMS_API_USERNAME=
|
||||
|
||||
VOIPMS_API_PASSWORD=
|
||||
|
||||
DEFAULT_TIMEZONE=-4
|
||||
VOIPMS_TIMEZONE=-4
|
||||
|
||||
# The default "from" number to be used for sending texts, placing calls, etc
|
||||
MY_DEFAULT_DID=
|
||||
|
||||
@@ -44,6 +44,15 @@ export VOIPMS_API_USERNAME='you@example.com'
|
||||
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:
|
||||
|
||||
```bash
|
||||
@@ -51,6 +60,7 @@ Credentials can also be passed directly:
|
||||
```
|
||||
|
||||
The endpoint can be overridden with `VOIPMS_API_ENDPOINT` or `--endpoint`.
|
||||
CDR timezone defaults can be set with `VOIPMS_TIMEZONE`.
|
||||
|
||||
## CLI Overview
|
||||
|
||||
@@ -261,9 +271,8 @@ be run behind a trusted network, firewall, VPN, or tunnel.
|
||||
Start it with:
|
||||
|
||||
```bash
|
||||
export VOIPMS_API_USERNAME='you@example.com'
|
||||
export VOIPMS_API_PASSWORD='your-api-password'
|
||||
export MCP_AUTH_TOKEN='choose-a-long-random-token'
|
||||
cp .env-example .env
|
||||
# Edit .env and optionally add MCP_AUTH_TOKEN='choose-a-long-random-token'
|
||||
|
||||
./bin/serve-mcp.sh
|
||||
```
|
||||
|
||||
@@ -5,6 +5,48 @@ HOST="${MCP_HOST:-0.0.0.0}"
|
||||
PORT="${MCP_PORT:-8787}"
|
||||
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
|
||||
MCP_AUTH_TOKEN="$(php -r 'echo bin2hex(random_bytes(24));')"
|
||||
export MCP_AUTH_TOKEN
|
||||
|
||||
+5
-3
@@ -9,13 +9,15 @@ so use a firewall, VPN, or private network when exposing it beyond this machine.
|
||||
## Start The Server
|
||||
|
||||
```bash
|
||||
export VOIPMS_API_USERNAME='you@example.com'
|
||||
export VOIPMS_API_PASSWORD='your-api-password'
|
||||
export MCP_AUTH_TOKEN='choose-a-long-random-token'
|
||||
cp .env-example .env
|
||||
# Edit .env and optionally add MCP_AUTH_TOKEN='choose-a-long-random-token'
|
||||
|
||||
./bin/serve-mcp.sh
|
||||
```
|
||||
|
||||
The launcher reads the project-root `.env` file when present. Already exported
|
||||
environment variables take precedence over `.env` values.
|
||||
|
||||
Defaults:
|
||||
|
||||
```text
|
||||
|
||||
+5
-1
@@ -3,10 +3,14 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../src/VoipMsClient.php';
|
||||
require_once __DIR__ . '/../src/EnvLoader.php';
|
||||
|
||||
use VoipMs\EnvLoader;
|
||||
use VoipMs\VoipMsClient;
|
||||
use VoipMs\VoipMsResponse;
|
||||
|
||||
EnvLoader::load(__DIR__ . '/../.env');
|
||||
|
||||
const MCP_PROTOCOL_VERSION = '2025-06-18';
|
||||
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(
|
||||
requiredStringArg($arguments, 'date_from'),
|
||||
requiredStringArg($arguments, 'date_to'),
|
||||
stringArg($arguments, 'timezone', getenv('VOIPMS_TIMEZONE') ?: '-4'),
|
||||
stringArg($arguments, 'timezone', getenv('VOIPMS_TIMEZONE') ?: getenv('DEFAULT_TIMEZONE') ?: '-4'),
|
||||
cdrFilters($arguments),
|
||||
)),
|
||||
'voipms_add_number_to_phonebook_group' => addNumberToPhonebookGroupTool($client, $arguments),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -5,9 +5,13 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/src/VoipMsResponse.php';
|
||||
require_once __DIR__ . '/src/VoipMsClient.php';
|
||||
require_once __DIR__ . '/src/EnvLoader.php';
|
||||
|
||||
use VoipMs\EnvLoader;
|
||||
use VoipMs\VoipMsClient;
|
||||
|
||||
EnvLoader::load(__DIR__ . '/.env');
|
||||
|
||||
/**
|
||||
* @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];
|
||||
}
|
||||
|
||||
$envTimezone = getenv('VOIPMS_TIMEZONE');
|
||||
$envTimezone = getenv('VOIPMS_TIMEZONE') ?: getenv('DEFAULT_TIMEZONE');
|
||||
if (!isset($params['timezone']) && is_string($envTimezone) && $envTimezone !== '') {
|
||||
$params['timezone'] = $envTimezone;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user