@modelcontextprotocol/inspector 1.0.1 → 1.0.2

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.
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-CH-qD91V.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-DdyO6kfY.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-CH-qD91V.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-DdyO6kfY.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;
@@ -17228,7 +17228,7 @@ object({
17228
17228
  token_type_hint: string().optional()
17229
17229
  }).strip();
17230
17230
  const name = "@modelcontextprotocol/inspector-client";
17231
- const version$1 = "1.0.1";
17231
+ const version$1 = "1.0.2";
17232
17232
  const packageJson = {
17233
17233
  name,
17234
17234
  version: version$1
@@ -32742,7 +32742,25 @@ function requireUtils() {
32742
32742
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
32743
32743
  const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
32744
32744
  const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
32745
- const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
32745
+ const isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
32746
+ const isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u);
32747
+ const isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u);
32748
+ const BYTE_HEX = new Array(256);
32749
+ {
32750
+ const HEX_DIGITS = "0123456789ABCDEF";
32751
+ for (let i2 = 0; i2 < 256; i2++) {
32752
+ BYTE_HEX[i2] = "%" + HEX_DIGITS[i2 >> 4] + HEX_DIGITS[i2 & 15];
32753
+ }
32754
+ }
32755
+ function percentEncodeNonAscii(cp2) {
32756
+ if (cp2 < 2048) {
32757
+ return BYTE_HEX[192 | cp2 >> 6] + BYTE_HEX[128 | cp2 & 63];
32758
+ }
32759
+ if (cp2 < 65536) {
32760
+ return BYTE_HEX[224 | cp2 >> 12] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
32761
+ }
32762
+ return BYTE_HEX[240 | cp2 >> 18] + BYTE_HEX[128 | cp2 >> 12 & 63] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
32763
+ }
32746
32764
  function stringArrayToHexStripped(input) {
32747
32765
  let acc = "";
32748
32766
  let code2 = 0;
@@ -32767,91 +32785,105 @@ function requireUtils() {
32767
32785
  }
32768
32786
  return acc;
32769
32787
  }
32788
+ const isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/);
32789
+ const isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/);
32790
+ const isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/);
32770
32791
  const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
32771
- function consumeIsZone(buffer) {
32772
- buffer.length = 0;
32773
- return true;
32774
- }
32775
- function consumeHextets(buffer, address, output) {
32776
- if (buffer.length) {
32777
- const hex = stringArrayToHexStripped(buffer);
32778
- if (hex !== "") {
32779
- address.push(hex);
32780
- } else {
32781
- output.error = true;
32782
- return false;
32792
+ function isZoneIdentifier(zone) {
32793
+ if (zone.length === 0) return false;
32794
+ for (let i2 = 0; i2 < zone.length; i2++) {
32795
+ if (isZoneCharacter(zone[i2])) continue;
32796
+ if (zone[i2] === "%" && i2 + 2 < zone.length && isHexPair(zone.slice(i2 + 1, i2 + 3))) {
32797
+ i2 += 2;
32798
+ continue;
32783
32799
  }
32784
- buffer.length = 0;
32800
+ return false;
32785
32801
  }
32786
32802
  return true;
32787
32803
  }
32788
- function getIPV6(input) {
32789
- let tokenCount = 0;
32790
- const output = { error: false, address: "", zone: "" };
32791
- const address = [];
32792
- const buffer = [];
32793
- let endipv6Encountered = false;
32794
- let endIpv6 = false;
32795
- let consume = consumeHextets;
32796
- for (let i2 = 0; i2 < input.length; i2++) {
32797
- const cursor = input[i2];
32798
- if (cursor === "[" || cursor === "]") {
32799
- continue;
32800
- }
32801
- if (cursor === ":") {
32802
- if (endipv6Encountered === true) {
32803
- endIpv6 = true;
32804
- }
32805
- if (!consume(buffer, address, output)) {
32806
- break;
32807
- }
32808
- if (++tokenCount > 7) {
32809
- output.error = true;
32810
- break;
32804
+ function compressIPv6ZeroRun(hextets) {
32805
+ let bestStart = -1;
32806
+ let bestLength = 0;
32807
+ let runStart = -1;
32808
+ let runLength = 0;
32809
+ for (let i2 = 0; i2 < hextets.length; i2++) {
32810
+ if (hextets[i2] === "0") {
32811
+ if (runStart === -1) runStart = i2;
32812
+ runLength++;
32813
+ if (runLength > bestLength) {
32814
+ bestLength = runLength;
32815
+ bestStart = runStart;
32811
32816
  }
32812
- if (i2 > 0 && input[i2 - 1] === ":") {
32813
- endipv6Encountered = true;
32814
- }
32815
- address.push(":");
32816
- continue;
32817
- } else if (cursor === "%") {
32818
- if (!consume(buffer, address, output)) {
32819
- break;
32820
- }
32821
- consume = consumeIsZone;
32822
32817
  } else {
32823
- buffer.push(cursor);
32818
+ runStart = -1;
32819
+ runLength = 0;
32820
+ }
32821
+ }
32822
+ if (bestLength < 2) return hextets.join(":");
32823
+ const head = hextets.slice(0, bestStart).join(":");
32824
+ const tail = hextets.slice(bestStart + bestLength).join(":");
32825
+ return head + "::" + tail;
32826
+ }
32827
+ function normalizeIPv6Address(input) {
32828
+ const compression = input.indexOf("::");
32829
+ if (compression !== -1 && input.indexOf("::", compression + 1) !== -1) return void 0;
32830
+ const left = compression === -1 ? input.split(":") : input.slice(0, compression).split(":");
32831
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(":");
32832
+ if (compression !== -1) {
32833
+ if (left.length === 1 && left[0] === "") left.length = 0;
32834
+ if (right.length === 1 && right[0] === "") right.length = 0;
32835
+ }
32836
+ const parts = left.concat(right);
32837
+ let hextetCount = 0;
32838
+ for (let i2 = 0; i2 < parts.length; i2++) {
32839
+ const part = parts[i2];
32840
+ if (part === "") return void 0;
32841
+ if (part.indexOf(".") !== -1) {
32842
+ if (i2 !== parts.length - 1 || compression !== -1 && right.length === 0 || !isIPv4(part)) return void 0;
32843
+ hextetCount += 2;
32824
32844
  continue;
32825
32845
  }
32846
+ if (!isHextet(part)) return void 0;
32847
+ parts[i2] = parseInt(part, 16).toString(16);
32848
+ hextetCount++;
32826
32849
  }
32827
- if (buffer.length) {
32828
- if (consume === consumeIsZone) {
32829
- output.zone = buffer.join("");
32830
- } else if (endIpv6) {
32831
- address.push(buffer.join(""));
32832
- } else {
32833
- address.push(stringArrayToHexStripped(buffer));
32834
- }
32850
+ if (compression === -1) {
32851
+ if (hextetCount !== 8) return void 0;
32852
+ return compressIPv6ZeroRun(parts);
32835
32853
  }
32836
- output.address = address.join("");
32837
- return output;
32854
+ if (hextetCount >= 8) return void 0;
32855
+ const expanded = parts.slice(0, left.length);
32856
+ for (let i2 = hextetCount; i2 < 8; i2++) expanded.push("0");
32857
+ for (let i2 = left.length; i2 < parts.length; i2++) expanded.push(parts[i2]);
32858
+ return compressIPv6ZeroRun(expanded);
32838
32859
  }
32839
32860
  function normalizeIPv6(host) {
32840
- if (findToken(host, ":") < 2) {
32841
- return { host, isIPV6: false };
32842
- }
32843
- const ipv62 = getIPV6(host);
32844
- if (!ipv62.error) {
32845
- let newHost = ipv62.address;
32846
- let escapedHost = ipv62.address;
32847
- if (ipv62.zone) {
32848
- newHost += "%" + ipv62.zone;
32849
- escapedHost += "%25" + ipv62.zone;
32850
- }
32851
- return { host: newHost, isIPV6: true, escapedHost };
32852
- } else {
32853
- return { host, isIPV6: false };
32854
- }
32861
+ const bracketed = host[0] === "[" && host[host.length - 1] === "]";
32862
+ const hasBracket = host[0] === "[" || host[host.length - 1] === "]";
32863
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true };
32864
+ let input = bracketed ? host.slice(1, -1) : host;
32865
+ if (bracketed && isIPvFuture(input)) {
32866
+ input = input.toLowerCase();
32867
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true };
32868
+ }
32869
+ if (findToken(input, ":") < 2) {
32870
+ return { host, isIPV6: false, error: bracketed };
32871
+ }
32872
+ let zoneIdentifier = "";
32873
+ const zoneSeparator = input.indexOf("%");
32874
+ if (zoneSeparator !== -1) {
32875
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === "%25" ? 3 : 1;
32876
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength);
32877
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true };
32878
+ input = input.slice(0, zoneSeparator);
32879
+ }
32880
+ const address = normalizeIPv6Address(input);
32881
+ if (address === void 0) return { host, isIPV6: false, error: true };
32882
+ return {
32883
+ host: address + (zoneIdentifier ? "%" + zoneIdentifier : ""),
32884
+ escapedHost: address + (zoneIdentifier ? "%25" + zoneIdentifier : ""),
32885
+ isIPV6: true
32886
+ };
32855
32887
  }
32856
32888
  function findToken(str, token) {
32857
32889
  let ind = 0;
@@ -32970,7 +33002,8 @@ function requireUtils() {
32970
33002
  function normalizePathEncoding(input) {
32971
33003
  let output = "";
32972
33004
  for (let i2 = 0; i2 < input.length; i2++) {
32973
- if (input[i2] === "%" && i2 + 2 < input.length) {
33005
+ const ch = input[i2];
33006
+ if (ch === "%" && i2 + 2 < input.length) {
32974
33007
  const hex = input.slice(i2 + 1, i2 + 3);
32975
33008
  if (isHexPair(hex)) {
32976
33009
  const normalizedHex = hex.toUpperCase();
@@ -32984,10 +33017,152 @@ function requireUtils() {
32984
33017
  continue;
32985
33018
  }
32986
33019
  }
32987
- if (isPathCharacter(input[i2])) {
32988
- output += input[i2];
33020
+ if (isPathCharacter(ch)) {
33021
+ output += ch;
33022
+ } else {
33023
+ const code2 = input.charCodeAt(i2);
33024
+ if (code2 < 128) {
33025
+ output += isEscapeSafe(code2) ? ch : BYTE_HEX[code2];
33026
+ } else if (code2 < 55296 || code2 > 57343) {
33027
+ output += percentEncodeNonAscii(code2);
33028
+ } else if (code2 <= 56319 && i2 + 1 < input.length) {
33029
+ const low = input.charCodeAt(i2 + 1);
33030
+ if (low >= 56320 && low <= 57343) {
33031
+ output += percentEncodeNonAscii(65536 + (code2 - 55296 << 10) + (low - 56320));
33032
+ i2++;
33033
+ } else {
33034
+ output += percentEncodeNonAscii(65533);
33035
+ }
33036
+ } else {
33037
+ output += percentEncodeNonAscii(65533);
33038
+ }
33039
+ }
33040
+ }
33041
+ return output;
33042
+ }
33043
+ function serializePathEncoding(input, pathNoScheme = false) {
33044
+ let output = "";
33045
+ let firstSegment = pathNoScheme && input[0] !== "/";
33046
+ for (let i2 = 0; i2 < input.length; i2++) {
33047
+ const ch = input[i2];
33048
+ if (ch === "%" && i2 + 2 < input.length) {
33049
+ const hex = input.slice(i2 + 1, i2 + 3);
33050
+ if (isHexPair(hex)) {
33051
+ output += "%" + hex.toUpperCase();
33052
+ i2 += 2;
33053
+ continue;
33054
+ }
33055
+ }
33056
+ if (ch === "/") {
33057
+ firstSegment = false;
33058
+ }
33059
+ if (isPathCharacter(ch) && (ch !== ":" || !firstSegment)) {
33060
+ output += ch;
33061
+ } else {
33062
+ const code2 = input.charCodeAt(i2);
33063
+ if (code2 < 128) {
33064
+ output += BYTE_HEX[code2];
33065
+ } else if (code2 < 55296 || code2 > 57343) {
33066
+ output += percentEncodeNonAscii(code2);
33067
+ } else if (code2 <= 56319 && i2 + 1 < input.length) {
33068
+ const low = input.charCodeAt(i2 + 1);
33069
+ if (low >= 56320 && low <= 57343) {
33070
+ output += percentEncodeNonAscii(65536 + (code2 - 55296 << 10) + (low - 56320));
33071
+ i2++;
33072
+ } else {
33073
+ output += percentEncodeNonAscii(65533);
33074
+ }
33075
+ } else {
33076
+ output += percentEncodeNonAscii(65533);
33077
+ }
33078
+ }
33079
+ }
33080
+ return output;
33081
+ }
33082
+ function encodeComponent(input, isAllowed) {
33083
+ let output = "";
33084
+ for (let i2 = 0; i2 < input.length; i2++) {
33085
+ const ch = input[i2];
33086
+ if (ch === "%" && i2 + 2 < input.length) {
33087
+ const hex = input.slice(i2 + 1, i2 + 3);
33088
+ if (isHexPair(hex)) {
33089
+ output += "%" + hex.toUpperCase();
33090
+ i2 += 2;
33091
+ continue;
33092
+ }
33093
+ }
33094
+ if (isAllowed(ch)) {
33095
+ output += ch;
33096
+ } else {
33097
+ const code2 = input.charCodeAt(i2);
33098
+ if (code2 < 128) {
33099
+ output += BYTE_HEX[code2];
33100
+ } else if (code2 < 55296 || code2 > 57343) {
33101
+ output += percentEncodeNonAscii(code2);
33102
+ } else if (code2 <= 56319 && i2 + 1 < input.length) {
33103
+ const low = input.charCodeAt(i2 + 1);
33104
+ if (low >= 56320 && low <= 57343) {
33105
+ output += percentEncodeNonAscii(65536 + (code2 - 55296 << 10) + (low - 56320));
33106
+ i2++;
33107
+ } else {
33108
+ output += percentEncodeNonAscii(65533);
33109
+ }
33110
+ } else {
33111
+ output += percentEncodeNonAscii(65533);
33112
+ }
33113
+ }
33114
+ }
33115
+ return output;
33116
+ }
33117
+ function encodeUserinfo(input) {
33118
+ return encodeComponent(input, isUserinfoCharacter);
33119
+ }
33120
+ function encodeQuery(input) {
33121
+ return encodeComponent(input, isQueryFragmentCharacter);
33122
+ }
33123
+ function encodeFragment(input) {
33124
+ return encodeComponent(input, isQueryFragmentCharacter);
33125
+ }
33126
+ function isEscapeSafe(cp2) {
33127
+ return cp2 >= 48 && cp2 <= 57 || cp2 >= 65 && cp2 <= 90 || cp2 >= 97 && cp2 <= 122 || cp2 === 42 || cp2 === 43 || cp2 === 45 || cp2 === 46 || cp2 === 47 || cp2 === 64 || cp2 === 95;
33128
+ }
33129
+ function normalizeQueryFragmentEncoding(input) {
33130
+ let output = "";
33131
+ for (let i2 = 0; i2 < input.length; i2++) {
33132
+ const ch = input[i2];
33133
+ if (ch === "%" && i2 + 2 < input.length) {
33134
+ const hex = input.slice(i2 + 1, i2 + 3);
33135
+ if (isHexPair(hex)) {
33136
+ const normalizedHex = hex.toUpperCase();
33137
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
33138
+ if (isUnreserved(decoded)) {
33139
+ output += decoded;
33140
+ } else {
33141
+ output += "%" + normalizedHex;
33142
+ }
33143
+ i2 += 2;
33144
+ continue;
33145
+ }
33146
+ }
33147
+ if (isQueryFragmentCharacter(ch)) {
33148
+ output += ch;
32989
33149
  } else {
32990
- output += escape(input[i2]);
33150
+ const code2 = input.charCodeAt(i2);
33151
+ if (code2 < 128) {
33152
+ output += isEscapeSafe(code2) ? ch : BYTE_HEX[code2];
33153
+ } else if (code2 < 55296 || code2 > 57343) {
33154
+ output += percentEncodeNonAscii(code2);
33155
+ } else if (code2 <= 56319 && i2 + 1 < input.length) {
33156
+ const low = input.charCodeAt(i2 + 1);
33157
+ if (low >= 56320 && low <= 57343) {
33158
+ output += percentEncodeNonAscii(65536 + (code2 - 55296 << 10) + (low - 56320));
33159
+ i2++;
33160
+ } else {
33161
+ output += percentEncodeNonAscii(65533);
33162
+ }
33163
+ } else {
33164
+ output += percentEncodeNonAscii(65533);
33165
+ }
32991
33166
  }
32992
33167
  }
32993
33168
  return output;
@@ -33010,14 +33185,18 @@ function requireUtils() {
33010
33185
  function recomposeAuthority(component) {
33011
33186
  const uriTokens = [];
33012
33187
  if (component.userinfo !== void 0) {
33013
- uriTokens.push(component.userinfo);
33188
+ uriTokens.push(encodeUserinfo(component.userinfo));
33014
33189
  uriTokens.push("@");
33015
33190
  }
33016
33191
  if (component.host !== void 0) {
33017
- let host = unescape(component.host);
33192
+ let host = component.host;
33018
33193
  if (!isIPv4(host)) {
33019
- const ipV6res = normalizeIPv6(host);
33020
- if (ipV6res.isIPV6 === true) {
33194
+ let ipV6res = normalizeIPv6(host);
33195
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
33196
+ host = normalizePercentEncoding(host, true);
33197
+ ipV6res = normalizeIPv6(host);
33198
+ }
33199
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
33021
33200
  host = `[${ipV6res.escapedHost}]`;
33022
33201
  } else {
33023
33202
  host = reescapeHostDelimiters(host, false);
@@ -33037,6 +33216,11 @@ function requireUtils() {
33037
33216
  reescapeHostDelimiters,
33038
33217
  normalizePercentEncoding,
33039
33218
  normalizePathEncoding,
33219
+ serializePathEncoding,
33220
+ normalizeQueryFragmentEncoding,
33221
+ encodeUserinfo,
33222
+ encodeQuery,
33223
+ encodeFragment,
33040
33224
  escapePreservingEscapes,
33041
33225
  removeDotSegments,
33042
33226
  isIPv4,
@@ -33052,7 +33236,7 @@ function requireSchemes() {
33052
33236
  if (hasRequiredSchemes) return schemes;
33053
33237
  hasRequiredSchemes = 1;
33054
33238
  const { isUUID } = requireUtils();
33055
- const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
33239
+ const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
33056
33240
  const supportedSchemeNames = (
33057
33241
  /** @type {const} */
33058
33242
  [
@@ -33113,9 +33297,10 @@ function requireSchemes() {
33113
33297
  wsComponent.secure = void 0;
33114
33298
  }
33115
33299
  if (wsComponent.resourceName) {
33116
- const [path, query] = wsComponent.resourceName.split("?");
33300
+ const queryIndex = wsComponent.resourceName.indexOf("?");
33301
+ const path = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
33117
33302
  wsComponent.path = path && path !== "/" ? path : void 0;
33118
- wsComponent.query = query;
33303
+ wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
33119
33304
  wsComponent.resourceName = void 0;
33120
33305
  }
33121
33306
  wsComponent.fragment = void 0;
@@ -33127,7 +33312,7 @@ function requireSchemes() {
33127
33312
  return urnComponent;
33128
33313
  }
33129
33314
  const matches = urnComponent.path.match(URN_REG);
33130
- if (matches) {
33315
+ if (matches && matches[0] === urnComponent.path) {
33131
33316
  const scheme = options2.scheme || urnComponent.scheme || "urn";
33132
33317
  urnComponent.nid = matches[1].toLowerCase();
33133
33318
  urnComponent.nss = matches[2];
@@ -33260,8 +33445,17 @@ var hasRequiredFastUri;
33260
33445
  function requireFastUri() {
33261
33446
  if (hasRequiredFastUri) return fastUri.exports;
33262
33447
  hasRequiredFastUri = 1;
33263
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils();
33448
+ const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = requireUtils();
33264
33449
  const { SCHEMES, getSchemeHandler } = requireSchemes();
33450
+ const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
33451
+ const MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
33452
+ function decodeValidScheme(scheme) {
33453
+ const decodedScheme = unescape(String(scheme));
33454
+ if (!VALID_SCHEME.test(decodedScheme)) {
33455
+ throw new TypeError(MALFORMED_SCHEME_ERROR);
33456
+ }
33457
+ return decodedScheme;
33458
+ }
33265
33459
  function normalize(uri2, options2) {
33266
33460
  if (typeof uri2 === "string") {
33267
33461
  uri2 = /** @type {T} */
@@ -33274,7 +33468,34 @@ function requireFastUri() {
33274
33468
  }
33275
33469
  function resolve2(baseURI, relativeURI, options2) {
33276
33470
  const schemelessOptions = options2 ? Object.assign({ scheme: "null" }, options2) : { scheme: "null" };
33277
- const resolved = resolveComponent(parse2(baseURI, schemelessOptions), parse2(relativeURI, schemelessOptions), schemelessOptions, true);
33471
+ const {
33472
+ parsed: baseParsed,
33473
+ malformedAuthorityOrPort: baseMalformed,
33474
+ malformedPercentEncoding: baseMalformedPercentEncoding,
33475
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
33476
+ malformedHost: baseMalformedHost,
33477
+ malformedScheme: baseMalformedScheme
33478
+ } = parseWithStatus(baseURI, schemelessOptions);
33479
+ const {
33480
+ parsed: relativeParsed,
33481
+ malformedAuthorityOrPort: relativeMalformed,
33482
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
33483
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
33484
+ malformedHost: relativeMalformedHost,
33485
+ malformedScheme: relativeMalformedScheme
33486
+ } = parseWithStatus(relativeURI, schemelessOptions);
33487
+ if (baseMalformed || relativeMalformed || baseMalformedPercentEncoding || relativeMalformedPercentEncoding || baseMalformedSchemeSpecific || relativeMalformedSchemeSpecific || baseMalformedHost || relativeMalformedHost || baseMalformedScheme || relativeMalformedScheme) {
33488
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
33489
+ }
33490
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
33491
+ const resolvedSchemeHandler = getSchemeHandler(options2 && options2.scheme || resolved.scheme);
33492
+ const resolvedHost = resolved.host;
33493
+ const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
33494
+ canonicalizeHost(resolved, options2 || {}, resolvedSchemeHandler, resolvedHostIsIP);
33495
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost);
33496
+ if (resolved.error && !encodedASCIIHost) {
33497
+ throw new Error(resolved.error);
33498
+ }
33278
33499
  schemelessOptions.skipEscape = true;
33279
33500
  return serialize(resolved, schemelessOptions);
33280
33501
  }
@@ -33334,7 +33555,7 @@ function requireFastUri() {
33334
33555
  function equal2(uriA, uriB, options2) {
33335
33556
  const normalizedA = normalizeComparableURI(uriA, options2);
33336
33557
  const normalizedB = normalizeComparableURI(uriB, options2);
33337
- return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
33558
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA === normalizedB;
33338
33559
  }
33339
33560
  function serialize(cmpts, opts) {
33340
33561
  const component = {
@@ -33355,19 +33576,22 @@ function requireFastUri() {
33355
33576
  };
33356
33577
  const options2 = Object.assign({}, opts);
33357
33578
  const uriTokens = [];
33579
+ if (component.scheme) {
33580
+ component.scheme = decodeValidScheme(component.scheme);
33581
+ }
33358
33582
  const schemeHandler = getSchemeHandler(options2.scheme || component.scheme);
33359
33583
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options2);
33584
+ const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
33585
+ const pathNoScheme = !options2.skipEscape && component.scheme === void 0 && !hasAuthority;
33360
33586
  if (component.path !== void 0) {
33361
33587
  if (!options2.skipEscape) {
33362
- component.path = escapePreservingEscapes(component.path);
33363
- if (component.scheme !== void 0) {
33364
- component.path = component.path.split("%3A").join(":");
33365
- }
33588
+ component.path = serializePathEncoding(component.path, pathNoScheme);
33366
33589
  } else {
33367
33590
  component.path = normalizePercentEncoding(component.path);
33368
33591
  }
33369
33592
  }
33370
33593
  if (options2.reference !== "suffix" && component.scheme) {
33594
+ component.scheme = decodeValidScheme(component.scheme);
33371
33595
  uriTokens.push(component.scheme, ":");
33372
33596
  }
33373
33597
  const authority = recomposeAuthority(component);
@@ -33385,21 +33609,25 @@ function requireFastUri() {
33385
33609
  if (!options2.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
33386
33610
  s2 = removeDotSegments(s2);
33387
33611
  }
33612
+ if (pathNoScheme) {
33613
+ s2 = serializePathEncoding(s2, true);
33614
+ }
33388
33615
  if (authority === void 0 && s2[0] === "/" && s2[1] === "/") {
33389
33616
  s2 = "/%2F" + s2.slice(2);
33390
33617
  }
33391
33618
  uriTokens.push(s2);
33392
33619
  }
33393
33620
  if (component.query !== void 0) {
33394
- uriTokens.push("?", component.query);
33621
+ uriTokens.push("?", encodeQuery(component.query));
33395
33622
  }
33396
33623
  if (component.fragment !== void 0) {
33397
- uriTokens.push("#", component.fragment);
33624
+ uriTokens.push("#", encodeFragment(component.fragment));
33398
33625
  }
33399
33626
  return uriTokens.join("");
33400
33627
  }
33401
33628
  const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
33402
33629
  const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
33630
+ const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
33403
33631
  function getParseError(parsed, matches) {
33404
33632
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
33405
33633
  return 'URI path must start with "/" when authority is present.';
@@ -33409,6 +33637,32 @@ function requireFastUri() {
33409
33637
  }
33410
33638
  return void 0;
33411
33639
  }
33640
+ function hasMalformedPercentEncoding(component) {
33641
+ if (component === void 0) return false;
33642
+ let percent = component.indexOf("%");
33643
+ while (percent !== -1) {
33644
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
33645
+ return true;
33646
+ }
33647
+ percent = component.indexOf("%", percent + 3);
33648
+ }
33649
+ return false;
33650
+ }
33651
+ function hasMalformedComponentPercentEncoding(matches) {
33652
+ const host = matches[4];
33653
+ return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !(host[0] === "[" && host[host.length - 1] === "]") && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
33654
+ }
33655
+ function canonicalizeHost(parsed, options2, schemeHandler, isIP) {
33656
+ if (!options2.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && parsed.host[0] !== "[" && (options2.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
33657
+ try {
33658
+ parsed.host = new URL("http://" + parsed.host).hostname;
33659
+ } catch (e2) {
33660
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e2;
33661
+ return true;
33662
+ }
33663
+ }
33664
+ return false;
33665
+ }
33412
33666
  function parseWithStatus(uri2, opts) {
33413
33667
  const options2 = Object.assign({}, opts);
33414
33668
  const parsed = {
@@ -33421,6 +33675,11 @@ function requireFastUri() {
33421
33675
  fragment: void 0
33422
33676
  };
33423
33677
  let malformedAuthorityOrPort = false;
33678
+ let malformedPercentEncoding = false;
33679
+ let malformedSchemeSpecific = false;
33680
+ let malformedHost = false;
33681
+ let malformedIPLiteral = false;
33682
+ let malformedScheme = false;
33424
33683
  let isIP = false;
33425
33684
  if (options2.reference === "suffix") {
33426
33685
  if (options2.scheme) {
@@ -33434,6 +33693,20 @@ function requireFastUri() {
33434
33693
  parsed.error = "URI authority must not contain a literal backslash.";
33435
33694
  malformedAuthorityOrPort = true;
33436
33695
  }
33696
+ const introducerMatch = uri2.match(AUTHORITY_INTRODUCER_REGION);
33697
+ if (introducerMatch !== null) {
33698
+ const region = introducerMatch[1];
33699
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
33700
+ if (normalizedRegion.length >= 2) {
33701
+ if (normalizedRegion.slice(0, 2) !== "//") {
33702
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
33703
+ malformedAuthorityOrPort = true;
33704
+ } else if (region.length !== normalizedRegion.length) {
33705
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
33706
+ malformedAuthorityOrPort = true;
33707
+ }
33708
+ }
33709
+ }
33437
33710
  const matches = uri2.match(URI_PARSE);
33438
33711
  if (matches) {
33439
33712
  parsed.scheme = matches[1];
@@ -33443,6 +33716,19 @@ function requireFastUri() {
33443
33716
  parsed.path = matches[6] || "";
33444
33717
  parsed.query = matches[7];
33445
33718
  parsed.fragment = matches[8];
33719
+ if (parsed.scheme !== void 0) {
33720
+ const decodedScheme = unescape(parsed.scheme);
33721
+ if (VALID_SCHEME.test(decodedScheme)) {
33722
+ parsed.scheme = decodedScheme.toLowerCase();
33723
+ } else {
33724
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
33725
+ malformedScheme = true;
33726
+ }
33727
+ }
33728
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches);
33729
+ if (malformedPercentEncoding) {
33730
+ parsed.error = parsed.error || "URI contains malformed percent-encoding.";
33731
+ }
33446
33732
  if (isNaN(parsed.port)) {
33447
33733
  parsed.port = matches[5];
33448
33734
  }
@@ -33454,9 +33740,15 @@ function requireFastUri() {
33454
33740
  if (parsed.host) {
33455
33741
  const ipv4result = isIPv4(parsed.host);
33456
33742
  if (ipv4result === false) {
33743
+ const bracketedIPLiteral = parsed.host[0] === "[" && parsed.host[parsed.host.length - 1] === "]";
33457
33744
  const ipv6result = normalizeIPv6(parsed.host);
33458
- parsed.host = ipv6result.host.toLowerCase();
33459
- isIP = ipv6result.isIPV6;
33745
+ isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
33746
+ malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true;
33747
+ parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
33748
+ if (malformedIPLiteral) {
33749
+ parsed.error = parsed.error || "URI host is malformed.";
33750
+ malformedAuthorityOrPort = true;
33751
+ }
33460
33752
  } else {
33461
33753
  isIP = true;
33462
33754
  }
@@ -33474,42 +33766,34 @@ function requireFastUri() {
33474
33766
  parsed.error = parsed.error || "URI is not a " + options2.reference + " reference.";
33475
33767
  }
33476
33768
  const schemeHandler = getSchemeHandler(options2.scheme || parsed.scheme);
33477
- if (!options2.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
33478
- if (parsed.host && (options2.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
33479
- try {
33480
- parsed.host = new URL("http://" + parsed.host).hostname;
33481
- } catch (e2) {
33482
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e2;
33483
- }
33484
- }
33485
- }
33769
+ malformedHost = canonicalizeHost(parsed, options2, schemeHandler, isIP);
33486
33770
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
33487
33771
  if (uri2.indexOf("%") !== -1) {
33488
- if (parsed.scheme !== void 0) {
33489
- parsed.scheme = unescape(parsed.scheme);
33490
- }
33491
- if (parsed.host !== void 0) {
33492
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
33772
+ if (parsed.host !== void 0 && !malformedIPLiteral) {
33773
+ const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
33774
+ parsed.host = reescapeHostDelimiters(host, isIP);
33493
33775
  }
33494
33776
  }
33495
33777
  if (parsed.path) {
33496
33778
  parsed.path = normalizePathEncoding(parsed.path);
33497
33779
  }
33780
+ if (parsed.query) {
33781
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query);
33782
+ }
33498
33783
  if (parsed.fragment) {
33499
- try {
33500
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
33501
- } catch {
33502
- parsed.error = parsed.error || "URI malformed";
33503
- }
33784
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
33504
33785
  }
33505
33786
  }
33506
33787
  if (schemeHandler && schemeHandler.parse) {
33507
33788
  schemeHandler.parse(parsed, options2);
33789
+ if (schemeHandler === SCHEMES.urn && parsed.nid === void 0) {
33790
+ malformedSchemeSpecific = true;
33791
+ }
33508
33792
  }
33509
33793
  } else {
33510
33794
  parsed.error = parsed.error || "URI can not be parsed.";
33511
33795
  }
33512
- return { parsed, malformedAuthorityOrPort };
33796
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
33513
33797
  }
33514
33798
  function parse2(uri2, opts) {
33515
33799
  return parseWithStatus(uri2, opts).parsed;
@@ -33518,20 +33802,28 @@ function requireFastUri() {
33518
33802
  return normalizeStringWithStatus(uri2, opts).normalized;
33519
33803
  }
33520
33804
  function normalizeStringWithStatus(uri2, opts) {
33521
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri2, opts);
33805
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri2, opts);
33522
33806
  return {
33523
- normalized: malformedAuthorityOrPort ? uri2 : serialize(parsed, opts),
33524
- malformedAuthorityOrPort
33807
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri2 : serialize(parsed, opts),
33808
+ malformedAuthorityOrPort,
33809
+ malformedPercentEncoding,
33810
+ malformedSchemeSpecific,
33811
+ malformedHost,
33812
+ malformedScheme
33525
33813
  };
33526
33814
  }
33527
33815
  function normalizeComparableURI(uri2, opts) {
33528
- if (typeof uri2 === "string") {
33529
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri2, opts);
33530
- return malformedAuthorityOrPort ? void 0 : normalized;
33816
+ if (typeof uri2 !== "string" && typeof uri2 !== "object") {
33817
+ return void 0;
33531
33818
  }
33532
- if (typeof uri2 === "object") {
33533
- return serialize(uri2, opts);
33819
+ let value;
33820
+ try {
33821
+ value = typeof uri2 === "string" ? uri2 : serialize(uri2, opts);
33822
+ } catch {
33823
+ return void 0;
33534
33824
  }
33825
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
33826
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized;
33535
33827
  }
33536
33828
  const fastUri$1 = {
33537
33829
  SCHEMES,
@@ -42799,7 +43091,7 @@ ${value2}`, dataLines++;
42799
43091
  }
42800
43092
  if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
42801
43093
  const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
42802
- id2 = value2.includes("\0") ? void 0 : value2;
43094
+ value2.includes("\0") || (id2 = value2);
42803
43095
  return;
42804
43096
  }
42805
43097
  if (firstCharCode === 58) {
@@ -42827,7 +43119,7 @@ ${value2}`, dataLines++;
42827
43119
  ${value}`, dataLines++;
42828
43120
  break;
42829
43121
  case "id":
42830
- id2 = value.includes("\0") ? void 0 : value;
43122
+ value.includes("\0") || (id2 = value);
42831
43123
  break;
42832
43124
  case "retry":
42833
43125
  /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
@@ -56144,7 +56436,7 @@ const useTheme = () => {
56144
56436
  [theme, setThemeWithSideEffect]
56145
56437
  );
56146
56438
  };
56147
- const version = "1.0.1";
56439
+ const version = "1.0.2";
56148
56440
  var __defProp$2 = Object.defineProperty;
56149
56441
  var __name$2 = (target, value) => __defProp$2(target, "name", { value, configurable: true });
56150
56442
  var [createTooltipContext, createTooltipScope] = /* @__PURE__ */ createContextScope("Tooltip", [
@@ -67002,13 +67294,13 @@ const App = () => {
67002
67294
  ) });
67003
67295
  if (window.location.pathname === "/oauth/callback") {
67004
67296
  const OAuthCallback = React.lazy(
67005
- () => __vitePreload(() => import("./OAuthCallback-DKaW5Tk8.js"), true ? [] : void 0)
67297
+ () => __vitePreload(() => import("./OAuthCallback-at8T3Iwi.js"), true ? [] : void 0)
67006
67298
  );
67007
67299
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthCallback, { onConnect: onOAuthConnect }) });
67008
67300
  }
67009
67301
  if (window.location.pathname === "/oauth/callback/debug") {
67010
67302
  const OAuthDebugCallback = React.lazy(
67011
- () => __vitePreload(() => import("./OAuthDebugCallback-Bx3X7Zkh.js"), true ? [] : void 0)
67303
+ () => __vitePreload(() => import("./OAuthDebugCallback-ZJ9hxfpr.js"), true ? [] : void 0)
67012
67304
  );
67013
67305
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthDebugCallback, { onConnect: onOAuthDebugConnect }) });
67014
67306
  }
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>MCP Inspector</title>
8
- <script type="module" crossorigin src="/assets/index-CH-qD91V.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DdyO6kfY.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DXy0twoH.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelcontextprotocol/inspector",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Model Context Protocol inspector",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Model Context Protocol a Series of LF Projects, LLC.",
@@ -42,19 +42,20 @@
42
42
  "start-client": "cd client && npm run preview",
43
43
  "test": "npm run prettier-check && cd client && npm test",
44
44
  "test-cli": "cd cli && npm run test",
45
+ "test-server": "cd server && npm run test",
45
46
  "test:e2e": "MCP_AUTO_OPEN_ENABLED=false npm run test:e2e --workspace=client",
46
47
  "prettier-fix": "prettier --write .",
47
48
  "prettier-check": "prettier --check .",
48
49
  "lint": "prettier --check . && cd client && npm run lint",
49
50
  "prepare": "husky && npm run build",
50
- "publish-all": "npm publish --workspaces --access public && npm publish --access public",
51
+ "publish-all": "npm publish --workspaces --access public --tag v1-latest && npm publish --access public --tag v1-latest",
51
52
  "update-version": "node scripts/update-version.js",
52
53
  "check-version": "node scripts/check-version-consistency.js"
53
54
  },
54
55
  "dependencies": {
55
- "@modelcontextprotocol/inspector-cli": "^1.0.1",
56
- "@modelcontextprotocol/inspector-client": "^1.0.1",
57
- "@modelcontextprotocol/inspector-server": "^1.0.1",
56
+ "@modelcontextprotocol/inspector-cli": "^1.0.2",
57
+ "@modelcontextprotocol/inspector-client": "^1.0.2",
58
+ "@modelcontextprotocol/inspector-server": "^1.0.2",
58
59
  "@modelcontextprotocol/sdk": "^1.25.2",
59
60
  "concurrently": "^9.2.0",
60
61
  "node-fetch": "^3.3.2",
@@ -80,13 +81,20 @@
80
81
  "overrides": {
81
82
  "get-intrinsic": "1.3.0",
82
83
  "shell-quote": "^1.8.4",
83
- "hono": "^4.12.25",
84
+ "hono": "^4.13.3",
84
85
  "form-data": "^4.0.6",
85
86
  "@babel/core@<=7.29.0": "^7.29.6",
86
87
  "esbuild@<0.28.1": "^0.28.1",
87
88
  "minimatch@<3.1.4": "3.1.5",
88
- "js-yaml@<4": "3.15.0",
89
- "js-yaml@>=4": "4.2.0"
89
+ "js-yaml@<4": "3.15.1",
90
+ "js-yaml@>=4": "4.3.1",
91
+ "@hono/node-server@<1.19.15": "^1.19.15",
92
+ "ip-address@<=10.3.0": "^10.5.0",
93
+ "fast-uri@<3.1.5": "^3.1.5",
94
+ "brace-expansion@<2": "^1.1.18",
95
+ "brace-expansion@>=4": "^5.0.9",
96
+ "nanoid@<3.3.18": "^3.3.18",
97
+ "postcss@<=8.5.22": "^8.5.26"
90
98
  },
91
99
  "engines": {
92
100
  "node": ">=22.7.5"
@@ -95,5 +103,9 @@
95
103
  "**/*.{js,ts,jsx,tsx,json,md}": [
96
104
  "prettier --write"
97
105
  ]
106
+ },
107
+ "allowScripts": {
108
+ "esbuild": false,
109
+ "fsevents": false
98
110
  }
99
111
  }
@@ -3,6 +3,7 @@ import cors from "cors";
3
3
  import { parseArgs } from "node:util";
4
4
  import { parse as shellParseArgs } from "shell-quote";
5
5
  import nodeFetch, { Headers as NodeHeaders } from "node-fetch";
6
+ import { assertSafeProxyTarget, createPinnedAgent, ProxyTargetError, } from "./proxy-security.js";
6
7
  // Type-compatible wrappers for node-fetch to work with browser-style types
7
8
  const fetch = nodeFetch;
8
9
  const Headers = NodeHeaders;
@@ -16,8 +17,6 @@ import rateLimit from "express-rate-limit";
16
17
  import { findActualExecutable } from "spawn-rx";
17
18
  import mcpProxy from "./mcpProxy.js";
18
19
  import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
19
- import { lookup as dnsLookup } from "node:dns/promises";
20
- import { isIP } from "node:net";
21
20
  import { fileURLToPath } from "url";
22
21
  import { dirname, join } from "path";
23
22
  import { readFileSync } from "fs";
@@ -675,83 +674,16 @@ app.get("/health", (req, res) => {
675
674
  status: "ok",
676
675
  });
677
676
  });
678
- // Raised when the /fetch proxy is asked to reach a disallowed target, so the
679
- // route can distinguish an SSRF rejection (403) from an upstream failure (500).
680
- class ProxyTargetError extends Error {
681
- }
682
677
  const MAX_PROXY_REDIRECTS = 5;
683
- // The /fetch proxy is used by the client to reach OAuth discovery/token
684
- // endpoints on the connected MCP server (bypassing browser CORS). Connecting
685
- // to loopback and RFC1918/private LAN hosts is a core use case (testing local
686
- // or self-hosted MCP servers), so those stay reachable. We only block
687
- // link-local addresses — 169.254.0.0/16 (the AWS/GCP/Azure/etc. cloud metadata
688
- // endpoint 169.254.169.254 lives here) and IPv6 link-local / the AWS IPv6 IMDS
689
- // address — which are never legitimate proxy targets and are the real SSRF
690
- // exfiltration vector.
691
- function isBlockedProxyAddress(ip) {
692
- const kind = isIP(ip);
693
- if (kind === 4) {
694
- const parts = ip.split(".").map(Number);
695
- // 169.254.0.0/16 — IPv4 link-local (incl. cloud metadata 169.254.169.254)
696
- return parts[0] === 169 && parts[1] === 254;
697
- }
698
- if (kind === 6) {
699
- const addr = ip.toLowerCase();
700
- // IPv4-mapped IPv6 (::ffff:x). The WHATWG URL parser serializes the embedded
701
- // IPv4 in hex (::ffff:a9fe:a9fe for 169.254.169.254), while DNS and other
702
- // sources may use the dotted form (::ffff:169.254.169.254) — handle both by
703
- // extracting the IPv4 and re-checking it.
704
- const mapped = addr.match(/^::ffff:(.+)$/);
705
- if (mapped) {
706
- const tail = mapped[1];
707
- if (isIP(tail) === 4) {
708
- return isBlockedProxyAddress(tail);
709
- }
710
- const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
711
- if (hex) {
712
- const high = parseInt(hex[1], 16);
713
- const low = parseInt(hex[2], 16);
714
- return isBlockedProxyAddress(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
715
- }
716
- }
717
- // fe80::/10 — IPv6 link-local
718
- if (/^fe[89ab]/.test(addr)) {
719
- return true;
720
- }
721
- // fd00:ec2::254 — AWS IPv6 instance metadata endpoint
722
- return addr === "fd00:ec2::254";
723
- }
724
- return false;
725
- }
726
- // Resolves the target host and rejects it if any resolved address is a blocked
727
- // (link-local/metadata) address, mitigating SSRF including the DNS case where a
728
- // hostname resolves to the cloud metadata IP.
729
- async function assertSafeProxyTarget(parsedUrl) {
730
- const host = parsedUrl.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
731
- let addresses;
732
- if (isIP(host)) {
733
- addresses = [host];
734
- }
735
- else {
736
- try {
737
- addresses = (await dnsLookup(host, { all: true })).map((r) => r.address);
738
- }
739
- catch {
740
- throw new ProxyTargetError(`Could not resolve host: ${host}`);
741
- }
742
- }
743
- if (addresses.length === 0) {
744
- throw new ProxyTargetError(`Could not resolve host: ${host}`);
745
- }
746
- for (const address of addresses) {
747
- if (isBlockedProxyAddress(address)) {
748
- throw new ProxyTargetError(`Refusing to proxy request to blocked address (${address})`);
749
- }
750
- }
751
- }
752
678
  // Fetch that follows redirects manually so every hop's target is re-validated
753
679
  // against assertSafeProxyTarget — otherwise an allowed host could redirect the
754
680
  // proxy into a blocked internal/metadata address.
681
+ //
682
+ // On each hop we call assertSafeProxyTarget (which resolves DNS and validates
683
+ // every returned IP) and then pass a createPinnedAgent to node-fetch so it
684
+ // connects to the IP we just validated instead of re-resolving the hostname.
685
+ // This eliminates the TOCTOU window between the block-list check and the
686
+ // actual TCP connection that a DNS-rebinding attack would exploit.
755
687
  async function safeProxyFetch(initialUrl, init) {
756
688
  let currentUrl = new URL(initialUrl);
757
689
  let method = init?.method ?? "GET";
@@ -761,12 +693,18 @@ async function safeProxyFetch(initialUrl, init) {
761
693
  if (!["http:", "https:"].includes(currentUrl.protocol)) {
762
694
  throw new ProxyTargetError("Only http/https URLs are allowed");
763
695
  }
764
- await assertSafeProxyTarget(currentUrl);
696
+ // Resolve DNS once, validate every address, and pin the connection to that
697
+ // validated set so node-fetch never does a second DNS lookup. All of them
698
+ // are passed through, so Node's address-family fallback still works while
699
+ // the reachable set stays limited to what was just validated.
700
+ const validatedAddresses = await assertSafeProxyTarget(currentUrl);
701
+ const agent = createPinnedAgent(currentUrl.protocol, validatedAddresses);
765
702
  const response = await fetch(currentUrl.toString(), {
766
703
  method,
767
704
  headers,
768
705
  body,
769
706
  redirect: "manual",
707
+ agent,
770
708
  });
771
709
  if (response.status >= 300 && response.status < 400) {
772
710
  const location = response.headers.get("location");
@@ -0,0 +1,154 @@
1
+ /**
2
+ * SSRF-prevention helpers for the /fetch reverse proxy.
3
+ *
4
+ * Extracted from index.ts so they can be unit-tested independently.
5
+ *
6
+ * ## DNS-rebinding / TOCTOU threat model
7
+ *
8
+ * `assertSafeProxyTarget` resolves the target hostname and validates every
9
+ * returned IP against the block-list. The resolved IPs are returned to the
10
+ * caller so they can be passed to `createPinnedAgent`, which builds an
11
+ * http/https.Agent whose `lookup` callback unconditionally returns the
12
+ * pre-validated IP. The agent is then passed to `node-fetch`, which calls the
13
+ * lookup hook instead of doing its own DNS query.
14
+ *
15
+ * Without this pinning there is a TOCTOU race: an attacker who controls the
16
+ * target domain can flip its DNS record to 169.254.169.254 (AWS/GCP/Azure
17
+ * instance-metadata) in the window between `assertSafeProxyTarget` returning
18
+ * and `fetch` performing its own resolution. Pinning eliminates that window.
19
+ */
20
+ import http from "node:http";
21
+ import https from "node:https";
22
+ import { isIP } from "node:net";
23
+ import { lookup as dnsLookup } from "node:dns/promises";
24
+ // ---------------------------------------------------------------------------
25
+ // Block-list
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Returns true if `ip` is a link-local or cloud-metadata address that must
29
+ * never be reachable through the proxy.
30
+ *
31
+ * Blocked ranges:
32
+ * - 169.254.0.0/16 (IPv4 link-local; includes 169.254.169.254 IMDS)
33
+ * - ::ffff:169.254.x.x (IPv4-mapped IPv6 variants of the above)
34
+ * - fe80::/10 (IPv6 link-local)
35
+ * - fd00:ec2::254 (AWS IPv6 instance-metadata)
36
+ */
37
+ export function isBlockedProxyAddress(ip) {
38
+ const kind = isIP(ip);
39
+ if (kind === 4) {
40
+ const parts = ip.split(".").map(Number);
41
+ return parts[0] === 169 && parts[1] === 254;
42
+ }
43
+ if (kind === 6) {
44
+ const addr = ip.toLowerCase();
45
+ // IPv4-mapped IPv6 — two serialization forms:
46
+ // dotted ::ffff:169.254.169.254
47
+ // hex ::ffff:a9fe:a9fe
48
+ const mapped = addr.match(/^::ffff:(.+)$/);
49
+ if (mapped) {
50
+ const tail = mapped[1];
51
+ if (isIP(tail) === 4) {
52
+ return isBlockedProxyAddress(tail);
53
+ }
54
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
55
+ if (hex) {
56
+ const high = parseInt(hex[1], 16);
57
+ const low = parseInt(hex[2], 16);
58
+ return isBlockedProxyAddress(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
59
+ }
60
+ }
61
+ if (/^fe[89ab]/.test(addr))
62
+ return true; // fe80::/10
63
+ return addr === "fd00:ec2::254"; // AWS IPv6 IMDS
64
+ }
65
+ return false;
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // DNS validation
69
+ // ---------------------------------------------------------------------------
70
+ /** Raised when the /fetch proxy is asked to reach a disallowed target. */
71
+ export class ProxyTargetError extends Error {
72
+ }
73
+ /**
74
+ * Resolves `parsedUrl`'s hostname and asserts that every returned IP is
75
+ * outside the block-list.
76
+ *
77
+ * Returns the list of validated IP strings so the caller can pin one of them
78
+ * into an HTTP agent (see `createPinnedAgent`), eliminating the TOCTOU window
79
+ * between this check and the actual `fetch()` call.
80
+ *
81
+ * @throws {ProxyTargetError} if the host cannot be resolved or any resolved
82
+ * address falls inside the block-list.
83
+ */
84
+ export async function assertSafeProxyTarget(parsedUrl) {
85
+ const host = parsedUrl.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
86
+ let addresses;
87
+ if (isIP(host)) {
88
+ addresses = [host];
89
+ }
90
+ else {
91
+ try {
92
+ addresses = (await dnsLookup(host, { all: true })).map((r) => r.address);
93
+ }
94
+ catch {
95
+ throw new ProxyTargetError(`Could not resolve host: ${host}`);
96
+ }
97
+ }
98
+ if (addresses.length === 0) {
99
+ throw new ProxyTargetError(`Could not resolve host: ${host}`);
100
+ }
101
+ for (const address of addresses) {
102
+ if (isBlockedProxyAddress(address)) {
103
+ throw new ProxyTargetError(`Refusing to proxy request to blocked address (${address})`);
104
+ }
105
+ }
106
+ return addresses;
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // IP-pinned agent factory
110
+ // ---------------------------------------------------------------------------
111
+ /**
112
+ * Creates an `http.Agent` or `https.Agent` whose internal `lookup` hook always
113
+ * returns `pinnedIps`, bypassing the OS resolver entirely.
114
+ *
115
+ * Pass this agent to `node-fetch` on every hop of `safeProxyFetch` so the TCP
116
+ * connection always goes to an IP that was validated by `assertSafeProxyTarget`
117
+ * — even if the domain's DNS record changes mid-flight.
118
+ *
119
+ * Every address `assertSafeProxyTarget` returned is handed to the hook, not
120
+ * just the first, so Node keeps its normal address-family fallback: a
121
+ * dual-stack host whose first address is unreachable (IPv6 on an IPv4-only
122
+ * network, say) still connects via a later one. The set it may choose from is
123
+ * exactly the prevalidated set, so the TOCTOU guarantee is unchanged.
124
+ *
125
+ * @param protocol - `"http:"` or `"https:"` (from `URL.protocol`)
126
+ * @param pinnedIps - Validated IPv4/IPv6 address strings, in preference order
127
+ */
128
+ export function createPinnedAgent(protocol, pinnedIps) {
129
+ if (pinnedIps.length === 0) {
130
+ throw new ProxyTargetError("No validated address to connect to");
131
+ }
132
+ const resolved = pinnedIps.map((address) => ({
133
+ address,
134
+ family: isIP(address) === 6 ? 6 : 4,
135
+ }));
136
+ // Node.js lookup signature: (hostname, options, callback).
137
+ //
138
+ // The callback shape depends on `options.all`. Node's `net.connect` runs with
139
+ // `autoSelectFamily` enabled by default (Node >= 20), so it calls the hook
140
+ // with `{ all: true }` and expects an ARRAY of `{ address, family }` — handing
141
+ // it the scalar form there fails the connection with ERR_INVALID_IP_ADDRESS.
142
+ // Both shapes are supported so the agent works on either call path.
143
+ const lookup = (_hostname, opts, callback) => {
144
+ if (opts?.all) {
145
+ callback(null, resolved);
146
+ return;
147
+ }
148
+ callback(null, resolved[0].address, resolved[0].family);
149
+ };
150
+ if (protocol === "https:") {
151
+ return new https.Agent({ lookup });
152
+ }
153
+ return new http.Agent({ lookup });
154
+ }