actual-mcp-server 0.22.5 → 0.22.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/src/auth/setup.js +5 -4
- package/dist/src/config.js +4 -2
- package/dist/src/lib/oidc-resource.js +37 -2
- package/dist/src/lib/oidc-scopes.js +53 -0
- package/dist/src/server/httpServer.js +18 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -508,11 +508,12 @@ All configuration is via environment variables. Copy `.env.example` to `.env` to
|
|
|
508
508
|
| `OIDC_ISSUER` | _(none)_ | If OIDC | OIDC issuer URL (e.g., `https://sso.example.com`) |
|
|
509
509
|
| `OIDC_ALLOW_INSECURE_ISSUER` | `false` | No | Allow a plaintext (http) OIDC issuer on a trusted network (#244). Off by default (http issuer refused at startup); set `true` only for local/LAN testing |
|
|
510
510
|
| `OIDC_RESOURCE` | _(none)_ | If OIDC | This server's canonical URL, the RFC 9728 resource identifier and the expected `aud` (#461). Use the full public MCP endpoint URL, e.g. `https://actual-mcp.example.com/http`, no trailing slash. Must be an absolute http(s) URL (a bare client id refuses to start); put a client-id `aud` in `OIDC_ACCEPTED_AUDIENCES` instead |
|
|
511
|
-
| `OIDC_ACCEPTED_AUDIENCES` | _(none)_ | No | Extra accepted `aud` values beyond `OIDC_RESOURCE`, comma-separated (#245). For IdPs that put the client-id in `aud` (e.g. Authentik). Strict allowlist, never a wildcard |
|
|
511
|
+
| `OIDC_ACCEPTED_AUDIENCES` | _(none)_ | No | Extra accepted `aud` values beyond `OIDC_RESOURCE`, comma-separated (#245). For IdPs that put the client-id in `aud` (e.g. Authentik) or the OAuth client's redirect URI (Cloudflare Access for SaaS). Strict allowlist, never a wildcard |
|
|
512
512
|
| `OIDC_JWKS_TRUSTED_HOSTS` | _(none)_ | No | Opt-in cross-origin JWKS hosts, comma-separated `host` or `host:port` (#254). For IdPs whose `jwks_uri` lives on another host, e.g. Google needs `www.googleapis.com`. Exact match, no wildcards; empty default keeps same-origin-only |
|
|
513
513
|
| `OIDC_JWKS_URI` | _(none)_ | No | Direct JWKS URL that bypasses OpenID discovery (#462), for issuers with no discovery document (Cloudflare Access: `https://<team>.cloudflareaccess.com/cdn-cgi/access/certs`). Same https, credentials and origin-or-trusted-host rules as a discovered `jwks_uri`, plus no query or fragment. When set, `/.well-known/oauth-authorization-server` is not served and `AUTH_BUDGET_ACL_IDENTITY_SOURCE=userinfo` is refused |
|
|
514
|
-
| `OIDC_TOKEN_SOURCE` | `authorization` | No | Where the bearer JWT is read from (#462): `authorization` (default) or `cf-access-jwt-assertion` for an MCP server behind Cloudflare Access with Managed OAuth (the edge forwards the signed Access JWT in that header; `Authorization` is then never consulted, `OIDC_SCOPES` must be empty, and the origin must be reachable only via Cloudflare). See the Cloudflare recipe in `docs/guides/AI_CLIENT_SETUP.md` |
|
|
515
|
-
| `OIDC_SCOPES` | _(none)_ | No | Comma-separated required scopes
|
|
514
|
+
| `OIDC_TOKEN_SOURCE` | `authorization` | No | Where the bearer JWT is read from (#462): `authorization` (default) or `cf-access-jwt-assertion` for an MCP server behind Cloudflare Access with Managed OAuth (the edge forwards the signed Access JWT in that header; `Authorization` is then never consulted, `OIDC_SCOPES` must be empty, `OIDC_SCOPES_SUPPORTED` is allowed, and the origin must be reachable only via Cloudflare). See the Cloudflare recipe in `docs/guides/AI_CLIENT_SETUP.md` |
|
|
515
|
+
| `OIDC_SCOPES` | _(none)_ | No | Comma-separated required (enforced) scopes, also advertised. Leave empty when the IdP's JWTs carry no `scope` claim (Casdoor, Cloudflare Access for SaaS) |
|
|
516
|
+
| `OIDC_SCOPES_SUPPORTED` | _(none)_ | No | Comma-separated scopes advertised in `scopes_supported` but not enforced. Use when the IdP's JWTs carry no `scope` claim but the client needs a scope such as `offline_access` advertised (e.g. Gemini with Cloudflare Access for SaaS). Required scopes from `OIDC_SCOPES` are always advertised too |
|
|
516
517
|
| `AUTH_BUDGET_ACL` | _(none)_ | No | Per-user budget ACL; see [AI Client Setup](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) |
|
|
517
518
|
| `AUTH_BUDGET_ACL_SOURCE` | `static` | No | `static` uses the `AUTH_BUDGET_ACL` map above. `actual` derives the ACL from the Actual server's own per-file access list, so revoking someone in Actual takes effect here without a config edit. Requires a multi-user (OpenID) Actual server that was password-bootstrapped first |
|
|
518
519
|
| `AUTH_BUDGET_ACL_CLAIM` | `auto` | No | Which token claim identifies the principal when the source is `actual`. `auto` mirrors Actual's own precedence (`preferred_username`, `login`, `email`, `id`, `sub`) and matches the result against Actual's `userName`. Set a single claim name to pin one instead |
|
|
@@ -666,14 +667,15 @@ AUTH_PROVIDER=oidc
|
|
|
666
667
|
OIDC_ISSUER=https://sso.yourdomain.com
|
|
667
668
|
OIDC_RESOURCE=https://actual-mcp.yourdomain.com/http # this server's public MCP URL (the expected 'aud')
|
|
668
669
|
OIDC_ACCEPTED_AUDIENCES=your-client-id # only if your IdP puts the client id in 'aud' (#245)
|
|
669
|
-
OIDC_SCOPES= # leave empty for Casdoor
|
|
670
|
+
OIDC_SCOPES= # leave empty for Casdoor / Cloudflare Access for SaaS
|
|
671
|
+
# OIDC_SCOPES_SUPPORTED=openid,offline_access # extra scopes advertised in discovery without enforcement (#472)
|
|
670
672
|
```
|
|
671
673
|
|
|
672
|
-
`OIDC_RESOURCE` is the canonical URL of this MCP server (the MCP authorization spec's "canonical server URI" and the RFC 9728 resource identifier), not an IdP client id: the protected-resource metadata route and the expected token audience are both derived from it, and a value that is not an absolute http(s) URL refuses to start with a message naming the variable (#461). Use the full public endpoint URL (`https://host/http`, or whatever `MCP_BRIDGE_HTTP_PATH` / `MCP_HTTP_PATH` advertises) rather than the bare origin: with the endpoint form the server publishes the path-specific document a
|
|
674
|
+
`OIDC_RESOURCE` is the canonical URL of this MCP server (the MCP authorization spec's "canonical server URI" and the RFC 9728 resource identifier), not an IdP client id: the protected-resource metadata route and the expected token audience are both derived from it, and a value that is not an absolute http(s) URL refuses to start with a message naming the variable (#461). Use the full public endpoint URL (`https://host/http`, or whatever `MCP_BRIDGE_HTTP_PATH` / `MCP_HTTP_PATH` advertises) rather than the bare origin: with the endpoint form the server publishes both the path-specific document (canonical) and a root copy for clients that look only at the root (#473), and it warns at startup when the resource path and advertised path disagree.
|
|
673
675
|
|
|
674
676
|
**OAuth discovery endpoints (automatic in OIDC mode).** When `AUTH_PROVIDER=oidc`, the server publishes the two metadata documents an OAuth client needs to bootstrap a login, so `mcp-remote` and Claude.ai's native connector can discover the flow without manual endpoint configuration:
|
|
675
677
|
|
|
676
|
-
- `GET /.well-known/oauth-protected-resource[<path>]` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server. The route follows `OIDC_RESOURCE`: `https://host/http
|
|
678
|
+
- `GET /.well-known/oauth-protected-resource[<path>]` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server. The route follows `OIDC_RESOURCE`: with `https://host/http`, the canonical document is served at `/.well-known/oauth-protected-resource/http` (RFC 9728 section 3.1) and a root copy is also served at `/.well-known/oauth-protected-resource` for clients that skip the path-specific URL (e.g. Google Gemini, #473); the bare origin serves only the root path. The official MCP SDK tries the path-specific URL first and falls back to the root.
|
|
677
679
|
- `GET /.well-known/oauth-authorization-server` (RFC 8414, #285): the authorization server metadata (its `authorization_endpoint` / `token_endpoint` / `registration_endpoint`), re-served from your IdP's own OpenID discovery document. This is here because several clients look for it on the resource-server origin, and some IdPs (e.g. Authentik) do not expose it where those clients look. It is fetched once at startup and served verbatim, exposes only endpoints your IdP already publishes publicly, and requires no authentication (a client reads it before it has a token). No extra configuration is needed; it is absent when `AUTH_PROVIDER` is not `oidc`.
|
|
678
680
|
|
|
679
681
|
**Where the per-user budget ACL comes from.** By default the ACL is the `AUTH_BUDGET_ACL` map you maintain by hand. Since v0.10.x it can instead be derived from the Actual server's own per-file access list, so granting or revoking someone in Actual takes effect here without a config edit and a restart:
|
|
@@ -693,7 +695,7 @@ Three things to know before enabling it:
|
|
|
693
695
|
|
|
694
696
|
It is opt-in and fails closed: if the budget list cannot be read, or the principal matches no file, access is denied rather than granted.
|
|
695
697
|
|
|
696
|
-
See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) for `AUTH_BUDGET_ACL` format and
|
|
698
|
+
See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) for `AUTH_BUDGET_ACL` format, Casdoor notes, and the Cloudflare Access for SaaS + Gemini recipe.
|
|
697
699
|
|
|
698
700
|
---
|
|
699
701
|
|
|
@@ -786,7 +788,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
786
788
|
|
|
787
789
|
---
|
|
788
790
|
|
|
789
|
-
**Version:** 0.22.
|
|
791
|
+
**Version:** 0.22.7 | **Tool Count:** 81 (verified LibreChat-compatible)
|
|
790
792
|
|
|
791
793
|
## Sponsor
|
|
792
794
|
|
package/dist/src/auth/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ import { MCPAuth } from 'mcp-auth';
|
|
|
10
10
|
import config from '../config.js';
|
|
11
11
|
import logger from '../logger.js';
|
|
12
12
|
import { validateOidcResource } from '../lib/oidc-resource.js';
|
|
13
|
+
import { parseScopeList, buildScopesSupported } from '../lib/oidc-scopes.js';
|
|
13
14
|
let _instance = null;
|
|
14
15
|
/**
|
|
15
16
|
* Returns an MCPAuth instance configured for this server's OIDC settings,
|
|
@@ -41,12 +42,12 @@ export function createMcpAuth() {
|
|
|
41
42
|
const resourceUrl = validateOidcResource(config.OIDC_RESOURCE);
|
|
42
43
|
if (_instance)
|
|
43
44
|
return _instance;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
: [];
|
|
45
|
+
const requiredScopes = parseScopeList(config.OIDC_SCOPES);
|
|
46
|
+
const scopesSupported = buildScopesSupported(config.OIDC_SCOPES_SUPPORTED, config.OIDC_SCOPES);
|
|
47
47
|
logger.info(`[OIDC] Configuring mcp-auth, issuer: ${config.OIDC_ISSUER}`);
|
|
48
48
|
logger.info(`[OIDC] Resource identifier: ${resourceUrl.origin}${resourceUrl.pathname}`);
|
|
49
|
-
logger.info(`[OIDC] Scopes required: ${
|
|
49
|
+
logger.info(`[OIDC] Scopes required: ${requiredScopes.length ? requiredScopes.join(', ') : '(none)'}`);
|
|
50
|
+
logger.info(`[OIDC] Scopes advertised: ${scopesSupported.length ? scopesSupported.join(', ') : '(none)'}`);
|
|
50
51
|
_instance = new MCPAuth({
|
|
51
52
|
protectedResources: [
|
|
52
53
|
{
|
package/dist/src/config.js
CHANGED
|
@@ -11,7 +11,7 @@ import { parseIdentityMap } from './auth/identity-map.js';
|
|
|
11
11
|
// accepts as NO cap at all: GHSA-v422-hmwv-36x6 by another route.
|
|
12
12
|
const BODY_LIMIT_UNITS = { b: 1, kb: 2 ** 10, mb: 2 ** 20, gb: 2 ** 30, tb: 2 ** 40, pb: 2 ** 50 };
|
|
13
13
|
const BODY_LIMIT_RE = new RegExp(`^(\\d+(?:\\.\\d+)?) *(${Object.keys(BODY_LIMIT_UNITS).join('|')})?$`, 'i');
|
|
14
|
-
function parseStrictByteSize(value) {
|
|
14
|
+
export function parseStrictByteSize(value) {
|
|
15
15
|
const m = BODY_LIMIT_RE.exec(value);
|
|
16
16
|
if (!m)
|
|
17
17
|
return null;
|
|
@@ -155,8 +155,10 @@ export const configSchema = z.object({
|
|
|
155
155
|
// validate it. In that mode Authorization is NEVER consulted (one source, one
|
|
156
156
|
// verification, no fallback). Closed enum, default is today's behaviour.
|
|
157
157
|
OIDC_TOKEN_SOURCE: z.enum(['authorization', 'cf-access-jwt-assertion']).default('authorization'),
|
|
158
|
-
// Comma-separated required scopes (e.g. "read,write"). Optional.
|
|
158
|
+
// Comma-separated required (enforced) scopes; also advertised (e.g. "read,write"). Optional.
|
|
159
159
|
OIDC_SCOPES: z.string().optional(),
|
|
160
|
+
// Comma-separated scopes advertised in discovery (scopes_supported) but not enforced. Optional.
|
|
161
|
+
OIDC_SCOPES_SUPPORTED: z.string().optional(),
|
|
160
162
|
// JSON map of principal → budget sync-ID list for per-user budget ACL.
|
|
161
163
|
// Keys: email, sub, or "group:<name>". Values: array of sync IDs or ["*"] for all.
|
|
162
164
|
// Example: {"alice@example.com":["budget-1"],"group:admin":["*"]}
|
|
@@ -107,6 +107,39 @@ export function advertisedPathFrom(advertisedUrl, httpPath) {
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
const METADATA_ROOT = '/.well-known/oauth-protected-resource';
|
|
111
|
+
/**
|
|
112
|
+
* The route mcp-auth 0.2.0 serves the RFC 9728 document under for `resource` (#473), or
|
|
113
|
+
* null for the origin form, where it serves the root itself. This copies the rule in
|
|
114
|
+
* mcp-auth's `create-resource-metadata-endpoint.js` exactly, raw pathname included: it
|
|
115
|
+
* must NOT use `stripped()` below, because mcp-auth registers the route under the
|
|
116
|
+
* pathname as given, trailing slash and all, and a rewrite aimed anywhere else 404s.
|
|
117
|
+
*/
|
|
118
|
+
export function resourceMetadataPath(resource) {
|
|
119
|
+
const { pathname } = new URL(resource);
|
|
120
|
+
return pathname === '/' ? null : `${METADATA_ROOT}${pathname}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The request URL a root metadata request is rewritten to (#473): the fixed target plus
|
|
124
|
+
* the original query string, verbatim from the first `?`. Pure so the query handling is
|
|
125
|
+
* testable: mcp-auth's route ignores the query, so a served 200 cannot tell a kept query
|
|
126
|
+
* from a dropped one.
|
|
127
|
+
*/
|
|
128
|
+
export function rootMetadataRewriteUrl(url, target) {
|
|
129
|
+
const q = url.indexOf('?');
|
|
130
|
+
return q >= 0 ? `${target}${url.slice(q)}` : target;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* True for a request the root rewrite applies to (#473). Exact lowercase path, with or
|
|
134
|
+
* without one trailing slash: Express 5's default non-strict routing already serves
|
|
135
|
+
* mcp-auth's own path-specific route both ways, so the root behaves the same, but it is
|
|
136
|
+
* two exact comparisons rather than a prefix, so no deeper path is ever rewritten. Only
|
|
137
|
+
* GET, HEAD (Express answers it through GET routes) and the OPTIONS CORS preflight.
|
|
138
|
+
*/
|
|
139
|
+
export function isRootMetadataRequest(method, path) {
|
|
140
|
+
return ((method === 'GET' || method === 'HEAD' || method === 'OPTIONS') &&
|
|
141
|
+
(path === METADATA_ROOT || path === `${METADATA_ROOT}/`));
|
|
142
|
+
}
|
|
110
143
|
/**
|
|
111
144
|
* Null when the identifier's path equals the advertised MCP path (the form under which
|
|
112
145
|
* mcp-auth serves the path-specific RFC 9728 document a strict client looks for);
|
|
@@ -122,13 +155,15 @@ export function resourceMetadataWarning(opts) {
|
|
|
122
155
|
return null;
|
|
123
156
|
const recommended = `${url.origin}${canonicalPath}`;
|
|
124
157
|
const onlyTrailingSlash = stripped(url.pathname) === canonicalPath;
|
|
158
|
+
const metadataPath = resourceMetadataPath(opts.resource);
|
|
125
159
|
// Express serves the metadata route with and without the slash, so the slash form is
|
|
126
160
|
// not a discovery 404; the hazard is the audience: a client that canonicalises the
|
|
127
161
|
// resource mints aud=${recommended}, which the closed allowlist then rejects.
|
|
128
162
|
const detail = onlyTrailingSlash
|
|
129
163
|
? `The only difference is the trailing slash. A client that canonicalises the resource will request a token for ${recommended}, which is not in the accepted audience set, so use the form without the slash.`
|
|
130
|
-
:
|
|
131
|
-
`a client that looks for the path-specific document under ${canonicalPath} and does not fall back to the root will fail discovery. `
|
|
164
|
+
: (metadataPath === null
|
|
165
|
+
? `Protected-resource metadata is served at the root ${METADATA_ROOT} only; a client that looks for the path-specific document under ${canonicalPath} and does not fall back to the root will fail discovery. `
|
|
166
|
+
: `Protected-resource metadata is served at ${metadataPath} and, for clients that look only there, the root ${METADATA_ROOT} (#473); a client that looks for the path-specific document under ${canonicalPath} will fail discovery. `) +
|
|
132
167
|
`Switching changes the expected aud and stops serving the current document, so during the switch add OIDC_ACCEPTED_AUDIENCES=${url.origin}${url.pathname === '/' ? '' : url.pathname} to keep tokens minted for the old value valid.`;
|
|
133
168
|
return (`[OIDC] ${VAR} is ${displayForm(url)} but the MCP endpoint is advertised at ${opts.advertisedPath}. ${detail} ` +
|
|
134
169
|
`Recommended: ${VAR}=${recommended}`);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC scope parsing and discovery helpers (#472).
|
|
3
|
+
*
|
|
4
|
+
* Separates scopes advertised in discovery (RFC 9728 `scopes_supported`) from
|
|
5
|
+
* scopes required (enforced) on incoming access tokens (`requiredScopes`).
|
|
6
|
+
*
|
|
7
|
+
* Some IdPs (such as Cloudflare Access for SaaS or Casdoor) issue JWTs without
|
|
8
|
+
* a `scope` claim, while clients (such as Google Gemini) require `offline_access`
|
|
9
|
+
* to be advertised in discovery in order to request and receive a refresh token.
|
|
10
|
+
*
|
|
11
|
+
* Pure and side-effect-free for hermetic unit testing.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Split a comma-separated scope string into trimmed, non-empty, de-duplicated tokens,
|
|
15
|
+
* preserving first-seen order.
|
|
16
|
+
*
|
|
17
|
+
* Returns an empty array if `csv` is undefined, empty, or whitespace-only.
|
|
18
|
+
*/
|
|
19
|
+
export function parseScopeList(csv) {
|
|
20
|
+
if (!csv)
|
|
21
|
+
return [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const part of csv.split(',')) {
|
|
25
|
+
const trimmed = part.trim();
|
|
26
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
27
|
+
seen.add(trimmed);
|
|
28
|
+
result.push(trimmed);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the full list of scopes advertised in RFC 9728 `scopes_supported`.
|
|
35
|
+
*
|
|
36
|
+
* Advertised scopes (`OIDC_SCOPES_SUPPORTED`) come first, followed by any
|
|
37
|
+
* required scopes (`OIDC_SCOPES`) not already present. A required scope is
|
|
38
|
+
* always advertised so clients cannot fail to request a scope the server demands.
|
|
39
|
+
* With `OIDC_SCOPES_SUPPORTED` unset, this matches `parseScopeList(requiredCsv)`.
|
|
40
|
+
*/
|
|
41
|
+
export function buildScopesSupported(advertisedCsv, requiredCsv) {
|
|
42
|
+
const advertised = parseScopeList(advertisedCsv);
|
|
43
|
+
const required = parseScopeList(requiredCsv);
|
|
44
|
+
const seen = new Set(advertised);
|
|
45
|
+
const result = [...advertised];
|
|
46
|
+
for (const s of required) {
|
|
47
|
+
if (!seen.has(s)) {
|
|
48
|
+
seen.add(s);
|
|
49
|
+
result.push(s);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -15,9 +15,10 @@ import { MCPAuthTokenVerificationError } from 'mcp-auth';
|
|
|
15
15
|
import { createMcpAuth } from '../auth/setup.js';
|
|
16
16
|
import { resolveJwksSource, buildTrustedJwksHosts, setResolvedOidcMetadata } from '../lib/oidc-discovery.js';
|
|
17
17
|
import { createAssertionPromotionMiddleware } from '../lib/oidc-token-source.js';
|
|
18
|
-
import { oidcResourceStartupWarnings } from '../lib/oidc-resource.js';
|
|
18
|
+
import { isRootMetadataRequest, oidcResourceStartupWarnings, resourceMetadataPath, rootMetadataRewriteUrl } from '../lib/oidc-resource.js';
|
|
19
19
|
import { stripJoseCause } from '../lib/oidc-error-cause.js';
|
|
20
20
|
import { buildAcceptedAudiences } from '../lib/oidc-audiences.js';
|
|
21
|
+
import { parseScopeList } from '../lib/oidc-scopes.js';
|
|
21
22
|
import { budgetAclMiddleware } from '../auth/budget-acl.js';
|
|
22
23
|
import { runAclPreflight } from '../auth/budget-acl-dynamic.js';
|
|
23
24
|
import * as https from 'node:https';
|
|
@@ -69,12 +70,25 @@ advertisedUrl) {
|
|
|
69
70
|
for (const w of oidcResourceStartupWarnings({ resource: config.OIDC_RESOURCE, advertisedUrl, httpPath }))
|
|
70
71
|
logger.warn(w);
|
|
71
72
|
if (mcpAuth) {
|
|
73
|
+
// #473: clients that skip the path-specific RFC 9728 discovery URL (e.g. Google Gemini)
|
|
74
|
+
// fetch /.well-known/oauth-protected-resource at the root. When OIDC_RESOURCE has a path,
|
|
75
|
+
// rewrite those requests onto the path-specific route so mcp-auth serves the identical
|
|
76
|
+
// document and CORS headers in-process. The target is fixed at startup from
|
|
77
|
+
// OIDC_RESOURCE, never taken from the request; the match rule is isRootMetadataRequest.
|
|
78
|
+
// Mounted inside `if (mcpAuth)` and before the router, so `none` mode never gets it and
|
|
79
|
+
// clients reach it without a token.
|
|
80
|
+
const metadataTarget = config.OIDC_RESOURCE ? resourceMetadataPath(config.OIDC_RESOURCE) : null;
|
|
81
|
+
if (metadataTarget) {
|
|
82
|
+
app.use((req, _res, next) => {
|
|
83
|
+
if (isRootMetadataRequest(req.method, req.path))
|
|
84
|
+
req.url = rootMetadataRewriteUrl(req.url, metadataTarget);
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
72
88
|
// Serve RFC 8707 Protected Resource Metadata (/.well-known/oauth-protected-resource/...)
|
|
73
89
|
app.use(mcpAuth.protectedResourceMetadataRouter());
|
|
74
90
|
// Protect ALL httpPath routes with JWT validation + budget ACL
|
|
75
|
-
const requiredScopes = config.OIDC_SCOPES
|
|
76
|
-
? config.OIDC_SCOPES.split(',').map((s) => s.trim()).filter(Boolean)
|
|
77
|
-
: [];
|
|
91
|
+
const requiredScopes = parseScopeList(config.OIDC_SCOPES);
|
|
78
92
|
// #245: the strict, closed audience allowlist (OIDC_RESOURCE plus any
|
|
79
93
|
// explicitly configured OIDC_ACCEPTED_AUDIENCES). jose treats the array as
|
|
80
94
|
// match-any membership, so an IdP that puts the client-id in `aud` works
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.22.
|
|
4
|
+
"version": "0.22.7",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|