From b68ca1bebd11050e376195c39eed2d26ad6df5a4 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Tue, 26 May 2026 22:16:15 -0400 Subject: [PATCH] Secure MCP endpoint for LAN clients --- .env.example | 3 ++- README.md | 27 +++++++++++++++++++++------ scripts/install-service.sh | 2 +- src/config.js | 9 ++++++++- src/http.js | 6 +++++- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index f356069..9834161 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ -# The service binds to localhost only. Set a token if your harness supports bearer auth. +# Keep localhost for same-Mac clients. For LAN clients, set this to the Mac's LAN IP. MACMINI_MCP_HOST=127.0.0.1 MACMINI_MCP_PORT=7331 +# Required whenever MACMINI_MCP_HOST is not loopback. # MACMINI_MCP_TOKEN=replace-with-a-long-random-token diff --git a/README.md b/README.md index 9646621..57fe97e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ npm run check npm run service:install ``` -The service endpoint is: +The service defaults to a same-Mac endpoint: ```text http://127.0.0.1:7331/mcp @@ -71,6 +71,21 @@ Service logs are stored in `.logs/`. For a harness that supports Streamable HTTP, configure the local MCP URL as `http://127.0.0.1:7331/mcp`. +For a trusted local-network harness such as a Raspberry Pi, set +`MACMINI_MCP_HOST` in `.env` to the Mac's LAN IP and set a strong +`MACMINI_MCP_TOKEN`. Then configure the remote MCP client with: + +```text +URL: http://:7331/mcp +Authorization: Bearer +``` + +Restart after changing `.env`: + +```sh +npm run service:restart +``` + For a harness that launches stdio servers, use: ```json @@ -90,11 +105,11 @@ On first use of a Notes, Calendar, or Reminders tool, macOS may ask for Automation access for Node. Permit only the applications you want the server to control under **System Settings > Privacy & Security > Automation**. -The HTTP service binds to `127.0.0.1` by default. To require bearer -authentication even locally, create `.env` from `.env.example` and set -`MACMINI_MCP_TOKEN`; clients must then send `Authorization: Bearer `. -Do not bind to a non-loopback address without enabling authentication and -reviewing the tools first. +The HTTP service binds to `127.0.0.1` by default. When configured to bind to a +LAN address, it refuses to start without `MACMINI_MCP_TOKEN`; clients must send +`Authorization: Bearer `. This is HTTP bearer authentication on your +local network, not encrypted transport. Use it only on a trusted LAN or put it +behind a private encrypted network such as a VPN. ## Development diff --git a/scripts/install-service.sh b/scripts/install-service.sh index 5db0992..479f132 100755 --- a/scripts/install-service.sh +++ b/scripts/install-service.sh @@ -47,5 +47,5 @@ launchctl enable "$DOMAIN/$LABEL" launchctl kickstart -k "$DOMAIN/$LABEL" echo "Installed $LABEL" -echo "Endpoint: http://127.0.0.1:7331/mcp" +echo "Endpoint: configured by .env (see the service log for the listening URL)" echo "Logs: $LOG_DIR" diff --git a/src/config.js b/src/config.js index 547dbaa..4bd1f83 100644 --- a/src/config.js +++ b/src/config.js @@ -14,9 +14,16 @@ export function getConfig() { throw new Error("MACMINI_MCP_PORT must be an integer between 1 and 65535."); } - return { + const config = { host: process.env.MACMINI_MCP_HOST || "127.0.0.1", port, token: process.env.MACMINI_MCP_TOKEN || "", }; + + const isLoopback = ["127.0.0.1", "::1", "localhost"].includes(config.host); + if (!isLoopback && !config.token) { + throw new Error("MACMINI_MCP_TOKEN is required when listening beyond localhost."); + } + + return config; } diff --git a/src/http.js b/src/http.js index 1b8057c..2706ffe 100644 --- a/src/http.js +++ b/src/http.js @@ -1,4 +1,5 @@ import http from "node:http"; +import { timingSafeEqual } from "node:crypto"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { getConfig } from "./config.js"; import { createMacMiniMcpServer } from "./server.js"; @@ -9,7 +10,10 @@ function authorized(request) { if (!config.token) { return true; } - return request.headers.authorization === `Bearer ${config.token}`; + const authorization = request.headers.authorization || ""; + const expected = Buffer.from(`Bearer ${config.token}`); + const provided = Buffer.from(authorization); + return provided.length === expected.length && timingSafeEqual(provided, expected); } const service = http.createServer(async (request, response) => {