mr-magic-mcp-server 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +43 -1
- package/package.json +1 -1
- package/src/transport/http-server.js +38 -2
- package/src/transport/mcp-http-server.js +23 -4
package/.env.example
CHANGED
|
@@ -118,6 +118,7 @@ MR_MAGIC_TOOL_ARG_CHUNK_SIZE=400 # Chunk size in chars (only used when LOG_TOOL
|
|
|
118
118
|
|
|
119
119
|
# Streamable HTTP MCP transport diagnostics
|
|
120
120
|
MR_MAGIC_MCP_HTTP_DIAGNOSTICS=0 # Set to 1 to log enriched request diagnostics at transport ingress
|
|
121
|
+
MR_MAGIC_ALLOWED_HOSTS= # Optional. Comma-separated extra allowed hostnames for DNS rebinding protection when binding to 0.0.0.0 (e.g. custom domains). RENDER_EXTERNAL_HOSTNAME is auto-included on Render.
|
|
121
122
|
|
|
122
123
|
# SDK repro harness verbose HTTP logging
|
|
123
124
|
MR_MAGIC_SDK_REPRO_HTTP_DEBUG=0 # Set to 1 for verbose HTTP previews in the SDK repro harness script
|
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ MR_MAGIC_HTTP_TIMEOUT_MS=10000 # Optional, default 10000. Global outbound HTTP t
|
|
|
99
99
|
MR_MAGIC_LOG_TOOL_ARGS_CHUNKS=0 # Optional, default 0. Set to 1/true to emit chunk-by-chunk MCP tool argument previews for truncation debugging.
|
|
100
100
|
MR_MAGIC_TOOL_ARG_CHUNK_SIZE=400 # Optional, default 400. Chunk size used when MR_MAGIC_LOG_TOOL_ARGS_CHUNKS is enabled.
|
|
101
101
|
MR_MAGIC_MCP_HTTP_DIAGNOSTICS=0 # Optional, default 0. Set to 1 to log enriched Streamable HTTP MCP request diagnostics at transport ingress.
|
|
102
|
+
MR_MAGIC_ALLOWED_HOSTS= # Optional. Comma-separated extra hostnames for DNS rebinding protection when binding to 0.0.0.0 (e.g. custom domains). Render hostname is auto-included via RENDER_EXTERNAL_HOSTNAME.
|
|
102
103
|
MR_MAGIC_SDK_REPRO_HTTP_DEBUG=0 # Optional, default 0. Set to 1 for verbose HTTP request/response previews in the SDK repro harness script.
|
|
103
104
|
UPSTASH_REDIS_REST_URL= # Get from https://console.upstash.com/redis/rest, required if MR_MAGIC_EXPORT_BACKEND=redis
|
|
104
105
|
UPSTASH_REDIS_REST_TOKEN= # Get from https://console.upstash.com/redis/rest, required if MR_MAGIC_EXPORT_BACKEND=redis
|
|
@@ -147,7 +148,9 @@ AIRTABLE_PERSONAL_ACCESS_TOKEN= # Required for push_catalog_to_airtable tool. Ge
|
|
|
147
148
|
- **PORT** overrides both HTTP entrypoints when your platform injects one
|
|
148
149
|
(Render, Fly, etc.). If unset, the MCP HTTP transport binds to `3444` and
|
|
149
150
|
the JSON HTTP automation server binds to `3333`. CLI flags such as
|
|
150
|
-
`mrmagic-cli server --port 4000` always take precedence.
|
|
151
|
+
`mrmagic-cli server --port 4000` always take precedence. On Render,
|
|
152
|
+
`PORT` is set automatically by the platform (default `10000`) — no manual
|
|
153
|
+
override is needed.
|
|
151
154
|
- **MR_MAGIC_DOWNLOAD_BASE_URL** should match the public URL that exposes the
|
|
152
155
|
`/downloads` routes. Include `:port` only when the HTTP server isn’t using
|
|
153
156
|
the default for its protocol.
|
|
@@ -170,6 +173,11 @@ AIRTABLE_PERSONAL_ACCESS_TOKEN= # Required for push_catalog_to_airtable tool. Ge
|
|
|
170
173
|
- **MR_MAGIC_MCP_HTTP_DIAGNOSTICS** (default `0`) enables detailed request
|
|
171
174
|
metadata logging at the Streamable HTTP transport boundary (method, content
|
|
172
175
|
type, body shape/length, session header, and safe body preview).
|
|
176
|
+
- **MR_MAGIC_ALLOWED_HOSTS** — comma-separated list of extra hostnames to allow
|
|
177
|
+
when the MCP HTTP server binds to `0.0.0.0` (required by the MCP SDK for DNS
|
|
178
|
+
rebinding protection). On Render, `RENDER_EXTERNAL_HOSTNAME` is automatically
|
|
179
|
+
included; set `MR_MAGIC_ALLOWED_HOSTS` only when you have a custom domain or
|
|
180
|
+
need to add additional hosts beyond `localhost`/`127.0.0.1`.
|
|
173
181
|
- **MR_MAGIC_SDK_REPRO_HTTP_DEBUG** (default `0`) enables HTTP-level debugging
|
|
174
182
|
output in `scripts/mcp-arg-boundary-sdk-repro.mjs` when validating argument
|
|
175
183
|
boundary behavior from the SDK client path.
|
|
@@ -316,6 +324,40 @@ npm run server:http # or server:mcp / server:mcp:http
|
|
|
316
324
|
Use a process manager (systemd, PM2, Docker CMD, etc.) to keep long-lived
|
|
317
325
|
servers running.
|
|
318
326
|
|
|
327
|
+
### Deploying on Render
|
|
328
|
+
|
|
329
|
+
The MCP HTTP server (`npm run server:mcp:http`) is ready to deploy on Render
|
|
330
|
+
with no extra configuration. Render automatically sets two environment
|
|
331
|
+
variables that the server reads at startup:
|
|
332
|
+
|
|
333
|
+
| Variable | Set by Render | Value |
|
|
334
|
+
|----------|--------------|-------|
|
|
335
|
+
| `RENDER` | ✅ always | `"true"` |
|
|
336
|
+
| `PORT` | ✅ always | `10000` (default, overridable in dashboard) |
|
|
337
|
+
|
|
338
|
+
When `RENDER=true` is detected, the server automatically binds to `0.0.0.0`
|
|
339
|
+
(required by Render's load balancer) on the `PORT` Render assigns. You do
|
|
340
|
+
**not** need to set `HOST`, `PORT`, or any other network variable in your
|
|
341
|
+
Render service settings.
|
|
342
|
+
|
|
343
|
+
Recommended Render service settings:
|
|
344
|
+
|
|
345
|
+
- **Start Command:** `npm run server:mcp:http`
|
|
346
|
+
- **Environment:** inject your provider credentials (`GENIUS_CLIENT_ID`,
|
|
347
|
+
`GENIUS_CLIENT_SECRET`, `MUSIXMATCH_FALLBACK_TOKEN`, etc.) via the Render
|
|
348
|
+
Dashboard → Environment tab
|
|
349
|
+
- **Health Check Path:** `/health` (returns `{ "status": "ok", "providers": [...] }`)
|
|
350
|
+
|
|
351
|
+
> **Note:** Render's default `PORT` is `10000`. If you set a custom `PORT`
|
|
352
|
+
> in the Render Dashboard the server will honour it; otherwise `10000` is used.
|
|
353
|
+
> The old `3444` default is only active when running outside Render without a
|
|
354
|
+
> `PORT` env var.
|
|
355
|
+
|
|
356
|
+
> **Custom domains:** If you have a custom domain on Render (e.g.,
|
|
357
|
+
> `mymcp.example.com`), add it to `MR_MAGIC_ALLOWED_HOSTS` in the Render
|
|
358
|
+
> Dashboard → Environment tab so the SDK's DNS rebinding protection accepts
|
|
359
|
+
> requests with that `Host` header.
|
|
360
|
+
|
|
319
361
|
- **MCP server (Stdio)** for local Model Context Protocol clients (use the
|
|
320
362
|
bundled CLI: `npm run server:mcp` or call `node ./src/bin/mcp-server.js`).
|
|
321
363
|
- **MCP server (Streamable HTTP)** for remote MCP clients that speak the Streamable HTTP
|
package/package.json
CHANGED
|
@@ -32,11 +32,47 @@ async function handleAction(action, track, actionOptions) {
|
|
|
32
32
|
|
|
33
33
|
export function startHttpServer(options = {}) {
|
|
34
34
|
const logger = createLogger('http-server');
|
|
35
|
-
const host = options.remote
|
|
36
|
-
|
|
35
|
+
const host = options.remote
|
|
36
|
+
? '0.0.0.0'
|
|
37
|
+
: (options.host || process.env.HOST || (process.env.RENDER ? '0.0.0.0' : '127.0.0.1'));
|
|
38
|
+
const port = Number(options.port) || Number(process.env.PORT) || 3333;
|
|
39
|
+
|
|
40
|
+
// When binding to 0.0.0.0, build an allowed-host set for DNS rebinding protection.
|
|
41
|
+
// Render sets RENDER_EXTERNAL_HOSTNAME automatically; add custom domains via
|
|
42
|
+
// MR_MAGIC_ALLOWED_HOSTS (comma-separated).
|
|
43
|
+
const allowedHosts =
|
|
44
|
+
host === '0.0.0.0'
|
|
45
|
+
? new Set([
|
|
46
|
+
'localhost',
|
|
47
|
+
'127.0.0.1',
|
|
48
|
+
...(process.env.RENDER_EXTERNAL_HOSTNAME
|
|
49
|
+
? [process.env.RENDER_EXTERNAL_HOSTNAME]
|
|
50
|
+
: []),
|
|
51
|
+
...(process.env.MR_MAGIC_ALLOWED_HOSTS
|
|
52
|
+
? process.env.MR_MAGIC_ALLOWED_HOSTS.split(',')
|
|
53
|
+
.map((h) => h.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
: [])
|
|
56
|
+
])
|
|
57
|
+
: null;
|
|
37
58
|
|
|
38
59
|
return new Promise((resolve) => {
|
|
39
60
|
const server = http.createServer(async (req, res) => {
|
|
61
|
+
// DNS rebinding protection: validate Host header when binding to all interfaces.
|
|
62
|
+
if (allowedHosts) {
|
|
63
|
+
const reqHostname = (req.headers.host || '').split(':')[0].toLowerCase();
|
|
64
|
+
if (!reqHostname || !allowedHosts.has(reqHostname)) {
|
|
65
|
+
logger.warn('DNS rebinding protection: rejected request with disallowed Host', {
|
|
66
|
+
host: req.headers.host,
|
|
67
|
+
url: req.url,
|
|
68
|
+
method: req.method
|
|
69
|
+
});
|
|
70
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify({ error: 'Forbidden: Host header not allowed' }));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
if (req.method === 'GET' && req.url === '/health') {
|
|
41
77
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
78
|
res.end(
|
|
@@ -64,8 +64,10 @@ export async function startMcpHttpServer(options = {}) {
|
|
|
64
64
|
const logger = createLogger('mcp-http-server');
|
|
65
65
|
const httpDiagnostics = process.env.MR_MAGIC_MCP_HTTP_DIAGNOSTICS === '1';
|
|
66
66
|
const configuredSessionless = Boolean(options.sessionless);
|
|
67
|
-
const host = options.remote
|
|
68
|
-
|
|
67
|
+
const host = options.remote
|
|
68
|
+
? '0.0.0.0'
|
|
69
|
+
: (options.host || process.env.HOST || (process.env.RENDER ? '0.0.0.0' : '127.0.0.1'));
|
|
70
|
+
const port = Number(options.port) || Number(process.env.PORT) || 3444;
|
|
69
71
|
|
|
70
72
|
const server = new Server(
|
|
71
73
|
{ name: 'mcp-http-server', version: '0.1.4' },
|
|
@@ -91,7 +93,24 @@ export async function startMcpHttpServer(options = {}) {
|
|
|
91
93
|
await server.connect(transport);
|
|
92
94
|
await logTokenStatus({ context: 'http-mcp' });
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
// When binding to 0.0.0.0 the SDK requires an explicit allowedHosts list for
|
|
97
|
+
// DNS rebinding protection. Build it from well-known safe hosts plus any
|
|
98
|
+
// platform-injected hostname (Render sets RENDER_EXTERNAL_HOSTNAME automatically).
|
|
99
|
+
const allowedHosts =
|
|
100
|
+
host === '0.0.0.0'
|
|
101
|
+
? [
|
|
102
|
+
'localhost',
|
|
103
|
+
'127.0.0.1',
|
|
104
|
+
...(process.env.RENDER_EXTERNAL_HOSTNAME
|
|
105
|
+
? [process.env.RENDER_EXTERNAL_HOSTNAME]
|
|
106
|
+
: []),
|
|
107
|
+
...(process.env.MR_MAGIC_ALLOWED_HOSTS
|
|
108
|
+
? process.env.MR_MAGIC_ALLOWED_HOSTS.split(',').map((h) => h.trim()).filter(Boolean)
|
|
109
|
+
: [])
|
|
110
|
+
]
|
|
111
|
+
: undefined;
|
|
112
|
+
|
|
113
|
+
const app = createMcpExpressApp({ host, ...(allowedHosts ? { allowedHosts } : {}) });
|
|
95
114
|
app.get('/health', async (_req, res) => {
|
|
96
115
|
res.json({ status: 'ok', providers: await getProviderStatus() });
|
|
97
116
|
});
|
|
@@ -199,7 +218,7 @@ export async function startMcpHttpServer(options = {}) {
|
|
|
199
218
|
});
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
if (process.argv[1]?.endsWith('mcp-http-server.js')) {
|
|
221
|
+
if (process.argv[1]?.endsWith('transport/mcp-http-server.js')) {
|
|
203
222
|
startMcpHttpServer().catch((error) => {
|
|
204
223
|
console.error(error);
|
|
205
224
|
process.exit(1);
|