@wrongstack/mcp 0.303.0 → 0.305.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.
@@ -49,6 +49,7 @@ export interface MCPAuthorizationStatus {
49
49
  export declare class MCPAuthorizationManager {
50
50
  private readonly options;
51
51
  private readonly pending;
52
+ private readonly starting;
52
53
  private readonly pendingTtlMs;
53
54
  private readonly discover;
54
55
  private readonly exchange;
@@ -59,6 +60,7 @@ export declare class MCPAuthorizationManager {
59
60
  status(serverName: string, resource: string): Promise<MCPAuthorizationStatus>;
60
61
  disconnect(serverName: string, resource: string): Promise<boolean>;
61
62
  private pruneExpired;
63
+ private activeAuthorizationCount;
62
64
  private emit;
63
65
  }
64
66
  export {};
@@ -28,6 +28,7 @@ export interface MCPAuthorizationServerMetadata {
28
28
  authorizationEndpoint: string;
29
29
  tokenEndpoint: string;
30
30
  registrationEndpoint?: string | undefined;
31
+ authorizationResponseIssuerParameterSupported?: boolean | undefined;
31
32
  scopesSupported: string[];
32
33
  }
33
34
  export interface MCPAuthorizationDiscoveryResult {
@@ -53,6 +54,8 @@ export interface MCPAuthorizationSession {
53
54
  redirectUri: string;
54
55
  clientId: string;
55
56
  resource: string;
57
+ issuer: string;
58
+ requireIssuerParameter: boolean;
56
59
  }
57
60
  export interface MCPTokenSet extends MCPAccessToken {
58
61
  refreshToken?: string | undefined;
@@ -121,7 +124,7 @@ export declare function validateMcpAuthorizationServerMetadata(value: unknown):
121
124
  */
122
125
  export declare function discoverMcpAuthorization(resource: string, options?: MCPAuthorizationDiscoveryOptions): Promise<MCPAuthorizationDiscoveryResult>;
123
126
  export declare function createMcpAuthorizationRequest(options: MCPAuthorizationRequestOptions): MCPAuthorizationSession;
124
- export declare function parseMcpAuthorizationCallback(callbackUrl: string, session: Pick<MCPAuthorizationSession, 'redirectUri' | 'state'>): string;
127
+ export declare function parseMcpAuthorizationCallback(callbackUrl: string, session: Pick<MCPAuthorizationSession, 'redirectUri' | 'state' | 'issuer' | 'requireIssuerParameter'>): string;
125
128
  export declare function exchangeMcpAuthorizationCode(options: MCPTokenExchangeOptions): Promise<MCPTokenSet>;
126
129
  export declare function refreshMcpAccessToken(options: MCPTokenRefreshOptions): Promise<MCPTokenSet>;
127
130
  export {};
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MCPAuthorizationProvider } from './authorization.js';
2
2
  import { type MCPGetPromptResult, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPReadResourceResult, type MCPServerMetadata } from './protocol.js';
3
- export { quoteWindowsArg } from './client-protocol-helpers.js';
4
3
  export { forceKillTree } from './client-process.js';
4
+ export { quoteWindowsArg } from './client-protocol-helpers.js';
5
5
  export type Transport = 'stdio' | 'sse' | 'streamable-http';
6
6
  export interface MCPClientOptions {
7
7
  name: string;
package/dist/index.js CHANGED
@@ -120,34 +120,59 @@ function parseAuthorizationServerMetadata(value, expectedIssuer) {
120
120
  throw new Error("MCP authorization server does not advertise required PKCE S256 support");
121
121
  }
122
122
  const registration = optionalString(metadata["registration_endpoint"], "registration_endpoint");
123
+ const authorizationEndpoint = secureOAuthUrl(
124
+ requiredString(metadata["authorization_endpoint"], "authorization_endpoint"),
125
+ "authorization endpoint"
126
+ ).toString();
127
+ const tokenEndpoint = secureOAuthUrl(
128
+ requiredString(metadata["token_endpoint"], "token_endpoint"),
129
+ "token endpoint"
130
+ ).toString();
131
+ const issuerParameterSupported = optionalBoolean(
132
+ metadata["authorization_response_iss_parameter_supported"],
133
+ "authorization_response_iss_parameter_supported"
134
+ );
135
+ if (issuerParameterSupported !== true && (new URL(authorizationEndpoint).origin !== new URL(issuer).origin || new URL(tokenEndpoint).origin !== new URL(issuer).origin)) {
136
+ throw new Error(
137
+ "MCP authorization server with cross-origin endpoints must support the authorization response issuer parameter"
138
+ );
139
+ }
123
140
  return {
124
141
  issuer,
125
- authorizationEndpoint: secureOAuthUrl(
126
- requiredString(metadata["authorization_endpoint"], "authorization_endpoint"),
127
- "authorization endpoint"
128
- ).toString(),
129
- tokenEndpoint: secureOAuthUrl(
130
- requiredString(metadata["token_endpoint"], "token_endpoint"),
131
- "token endpoint"
132
- ).toString(),
142
+ authorizationEndpoint,
143
+ tokenEndpoint,
133
144
  registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
145
+ authorizationResponseIssuerParameterSupported: issuerParameterSupported,
134
146
  scopesSupported: optionalStringArray(metadata["scopes_supported"], "scopes_supported", 128)
135
147
  };
136
148
  }
137
149
  function validateMcpAuthorizationServerMetadata(value) {
138
150
  const metadata = record(value, "stored authorization server metadata");
139
151
  const registration = optionalString(metadata["registrationEndpoint"], "registrationEndpoint");
152
+ const issuer = secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString();
153
+ const authorizationEndpoint = secureOAuthUrl(
154
+ requiredString(metadata["authorizationEndpoint"], "authorizationEndpoint"),
155
+ "authorization endpoint"
156
+ ).toString();
157
+ const tokenEndpoint = secureOAuthUrl(
158
+ requiredString(metadata["tokenEndpoint"], "tokenEndpoint"),
159
+ "token endpoint"
160
+ ).toString();
161
+ const issuerParameterSupported = optionalBoolean(
162
+ metadata["authorizationResponseIssuerParameterSupported"],
163
+ "authorizationResponseIssuerParameterSupported"
164
+ );
165
+ if (issuerParameterSupported !== true && (new URL(authorizationEndpoint).origin !== new URL(issuer).origin || new URL(tokenEndpoint).origin !== new URL(issuer).origin)) {
166
+ throw new Error(
167
+ "MCP authorization server with cross-origin endpoints must support the authorization response issuer parameter"
168
+ );
169
+ }
140
170
  return {
141
- issuer: secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString(),
142
- authorizationEndpoint: secureOAuthUrl(
143
- requiredString(metadata["authorizationEndpoint"], "authorizationEndpoint"),
144
- "authorization endpoint"
145
- ).toString(),
146
- tokenEndpoint: secureOAuthUrl(
147
- requiredString(metadata["tokenEndpoint"], "tokenEndpoint"),
148
- "token endpoint"
149
- ).toString(),
171
+ issuer,
172
+ authorizationEndpoint,
173
+ tokenEndpoint,
150
174
  registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
175
+ authorizationResponseIssuerParameterSupported: issuerParameterSupported,
151
176
  scopesSupported: optionalStringArray(metadata["scopesSupported"], "scopesSupported", 128)
152
177
  };
153
178
  }
@@ -212,7 +237,9 @@ function createMcpAuthorizationRequest(options) {
212
237
  codeVerifier,
213
238
  redirectUri,
214
239
  clientId,
215
- resource
240
+ resource,
241
+ issuer: options.authorizationServer.issuer,
242
+ requireIssuerParameter: options.authorizationServer.authorizationResponseIssuerParameterSupported === true
216
243
  };
217
244
  }
218
245
  function parseMcpAuthorizationCallback(callbackUrl, session) {
@@ -226,14 +253,33 @@ function parseMcpAuthorizationCallback(callbackUrl, session) {
226
253
  if (callback.protocol !== expected.protocol || callback.hostname !== expected.hostname || callback.port !== expected.port || callback.pathname !== expected.pathname) {
227
254
  throw new Error("MCP OAuth callback redirect URI does not match the authorization session");
228
255
  }
229
- const returnedState = callback.searchParams.get("state") ?? "";
256
+ const returnedStates = callback.searchParams.getAll("state");
257
+ if (returnedStates.length !== 1) {
258
+ throw new Error("MCP OAuth callback must contain exactly one state parameter");
259
+ }
260
+ const returnedState = returnedStates[0];
230
261
  if (!constantTimeEqual(returnedState, session.state)) {
231
262
  throw new Error("MCP OAuth callback state mismatch");
232
263
  }
233
- const oauthError = callback.searchParams.get("error");
234
- if (oauthError)
235
- throw new Error(`MCP OAuth authorization failed: ${boundedErrorCode(oauthError)}`);
236
- return boundedCredential(callback.searchParams.get("code") ?? "", "authorization code");
264
+ const returnedIssuers = callback.searchParams.getAll("iss");
265
+ if (session.requireIssuerParameter && returnedIssuers.length !== 1) {
266
+ throw new Error("MCP OAuth callback must contain exactly one issuer parameter");
267
+ }
268
+ if (returnedIssuers.length > 0 && returnedIssuers.some((issuer) => issuer !== session.issuer)) {
269
+ throw new Error("MCP OAuth callback issuer mismatch");
270
+ }
271
+ const oauthErrors = callback.searchParams.getAll("error");
272
+ const codes = callback.searchParams.getAll("code");
273
+ if (oauthErrors.length > 1 || codes.length > 1 || oauthErrors.length === 1 && codes.length > 0) {
274
+ throw new Error("MCP OAuth callback contains ambiguous response parameters");
275
+ }
276
+ if (oauthErrors.length === 1) {
277
+ throw new Error(`MCP OAuth authorization failed: ${boundedErrorCode(oauthErrors[0])}`);
278
+ }
279
+ if (codes.length !== 1) {
280
+ throw new Error("MCP OAuth callback must contain exactly one authorization code");
281
+ }
282
+ return boundedCredential(codes[0], "authorization code");
237
283
  }
238
284
  async function exchangeMcpAuthorizationCode(options) {
239
285
  const resource = canonicalMcpResource(options.resource);
@@ -315,6 +361,13 @@ function requiredString(value, field) {
315
361
  function optionalString(value, field) {
316
362
  return value === void 0 ? void 0 : requiredString(value, field);
317
363
  }
364
+ function optionalBoolean(value, field) {
365
+ if (value === void 0) return void 0;
366
+ if (typeof value !== "boolean") {
367
+ throw new Error(`MCP authorization field "${field}" must be a boolean`);
368
+ }
369
+ return value;
370
+ }
318
371
  function boundedStringArray(value, field, maxItems) {
319
372
  if (!Array.isArray(value) || value.length > maxItems) {
320
373
  throw new Error(`MCP authorization field "${field}" must be an array of at most ${maxItems}`);
@@ -595,6 +648,7 @@ var MCPAuthorizationManager = class {
595
648
  }
596
649
  options;
597
650
  pending = /* @__PURE__ */ new Map();
651
+ starting = /* @__PURE__ */ new Map();
598
652
  pendingTtlMs;
599
653
  discover;
600
654
  exchange;
@@ -603,33 +657,48 @@ var MCPAuthorizationManager = class {
603
657
  const resource = canonicalMcpResource(input.resource);
604
658
  const key = authorizationKey(input.serverName, resource);
605
659
  this.pruneExpired();
606
- if (!this.pending.has(key) && this.pending.size >= MAX_PENDING_AUTHORIZATIONS) {
660
+ if (this.starting.has(key)) {
661
+ throw new Error("MCP authorization discovery is already in progress for this server");
662
+ }
663
+ if (!this.pending.has(key) && this.activeAuthorizationCount() >= MAX_PENDING_AUTHORIZATIONS) {
607
664
  throw new Error("Too many pending MCP authorization sessions");
608
665
  }
609
- const discovery = await this.discover(resource, {
610
- challengeHeader: input.challengeHeader,
611
- signal: input.signal
612
- });
613
- const challengeScopes = parseMcpBearerChallenge(input.challengeHeader ?? null, resource).scopes;
614
- const scopes = input.scopes ? [...input.scopes] : challengeScopes;
615
- const session = createMcpAuthorizationRequest({
616
- authorizationServer: discovery.authorizationServer,
617
- clientId: input.clientId,
618
- redirectUri: input.redirectUri,
619
- resource,
620
- scopes
621
- });
622
- const normalizedScopes = new URL(session.authorizationUrl).searchParams.get("scope")?.split(" ").filter(Boolean) ?? [];
623
- const expiresAt = this.now() + this.pendingTtlMs;
624
- this.pending.set(key, { session, discovery, scopes: normalizedScopes, expiresAt });
625
- return {
626
- serverName: boundedServerName(input.serverName),
627
- resource,
628
- authorizationUrl: session.authorizationUrl,
629
- redirectUri: session.redirectUri,
630
- scopes: [...normalizedScopes],
631
- expiresAt
632
- };
666
+ const attempt = Symbol(key);
667
+ this.starting.set(key, attempt);
668
+ try {
669
+ const discovery = await this.discover(resource, {
670
+ challengeHeader: input.challengeHeader,
671
+ signal: input.signal
672
+ });
673
+ if (this.starting.get(key) !== attempt) {
674
+ throw new Error("MCP authorization discovery was cancelled");
675
+ }
676
+ const challengeScopes = parseMcpBearerChallenge(
677
+ input.challengeHeader ?? null,
678
+ resource
679
+ ).scopes;
680
+ const scopes = input.scopes ? [...input.scopes] : challengeScopes;
681
+ const session = createMcpAuthorizationRequest({
682
+ authorizationServer: discovery.authorizationServer,
683
+ clientId: input.clientId,
684
+ redirectUri: input.redirectUri,
685
+ resource,
686
+ scopes
687
+ });
688
+ const normalizedScopes = new URL(session.authorizationUrl).searchParams.get("scope")?.split(" ").filter(Boolean) ?? [];
689
+ const expiresAt = this.now() + this.pendingTtlMs;
690
+ this.pending.set(key, { session, discovery, scopes: normalizedScopes, expiresAt });
691
+ return {
692
+ serverName: boundedServerName(input.serverName),
693
+ resource,
694
+ authorizationUrl: session.authorizationUrl,
695
+ redirectUri: session.redirectUri,
696
+ scopes: [...normalizedScopes],
697
+ expiresAt
698
+ };
699
+ } finally {
700
+ if (this.starting.get(key) === attempt) this.starting.delete(key);
701
+ }
633
702
  }
634
703
  async complete(input) {
635
704
  const serverName = boundedServerName(input.serverName);
@@ -690,7 +759,9 @@ var MCPAuthorizationManager = class {
690
759
  async disconnect(serverName, resource) {
691
760
  const normalizedName = boundedServerName(serverName);
692
761
  const normalizedResource = canonicalMcpResource(resource);
693
- this.pending.delete(authorizationKey(normalizedName, normalizedResource));
762
+ const key = authorizationKey(normalizedName, normalizedResource);
763
+ this.starting.delete(key);
764
+ this.pending.delete(key);
694
765
  const removed = await this.options.store.remove(normalizedName, normalizedResource);
695
766
  if (removed) {
696
767
  this.options.onStateChange?.({
@@ -707,6 +778,13 @@ var MCPAuthorizationManager = class {
707
778
  if (value.expiresAt <= now) this.pending.delete(key);
708
779
  }
709
780
  }
781
+ activeAuthorizationCount() {
782
+ let count = this.pending.size;
783
+ for (const key of this.starting.keys()) {
784
+ if (!this.pending.has(key)) count++;
785
+ }
786
+ return count;
787
+ }
710
788
  emit(state, value) {
711
789
  this.options.onStateChange?.({
712
790
  serverName: value.serverName,
@@ -741,6 +819,57 @@ function boundedServerName(value) {
741
819
  import { spawn as spawn2 } from "node:child_process";
742
820
  import { buildChildEnv, buildWin32CmdShimInvocation, toErrorMessage } from "@wrongstack/core/utils";
743
821
 
822
+ // src/client-process.ts
823
+ import { spawn } from "node:child_process";
824
+ function forceKillTree(child) {
825
+ if (child.pid === void 0) {
826
+ try {
827
+ child.kill("SIGKILL");
828
+ } catch {
829
+ }
830
+ return;
831
+ }
832
+ if (process.platform === "win32") {
833
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
834
+ stdio: "ignore",
835
+ windowsHide: true
836
+ });
837
+ killer.once("error", () => {
838
+ try {
839
+ child.kill("SIGKILL");
840
+ } catch {
841
+ }
842
+ });
843
+ killer.unref();
844
+ return;
845
+ }
846
+ try {
847
+ child.kill("SIGKILL");
848
+ } catch {
849
+ }
850
+ }
851
+
852
+ // src/client-protocol-helpers.ts
853
+ var MAX_PROTOCOL_INPUT_CHARS = 8192;
854
+ function validateProtocolString(value, label, allowEmpty = false) {
855
+ if (typeof value !== "string" || !allowEmpty && value.length === 0) {
856
+ throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
857
+ }
858
+ if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
859
+ throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
860
+ }
861
+ }
862
+ function pageParams(cursor, label) {
863
+ if (cursor === void 0) return {};
864
+ validateProtocolString(cursor, label);
865
+ return { cursor };
866
+ }
867
+ function parseEmptyResult(value) {
868
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
869
+ throw new Error("Malformed MCP empty result: expected object");
870
+ }
871
+ }
872
+
744
873
  // src/constants.ts
745
874
  var MCP_CONSTANTS = Object.freeze({
746
875
  /** MCP protocol version advertised during handshake. */
@@ -2040,57 +2169,6 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
2040
2169
  }
2041
2170
  };
2042
2171
 
2043
- // src/client-protocol-helpers.ts
2044
- var MAX_PROTOCOL_INPUT_CHARS = 8192;
2045
- function validateProtocolString(value, label, allowEmpty = false) {
2046
- if (typeof value !== "string" || !allowEmpty && value.length === 0) {
2047
- throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
2048
- }
2049
- if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
2050
- throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
2051
- }
2052
- }
2053
- function pageParams(cursor, label) {
2054
- if (cursor === void 0) return {};
2055
- validateProtocolString(cursor, label);
2056
- return { cursor };
2057
- }
2058
- function parseEmptyResult(value) {
2059
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2060
- throw new Error("Malformed MCP empty result: expected object");
2061
- }
2062
- }
2063
-
2064
- // src/client-process.ts
2065
- import { spawn } from "node:child_process";
2066
- function forceKillTree(child) {
2067
- if (child.pid === void 0) {
2068
- try {
2069
- child.kill("SIGKILL");
2070
- } catch {
2071
- }
2072
- return;
2073
- }
2074
- if (process.platform === "win32") {
2075
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
2076
- stdio: "ignore",
2077
- windowsHide: true
2078
- });
2079
- killer.once("error", () => {
2080
- try {
2081
- child.kill("SIGKILL");
2082
- } catch {
2083
- }
2084
- });
2085
- killer.unref();
2086
- return;
2087
- }
2088
- try {
2089
- child.kill("SIGKILL");
2090
- } catch {
2091
- }
2092
- }
2093
-
2094
2172
  // src/client.ts
2095
2173
  var MCPClient = class _MCPClient {
2096
2174
  constructor(opts) {
@@ -2477,7 +2555,11 @@ var MCPClient = class _MCPClient {
2477
2555
  if (child.exitCode !== null || child.signalCode !== null) resolve();
2478
2556
  });
2479
2557
  try {
2480
- child.kill();
2558
+ if (process.platform === "win32" && child.stdin && !child.stdin.destroyed && child.stdin.writable) {
2559
+ child.stdin.end();
2560
+ } else {
2561
+ child.kill();
2562
+ }
2481
2563
  } catch {
2482
2564
  }
2483
2565
  const GRACEFUL_MS = 800;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "0.303.0",
3
+ "version": "0.305.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "!dist/**/*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@wrongstack/core": "0.303.0"
29
+ "@wrongstack/core": "0.305.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.1.2",