@adcp/sdk 14.3.0 → 14.4.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.
Files changed (33) hide show
  1. package/bin/adcp.js +2 -4
  2. package/dist/lib/auth/oauth/MCPOAuthProvider.d.mts +6 -2
  3. package/dist/lib/auth/oauth/MCPOAuthProvider.d.ts +6 -2
  4. package/dist/lib/auth/oauth/MCPOAuthProvider.js +63 -3
  5. package/dist/lib/auth/oauth/MCPOAuthProvider.mjs +75 -3
  6. package/dist/lib/auth/oauth/diagnose.js +14 -0
  7. package/dist/lib/auth/oauth/diagnose.mjs +14 -0
  8. package/dist/lib/auth/oauth/file-storage.d.mts +4 -2
  9. package/dist/lib/auth/oauth/file-storage.d.ts +4 -2
  10. package/dist/lib/auth/oauth/file-storage.js +7 -1
  11. package/dist/lib/auth/oauth/file-storage.mjs +7 -1
  12. package/dist/lib/auth/oauth/types.d.mts +10 -0
  13. package/dist/lib/auth/oauth/types.d.ts +10 -0
  14. package/dist/lib/auth/oauth/types.js +39 -0
  15. package/dist/lib/auth/oauth/types.mjs +37 -0
  16. package/dist/lib/auth/oauth/web-flow.js +11 -1
  17. package/dist/lib/auth/oauth/web-flow.mjs +20 -2
  18. package/dist/lib/protocols/mcp.js +2 -0
  19. package/dist/lib/protocols/mcp.mjs +2 -0
  20. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  21. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  22. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  23. package/dist/lib/types/adcp.d.mts +6 -0
  24. package/dist/lib/types/adcp.d.ts +6 -0
  25. package/dist/lib/version.d.mts +3 -3
  26. package/dist/lib/version.d.ts +3 -3
  27. package/dist/lib/version.js +3 -3
  28. package/dist/lib/version.mjs +3 -3
  29. package/docs/TYPE-SUMMARY.md +1 -1
  30. package/docs/llms.txt +1 -1
  31. package/docs/migration-14.x-rc-worksheet.md +5 -5
  32. package/docs/migration-oauth-issuer-bindings.md +82 -0
  33. package/package.json +5 -5
package/bin/adcp.js CHANGED
@@ -1386,9 +1386,7 @@ async function ensureOAuthTokensForAlias(alias, url, { quiet = false, allowHttp
1386
1386
  return existing;
1387
1387
  }
1388
1388
 
1389
- // Spread the in-flight PKCE verifier too: if a prior attempt crashed mid-
1390
- // flow the verifier lives on the saved record, and without it the MCP SDK
1391
- // throws "No PKCE code verifier found" when the AS redirects back.
1389
+ // A fresh login starts a new PKCE leg and rediscovers the authorization server.
1392
1390
  const tempAgent = {
1393
1391
  id: alias,
1394
1392
  name: alias,
@@ -1396,7 +1394,6 @@ async function ensureOAuthTokensForAlias(alias, url, { quiet = false, allowHttp
1396
1394
  protocol: 'mcp',
1397
1395
  ...(existing?.oauth_client && { oauth_client: existing.oauth_client }),
1398
1396
  ...(existing?.oauth_tokens && { oauth_tokens: existing.oauth_tokens }),
1399
- ...(existing?.oauth_code_verifier && { oauth_code_verifier: existing.oauth_code_verifier }),
1400
1397
  };
1401
1398
 
1402
1399
  const { Client: MCPClient } = require('@modelcontextprotocol/sdk/client/index.js');
@@ -6671,6 +6668,7 @@ credential material — never sync or commit.
6671
6668
  delete agentConfig.oauth_tokens;
6672
6669
  delete agentConfig.oauth_client;
6673
6670
  delete agentConfig.oauth_code_verifier;
6671
+ delete agentConfig.oauth_discovery_state;
6674
6672
  saveAgent(alias, agentConfig);
6675
6673
 
6676
6674
  console.log(`\n✅ Cleared OAuth tokens for '${alias}'`);
@@ -4,8 +4,9 @@
4
4
  * Implements the MCP SDK's OAuthClientProvider interface
5
5
  * using AgentConfig for token storage.
6
6
  */
7
- import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
7
+ import type { OAuthClientProvider, OAuthDiscoveryState } from '@modelcontextprotocol/sdk/client/auth.js';
8
8
  import type { OAuthClientMetadata, OAuthClientInformation, OAuthClientInformationFull, OAuthTokens, OAuthFlowHandler, OAuthProviderConfig, OAuthConfigStorage, AgentConfig } from './types.mjs';
9
+ export declare function assertLegacyOAuthSdk(): void;
9
10
  /**
10
11
  * MCP OAuth Client Provider
11
12
  *
@@ -90,6 +91,9 @@ export declare class MCPOAuthProvider implements OAuthClientProvider {
90
91
  * Redirect user to authorization URL
91
92
  */
92
93
  redirectToAuthorization(authorizationUrl: URL): Promise<void>;
94
+ /** Persist the authorization-server identity across the callback leg. */
95
+ saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
96
+ discoveryState(): Promise<OAuthDiscoveryState | undefined>;
93
97
  /**
94
98
  * Save PKCE code verifier
95
99
  */
@@ -103,7 +107,7 @@ export declare class MCPOAuthProvider implements OAuthClientProvider {
103
107
  /**
104
108
  * Invalidate credentials when server indicates they're invalid
105
109
  */
106
- invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier'): Promise<void>;
110
+ invalidateCredentials(scope: Parameters<NonNullable<OAuthClientProvider['invalidateCredentials']>>[0]): Promise<void>;
107
111
  /**
108
112
  * Persist agent config to storage if configured
109
113
  */
@@ -4,8 +4,9 @@
4
4
  * Implements the MCP SDK's OAuthClientProvider interface
5
5
  * using AgentConfig for token storage.
6
6
  */
7
- import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
7
+ import type { OAuthClientProvider, OAuthDiscoveryState } from '@modelcontextprotocol/sdk/client/auth.js';
8
8
  import type { OAuthClientMetadata, OAuthClientInformation, OAuthClientInformationFull, OAuthTokens, OAuthFlowHandler, OAuthProviderConfig, OAuthConfigStorage, AgentConfig } from './types';
9
+ export declare function assertLegacyOAuthSdk(): void;
9
10
  /**
10
11
  * MCP OAuth Client Provider
11
12
  *
@@ -90,6 +91,9 @@ export declare class MCPOAuthProvider implements OAuthClientProvider {
90
91
  * Redirect user to authorization URL
91
92
  */
92
93
  redirectToAuthorization(authorizationUrl: URL): Promise<void>;
94
+ /** Persist the authorization-server identity across the callback leg. */
95
+ saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
96
+ discoveryState(): Promise<OAuthDiscoveryState | undefined>;
93
97
  /**
94
98
  * Save PKCE code verifier
95
99
  */
@@ -103,7 +107,7 @@ export declare class MCPOAuthProvider implements OAuthClientProvider {
103
107
  /**
104
108
  * Invalidate credentials when server indicates they're invalid
105
109
  */
106
- invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier'): Promise<void>;
110
+ invalidateCredentials(scope: Parameters<NonNullable<OAuthClientProvider['invalidateCredentials']>>[0]): Promise<void>;
107
111
  /**
108
112
  * Persist agent config to storage if configured
109
113
  */
@@ -18,12 +18,40 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var MCPOAuthProvider_exports = {};
20
20
  __export(MCPOAuthProvider_exports, {
21
- MCPOAuthProvider: () => MCPOAuthProvider
21
+ MCPOAuthProvider: () => MCPOAuthProvider,
22
+ assertLegacyOAuthSdk: () => assertLegacyOAuthSdk
22
23
  });
23
24
  module.exports = __toCommonJS(MCPOAuthProvider_exports);
24
25
  var import_types = require('./types.js');
25
26
  var import_crypto = require("crypto");
27
+ var import_node_fs = require("node:fs");
28
+ var import_node_path = require("node:path");
29
+ var import_node_module = require("node:module");
26
30
  var import_resource_url = require('./resource-url.js');
31
+ let legacyOAuthSdkChecked = false;
32
+ function assertLegacyOAuthSdk() {
33
+ if (legacyOAuthSdkChecked) return;
34
+ try {
35
+ const clientPath = (0, import_node_module.createRequire)((0, import_node_path.resolve)(__dirname, "MCPOAuthProvider.js")).resolve(
36
+ "@modelcontextprotocol/sdk/client/index.js"
37
+ );
38
+ const { version } = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.resolve)((0, import_node_path.dirname)(clientPath), "../../../package.json"), "utf8"));
39
+ const match = typeof version === "string" && /^1\.(\d+)\.\d+$/.exec(version);
40
+ if (!match || Number(match[1]) < 31) {
41
+ throw new import_types.OAuthError(
42
+ "Legacy MCP OAuth requires @modelcontextprotocol/sdk >=1.31.0 within major 1. Upgrade the installed peer dependency.",
43
+ "mcp_oauth_sdk_upgrade_required"
44
+ );
45
+ }
46
+ } catch (error) {
47
+ if (error instanceof import_types.OAuthError) throw error;
48
+ throw new import_types.OAuthError(
49
+ "Cannot verify a patched legacy MCP SDK. Install @modelcontextprotocol/sdk ^1.31.0 and keep it external when bundling.",
50
+ "mcp_oauth_sdk_upgrade_required"
51
+ );
52
+ }
53
+ legacyOAuthSdkChecked = true;
54
+ }
27
55
  class MCPOAuthProvider {
28
56
  agent;
29
57
  storage;
@@ -97,6 +125,8 @@ class MCPOAuthProvider {
97
125
  */
98
126
  async clientInformation() {
99
127
  if (this.agent.oauth_client) {
128
+ if (this.agent.oauth_client.client_secret) assertLegacyOAuthSdk();
129
+ (0, import_types.assertOAuthCredentialIssuer)(this.agent.oauth_client, void 0, !!this.agent.oauth_client.client_secret);
100
130
  return (0, import_types.toMCPClientInfo)(this.agent.oauth_client);
101
131
  }
102
132
  return void 0;
@@ -113,6 +143,8 @@ class MCPOAuthProvider {
113
143
  */
114
144
  async tokens() {
115
145
  if (this.agent.oauth_tokens) {
146
+ if (this.agent.oauth_tokens.refresh_token) assertLegacyOAuthSdk();
147
+ (0, import_types.assertOAuthCredentialIssuer)(this.agent.oauth_tokens, void 0, !!this.agent.oauth_tokens.refresh_token);
116
148
  return (0, import_types.toMCPTokens)(this.agent.oauth_tokens);
117
149
  }
118
150
  return void 0;
@@ -124,13 +156,34 @@ class MCPOAuthProvider {
124
156
  async saveTokens(tokens) {
125
157
  this.agent.oauth_tokens = (0, import_types.fromMCPTokens)(tokens);
126
158
  delete this.agent.oauth_code_verifier;
159
+ delete this.agent.oauth_discovery_state;
127
160
  await this.persistAgent();
128
161
  }
129
162
  /**
130
163
  * Redirect user to authorization URL
131
164
  */
132
165
  async redirectToAuthorization(authorizationUrl) {
133
- await this.flowHandler.redirectToAuthorization(authorizationUrl);
166
+ try {
167
+ await this.flowHandler.redirectToAuthorization(authorizationUrl);
168
+ } catch (error) {
169
+ await this.invalidateCredentials("verifier");
170
+ throw error;
171
+ }
172
+ }
173
+ /** Persist the authorization-server identity across the callback leg. */
174
+ async saveDiscoveryState(state) {
175
+ const issuer = String(state.authorizationServerUrl);
176
+ if (this.agent.oauth_tokens?.refresh_token) {
177
+ (0, import_types.assertOAuthCredentialIssuer)(this.agent.oauth_tokens, issuer, true);
178
+ }
179
+ if (this.agent.oauth_client?.client_secret) {
180
+ (0, import_types.assertOAuthCredentialIssuer)(this.agent.oauth_client, issuer, true);
181
+ }
182
+ this.agent.oauth_discovery_state = state;
183
+ await this.persistAgent();
184
+ }
185
+ async discoveryState() {
186
+ return this.agent.oauth_code_verifier ? this.agent.oauth_discovery_state : void 0;
134
187
  }
135
188
  /**
136
189
  * Save PKCE code verifier
@@ -158,6 +211,7 @@ class MCPOAuthProvider {
158
211
  async invalidateCredentials(scope) {
159
212
  switch (scope) {
160
213
  case "all":
214
+ delete this.agent.oauth_discovery_state;
161
215
  delete this.agent.oauth_tokens;
162
216
  delete this.agent.oauth_client;
163
217
  delete this.agent.oauth_code_verifier;
@@ -166,9 +220,14 @@ class MCPOAuthProvider {
166
220
  delete this.agent.oauth_tokens;
167
221
  break;
168
222
  case "client":
223
+ delete this.agent.oauth_discovery_state;
169
224
  delete this.agent.oauth_client;
170
225
  break;
226
+ case "discovery":
227
+ delete this.agent.oauth_discovery_state;
228
+ break;
171
229
  case "verifier":
230
+ delete this.agent.oauth_discovery_state;
172
231
  delete this.agent.oauth_code_verifier;
173
232
  break;
174
233
  }
@@ -243,5 +302,6 @@ class MCPOAuthProvider {
243
302
  }
244
303
  // Annotate the CommonJS export names for ESM import in node:
245
304
  0 && (module.exports = {
246
- MCPOAuthProvider
305
+ MCPOAuthProvider,
306
+ assertLegacyOAuthSdk
247
307
  });
@@ -1,6 +1,46 @@
1
- import { DEFAULT_CLIENT_METADATA, toMCPTokens, fromMCPTokens, toMCPClientInfo, fromMCPClientInfo } from "./types.mjs";
1
+ import { fileURLToPath as __adcpFileURLToPath } from "node:url";
2
+ import { dirname as __adcpDirname } from "node:path";
3
+ import { createRequire as __adcpCreateRequire } from "node:module";
4
+ const __dirname = __adcpDirname(__adcpFileURLToPath(import.meta.url));
5
+ const require2 = __adcpCreateRequire(import.meta.url);
6
+ import {
7
+ DEFAULT_CLIENT_METADATA,
8
+ toMCPTokens,
9
+ fromMCPTokens,
10
+ toMCPClientInfo,
11
+ fromMCPClientInfo,
12
+ assertOAuthCredentialIssuer,
13
+ OAuthError
14
+ } from "./types.mjs";
2
15
  import { randomBytes } from "crypto";
16
+ import { readFileSync } from "node:fs";
17
+ import { dirname, resolve } from "node:path";
18
+ import { createRequire } from "node:module";
3
19
  import { validateOAuthResourceUrl } from "./resource-url.mjs";
20
+ let legacyOAuthSdkChecked = false;
21
+ function assertLegacyOAuthSdk() {
22
+ if (legacyOAuthSdkChecked) return;
23
+ try {
24
+ const clientPath = createRequire(resolve(__dirname, "MCPOAuthProvider.js")).resolve(
25
+ "@modelcontextprotocol/sdk/client/index.js"
26
+ );
27
+ const { version } = JSON.parse(readFileSync(resolve(dirname(clientPath), "../../../package.json"), "utf8"));
28
+ const match = typeof version === "string" && /^1\.(\d+)\.\d+$/.exec(version);
29
+ if (!match || Number(match[1]) < 31) {
30
+ throw new OAuthError(
31
+ "Legacy MCP OAuth requires @modelcontextprotocol/sdk >=1.31.0 within major 1. Upgrade the installed peer dependency.",
32
+ "mcp_oauth_sdk_upgrade_required"
33
+ );
34
+ }
35
+ } catch (error) {
36
+ if (error instanceof OAuthError) throw error;
37
+ throw new OAuthError(
38
+ "Cannot verify a patched legacy MCP SDK. Install @modelcontextprotocol/sdk ^1.31.0 and keep it external when bundling.",
39
+ "mcp_oauth_sdk_upgrade_required"
40
+ );
41
+ }
42
+ legacyOAuthSdkChecked = true;
43
+ }
4
44
  class MCPOAuthProvider {
5
45
  agent;
6
46
  storage;
@@ -74,6 +114,8 @@ class MCPOAuthProvider {
74
114
  */
75
115
  async clientInformation() {
76
116
  if (this.agent.oauth_client) {
117
+ if (this.agent.oauth_client.client_secret) assertLegacyOAuthSdk();
118
+ assertOAuthCredentialIssuer(this.agent.oauth_client, void 0, !!this.agent.oauth_client.client_secret);
77
119
  return toMCPClientInfo(this.agent.oauth_client);
78
120
  }
79
121
  return void 0;
@@ -90,6 +132,8 @@ class MCPOAuthProvider {
90
132
  */
91
133
  async tokens() {
92
134
  if (this.agent.oauth_tokens) {
135
+ if (this.agent.oauth_tokens.refresh_token) assertLegacyOAuthSdk();
136
+ assertOAuthCredentialIssuer(this.agent.oauth_tokens, void 0, !!this.agent.oauth_tokens.refresh_token);
93
137
  return toMCPTokens(this.agent.oauth_tokens);
94
138
  }
95
139
  return void 0;
@@ -101,13 +145,34 @@ class MCPOAuthProvider {
101
145
  async saveTokens(tokens) {
102
146
  this.agent.oauth_tokens = fromMCPTokens(tokens);
103
147
  delete this.agent.oauth_code_verifier;
148
+ delete this.agent.oauth_discovery_state;
104
149
  await this.persistAgent();
105
150
  }
106
151
  /**
107
152
  * Redirect user to authorization URL
108
153
  */
109
154
  async redirectToAuthorization(authorizationUrl) {
110
- await this.flowHandler.redirectToAuthorization(authorizationUrl);
155
+ try {
156
+ await this.flowHandler.redirectToAuthorization(authorizationUrl);
157
+ } catch (error) {
158
+ await this.invalidateCredentials("verifier");
159
+ throw error;
160
+ }
161
+ }
162
+ /** Persist the authorization-server identity across the callback leg. */
163
+ async saveDiscoveryState(state) {
164
+ const issuer = String(state.authorizationServerUrl);
165
+ if (this.agent.oauth_tokens?.refresh_token) {
166
+ assertOAuthCredentialIssuer(this.agent.oauth_tokens, issuer, true);
167
+ }
168
+ if (this.agent.oauth_client?.client_secret) {
169
+ assertOAuthCredentialIssuer(this.agent.oauth_client, issuer, true);
170
+ }
171
+ this.agent.oauth_discovery_state = state;
172
+ await this.persistAgent();
173
+ }
174
+ async discoveryState() {
175
+ return this.agent.oauth_code_verifier ? this.agent.oauth_discovery_state : void 0;
111
176
  }
112
177
  /**
113
178
  * Save PKCE code verifier
@@ -135,6 +200,7 @@ class MCPOAuthProvider {
135
200
  async invalidateCredentials(scope) {
136
201
  switch (scope) {
137
202
  case "all":
203
+ delete this.agent.oauth_discovery_state;
138
204
  delete this.agent.oauth_tokens;
139
205
  delete this.agent.oauth_client;
140
206
  delete this.agent.oauth_code_verifier;
@@ -143,9 +209,14 @@ class MCPOAuthProvider {
143
209
  delete this.agent.oauth_tokens;
144
210
  break;
145
211
  case "client":
212
+ delete this.agent.oauth_discovery_state;
146
213
  delete this.agent.oauth_client;
147
214
  break;
215
+ case "discovery":
216
+ delete this.agent.oauth_discovery_state;
217
+ break;
148
218
  case "verifier":
219
+ delete this.agent.oauth_discovery_state;
149
220
  delete this.agent.oauth_code_verifier;
150
221
  break;
151
222
  }
@@ -219,5 +290,6 @@ class MCPOAuthProvider {
219
290
  }
220
291
  }
221
292
  export {
222
- MCPOAuthProvider
293
+ MCPOAuthProvider,
294
+ assertLegacyOAuthSdk
223
295
  };
@@ -23,6 +23,7 @@ __export(diagnose_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(diagnose_exports);
25
25
  var import_net = require('../../net/index.js');
26
+ var import_types = require('./types.js');
26
27
  var import_diagnostics = require('./diagnostics.js');
27
28
  const AUTH_DIAGNOSIS_SCHEMA_VERSION = 1;
28
29
  async function runAuthDiagnosis(agent, options = {}) {
@@ -48,6 +49,17 @@ async function runAuthDiagnosis(agent, options = {}) {
48
49
  if (!options.skipRefresh && agent.oauth_tokens?.refresh_token) {
49
50
  const tokenEndpoint = extractTokenEndpoint(asCapture);
50
51
  const clientId = agent.oauth_client?.client_id;
52
+ let issuerError;
53
+ if (tokenEndpoint && clientId) {
54
+ try {
55
+ const issuer = new URL(extractIssuer(prmCapture) ?? "").toString();
56
+ (0, import_types.assertOAuthServerIssuer)(asCapture.body, issuer);
57
+ (0, import_types.assertOAuthCredentialIssuer)(agent.oauth_tokens, issuer, true);
58
+ (0, import_types.assertOAuthCredentialIssuer)(agent.oauth_client, issuer, !!agent.oauth_client?.client_secret);
59
+ } catch (error) {
60
+ issuerError = error instanceof import_types.OAuthError ? error.message : "Authorization-server issuer could not be validated";
61
+ }
62
+ }
51
63
  const resource = agentUrl;
52
64
  if (!tokenEndpoint) {
53
65
  steps.push({
@@ -59,6 +71,8 @@ async function runAuthDiagnosis(agent, options = {}) {
59
71
  name: "token_refresh_attempt",
60
72
  error: "No saved oauth_client.client_id \u2014 cannot attempt refresh without it"
61
73
  });
74
+ } else if (issuerError) {
75
+ steps.push({ name: "token_refresh_attempt", error: issuerError });
62
76
  } else {
63
77
  const refreshCapture = await attemptTokenRefresh({
64
78
  tokenEndpoint,
@@ -1,4 +1,5 @@
1
1
  import { ssrfSafeFetch, decodeBodyAsJsonOrText, SsrfRefusedError } from "../../net/index.mjs";
2
+ import { assertOAuthCredentialIssuer, assertOAuthServerIssuer, OAuthError } from "./types.mjs";
2
3
  import { decodeAccessTokenClaims, parseWWWAuthenticate, validateTokenAudience } from "./diagnostics.mjs";
3
4
  const AUTH_DIAGNOSIS_SCHEMA_VERSION = 1;
4
5
  async function runAuthDiagnosis(agent, options = {}) {
@@ -24,6 +25,17 @@ async function runAuthDiagnosis(agent, options = {}) {
24
25
  if (!options.skipRefresh && agent.oauth_tokens?.refresh_token) {
25
26
  const tokenEndpoint = extractTokenEndpoint(asCapture);
26
27
  const clientId = agent.oauth_client?.client_id;
28
+ let issuerError;
29
+ if (tokenEndpoint && clientId) {
30
+ try {
31
+ const issuer = new URL(extractIssuer(prmCapture) ?? "").toString();
32
+ assertOAuthServerIssuer(asCapture.body, issuer);
33
+ assertOAuthCredentialIssuer(agent.oauth_tokens, issuer, true);
34
+ assertOAuthCredentialIssuer(agent.oauth_client, issuer, !!agent.oauth_client?.client_secret);
35
+ } catch (error) {
36
+ issuerError = error instanceof OAuthError ? error.message : "Authorization-server issuer could not be validated";
37
+ }
38
+ }
27
39
  const resource = agentUrl;
28
40
  if (!tokenEndpoint) {
29
41
  steps.push({
@@ -35,6 +47,8 @@ async function runAuthDiagnosis(agent, options = {}) {
35
47
  name: "token_refresh_attempt",
36
48
  error: "No saved oauth_client.client_id \u2014 cannot attempt refresh without it"
37
49
  });
50
+ } else if (issuerError) {
51
+ steps.push({ name: "token_refresh_attempt", error: issuerError });
38
52
  } else {
39
53
  const refreshCapture = await attemptTokenRefresh({
40
54
  tokenEndpoint,
@@ -28,8 +28,10 @@ export interface FileOAuthStorageOptions {
28
28
  *
29
29
  * Does NOT touch fields outside the OAuth envelope (`auth_token`, custom
30
30
  * fields) when saving — the MCP provider only mutates `oauth_tokens`,
31
- * `oauth_client`, `oauth_resource`, and `oauth_code_verifier`, and we preserve
32
- * everything else.
31
+ * `oauth_client`, `oauth_resource`, `oauth_code_verifier`, and `oauth_discovery_state`.
32
+ * Those fields are authoritative: an absent value deletes the stored value.
33
+ * Load the complete agent before constructing a provider; partial snapshots
34
+ * cannot preserve omitted OAuth fields. Other fields keep merge semantics.
33
35
  *
34
36
  * @example
35
37
  * ```ts
@@ -28,8 +28,10 @@ export interface FileOAuthStorageOptions {
28
28
  *
29
29
  * Does NOT touch fields outside the OAuth envelope (`auth_token`, custom
30
30
  * fields) when saving — the MCP provider only mutates `oauth_tokens`,
31
- * `oauth_client`, `oauth_resource`, and `oauth_code_verifier`, and we preserve
32
- * everything else.
31
+ * `oauth_client`, `oauth_resource`, `oauth_code_verifier`, and `oauth_discovery_state`.
32
+ * Those fields are authoritative: an absent value deletes the stored value.
33
+ * Load the complete agent before constructing a provider; partial snapshots
34
+ * cannot preserve omitted OAuth fields. Other fields keep merge semantics.
33
35
  *
34
36
  * @example
35
37
  * ```ts
@@ -75,7 +75,8 @@ function createFileOAuthStorage(options) {
75
75
  oauth_client: stored.oauth_client,
76
76
  oauth_resource: stored.oauth_resource,
77
77
  oauth_client_credentials: stored.oauth_client_credentials,
78
- oauth_code_verifier: stored.oauth_code_verifier
78
+ oauth_code_verifier: stored.oauth_code_verifier,
79
+ oauth_discovery_state: stored.oauth_discovery_state
79
80
  };
80
81
  },
81
82
  async saveAgent(agent) {
@@ -93,6 +94,11 @@ function createFileOAuthStorage(options) {
93
94
  ...agent.oauth_client_credentials !== void 0 ? { oauth_client_credentials: agent.oauth_client_credentials } : {},
94
95
  ...agent.oauth_code_verifier !== void 0 ? { oauth_code_verifier: agent.oauth_code_verifier } : {}
95
96
  };
97
+ if (agent.oauth_tokens === void 0) delete next.oauth_tokens;
98
+ if (agent.oauth_client === void 0) delete next.oauth_client;
99
+ if (agent.oauth_code_verifier === void 0) delete next.oauth_code_verifier;
100
+ if (agent.oauth_discovery_state === void 0) delete next.oauth_discovery_state;
101
+ else next.oauth_discovery_state = agent.oauth_discovery_state;
96
102
  if (agent.oauth_resource === void 0) delete next.oauth_resource;
97
103
  config.agents[key] = next;
98
104
  await writeConfig(config);
@@ -42,7 +42,8 @@ function createFileOAuthStorage(options) {
42
42
  oauth_client: stored.oauth_client,
43
43
  oauth_resource: stored.oauth_resource,
44
44
  oauth_client_credentials: stored.oauth_client_credentials,
45
- oauth_code_verifier: stored.oauth_code_verifier
45
+ oauth_code_verifier: stored.oauth_code_verifier,
46
+ oauth_discovery_state: stored.oauth_discovery_state
46
47
  };
47
48
  },
48
49
  async saveAgent(agent) {
@@ -60,6 +61,11 @@ function createFileOAuthStorage(options) {
60
61
  ...agent.oauth_client_credentials !== void 0 ? { oauth_client_credentials: agent.oauth_client_credentials } : {},
61
62
  ...agent.oauth_code_verifier !== void 0 ? { oauth_code_verifier: agent.oauth_code_verifier } : {}
62
63
  };
64
+ if (agent.oauth_tokens === void 0) delete next.oauth_tokens;
65
+ if (agent.oauth_client === void 0) delete next.oauth_client;
66
+ if (agent.oauth_code_verifier === void 0) delete next.oauth_code_verifier;
67
+ if (agent.oauth_discovery_state === void 0) delete next.oauth_discovery_state;
68
+ else next.oauth_discovery_state = agent.oauth_discovery_state;
63
69
  if (agent.oauth_resource === void 0) delete next.oauth_resource;
64
70
  config.agents[key] = next;
65
71
  await writeConfig(config);
@@ -13,6 +13,8 @@ export type { AgentConfig, AgentOAuthTokens, AgentOAuthClient };
13
13
  * Agent config storage interface
14
14
  *
15
15
  * Implement this to persist OAuth tokens back to agent configuration.
16
+ * Round-trip pending `oauth_code_verifier` and `oauth_discovery_state` verbatim,
17
+ * and remove OAuth fields absent from the authoritative agent snapshot.
16
18
  * This allows different storage backends (file, database, memory)
17
19
  * while keeping tokens in the agent config structure.
18
20
  */
@@ -132,3 +134,11 @@ export declare function toMCPClientInfo(client: AgentOAuthClient): OAuthClientIn
132
134
  * Convert MCP SDK OAuthClientInformationFull to AgentOAuthClient format
133
135
  */
134
136
  export declare function fromMCPClientInfo(info: OAuthClientInformationFull): AgentOAuthClient;
137
+ /** Internal credential-binding guard; old secrets must be migrated from trusted configuration. */
138
+ export declare function assertOAuthCredentialIssuer(credential: {
139
+ issuer?: unknown;
140
+ }, expectedIssuer?: string, requireIssuer?: boolean): void;
141
+ /** Internal metadata guard, matching the modern MCP SDK's asymmetric issuer-echo rule. */
142
+ export declare function assertOAuthServerIssuer(metadata: {
143
+ issuer?: unknown;
144
+ } | undefined, authorizationServerUrl: string): void;
@@ -13,6 +13,8 @@ export type { AgentConfig, AgentOAuthTokens, AgentOAuthClient };
13
13
  * Agent config storage interface
14
14
  *
15
15
  * Implement this to persist OAuth tokens back to agent configuration.
16
+ * Round-trip pending `oauth_code_verifier` and `oauth_discovery_state` verbatim,
17
+ * and remove OAuth fields absent from the authoritative agent snapshot.
16
18
  * This allows different storage backends (file, database, memory)
17
19
  * while keeping tokens in the agent config structure.
18
20
  */
@@ -132,3 +134,11 @@ export declare function toMCPClientInfo(client: AgentOAuthClient): OAuthClientIn
132
134
  * Convert MCP SDK OAuthClientInformationFull to AgentOAuthClient format
133
135
  */
134
136
  export declare function fromMCPClientInfo(info: OAuthClientInformationFull): AgentOAuthClient;
137
+ /** Internal credential-binding guard; old secrets must be migrated from trusted configuration. */
138
+ export declare function assertOAuthCredentialIssuer(credential: {
139
+ issuer?: unknown;
140
+ }, expectedIssuer?: string, requireIssuer?: boolean): void;
141
+ /** Internal metadata guard, matching the modern MCP SDK's asymmetric issuer-echo rule. */
142
+ export declare function assertOAuthServerIssuer(metadata: {
143
+ issuer?: unknown;
144
+ } | undefined, authorizationServerUrl: string): void;
@@ -22,6 +22,8 @@ __export(types_exports, {
22
22
  OAuthCancelledError: () => OAuthCancelledError,
23
23
  OAuthError: () => OAuthError,
24
24
  OAuthTimeoutError: () => OAuthTimeoutError,
25
+ assertOAuthCredentialIssuer: () => assertOAuthCredentialIssuer,
26
+ assertOAuthServerIssuer: () => assertOAuthServerIssuer,
25
27
  fromMCPClientInfo: () => fromMCPClientInfo,
26
28
  fromMCPTokens: () => fromMCPTokens,
27
29
  toMCPClientInfo: () => toMCPClientInfo,
@@ -60,6 +62,7 @@ class OAuthTimeoutError extends OAuthError {
60
62
  }
61
63
  function toMCPTokens(tokens) {
62
64
  return {
65
+ ...tokens.issuer !== void 0 && tokens.issuer !== null && { issuer: tokens.issuer },
63
66
  access_token: tokens.access_token,
64
67
  refresh_token: tokens.refresh_token,
65
68
  token_type: tokens.token_type || "Bearer",
@@ -69,6 +72,7 @@ function toMCPTokens(tokens) {
69
72
  }
70
73
  function fromMCPTokens(tokens) {
71
74
  const result = {
75
+ ...tokens.issuer !== void 0 && tokens.issuer !== null && { issuer: tokens.issuer },
72
76
  access_token: tokens.access_token,
73
77
  token_type: tokens.token_type,
74
78
  scope: tokens.scope
@@ -84,6 +88,7 @@ function fromMCPTokens(tokens) {
84
88
  }
85
89
  function toMCPClientInfo(client) {
86
90
  return {
91
+ ...client.issuer !== void 0 && client.issuer !== null && { issuer: client.issuer },
87
92
  client_id: client.client_id,
88
93
  client_secret: client.client_secret,
89
94
  client_secret_expires_at: client.client_secret_expires_at
@@ -91,17 +96,51 @@ function toMCPClientInfo(client) {
91
96
  }
92
97
  function fromMCPClientInfo(info) {
93
98
  return {
99
+ ...info.issuer !== void 0 && info.issuer !== null && { issuer: info.issuer },
94
100
  client_id: info.client_id,
95
101
  client_secret: info.client_secret,
96
102
  client_secret_expires_at: info.client_secret_expires_at
97
103
  };
98
104
  }
105
+ function isOAuthIssuer(value) {
106
+ if (typeof value !== "string" || value.length === 0) return false;
107
+ try {
108
+ const url = new URL(value);
109
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && !url.search && !url.hash;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ function assertOAuthCredentialIssuer(credential, expectedIssuer, requireIssuer = false) {
115
+ const issuer = credential.issuer;
116
+ if ((issuer === void 0 || issuer === null) && !requireIssuer) return;
117
+ if (!isOAuthIssuer(issuer)) {
118
+ throw new OAuthError(
119
+ "Saved OAuth credentials need a valid issuer. Clear them and sign in again, or set their issuer from trusted configuration.",
120
+ "oauth_issuer_required"
121
+ );
122
+ }
123
+ if (expectedIssuer !== void 0 && issuer !== expectedIssuer && !(issuer.endsWith("/") && issuer.slice(0, -1) === expectedIssuer) && !(expectedIssuer.endsWith("/") && expectedIssuer.slice(0, -1) === issuer)) {
124
+ throw new OAuthError("OAuth credentials belong to a different authorization server.", "oauth_issuer_mismatch");
125
+ }
126
+ }
127
+ function assertOAuthServerIssuer(metadata, authorizationServerUrl) {
128
+ const issuer = metadata?.issuer;
129
+ if (!isOAuthIssuer(issuer) || issuer !== authorizationServerUrl && !(authorizationServerUrl.endsWith("/") && issuer === authorizationServerUrl.slice(0, -1))) {
130
+ throw new OAuthError(
131
+ "Authorization-server metadata does not match the discovered issuer.",
132
+ "oauth_issuer_mismatch"
133
+ );
134
+ }
135
+ }
99
136
  // Annotate the CommonJS export names for ESM import in node:
100
137
  0 && (module.exports = {
101
138
  DEFAULT_CLIENT_METADATA,
102
139
  OAuthCancelledError,
103
140
  OAuthError,
104
141
  OAuthTimeoutError,
142
+ assertOAuthCredentialIssuer,
143
+ assertOAuthServerIssuer,
105
144
  fromMCPClientInfo,
106
145
  fromMCPTokens,
107
146
  toMCPClientInfo,
@@ -30,6 +30,7 @@ class OAuthTimeoutError extends OAuthError {
30
30
  }
31
31
  function toMCPTokens(tokens) {
32
32
  return {
33
+ ...tokens.issuer !== void 0 && tokens.issuer !== null && { issuer: tokens.issuer },
33
34
  access_token: tokens.access_token,
34
35
  refresh_token: tokens.refresh_token,
35
36
  token_type: tokens.token_type || "Bearer",
@@ -39,6 +40,7 @@ function toMCPTokens(tokens) {
39
40
  }
40
41
  function fromMCPTokens(tokens) {
41
42
  const result = {
43
+ ...tokens.issuer !== void 0 && tokens.issuer !== null && { issuer: tokens.issuer },
42
44
  access_token: tokens.access_token,
43
45
  token_type: tokens.token_type,
44
46
  scope: tokens.scope
@@ -54,6 +56,7 @@ function fromMCPTokens(tokens) {
54
56
  }
55
57
  function toMCPClientInfo(client) {
56
58
  return {
59
+ ...client.issuer !== void 0 && client.issuer !== null && { issuer: client.issuer },
57
60
  client_id: client.client_id,
58
61
  client_secret: client.client_secret,
59
62
  client_secret_expires_at: client.client_secret_expires_at
@@ -61,16 +64,50 @@ function toMCPClientInfo(client) {
61
64
  }
62
65
  function fromMCPClientInfo(info) {
63
66
  return {
67
+ ...info.issuer !== void 0 && info.issuer !== null && { issuer: info.issuer },
64
68
  client_id: info.client_id,
65
69
  client_secret: info.client_secret,
66
70
  client_secret_expires_at: info.client_secret_expires_at
67
71
  };
68
72
  }
73
+ function isOAuthIssuer(value) {
74
+ if (typeof value !== "string" || value.length === 0) return false;
75
+ try {
76
+ const url = new URL(value);
77
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && !url.search && !url.hash;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ function assertOAuthCredentialIssuer(credential, expectedIssuer, requireIssuer = false) {
83
+ const issuer = credential.issuer;
84
+ if ((issuer === void 0 || issuer === null) && !requireIssuer) return;
85
+ if (!isOAuthIssuer(issuer)) {
86
+ throw new OAuthError(
87
+ "Saved OAuth credentials need a valid issuer. Clear them and sign in again, or set their issuer from trusted configuration.",
88
+ "oauth_issuer_required"
89
+ );
90
+ }
91
+ if (expectedIssuer !== void 0 && issuer !== expectedIssuer && !(issuer.endsWith("/") && issuer.slice(0, -1) === expectedIssuer) && !(expectedIssuer.endsWith("/") && expectedIssuer.slice(0, -1) === issuer)) {
92
+ throw new OAuthError("OAuth credentials belong to a different authorization server.", "oauth_issuer_mismatch");
93
+ }
94
+ }
95
+ function assertOAuthServerIssuer(metadata, authorizationServerUrl) {
96
+ const issuer = metadata?.issuer;
97
+ if (!isOAuthIssuer(issuer) || issuer !== authorizationServerUrl && !(authorizationServerUrl.endsWith("/") && issuer === authorizationServerUrl.slice(0, -1))) {
98
+ throw new OAuthError(
99
+ "Authorization-server metadata does not match the discovered issuer.",
100
+ "oauth_issuer_mismatch"
101
+ );
102
+ }
103
+ }
69
104
  export {
70
105
  DEFAULT_CLIENT_METADATA,
71
106
  OAuthCancelledError,
72
107
  OAuthError,
73
108
  OAuthTimeoutError,
109
+ assertOAuthCredentialIssuer,
110
+ assertOAuthServerIssuer,
74
111
  fromMCPClientInfo,
75
112
  fromMCPTokens,
76
113
  toMCPClientInfo,
@@ -144,6 +144,7 @@ async function startWebOAuthFlow(opts) {
144
144
  if (!asMetadata) {
145
145
  throw new import_types.OAuthError(`No OAuth metadata at ${asUrl.toString()}`, "no_authorization_server_metadata", agent.id);
146
146
  }
147
+ (0, import_types.assertOAuthServerIssuer)(asMetadata, asUrl.toString());
147
148
  const resource = resourceOverride ? new URL(resourceOverride) : prm?.resource ? new URL(prm.resource) : (0, import_auth_utils.resourceUrlFromServerUrl)(new URL(agent.agent_uri));
148
149
  const baseClientMetadata = {
149
150
  ...import_types.DEFAULT_CLIENT_METADATA,
@@ -230,6 +231,12 @@ async function completeWebOAuthFlow(opts) {
230
231
  const asMetadata = await (0, import_auth.discoverAuthorizationServerMetadata)(flow.authorizationServerUrl, {
231
232
  fetchFn: guardedFetch
232
233
  });
234
+ (0, import_types.assertOAuthServerIssuer)(asMetadata, flow.authorizationServerUrl);
235
+ (0, import_types.assertOAuthCredentialIssuer)(
236
+ flow.clientInformation,
237
+ flow.authorizationServerUrl,
238
+ !!flow.clientInformation.client_secret
239
+ );
233
240
  let tokens;
234
241
  try {
235
242
  tokens = await (0, import_auth.exchangeAuthorization)(flow.authorizationServerUrl, {
@@ -241,6 +248,7 @@ async function completeWebOAuthFlow(opts) {
241
248
  resource: flow.resource ? new URL(flow.resource) : void 0,
242
249
  fetchFn: guardedFetch
243
250
  });
251
+ tokens = { ...tokens, issuer: flow.authorizationServerUrl };
244
252
  } catch (err) {
245
253
  throw wrapTokenExchangeError(err);
246
254
  }
@@ -410,6 +418,7 @@ function createSsrfSafeOAuthFetch(trustedFetchFn, allowHttp) {
410
418
  async function resolveClientInformation(args) {
411
419
  const { agent, agentStorage, asUrl, asMetadata, clientMetadata, fetchFn, allowConfidentialClient } = args;
412
420
  if (agent.oauth_client) {
421
+ (0, import_types.assertOAuthCredentialIssuer)(agent.oauth_client, asUrl.toString(), !!agent.oauth_client.client_secret);
413
422
  return (0, import_types.toMCPClientInfo)(agent.oauth_client);
414
423
  }
415
424
  if (!asMetadata.registration_endpoint) {
@@ -419,11 +428,12 @@ async function resolveClientInformation(args) {
419
428
  agent.id
420
429
  );
421
430
  }
422
- const registered = await (0, import_auth.registerClient)(asUrl.toString(), {
431
+ const registration = await (0, import_auth.registerClient)(asUrl.toString(), {
423
432
  metadata: asMetadata,
424
433
  clientMetadata,
425
434
  fetchFn
426
435
  });
436
+ const registered = { ...registration, issuer: asUrl.toString() };
427
437
  if (registered.client_secret && !allowConfidentialClient) {
428
438
  throw new ConfidentialClientNotAllowedError(agent.id);
429
439
  }
@@ -7,7 +7,15 @@ import {
7
7
  startAuthorization
8
8
  } from "@modelcontextprotocol/sdk/client/auth.js";
9
9
  import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
10
- import { DEFAULT_CLIENT_METADATA, fromMCPClientInfo, fromMCPTokens, OAuthError, toMCPClientInfo } from "./types.mjs";
10
+ import {
11
+ DEFAULT_CLIENT_METADATA,
12
+ fromMCPClientInfo,
13
+ fromMCPTokens,
14
+ OAuthError,
15
+ toMCPClientInfo,
16
+ assertOAuthCredentialIssuer,
17
+ assertOAuthServerIssuer
18
+ } from "./types.mjs";
11
19
  import { validateOAuthResourceUrl } from "./resource-url.mjs";
12
20
  import { ssrfSafeFetch } from "../../net/ssrf-fetch.mjs";
13
21
  const DEFAULT_WEB_FLOW_TTL_MS = 10 * 60 * 1e3;
@@ -115,6 +123,7 @@ async function startWebOAuthFlow(opts) {
115
123
  if (!asMetadata) {
116
124
  throw new OAuthError(`No OAuth metadata at ${asUrl.toString()}`, "no_authorization_server_metadata", agent.id);
117
125
  }
126
+ assertOAuthServerIssuer(asMetadata, asUrl.toString());
118
127
  const resource = resourceOverride ? new URL(resourceOverride) : prm?.resource ? new URL(prm.resource) : resourceUrlFromServerUrl(new URL(agent.agent_uri));
119
128
  const baseClientMetadata = {
120
129
  ...DEFAULT_CLIENT_METADATA,
@@ -201,6 +210,12 @@ async function completeWebOAuthFlow(opts) {
201
210
  const asMetadata = await discoverAuthorizationServerMetadata(flow.authorizationServerUrl, {
202
211
  fetchFn: guardedFetch
203
212
  });
213
+ assertOAuthServerIssuer(asMetadata, flow.authorizationServerUrl);
214
+ assertOAuthCredentialIssuer(
215
+ flow.clientInformation,
216
+ flow.authorizationServerUrl,
217
+ !!flow.clientInformation.client_secret
218
+ );
204
219
  let tokens;
205
220
  try {
206
221
  tokens = await exchangeAuthorization(flow.authorizationServerUrl, {
@@ -212,6 +227,7 @@ async function completeWebOAuthFlow(opts) {
212
227
  resource: flow.resource ? new URL(flow.resource) : void 0,
213
228
  fetchFn: guardedFetch
214
229
  });
230
+ tokens = { ...tokens, issuer: flow.authorizationServerUrl };
215
231
  } catch (err) {
216
232
  throw wrapTokenExchangeError(err);
217
233
  }
@@ -381,6 +397,7 @@ function createSsrfSafeOAuthFetch(trustedFetchFn, allowHttp) {
381
397
  async function resolveClientInformation(args) {
382
398
  const { agent, agentStorage, asUrl, asMetadata, clientMetadata, fetchFn, allowConfidentialClient } = args;
383
399
  if (agent.oauth_client) {
400
+ assertOAuthCredentialIssuer(agent.oauth_client, asUrl.toString(), !!agent.oauth_client.client_secret);
384
401
  return toMCPClientInfo(agent.oauth_client);
385
402
  }
386
403
  if (!asMetadata.registration_endpoint) {
@@ -390,11 +407,12 @@ async function resolveClientInformation(args) {
390
407
  agent.id
391
408
  );
392
409
  }
393
- const registered = await registerClient(asUrl.toString(), {
410
+ const registration = await registerClient(asUrl.toString(), {
394
411
  metadata: asMetadata,
395
412
  clientMetadata,
396
413
  fetchFn
397
414
  });
415
+ const registered = { ...registration, issuer: asUrl.toString() };
398
416
  if (registered.client_secret && !allowConfidentialClient) {
399
417
  throw new ConfidentialClientNotAllowedError(agent.id);
400
418
  }
@@ -34,6 +34,7 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamable
34
34
  var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
35
35
  var import_auth = require("@modelcontextprotocol/sdk/client/auth.js");
36
36
  var import_node_crypto = require("node:crypto");
37
+ var import_MCPOAuthProvider = require('../auth/oauth/MCPOAuthProvider.js');
37
38
  var import_auth2 = require('../auth/index.js');
38
39
  var import_errors = require('../errors/index.js');
39
40
  var import_tracing = require('../observability/tracing.js');
@@ -713,6 +714,7 @@ async function connectMCP(options) {
713
714
  fetchFn,
714
715
  allowPrivateIp
715
716
  } = options;
717
+ if (authProvider) (0, import_MCPOAuthProvider.assertLegacyOAuthSdk)();
716
718
  const baseUrl = new URL(agentUrl);
717
719
  debugLogs.push({
718
720
  type: "info",
@@ -11,6 +11,7 @@ import {
11
11
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
12
12
  import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
13
13
  import { hkdfSync, randomBytes } from "node:crypto";
14
+ import { assertLegacyOAuthSdk } from "../auth/oauth/MCPOAuthProvider.mjs";
14
15
  import { createMCPRequestHeaders } from "../auth/index.mjs";
15
16
  import { is401Error } from "../errors/index.mjs";
16
17
  import { withSpan, injectTraceHeaders } from "../observability/tracing.mjs";
@@ -700,6 +701,7 @@ async function connectMCP(options) {
700
701
  fetchFn,
701
702
  allowPrivateIp
702
703
  } = options;
704
+ if (authProvider) assertLegacyOAuthSdk();
703
705
  const baseUrl = new URL(agentUrl);
704
706
  debugLogs.push({
705
707
  type: "info",
@@ -4,5 +4,5 @@
4
4
  "source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
5
5
  "source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
6
6
  "upstream_adcp_version": "2.5.3",
7
- "synced_at": "2026-10-06T17:21:37.513Z"
7
+ "synced_at": "2026-10-06T19:28:36.184Z"
8
8
  }
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.3.0";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.4.0";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.3.0";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.4.0";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -145,6 +145,8 @@ export interface DayParting {
145
145
  * OAuth tokens for agent authentication
146
146
  */
147
147
  export interface AgentOAuthTokens {
148
+ /** Authorization server that issued these tokens. Required when reusing a refresh token. */
149
+ issuer?: string;
148
150
  /** OAuth access token */
149
151
  access_token: string;
150
152
  /** OAuth refresh token (for token renewal) */
@@ -162,6 +164,8 @@ export interface AgentOAuthTokens {
162
164
  * OAuth client information (from dynamic registration)
163
165
  */
164
166
  export interface AgentOAuthClient {
167
+ /** Authorization server where this client was registered. Required when reusing a client secret. */
168
+ issuer?: string;
165
169
  /** OAuth client ID */
166
170
  client_id: string;
167
171
  /** OAuth client secret (for confidential clients) */
@@ -401,6 +405,8 @@ export interface AgentConfig {
401
405
  * authorization flow.
402
406
  */
403
407
  oauth_client_credentials?: AgentOAuthClientCredentials;
408
+ /** Opaque discovery state paired with the pending PKCE verifier; persist and return verbatim. */
409
+ oauth_discovery_state?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthDiscoveryState;
404
410
  /**
405
411
  * Additional HTTP headers to include in every request to this agent.
406
412
  * Useful for sending API keys, org IDs, or other vendor-specific headers
@@ -145,6 +145,8 @@ export interface DayParting {
145
145
  * OAuth tokens for agent authentication
146
146
  */
147
147
  export interface AgentOAuthTokens {
148
+ /** Authorization server that issued these tokens. Required when reusing a refresh token. */
149
+ issuer?: string;
148
150
  /** OAuth access token */
149
151
  access_token: string;
150
152
  /** OAuth refresh token (for token renewal) */
@@ -162,6 +164,8 @@ export interface AgentOAuthTokens {
162
164
  * OAuth client information (from dynamic registration)
163
165
  */
164
166
  export interface AgentOAuthClient {
167
+ /** Authorization server where this client was registered. Required when reusing a client secret. */
168
+ issuer?: string;
165
169
  /** OAuth client ID */
166
170
  client_id: string;
167
171
  /** OAuth client secret (for confidential clients) */
@@ -401,6 +405,8 @@ export interface AgentConfig {
401
405
  * authorization flow.
402
406
  */
403
407
  oauth_client_credentials?: AgentOAuthClientCredentials;
408
+ /** Opaque discovery state paired with the pending PKCE verifier; persist and return verbatim. */
409
+ oauth_discovery_state?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthDiscoveryState;
404
410
  /**
405
411
  * Additional HTTP headers to include in every request to this agent.
406
412
  * Useful for sending API keys, org IDs, or other vendor-specific headers
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.3.0";
4
+ export declare const LIBRARY_VERSION = "14.4.0";
5
5
  /**
6
6
  * AdCP specification version this library is built for
7
7
  */
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
33
33
  * Full version information
34
34
  */
35
35
  export declare const VERSION_INFO: {
36
- readonly library: "14.3.0";
36
+ readonly library: "14.4.0";
37
37
  readonly adcp: "3.2.1";
38
38
  readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.1.19", "3.1.20", "3.1.21", "3.1.22", "3.1.23", "3.1.24", "3.2.1", "3.2"];
39
- readonly generatedAt: "2026-10-06T16:34:47.947Z";
39
+ readonly generatedAt: "2026-10-06T19:00:01.077Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.3.0";
4
+ export declare const LIBRARY_VERSION = "14.4.0";
5
5
  /**
6
6
  * AdCP specification version this library is built for
7
7
  */
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
33
33
  * Full version information
34
34
  */
35
35
  export declare const VERSION_INFO: {
36
- readonly library: "14.3.0";
36
+ readonly library: "14.4.0";
37
37
  readonly adcp: "3.2.1";
38
38
  readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.1.19", "3.1.20", "3.1.21", "3.1.22", "3.1.23", "3.1.24", "3.2.1", "3.2"];
39
- readonly generatedAt: "2026-10-06T16:34:47.947Z";
39
+ readonly generatedAt: "2026-10-06T19:00:01.077Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -31,7 +31,7 @@ __export(version_exports, {
31
31
  toReleasePrecisionVersion: () => toReleasePrecisionVersion
32
32
  });
33
33
  module.exports = __toCommonJS(version_exports);
34
- const LIBRARY_VERSION = "14.3.0";
34
+ const LIBRARY_VERSION = "14.4.0";
35
35
  const ADCP_VERSION = "3.2.1";
36
36
  const ADCP_MAJOR_VERSION = 3;
37
37
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -95,10 +95,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
95
95
  "3.2"
96
96
  ];
97
97
  const VERSION_INFO = {
98
- library: "14.3.0",
98
+ library: "14.4.0",
99
99
  adcp: "3.2.1",
100
100
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
101
- generatedAt: "2026-10-06T16:34:47.947Z"
101
+ generatedAt: "2026-10-06T19:00:01.077Z"
102
102
  };
103
103
  function getAdcpVersion() {
104
104
  return ADCP_VERSION;
@@ -1,4 +1,4 @@
1
- const LIBRARY_VERSION = "14.3.0";
1
+ const LIBRARY_VERSION = "14.4.0";
2
2
  const ADCP_VERSION = "3.2.1";
3
3
  const ADCP_MAJOR_VERSION = 3;
4
4
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -62,10 +62,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
62
62
  "3.2"
63
63
  ];
64
64
  const VERSION_INFO = {
65
- library: "14.3.0",
65
+ library: "14.4.0",
66
66
  adcp: "3.2.1",
67
67
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
68
- generatedAt: "2026-10-06T16:34:47.947Z"
68
+ generatedAt: "2026-10-06T19:00:01.077Z"
69
69
  };
70
70
  function getAdcpVersion() {
71
71
  return ADCP_VERSION;
@@ -1,7 +1,7 @@
1
1
  # AdCP Type Summary
2
2
 
3
3
  > Generated at: 2026-10-06
4
- > @adcp/sdk v14.3.0
4
+ > @adcp/sdk v14.4.0
5
5
 
6
6
  Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`.
7
7
 
package/docs/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # Ad Context Protocol (AdCP)
2
2
 
3
3
  > Generated at: 2026-10-06
4
- > Library: @adcp/sdk v14.3.0
4
+ > Library: @adcp/sdk v14.4.0
5
5
  > AdCP major version: 3
6
6
  > Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
7
7
  > Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
@@ -11,8 +11,8 @@ below as the publication/deployment gate.
11
11
 
12
12
  | Fact | Value |
13
13
  | --- | --- |
14
- | Exact npm package | `@adcp/sdk@14.3.0` |
15
- | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@14.3.0 dist.integrity` |
14
+ | Exact npm package | `@adcp/sdk@14.4.0` |
15
+ | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@14.4.0 dist.integrity` |
16
16
  | Node.js runtime | `^20.19.0 || >=22.12.0` |
17
17
  | Default AdCP wire release | `3.2.1` |
18
18
  | Maintained wire releases | `v2.5`, `v2.6`, `v3`, `3.0.0`, `3.0`, `3.0.1`, `3.0.2`, `3.0.3`, `3.0.4`, `3.0.5`, `3.0.6`, `3.0.7`, `3.0.8`, `3.0.9`, `3.0.10`, `3.0.11`, `3.0.12`, `3.0.13`, `3.0.14`, `3.0.15`, `3.0.16`, `3.0.17`, `3.0.18`, `3.0.19`, `3.0.20`, `3.0.21`, `3.0.22`, `3.0.23`, `3.0.24`, `3.0.25`, `3.1.0`, `3.1`, `3.1.1`, `3.1.2`, `3.1.3`, `3.1.4`, `3.1.5`, `3.1.6`, `3.1.7`, `3.1.8`, `3.1.9`, `3.1.10`, `3.1.11`, `3.1.12`, `3.1.13`, `3.1.14`, `3.1.15`, `3.1.16`, `3.1.17`, `3.1.18`, `3.1.19`, `3.1.20`, `3.1.21`, `3.1.22`, `3.1.23`, `3.1.24`, `3.2.1`, `3.2` |
@@ -21,8 +21,8 @@ below as the publication/deployment gate.
21
21
  Install exact production inputs rather than a moving range or dist-tag:
22
22
 
23
23
  ```bash
24
- npm install --save-exact '@adcp/sdk@14.3.0'
25
- npm view '@adcp/sdk@14.3.0' dist.integrity
24
+ npm install --save-exact '@adcp/sdk@14.4.0'
25
+ npm view '@adcp/sdk@14.4.0' dist.integrity
26
26
  ```
27
27
 
28
28
  ### Required and optional peers
@@ -32,7 +32,7 @@ Install the peer packages used by your application at versions satisfying these
32
32
  | Peer | Supported range | Installation |
33
33
  | --- | --- | --- |
34
34
  | `@a2a-js/sdk` | `^1.0.1` | Required |
35
- | `@modelcontextprotocol/sdk` | `^1.24.0` | Required |
35
+ | `@modelcontextprotocol/sdk` | `^1.31.0` | Required |
36
36
  | `@opentelemetry/api` | `^1.0.0` | Optional |
37
37
  | `redis` | `^4.6.0 || ^5.0.0 || ^6.0.0` | Optional |
38
38
  | `zod` | `^4.1.5` | Required |
@@ -0,0 +1,82 @@
1
+ # OAuth issuer bindings
2
+
3
+ The corrective release after SDK 14.3.0 upgrades the official MCP client and
4
+ server to 2.2.0 and requires the legacy `@modelcontextprotocol/sdk` peer at
5
+ `^1.31.0`. These versions address
6
+ [GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h).
7
+ Installations pinned to legacy SDK 1.24–1.30 must upgrade that peer dependency.
8
+ The legacy OAuth transport rejects vulnerable versions even if package-manager
9
+ peer checks were bypassed. The Node.js requirement remains `^20.19.0 || >=22.12.0`.
10
+
11
+ OAuth refresh tokens and client secrets belong to an authorization server.
12
+ `AgentOAuthTokens` and `AgentOAuthClient` now preserve an optional `issuer`
13
+ field through the SDK's conversions, providers, and file storage. Newly saved
14
+ credentials receive their binding from the authorization flow. The provider
15
+ persists discovery alongside the pending PKCE verifier and clears that temporary
16
+ state after exchange; a callback uses the original authorization server even if
17
+ resource discovery changes while the browser is away. Custom `OAuthConfigStorage`
18
+ implementations must preserve `oauth_discovery_state` alongside `oauth_code_verifier`. Web flows
19
+ validate the discovered server's metadata issuer before registration and
20
+ token exchange; diagnostic refreshes validate it before sending credentials.
21
+ The provider refuses a changed authorization server before replacing a trusted
22
+ confidential registration or reusing a refresh token. Clear credentials explicitly
23
+ if an independently verified server migration requires a fresh registration.
24
+
25
+ File storage saves the OAuth fields as an authoritative snapshot: omitted tokens,
26
+ client, verifier, or discovery state are deleted. Load the complete agent from
27
+ storage before creating a provider; keep unrelated persisted fields as before.
28
+
29
+ ## Existing saved credentials
30
+
31
+ `MCPOAuthProvider` refuses to reuse a saved refresh token or client secret
32
+ without a valid issuer, with `oauth_issuer_required`. The web flow and
33
+ diagnostic runner also refuse mismatched bindings. Access-token-only and
34
+ public-client configurations do not require a new field. A saved unbound refresh
35
+ token blocks every call through the provider, even while its access token is
36
+ still valid; clear the saved credentials before reconnecting.
37
+
38
+ For a saved CLI alias, run `adcp <alias> --clear-oauth`, then
39
+ `adcp <alias> --oauth` to sign in again.
40
+
41
+ For legacy saved credentials, clear `oauth_tokens` and any confidential
42
+ `oauth_client` registration, then sign in again. Alternatively, an operator
43
+ may add the authorization-server issuer established independently from
44
+ trusted registration records. **Do not infer it from the current MCP server's
45
+ discovery metadata**, an access token's unverified claims, or its resource URL.
46
+ If the issuer is unknown, sign in again. Raw `issuer: null` is treated as absent for access-token-only and public-client
47
+ configurations; secrets still require a valid string issuer.
48
+
49
+ ```ts
50
+ agent.oauth_tokens = {
51
+ ...agent.oauth_tokens!,
52
+ issuer: 'https://auth.example.com', // independently verified token issuer
53
+ };
54
+ agent.oauth_client = {
55
+ ...agent.oauth_client!,
56
+ issuer: 'https://auth.example.com', // where this client was registered
57
+ };
58
+ ```
59
+
60
+ An issuer is an authorization-server URL, including its tenant path when one
61
+ exists. A single trailing slash difference is tolerated for credential
62
+ comparison; distinct tenant paths remain distinct. Stored pending web flows
63
+ containing an unbound confidential client must be restarted.
64
+
65
+ ## Custom providers
66
+
67
+ Preserve the complete `issuer` field passed to `saveTokens()` and
68
+ `saveClientInformation()`, and return it from the corresponding getters. Add
69
+ an independently verified issuer to pre-registered client information. The
70
+ upstream bundled `ClientCredentialsProvider`, `PrivateKeyJwtProvider`,
71
+ `StaticPrivateKeyJwtProvider`, and `CrossAppAccessProvider` also need
72
+ `expectedIssuer` configured. These are adopter-owned provider settings; a
73
+ dependency upgrade cannot reconstruct the ownership of older credentials.
74
+
75
+ Calls made directly to the upstream `exchangeAuthorization()` and
76
+ `refreshAuthorization()` helpers need their own credential-issuer checks.
77
+ The SDK's web-flow and diagnosis helpers perform those checks. The SDK's
78
+ client-credentials flow uses an explicitly configured token endpoint rather
79
+ than selecting one from MCP server discovery.
80
+
81
+ If affected credentials were sent to an untrusted authorization server,
82
+ follow the upstream advisory's credential-rotation guidance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/sdk",
3
- "version": "14.3.0",
3
+ "version": "14.4.0",
4
4
  "description": "AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)",
5
5
  "workspaces": [
6
6
  ".",
@@ -713,10 +713,10 @@
713
713
  "email": "bugs@adcontextprotocol.org"
714
714
  },
715
715
  "dependencies": {
716
- "@modelcontextprotocol/client": "2.0.0",
716
+ "@modelcontextprotocol/client": "2.2.0",
717
717
  "@modelcontextprotocol/ext-apps": "2.0.0",
718
718
  "@modelcontextprotocol/node": "2.0.0",
719
- "@modelcontextprotocol/server": "2.0.0",
719
+ "@modelcontextprotocol/server": "2.2.0",
720
720
  "@types/ws": "^8.18.1",
721
721
  "ajv": "^8.20.0",
722
722
  "ajv-formats": "^3.0.1",
@@ -733,7 +733,7 @@
733
733
  },
734
734
  "peerDependencies": {
735
735
  "@a2a-js/sdk": "^1.0.1",
736
- "@modelcontextprotocol/sdk": "^1.24.0",
736
+ "@modelcontextprotocol/sdk": "^1.31.0",
737
737
  "@opentelemetry/api": "^1.0.0",
738
738
  "redis": "^4.6.0 || ^5.0.0 || ^6.0.0",
739
739
  "zod": "^4.1.5"
@@ -753,7 +753,7 @@
753
753
  "@changesets/cli": "^3.0.3",
754
754
  "@commitlint/cli": "^20.5.3",
755
755
  "@commitlint/config-conventional": "^20.5.3",
756
- "@modelcontextprotocol/sdk": "^1.30.0",
756
+ "@modelcontextprotocol/sdk": "^1.31.0",
757
757
  "@opentelemetry/api": "^1.9.1",
758
758
  "@types/express": "4.17.25",
759
759
  "@types/node": "^20.19.43",