Harden server; add authenticated Streamable HTTP transport and Nix module
- Streamable HTTP transport (MCP_TRANSPORT=http) with mandatory auth: static bearer tokens or OIDC resource-server mode (JWT via JWKS, or RFC 7662 introspection), RFC 9728 protected-resource metadata, and DNS-rebinding protection - secrets loadable from files via *_FILE variants - timeouts on all outbound requests; JORTT_READ_ONLY tool filtering - OpenAPI spec committed and bundled; no implicit network fetches - Nix flake: sandboxed package build + hardened NixOS module taking an EnvironmentFile (agenix-friendly) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
240a53fade
commit
a73af353d4
17 changed files with 1255 additions and 77 deletions
50
src/config.ts
Normal file
50
src/config.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
|
||||
/**
|
||||
* Read a configuration value from the environment, with `<NAME>_FILE`
|
||||
* indirection: if `<NAME>_FILE` is set, the value is read from that file
|
||||
* (trailing whitespace stripped). This keeps secrets out of process
|
||||
* environments and lets systemd credentials / agenix-managed files be used
|
||||
* directly.
|
||||
*/
|
||||
export function envOrFile(name: string): string | undefined {
|
||||
const direct = process.env[name];
|
||||
const file = process.env[`${name}_FILE`];
|
||||
if (direct && file) {
|
||||
throw new Error(`Both ${name} and ${name}_FILE are set; use only one.`);
|
||||
}
|
||||
if (file) {
|
||||
try {
|
||||
return readFileSync(file, "utf8").replace(/\s+$/, "");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Could not read ${name}_FILE (${file}): ${message}`);
|
||||
}
|
||||
}
|
||||
return direct || undefined;
|
||||
}
|
||||
|
||||
/** Like envOrFile, but exits with a helpful message when unset. */
|
||||
export function requireEnvOrFile(name: string, hint: string): string {
|
||||
const value = envOrFile(name);
|
||||
if (!value) {
|
||||
console.error(`[jortt-mcp] Missing required configuration: ${name}\n${hint}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function envFlag(name: string): boolean {
|
||||
const value = process.env[name]?.toLowerCase();
|
||||
return value === "1" || value === "true" || value === "yes";
|
||||
}
|
||||
|
||||
/** Parse a comma- or whitespace-separated env list. */
|
||||
export function envList(name: string): string[] {
|
||||
const value = process.env[name];
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(/[,\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue