Implement .env configuration
This commit is contained in:
+1
-1
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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/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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user