@adcp/sdk 14.2.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 (68) 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/reporting/inspection.d.mts +3 -3
  21. package/dist/lib/reporting/inspection.d.ts +3 -3
  22. package/dist/lib/reporting/inspection.js +229 -84
  23. package/dist/lib/reporting/inspection.mjs +229 -84
  24. package/dist/lib/reporting/ledger/status-ingest.js +3 -19
  25. package/dist/lib/reporting/ledger/status-ingest.mjs +2 -18
  26. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  27. package/dist/lib/server/create-adcp-server.js +5 -2
  28. package/dist/lib/server/create-adcp-server.mjs +5 -2
  29. package/dist/lib/signing/canonicalize.d.mts +2 -0
  30. package/dist/lib/signing/canonicalize.d.ts +2 -0
  31. package/dist/lib/signing/canonicalize.js +1 -1
  32. package/dist/lib/signing/canonicalize.mjs +1 -1
  33. package/dist/lib/signing/verifier.js +1 -1
  34. package/dist/lib/signing/verifier.mjs +1 -1
  35. package/dist/lib/testing/storyboard/request-signing/builder.js +13 -1
  36. package/dist/lib/testing/storyboard/request-signing/builder.mjs +13 -1
  37. package/dist/lib/testing/storyboard/request-signing/grader.js +1 -0
  38. package/dist/lib/testing/storyboard/request-signing/grader.mjs +1 -0
  39. package/dist/lib/testing/storyboard/request-signing/vector-loader.js +1 -1
  40. package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +1 -1
  41. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  42. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  43. package/dist/lib/types/adcp.d.mts +6 -0
  44. package/dist/lib/types/adcp.d.ts +6 -0
  45. package/dist/lib/utils/pointer-utils.d.mts +2 -0
  46. package/dist/lib/utils/pointer-utils.d.ts +2 -0
  47. package/dist/lib/utils/pointer-utils.js +43 -0
  48. package/dist/lib/utils/pointer-utils.mjs +19 -0
  49. package/dist/lib/validation/diagnostic-limits.d.mts +4 -0
  50. package/dist/lib/validation/diagnostic-limits.d.ts +4 -0
  51. package/dist/lib/validation/diagnostic-limits.js +57 -0
  52. package/dist/lib/validation/diagnostic-limits.mjs +32 -0
  53. package/dist/lib/validation/schema-errors.d.mts +3 -1
  54. package/dist/lib/validation/schema-errors.d.ts +3 -1
  55. package/dist/lib/validation/schema-errors.js +18 -3
  56. package/dist/lib/validation/schema-errors.mjs +18 -3
  57. package/dist/lib/validation/schema-validator.js +56 -4
  58. package/dist/lib/validation/schema-validator.mjs +56 -4
  59. package/dist/lib/version.d.mts +3 -3
  60. package/dist/lib/version.d.ts +3 -3
  61. package/dist/lib/version.js +3 -3
  62. package/dist/lib/version.mjs +3 -3
  63. package/docs/TYPE-SUMMARY.md +2 -2
  64. package/docs/llms.txt +2 -2
  65. package/docs/migration-14.x-rc-worksheet.md +5 -5
  66. package/docs/migration-oauth-issuer-bindings.md +82 -0
  67. package/package.json +6 -6
  68. package/skills/call-adcp-agent/SKILL.md +4 -1
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,