actual-mcp-server 0.8.11 → 0.8.13

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 CHANGED
@@ -628,6 +628,11 @@ OIDC_RESOURCE=your-client-id # must match 'aud' JWT claim
628
628
  OIDC_SCOPES= # leave empty for Casdoor
629
629
  ```
630
630
 
631
+ **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:
632
+
633
+ - `GET /.well-known/oauth-protected-resource` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server.
634
+ - `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`.
635
+
631
636
  See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) for `AUTH_BUDGET_ACL` format and Casdoor notes.
632
637
 
633
638
  ---
@@ -722,4 +727,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
722
727
 
723
728
  ---
724
729
 
725
- **Version:** 0.8.11 | **Tool Count:** 71 (verified LibreChat-compatible)
730
+ **Version:** 0.8.13 | **Tool Count:** 71 (verified LibreChat-compatible)
@@ -172,11 +172,21 @@ export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false, trus
172
172
  return jwks.toString();
173
173
  }
174
174
  /**
175
- * Fetch the issuer's OpenID discovery document and return a validated `jwks_uri`.
176
- * Fails closed on any problem (non-https issuer, redirect, non-200, bad JSON,
177
- * missing/invalid/cross-host jwks_uri). `fetchImpl` is injectable for testing.
175
+ * Fetch the issuer's OpenID discovery document ONCE and return both the raw
176
+ * document and its validated `jwks_uri` (#285). Fails closed on any problem
177
+ * (non-https issuer, redirect, non-200, bad JSON, missing/invalid/cross-host
178
+ * jwks_uri). `fetchImpl` is injectable for testing.
179
+ *
180
+ * The returned `metadata` is the IdP's own public discovery document. Its
181
+ * intended second use (beyond resolving the JWKS URI) is to be re-served
182
+ * verbatim at `/.well-known/oauth-authorization-server` as RFC 8414 Authorization
183
+ * Server Metadata, so a client that resolves that well-known path against this
184
+ * resource server (as mcp-remote and Claude.ai do) can find the IdP's
185
+ * `token_endpoint`. Because this fetch happens ONCE at startup and the document
186
+ * is served from memory, no request ever triggers an outbound fetch (no
187
+ * request-path SSRF surface).
178
188
  */
179
- export async function discoverJwksUri(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
189
+ export async function discoverOidcMetadata(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
180
190
  assertSecureIssuer(issuer, allowInsecure);
181
191
  const base = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
182
192
  const discoveryUrl = `${base}/.well-known/openid-configuration`;
@@ -200,7 +210,20 @@ export async function discoverJwksUri(issuer, allowInsecure = false, trustedHost
200
210
  catch {
201
211
  throw new Error(`OIDC discovery document at ${discoveryUrl} is not valid JSON`);
202
212
  }
213
+ // resolveJwksUri validates that `doc` is an object AND resolves/validates the
214
+ // jwks_uri (https, same-origin or explicitly allowlisted). It throws (fail
215
+ // closed) on any problem, so reaching past it guarantees a usable object with a
216
+ // trusted jwks_uri before the document is exposed.
203
217
  const jwksUri = resolveJwksUri(doc, issuer, allowInsecure, trustedHosts);
204
218
  logger.info('Resolved JWKS URI from OIDC discovery', { jwksUri });
219
+ return { jwksUri, metadata: doc };
220
+ }
221
+ /**
222
+ * Back-compat convenience: resolve only the validated `jwks_uri`. Delegates to
223
+ * discoverOidcMetadata so the single hardened fetch and every fail-closed check
224
+ * are shared. Retained for callers (and their tests) that only need the key set.
225
+ */
226
+ export async function discoverJwksUri(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
227
+ const { jwksUri } = await discoverOidcMetadata(issuer, allowInsecure, trustedHosts, fetchImpl);
205
228
  return jwksUri;
206
229
  }
@@ -13,7 +13,7 @@ import config from '../config.js';
13
13
  import { createRemoteJWKSet, jwtVerify } from 'jose';
14
14
  import { MCPAuthTokenVerificationError } from 'mcp-auth';
15
15
  import { createMcpAuth } from '../auth/setup.js';
16
- import { discoverJwksUri, buildTrustedJwksHosts } from '../lib/oidc-discovery.js';
16
+ import { discoverOidcMetadata, buildTrustedJwksHosts } from '../lib/oidc-discovery.js';
17
17
  import { buildAcceptedAudiences } from '../lib/oidc-audiences.js';
18
18
  import { budgetAclMiddleware } from '../auth/budget-acl.js';
19
19
  import * as https from 'node:https';
@@ -86,7 +86,24 @@ advertisedUrl) {
86
86
  // #254: opt-in cross-origin JWKS trusted hosts (e.g. Google serves keys from
87
87
  // www.googleapis.com). Parsed fail-fast here at the composition root and
88
88
  // threaded as a parameter; empty (the default) keeps same-origin-only.
89
- const jwksUri = await discoverJwksUri(config.OIDC_ISSUER, config.OIDC_ALLOW_INSECURE_ISSUER, buildTrustedJwksHosts(config.OIDC_JWKS_TRUSTED_HOSTS));
89
+ const { jwksUri, metadata: oidcMetadata } = await discoverOidcMetadata(config.OIDC_ISSUER, config.OIDC_ALLOW_INSECURE_ISSUER, buildTrustedJwksHosts(config.OIDC_JWKS_TRUSTED_HOSTS));
90
+ // #285: serve RFC 8414 OAuth Authorization Server Metadata at this origin so
91
+ // mcp-remote and Claude.ai's native OAuth connector can discover the IdP's
92
+ // token_endpoint. We already serve RFC 9728 protected-resource metadata (whose
93
+ // authorization_servers points at the IdP), but several clients resolve the
94
+ // oauth-authorization-server well-known path against the RESOURCE server root,
95
+ // and some IdPs (e.g. Authentik) do not expose it where those clients look.
96
+ // The document is the IdP's own public discovery doc, fetched ONCE above (so no
97
+ // request-path fetch and no SSRF surface) and re-served verbatim as a BARE JSON
98
+ // object. Never emit a {jsonrpc,result} envelope on a well-known metadata path:
99
+ // that shape is not RFC-compliant and would break client parsing (the server-info
100
+ // probe was moved off this reserved path to /mcp-info as a bare object in #286).
101
+ // Unauthenticated by design: a client reads this before it holds a token, so it is
102
+ // registered here, before the bearer-auth middleware, and only when
103
+ // AUTH_PROVIDER=oidc (this block).
104
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => {
105
+ res.json(oidcMetadata);
106
+ });
90
107
  const jwks = createRemoteJWKSet(new URL(jwksUri));
91
108
  const customJwtVerify = async (token) => {
92
109
  // Enforce the audience claim (#160, OWASP A07). Without it, any
@@ -568,34 +585,26 @@ advertisedUrl) {
568
585
  }
569
586
  await transport.handleRequest(req, res);
570
587
  });
571
- // quick GET info endpoints (some clients probe)
588
+ // Quick GET server-info probe used by our test harness (testMcpClient.ts, the
589
+ // Playwright E2E) to check the server is up and advertises tools before the full
590
+ // JSON-RPC handshake. #286: this used to live on /.well-known/oauth-protected-resource
591
+ // wrapped in a {jsonrpc,result} envelope, which squatted the RFC 9728 reserved OAuth
592
+ // path with non-metadata (a strict OAuth client probing it got garbage). It now lives
593
+ // on the non-reserved /mcp-info as a BARE object. The reserved OAuth path is therefore
594
+ // unregistered in non-OIDC mode (it correctly 404s: a static-bearer server has no
595
+ // authorization server to advertise), and in OIDC mode mcpAuth.protectedResourceMetadataRouter()
596
+ // (registered above) still serves the compliant RFC 9728 document.
572
597
  const serverIp = process.env.MCP_BRIDGE_PUBLIC_HOST || getLocalIp();
573
- app.get('/.well-known/oauth-protected-resource', (_req, res) => {
574
- res.json({
575
- jsonrpc: '2.0',
576
- result: {
577
- description: serverDescription || "Actual MCP server",
578
- instructions: serverInstructions || "Welcome to the Actual MCP server.",
579
- serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
580
- capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
581
- tools: toolsList,
582
- advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
583
- },
584
- });
585
- });
586
- app.get('/.well-known/oauth-protected-resource/http', (_req, res) => {
587
- res.json({
588
- jsonrpc: '2.0',
589
- result: {
590
- description: serverDescription || "Actual MCP server",
591
- instructions: serverInstructions || "Welcome to the Actual MCP server.",
592
- serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
593
- capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
594
- tools: toolsList,
595
- advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
596
- },
597
- });
598
+ const mcpInfo = () => ({
599
+ description: serverDescription || "Actual MCP server",
600
+ instructions: serverInstructions || "Welcome to the Actual MCP server.",
601
+ serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
602
+ capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
603
+ tools: toolsList,
604
+ advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
598
605
  });
606
+ app.get('/mcp-info', (_req, res) => { res.json(mcpInfo()); });
607
+ app.get('/mcp-info/http', (_req, res) => { res.json(mcpInfo()); });
599
608
  app.get('/health', (_req, res) => {
600
609
  const state = getConnectionState();
601
610
  const poolStats = state.connectionPool || null;
@@ -4,8 +4,10 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
4
4
  const fetchAny = globalFetch ?? (await import('node-fetch')).default;
5
5
  const fetchFn = fetchAny;
6
6
  const base = new URL(advertisedUrl);
7
- // quick probe endpoint
8
- const probeUrl = `${base.origin}/.well-known/oauth-protected-resource`;
7
+ // quick probe endpoint (#286: the server-info probe moved off the reserved
8
+ // /.well-known/oauth-protected-resource OAuth path to the non-reserved /mcp-info,
9
+ // now a bare object; the `result ?? raw` unwrap below tolerates both shapes).
10
+ const probeUrl = `${base.origin}/mcp-info`;
9
11
  console.info(`MCP client test: probing ${probeUrl}`);
10
12
  const probeRes = await fetchFn(probeUrl, { method: 'GET' });
11
13
  if (!probeRes.ok) {
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.8.11",
4
+ "version": "0.8.13",
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/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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_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/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/payees_delete.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/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/budget_acl_enforcement.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/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/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/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/payee_rules_reference_filter.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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_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/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/payees_delete.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/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/budget_acl_enforcement.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/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/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/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/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",
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",