feat: add Streamable HTTP transport support
Add support for running the MCP server over HTTP with Server-Sent Events (SSE) using the MCP Streamable HTTP specification, alongside the existing STDIO transport. New features: - Transport abstraction with Transport interface - HTTP transport with session management - SSE support for server-initiated notifications - CORS security with configurable allowed origins - Optional TLS support - CLI flags for HTTP configuration (--transport, --http-address, etc.) - NixOS module options for HTTP transport The HTTP transport implements: - POST /mcp: JSON-RPC requests with session management - GET /mcp: SSE stream for server notifications - DELETE /mcp: Session termination - Origin validation (localhost-only by default) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,13 +89,56 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
http = {
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1:8080";
|
||||
description = "HTTP listen address for the MCP server.";
|
||||
};
|
||||
|
||||
endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/mcp";
|
||||
description = "HTTP endpoint path for MCP requests.";
|
||||
};
|
||||
|
||||
allowedOrigins = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "http://localhost:3000" "https://example.com" ];
|
||||
description = ''
|
||||
Allowed Origin headers for CORS.
|
||||
Empty list means only localhost origins are allowed.
|
||||
'';
|
||||
};
|
||||
|
||||
sessionTTL = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "30m";
|
||||
description = "Session TTL for HTTP transport (Go duration format).";
|
||||
};
|
||||
|
||||
tls = {
|
||||
enable = lib.mkEnableOption "TLS for HTTP transport";
|
||||
|
||||
certFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to TLS certificate file.";
|
||||
};
|
||||
|
||||
keyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to TLS private key file.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to open the firewall for the MCP server.
|
||||
Note: MCP typically runs over stdio, so this is usually not needed.
|
||||
'';
|
||||
description = "Whether to open the firewall for the MCP HTTP server.";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,6 +154,10 @@ in
|
||||
assertion = cfg.database.connectionString == "" || cfg.database.connectionStringFile == null;
|
||||
message = "services.nixos-options-mcp.database: connectionString and connectionStringFile are mutually exclusive";
|
||||
}
|
||||
{
|
||||
assertion = !cfg.http.tls.enable || (cfg.http.tls.certFile != null && cfg.http.tls.keyFile != null);
|
||||
message = "services.nixos-options-mcp.http.tls: both certFile and keyFile must be set when TLS is enabled";
|
||||
}
|
||||
];
|
||||
|
||||
users.users.${cfg.user} = lib.mkIf (cfg.user == "nixos-options-mcp") {
|
||||
@@ -145,6 +192,19 @@ in
|
||||
nixos-options index "${rev}" || true
|
||||
'') cfg.indexOnStart}
|
||||
'';
|
||||
|
||||
# Build HTTP transport flags
|
||||
httpFlags = lib.concatStringsSep " " ([
|
||||
"--transport http"
|
||||
"--http-address '${cfg.http.address}'"
|
||||
"--http-endpoint '${cfg.http.endpoint}'"
|
||||
"--session-ttl '${cfg.http.sessionTTL}'"
|
||||
] ++ lib.optionals (cfg.http.allowedOrigins != []) (
|
||||
map (origin: "--allowed-origins '${origin}'") cfg.http.allowedOrigins
|
||||
) ++ lib.optionals cfg.http.tls.enable [
|
||||
"--tls-cert '${cfg.http.tls.certFile}'"
|
||||
"--tls-key '${cfg.http.tls.keyFile}'"
|
||||
]);
|
||||
in
|
||||
if useConnectionStringFile then ''
|
||||
# Read database connection string from file
|
||||
@@ -155,10 +215,10 @@ in
|
||||
export NIXOS_OPTIONS_DATABASE="$(cat "${cfg.database.connectionStringFile}")"
|
||||
|
||||
${indexCommands}
|
||||
exec nixos-options serve
|
||||
exec nixos-options serve ${httpFlags}
|
||||
'' else ''
|
||||
${indexCommands}
|
||||
exec nixos-options serve
|
||||
exec nixos-options serve ${httpFlags}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
@@ -188,5 +248,14 @@ in
|
||||
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/nixos-options-mcp") "nixos-options-mcp";
|
||||
};
|
||||
};
|
||||
|
||||
# Open firewall for HTTP port if configured
|
||||
networking.firewall = lib.mkIf cfg.openFirewall (let
|
||||
# Extract port from address (format: "host:port" or ":port")
|
||||
addressParts = lib.splitString ":" cfg.http.address;
|
||||
port = lib.toInt (lib.last addressParts);
|
||||
in {
|
||||
allowedTCPPorts = [ port ];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user