@ultimatelemon-eu/mcp-http 2.3.1 → 2.4.0
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/README.md +10 -1
- package/dist/oauth.js +2 -2
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ process.once('SIGTERM', () => {
|
|
|
42
42
|
| `host` | `'0.0.0.0'` | `'127.0.0.1'` keeps it local |
|
|
43
43
|
| `basePath` | `''` | Also serve MCP on `<basePath>` and `<basePath>/mcp`, and `<basePath>/healthz` |
|
|
44
44
|
| `health` | always ok | Return `false` for a 503 on `/healthz` |
|
|
45
|
-
| `apiKey` | off | Require the caller's key; `hint` is added to the 401
|
|
45
|
+
| `apiKey` | off | Require the caller's key; `hint` is added to the 401, `verify` checks it |
|
|
46
46
|
| `oauth` | off | Let callers sign in with the upstream service; see [OAuth](#oauth) |
|
|
47
47
|
| `logger` | silent | Anything with `error(obj, msg)`, such as pino |
|
|
48
48
|
|
|
@@ -57,6 +57,15 @@ before building an MCP server. The key reaches `createMcpServer` as
|
|
|
57
57
|
`apiKey`; it is never logged or stored. Other per-caller settings can travel
|
|
58
58
|
as headers of their own, read from `headers` in the same context.
|
|
59
59
|
|
|
60
|
+
When the server hands out keys itself, `verify` checks one before any MCP
|
|
61
|
+
work: return `false` and the caller gets a 401 with `error="invalid_token"`.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
apiKey: {
|
|
65
|
+
verify: (key) => findUserByToken(key) !== undefined;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
60
69
|
This suits public servers where each user brings their own credentials: no
|
|
61
70
|
sessions, no database. In claude.ai, add the connector with
|
|
62
71
|
"No sign-in" and the key as a request header.
|
package/dist/oauth.js
CHANGED
|
@@ -9,7 +9,7 @@ const UPSTREAM_LEEWAY_MS = 5 * 60 * 1000;
|
|
|
9
9
|
const COOKIE = 'mcp_oauth_state';
|
|
10
10
|
function sealer(issuer) {
|
|
11
11
|
return {
|
|
12
|
-
seal(value, ttlMs) {
|
|
12
|
+
seal: (value, ttlMs) => {
|
|
13
13
|
const sealed = {
|
|
14
14
|
...value,
|
|
15
15
|
i: issuer,
|
|
@@ -17,7 +17,7 @@ function sealer(issuer) {
|
|
|
17
17
|
};
|
|
18
18
|
return encrypt(JSON.stringify(sealed));
|
|
19
19
|
},
|
|
20
|
-
open(value, type) {
|
|
20
|
+
open: (value, type) => {
|
|
21
21
|
if (!value || !isEncrypted(value))
|
|
22
22
|
return undefined;
|
|
23
23
|
try {
|
package/dist/server.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface RequestContext {
|
|
|
17
17
|
export interface ApiKeyOptions {
|
|
18
18
|
/** Added to the 401 message, e.g. where to get a key. */
|
|
19
19
|
hint?: string;
|
|
20
|
+
/** For keys this server issued itself: return false to answer 401 before building a server. */
|
|
21
|
+
verify?: (key: string) => boolean | Promise<boolean>;
|
|
20
22
|
}
|
|
21
23
|
export interface McpHttpOptions {
|
|
22
24
|
/** Builds a fresh server per request; the transport is stateless. */
|
package/dist/server.js
CHANGED
|
@@ -175,14 +175,19 @@ export async function startMcpHttpServer(options) {
|
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
const apiKey = options.apiKey ? (headerKey(req) ?? bearer) : undefined;
|
|
178
|
+
const hint = options.apiKey?.hint ? ` ${options.apiKey.hint}` : '';
|
|
178
179
|
if (!apiKey) {
|
|
179
|
-
const hint = options.apiKey?.hint ? ` ${options.apiKey.hint}` : '';
|
|
180
180
|
res.setHeader('www-authenticate', oauth ? oauth.challenge() : 'Bearer');
|
|
181
181
|
jsonRpcError(res, 401, options.apiKey
|
|
182
182
|
? `Missing API key: send it in the X-Api-Key header or as Authorization: Bearer <key>.${hint}`
|
|
183
183
|
: 'Sign in to use this server.');
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
|
+
if (options.apiKey?.verify && !(await options.apiKey.verify(apiKey))) {
|
|
187
|
+
res.setHeader('www-authenticate', 'Bearer error="invalid_token"');
|
|
188
|
+
jsonRpcError(res, 401, `The API key is not valid.${hint}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
186
191
|
await nodeHandler(Object.assign(req, {
|
|
187
192
|
auth: { token: apiKey, clientId: 'api-key', scopes: [] }
|
|
188
193
|
}), res);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimatelemon-eu/mcp-http",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Run an MCP server over stateless Streamable HTTP: /mcp, /healthz, a base path for proxies, the caller's own API key or a stateless OAuth sign-in, and errors that never leak a stack trace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|