@fruggr/zendesk-mcp-server 3.3.1 → 3.5.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 +6 -3
- package/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/{http-BYYYoQaK.js → http-DDdso3Av.js} +366 -28
- package/dist/index.js +4974 -717
- package/dist/pkce-DE_v9NQy.js +60 -0
- package/dist/sbom.cdx.json +7 -7
- package/dist/{token-store-BLAkLLQO.js → token-store-CQx_KFe2.js} +16 -7
- package/package.json +5 -5
- package/dist/token-persistence-BUHh23BM.js +0 -147
package/README.md
CHANGED
|
@@ -224,13 +224,16 @@ clients, MCP client wiring, CORS and what stays the operator's job:
|
|
|
224
224
|
## Tool surface
|
|
225
225
|
|
|
226
226
|
Tools are grouped into namespaces: **Tickets**, **Help Center**, **Users &
|
|
227
|
-
Organizations**, **Search**,
|
|
228
|
-
|
|
227
|
+
Organizations**, **Search**, **Connector preferences** — this server's own
|
|
228
|
+
per-user settings, such as SLA alert thresholds, never written to Zendesk — and
|
|
229
|
+
**Requests** — the end-user surface, which is opt-in and covered under
|
|
230
|
+
[End-user mode](#end-user-mode). The server registers
|
|
229
231
|
them in one of three modes, so you can trade granularity against context budget:
|
|
230
232
|
|
|
231
233
|
- **`all`**: every operation as its own tool, for clients with good tool selection;
|
|
232
234
|
- **`namespace`** (default): one proxy tool per namespace, a balanced middle ground;
|
|
233
|
-
- **`single`**: a single `zendesk` tool, for models with limited tool slots
|
|
235
|
+
- **`single`**: a single `zendesk` tool, for models with limited tool slots (the
|
|
236
|
+
connector's own preferences keep their `connector_preferences` proxy beside it).
|
|
234
237
|
|
|
235
238
|
Proxies take `{ "operation": "<tool_name>", "params": { … } }` and validate
|
|
236
239
|
`params` through the original schema. `--namespace`, `--tool` and `--read-only`
|
|
@@ -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.7
|
|
5202
5202
|
|
|
5203
5203
|
License: MIT
|
|
5204
5204
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { A as __commonJSMin, C as isLegacyRequest, D as STARTUP_DOCS, E as OAuthErrorCode, M as __require, N as __toESM, O as createStartupError, S as createMcpHandler, T as OAuthError, _ as deadlineSignal, a as watchGrantAlerts, b as DEFAULT_MAX_REQUEST_BODY_SIZE, c as configDir, g as REQUEST_TIMEOUT_MS, h as zendeskGet, i as defaultServerFactory, j as __exportAll, k as errnoCode, m as silentLogger, n as listenedUris, o as startSlaRuntime, p as createRequestStateCodec, r as createServerCaches, t as createListenFeed, w as verifyBearerToken, x as WebStandardStreamableHTTPServerTransport, y as getOAuthUrls } from "./index.js";
|
|
3
|
+
import { a as supportedScopes, i as requestedScope, n as generateCodeVerifier, o as escapeHtml, t as generateCodeChallenge } from "./pkce-DE_v9NQy.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";
|
|
@@ -667,6 +667,185 @@ var NodeStreamableHTTPServerTransport = class {
|
|
|
667
667
|
this._webStandardTransport.closeStandaloneSSEStream();
|
|
668
668
|
}
|
|
669
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
|
+
}
|
|
670
849
|
//#endregion
|
|
671
850
|
//#region node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
672
851
|
const encoder$1 = new TextEncoder();
|
|
@@ -2824,7 +3003,8 @@ const deriveKeySet = (master) => {
|
|
|
2824
3003
|
kid: kidFor(master, "at-rest"),
|
|
2825
3004
|
key: createSecretKey(derive(master, "at-rest"))
|
|
2826
3005
|
},
|
|
2827
|
-
cookieKey: derive(master, "cookie").toString("base64url")
|
|
3006
|
+
cookieKey: derive(master, "cookie").toString("base64url"),
|
|
3007
|
+
requestState: derive(master, "request-state")
|
|
2828
3008
|
};
|
|
2829
3009
|
};
|
|
2830
3010
|
const deriveKeyRing = (raw) => {
|
|
@@ -36261,6 +36441,7 @@ const createAuthorizationServer = (options) => {
|
|
|
36261
36441
|
return {
|
|
36262
36442
|
issuer,
|
|
36263
36443
|
resource,
|
|
36444
|
+
requestStateKey: ring[0].requestState,
|
|
36264
36445
|
protectedResourceMetadata: {
|
|
36265
36446
|
resource,
|
|
36266
36447
|
authorization_servers: [issuer],
|
|
@@ -36326,6 +36507,47 @@ const prepareAuthorizationServer = async (config, logger = silentLogger) => {
|
|
|
36326
36507
|
});
|
|
36327
36508
|
};
|
|
36328
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
|
|
36329
36551
|
//#region src/transports/http.ts
|
|
36330
36552
|
const WILDCARD_HOSTS = /* @__PURE__ */ new Set([
|
|
36331
36553
|
"0.0.0.0",
|
|
@@ -36344,7 +36566,7 @@ const DEFAULT_BROWSER_MCP_CLIENT_ORIGINS = [
|
|
|
36344
36566
|
"https://grok.com"
|
|
36345
36567
|
];
|
|
36346
36568
|
const CORS_ALLOWED_METHODS = "GET, POST, DELETE, OPTIONS";
|
|
36347
|
-
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";
|
|
36348
36570
|
const CORS_EXPOSE_HEADERS = "mcp-session-id";
|
|
36349
36571
|
const CORS_MAX_AGE = "600";
|
|
36350
36572
|
const LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -36480,6 +36702,7 @@ const sendUnauthorized = (res, issuer) => {
|
|
|
36480
36702
|
const wwwAuthenticate = `Bearer resource_metadata="${issuer}${PROTECTED_RESOURCE_PATHS[0]}", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
|
|
36481
36703
|
sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
|
|
36482
36704
|
};
|
|
36705
|
+
const asNodeRequest = (req) => req;
|
|
36483
36706
|
const MAX_BODY_BYTES = 4194304;
|
|
36484
36707
|
const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
|
|
36485
36708
|
const chunks = [];
|
|
@@ -36548,24 +36771,29 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36548
36771
|
const buildAuthorizationServer = await prepareAuthorizationServer(config, logger);
|
|
36549
36772
|
let authorizationServer;
|
|
36550
36773
|
const sessions = /* @__PURE__ */ new Map();
|
|
36774
|
+
const sla = startSlaRuntime(config);
|
|
36551
36775
|
const idleTimeoutMs = options.sessionIdleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
|
|
36552
36776
|
const maxBodyBytes = options.maxBodyBytes ?? 4194304;
|
|
36553
|
-
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) => {
|
|
36554
36790
|
const session = sessions.get(sessionId);
|
|
36555
36791
|
if (!session || session.subject !== token.subject) return false;
|
|
36556
36792
|
if (session.canWrite && !token.canWrite) return false;
|
|
36557
36793
|
session.auth.zendeskToken = token.zendeskAccessToken;
|
|
36558
36794
|
session.auth.grantId = token.grantId;
|
|
36559
36795
|
session.lastActivityAt = Date.now();
|
|
36560
|
-
|
|
36561
|
-
ok: true,
|
|
36562
|
-
value: void 0
|
|
36563
|
-
};
|
|
36564
|
-
if (!body.ok) {
|
|
36565
|
-
respondBodyError(req, res, body);
|
|
36566
|
-
return true;
|
|
36567
|
-
}
|
|
36568
|
-
await session.transport.handleRequest(req, res, body.value);
|
|
36796
|
+
await session.transport.handleRequest(req, res, body);
|
|
36569
36797
|
return true;
|
|
36570
36798
|
};
|
|
36571
36799
|
const openSession = async (req, res, body, token, as) => {
|
|
@@ -36573,11 +36801,21 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36573
36801
|
zendeskToken: token.zendeskAccessToken,
|
|
36574
36802
|
grantId: token.grantId
|
|
36575
36803
|
};
|
|
36576
|
-
const
|
|
36577
|
-
const server =
|
|
36578
|
-
|
|
36579
|
-
|
|
36580
|
-
|
|
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
|
+
slaRuntime: sla.runtime,
|
|
36811
|
+
requestStateCodec: createRequestStateCodec({
|
|
36812
|
+
key: as.requestStateKey,
|
|
36813
|
+
principal: () => auth.grantId
|
|
36814
|
+
}),
|
|
36815
|
+
notifyResourceUpdated: (uri) => {
|
|
36816
|
+
server.server.sendResourceUpdated({ uri }).catch(() => void 0);
|
|
36817
|
+
}
|
|
36818
|
+
});
|
|
36581
36819
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
36582
36820
|
sessionIdGenerator: () => randomUUID(),
|
|
36583
36821
|
onsessioninitialized: (newId) => {
|
|
@@ -36585,7 +36823,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36585
36823
|
transport,
|
|
36586
36824
|
auth,
|
|
36587
36825
|
subject: token.subject,
|
|
36588
|
-
canWrite,
|
|
36826
|
+
canWrite: !sessionConfig.readOnly,
|
|
36589
36827
|
lastActivityAt: Date.now(),
|
|
36590
36828
|
close: async () => {
|
|
36591
36829
|
await transport.close();
|
|
@@ -36600,24 +36838,121 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36600
36838
|
await server.connect(transport);
|
|
36601
36839
|
await transport.handleRequest(req, res, body);
|
|
36602
36840
|
};
|
|
36603
|
-
const
|
|
36604
|
-
const
|
|
36605
|
-
|
|
36841
|
+
const createGrantEntry = (token, as) => {
|
|
36842
|
+
const { grantId } = token;
|
|
36843
|
+
const auth = { zendeskToken: token.zendeskAccessToken };
|
|
36844
|
+
const getToken = () => auth.zendeskToken;
|
|
36845
|
+
const onUnauthorized = () => {
|
|
36846
|
+
entry.revoked = true;
|
|
36847
|
+
as.revokeGrant(grantId);
|
|
36848
|
+
};
|
|
36849
|
+
const caches = createServerCaches(config, getToken, onUnauthorized);
|
|
36850
|
+
const requestStateCodec = createRequestStateCodec({
|
|
36851
|
+
key: as.requestStateKey,
|
|
36852
|
+
principal: () => grantId
|
|
36853
|
+
});
|
|
36854
|
+
const handler = createMcpHandler(({ authInfo }) => {
|
|
36855
|
+
const verified = authInfo?.extra?.["verified"];
|
|
36856
|
+
if (!verified) throw new Error("2026-07-28 request without a verified token");
|
|
36857
|
+
return serverFactory({
|
|
36858
|
+
config: effectiveConfig(verified),
|
|
36859
|
+
getToken,
|
|
36860
|
+
logger,
|
|
36861
|
+
onUnauthorized,
|
|
36862
|
+
caches,
|
|
36863
|
+
requestStateCodec,
|
|
36864
|
+
slaRuntime: sla.runtime,
|
|
36865
|
+
alertPolling: "grant",
|
|
36866
|
+
notifyResourceUpdated: (uri) => handler.notify.resourceUpdated(uri)
|
|
36867
|
+
});
|
|
36868
|
+
}, {
|
|
36869
|
+
legacy: "reject",
|
|
36870
|
+
onerror: (err) => logger.debug("mcp_request_rejected", { error: err.message })
|
|
36871
|
+
});
|
|
36872
|
+
const listens = createListenFeed();
|
|
36873
|
+
const stopAlerts = watchGrantAlerts({
|
|
36874
|
+
config,
|
|
36875
|
+
getToken,
|
|
36876
|
+
caches,
|
|
36877
|
+
slaRuntime: sla.runtime,
|
|
36878
|
+
logger,
|
|
36879
|
+
listens,
|
|
36880
|
+
notify: (uri) => handler.notify.resourceUpdated(uri),
|
|
36881
|
+
onUnauthorized
|
|
36882
|
+
});
|
|
36883
|
+
const entry = {
|
|
36884
|
+
auth,
|
|
36885
|
+
revoked: false,
|
|
36886
|
+
inFlight: 0,
|
|
36887
|
+
listens,
|
|
36888
|
+
handle: toNodeHandler(handler),
|
|
36889
|
+
close: () => {
|
|
36890
|
+
stopAlerts();
|
|
36891
|
+
listens.closeAll();
|
|
36892
|
+
return handler.close();
|
|
36893
|
+
}
|
|
36894
|
+
};
|
|
36895
|
+
return entry;
|
|
36896
|
+
};
|
|
36897
|
+
const serveModern = async (req, res, body, token, as) => {
|
|
36898
|
+
const entry = grants.acquire(token.grantId, token.subject, () => createGrantEntry(token, as));
|
|
36899
|
+
if (!entry) {
|
|
36606
36900
|
sendUnauthorized(res, as.issuer);
|
|
36607
36901
|
return;
|
|
36608
36902
|
}
|
|
36903
|
+
entry.auth.zendeskToken = token.zendeskAccessToken;
|
|
36904
|
+
const request = asNodeRequest(req);
|
|
36905
|
+
request.auth = {
|
|
36906
|
+
token: "",
|
|
36907
|
+
clientId: token.clientId,
|
|
36908
|
+
scopes: [],
|
|
36909
|
+
expiresAt: token.expiresAt,
|
|
36910
|
+
extra: { verified: token }
|
|
36911
|
+
};
|
|
36912
|
+
const listened = listenedUris(body);
|
|
36913
|
+
const counted = listened === void 0;
|
|
36914
|
+
if (listened) {
|
|
36915
|
+
const stream = Symbol("listen");
|
|
36916
|
+
res.once("close", () => entry.listens.close(stream));
|
|
36917
|
+
entry.listens.open(stream, listened);
|
|
36918
|
+
}
|
|
36919
|
+
if (counted) entry.inFlight += 1;
|
|
36920
|
+
try {
|
|
36921
|
+
await entry.handle(request, res, body);
|
|
36922
|
+
} finally {
|
|
36923
|
+
if (counted) entry.inFlight -= 1;
|
|
36924
|
+
if (entry.revoked && entry.inFlight === 0) await grants.close(token.grantId, entry);
|
|
36925
|
+
}
|
|
36926
|
+
};
|
|
36927
|
+
const serveLegacy = async (req, res, body, token, as) => {
|
|
36609
36928
|
const sessionId = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
36610
|
-
if (sessionId && await dispatchToSession(req, res, sessionId, token)) return;
|
|
36929
|
+
if (sessionId && await dispatchToSession(req, res, sessionId, token, body)) return;
|
|
36611
36930
|
if (req.method !== "POST") {
|
|
36612
36931
|
sendJsonRpcError(res, 400, -32e3, "No active session; initialize via POST first.");
|
|
36613
36932
|
return;
|
|
36614
36933
|
}
|
|
36615
|
-
|
|
36616
|
-
|
|
36617
|
-
|
|
36934
|
+
await openSession(req, res, body, token, as);
|
|
36935
|
+
};
|
|
36936
|
+
const handleMcpRequest = async (req, res, as) => {
|
|
36937
|
+
const token = await authenticate(req, as);
|
|
36938
|
+
if (!token) {
|
|
36939
|
+
sendUnauthorized(res, as.issuer);
|
|
36940
|
+
return;
|
|
36941
|
+
}
|
|
36942
|
+
let body;
|
|
36943
|
+
if (req.method === "POST") {
|
|
36944
|
+
const read = await readJsonBody(req, maxBodyBytes);
|
|
36945
|
+
if (!read.ok) {
|
|
36946
|
+
respondBodyError(req, res, read);
|
|
36947
|
+
return;
|
|
36948
|
+
}
|
|
36949
|
+
body = read.value;
|
|
36950
|
+
}
|
|
36951
|
+
if (body !== void 0 && !await isLegacyRequest(await toWebRequest(asNodeRequest(req), body), body)) {
|
|
36952
|
+
await serveModern(req, res, body, token, as);
|
|
36618
36953
|
return;
|
|
36619
36954
|
}
|
|
36620
|
-
await
|
|
36955
|
+
await serveLegacy(req, res, body, token, as);
|
|
36621
36956
|
};
|
|
36622
36957
|
const route = async (req, res, as) => {
|
|
36623
36958
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -36686,6 +37021,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36686
37021
|
});
|
|
36687
37022
|
}
|
|
36688
37023
|
}
|
|
37024
|
+
await grants.sweep();
|
|
36689
37025
|
};
|
|
36690
37026
|
const sweeper = setInterval(() => void sweepIdleSessions(), options.sweepIntervalMs ?? SESSION_SWEEP_INTERVAL_MS);
|
|
36691
37027
|
sweeper.unref();
|
|
@@ -36693,8 +37029,10 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
|
|
|
36693
37029
|
port: boundPort,
|
|
36694
37030
|
close: async () => {
|
|
36695
37031
|
clearInterval(sweeper);
|
|
37032
|
+
sla.stop();
|
|
36696
37033
|
await Promise.all([...sessions.values()].map((session) => session.close()));
|
|
36697
37034
|
sessions.clear();
|
|
37035
|
+
await grants.closeAll();
|
|
36698
37036
|
httpServer.closeAllConnections();
|
|
36699
37037
|
await new Promise((resolve, reject) => {
|
|
36700
37038
|
httpServer.close((err) => err ? reject(err) : resolve());
|