Secure MCP endpoint for LAN clients
This commit is contained in:
+2
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user