mcp-trellis 3.0.0 → 3.1.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 +61 -52
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -0
- package/dist/app.js.map +1 -1
- package/dist/auth/signed/payload.d.ts +31 -0
- package/dist/auth/signed/payload.d.ts.map +1 -0
- package/dist/auth/signed/payload.js +85 -0
- package/dist/auth/signed/payload.js.map +1 -0
- package/dist/auth/signed/refresh.d.ts +34 -0
- package/dist/auth/signed/refresh.d.ts.map +1 -0
- package/dist/auth/signed/refresh.js +95 -0
- package/dist/auth/signed/refresh.js.map +1 -0
- package/dist/auth/signed/token.d.ts +41 -0
- package/dist/auth/signed/token.d.ts.map +1 -0
- package/dist/auth/signed/token.js +103 -0
- package/dist/auth/signed/token.js.map +1 -0
- package/dist/deprecated.d.ts +15 -15
- package/dist/deprecated.d.ts.map +1 -1
- package/dist/deprecated.js +15 -15
- package/dist/deprecated.js.map +1 -1
- package/dist/http/body.d.ts +4 -0
- package/dist/http/body.d.ts.map +1 -1
- package/dist/http/body.js +9 -2
- package/dist/http/body.js.map +1 -1
- package/dist/mcp/methods.d.ts +7 -0
- package/dist/mcp/methods.d.ts.map +1 -1
- package/dist/mcp/methods.js +13 -1
- package/dist/mcp/methods.js.map +1 -1
- package/dist/mcp/rpc-handlers.d.ts.map +1 -1
- package/dist/mcp/rpc-handlers.js +2 -2
- package/dist/mcp/rpc-handlers.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +4 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/oauth/cimd/cimd-ssrf.d.ts +8 -4
- package/dist/oauth/cimd/cimd-ssrf.d.ts.map +1 -1
- package/dist/oauth/cimd/cimd-ssrf.js +9 -68
- package/dist/oauth/cimd/cimd-ssrf.js.map +1 -1
- package/dist/oauth/cimd/cimd.d.ts +3 -16
- package/dist/oauth/cimd/cimd.d.ts.map +1 -1
- package/dist/oauth/cimd/cimd.js +16 -6
- package/dist/oauth/cimd/cimd.js.map +1 -1
- package/dist/oauth/cimd/dns.d.ts +4 -0
- package/dist/oauth/cimd/dns.d.ts.map +1 -0
- package/dist/oauth/cimd/dns.js +45 -0
- package/dist/oauth/cimd/dns.js.map +1 -0
- package/dist/oauth/cimd/failure.d.ts.map +1 -1
- package/dist/oauth/cimd/failure.js +4 -1
- package/dist/oauth/cimd/failure.js.map +1 -1
- package/dist/oauth/cimd/types.d.ts +15 -1
- package/dist/oauth/cimd/types.d.ts.map +1 -1
- package/dist/oauth/cimd/types.js +1 -1
- package/dist/oauth/cimd/types.js.map +1 -1
- package/dist/oauth/cimd/url-guard.d.ts +5 -0
- package/dist/oauth/cimd/url-guard.d.ts.map +1 -0
- package/dist/oauth/cimd/url-guard.js +53 -0
- package/dist/oauth/cimd/url-guard.js.map +1 -0
- package/dist/oauth/config.d.ts +3 -0
- package/dist/oauth/config.d.ts.map +1 -1
- package/dist/oauth/config.js +5 -1
- package/dist/oauth/config.js.map +1 -1
- package/dist/oauth/deprecated.d.ts +22 -22
- package/dist/oauth/deprecated.d.ts.map +1 -1
- package/dist/oauth/deprecated.js +22 -22
- package/dist/oauth/deprecated.js.map +1 -1
- package/dist/oauth/endpoints/authorize.js +1 -1
- package/dist/oauth/endpoints/authorize.js.map +1 -1
- package/dist/oauth/endpoints/consent/error-page.d.ts +6 -0
- package/dist/oauth/endpoints/consent/error-page.d.ts.map +1 -0
- package/dist/oauth/endpoints/consent/error-page.js +60 -0
- package/dist/oauth/endpoints/consent/error-page.js.map +1 -0
- package/dist/oauth/endpoints/{consent.d.ts → consent/handler.d.ts} +2 -2
- package/dist/oauth/endpoints/consent/handler.d.ts.map +1 -0
- package/dist/oauth/endpoints/{consent.js → consent/handler.js} +20 -13
- package/dist/oauth/endpoints/consent/handler.js.map +1 -0
- package/dist/oauth/endpoints/{consent-page.d.ts → consent/page.d.ts} +2 -2
- package/dist/oauth/endpoints/consent/page.d.ts.map +1 -0
- package/dist/oauth/endpoints/{consent-page.js → consent/page.js} +13 -3
- package/dist/oauth/endpoints/consent/page.js.map +1 -0
- package/dist/oauth/endpoints/metadata.d.ts +0 -1
- package/dist/oauth/endpoints/metadata.d.ts.map +1 -1
- package/dist/oauth/endpoints/metadata.js.map +1 -1
- package/dist/oauth/endpoints/register.js +3 -3
- package/dist/oauth/endpoints/register.js.map +1 -1
- package/dist/oauth/endpoints/token.d.ts.map +1 -1
- package/dist/oauth/endpoints/token.js +2 -5
- package/dist/oauth/endpoints/token.js.map +1 -1
- package/dist/oauth/endpoints/wellknown.d.ts.map +1 -1
- package/dist/oauth/endpoints/wellknown.js +2 -9
- package/dist/oauth/endpoints/wellknown.js.map +1 -1
- package/dist/oauth/policy/redirect.d.ts +3 -5
- package/dist/oauth/policy/redirect.d.ts.map +1 -1
- package/dist/oauth/policy/redirect.js +13 -5
- package/dist/oauth/policy/redirect.js.map +1 -1
- package/dist/oauth/router.js +1 -1
- package/dist/oauth/router.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/docs/ROADMAP.md +17 -229
- package/docs/compatibility.md +15 -3
- package/docs/reference.md +3 -2
- package/docs/security.md +58 -4
- package/docs/troubleshooting.md +4 -1
- package/package.json +7 -4
- package/dist/auth/signed-token.d.ts +0 -26
- package/dist/auth/signed-token.d.ts.map +0 -1
- package/dist/auth/signed-token.js +0 -96
- package/dist/auth/signed-token.js.map +0 -1
- package/dist/oauth/endpoints/consent-page.d.ts.map +0 -1
- package/dist/oauth/endpoints/consent-page.js.map +0 -1
- package/dist/oauth/endpoints/consent.d.ts.map +0 -1
- package/dist/oauth/endpoints/consent.js.map +0 -1
- package/docs/diagrams/demo-tool-result.png +0 -0
- package/docs/diagrams/mcp-trellis-architecture.html +0 -305
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wellknown.d.ts","sourceRoot":"","sources":["../../../src/oauth/endpoints/wellknown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wellknown.d.ts","sourceRoot":"","sources":["../../../src/oauth/endpoints/wellknown.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD,eAAO,MAAM,eAAe,GAC1B,SAAS,OAAO,EAChB,SAAS,kBAAkB,EAC3B,OAAO;IACL,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,KACA,OAAO,CAAC,QAAQ,GAAG,IAAI,CAuBzB,CAAC"}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { GET_ONLY, jsonResponse, requireHttpMethod } from "../../http/http.js";
|
|
2
2
|
import { pickDefined } from "../../util/defined.js";
|
|
3
|
-
import { unregisteredClientsAllowed } from "../config.js";
|
|
4
|
-
import {
|
|
5
|
-
import { authorizationServerMetadata, protectedResourceMetadata, } from "./metadata.js";
|
|
6
|
-
const grantTypesOf = (ports) => {
|
|
7
|
-
const grants = [GRANT_TYPES.authorizationCode];
|
|
8
|
-
if (ports.refreshAccessToken)
|
|
9
|
-
grants.push(GRANT_TYPES.refreshToken);
|
|
10
|
-
return grants;
|
|
11
|
-
};
|
|
3
|
+
import { grantTypesOf, unregisteredClientsAllowed } from "../config.js";
|
|
4
|
+
import { authorizationServerMetadata, protectedResourceMetadata } from "./metadata.js";
|
|
12
5
|
export const handleWellKnown = async (request, options, paths) => {
|
|
13
6
|
const url = new URL(request.url);
|
|
14
7
|
const path = url.pathname;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wellknown.js","sourceRoot":"","sources":["../../../src/oauth/endpoints/wellknown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"wellknown.js","sourceRoot":"","sources":["../../../src/oauth/endpoints/wellknown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAExE,OAAO,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAEvF,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,OAAgB,EAChB,OAA2B,EAC3B,KAKC,EACyB,EAAE;IAC5B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvE,MAAM,WAAW,GAAG,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzD,IAAI,WAAW;QAAE,OAAO,WAAW,CAAC;IAEpC,MAAM,QAAQ,GAAG;QACf,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC;QACvC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,0BAA0B,CAAC,CAAC;QAC/D,UAAU,EAAE,0BAA0B,CAAC,OAAO,CAAC;QAC/C,iBAAiB,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;QACrD,aAAa,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI;KACrC,CAAC;IAEF,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QACnC,CAAC,CAAC,yBAAyB,CAAC,QAAQ,CAAC;QACrC,CAAC,CAAC,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC"}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export declare const CLAUDE_CALLBACK = "https://claude.ai/api/mcp/auth_callback";
|
|
2
|
-
/**
|
|
3
|
-
* ChatGPT's stable connector redirect. The callback-specific form
|
|
4
|
-
* (`https://chatgpt.com/connector/oauth/{id}`) cannot be listed ahead of time;
|
|
5
|
-
* a CIMD client binds it through its document instead.
|
|
6
|
-
*/
|
|
2
|
+
/** ChatGPT's stable connector redirect. */
|
|
7
3
|
export declare const CHATGPT_CALLBACK = "https://chatgpt.com/connector_platform_oauth_redirect";
|
|
8
4
|
export type RedirectAllowlistOptions = {
|
|
9
5
|
/** Extra exact-match redirect URIs. */
|
|
@@ -17,6 +13,8 @@ export type RedirectAllowlistOptions = {
|
|
|
17
13
|
allowLoopback?: boolean;
|
|
18
14
|
/** Allow the Claude.ai Custom Connector callback. Default true. */
|
|
19
15
|
allowClaude?: boolean;
|
|
16
|
+
/** Allow ChatGPT's per-connector callback (`/connector/oauth/{id}`). Default false. */
|
|
17
|
+
allowChatgptConnector?: boolean;
|
|
20
18
|
};
|
|
21
19
|
export declare const isAllowedRedirectUri: (uri: string, options?: RedirectAllowlistOptions) => boolean;
|
|
22
20
|
//# sourceMappingURL=redirect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../../src/oauth/policy/redirect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,4CAA4C,CAAC;AAEzE
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../../src/oauth/policy/redirect.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,4CAA4C,CAAC;AAEzE,2CAA2C;AAC3C,eAAO,MAAM,gBAAgB,0DAA0D,CAAC;AAUxF,MAAM,MAAM,wBAAwB,GAAG;IACrC,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mEAAmE;IACnE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AA+CF,eAAO,MAAM,oBAAoB,GAC/B,KAAK,MAAM,EACX,UAAS,wBAA6B,KACrC,OAGF,CAAC"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export const CLAUDE_CALLBACK = "https://claude.ai/api/mcp/auth_callback";
|
|
2
|
+
/** ChatGPT's stable connector redirect. */
|
|
3
|
+
export const CHATGPT_CALLBACK = "https://chatgpt.com/connector_platform_oauth_redirect";
|
|
2
4
|
/**
|
|
3
|
-
* ChatGPT's
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* ChatGPT's per-connector redirect, `https://chatgpt.com/connector/oauth/{id}`.
|
|
6
|
+
* The id is minted per connector, so it can't be listed ahead of time. Matched
|
|
7
|
+
* against the raw string: one id segment, no port, query or fragment.
|
|
6
8
|
*/
|
|
7
|
-
|
|
9
|
+
const CHATGPT_CONNECTOR_CALLBACK = /^https:\/\/chatgpt\.com\/connector\/oauth\/[A-Za-z0-9_-]{1,128}$/;
|
|
8
10
|
const isClaudeCallback = (uri, options) => options.allowClaude !== false && uri === CLAUDE_CALLBACK;
|
|
11
|
+
const isChatgptConnectorCallback = (uri, options) => options.allowChatgptConnector === true && CHATGPT_CONNECTOR_CALLBACK.test(uri);
|
|
9
12
|
const isExtraAllowed = (uri, options) => Boolean(options.extra?.includes(uri));
|
|
10
13
|
/**
|
|
11
14
|
* WHATWG URL keeps IPv6 hosts bracketed, so `[::1]` is the parsed hostname.
|
|
@@ -26,7 +29,12 @@ const isLoopbackHttp = (uri, options) => {
|
|
|
26
29
|
return false;
|
|
27
30
|
}
|
|
28
31
|
};
|
|
29
|
-
const REDIRECT_PREDICATES = [
|
|
32
|
+
const REDIRECT_PREDICATES = [
|
|
33
|
+
isClaudeCallback,
|
|
34
|
+
isChatgptConnectorCallback,
|
|
35
|
+
isExtraAllowed,
|
|
36
|
+
isLoopbackHttp,
|
|
37
|
+
];
|
|
30
38
|
/** RFC 6749 forbids a fragment in redirect_uri; embedded credentials are never legitimate. */
|
|
31
39
|
const carriesFragmentOrCredentials = (uri) => {
|
|
32
40
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../../src/oauth/policy/redirect.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,yCAAyC,CAAC;AAEzE
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../../src/oauth/policy/redirect.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,yCAAyC,CAAC;AAEzE,2CAA2C;AAC3C,MAAM,CAAC,MAAM,gBAAgB,GAAG,uDAAuD,CAAC;AAExF;;;;GAIG;AACH,MAAM,0BAA0B,GAC9B,kEAAkE,CAAC;AAoBrE,MAAM,gBAAgB,GAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAC3D,OAAO,CAAC,WAAW,KAAK,KAAK,IAAI,GAAG,KAAK,eAAe,CAAC;AAE3D,MAAM,0BAA0B,GAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CACrE,OAAO,CAAC,qBAAqB,KAAK,IAAI,IAAI,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEjF,MAAM,cAAc,GAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAElG;;;;GAIG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvD,MAAM,cAAc,GAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;IACzD,IAAI,OAAO,CAAC,aAAa,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC;QAC9C,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAwB;IAC/C,gBAAgB;IAChB,0BAA0B;IAC1B,cAAc;IACd,cAAc;CACf,CAAC;AAEF,8FAA8F;AAC9F,MAAM,4BAA4B,GAAG,CAAC,GAAW,EAAW,EAAE;IAC5D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,MAAM,CAAC,IAAI,KAAK,EAAE,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,GAAW,EACX,UAAoC,EAAE,EAC7B,EAAE;IACX,IAAI,4BAA4B,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAC1E,CAAC,CAAC"}
|
package/dist/oauth/router.js
CHANGED
|
@@ -4,7 +4,7 @@ import { assertCodeSecret, assertScopeConfig, oauthError, unregisteredClientsAll
|
|
|
4
4
|
import { DEFAULT_OAUTH_PATH, OAUTH_ERRORS } from "./constants.js";
|
|
5
5
|
import { processCodeStore } from "./crypto/codes.js";
|
|
6
6
|
import { handleAuthorize } from "./endpoints/authorize.js";
|
|
7
|
-
import { handleConsent } from "./endpoints/consent.js";
|
|
7
|
+
import { handleConsent } from "./endpoints/consent/handler.js";
|
|
8
8
|
import { handleRegister } from "./endpoints/register.js";
|
|
9
9
|
import { handleRevoke } from "./endpoints/revoke.js";
|
|
10
10
|
import { handleToken } from "./endpoints/token.js";
|
package/dist/oauth/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/oauth/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/oauth/router.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createMcpApp, type McpApp, type McpAppAuth, type McpAppOptions, type UnifiedAuditEntry, type VerifiedToken, } from "./app.js";
|
|
2
2
|
export { consoleAudit } from "./audit.js";
|
|
3
3
|
export { parseBearer, rejectQueryToken, type WwwAuthenticateOptions, wwwAuthenticateHeader, } from "./auth/bearer.js";
|
|
4
|
-
export { type SignedTokenAuthOptions, signedTokenAuth, } from "./auth/signed
|
|
4
|
+
export { type SignedTokenAuthOptions, signedTokenAuth, } from "./auth/signed/token.js";
|
|
5
5
|
export { authMethodsFor, CLIENT_PROFILES, type ClientName, type ClientProfile, DEFAULT_CLIENTS, hasDynamicClient, preRegisteredClients, redirectUrisFor, } from "./clients.js";
|
|
6
6
|
export * from "./deprecated.js";
|
|
7
7
|
export { type JsonResponseInput, type JsonResponseOptions, jsonResponse, } from "./http/http.js";
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createMcpApp, } from "./app.js";
|
|
2
2
|
export { consoleAudit } from "./audit.js";
|
|
3
3
|
export { parseBearer, rejectQueryToken, wwwAuthenticateHeader, } from "./auth/bearer.js";
|
|
4
|
-
export { signedTokenAuth, } from "./auth/signed
|
|
4
|
+
export { signedTokenAuth, } from "./auth/signed/token.js";
|
|
5
5
|
export { authMethodsFor, CLIENT_PROFILES, DEFAULT_CLIENTS, hasDynamicClient, preRegisteredClients, redirectUrisFor, } from "./clients.js";
|
|
6
6
|
// Moved to `mcp-trellis/advanced`; deprecated aliases until 3.0.
|
|
7
7
|
export * from "./deprecated.js";
|
package/docs/ROADMAP.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# mcp-trellis — roadmap
|
|
2
2
|
|
|
3
|
-
Status: 2026-
|
|
3
|
+
Status: 2026-11-07. Written against MCP spec revision `2026-07-28`. What shipped,
|
|
4
|
+
version by version, is in [CHANGELOG.md](../CHANGELOG.md).
|
|
4
5
|
|
|
5
6
|
## What this library is
|
|
6
7
|
|
|
@@ -9,29 +10,15 @@ Status: 2026-09-14. Written against MCP spec revision `2026-07-28`.
|
|
|
9
10
|
> vendor.
|
|
10
11
|
|
|
11
12
|
The differentiating axis is **clients**, not runtimes. Runtime portability is
|
|
12
|
-
table stakes; the official SDK has it too.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
table stakes; the official SDK has it too. mcp-trellis puts both halves — the MCP
|
|
14
|
+
handler *and* a self-hosted OAuth authorization server — in one zero-dependency
|
|
15
|
+
package that reuses the session your app already has, so you can drop it into a
|
|
16
|
+
Next.js route or a Deno server.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| | Portable runtime | Self-hosted AS | One npm install | Zero-dep |
|
|
21
|
-
|---|---|---|---|---|
|
|
22
|
-
| Official SDK v2 | yes | **no** | yes (handler only) | no |
|
|
23
|
-
| `workers-oauth-provider` | Workers only | yes | yes | — |
|
|
24
|
-
| Auth0 / Authlete / Clerk | yes | **no** (SaaS) | no (IdP + MCP) | — |
|
|
25
|
-
| Better Auth | yes | yes | yes, but full stack + DB | no |
|
|
26
|
-
| **xmcp** | yes | **no** (compose) | no — pair with IdP | — |
|
|
27
|
-
| **Scalekit** | yes | managed connector IdP | no — pair with MCP runtime | — |
|
|
28
|
-
| **WorkOS Standalone Connect** | yes | managed | no — IdP + your MCP | — |
|
|
29
|
-
| **Descope** | yes | managed | no — IdP + your MCP | — |
|
|
30
|
-
| **mcp-trellis** | yes | yes | **yes** (handler + AS) | yes |
|
|
31
|
-
|
|
32
|
-
The official SDK explicitly does not provide an authorization server — it offers
|
|
33
|
-
SDK-level opt-ins and expects you to bring the infrastructure. That gap is the
|
|
34
|
-
product.
|
|
18
|
+
The official TypeScript SDK v2 ships resource-server helpers only and never
|
|
19
|
+
issues tokens. How mcp-trellis compares with Better Auth, `workers-oauth-provider`,
|
|
20
|
+
Scalekit and WorkOS is in the
|
|
21
|
+
[README](../README.md#compared-with-the-alternatives).
|
|
35
22
|
|
|
36
23
|
### The rule that decides design questions
|
|
37
24
|
|
|
@@ -41,202 +28,9 @@ goes in the code, not the README.** Audience validation moved into
|
|
|
41
28
|
|
|
42
29
|
### The acceptance test
|
|
43
30
|
|
|
44
|
-
From `npm install` to a connected client: **
|
|
31
|
+
From `npm install` to a connected client: **at most 15 lines and one secret.**
|
|
45
32
|
Anything that forces the user to route requests, read an RFC, or install a
|
|
46
|
-
second package fails.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Shipped
|
|
51
|
-
|
|
52
|
-
**Phase 0 — trust.** Query-string tokens now cover RFC 6750's `access_token`;
|
|
53
|
-
audit fires on every denial including transport-level ones, and a throwing audit
|
|
54
|
-
port can no longer fail a request; tool exceptions are redacted by default;
|
|
55
|
-
a relative `loginUrl` resolves instead of throwing; explicit `id: null` returns
|
|
56
|
-
a proper JSON-RPC error.
|
|
57
|
-
|
|
58
|
-
**Phase 1 — the app layer and the Gemini unblock.**
|
|
59
|
-
|
|
60
|
-
- `createMcpApp` — one call mounts tools, the AS, both discovery documents, and
|
|
61
|
-
the routing between them. `createMcpHandler` / `createOAuthRouter` stay
|
|
62
|
-
exported for anyone who wants to compose the pieces.
|
|
63
|
-
- **Audience validation is enforced by the library.** `verifyToken` returns the
|
|
64
|
-
token's audience; `createMcpApp` compares it against `canonicalResource` and
|
|
65
|
-
rejects mismatches before any tool runs.
|
|
66
|
-
- **Client profiles** (`claude` / `gemini` / `codex`) compose the redirect
|
|
67
|
-
allowlist and the advertised token-endpoint auth methods. Client-specific
|
|
68
|
-
knowledge is the asset, so it lives in the library.
|
|
69
|
-
- **Confidential clients and pre-registration** via a `clientStore` port —
|
|
70
|
-
`client_secret_basic` and `client_secret_post`. This is what Gemini Enterprise
|
|
71
|
-
needs, and it was previously impossible: the AS advertised `["none"]` only.
|
|
72
|
-
Secret storage and comparison stay in the port; credentials never enter the
|
|
73
|
-
library.
|
|
74
|
-
- **Naming only confidential clients is enforced, not just advertised.**
|
|
75
|
-
The first cut of this let `clients: ["gemini"]` configure the AS's metadata
|
|
76
|
-
while leaving DCR and the loopback allowlist wide open — a self-registered
|
|
77
|
-
public client could still complete a normal PKCE flow and get a token,
|
|
78
|
-
quietly defeating the "confidential clients only" intent. Fixed:
|
|
79
|
-
`allowUnregisteredClients: false` (derived automatically by `createMcpApp`
|
|
80
|
-
when no configured client registers dynamically) unmounts `/register`, drops
|
|
81
|
-
it from AS metadata, and rejects any `client_id` the `clientStore` doesn't
|
|
82
|
-
resolve — `unauthorized_client` at `authorize`, `invalid_client` at `token`.
|
|
83
|
-
Public type break: `AuthorizationServerMetadata.registration_endpoint` is
|
|
84
|
-
now optional (`string | undefined`), omitted when DCR is off.
|
|
85
|
-
- **Scope threaded end-to-end.** `authorize` validates the requested scope
|
|
86
|
-
against the advertised set, the auth code carries the grant, and
|
|
87
|
-
`mintAccessToken` receives it. `ToolDef.scope` finally sits on a chain that
|
|
88
|
-
reaches the OAuth layer.
|
|
89
|
-
- Node adapter drains the request stream when `req.body` is absent, so raw
|
|
90
|
-
`http.createServer` works without a body-parser.
|
|
91
|
-
- `[::1]` added to the loopback redirect allowlist.
|
|
92
|
-
- **`defineTool` / `apiTool`.** The library was easy to run but not easy to
|
|
93
|
-
*author tools for* — every tool hand-wrote its own JSON Schema disconnected
|
|
94
|
-
from its handler's types, and every API-backed tool re-invented fetch/error
|
|
95
|
-
plumbing. `defineTool` adds optional Standard Schema v1 parsing in front of
|
|
96
|
-
parsing in front of a handler — typed args, validation failures short-circuit
|
|
97
|
-
before the handler runs. `apiTool` builds on it: give it `request` (+
|
|
98
|
-
optional `respond`) and get a working REST-call tool, non-2xx handled as
|
|
99
|
-
`isError` automatically. `inputSchema` (JSON Schema, for `tools/list`) stays
|
|
100
|
-
required and separate — no library-agnostic way exists to derive it from an
|
|
101
|
-
arbitrary Standard Schema validator, so the two must be kept in sync by hand.
|
|
102
|
-
- **No host port can crash `fetch`.** Nothing guarded `authenticate`,
|
|
103
|
-
`context`, `resolveUser`, `mintAccessToken`, `clientStore`, etc. — any of
|
|
104
|
-
them throwing turned `.fetch()` into a rejected promise instead of a
|
|
105
|
-
`Response`, with the fallout depending on the runtime (a generic error page
|
|
106
|
-
on Workers, a hung socket with no response ever sent on raw
|
|
107
|
-
`http.createServer`). `createMcpHandler`, `createOAuthRouter`,
|
|
108
|
-
`createMcpApp`, and `asNodeHandler` now each catch at their own boundary and
|
|
109
|
-
return a real `500` instead.
|
|
110
|
-
|
|
111
|
-
- **RFC 7009 token revocation.** The library never stores access or refresh
|
|
112
|
-
tokens, so revocation is a host port — `revokeToken` — advertised and mounted
|
|
113
|
-
only when implemented (same honest-advertisement pattern as refresh).
|
|
114
|
-
Well-formed authenticated `POST /revoke` returns 200 even if the token is
|
|
115
|
-
unknown (`invalid_request` 400 and `invalid_client` 401 still apply).
|
|
116
|
-
Stolen-token invalidation lives in the host store that `verifyToken` and
|
|
117
|
-
`refreshAccessToken` consult.
|
|
118
|
-
|
|
119
|
-
- **Claims on `Principal`.** Optional `claims` from `verifyToken` /
|
|
120
|
-
`VerifiedToken` flow into `context` (plan/role metadata — not the
|
|
121
|
-
Host→tenant isolation key). Host-origin allowlisting (`allowedOrigins` on
|
|
122
|
-
`asNodeHandler`) is **required** when origin is Host-derived. Separately,
|
|
123
|
-
browser `Origin` on MCP is gated by `allowedRequestOrigins` (fail-closed
|
|
124
|
-
when the header is present; native clients omit it).
|
|
125
|
-
|
|
126
|
-
- **`validateArgs` fail-fast.** With `validateArgs: true`, unsupported JSON
|
|
127
|
-
Schema keywords throw at `createToolRegistry` construction instead of
|
|
128
|
-
silently looking enforced. Default (off) is unchanged.
|
|
129
|
-
|
|
130
|
-
**Phase 2 — the authorize endpoint (0.2.2 / 0.3.0).**
|
|
131
|
-
|
|
132
|
-
- **Consent step at `/authorize`.** A resolved session no longer issues a
|
|
133
|
-
code and redirects on its own; it renders an approval interstitial (built
|
|
134
|
-
in, or your own via `consent.render`) and only issues a code after an
|
|
135
|
-
explicit `POST ${oauthPath}/consent` approval. Closes a cross-site flow
|
|
136
|
-
where an attacker-controlled page could otherwise walk a logged-in user
|
|
137
|
-
through `/authorize` with an attacker-chosen `client_id`/`code_challenge`
|
|
138
|
-
and a loopback `redirect_uri`, catching the code with a local listener —
|
|
139
|
-
PKCE alone does not defend against this, it proves possession of the
|
|
140
|
-
verifier, not the identity of a legitimate client. Approval tickets are
|
|
141
|
-
AES-GCM sealed (`sealed.ts`), single-use, and re-checked against the
|
|
142
|
-
resolved user at redemption.
|
|
143
|
-
- **DCR `client_id` is bound to its own `redirect_uris`**, zero storage — the
|
|
144
|
-
id issued by `/register` is a sealed, self-verifying assertion of the list
|
|
145
|
-
it registered. Stops a substituted `redirect_uri` on a legitimate
|
|
146
|
-
connector's id (not impersonation via self-registration — the consent step
|
|
147
|
-
above is what stops that). `requireRegisteredClients` (default `true`
|
|
148
|
-
since 1.0) is the stricter default, and a direct stepping stone to CIMD below.
|
|
149
|
-
- **`codeSecret` is validated** — minimum 32 characters, and the literal
|
|
150
|
-
values this package's own docs/examples publish are denylisted by name.
|
|
151
|
-
It is the single key that can forge an auth code, consent ticket, or DCR
|
|
152
|
-
client assertion for any userId/scope/resource, and had no validation at
|
|
153
|
-
all before this.
|
|
154
|
-
- **Loopback redirect allowlist narrowed** — `localhost` and `https:`
|
|
155
|
-
loopback are no longer accepted (RFC 8252 §8.3); a `redirect_uri` carrying
|
|
156
|
-
a fragment or embedded credentials is rejected for every predicate, not
|
|
157
|
-
only loopback.
|
|
158
|
-
- **`defaultScopes` required for multi-scope servers.** `createOAuthRouter`
|
|
159
|
-
now throws at construction when `scopes` has more than one entry and
|
|
160
|
-
`defaultScopes` is unset, instead of silently granting the full advertised
|
|
161
|
-
set to a client that asked for nothing.
|
|
162
|
-
- **`timingSafeEqual("", "")` now returns `false`**, not `true` — a public
|
|
163
|
-
export, so a host comparing a presented secret against an unset stored one
|
|
164
|
-
no longer authenticates.
|
|
165
|
-
- **Request body size limits** on `/mcp`, `/token`, `/revoke`, and
|
|
166
|
-
`/register` — previously unbounded on every one of them.
|
|
167
|
-
|
|
168
|
-
**Phase 3 — credentials and correctness (0.4.0).**
|
|
169
|
-
|
|
170
|
-
- **Auth codes sealed (AES-GCM), not signed-only.** v1 (HMAC) codes were
|
|
171
|
-
readable by anyone who saw one; v2 codes are encrypted too. Soft
|
|
172
|
-
migration in 0.4.0 / 0.5.0 (read both, write v2); **1.0.0 drops v1 reading**.
|
|
173
|
-
- **`ClientStore.secretHash`**, preferred over `verifySecret`: return a
|
|
174
|
-
stored hash from the new `hashClientSecret` and the library does the
|
|
175
|
-
comparison with the same constant-time primitive it uses everywhere
|
|
176
|
-
else. Keyed by `codeSecret` — no second secret to manage.
|
|
177
|
-
- **Client-auth enumeration oracle closed.** `/token` and `/revoke`
|
|
178
|
-
collapsed five distinguishable error messages into one; a locked-down
|
|
179
|
-
server also pays a fixed-cost dummy comparison for an unknown
|
|
180
|
-
`client_id`, so response timing can't distinguish "unknown" from
|
|
181
|
-
"known, wrong secret" either. Unaffected: public-client traffic, and
|
|
182
|
-
`/authorize`'s deliberately-still-descriptive `unauthorized_client`.
|
|
183
|
-
- **`/revoke` accepts JSON**, matching `/token` — previously form-only,
|
|
184
|
-
so a JSON revoke silently failed while the caller believed it worked.
|
|
185
|
-
- **`apiTool` no longer forwards the upstream error body by default** —
|
|
186
|
-
status only; opt in via the new `onError`.
|
|
187
|
-
- **`missingObjectType`** — a schema using `properties`/`required`
|
|
188
|
-
without `type: "object"` now throws at construction under
|
|
189
|
-
`validateArgs: true`, and is enforced at runtime by dispatching on the
|
|
190
|
-
value's actual shape rather than trusting a schema author remembered to
|
|
191
|
-
declare `type`.
|
|
192
|
-
- **Union `type` arrays** (`type: ["string", "null"]`) now evaluate
|
|
193
|
-
correctly — previously always failed.
|
|
194
|
-
- **`/authorize` errors redirect to the client's callback** (RFC 6749
|
|
195
|
-
§4.1.2.1) once `redirect_uri` is validated, instead of a bare JSON body
|
|
196
|
-
the connector never parses — this was silently connector-breaking.
|
|
197
|
-
|
|
198
|
-
**Phase 4 — protocol correctness and observability (0.5.0).**
|
|
199
|
-
|
|
200
|
-
- **`jsonrpc` validated on every `/mcp` request** — `-32600` on anything
|
|
201
|
-
but exactly `"2.0"`, including a missing field. Previously accepted
|
|
202
|
-
silently.
|
|
203
|
-
- **`MCP-Protocol-Version` header read and validated**, per the
|
|
204
|
-
2025-06-18 spec — unsupported → 400, missing → assumed `2025-03-26`
|
|
205
|
-
(the spec's own compatibility default), not checked on `initialize`
|
|
206
|
-
(which is what negotiates the version). Previously only appeared in the
|
|
207
|
-
CORS allow-list, never actually read.
|
|
208
|
-
- **`initialize` defaults to the newest supported protocol version**
|
|
209
|
-
(`2025-06-18`) for a missing/unrecognized request, not the oldest.
|
|
210
|
-
- **401 responses echo the real request id** instead of hardcoding `null`.
|
|
211
|
-
- **`/mcp/` routes identically to `/mcp`.** `canonicalResource` normalizes
|
|
212
|
-
its `resourcePath` argument internally, so route matching, `/authorize`,
|
|
213
|
-
and `/token` all agree — previously only some call sites normalized a
|
|
214
|
-
trailing slash and others read a raw, unnormalized value.
|
|
215
|
-
- **`createOAuthRouter` refuses to construct** if `oauthPath` would equal
|
|
216
|
-
`resourcePath` (they'd shadow each other) or if `resourcePath` starts
|
|
217
|
-
with `/.well-known` (reserved for discovery documents).
|
|
218
|
-
- **`auditTimeoutMs`** (default 1000ms) — a hanging `ports.audit` can no
|
|
219
|
-
longer stall a response indefinitely; it already couldn't fail one.
|
|
220
|
-
- **`OAuthPorts.audit` / `McpAppAuth.audit`** — the OAuth-side twin of the
|
|
221
|
-
MCP audit hook. Sees the real reason behind a collapsed `invalid_client`
|
|
222
|
-
or a rejected `codeSecret`, which the caller never does.
|
|
223
|
-
|
|
224
|
-
**2.0 — CIMD, unified audit, recipes.**
|
|
225
|
-
|
|
226
|
-
- **CIMD (Client ID Metadata Documents)** — opt-in (`cimd: true`). HTTPS URL
|
|
227
|
-
`client_id`s are fetched and validated with SSRF hardening (public hosts
|
|
228
|
-
only, size / redirect / timeout caps). Advertises
|
|
229
|
-
`client_id_metadata_document_supported`. Optional `cimdCache` port.
|
|
230
|
-
Off by default until connect-pinned fetch closes the DNS-rebinding ceiling.
|
|
231
|
-
- **Unified `audit` sink** on `createMcpApp` — one callback for MCP and
|
|
232
|
-
OAuth events (`source: "mcp" | "oauth"`); `auth.audit` remains an
|
|
233
|
-
OAuth-only override.
|
|
234
|
-
- **Framework recipes** — `examples/nextjs-saas/`, `examples/express.ts`.
|
|
235
|
-
- **Reference MCP Registry server** — `examples/project-desk/`
|
|
236
|
-
(`io.github.amir1824/project-desk`, remotes only; not the SDK package).
|
|
237
|
-
- Fail-closed defaults: `allowInMemoryCodeStore` / `validateArgs`.
|
|
238
|
-
- **RFC 9207 `iss`** on authorize / error redirects; AS metadata advertises
|
|
239
|
-
`authorization_response_iss_parameter_supported`.
|
|
33
|
+
second package fails. `npm run check:quickstart` enforces the line count.
|
|
240
34
|
|
|
241
35
|
---
|
|
242
36
|
|
|
@@ -244,10 +38,11 @@ a proper JSON-RPC error.
|
|
|
244
38
|
|
|
245
39
|
### 1. Protocol currency — `2026-07-28`
|
|
246
40
|
|
|
247
|
-
Deferred deliberately:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
is worse than useless, so this becomes mandatory once
|
|
41
|
+
Deferred deliberately: the revisions implemented today (`2024-11-05`,
|
|
42
|
+
`2025-03-26`, `2025-06-18`) are what clients negotiate with this library, see
|
|
43
|
+
[compatibility.md](compatibility.md). But a batteries-included server that
|
|
44
|
+
speaks an outdated revision is worse than useless, so this becomes mandatory once
|
|
45
|
+
real clients move.
|
|
251
46
|
|
|
252
47
|
Work: remove `initialize` / `ping`, add `server/discover` (MUST), carry protocol
|
|
253
48
|
version and client capabilities in `_meta` per request, `resultType` on every
|
|
@@ -276,10 +71,3 @@ Backed by the spec, not just by our own scoping:
|
|
|
276
71
|
|
|
277
72
|
Deferred until asked: MRTR (`input_required`), `subscriptions/listen`,
|
|
278
73
|
`io.modelcontextprotocol/tasks`.
|
|
279
|
-
|
|
280
|
-
## Known gaps
|
|
281
|
-
|
|
282
|
-
Tracked, not scheduled: `pruneMemory` scans the whole jti map on every code
|
|
283
|
-
consume; `mcpWwwAuthenticate` and `wwwAuthenticateHeader` build the same
|
|
284
|
-
header twice. (Unsupported JSON Schema keywords under `validateArgs: true`
|
|
285
|
-
now throw at `createToolRegistry` construction — no longer a silent gap.)
|
package/docs/compatibility.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Compatibility evidence
|
|
2
2
|
|
|
3
|
-
Updated 2026-
|
|
3
|
+
Updated 2026-11-07. Repository implementation and automated coverage are in good
|
|
4
4
|
shape after the pre-prod security pass. **Vendor-client UI sessions are still
|
|
5
5
|
Pending** — do not convert product rows to ✅ until a live Claude/ChatGPT/Codex/Gemini
|
|
6
6
|
check below is filled in.
|
|
@@ -8,7 +8,7 @@ check below is filled in.
|
|
|
8
8
|
| Product | Server profile | Repository evidence | Live evidence |
|
|
9
9
|
|---|---|---|---|
|
|
10
10
|
| Claude custom connector | `claude`: DCR / CIMD, public client, S256 PKCE; built-in callback | `test/oauth/policy/clients.test.ts`, `test/oauth/endpoints/dcr.test.ts`, `test/oauth/cimd/cimd.test.ts`, `test/oauth/crypto/pkce.test.ts`; saas-demo e2e + `scripts/prep-claude-oauth.mjs` | Pending (UI) |
|
|
11
|
-
| ChatGPT | `codex`: public-client profile;
|
|
11
|
+
| ChatGPT | `codex`: public-client profile; ChatGPT's stable callback and its per-connector `https://chatgpt.com/connector/oauth/{id}` callback are allowed | Profile tests only; no ChatGPT UI test | Pending |
|
|
12
12
|
| Codex | `codex`: public client, PKCE, loopback callbacks allowed | Profile/redirect tests; no actual Codex login run | Pending |
|
|
13
13
|
| Gemini Enterprise | `gemini`: `clientStore`, pre-registration, `client_secret_basic` / `client_secret_post` | Client authentication tests; no Enterprise environment | Pending |
|
|
14
14
|
|
|
@@ -53,7 +53,19 @@ release; it does **not** replace the live client row.
|
|
|
53
53
|
|
|
54
54
|
| Date | Commit / package | Command | Result |
|
|
55
55
|
|---|---|---|---|
|
|
56
|
-
| 2026-
|
|
56
|
+
| 2026-11-07 | `3.1.0` release-branch working tree | `npm run smoke:pack` | Pass — `createMcpApp` + `asNodeHandler` import from tarball |
|
|
57
|
+
|
|
58
|
+
## Production use (maintainer-reported)
|
|
59
|
+
|
|
60
|
+
The maintainer reports that two of their own production MCP servers run on
|
|
61
|
+
mcp-trellis and are connected to Claude. On 2026-11-07 a Claude session called a
|
|
62
|
+
read tool on each of them over its connector and got a normal result (an empty
|
|
63
|
+
list from one, 14 note paths from the other).
|
|
64
|
+
|
|
65
|
+
This is **not** a logged live pass. The Claude build and plan, the mcp-trellis
|
|
66
|
+
version those servers run, and the discovery, authorize and token exchange were
|
|
67
|
+
not recorded, and it says nothing about ChatGPT, Codex or Gemini. The "Live
|
|
68
|
+
evidence" column above stays Pending until a pass is recorded in the table below.
|
|
57
69
|
|
|
58
70
|
## Evidence required for a live pass
|
|
59
71
|
|
package/docs/reference.md
CHANGED
|
@@ -107,6 +107,7 @@ entry — those are protocol errors, not auth denials.
|
|
|
107
107
|
| `extra` | `[]` | Exact-match redirect URIs |
|
|
108
108
|
| `allowLoopback` | `true` | Allow `http:` loopback — `127.0.0.1`, `[::1]` (any port). `localhost` and `https:` loopback are not accepted — see [security.md](security.md) |
|
|
109
109
|
| `allowClaude` | `true` | Allow the Claude.ai Custom Connector callback |
|
|
110
|
+
| `allowChatgptConnector` | `false` | Allow ChatGPT's per-connector callback `https://chatgpt.com/connector/oauth/{id}` (one `[A-Za-z0-9_-]{1,128}` id, exact string match). `createMcpApp` turns it on for `clients: ["codex"]` |
|
|
110
111
|
|
|
111
112
|
Pre-registered clients, and DCR-issued clients whose id was sealed by this
|
|
112
113
|
server's own `/register`, bypass this allowlist entirely — they are
|
|
@@ -157,7 +158,7 @@ A single-scope server has no such ambiguity and is unaffected.
|
|
|
157
158
|
- `defineTool`, `apiTool` — typed, validated tool authoring on top of `ToolDef`. `apiTool`'s `timeoutMs` (default 30000, or `false` to disable) and `maxResponseBytes` (default 1 MiB) bound the upstream call; either surfaces as `isError: true`, not a thrown exception. `redirect` (`"manual"` by default, `"error"`, or `"follow"`) controls upstream redirects — see the guide before enabling `"follow"`. An abort on the `Request` your `request` callback returns surfaces as `Request was cancelled`
|
|
158
159
|
- `CLIENT_PROFILES`, `DEFAULT_CLIENTS`, `authMethodsFor`, `redirectUrisFor`, `preRegisteredClients`, `hasDynamicClient`
|
|
159
160
|
- `parseBearer`, `wwwAuthenticateHeader`, `rejectQueryToken`
|
|
160
|
-
- `signedTokenAuth` — one-secret convenience `McpAppAuth` (HMAC access tokens via HKDF;
|
|
161
|
+
- `signedTokenAuth` — one-secret convenience `McpAppAuth` (HMAC access tokens via HKDF). `secret` may be a list for rotation (first signs, all verify). Opt-in: `isUserActive(userId)` runs on every verified token and every refresh; `refreshTtlSeconds` adds single-use rotating refresh tokens and `/revoke` for them (needs `codeStore` and `isUserActive`). Access tokens are not revocable before `ttlSeconds`. Type `SignedTokenAuthOptions`
|
|
161
162
|
- `rpcResult`, `rpcError`, JSON-RPC error constants
|
|
162
163
|
- `PROTOCOL_VERSIONS`, `DEFAULT_PROTOCOL_VERSION`
|
|
163
164
|
- `jsonResponse({ data, status?, headers?, cors? })` — type `JsonResponseInput` exported
|
|
@@ -197,7 +198,7 @@ Building blocks for hosts assembling their own transport or OAuth flow. Less sta
|
|
|
197
198
|
- `buildErrorRedirectUrl` — the RFC 6749 §4.1.2.1 error-redirect builder `/authorize` uses
|
|
198
199
|
- Types: `AuthCodeRecord`, `CodeStore`, `ClientAuth`
|
|
199
200
|
|
|
200
|
-
**Deprecated in 2.1:** every symbol above is still importable from `mcp-trellis` or `mcp-trellis/oauth`, where it used to live, as a `@deprecated` alias. Those aliases are removed in
|
|
201
|
+
**Deprecated in 2.1:** every symbol above is still importable from `mcp-trellis` or `mcp-trellis/oauth`, where it used to live, as a `@deprecated` alias. Those aliases are removed in the next major release.
|
|
201
202
|
|
|
202
203
|
</details>
|
|
203
204
|
|
package/docs/security.md
CHANGED
|
@@ -33,6 +33,7 @@ This is a **library** threat model, not a third-party audit badge.
|
|
|
33
33
|
| Scope escalation at authorize | Requested `scope` is validated against the advertised `scopes` and rejected with `invalid_scope`; the grant is bound into the auth code and handed to `mintAccessToken` |
|
|
34
34
|
| An unscoped tool silently callable by any authenticated principal, including one holding none of the advertised scopes — the gap `defaultScopes: []` alone doesn't close, since that only governs the token grant, not per-tool enforcement | `createMcpApp` refuses to construct once `scopes` has more than one entry if any tool omits `ToolDef.scope` — pass `scope: null` to state the omission on purpose. A single-scope server has no such ambiguity and is unaffected. `tools/call` itself always enforces whatever `scope` a tool does declare, independent of this guard (403 `insufficient_scope`) |
|
|
35
35
|
| Scope reduction lost on refresh | Optional `scope` on the refresh token request is validated against advertised scopes and forwarded on `RefreshAccessTokenInput.scope`. **`refreshAccessToken` MUST enforce ⊆ originally granted scopes** (opaque RT — same pattern as `resource`). The token response omits `scope` when neither the port nor the grant supplies one (RFC 6749 §5.1) — clients must not assume the field is always present |
|
|
36
|
+
| A removed or disabled user keeps a working connector | `signedTokenAuth` tokens are stateless, so they outlive the login session. `isUserActive` closes that; see [Sessions and expiry](#sessions-and-expiry) |
|
|
36
37
|
| Stolen access or refresh token | Host `revokeToken` (denylist or equivalent) that `verifyToken` and `refreshAccessToken` consult. Unknown / wrong-client tokens MUST no-op, not throw. Well-formed authenticated revoke is 200 even if the token is unknown |
|
|
37
38
|
| Stolen confidential-client secret | `clientStore.verifySecret` owns comparison — store hashes, not plaintext. The library never sees or persists credentials. **Prefer `clientStore.secretHash`** instead: return a stored hash from `hashClientSecret` and the library does the comparison with the same constant-time primitive it uses everywhere else, rather than a hand-rolled `stored === presented` in your own `verifySecret` being a timing oracle with no warning |
|
|
38
39
|
| An unauthenticated caller enumerating which `client_id`s a locked-down server knows, via distinguishable `/token`/`/revoke` error text or response timing | Every client-auth failure at `/token` and `/revoke` collapses to one `invalid_client` / "client authentication failed" — previously up to five distinguishable messages named exactly why (wrong method, missing secret, misconfigured store, unknown id). Every rejection path in `firstClientAuthError` for a client_id that *is* known — wrong auth method, no secret presented, wrong secret, no `secretHash`/`verifySecret` on file — runs the same fixed-cost dummy or real comparison before returning, so response timing alone can't tell "known client, wrong method" apart from "known client, wrong secret." An unknown `client_id` on a locked-down server (`allowUnregisteredClients: false`) pays the identical fixed cost, so the crypto-verification cost itself can't distinguish "unknown" from "known, wrong secret" either. `/authorize`'s `unauthorized_client` is unchanged and stays descriptive — a human debugging a browser flow, not a bare API response, and a locked-down server already announces the same fact by omitting `registration_endpoint` |
|
|
@@ -41,10 +42,10 @@ This is a **library** threat model, not a third-party audit badge.
|
|
|
41
42
|
| `apiTool` as a credential-forwarding / SSRF primitive via upstream redirects | Redirects are not followed by default (`redirect: "manual"`): `fetch` would forward custom auth headers (`X-Api-Key`) and a 307 body to the `Location` and could fetch an internal address, returning it to the model. `"follow"` is an explicit opt-in for a fully trusted upstream |
|
|
42
43
|
| A tool call that amplifies a small request into a huge validation error | Validation stops after 20 violations and the message is capped at 2000 characters; audit copies of tool errors are capped at 1000 |
|
|
43
44
|
| `/authorize` errors delivered as a bare JSON body a connector can't act on | RFC 6749 §4.1.2.1: once `redirect_uri` is validated, every remaining error (`unsupported_response_type`, PKCE, `invalid_scope`, `invalid_target`, …) redirects to the client's own callback with `?error=…&state=…` instead. `redirect_uri` validity itself, and an oversized `state`, stay direct JSON — the URI is what's in question in the first case, and reflecting an oversized `state` into its own error redirect would recreate the DoS the length cap exists to prevent |
|
|
44
|
-
| DCR-issued `client_id` reused with a substituted `redirect_uri` | `/register` no longer returns a bare random id — the `client_id` **is** a sealed, self-verifying assertion of the exact `redirect_uris` it registered (`sealed.ts`, type `"client"`), with zero server-side storage. `/authorize` unseals it and binds the request to that list, the same protection a stored registration would give. This does not require the *identity* of the client to be verified — an attacker can still call `/register` and mint their own sealed id — so it stops URI substitution on a legitimate connector's id, not impersonation; the consent step above is what stops impersonation. `requireRegisteredClients` defaults to **true** since 1.0 (reject any `client_id` that isn't `clientStore`-resolved or self-sealed); set `false` only to accept invented public ids. Enforced at `/authorize`, and — since
|
|
45
|
+
| DCR-issued `client_id` reused with a substituted `redirect_uri` | `/register` no longer returns a bare random id — the `client_id` **is** a sealed, self-verifying assertion of the exact `redirect_uris` it registered (`sealed.ts`, type `"client"`), with zero server-side storage. `/authorize` unseals it and binds the request to that list, the same protection a stored registration would give. This does not require the *identity* of the client to be verified — an attacker can still call `/register` and mint their own sealed id — so it stops URI substitution on a legitimate connector's id, not impersonation; the consent step above is what stops impersonation. `requireRegisteredClients` defaults to **true** since 1.0 (reject any `client_id` that isn't `clientStore`-resolved or self-sealed); set `false` only to accept invented public ids. Enforced at `/authorize`, and — since 2.0.0 — at `/token`'s `refresh_token` grant and at `/revoke` too, closing a gap where an invented `client_id` that never went through `/authorize` could still authenticate on either (the `authorization_code` grant was never actually exposed by this: a redeemable code's `client_id` is bound into its sealed payload at `/authorize` time, where the check already ran). Opt into CIMD with `cimd: true` for URL-shaped client_ids (off by default) |
|
|
45
46
|
| CIMD fetch as SSRF proxy (attacker supplies `https://…` `client_id`) | **Off by default** (`cimd: true` to enable). When on: HTTPS-only fetch, private/loopback/link-local/NAT64/`::ffff:` blocked (incl. bracketed IPv6 **and full-form mapped encodings DNS may return**, e.g. `0:0:0:0:0:ffff:7f00:1`), DNS lookup required before fetch and re-checked after the body, unparseable IPv6 lookup results fail closed, 5 KB stream-capped body / 10 s per hop covering the body read / 3 same-origin redirects (cross-origin redirects rejected), public clients only, `http:` redirect_uris loopback-only (`127.0.0.1` / `[::1]`, never `localhost`). Also blocked: `192.0.0/24`, `198.18/15`, multicast/reserved `224/4`–`240/4`, IPv6 multicast `ff00::/8`, 6to4 `2002::/16`, IPv4-compatible `::a.b.c.d` (and full-form private embeds). `/token` and `/revoke` **re-resolve** CIMD — never accept URL-shaped ids by shape alone; pass `cimdCache` (strongly recommended) or every refresh/revoke re-fetches the document, and a failed resolve is audited as `cimd_resolve_failed` at `/authorize`, `/token` and `/revoke` with a reason code (`http_status: status 403 (authorize)`; never an address, header, body or redirect target) while the connector gets one generic `unauthorized_client`. **Ceiling:** TCP connect is not pinned to the looked-up addresses (zero-dep Web `fetch`); classic DNS rebinding between lookup and connect remains possible until a custom dispatcher pins the IP — do not treat CIMD as production-complete SSRF defense until then |
|
|
46
47
|
| Naming only confidential clients doesn't actually block public ones | `clients: ["gemini"]` (or `allowUnregisteredClients: false`) is **enforced**: DCR unmounted, dropped from AS metadata, unresolved `client_id` rejected with `unauthorized_client` at `authorize` and `invalid_client` at `token` and `revoke` |
|
|
47
|
-
| DCR / token-endpoint abuse | Edge or reverse-proxy rate limiting on `/register` and `/token`, **plus** a library-enforced request body cap (see "Also designed in") — the library does not rate-limit by request count, but an unbounded body was previously a memory-exhaustion vector on its own regardless of rate |
|
|
48
|
+
| DCR / token-endpoint abuse | Edge or reverse-proxy rate limiting on `/register` and `/token` — and on `/authorize` when `cimd: true`, where each unresolvable `client_id` costs an outbound fetch and one `cimd_resolve_failed` audit entry — **plus** a library-enforced request body cap (see "Also designed in") — the library does not rate-limit by request count, but an unbounded body was previously a memory-exhaustion vector on its own regardless of rate |
|
|
48
49
|
| A weak or copy-pasted `codeSecret` (it can forge an auth code — or, since 0.3.0, a consent ticket or DCR client assertion — for any userId/scope/resource) | Validated at the point of use: a string or array form throws at `createOAuthRouter` construction (every array entry checked, and the array itself must be non-empty), a function form on every call. Minimum 32 characters per entry, and the exact literals this package's own docs and examples have published are denylisted by name, as is any secret containing the examples' `do-not-reuse` marker — copy-paste is the realistic failure mode. The examples themselves read `MCP_SECRET` (or `OAUTH_CODE_SECRET` for the advanced multi-secret recipes) from the environment and ship no usable default, and a test fails if any `codeSecret` literal in `examples/`, `README.md` or `docs/` is one the library would accept. This only catches the obvious cases; a leaked or brute-forceable 32+ character secret is still a full bypass, same as before |
|
|
49
50
|
| An operator unable to diagnose *why* a client was rejected, once `/token`/`/revoke` collapsed every failure to one generic `invalid_client` (see the enumeration-oracle row above) | `OAuthPorts.audit` (and `McpAppAuth.audit` on `createMcpApp`) — opt-in, sees the real reason (`"client requires client_secret_basic, got none"`, a rejected `codeSecret`'s actual message, …) that never reaches the caller. Same "never fails the request" guarantee as the MCP-side `audit` port |
|
|
50
51
|
| A hanging `ports.audit` sink stalling every audited response indefinitely | `auditTimeoutMs` (default 1000ms) on both `McpHandlerOptions` and `OAuthRouterOptions` races the audit call against a timeout; "can't fail a request" already held, this adds "can't delay one" past a bound. A slow sink keeps running in the background and its own internal `catch` means it can never surface as an unhandled rejection later |
|
|
@@ -53,15 +54,68 @@ This is a **library** threat model, not a third-party audit badge.
|
|
|
53
54
|
|
|
54
55
|
**Known limits:** in-memory jti map is process-local (multi-instance needs `codeStore`); the loopback redirect allowlist permits any path or port on `127.0.0.1`/`[::1]` over `http:` only — `localhost` and `https:` loopback are deliberately not accepted (RFC 8252 §8.3: `localhost` is DNS-resolvable, not a loopback guarantee, and native-app loopback redirects are `http:` only); DCR binds a client to its own `redirect_uris` via a sealed assertion but does not verify the *registrant's identity* (opt into CIMD with `cimd: true`); CIMD DNS checks do not pin the TCP connect (see CIMD SSRF row).
|
|
55
56
|
|
|
57
|
+
## Sessions and expiry
|
|
58
|
+
|
|
59
|
+
The connector holds a token, never the app's login cookie. What each event does
|
|
60
|
+
to it, with `signedTokenAuth`:
|
|
61
|
+
|
|
62
|
+
| Event | What the connector sees |
|
|
63
|
+
|-------|-------------------------|
|
|
64
|
+
| The app session ends (cookie expires, logout) | Nothing. The next `/authorize` redirects to `loginUrl`; a token already issued keeps working |
|
|
65
|
+
| The access token reaches its `ttlSeconds` (default 1 h) | `401` with `WWW-Authenticate: Bearer …, error="invalid_token"` (one code for a bad signature, expiry, wrong audience or an inactive user). With a refresh token the client renews silently; without one the user goes through the browser again |
|
|
66
|
+
| The user is removed, disabled or signed out everywhere | With `isUserActive`: `401` on the next MCP request, `invalid_grant` on the next refresh. Without it: the access token works until it expires. `isUserActive` runs on every request, only `true` passes, a throw is a `500`; keep it cheap or cached, a call that hangs stalls the request |
|
|
67
|
+
| The user's role or scopes change | The token keeps the scopes granted at issue until it expires. Enforce current permissions in the data layer |
|
|
68
|
+
| The user sits on the consent page over 5 minutes, the session ends before Approve, or Approve is posted twice | A short page in the browser (JSON for a caller that does not ask for `text/html`), status `400`. `consent.render` customizes the consent screen only |
|
|
69
|
+
| `secret` becomes a list `[new, old]` | Old tokens and DCR `client_id`s keep working, new ones are signed with `new`. Remove `old` once its tokens have expired; until then it can still forge |
|
|
70
|
+
| `initialize` and `ping` | Succeed with no token or a bad one, so a "connected" status proves nothing. A `GET /mcp` with an `Authorization` header is answered `405` without checking it. Only `tools/list` and `tools/call` check the token |
|
|
71
|
+
|
|
72
|
+
Access tokens are bearer tokens: not bound to a client and not revocable before
|
|
73
|
+
they expire, so keep `ttlSeconds` short.
|
|
74
|
+
|
|
75
|
+
**Refresh (`refreshTtlSeconds`).** Off unless set. Refresh tokens are signed, not
|
|
76
|
+
stored; the only state is a single-use mark per token in `codeStore`.
|
|
77
|
+
|
|
78
|
+
- **Lifetime.** Absolute: fixed when the authorization code is redeemed and never extended by refreshing, so
|
|
79
|
+
the user approves again after `refreshTtlSeconds`. The last access token issued
|
|
80
|
+
can still run up to `ttlSeconds` past that.
|
|
81
|
+
- **Single use.** Each refresh token works once and is replaced. `codeStore.consume`
|
|
82
|
+
must be atomic (Redis `SET NX EX`, a unique-key insert) with a TTL no shorter than
|
|
83
|
+
the token. The process-local store does not hold across instances and Workers KV
|
|
84
|
+
is not atomic, so neither gives single use. Pass the same `codeStore` instance to
|
|
85
|
+
`signedTokenAuth` as the router uses; it refuses to start without one. One mark
|
|
86
|
+
per refresh stays until the grant's expiry, so size the store and rate-limit
|
|
87
|
+
`/token` per `client_id`.
|
|
88
|
+
- **Binding.** To the user, client, resource and scopes of the consent. A refresh
|
|
89
|
+
may narrow scope, never widen it. Narrowing applies to that one access token: the
|
|
90
|
+
successor refresh token keeps the original grant's scopes, so the next refresh can
|
|
91
|
+
ask for them again (RFC 6749 §6). Every failure is one `invalid_grant`, and every
|
|
92
|
+
check that depends on the caller's input runs before the token is burned, so a
|
|
93
|
+
wrong client cannot spend the rightful holder's token.
|
|
94
|
+
- **Size.** A refresh token is capped at 4096 characters. Issuing a longer one (a
|
|
95
|
+
user id of roughly 2.8 KB of ASCII, or a very long scope list) throws: at the
|
|
96
|
+
first issuance that is a `500` at `/token` after the authorization code was
|
|
97
|
+
spent, on every login for that user. Keep user ids short.
|
|
98
|
+
- **No grace window.** A client that loses a refresh response and retries is logged
|
|
99
|
+
out and re-authorizes, as it is if minting fails after the old token was burned.
|
|
100
|
+
- **No token family.** Replaying a used token is refused, but the successor a thief
|
|
101
|
+
obtained first stays valid until its expiry or until `isUserActive` turns false.
|
|
102
|
+
For family revocation use an opaque server-side store
|
|
103
|
+
(`examples/project-desk/src/tokens.ts`).
|
|
104
|
+
- **`/revoke`** burns a refresh token only and answers `200` for anything else,
|
|
105
|
+
including an access token. A store failure on a valid refresh token is a `500`, so
|
|
106
|
+
revoking the current refresh token never reports success while it still works.
|
|
107
|
+
Revoking one that was already replaced by a refresh is a no-op: it answers `200`
|
|
108
|
+
and the successor keeps working (see "No token family").
|
|
109
|
+
|
|
56
110
|
## Rotating codeSecret
|
|
57
111
|
|
|
58
112
|
`codeSecret` is the one secret behind auth codes, consent tickets, DCR
|
|
59
113
|
client assertions, and (when `clientStore.secretHash` is used)
|
|
60
|
-
confidential-client secret hashes. Before
|
|
114
|
+
confidential-client secret hashes. Before 2.0.0 it was a single string —
|
|
61
115
|
rotating it invalidated every DCR-issued `client_id` still in a connector's
|
|
62
116
|
hands and every stored `secretHash`, with no way to phase the change in.
|
|
63
117
|
|
|
64
|
-
Since
|
|
118
|
+
Since 2.0.0, `codeSecret` also accepts an array:
|
|
65
119
|
|
|
66
120
|
```ts
|
|
67
121
|
codeSecret: [newSecret, oldSecret],
|