mcp-trellis 2.1.0 → 2.2.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 +41 -39
- package/dist/auth/signed-token.d.ts +26 -0
- package/dist/auth/signed-token.d.ts.map +1 -0
- package/dist/auth/signed-token.js +96 -0
- package/dist/auth/signed-token.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/docs/ROADMAP.md +3 -1
- package/docs/compatibility.md +13 -11
- package/docs/diagrams/demo-tool-result.png +0 -0
- package/docs/guide.md +11 -5
- package/docs/reference.md +1 -0
- package/docs/security.md +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
# mcp-trellis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add authenticated AI connectors to your existing SaaS.
|
|
4
|
+
Reuse your users, login and permissions. Trellis handles MCP + OAuth.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
```
|
|
7
|
+
Existing SaaS
|
|
8
|
+
users + login + data
|
|
9
|
+
↓
|
|
10
|
+
mcp-trellis
|
|
11
|
+
↓
|
|
12
|
+
Claude / ChatGPT / Gemini
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
verification and data permissions.
|
|
15
|
+
[](https://www.npmjs.com/package/mcp-trellis)
|
|
16
|
+
[](https://github.com/amir1824/mcp-trellis/actions/workflows/ci.yml)
|
|
17
|
+
[](LICENSE)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
flowchart LR
|
|
20
|
-
A[Existing app: login + user data] --> B[mcp-trellis: OAuth + MCP]
|
|
21
|
-
B --> C[AI client: user-scoped tools]
|
|
22
|
-
```
|
|
19
|
+
## Demo
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+

|
|
25
22
|
|
|
26
23
|
```bash
|
|
27
24
|
npm install mcp-trellis
|
|
28
25
|
```
|
|
29
26
|
|
|
27
|
+
## The 30-second example
|
|
28
|
+
|
|
30
29
|
```ts
|
|
31
|
-
import { createMcpApp } from "mcp-trellis";
|
|
32
|
-
import {
|
|
30
|
+
import { createMcpApp, signedTokenAuth } from "mcp-trellis";
|
|
31
|
+
import { session, projectsForUser, codeStore } from "./your-app.js";
|
|
33
32
|
|
|
34
33
|
export const mcp = createMcpApp<{ userId: string }>({
|
|
35
34
|
serverInfo: { name: "my-saas", version: "1.0.0" },
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
scope: "mcp",
|
|
45
|
-
handler: async ({ userId }) => JSON.stringify(await projectsForUser(userId)),
|
|
46
|
-
}],
|
|
35
|
+
auth: signedTokenAuth({
|
|
36
|
+
secret: process.env.MCP_SECRET!,
|
|
37
|
+
resolveUser: (req) => session(req),
|
|
38
|
+
loginUrl: (_r, n) => `/login?next=${encodeURIComponent(n)}`,
|
|
39
|
+
codeStore,
|
|
40
|
+
}),
|
|
41
|
+
context: (_r, p) => ({ userId: p!.id }),
|
|
42
|
+
tools: [{ name: "list_my_projects", description: "List projects",
|
|
43
|
+
inputSchema: { type: "object", properties: {} }, scope: "mcp",
|
|
44
|
+
handler: async ({ userId }) => JSON.stringify(await projectsForUser(userId)) }],
|
|
47
45
|
});
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
`codeStore` must be a shared single-use store (Redis/KV — see [examples/stores.ts](examples/stores.ts)). Advanced ports (`mintAccessToken`, `verifyToken`, refresh, revoke) remain available when you outgrow the helper. Node ≥20.
|
|
51
49
|
|
|
52
|
-
[
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
**[View the complete SaaS demo](examples/saas-demo/README.md)** — working login, consent, two users with different projects.
|
|
51
|
+
|
|
52
|
+
Also listed on the Official MCP Registry as **Project desk** (reference remote server; not the SDK) — see [`examples/project-desk`](examples/project-desk/).
|
|
55
53
|
|
|
56
54
|
## When should I use this?
|
|
57
55
|
|
|
@@ -68,7 +66,7 @@ This shows the integration shape: `your-app.js` is your application's auth/data
|
|
|
68
66
|
|
|
69
67
|
## Run a real tool
|
|
70
68
|
|
|
71
|
-
The [Project desk demo](examples/saas-demo/README.md)
|
|
69
|
+
The [Project desk demo](examples/saas-demo/README.md) is the canonical example ([`examples/project-desk`](examples/project-desk/) implementation): app login → OAuth consent → access token → `list_my_projects`. Alice sees her two projects; Bob sees his own. It uses fictional data and an independent app session.
|
|
72
70
|
|
|
73
71
|
**Live Claude / ChatGPT recordings are still pending.** Automated OAuth tests are not evidence that a current vendor client has connected successfully.
|
|
74
72
|
|
|
@@ -84,9 +82,13 @@ Protocol versions implemented: `2024-11-05`, `2025-03-26`, `2025-06-18`; default
|
|
|
84
82
|
|
|
85
83
|
## Integrate with your app
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
Prefer `signedTokenAuth` for a one-secret start. For refresh, revoke, or your own JWT/JWKS, implement `resolveUser`, `loginUrl`, `mintAccessToken` and `verifyToken` yourself. Mint tokens for the requested MCP resource and return their verified `audience`; the library rejects an audience mismatch before executing tools. A logged-in user still sees an OAuth consent screen. Your data layer must enforce ownership and tenant permissions. Browser callers that send an `Origin` header need `allowedRequestOrigins` (fail-closed by default) — see [security.md](docs/security.md).
|
|
86
|
+
|
|
87
|
+
[SaaS demo](examples/saas-demo/README.md) · [Next.js starter](examples/nextjs-saas/README.md) · [Host recipes](docs/guide.md) · [Security](docs/security.md) · [npm](https://www.npmjs.com/package/mcp-trellis)
|
|
88
|
+
|
|
89
|
+
## How it compares
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
mcp-trellis is the **vendor-neutral embedded BYO-auth** path: MCP handler and self-hosted OAuth AS in one npm install. Contrast composition stacks such as **xmcp + Scalekit** (or WorkOS / Descope connectors) that split the MCP runtime and the identity product across 2–3 packages. Full comparison table: [docs/ROADMAP.md](docs/ROADMAP.md).
|
|
90
92
|
|
|
91
93
|
## Architecture
|
|
92
94
|
|
|
@@ -107,7 +109,7 @@ Host recipes, ports, tools, and multi-tenant: [docs/guide.md](docs/guide.md).
|
|
|
107
109
|
| [docs/security.md](docs/security.md) | Protocol promises, threat model, not in scope |
|
|
108
110
|
| [docs/ROADMAP.md](docs/ROADMAP.md) | What's next |
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
Canonical example: [`examples/project-desk`](examples/project-desk/) ([local docs](examples/saas-demo/)). Recipes: [`examples/`](examples/).
|
|
111
113
|
|
|
112
114
|
## Contributing
|
|
113
115
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convenience auth: one secret seals OAuth codes (via `codeSecret`) and
|
|
3
|
+
* signs short-lived access tokens. Domain separation is HKDF, not luck —
|
|
4
|
+
* access-token HMAC uses `mcp-trellis:signed-access-token:v1`, distinct from
|
|
5
|
+
* the sealed-code and client-secret-hash labels.
|
|
6
|
+
*
|
|
7
|
+
* ponytail: stateless tokens cannot be revoked before `exp`. No
|
|
8
|
+
* `refreshAccessToken` / `revokeToken` — `/revoke` stays unmounted and
|
|
9
|
+
* refresh unadvertised. Upgrade: supply those ports plus a store (see
|
|
10
|
+
* `examples/project-desk/src/tokens.ts`).
|
|
11
|
+
*/
|
|
12
|
+
import type { McpAppAuth } from "../app-options.js";
|
|
13
|
+
import type { CodeStore } from "../oauth/crypto/codes.js";
|
|
14
|
+
import type { OAuthUser } from "../oauth/types.js";
|
|
15
|
+
export type SignedTokenAuthOptions = {
|
|
16
|
+
secret: string;
|
|
17
|
+
resolveUser: (request: Request) => Promise<OAuthUser | null>;
|
|
18
|
+
loginUrl: (request: Request, nextPath: string) => string;
|
|
19
|
+
/** Access-token lifetime in seconds. Default 3600. Must be finite and > 0. */
|
|
20
|
+
ttlSeconds?: number;
|
|
21
|
+
/** Passthrough — production still needs a shared store across instances. */
|
|
22
|
+
codeStore?: CodeStore;
|
|
23
|
+
};
|
|
24
|
+
/** Builds a complete `McpAppAuth` from one secret plus your session resolver. */
|
|
25
|
+
export declare const signedTokenAuth: (options: SignedTokenAuthOptions) => McpAppAuth;
|
|
26
|
+
//# sourceMappingURL=signed-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed-token.d.ts","sourceRoot":"","sources":["../../src/auth/signed-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAqC,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAMtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC7D,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AA8EF,iFAAiF;AACjF,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,UAiBjE,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convenience auth: one secret seals OAuth codes (via `codeSecret`) and
|
|
3
|
+
* signs short-lived access tokens. Domain separation is HKDF, not luck —
|
|
4
|
+
* access-token HMAC uses `mcp-trellis:signed-access-token:v1`, distinct from
|
|
5
|
+
* the sealed-code and client-secret-hash labels.
|
|
6
|
+
*
|
|
7
|
+
* ponytail: stateless tokens cannot be revoked before `exp`. No
|
|
8
|
+
* `refreshAccessToken` / `revokeToken` — `/revoke` stays unmounted and
|
|
9
|
+
* refresh unadvertised. Upgrade: supply those ports plus a store (see
|
|
10
|
+
* `examples/project-desk/src/tokens.ts`).
|
|
11
|
+
*/
|
|
12
|
+
import { bytesToBase64Url, fromBase64Url } from "../oauth/crypto/base64url.js";
|
|
13
|
+
import { hkdfKey } from "../oauth/crypto/hkdf.js";
|
|
14
|
+
const HKDF_INFO = "mcp-trellis:signed-access-token:v1";
|
|
15
|
+
const HMAC_SHA256 = { name: "HMAC", hash: "SHA-256", length: 256 };
|
|
16
|
+
const DEFAULT_TTL_SECONDS = 3600;
|
|
17
|
+
const accessKey = (secret, usage) => hkdfKey(secret, { info: HKDF_INFO, algorithm: HMAC_SHA256, usage });
|
|
18
|
+
const signPayload = async (secret, payload) => {
|
|
19
|
+
const key = await accessKey(secret, "sign");
|
|
20
|
+
const body = bytesToBase64Url(new TextEncoder().encode(JSON.stringify(payload)));
|
|
21
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
|
|
22
|
+
return `${body}.${bytesToBase64Url(signature)}`;
|
|
23
|
+
};
|
|
24
|
+
const parsePayload = (raw) => {
|
|
25
|
+
if (raw === null || typeof raw !== "object")
|
|
26
|
+
return null;
|
|
27
|
+
const record = raw;
|
|
28
|
+
if (typeof record.userId !== "string" || typeof record.audience !== "string")
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof record.exp !== "number" || !Number.isFinite(record.exp))
|
|
31
|
+
return null;
|
|
32
|
+
if (record.exp < Date.now())
|
|
33
|
+
return null;
|
|
34
|
+
if (!Array.isArray(record.scopes) || !record.scopes.every((s) => typeof s === "string")) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
userId: record.userId,
|
|
39
|
+
scopes: record.scopes,
|
|
40
|
+
audience: record.audience,
|
|
41
|
+
exp: record.exp,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const verifyPayload = async (secret, token) => {
|
|
45
|
+
const parts = token.split(".");
|
|
46
|
+
if (parts.length !== 2)
|
|
47
|
+
return null;
|
|
48
|
+
const [body, signature] = parts;
|
|
49
|
+
if (!body || !signature)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const key = await accessKey(secret, "verify");
|
|
53
|
+
const ok = await crypto.subtle.verify("HMAC", key, fromBase64Url(signature), new TextEncoder().encode(body));
|
|
54
|
+
if (!ok)
|
|
55
|
+
return null;
|
|
56
|
+
return parsePayload(JSON.parse(new TextDecoder().decode(fromBase64Url(body))));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const mint = async (secret, ttlSeconds, input) => {
|
|
63
|
+
const scopes = input.scope.split(" ").filter(Boolean);
|
|
64
|
+
const accessToken = await signPayload(secret, {
|
|
65
|
+
userId: input.userId,
|
|
66
|
+
scopes,
|
|
67
|
+
audience: input.resource,
|
|
68
|
+
exp: Date.now() + ttlSeconds * 1000,
|
|
69
|
+
});
|
|
70
|
+
return { accessToken, expiresIn: ttlSeconds, scope: input.scope };
|
|
71
|
+
};
|
|
72
|
+
const toVerified = (payload) => ({
|
|
73
|
+
userId: payload.userId,
|
|
74
|
+
scopes: payload.scopes,
|
|
75
|
+
audience: payload.audience,
|
|
76
|
+
});
|
|
77
|
+
/** Builds a complete `McpAppAuth` from one secret plus your session resolver. */
|
|
78
|
+
export const signedTokenAuth = (options) => {
|
|
79
|
+
const ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
80
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
81
|
+
throw new Error(`signedTokenAuth: ttlSeconds must be a finite number > 0 (got ${ttlSeconds})`);
|
|
82
|
+
}
|
|
83
|
+
const { secret } = options;
|
|
84
|
+
return {
|
|
85
|
+
codeSecret: secret,
|
|
86
|
+
resolveUser: options.resolveUser,
|
|
87
|
+
loginUrl: options.loginUrl,
|
|
88
|
+
mintAccessToken: (input) => mint(secret, ttlSeconds, input),
|
|
89
|
+
verifyToken: async (token) => {
|
|
90
|
+
const payload = await verifyPayload(secret, token);
|
|
91
|
+
return payload ? toVerified(payload) : null;
|
|
92
|
+
},
|
|
93
|
+
...(options.codeStore !== undefined ? { codeStore: options.codeStore } : {}),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=signed-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signed-token.js","sourceRoot":"","sources":["../../src/auth/signed-token.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +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-token.js";
|
|
4
5
|
export { authMethodsFor, CLIENT_PROFILES, type ClientName, type ClientProfile, DEFAULT_CLIENTS, hasDynamicClient, preRegisteredClients, redirectUrisFor, } from "./clients.js";
|
|
5
6
|
export * from "./deprecated.js";
|
|
6
7
|
export { type JsonResponseInput, type JsonResponseOptions, jsonResponse, } from "./http/http.js";
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,sBAAsB,EAC3B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,sBAAsB,EAC3B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,cAAc,EACd,eAAe,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,cAAc,iBAAiB,CAAC;AAChC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,UAAU,EACf,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,QAAQ,EACR,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,OAAO,EACP,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,kFAAkF;AAClF,YAAY,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +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-token.js";
|
|
4
5
|
export { authMethodsFor, CLIENT_PROFILES, DEFAULT_CLIENTS, hasDynamicClient, preRegisteredClients, redirectUrisFor, } from "./clients.js";
|
|
5
6
|
// Moved to `mcp-trellis/advanced`; deprecated aliases until 3.0.
|
|
6
7
|
export * from "./deprecated.js";
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,GAMb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,gBAAgB,EAEhB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,GAMb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,WAAW,EACX,gBAAgB,EAEhB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,cAAc,EACd,eAAe,EAGf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,iEAAiE;AACjE,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAGL,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,gBAAgB,GAMjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EAIpB,QAAQ,EACR,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,kBAAkB,GAOnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAGL,OAAO,EAEP,UAAU,GAEX,MAAM,gBAAgB,CAAC"}
|
package/docs/ROADMAP.md
CHANGED
|
@@ -231,7 +231,9 @@ a proper JSON-RPC error.
|
|
|
231
231
|
- **Unified `audit` sink** on `createMcpApp` — one callback for MCP and
|
|
232
232
|
OAuth events (`source: "mcp" | "oauth"`); `auth.audit` remains an
|
|
233
233
|
OAuth-only override.
|
|
234
|
-
- **Framework recipes** — `examples/nextjs-
|
|
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).
|
|
235
237
|
- Fail-closed defaults: `allowInMemoryCodeStore` / `validateArgs`.
|
|
236
238
|
- **RFC 9207 `iss`** on authorize / error redirects; AS metadata advertises
|
|
237
239
|
`authorization_response_iss_parameter_supported`.
|
package/docs/compatibility.md
CHANGED
|
@@ -18,24 +18,25 @@ The Gemini profile does not imply compatibility with Gemini CLI or every Gemini
|
|
|
18
18
|
|
|
19
19
|
## Demo deploy (Worker)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
One-liner after you add a minimal `wrangler.toml` (name, `compatibility_date`, and secrets
|
|
24
|
-
`OAUTH_CODE_SECRET` / `ACCESS_TOKEN_SECRET`):
|
|
21
|
+
**Project desk** (Registry remotes target): [`examples/project-desk/worker.ts`](../examples/project-desk/worker.ts)
|
|
22
|
+
via root [`wrangler.toml`](../wrangler.toml).
|
|
25
23
|
|
|
26
24
|
```bash
|
|
25
|
+
npx wrangler secret put DEMO_PASSWORD # project-desk-demo for the public demo
|
|
27
26
|
npx wrangler secret put OAUTH_CODE_SECRET
|
|
28
|
-
npx wrangler
|
|
29
|
-
npx wrangler deploy examples/cloudflare-worker.ts
|
|
27
|
+
npx wrangler deploy
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
replace the live client rows below.
|
|
30
|
+
MCP URL: `https://mcp-trellis-project-desk.<workers-subdomain>.workers.dev/mcp`.
|
|
31
|
+
Operator checklist: [`examples/project-desk/PUBLISH.md`](../examples/project-desk/PUBLISH.md).
|
|
32
|
+
This does **not** replace the live client rows below.
|
|
33
|
+
|
|
34
|
+
Minimal ping-only Worker recipe (not the hosted demo):
|
|
35
|
+
[`examples/cloudflare-worker.ts`](../examples/cloudflare-worker.ts).
|
|
35
36
|
|
|
36
37
|
## Host contract covered in-repo
|
|
37
38
|
|
|
38
|
-
`examples/
|
|
39
|
+
`examples/project-desk` mints refresh tokens and enforces RFC 6749 §6: requested
|
|
39
40
|
refresh `scope` must be ⊆ the scopes originally bound to that refresh token
|
|
40
41
|
(`isScopeSubset` pattern also in `examples/stores.ts`). Escalation →
|
|
41
42
|
`invalid_grant`. Covered by `test/e2e/saas-demo.test.ts`.
|
|
@@ -58,10 +59,11 @@ release; it does **not** replace the live client row.
|
|
|
58
59
|
|
|
59
60
|
Record client product, version/build (or web UI and test date), account tier, package version/commit, hosting environment and configured callback. Capture discovery → registration → app login → consent → token exchange → initialize → tools/list → `list_my_projects`. Verify wrong audience/invalid token rejection and user isolation separately. Save sanitized output or recording; exclude cookies, passwords, codes and tokens.
|
|
60
61
|
|
|
61
|
-
Against a public HTTPS demo (`PUBLIC_ORIGIN` + `DEMO_PASSWORD`):
|
|
62
|
+
Against a public HTTPS demo (`PUBLIC_ORIGIN` + `DEMO_PASSWORD=project-desk-demo`):
|
|
62
63
|
|
|
63
64
|
```bash
|
|
64
65
|
npm run demo # reverse-proxy to HTTPS; set PUBLIC_ORIGIN
|
|
66
|
+
# or use the deployed Worker from examples/project-desk/PUBLISH.md
|
|
65
67
|
node scripts/prep-claude-oauth.mjs # scripted OAuth + refresh scope + tool
|
|
66
68
|
# Then open the vendor UI with MCP URL https://YOUR-HOST/mcp and record below.
|
|
67
69
|
```
|
|
Binary file
|
package/docs/guide.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Practical deep dive: architecture, host recipes, ports, tools, and multi-tenant
|
|
4
4
|
connectors. For a working minimal server, start from the [README](../README.md).
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
The **canonical example** is [`examples/project-desk`](../examples/project-desk/)
|
|
6
|
+
(Project desk: login → consent → user-scoped tools; local docs in
|
|
7
|
+
[`examples/saas-demo`](../examples/saas-demo/)). API tables and exports live
|
|
8
|
+
in [reference.md](reference.md). Security guarantees and threat model:
|
|
9
|
+
[security.md](security.md).
|
|
7
10
|
|
|
8
11
|
[Architecture](#how-it-fits-together) · [Clients](#clients) · [Recipes](#recipes) ·
|
|
9
12
|
[Compose the primitives](#advanced-compose-the-primitives) ·
|
|
@@ -69,7 +72,7 @@ registration.
|
|
|
69
72
|
// Worker
|
|
70
73
|
export default { fetch: (req: Request, env: Env) => buildApp(env).fetch(req) };
|
|
71
74
|
|
|
72
|
-
// Next.js App Router —
|
|
75
|
+
// Next.js App Router — see examples/nextjs-saas/
|
|
73
76
|
export const GET = (req: Request) => app.fetch(req);
|
|
74
77
|
export const POST = (req: Request) => app.fetch(req);
|
|
75
78
|
export const OPTIONS = (req: Request) => app.fetch(req);
|
|
@@ -181,8 +184,7 @@ Also mount `.well-known/*` and `/mcp/oauth/*` the same way (or route everything
|
|
|
181
184
|
|
|
182
185
|
Express, Cloud Functions, Cloud Run, `http.createServer` — bridge with
|
|
183
186
|
`asNodeHandler`. Runnable recipe: [`examples/http-server.ts`](../examples/http-server.ts)
|
|
184
|
-
(set `
|
|
185
|
-
then `npx tsx examples/http-server.ts`).
|
|
187
|
+
(set `MCP_SECRET`, e.g. from `openssl rand -base64 32`, then `npx tsx examples/http-server.ts`).
|
|
186
188
|
|
|
187
189
|
## Ports — what you implement
|
|
188
190
|
|
|
@@ -192,6 +194,10 @@ The library stays protocol-shaped. Your app plugs in the seams:
|
|
|
192
194
|
|
|
193
195
|
### App (`createMcpApp`)
|
|
194
196
|
|
|
197
|
+
Prefer `signedTokenAuth({ secret, resolveUser, loginUrl, codeStore })` to fill
|
|
198
|
+
`codeSecret` / `mintAccessToken` / `verifyToken` from one secret. The table
|
|
199
|
+
below is the advanced port surface when you outgrow the helper.
|
|
200
|
+
|
|
195
201
|
| Port | Role |
|
|
196
202
|
|------|------|
|
|
197
203
|
| `verifyToken(token, req)` | Decode a bearer and return `{ userId, scopes, audience, claims? }`, or `null`. **The library compares `audience` against this server's canonical resource** — you cannot forget the check. Optional `claims` are passed to `context` on `principal` |
|
package/docs/reference.md
CHANGED
|
@@ -157,6 +157,7 @@ A single-scope server has no such ambiguity and is unaffected.
|
|
|
157
157
|
- `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
|
|
158
158
|
- `CLIENT_PROFILES`, `DEFAULT_CLIENTS`, `authMethodsFor`, `redirectUrisFor`, `preRegisteredClients`, `hasDynamicClient`
|
|
159
159
|
- `parseBearer`, `wwwAuthenticateHeader`, `rejectQueryToken`
|
|
160
|
+
- `signedTokenAuth` — one-secret convenience `McpAppAuth` (HMAC access tokens via HKDF; no refresh/revoke). Type `SignedTokenAuthOptions`
|
|
160
161
|
- `rpcResult`, `rpcError`, JSON-RPC error constants
|
|
161
162
|
- `PROTOCOL_VERSIONS`, `DEFAULT_PROTOCOL_VERSION`
|
|
162
163
|
- `jsonResponse({ data, status?, headers?, cors? })` — type `JsonResponseInput` exported
|
package/docs/security.md
CHANGED
|
@@ -43,7 +43,7 @@ This is a **library** threat model, not a third-party audit badge.
|
|
|
43
43
|
| 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 re-resolve is audited as `cimd_resolve_failed`. **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 |
|
|
44
44
|
| 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` |
|
|
45
45
|
| 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 |
|
|
46
|
-
| 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 `
|
|
46
|
+
| 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 |
|
|
47
47
|
| 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 |
|
|
48
48
|
| 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 |
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-trellis",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Add authenticated remote MCP connectors to existing TypeScript SaaS apps using your existing users and auth.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
"authorization-server",
|
|
28
28
|
"connector",
|
|
29
29
|
"typescript",
|
|
30
|
-
"mcp-server"
|
|
30
|
+
"mcp-server",
|
|
31
|
+
"saas",
|
|
32
|
+
"remote-mcp",
|
|
33
|
+
"byo-auth",
|
|
34
|
+
"ai-connector"
|
|
31
35
|
],
|
|
32
36
|
"types": "./dist/server.d.ts",
|
|
33
37
|
"exports": {
|