raidcom-mcp 0.1.2 → 0.1.3
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/CHANGELOG.md +7 -0
- package/INSTALL.md +1 -1
- package/README.md +2 -0
- package/dist/config.js +10 -0
- package/dist/http.js +21 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.3 — 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`RAIDCOM_HTTP_CLIENT_IP_HEADER`** reads the client's address from a header a trusted proxy sets, such as `cf-connecting-ip` behind Cloudflare. On Render, which sits behind Cloudflare, counting `X-Forwarded-For` entries gave a Cloudflare address, so every user shared the rate limit and the in-progress cap of their Cloudflare edge.
|
|
10
|
+
- **`RAIDCOM_HTTP_DEBUG_CLIENT=true`** makes `/healthz` show the forwarding headers it received, to choose the proxy settings.
|
|
11
|
+
|
|
5
12
|
## 0.1.2 — 2026-10-27
|
|
6
13
|
|
|
7
14
|
0.1.0 and 0.1.1 were withdrawn from npm and can't be published again there; this is the first version on npm since. 0.1.1's GitHub release and Docker image stay available.
|
package/INSTALL.md
CHANGED
|
@@ -145,7 +145,7 @@ Which settings are required depends on where it listens:
|
|
|
145
145
|
- **Loopback only** (the default `127.0.0.1`): a token, or `RAIDCOM_HTTP_OPEN=true`.
|
|
146
146
|
- **Reachable by other machines**: also TLS. Either give the server a certificate (`RAIDCOM_HTTP_TLS_CERT`, `RAIDCOM_HTTP_TLS_KEY`) or put it behind a reverse proxy or load balancer that ends TLS and set `RAIDCOM_HTTP_BEHIND_TLS_PROXY=true`.
|
|
147
147
|
- **A public name**: list it in `RAIDCOM_HTTP_ALLOWED_HOSTS` (for example `raidcom.example.com`), or requests are refused with 403 to defend against DNS rebinding.
|
|
148
|
-
- **Behind a proxy**: set `RAIDCOM_HTTP_TRUST_PROXY` to the number of proxies in front of the server (`true` means one), so the rate limits apply per user. Each proxy adds an entry to `X-Forwarded-For`; the server takes the entry that many places from the right, and ignores entries further left, which the client could have written. Too low a number makes every user look like the same proxy (they then share one rate limit); too high lets a client choose its own address. To check the number, open `/healthz`: behind proxies it shows `client`, which should be your own public address (compare with `curl https://ifconfig.me`). Hosting platforms often have more than one proxy
|
|
148
|
+
- **Behind a proxy**: set `RAIDCOM_HTTP_TRUST_PROXY` to the number of proxies in front of the server (`true` means one), so the rate limits apply per user. Each proxy adds an entry to `X-Forwarded-For`; the server takes the entry that many places from the right, and ignores entries further left, which the client could have written. Too low a number makes every user look like the same proxy (they then share one rate limit); too high lets a client choose its own address. To check the number, open `/healthz`: behind proxies it shows `client`, which should be your own public address (compare with `curl https://ifconfig.me`). Hosting platforms often have more than one proxy, and some put a CDN in front whose own proxies vary in number. If the address `/healthz` shows never matches yours, set `RAIDCOM_HTTP_DEBUG_CLIENT=true` and open `/healthz` again: it lists the forwarding headers it received. Behind Cloudflare (Render, for example) use `RAIDCOM_HTTP_CLIENT_IP_HEADER=cf-connecting-ip` instead of counting proxies; Cloudflare overwrites that header on every request, so a client can't set it. Turn the debug setting off again afterwards.
|
|
149
149
|
|
|
150
150
|
A public service that anyone may use can run with `RAIDCOM_HTTP_OPEN=true`: the server holds no secrets and cannot reach any array, so the risk is limited to load, which the rate limit and the 256 KB body limit bound.
|
|
151
151
|
|
package/README.md
CHANGED
|
@@ -98,6 +98,8 @@ The endpoint is `/mcp` (stateless Streamable HTTP); `/healthz` reports liveness.
|
|
|
98
98
|
| `RAIDCOM_HTTP_ALLOWED_HOSTS` | none | Extra `Host` values to accept (`*` for any) |
|
|
99
99
|
| `RAIDCOM_HTTP_ALLOWED_ORIGINS` | none | `Origin` values to accept (`*` for any) |
|
|
100
100
|
| `RAIDCOM_HTTP_RATE_LIMIT` | `120` | Requests per minute per client (`0` for none) |
|
|
101
|
+
| `RAIDCOM_HTTP_CLIENT_IP_HEADER` | none | A header your proxy sets to the client's address, e.g. `cf-connecting-ip` behind Cloudflare (Render uses Cloudflare). Takes precedence over `X-Forwarded-For` |
|
|
102
|
+
| `RAIDCOM_HTTP_DEBUG_CLIENT` | `false` | `/healthz` also shows the forwarding headers it received, to choose the two settings above |
|
|
101
103
|
| `RAIDCOM_HTTP_TRUST_PROXY` | `false` | Take the client address from `X-Forwarded-For`: `true` for one proxy in front, or the number of proxies (see [INSTALL.md](INSTALL.md#run-over-http)) |
|
|
102
104
|
| `RAIDCOM_CATALOG`, `RAIDCOM_LIMITS`, `RAIDCOM_RECIPES`, `RAIDCOM_EXAMPLES` | bundled | Use another spec file (an empty `RAIDCOM_EXAMPLES` turns the example questions off) |
|
|
103
105
|
|
package/dist/config.js
CHANGED
|
@@ -30,6 +30,14 @@ export function loadServerConfig(env = process.env) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
const MIN_TOKEN_LENGTH = 32;
|
|
33
|
+
/** RAIDCOM_HTTP_CLIENT_IP_HEADER: a header name, lower-cased. */
|
|
34
|
+
function clientHeader(v) {
|
|
35
|
+
if (!v)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (!/^[A-Za-z0-9-]{1,64}$/.test(v))
|
|
38
|
+
throw new Error(`[raidcom-mcp] RAIDCOM_HTTP_CLIENT_IP_HEADER must be a header name such as cf-connecting-ip, not "${v}"`);
|
|
39
|
+
return v.toLowerCase();
|
|
40
|
+
}
|
|
33
41
|
/** RAIDCOM_HTTP_TRUST_PROXY: unset or false → 0, true → 1, or a number of proxies from 1 to 10. */
|
|
34
42
|
function proxyHops(v) {
|
|
35
43
|
if (v === undefined || v === "" || v === "false" || v === "0")
|
|
@@ -114,5 +122,7 @@ export function loadHttpConfig(env = process.env) {
|
|
|
114
122
|
behindTlsProxy,
|
|
115
123
|
rateLimitPerMinute: Math.floor(rate),
|
|
116
124
|
trustProxy: proxyHops(env.RAIDCOM_HTTP_TRUST_PROXY),
|
|
125
|
+
clientIpHeader: clientHeader(env.RAIDCOM_HTTP_CLIENT_IP_HEADER),
|
|
126
|
+
debugClient: env.RAIDCOM_HTTP_DEBUG_CLIENT === "true",
|
|
117
127
|
};
|
|
118
128
|
}
|
package/dist/http.js
CHANGED
|
@@ -96,7 +96,14 @@ export class RateLimiter {
|
|
|
96
96
|
* proxy added. Entries further left come from the client and can say anything. (Same rule as Express's
|
|
97
97
|
* `trust proxy` set to a number.)
|
|
98
98
|
*/
|
|
99
|
-
export function clientAddress(req, hops) {
|
|
99
|
+
export function clientAddress(req, hops, header) {
|
|
100
|
+
// a header the proxy sets to the client's address, such as cf-connecting-ip; used only if it holds an IP address
|
|
101
|
+
if (header) {
|
|
102
|
+
const raw = req.headers[header];
|
|
103
|
+
const v = (Array.isArray(raw) ? raw[0] : raw)?.trim();
|
|
104
|
+
if (v && /^[0-9A-Fa-f:.]{2,45}$/.test(v))
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
100
107
|
if (hops > 0) {
|
|
101
108
|
const xff = req.headers["x-forwarded-for"];
|
|
102
109
|
const entries = (Array.isArray(xff) ? xff.join(",") : (xff ?? "")).split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -116,7 +123,7 @@ export async function startHttp(cfg, createServer) {
|
|
|
116
123
|
let active = 0;
|
|
117
124
|
const perClient = new Map();
|
|
118
125
|
const refuse = (req, res, status, message, headers) => {
|
|
119
|
-
console.error(`[raidcom-mcp] refused ${req.method} ${req.url} from ${clientAddress(req, cfg.trustProxy)}: ${status} ${message}`);
|
|
126
|
+
console.error(`[raidcom-mcp] refused ${req.method} ${req.url} from ${clientAddress(req, cfg.trustProxy, cfg.clientIpHeader)}: ${status} ${message}`);
|
|
120
127
|
reject(res, status, message, headers);
|
|
121
128
|
};
|
|
122
129
|
const handler = async (req, res) => {
|
|
@@ -125,7 +132,17 @@ export async function startHttp(cfg, createServer) {
|
|
|
125
132
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
126
133
|
// Behind proxies, the health check also shows the address the server takes as the caller's, so the number of
|
|
127
134
|
// proxies (RAIDCOM_HTTP_TRUST_PROXY) can be checked: it should be your own public address.
|
|
128
|
-
const
|
|
135
|
+
const proxied = cfg.trustProxy > 0 || !!cfg.clientIpHeader;
|
|
136
|
+
const seen = (h) => (Array.isArray(req.headers[h]) ? req.headers[h].join(", ") : req.headers[h]);
|
|
137
|
+
const body = {
|
|
138
|
+
status: "ok",
|
|
139
|
+
version: VERSION,
|
|
140
|
+
...(proxied ? { client: clientAddress(req, cfg.trustProxy, cfg.clientIpHeader) } : {}),
|
|
141
|
+
// RAIDCOM_HTTP_DEBUG_CLIENT=true: what the proxies sent, to choose RAIDCOM_HTTP_TRUST_PROXY or _CLIENT_IP_HEADER
|
|
142
|
+
...(cfg.debugClient
|
|
143
|
+
? { forwarding: { socket: req.socket.remoteAddress, "x-forwarded-for": seen("x-forwarded-for"), "cf-connecting-ip": seen("cf-connecting-ip"), "true-client-ip": seen("true-client-ip"), "x-real-ip": seen("x-real-ip") } }
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
129
146
|
return res.end(req.method === "HEAD" ? undefined : JSON.stringify(body));
|
|
130
147
|
}
|
|
131
148
|
if (path !== MCP_PATH)
|
|
@@ -135,7 +152,7 @@ export async function startHttp(cfg, createServer) {
|
|
|
135
152
|
const origin = req.headers.origin;
|
|
136
153
|
if (origin !== undefined && !anyOrigin && !origins.has(origin.toLowerCase()))
|
|
137
154
|
return refuse(req, res, 403, "Origin not allowed. Add it to RAIDCOM_HTTP_ALLOWED_ORIGINS if this page should reach the server.");
|
|
138
|
-
const client = clientAddress(req, cfg.trustProxy);
|
|
155
|
+
const client = clientAddress(req, cfg.trustProxy, cfg.clientIpHeader);
|
|
139
156
|
if (!limiter.allow(client))
|
|
140
157
|
return refuse(req, res, 429, "Too many requests. Try again in a minute.", { "retry-after": "60" });
|
|
141
158
|
if (token && !authorized(req.headers.authorization, token))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raidcom-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"mcpName": "io.github.visubramaniam/raidcom-mcp",
|
|
5
5
|
"description": "MCP server that builds and checks Hitachi raidcom commands from structured requests. Build-only: it never connects to a storage system. Not an official Hitachi Vantara product.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"node": ">=22.19.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
32
|
+
"@modelcontextprotocol/sdk": "1.32.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/node": "22.
|
|
35
|
+
"@types/node": "22.20.5",
|
|
36
36
|
"typescript": "7.0.2"
|
|
37
37
|
},
|
|
38
38
|
"author": "Vinod Subramaniam",
|