@fruggr/zendesk-mcp-server 3.3.0 → 3.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/dist/THIRD-PARTY-NOTICES.md +8 -8
- package/dist/{http-GIKJdf05.js → http-DU1dSrPe.js} +382 -42
- package/dist/index.js +3233 -598
- package/dist/sbom.cdx.json +22 -22
- package/dist/{token-persistence-BeetFqPF.js → token-persistence-r0v9RO3q.js} +3 -3
- package/dist/{token-store-Aq7gP39o.js → token-store-DyO12sbo.js} +2 -2
- package/package.json +8 -8
|
@@ -30,9 +30,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
30
30
|
SOFTWARE.
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
## @modelcontextprotocol/core@2.
|
|
33
|
+
## @modelcontextprotocol/core@2.3.1
|
|
34
34
|
|
|
35
|
-
License:
|
|
35
|
+
License: Apache-2.0
|
|
36
36
|
|
|
37
37
|
```
|
|
38
38
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
|
@@ -253,9 +253,9 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
|
|
253
253
|
the full license text.
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
## @modelcontextprotocol/node@2.1.
|
|
256
|
+
## @modelcontextprotocol/node@2.1.1
|
|
257
257
|
|
|
258
|
-
License:
|
|
258
|
+
License: Apache-2.0
|
|
259
259
|
|
|
260
260
|
```
|
|
261
261
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
|
@@ -476,9 +476,9 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
|
|
476
476
|
the full license text.
|
|
477
477
|
```
|
|
478
478
|
|
|
479
|
-
## @modelcontextprotocol/server@2.
|
|
479
|
+
## @modelcontextprotocol/server@2.3.1
|
|
480
480
|
|
|
481
|
-
License:
|
|
481
|
+
License: Apache-2.0
|
|
482
482
|
|
|
483
483
|
```
|
|
484
484
|
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
|
@@ -2629,7 +2629,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
|
2629
2629
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2630
2630
|
```
|
|
2631
2631
|
|
|
2632
|
-
## mdast-util-find-and-replace@3.0.
|
|
2632
|
+
## mdast-util-find-and-replace@3.0.3
|
|
2633
2633
|
|
|
2634
2634
|
License: MIT
|
|
2635
2635
|
|
|
@@ -5198,7 +5198,7 @@ The above copyright notice and this permission notice shall be included in all c
|
|
|
5198
5198
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
5199
5199
|
```
|
|
5200
5200
|
|
|
5201
|
-
## zod@4.6.
|
|
5201
|
+
## zod@4.6.6
|
|
5202
5202
|
|
|
5203
5203
|
License: MIT
|
|
5204
5204
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { d as escapeHtml, l as requestedScope, n as configDir, o as generateCodeChallenge, s as generateCodeVerifier, u as supportedScopes } from "./token-persistence-
|
|
2
|
+
import { C as __exportAll, S as __commonJSMin, T as __toESM, _ as OAuthErrorCode, a as silentLogger, b as errnoCode, c as deadlineSignal, d as DEFAULT_MAX_REQUEST_BODY_SIZE, f as WebStandardStreamableHTTPServerTransport, g as OAuthError, h as verifyBearerToken, m as isLegacyRequest, n as defaultServerFactory, o as zendeskGet, p as createMcpHandler, r as createRequestStateCodec, s as REQUEST_TIMEOUT_MS, t as createServerCaches, u as getOAuthUrls, v as STARTUP_DOCS, w as __require, y as createStartupError } from "./index.js";
|
|
3
|
+
import { d as escapeHtml, l as requestedScope, n as configDir, o as generateCodeChallenge, s as generateCodeVerifier, u as supportedScopes } from "./token-persistence-r0v9RO3q.js";
|
|
4
4
|
import * as util$1 from "node:util";
|
|
5
5
|
import { format, inspect } from "node:util";
|
|
6
6
|
import * as crypto$2 from "node:crypto";
|
|
@@ -16,7 +16,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
16
16
|
import { accessSync, chmodSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { Http2ServerRequest, constants as constants$1 } from "http2";
|
|
18
18
|
import crypto$1 from "crypto";
|
|
19
|
-
//#region node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.
|
|
19
|
+
//#region node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.13/node_modules/@hono/node-server/dist/index.mjs
|
|
20
20
|
var RequestError = class extends Error {
|
|
21
21
|
constructor(message, options) {
|
|
22
22
|
super(message, options);
|
|
@@ -504,7 +504,7 @@ var getRequestListener = (fetchCallback, options = {}) => {
|
|
|
504
504
|
};
|
|
505
505
|
};
|
|
506
506
|
//#endregion
|
|
507
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.
|
|
507
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.1_@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/node/dist/index.mjs
|
|
508
508
|
/**
|
|
509
509
|
* Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
|
|
510
510
|
* It supports both SSE streaming and direct HTTP responses.
|
|
@@ -543,8 +543,12 @@ var getRequestListener = (fetchCallback, options = {}) => {
|
|
|
543
543
|
*
|
|
544
544
|
* @example Using with a pre-parsed request body (e.g. Express)
|
|
545
545
|
* ```ts source="./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_express"
|
|
546
|
-
* app.post('/mcp', (req, res) => {
|
|
547
|
-
* transport
|
|
546
|
+
* app.post('/mcp', async (req, res) => {
|
|
547
|
+
* // Stateless example: create a server and a transport per request.
|
|
548
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
549
|
+
* const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
550
|
+
* await server.connect(transport);
|
|
551
|
+
* await transport.handleRequest(req, res, req.body);
|
|
548
552
|
* });
|
|
549
553
|
* ```
|
|
550
554
|
*/
|
|
@@ -663,6 +667,185 @@ var NodeStreamableHTTPServerTransport = class {
|
|
|
663
667
|
this._webStandardTransport.closeStandaloneSSEStream();
|
|
664
668
|
}
|
|
665
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* The rejection {@linkcode toWebRequest} produces for a body over the size
|
|
672
|
+
* limit, recognisable by `name` and `status` without matching message text.
|
|
673
|
+
*/
|
|
674
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
675
|
+
status = 413;
|
|
676
|
+
constructor(maxBytes) {
|
|
677
|
+
super(`Payload Too Large: Request body must not exceed ${maxBytes} bytes`);
|
|
678
|
+
this.name = "RequestBodyTooLargeError";
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
function resolveMaxRequestBodySize(value) {
|
|
682
|
+
if (value === void 0) return DEFAULT_MAX_REQUEST_BODY_SIZE;
|
|
683
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new RangeError(`maxRequestBodySize must be a positive number of bytes, got ${String(value)}`);
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Adapts a web-standard MCP handler (`handler.fetch`) to a Node.js
|
|
688
|
+
* `(req, res, parsedBody?)` request handler. The returned function converts the
|
|
689
|
+
* Node request to a web-standard `Request`, calls `handler.fetch`, then writes
|
|
690
|
+
* the `Response` back to `res` (honoring write backpressure for streamed SSE
|
|
691
|
+
* responses).
|
|
692
|
+
*
|
|
693
|
+
* `req.auth` is forwarded as the handler's pass-through `authInfo`. A function
|
|
694
|
+
* third argument (Express's `next`) is ignored, never treated as a body.
|
|
695
|
+
*
|
|
696
|
+
* Pass `{ onerror }` to observe the adapter-level error fallback (request
|
|
697
|
+
* conversion / `handler.fetch` throw) before the `500` response is written.
|
|
698
|
+
*/
|
|
699
|
+
function toNodeHandler(handler, opts) {
|
|
700
|
+
const maxRequestBodySize = resolveMaxRequestBodySize(opts?.maxRequestBodySize);
|
|
701
|
+
return async (req, res, parsedBody) => {
|
|
702
|
+
if (typeof parsedBody === "function") parsedBody = void 0;
|
|
703
|
+
let finished = false;
|
|
704
|
+
const abort = new AbortController();
|
|
705
|
+
res.on("close", () => {
|
|
706
|
+
if (!finished) abort.abort();
|
|
707
|
+
});
|
|
708
|
+
if (res.destroyed === true) abort.abort();
|
|
709
|
+
let response;
|
|
710
|
+
try {
|
|
711
|
+
const request = await toWebRequest(req, parsedBody, {
|
|
712
|
+
signal: abort.signal,
|
|
713
|
+
maxRequestBodySize
|
|
714
|
+
});
|
|
715
|
+
response = await handler.fetch(request, {
|
|
716
|
+
...req.auth !== void 0 && { authInfo: req.auth },
|
|
717
|
+
...parsedBody !== void 0 && { parsedBody }
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error instanceof RequestBodyTooLargeError) response = Response.json({
|
|
721
|
+
jsonrpc: "2.0",
|
|
722
|
+
error: {
|
|
723
|
+
code: -32e3,
|
|
724
|
+
message: error.message
|
|
725
|
+
},
|
|
726
|
+
id: null
|
|
727
|
+
}, {
|
|
728
|
+
status: 413,
|
|
729
|
+
headers: { connection: "close" }
|
|
730
|
+
});
|
|
731
|
+
else {
|
|
732
|
+
try {
|
|
733
|
+
opts?.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
734
|
+
} catch {}
|
|
735
|
+
response = internalServerErrorResponse(echoableRequestId(parsedBody));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const headers = {};
|
|
739
|
+
for (const [name, value] of response.headers) headers[name] = value;
|
|
740
|
+
res.writeHead(response.status, headers);
|
|
741
|
+
if (response.body === null) {
|
|
742
|
+
finished = true;
|
|
743
|
+
res.end();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
let drainResolve;
|
|
747
|
+
const releaseDrainWait = () => {
|
|
748
|
+
drainResolve?.();
|
|
749
|
+
drainResolve = void 0;
|
|
750
|
+
};
|
|
751
|
+
res.on("drain", releaseDrainWait);
|
|
752
|
+
const closed = new Promise((resolve) => {
|
|
753
|
+
abort.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
754
|
+
});
|
|
755
|
+
try {
|
|
756
|
+
for await (const chunk of response.body) {
|
|
757
|
+
if (abort.signal.aborted) break;
|
|
758
|
+
if (res.write(chunk) === false) await Promise.race([new Promise((resolve) => {
|
|
759
|
+
drainResolve = resolve;
|
|
760
|
+
}), closed]);
|
|
761
|
+
}
|
|
762
|
+
} catch {}
|
|
763
|
+
finished = true;
|
|
764
|
+
res.end();
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function singleHeaderValue(value) {
|
|
768
|
+
return Array.isArray(value) ? value[0] : value;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Convert a Node.js `IncomingMessage` (duck-typed — an Express `req` works) to
|
|
772
|
+
* the web-standard `Request` that `handler.fetch()` and `isLegacyRequest()`
|
|
773
|
+
* take. This is the conversion {@linkcode toNodeHandler} performs internally,
|
|
774
|
+
* exported for hand-wired compositions:
|
|
775
|
+
*
|
|
776
|
+
* ```ts
|
|
777
|
+
* const probe = await toWebRequest(req, req.body);
|
|
778
|
+
* await ((await isLegacyRequest(probe)) ? legacy(req, res) : modern(req, res, req.body));
|
|
779
|
+
* ```
|
|
780
|
+
*
|
|
781
|
+
* With no `parsedBody` the Node stream is read to completion (up to
|
|
782
|
+
* `maxRequestBodySize`, 4 MiB by default — a longer body rejects with an error
|
|
783
|
+
* carrying `name: 'RequestBodyTooLargeError'` and `status: 413`) — read the
|
|
784
|
+
* body from the returned `Request` afterwards, not from `req`. When a body
|
|
785
|
+
* parser already consumed the stream (`express.json()`), pass the parsed value
|
|
786
|
+
* as `parsedBody` and nothing is read from `req`.
|
|
787
|
+
*/
|
|
788
|
+
async function toWebRequest(req, parsedBody, options) {
|
|
789
|
+
const maxRequestBodySize = resolveMaxRequestBodySize(options?.maxRequestBodySize);
|
|
790
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
791
|
+
const url = `http://${singleHeaderValue(req.headers["host"]) ?? singleHeaderValue(req.headers[":authority"]) ?? "localhost"}${req.url ?? "/"}`;
|
|
792
|
+
const headers = new Headers();
|
|
793
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
794
|
+
if (value === void 0 || name.startsWith(":")) continue;
|
|
795
|
+
if (Array.isArray(value)) for (const item of value) headers.append(name, item);
|
|
796
|
+
else headers.set(name, value);
|
|
797
|
+
}
|
|
798
|
+
let body;
|
|
799
|
+
if (method !== "GET" && method !== "HEAD") if (parsedBody === void 0) {
|
|
800
|
+
if (Number(singleHeaderValue(req.headers["content-length"])) > maxRequestBodySize) throw new RequestBodyTooLargeError(maxRequestBodySize);
|
|
801
|
+
const decoder = new TextDecoder();
|
|
802
|
+
let collected = "";
|
|
803
|
+
let received = 0;
|
|
804
|
+
for await (const chunk of req) {
|
|
805
|
+
received += typeof chunk === "string" ? new TextEncoder().encode(chunk).byteLength : chunk.byteLength;
|
|
806
|
+
if (received > maxRequestBodySize) throw new RequestBodyTooLargeError(maxRequestBodySize);
|
|
807
|
+
collected += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
808
|
+
}
|
|
809
|
+
collected += decoder.decode();
|
|
810
|
+
if (collected.length > 0) body = collected;
|
|
811
|
+
} else {
|
|
812
|
+
const serialized = JSON.stringify(parsedBody);
|
|
813
|
+
headers.delete("content-encoding");
|
|
814
|
+
headers.delete("transfer-encoding");
|
|
815
|
+
if (serialized === void 0) headers.delete("content-length");
|
|
816
|
+
else {
|
|
817
|
+
body = serialized;
|
|
818
|
+
headers.set("content-length", String(new TextEncoder().encode(serialized).byteLength));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return new Request(url, {
|
|
822
|
+
method,
|
|
823
|
+
headers,
|
|
824
|
+
...options?.signal !== void 0 && { signal: options.signal },
|
|
825
|
+
...body !== void 0 && { body }
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The JSON-RPC id to echo on an adapter-built error response: the body's `id`
|
|
830
|
+
* when the body is a single JSON-RPC request whose id is a string or number,
|
|
831
|
+
* `null` otherwise.
|
|
832
|
+
*/
|
|
833
|
+
function echoableRequestId(body) {
|
|
834
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
835
|
+
const { method, id } = body;
|
|
836
|
+
if (typeof method !== "string") return null;
|
|
837
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
838
|
+
}
|
|
839
|
+
function internalServerErrorResponse(id) {
|
|
840
|
+
return Response.json({
|
|
841
|
+
jsonrpc: "2.0",
|
|
842
|
+
error: {
|
|
843
|
+
code: -32603,
|
|
844
|
+
message: "Internal server error"
|
|
845
|
+
},
|
|
846
|
+
id
|
|
847
|
+
}, { status: 500 });
|
|
848
|
+
}
|
|
666
849
|
//#endregion
|
|
667
850
|
//#region node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
668
851
|
const encoder$1 = new TextEncoder();
|
|
@@ -2820,7 +3003,8 @@ const deriveKeySet = (master) => {
|
|
|
2820
3003
|
kid: kidFor(master, "at-rest"),
|
|
2821
3004
|
key: createSecretKey(derive(master, "at-rest"))
|
|
2822
3005
|
},
|
|
2823
|
-
cookieKey: derive(master, "cookie").toString("base64url")
|
|
3006
|
+
cookieKey: derive(master, "cookie").toString("base64url"),
|
|
3007
|
+
requestState: derive(master, "request-state")
|
|
2824
3008
|
};
|
|
2825
3009
|
};
|
|
2826
3010
|
const deriveKeyRing = (raw) => {
|
|
@@ -36178,16 +36362,16 @@ const resolveMasterSecret = (input, logger = silentLogger) => {
|
|
|
36178
36362
|
//#endregion
|
|
36179
36363
|
//#region src/auth/server/authorization-server.ts
|
|
36180
36364
|
const decoder = new TextDecoder();
|
|
36181
|
-
const asVerified = (claims, issuer
|
|
36182
|
-
|
|
36183
|
-
if (!(claims.iss === issuer && audiences.includes(resource) && typeof claims.exp === "number" && claims.exp * 1e3 > Date.now() && typeof claims.zd === "string" && typeof claims.gid === "string")) return void 0;
|
|
36365
|
+
const asVerified = (claims, issuer) => {
|
|
36366
|
+
if (!(claims.iss === issuer && typeof claims.exp === "number" && typeof claims.zd === "string" && typeof claims.gid === "string")) return void 0;
|
|
36184
36367
|
return {
|
|
36185
36368
|
zendeskAccessToken: claims.zd,
|
|
36186
36369
|
grantId: claims.gid,
|
|
36187
36370
|
clientId: String(claims.client_id),
|
|
36188
36371
|
subject: String(claims.sub),
|
|
36189
36372
|
canWrite: typeof claims.scope === "string" && claims.scope.split(" ").includes("write"),
|
|
36190
|
-
expiresAt: claims.exp
|
|
36373
|
+
expiresAt: claims.exp,
|
|
36374
|
+
resource: typeof claims.aud === "string" ? claims.aud : void 0
|
|
36191
36375
|
};
|
|
36192
36376
|
};
|
|
36193
36377
|
const createAuthorizationServer = (options) => {
|
|
@@ -36252,11 +36436,12 @@ const createAuthorizationServer = (options) => {
|
|
|
36252
36436
|
const keySet = ring.find((set) => set.accessToken.kid === kid);
|
|
36253
36437
|
if (!keySet) return void 0;
|
|
36254
36438
|
const { plaintext } = await compactDecrypt(bearer, keySet.accessToken.key);
|
|
36255
|
-
return asVerified(JSON.parse(decoder.decode(plaintext)), issuer
|
|
36439
|
+
return asVerified(JSON.parse(decoder.decode(plaintext)), issuer);
|
|
36256
36440
|
};
|
|
36257
36441
|
return {
|
|
36258
36442
|
issuer,
|
|
36259
36443
|
resource,
|
|
36444
|
+
requestStateKey: ring[0].requestState,
|
|
36260
36445
|
protectedResourceMetadata: {
|
|
36261
36446
|
resource,
|
|
36262
36447
|
authorization_servers: [issuer],
|
|
@@ -36322,6 +36507,47 @@ const prepareAuthorizationServer = async (config, logger = silentLogger) => {
|
|
|
36322
36507
|
});
|
|
36323
36508
|
};
|
|
36324
36509
|
//#endregion
|
|
36510
|
+
//#region src/utils/grant-registry.ts
|
|
36511
|
+
const createGrantRegistry = ({ idleTimeoutMs, now = Date.now, onCloseError = () => void 0 }) => {
|
|
36512
|
+
const slots = /* @__PURE__ */ new Map();
|
|
36513
|
+
const drop = async (grantId, slot) => {
|
|
36514
|
+
slots.delete(grantId);
|
|
36515
|
+
try {
|
|
36516
|
+
await slot.entry.close();
|
|
36517
|
+
} catch (error) {
|
|
36518
|
+
onCloseError(grantId, error);
|
|
36519
|
+
}
|
|
36520
|
+
};
|
|
36521
|
+
return {
|
|
36522
|
+
acquire: (grantId, subject, create) => {
|
|
36523
|
+
const existing = slots.get(grantId);
|
|
36524
|
+
if (existing && existing.subject !== subject) return void 0;
|
|
36525
|
+
const slot = existing ?? {
|
|
36526
|
+
subject,
|
|
36527
|
+
entry: create(),
|
|
36528
|
+
lastActivityAt: now()
|
|
36529
|
+
};
|
|
36530
|
+
slot.lastActivityAt = now();
|
|
36531
|
+
slots.set(grantId, slot);
|
|
36532
|
+
return slot.entry;
|
|
36533
|
+
},
|
|
36534
|
+
close: async (grantId, entry) => {
|
|
36535
|
+
const slot = slots.get(grantId);
|
|
36536
|
+
if (slot && (entry === void 0 || slot.entry === entry)) await drop(grantId, slot);
|
|
36537
|
+
},
|
|
36538
|
+
sweep: async () => {
|
|
36539
|
+
const cutoff = now() - idleTimeoutMs;
|
|
36540
|
+
for (const [grantId, slot] of [...slots]) if (slots.get(grantId) === slot && slot.lastActivityAt <= cutoff) await drop(grantId, slot);
|
|
36541
|
+
},
|
|
36542
|
+
closeAll: async () => {
|
|
36543
|
+
await Promise.all([...slots].map(([grantId, slot]) => drop(grantId, slot)));
|
|
36544
|
+
},
|
|
36545
|
+
get size() {
|
|
36546
|
+
return slots.size;
|
|
36547
|
+
}
|
|
36548
|
+
};
|
|
36549
|
+
};
|
|
36550
|
+
//#endregion
|
|
36325
36551
|
//#region src/transports/http.ts
|
|
36326
36552
|
const WILDCARD_HOSTS = /* @__PURE__ */ new Set([
|
|
36327
36553
|
"0.0.0.0",
|
|
@@ -36340,7 +36566,7 @@ const DEFAULT_BROWSER_MCP_CLIENT_ORIGINS = [
|
|
|
36340
36566
|
"https://grok.com"
|
|
36341
36567
|
];
|
|
36342
36568
|
const CORS_ALLOWED_METHODS = "GET, POST, DELETE, OPTIONS";
|
|
36343
|
-
const CORS_ALLOWED_HEADERS = "Authorization, Content-Type, Accept, mcp-session-id, mcp-protocol-version, last-event-id";
|
|
36569
|
+
const CORS_ALLOWED_HEADERS = "Authorization, Content-Type, Accept, mcp-session-id, mcp-protocol-version, last-event-id, mcp-method, mcp-name";
|
|
36344
36570
|
const CORS_EXPOSE_HEADERS = "mcp-session-id";
|
|
36345
36571
|
const CORS_MAX_AGE = "600";
|
|
36346
36572
|
const LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -36414,11 +36640,36 @@ const resolvePublicUrl = (config, logger = silentLogger) => {
|
|
|
36414
36640
|
return `http://${config.host}:${config.port}`;
|
|
36415
36641
|
};
|
|
36416
36642
|
const MISSING_BEARER_MESSAGE = "Missing or invalid access token. Sign in through this server: its OAuth authorization server is named in the protected resource metadata.";
|
|
36417
|
-
const
|
|
36643
|
+
const SPACES = / +/;
|
|
36644
|
+
/**
|
|
36645
|
+
* The access token on this request, or `undefined` when it must be refused.
|
|
36646
|
+
* The SDK parses the header and checks the audience against our `/mcp`
|
|
36647
|
+
* resource (`expectedResource`) and the expiry; decrypting and every other
|
|
36648
|
+
* claim stay with the authorization server.
|
|
36649
|
+
*/
|
|
36650
|
+
const authenticate = async (request, as) => {
|
|
36418
36651
|
const header = request.headers["authorization"];
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36652
|
+
const verifier = { verifyAccessToken: async (bearer) => {
|
|
36653
|
+
const token = await as.verifyAccessToken(bearer);
|
|
36654
|
+
if (!token) throw new OAuthError(OAuthErrorCode.InvalidToken, "Invalid access token");
|
|
36655
|
+
return {
|
|
36656
|
+
token: bearer,
|
|
36657
|
+
clientId: token.clientId,
|
|
36658
|
+
scopes: [],
|
|
36659
|
+
expiresAt: token.expiresAt,
|
|
36660
|
+
...token.resource && URL.canParse(token.resource) && { resource: new URL(token.resource) },
|
|
36661
|
+
extra: { verified: token }
|
|
36662
|
+
};
|
|
36663
|
+
} };
|
|
36664
|
+
try {
|
|
36665
|
+
const normalized = typeof header === "string" ? header.replace(SPACES, " ") : void 0;
|
|
36666
|
+
return (await verifyBearerToken(normalized, {
|
|
36667
|
+
verifier,
|
|
36668
|
+
expectedResource: new URL(as.resource)
|
|
36669
|
+
})).extra?.["verified"];
|
|
36670
|
+
} catch {
|
|
36671
|
+
return;
|
|
36672
|
+
}
|
|
36422
36673
|
};
|
|
36423
36674
|
const PROTECTED_RESOURCE_PATHS = ["/.well-known/oauth-protected-resource/mcp", "/.well-known/oauth-protected-resource"];
|
|
36424
36675
|
const sendJson = (res, status, body) => {
|
|
@@ -36451,6 +36702,8 @@ const sendUnauthorized = (res, issuer) => {
|
|
|
36451
36702
|
const wwwAuthenticate = `Bearer resource_metadata="${issuer}${PROTECTED_RESOURCE_PATHS[0]}", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
|
|
36452
36703
|
sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
|
|
36453
36704
|
};
|
|
36705
|
+
const asNodeRequest = (req) => req;
|
|
36706
|
+
const isListenRequest = (body) => typeof body === "object" && body !== null && body.method === "subscriptions/listen";
|
|
36454
36707
|
const MAX_BODY_BYTES = 4194304;
|
|
36455
36708
|
const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
|
|
36456
36709
|
const chunks = [];
|
|
@@ -36521,22 +36774,26 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36521
36774
|
const sessions = /* @__PURE__ */ new Map();
|
|
36522
36775
|
const idleTimeoutMs = options.sessionIdleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
|
|
36523
36776
|
const maxBodyBytes = options.maxBodyBytes ?? 4194304;
|
|
36524
|
-
const
|
|
36777
|
+
const serverFactory = options.serverFactory ?? defaultServerFactory;
|
|
36778
|
+
const grants = createGrantRegistry({
|
|
36779
|
+
idleTimeoutMs,
|
|
36780
|
+
onCloseError: (grantId, err) => logger.warn("grant_close_failed", {
|
|
36781
|
+
grantId,
|
|
36782
|
+
error: err instanceof Error ? err.message : String(err)
|
|
36783
|
+
})
|
|
36784
|
+
});
|
|
36785
|
+
const effectiveConfig = (token) => !config.readOnly && token.canWrite ? config : {
|
|
36786
|
+
...config,
|
|
36787
|
+
readOnly: true
|
|
36788
|
+
};
|
|
36789
|
+
const dispatchToSession = async (req, res, sessionId, token, body) => {
|
|
36525
36790
|
const session = sessions.get(sessionId);
|
|
36526
36791
|
if (!session || session.subject !== token.subject) return false;
|
|
36527
36792
|
if (session.canWrite && !token.canWrite) return false;
|
|
36528
36793
|
session.auth.zendeskToken = token.zendeskAccessToken;
|
|
36529
36794
|
session.auth.grantId = token.grantId;
|
|
36530
36795
|
session.lastActivityAt = Date.now();
|
|
36531
|
-
|
|
36532
|
-
ok: true,
|
|
36533
|
-
value: void 0
|
|
36534
|
-
};
|
|
36535
|
-
if (!body.ok) {
|
|
36536
|
-
respondBodyError(req, res, body);
|
|
36537
|
-
return true;
|
|
36538
|
-
}
|
|
36539
|
-
await session.transport.handleRequest(req, res, body.value);
|
|
36796
|
+
await session.transport.handleRequest(req, res, body);
|
|
36540
36797
|
return true;
|
|
36541
36798
|
};
|
|
36542
36799
|
const openSession = async (req, res, body, token, as) => {
|
|
@@ -36544,11 +36801,20 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36544
36801
|
zendeskToken: token.zendeskAccessToken,
|
|
36545
36802
|
grantId: token.grantId
|
|
36546
36803
|
};
|
|
36547
|
-
const
|
|
36548
|
-
const server =
|
|
36549
|
-
|
|
36550
|
-
|
|
36551
|
-
|
|
36804
|
+
const sessionConfig = effectiveConfig(token);
|
|
36805
|
+
const server = serverFactory({
|
|
36806
|
+
config: sessionConfig,
|
|
36807
|
+
getToken: () => auth.zendeskToken,
|
|
36808
|
+
logger,
|
|
36809
|
+
onUnauthorized: () => void as.revokeGrant(auth.grantId),
|
|
36810
|
+
requestStateCodec: createRequestStateCodec({
|
|
36811
|
+
key: as.requestStateKey,
|
|
36812
|
+
principal: () => auth.grantId
|
|
36813
|
+
}),
|
|
36814
|
+
notifyResourceUpdated: (uri) => {
|
|
36815
|
+
server.server.sendResourceUpdated({ uri }).catch(() => void 0);
|
|
36816
|
+
}
|
|
36817
|
+
});
|
|
36552
36818
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
36553
36819
|
sessionIdGenerator: () => randomUUID(),
|
|
36554
36820
|
onsessioninitialized: (newId) => {
|
|
@@ -36556,7 +36822,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36556
36822
|
transport,
|
|
36557
36823
|
auth,
|
|
36558
36824
|
subject: token.subject,
|
|
36559
|
-
canWrite,
|
|
36825
|
+
canWrite: !sessionConfig.readOnly,
|
|
36560
36826
|
lastActivityAt: Date.now(),
|
|
36561
36827
|
close: async () => {
|
|
36562
36828
|
await transport.close();
|
|
@@ -36571,25 +36837,97 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36571
36837
|
await server.connect(transport);
|
|
36572
36838
|
await transport.handleRequest(req, res, body);
|
|
36573
36839
|
};
|
|
36574
|
-
const
|
|
36575
|
-
const
|
|
36576
|
-
const
|
|
36577
|
-
|
|
36840
|
+
const createGrantEntry = (token, as) => {
|
|
36841
|
+
const { grantId } = token;
|
|
36842
|
+
const auth = { zendeskToken: token.zendeskAccessToken };
|
|
36843
|
+
const getToken = () => auth.zendeskToken;
|
|
36844
|
+
const onUnauthorized = () => {
|
|
36845
|
+
entry.revoked = true;
|
|
36846
|
+
as.revokeGrant(grantId);
|
|
36847
|
+
};
|
|
36848
|
+
const caches = createServerCaches(config, getToken, onUnauthorized);
|
|
36849
|
+
const requestStateCodec = createRequestStateCodec({
|
|
36850
|
+
key: as.requestStateKey,
|
|
36851
|
+
principal: () => grantId
|
|
36852
|
+
});
|
|
36853
|
+
const handler = createMcpHandler(({ authInfo }) => {
|
|
36854
|
+
const verified = authInfo?.extra?.["verified"];
|
|
36855
|
+
if (!verified) throw new Error("2026-07-28 request without a verified token");
|
|
36856
|
+
return serverFactory({
|
|
36857
|
+
config: effectiveConfig(verified),
|
|
36858
|
+
getToken,
|
|
36859
|
+
logger,
|
|
36860
|
+
onUnauthorized,
|
|
36861
|
+
caches,
|
|
36862
|
+
requestStateCodec,
|
|
36863
|
+
notifyResourceUpdated: (uri) => handler.notify.resourceUpdated(uri)
|
|
36864
|
+
});
|
|
36865
|
+
}, {
|
|
36866
|
+
legacy: "reject",
|
|
36867
|
+
onerror: (err) => logger.debug("mcp_request_rejected", { error: err.message })
|
|
36868
|
+
});
|
|
36869
|
+
const entry = {
|
|
36870
|
+
auth,
|
|
36871
|
+
revoked: false,
|
|
36872
|
+
inFlight: 0,
|
|
36873
|
+
handle: toNodeHandler(handler),
|
|
36874
|
+
close: () => handler.close()
|
|
36875
|
+
};
|
|
36876
|
+
return entry;
|
|
36877
|
+
};
|
|
36878
|
+
const serveModern = async (req, res, body, token, as) => {
|
|
36879
|
+
const entry = grants.acquire(token.grantId, token.subject, () => createGrantEntry(token, as));
|
|
36880
|
+
if (!entry) {
|
|
36578
36881
|
sendUnauthorized(res, as.issuer);
|
|
36579
36882
|
return;
|
|
36580
36883
|
}
|
|
36884
|
+
entry.auth.zendeskToken = token.zendeskAccessToken;
|
|
36885
|
+
const request = asNodeRequest(req);
|
|
36886
|
+
request.auth = {
|
|
36887
|
+
token: "",
|
|
36888
|
+
clientId: token.clientId,
|
|
36889
|
+
scopes: [],
|
|
36890
|
+
expiresAt: token.expiresAt,
|
|
36891
|
+
extra: { verified: token }
|
|
36892
|
+
};
|
|
36893
|
+
const counted = !isListenRequest(body);
|
|
36894
|
+
if (counted) entry.inFlight += 1;
|
|
36895
|
+
try {
|
|
36896
|
+
await entry.handle(request, res, body);
|
|
36897
|
+
} finally {
|
|
36898
|
+
if (counted) entry.inFlight -= 1;
|
|
36899
|
+
if (entry.revoked && entry.inFlight === 0) await grants.close(token.grantId, entry);
|
|
36900
|
+
}
|
|
36901
|
+
};
|
|
36902
|
+
const serveLegacy = async (req, res, body, token, as) => {
|
|
36581
36903
|
const sessionId = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
36582
|
-
if (sessionId && await dispatchToSession(req, res, sessionId, token)) return;
|
|
36904
|
+
if (sessionId && await dispatchToSession(req, res, sessionId, token, body)) return;
|
|
36583
36905
|
if (req.method !== "POST") {
|
|
36584
36906
|
sendJsonRpcError(res, 400, -32e3, "No active session; initialize via POST first.");
|
|
36585
36907
|
return;
|
|
36586
36908
|
}
|
|
36587
|
-
|
|
36588
|
-
|
|
36589
|
-
|
|
36909
|
+
await openSession(req, res, body, token, as);
|
|
36910
|
+
};
|
|
36911
|
+
const handleMcpRequest = async (req, res, as) => {
|
|
36912
|
+
const token = await authenticate(req, as);
|
|
36913
|
+
if (!token) {
|
|
36914
|
+
sendUnauthorized(res, as.issuer);
|
|
36915
|
+
return;
|
|
36916
|
+
}
|
|
36917
|
+
let body;
|
|
36918
|
+
if (req.method === "POST") {
|
|
36919
|
+
const read = await readJsonBody(req, maxBodyBytes);
|
|
36920
|
+
if (!read.ok) {
|
|
36921
|
+
respondBodyError(req, res, read);
|
|
36922
|
+
return;
|
|
36923
|
+
}
|
|
36924
|
+
body = read.value;
|
|
36925
|
+
}
|
|
36926
|
+
if (body !== void 0 && !await isLegacyRequest(await toWebRequest(asNodeRequest(req), body), body)) {
|
|
36927
|
+
await serveModern(req, res, body, token, as);
|
|
36590
36928
|
return;
|
|
36591
36929
|
}
|
|
36592
|
-
await
|
|
36930
|
+
await serveLegacy(req, res, body, token, as);
|
|
36593
36931
|
};
|
|
36594
36932
|
const route = async (req, res, as) => {
|
|
36595
36933
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -36658,6 +36996,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36658
36996
|
});
|
|
36659
36997
|
}
|
|
36660
36998
|
}
|
|
36999
|
+
await grants.sweep();
|
|
36661
37000
|
};
|
|
36662
37001
|
const sweeper = setInterval(() => void sweepIdleSessions(), options.sweepIntervalMs ?? SESSION_SWEEP_INTERVAL_MS);
|
|
36663
37002
|
sweeper.unref();
|
|
@@ -36667,6 +37006,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36667
37006
|
clearInterval(sweeper);
|
|
36668
37007
|
await Promise.all([...sessions.values()].map((session) => session.close()));
|
|
36669
37008
|
sessions.clear();
|
|
37009
|
+
await grants.closeAll();
|
|
36670
37010
|
httpServer.closeAllConnections();
|
|
36671
37011
|
await new Promise((resolve, reject) => {
|
|
36672
37012
|
httpServer.close((err) => err ? reject(err) : resolve());
|