Secure MCP endpoint for LAN clients

This commit is contained in:
Adolfo Reyna
2026-05-26 22:16:15 -04:00
parent e2384654fd
commit b68ca1bebd
5 changed files with 37 additions and 10 deletions
+2 -1
View File
@@ -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_HOST=127.0.0.1
MACMINI_MCP_PORT=7331 MACMINI_MCP_PORT=7331
# Required whenever MACMINI_MCP_HOST is not loopback.
# MACMINI_MCP_TOKEN=replace-with-a-long-random-token # MACMINI_MCP_TOKEN=replace-with-a-long-random-token
+21 -6
View File
@@ -35,7 +35,7 @@ npm run check
npm run service:install npm run service:install
``` ```
The service endpoint is: The service defaults to a same-Mac endpoint:
```text ```text
http://127.0.0.1:7331/mcp 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 For a harness that supports Streamable HTTP, configure the local MCP URL as
`http://127.0.0.1:7331/mcp`. `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://<mac-lan-ip>:7331/mcp
Authorization: Bearer <MACMINI_MCP_TOKEN>
```
Restart after changing `.env`:
```sh
npm run service:restart
```
For a harness that launches stdio servers, use: For a harness that launches stdio servers, use:
```json ```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 Automation access for Node. Permit only the applications you want the server
to control under **System Settings > Privacy & Security > Automation**. to control under **System Settings > Privacy & Security > Automation**.
The HTTP service binds to `127.0.0.1` by default. To require bearer The HTTP service binds to `127.0.0.1` by default. When configured to bind to a
authentication even locally, create `.env` from `.env.example` and set LAN address, it refuses to start without `MACMINI_MCP_TOKEN`; clients must send
`MACMINI_MCP_TOKEN`; clients must then send `Authorization: Bearer <token>`. `Authorization: Bearer <token>`. This is HTTP bearer authentication on your
Do not bind to a non-loopback address without enabling authentication and local network, not encrypted transport. Use it only on a trusted LAN or put it
reviewing the tools first. behind a private encrypted network such as a VPN.
## Development ## Development
+1 -1
View File
@@ -47,5 +47,5 @@ launchctl enable "$DOMAIN/$LABEL"
launchctl kickstart -k "$DOMAIN/$LABEL" launchctl kickstart -k "$DOMAIN/$LABEL"
echo "Installed $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" echo "Logs: $LOG_DIR"
+8 -1
View File
@@ -14,9 +14,16 @@ export function getConfig() {
throw new Error("MACMINI_MCP_PORT must be an integer between 1 and 65535."); 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", host: process.env.MACMINI_MCP_HOST || "127.0.0.1",
port, port,
token: process.env.MACMINI_MCP_TOKEN || "", 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;
} }
+5 -1
View File
@@ -1,4 +1,5 @@
import http from "node:http"; import http from "node:http";
import { timingSafeEqual } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { getConfig } from "./config.js"; import { getConfig } from "./config.js";
import { createMacMiniMcpServer } from "./server.js"; import { createMacMiniMcpServer } from "./server.js";
@@ -9,7 +10,10 @@ function authorized(request) {
if (!config.token) { if (!config.token) {
return true; 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) => { const service = http.createServer(async (request, response) => {