@kwonye/mcpx 0.1.85 → 0.1.87

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.
@@ -2208,7 +2208,7 @@ async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
2208
2208
  }
2209
2209
 
2210
2210
  // src/version.ts
2211
- var APP_VERSION = "0.1.85";
2211
+ var APP_VERSION = "0.1.87";
2212
2212
 
2213
2213
  // src/core/update.ts
2214
2214
  import fs7 from "node:fs";
@@ -2485,7 +2485,7 @@ import {
2485
2485
  StdioClientTransport,
2486
2486
  getDefaultEnvironment
2487
2487
  } from "@modelcontextprotocol/sdk/client/stdio.js";
2488
- var JSON_RPC_VERSION2 = "2.0";
2488
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2489
2489
  var SERVER_VERSION = APP_VERSION;
2490
2490
  var DEFAULT_UPSTREAM_TIMEOUT_MS = 60000;
2491
2491
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
@@ -2653,10 +2653,7 @@ function parseNamespacedUri(uri) {
2653
2653
  };
2654
2654
  }
2655
2655
  async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
2656
- if (upstream.spec.transport === "stdio") {
2657
- return callStdioUpstream(upstream, method, params, secrets, runtime);
2658
- }
2659
- return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
2656
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
2660
2657
  }
2661
2658
  async function getUpstreamTokenCounts(config, secrets, runtime) {
2662
2659
  if (!runtime.tokenCache) {
@@ -2749,75 +2746,110 @@ function buildStdioServerParameters(spec, secrets) {
2749
2746
  env: resolveStdioEnv(spec, secrets)
2750
2747
  };
2751
2748
  }
2752
- async function closeStdioConnection(entry) {
2749
+ async function buildHttpHeaders(upstream, secrets, options) {
2750
+ const headers = {};
2751
+ for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2752
+ if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2753
+ const oauthServerName = oauthReferenceServerName(value);
2754
+ const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, {
2755
+ forceRefresh: options.forceOAuthRefresh
2756
+ });
2757
+ headers[key] = `Bearer ${accessToken}`;
2758
+ } else {
2759
+ headers[key] = secrets.resolveMaybeSecret(value);
2760
+ }
2761
+ }
2762
+ if (options.passthroughAuthorizationHeader) {
2763
+ headers.Authorization = options.passthroughAuthorizationHeader;
2764
+ }
2765
+ return headers;
2766
+ }
2767
+ async function closeUpstreamConnection(entry) {
2753
2768
  try {
2754
2769
  const connection = await entry.promise;
2755
2770
  await connection.transport.close();
2756
2771
  } catch {}
2757
2772
  }
2758
- async function reconcileStdioConnections(config, runtime) {
2773
+ async function reconcileUpstreamConnections(config, runtime) {
2759
2774
  const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
2760
- const staleNames = [];
2761
- for (const [name, entry] of runtime.stdioConnections.entries()) {
2762
- const expectedFingerprint = activeSpecs.get(name);
2775
+ const staleKeys = [];
2776
+ for (const [key, entry] of runtime.upstreamConnections.entries()) {
2777
+ const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
2778
+ const expectedFingerprint = activeSpecs.get(upstreamName);
2763
2779
  if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
2764
- staleNames.push(name);
2780
+ staleKeys.push(key);
2765
2781
  }
2766
2782
  }
2767
- for (const name of staleNames) {
2768
- const entry = runtime.stdioConnections.get(name);
2783
+ for (const key of staleKeys) {
2784
+ const entry = runtime.upstreamConnections.get(key);
2769
2785
  if (!entry) {
2770
2786
  continue;
2771
2787
  }
2772
- runtime.stdioConnections.delete(name);
2773
- await closeStdioConnection(entry);
2788
+ runtime.upstreamConnections.delete(key);
2789
+ await closeUpstreamConnection(entry);
2774
2790
  }
2775
2791
  }
2776
- function invalidateStdioConnection(name, runtime) {
2777
- const existing = runtime.stdioConnections.get(name);
2792
+ function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
2793
+ const key = passthroughAuthorizationHeader ? `${upstreamName}:passthrough` : upstreamName;
2794
+ const existing = runtime.upstreamConnections.get(key);
2778
2795
  if (!existing) {
2779
2796
  return;
2780
2797
  }
2781
- runtime.stdioConnections.delete(name);
2782
- closeStdioConnection(existing);
2798
+ runtime.upstreamConnections.delete(key);
2799
+ closeUpstreamConnection(existing);
2783
2800
  }
2784
- async function getStdioConnection(upstream, secrets, runtime) {
2801
+ async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
2802
+ const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
2803
+ const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:passthrough` : upstream.name;
2785
2804
  const fingerprint = specFingerprint(upstream.spec);
2786
- const existing = runtime.stdioConnections.get(upstream.name);
2787
- if (existing) {
2788
- if (existing.fingerprint === fingerprint) {
2789
- return existing.promise;
2805
+ if (!forceOAuthRefresh) {
2806
+ const existing = runtime.upstreamConnections.get(cacheKey);
2807
+ if (existing) {
2808
+ if (existing.fingerprint === fingerprint) {
2809
+ return existing.promise;
2810
+ }
2811
+ runtime.upstreamConnections.delete(cacheKey);
2812
+ closeUpstreamConnection(existing);
2790
2813
  }
2791
- runtime.stdioConnections.delete(upstream.name);
2792
- closeStdioConnection(existing);
2793
2814
  }
2794
2815
  const promise = (async () => {
2795
- const transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2796
- const client = new Client({
2797
- name: "mcpx",
2798
- version: SERVER_VERSION
2799
- });
2816
+ let transport;
2817
+ if (upstream.spec.transport === "stdio") {
2818
+ transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2819
+ } else {
2820
+ const headers = await buildHttpHeaders(upstream, secrets, { forceOAuthRefresh, passthroughAuthorizationHeader });
2821
+ const capturingFetch = async (url, init) => {
2822
+ const response = await fetch(url, init);
2823
+ const wwwAuth = response.headers.get("www-authenticate");
2824
+ if (wwwAuth) {
2825
+ if (!runtime.lastWwwAuthenticate) {
2826
+ runtime.lastWwwAuthenticate = new Map;
2827
+ }
2828
+ runtime.lastWwwAuthenticate.set(upstream.name, wwwAuth);
2829
+ }
2830
+ return response;
2831
+ };
2832
+ transport = new StreamableHTTPClientTransport(new URL2(upstream.spec.url), { requestInit: { headers }, fetch: capturingFetch });
2833
+ }
2834
+ const client = new Client({ name: "mcpx", version: SERVER_VERSION });
2800
2835
  await withTimeout(client.connect(transport), DEFAULT_CONNECT_TIMEOUT_MS, `Upstream ${upstream.name} failed to connect within ${DEFAULT_CONNECT_TIMEOUT_MS}ms.`);
2801
- return {
2802
- fingerprint,
2803
- client,
2804
- transport
2805
- };
2836
+ return { fingerprint, client, transport };
2806
2837
  })();
2807
- runtime.stdioConnections.set(upstream.name, {
2808
- fingerprint,
2809
- promise
2810
- });
2838
+ runtime.upstreamConnections.set(cacheKey, { fingerprint, promise });
2811
2839
  try {
2812
2840
  return await promise;
2813
2841
  } catch (error) {
2814
- runtime.stdioConnections.delete(upstream.name);
2842
+ runtime.upstreamConnections.delete(cacheKey);
2815
2843
  throw error;
2816
2844
  }
2817
2845
  }
2818
- async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2846
+ async function callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, forceOAuthRefresh) {
2847
+ const hasOAuth = upstream.spec.transport === "http" && Object.values(upstream.spec.headers ?? {}).some(isOAuthReference);
2819
2848
  try {
2820
- const connection = await getStdioConnection(upstream, secrets, runtime);
2849
+ const connection = await getUpstreamConnection(upstream, secrets, runtime, {
2850
+ forceOAuthRefresh,
2851
+ passthroughAuthorizationHeader
2852
+ });
2821
2853
  const timeoutMs = getConfiguredTimeoutMs();
2822
2854
  const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
2823
2855
  if (method === "tools/list") {
@@ -2838,161 +2870,17 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2838
2870
  if (method === "prompts/get") {
2839
2871
  return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
2840
2872
  }
2841
- throw new Error(`Unsupported stdio passthrough method: ${method}`);
2873
+ throw new Error(`Unsupported method: ${method}`);
2842
2874
  } catch (error) {
2843
- invalidateStdioConnection(upstream.name, runtime);
2844
- throw error;
2845
- }
2846
- }
2847
- async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
2848
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
2849
- }
2850
- async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
2851
- const headers = {
2852
- "content-type": "application/json",
2853
- accept: "application/json, text/event-stream"
2854
- };
2855
- for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2856
- if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2857
- const oauthServerName = oauthReferenceServerName(value);
2858
- const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
2859
- headers[key] = `Bearer ${accessToken}`;
2860
- } else {
2861
- headers[key] = secrets.resolveMaybeSecret(value);
2875
+ invalidateUpstreamConnection(upstream.name, passthroughAuthorizationHeader, runtime);
2876
+ if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
2877
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
2862
2878
  }
2863
- }
2864
- if (passthroughAuthorizationHeader) {
2865
- headers.Authorization = passthroughAuthorizationHeader;
2866
- }
2867
- const timeoutMs = getConfiguredTimeoutMs();
2868
- const timeoutController = new AbortController;
2869
- const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
2870
- let response;
2871
- try {
2872
- response = await fetch(upstream.spec.url, {
2873
- method: "POST",
2874
- headers,
2875
- body: JSON.stringify({
2876
- jsonrpc: JSON_RPC_VERSION2,
2877
- id,
2878
- method,
2879
- params
2880
- }),
2881
- signal: timeoutController.signal
2882
- });
2883
- } catch (error) {
2884
- const name = error.name;
2885
- if (name === "AbortError") {
2886
- throw new Error(`Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`);
2879
+ if (upstream.spec.transport === "http" && error instanceof StreamableHTTPError && error.code) {
2880
+ const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
2881
+ throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
2887
2882
  }
2888
2883
  throw error;
2889
- } finally {
2890
- clearTimeout(timeoutHandle);
2891
- }
2892
- if (!response.ok) {
2893
- const responseText = await response.text();
2894
- if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
2895
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
2896
- }
2897
- throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
2898
- }
2899
- let payload = null;
2900
- const contentType = response.headers.get("content-type") ?? "";
2901
- if (contentType.includes("application/json")) {
2902
- payload = JSON.parse(await response.text());
2903
- } else if (contentType.includes("text/event-stream")) {
2904
- payload = await readSseJsonRpcResponse(response, id);
2905
- } else {
2906
- try {
2907
- payload = JSON.parse(await response.text());
2908
- } catch {
2909
- payload = null;
2910
- }
2911
- }
2912
- if (!payload) {
2913
- throw new Error(`Upstream ${upstream.name} response could not be parsed as JSON-RPC payload.`);
2914
- }
2915
- if (payload.error) {
2916
- throw new Error(`Upstream ${upstream.name} error: ${payload.error.message}`);
2917
- }
2918
- return payload.result;
2919
- }
2920
- async function readSseJsonRpcResponse(response, expectedId) {
2921
- if (!response.body) {
2922
- return null;
2923
- }
2924
- const decoder = new TextDecoder;
2925
- const reader = response.body.getReader();
2926
- let buffer = "";
2927
- let dataLines = [];
2928
- let latestPayload = null;
2929
- const consumeEvent = () => {
2930
- if (dataLines.length === 0) {
2931
- return null;
2932
- }
2933
- const combined = dataLines.join(`
2934
- `).trim();
2935
- dataLines = [];
2936
- if (!combined || combined === "[DONE]") {
2937
- return null;
2938
- }
2939
- try {
2940
- const parsed = JSON.parse(combined);
2941
- latestPayload = parsed;
2942
- const parsedId = parsed.id ?? null;
2943
- if (parsedId === expectedId) {
2944
- return parsed;
2945
- }
2946
- return null;
2947
- } catch {
2948
- return null;
2949
- }
2950
- };
2951
- try {
2952
- while (true) {
2953
- const { value, done } = await reader.read();
2954
- if (done) {
2955
- break;
2956
- }
2957
- buffer += decoder.decode(value, { stream: true });
2958
- while (true) {
2959
- const newlineIndex = buffer.indexOf(`
2960
- `);
2961
- if (newlineIndex < 0) {
2962
- break;
2963
- }
2964
- let line = buffer.slice(0, newlineIndex);
2965
- buffer = buffer.slice(newlineIndex + 1);
2966
- if (line.endsWith("\r")) {
2967
- line = line.slice(0, -1);
2968
- }
2969
- if (line.startsWith("data:")) {
2970
- dataLines.push(line.slice("data:".length).trimStart());
2971
- continue;
2972
- }
2973
- if (line === "") {
2974
- const matched = consumeEvent();
2975
- if (matched) {
2976
- return matched;
2977
- }
2978
- }
2979
- }
2980
- }
2981
- buffer += decoder.decode();
2982
- if (buffer.length > 0) {
2983
- let finalLine = buffer;
2984
- if (finalLine.endsWith("\r")) {
2985
- finalLine = finalLine.slice(0, -1);
2986
- }
2987
- if (finalLine.startsWith("data:")) {
2988
- dataLines.push(finalLine.slice("data:".length).trimStart());
2989
- }
2990
- }
2991
- return consumeEvent() ?? latestPayload;
2992
- } finally {
2993
- try {
2994
- await reader.cancel();
2995
- } catch {}
2996
2884
  }
2997
2885
  }
2998
2886
  function authHeaderIsValid(request, expectedToken) {
@@ -3255,7 +3143,7 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
3255
3143
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
3256
3144
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
3257
3145
  }
3258
- await reconcileStdioConnections(config, runtime);
3146
+ await reconcileUpstreamConnections(config, runtime);
3259
3147
  if (request.method === "custom/tokenCounts") {
3260
3148
  const result = await getUpstreamTokenCounts(config, secrets, runtime);
3261
3149
  return makeResult(id, result);
@@ -3351,7 +3239,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
3351
3239
  function createGatewayServer(options) {
3352
3240
  const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
3353
3241
  const runtime = {
3354
- stdioConnections: new Map,
3242
+ upstreamConnections: new Map,
3355
3243
  upstreamErrors: new Map
3356
3244
  };
3357
3245
  const server = http2.createServer(async (request, response) => {
@@ -3517,10 +3405,10 @@ function createGatewayServer(options) {
3517
3405
  }
3518
3406
  });
3519
3407
  server.on("close", () => {
3520
- for (const entry of runtime.stdioConnections.values()) {
3521
- closeStdioConnection(entry);
3408
+ for (const entry of runtime.upstreamConnections.values()) {
3409
+ closeUpstreamConnection(entry);
3522
3410
  }
3523
- runtime.stdioConnections.clear();
3411
+ runtime.upstreamConnections.clear();
3524
3412
  });
3525
3413
  server.listen(options.port, "127.0.0.1");
3526
3414
  return server;
@@ -2485,7 +2485,7 @@ import {
2485
2485
  StdioClientTransport,
2486
2486
  getDefaultEnvironment
2487
2487
  } from "@modelcontextprotocol/sdk/client/stdio.js";
2488
- var JSON_RPC_VERSION2 = "2.0";
2488
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2489
2489
  var SERVER_VERSION = APP_VERSION;
2490
2490
  var DEFAULT_UPSTREAM_TIMEOUT_MS = 60000;
2491
2491
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
@@ -2653,10 +2653,7 @@ function parseNamespacedUri(uri) {
2653
2653
  };
2654
2654
  }
2655
2655
  async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
2656
- if (upstream.spec.transport === "stdio") {
2657
- return callStdioUpstream(upstream, method, params, secrets, runtime);
2658
- }
2659
- return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
2656
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
2660
2657
  }
2661
2658
  async function getUpstreamTokenCounts(config, secrets, runtime) {
2662
2659
  if (!runtime.tokenCache) {
@@ -2749,75 +2746,110 @@ function buildStdioServerParameters(spec, secrets) {
2749
2746
  env: resolveStdioEnv(spec, secrets)
2750
2747
  };
2751
2748
  }
2752
- async function closeStdioConnection(entry) {
2749
+ async function buildHttpHeaders(upstream, secrets, options) {
2750
+ const headers = {};
2751
+ for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2752
+ if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2753
+ const oauthServerName = oauthReferenceServerName(value);
2754
+ const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, {
2755
+ forceRefresh: options.forceOAuthRefresh
2756
+ });
2757
+ headers[key] = `Bearer ${accessToken}`;
2758
+ } else {
2759
+ headers[key] = secrets.resolveMaybeSecret(value);
2760
+ }
2761
+ }
2762
+ if (options.passthroughAuthorizationHeader) {
2763
+ headers.Authorization = options.passthroughAuthorizationHeader;
2764
+ }
2765
+ return headers;
2766
+ }
2767
+ async function closeUpstreamConnection(entry) {
2753
2768
  try {
2754
2769
  const connection = await entry.promise;
2755
2770
  await connection.transport.close();
2756
2771
  } catch {}
2757
2772
  }
2758
- async function reconcileStdioConnections(config, runtime) {
2773
+ async function reconcileUpstreamConnections(config, runtime) {
2759
2774
  const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
2760
- const staleNames = [];
2761
- for (const [name, entry] of runtime.stdioConnections.entries()) {
2762
- const expectedFingerprint = activeSpecs.get(name);
2775
+ const staleKeys = [];
2776
+ for (const [key, entry] of runtime.upstreamConnections.entries()) {
2777
+ const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
2778
+ const expectedFingerprint = activeSpecs.get(upstreamName);
2763
2779
  if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
2764
- staleNames.push(name);
2780
+ staleKeys.push(key);
2765
2781
  }
2766
2782
  }
2767
- for (const name of staleNames) {
2768
- const entry = runtime.stdioConnections.get(name);
2783
+ for (const key of staleKeys) {
2784
+ const entry = runtime.upstreamConnections.get(key);
2769
2785
  if (!entry) {
2770
2786
  continue;
2771
2787
  }
2772
- runtime.stdioConnections.delete(name);
2773
- await closeStdioConnection(entry);
2788
+ runtime.upstreamConnections.delete(key);
2789
+ await closeUpstreamConnection(entry);
2774
2790
  }
2775
2791
  }
2776
- function invalidateStdioConnection(name, runtime) {
2777
- const existing = runtime.stdioConnections.get(name);
2792
+ function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
2793
+ const key = passthroughAuthorizationHeader ? `${upstreamName}:passthrough` : upstreamName;
2794
+ const existing = runtime.upstreamConnections.get(key);
2778
2795
  if (!existing) {
2779
2796
  return;
2780
2797
  }
2781
- runtime.stdioConnections.delete(name);
2782
- closeStdioConnection(existing);
2798
+ runtime.upstreamConnections.delete(key);
2799
+ closeUpstreamConnection(existing);
2783
2800
  }
2784
- async function getStdioConnection(upstream, secrets, runtime) {
2801
+ async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
2802
+ const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
2803
+ const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:passthrough` : upstream.name;
2785
2804
  const fingerprint = specFingerprint(upstream.spec);
2786
- const existing = runtime.stdioConnections.get(upstream.name);
2787
- if (existing) {
2788
- if (existing.fingerprint === fingerprint) {
2789
- return existing.promise;
2805
+ if (!forceOAuthRefresh) {
2806
+ const existing = runtime.upstreamConnections.get(cacheKey);
2807
+ if (existing) {
2808
+ if (existing.fingerprint === fingerprint) {
2809
+ return existing.promise;
2810
+ }
2811
+ runtime.upstreamConnections.delete(cacheKey);
2812
+ closeUpstreamConnection(existing);
2790
2813
  }
2791
- runtime.stdioConnections.delete(upstream.name);
2792
- closeStdioConnection(existing);
2793
2814
  }
2794
2815
  const promise = (async () => {
2795
- const transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2796
- const client = new Client({
2797
- name: "mcpx",
2798
- version: SERVER_VERSION
2799
- });
2816
+ let transport;
2817
+ if (upstream.spec.transport === "stdio") {
2818
+ transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2819
+ } else {
2820
+ const headers = await buildHttpHeaders(upstream, secrets, { forceOAuthRefresh, passthroughAuthorizationHeader });
2821
+ const capturingFetch = async (url, init) => {
2822
+ const response = await fetch(url, init);
2823
+ const wwwAuth = response.headers.get("www-authenticate");
2824
+ if (wwwAuth) {
2825
+ if (!runtime.lastWwwAuthenticate) {
2826
+ runtime.lastWwwAuthenticate = new Map;
2827
+ }
2828
+ runtime.lastWwwAuthenticate.set(upstream.name, wwwAuth);
2829
+ }
2830
+ return response;
2831
+ };
2832
+ transport = new StreamableHTTPClientTransport(new URL2(upstream.spec.url), { requestInit: { headers }, fetch: capturingFetch });
2833
+ }
2834
+ const client = new Client({ name: "mcpx", version: SERVER_VERSION });
2800
2835
  await withTimeout(client.connect(transport), DEFAULT_CONNECT_TIMEOUT_MS, `Upstream ${upstream.name} failed to connect within ${DEFAULT_CONNECT_TIMEOUT_MS}ms.`);
2801
- return {
2802
- fingerprint,
2803
- client,
2804
- transport
2805
- };
2836
+ return { fingerprint, client, transport };
2806
2837
  })();
2807
- runtime.stdioConnections.set(upstream.name, {
2808
- fingerprint,
2809
- promise
2810
- });
2838
+ runtime.upstreamConnections.set(cacheKey, { fingerprint, promise });
2811
2839
  try {
2812
2840
  return await promise;
2813
2841
  } catch (error) {
2814
- runtime.stdioConnections.delete(upstream.name);
2842
+ runtime.upstreamConnections.delete(cacheKey);
2815
2843
  throw error;
2816
2844
  }
2817
2845
  }
2818
- async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2846
+ async function callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, forceOAuthRefresh) {
2847
+ const hasOAuth = upstream.spec.transport === "http" && Object.values(upstream.spec.headers ?? {}).some(isOAuthReference);
2819
2848
  try {
2820
- const connection = await getStdioConnection(upstream, secrets, runtime);
2849
+ const connection = await getUpstreamConnection(upstream, secrets, runtime, {
2850
+ forceOAuthRefresh,
2851
+ passthroughAuthorizationHeader
2852
+ });
2821
2853
  const timeoutMs = getConfiguredTimeoutMs();
2822
2854
  const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
2823
2855
  if (method === "tools/list") {
@@ -2838,161 +2870,17 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2838
2870
  if (method === "prompts/get") {
2839
2871
  return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
2840
2872
  }
2841
- throw new Error(`Unsupported stdio passthrough method: ${method}`);
2873
+ throw new Error(`Unsupported method: ${method}`);
2842
2874
  } catch (error) {
2843
- invalidateStdioConnection(upstream.name, runtime);
2844
- throw error;
2845
- }
2846
- }
2847
- async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
2848
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
2849
- }
2850
- async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
2851
- const headers = {
2852
- "content-type": "application/json",
2853
- accept: "application/json, text/event-stream"
2854
- };
2855
- for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2856
- if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2857
- const oauthServerName = oauthReferenceServerName(value);
2858
- const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
2859
- headers[key] = `Bearer ${accessToken}`;
2860
- } else {
2861
- headers[key] = secrets.resolveMaybeSecret(value);
2875
+ invalidateUpstreamConnection(upstream.name, passthroughAuthorizationHeader, runtime);
2876
+ if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
2877
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
2862
2878
  }
2863
- }
2864
- if (passthroughAuthorizationHeader) {
2865
- headers.Authorization = passthroughAuthorizationHeader;
2866
- }
2867
- const timeoutMs = getConfiguredTimeoutMs();
2868
- const timeoutController = new AbortController;
2869
- const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
2870
- let response;
2871
- try {
2872
- response = await fetch(upstream.spec.url, {
2873
- method: "POST",
2874
- headers,
2875
- body: JSON.stringify({
2876
- jsonrpc: JSON_RPC_VERSION2,
2877
- id,
2878
- method,
2879
- params
2880
- }),
2881
- signal: timeoutController.signal
2882
- });
2883
- } catch (error) {
2884
- const name = error.name;
2885
- if (name === "AbortError") {
2886
- throw new Error(`Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`);
2879
+ if (upstream.spec.transport === "http" && error instanceof StreamableHTTPError && error.code) {
2880
+ const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
2881
+ throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
2887
2882
  }
2888
2883
  throw error;
2889
- } finally {
2890
- clearTimeout(timeoutHandle);
2891
- }
2892
- if (!response.ok) {
2893
- const responseText = await response.text();
2894
- if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
2895
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
2896
- }
2897
- throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
2898
- }
2899
- let payload = null;
2900
- const contentType = response.headers.get("content-type") ?? "";
2901
- if (contentType.includes("application/json")) {
2902
- payload = JSON.parse(await response.text());
2903
- } else if (contentType.includes("text/event-stream")) {
2904
- payload = await readSseJsonRpcResponse(response, id);
2905
- } else {
2906
- try {
2907
- payload = JSON.parse(await response.text());
2908
- } catch {
2909
- payload = null;
2910
- }
2911
- }
2912
- if (!payload) {
2913
- throw new Error(`Upstream ${upstream.name} response could not be parsed as JSON-RPC payload.`);
2914
- }
2915
- if (payload.error) {
2916
- throw new Error(`Upstream ${upstream.name} error: ${payload.error.message}`);
2917
- }
2918
- return payload.result;
2919
- }
2920
- async function readSseJsonRpcResponse(response, expectedId) {
2921
- if (!response.body) {
2922
- return null;
2923
- }
2924
- const decoder = new TextDecoder;
2925
- const reader = response.body.getReader();
2926
- let buffer = "";
2927
- let dataLines = [];
2928
- let latestPayload = null;
2929
- const consumeEvent = () => {
2930
- if (dataLines.length === 0) {
2931
- return null;
2932
- }
2933
- const combined = dataLines.join(`
2934
- `).trim();
2935
- dataLines = [];
2936
- if (!combined || combined === "[DONE]") {
2937
- return null;
2938
- }
2939
- try {
2940
- const parsed = JSON.parse(combined);
2941
- latestPayload = parsed;
2942
- const parsedId = parsed.id ?? null;
2943
- if (parsedId === expectedId) {
2944
- return parsed;
2945
- }
2946
- return null;
2947
- } catch {
2948
- return null;
2949
- }
2950
- };
2951
- try {
2952
- while (true) {
2953
- const { value, done } = await reader.read();
2954
- if (done) {
2955
- break;
2956
- }
2957
- buffer += decoder.decode(value, { stream: true });
2958
- while (true) {
2959
- const newlineIndex = buffer.indexOf(`
2960
- `);
2961
- if (newlineIndex < 0) {
2962
- break;
2963
- }
2964
- let line = buffer.slice(0, newlineIndex);
2965
- buffer = buffer.slice(newlineIndex + 1);
2966
- if (line.endsWith("\r")) {
2967
- line = line.slice(0, -1);
2968
- }
2969
- if (line.startsWith("data:")) {
2970
- dataLines.push(line.slice("data:".length).trimStart());
2971
- continue;
2972
- }
2973
- if (line === "") {
2974
- const matched = consumeEvent();
2975
- if (matched) {
2976
- return matched;
2977
- }
2978
- }
2979
- }
2980
- }
2981
- buffer += decoder.decode();
2982
- if (buffer.length > 0) {
2983
- let finalLine = buffer;
2984
- if (finalLine.endsWith("\r")) {
2985
- finalLine = finalLine.slice(0, -1);
2986
- }
2987
- if (finalLine.startsWith("data:")) {
2988
- dataLines.push(finalLine.slice("data:".length).trimStart());
2989
- }
2990
- }
2991
- return consumeEvent() ?? latestPayload;
2992
- } finally {
2993
- try {
2994
- await reader.cancel();
2995
- } catch {}
2996
2884
  }
2997
2885
  }
2998
2886
  function authHeaderIsValid(request, expectedToken) {
@@ -3255,7 +3143,7 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
3255
3143
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
3256
3144
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
3257
3145
  }
3258
- await reconcileStdioConnections(config, runtime);
3146
+ await reconcileUpstreamConnections(config, runtime);
3259
3147
  if (request.method === "custom/tokenCounts") {
3260
3148
  const result = await getUpstreamTokenCounts(config, secrets, runtime);
3261
3149
  return makeResult(id, result);
@@ -3351,7 +3239,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
3351
3239
  function createGatewayServer(options) {
3352
3240
  const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
3353
3241
  const runtime = {
3354
- stdioConnections: new Map,
3242
+ upstreamConnections: new Map,
3355
3243
  upstreamErrors: new Map
3356
3244
  };
3357
3245
  const server = http2.createServer(async (request, response) => {
@@ -3517,10 +3405,10 @@ function createGatewayServer(options) {
3517
3405
  }
3518
3406
  });
3519
3407
  server.on("close", () => {
3520
- for (const entry of runtime.stdioConnections.values()) {
3521
- closeStdioConnection(entry);
3408
+ for (const entry of runtime.upstreamConnections.values()) {
3409
+ closeUpstreamConnection(entry);
3522
3410
  }
3523
- runtime.stdioConnections.clear();
3411
+ runtime.upstreamConnections.clear();
3524
3412
  });
3525
3413
  server.listen(options.port, "127.0.0.1");
3526
3414
  return server;
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  stopDaemon,
39
39
  syncAllClients,
40
40
  toSecretRef
41
- } from "./cli-p2mah4z3.js";
41
+ } from "./cli-7k3vf6tg.js";
42
42
  import {
43
43
  loadConfig,
44
44
  loadMergedConfig,
@@ -41,7 +41,7 @@ import {
41
41
  stopDaemon,
42
42
  syncAllClients,
43
43
  toSecretRef
44
- } from "../cli-p2mah4z3.js";
44
+ } from "../cli-7k3vf6tg.js";
45
45
  import {
46
46
  defaultConfig,
47
47
  loadConfig,
@@ -1,6 +1,7 @@
1
1
  import http from "node:http";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
5
  import { SecretsManager } from "../core/secrets.js";
5
6
  import type { McpxConfig, UpstreamTokenCount } from "../types.js";
6
7
  interface GatewayServerOptions {
@@ -8,22 +9,23 @@ interface GatewayServerOptions {
8
9
  expectedToken: string;
9
10
  secrets: SecretsManager;
10
11
  }
11
- interface StdioConnection {
12
+ interface UpstreamConnection {
12
13
  fingerprint: string;
13
14
  client: Client;
14
- transport: StdioClientTransport;
15
+ transport: StdioClientTransport | StreamableHTTPClientTransport;
15
16
  }
16
- interface StdioConnectionEntry {
17
+ interface UpstreamConnectionEntry {
17
18
  fingerprint: string;
18
- promise: Promise<StdioConnection>;
19
+ promise: Promise<UpstreamConnection>;
19
20
  }
20
21
  interface GatewayRuntimeState {
21
- stdioConnections: Map<string, StdioConnectionEntry>;
22
+ upstreamConnections: Map<string, UpstreamConnectionEntry>;
22
23
  tokenCache?: Map<string, {
23
24
  fingerprint: string;
24
25
  count: UpstreamTokenCount;
25
26
  }>;
26
27
  upstreamErrors?: Map<string, string>;
28
+ lastWwwAuthenticate?: Map<string, string>;
27
29
  }
28
30
  export declare function getUpstreamTokenCounts(config: McpxConfig, secrets: SecretsManager, runtime: GatewayRuntimeState): Promise<Record<string, UpstreamTokenCount>>;
29
31
  export declare function createGatewayServer(options: GatewayServerOptions): http.Server;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/gateway/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AAInD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAIV,UAAU,EAIV,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAarB,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAED,UAAU,oBAAoB;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CACnC;AAED,UAAU,mBAAmB;IAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC,CAAC;IAC7E,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAsOD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAgE7C;AAi4BD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAkM9E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/gateway/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,6BAA6B,EAAuB,MAAM,oDAAoD,CAAC;AAIxH,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAIV,UAAU,EAIV,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAarB,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,UAAU,kBAAkB;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,oBAAoB,GAAG,6BAA6B,CAAC;CACjE;AAED,UAAU,uBAAuB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACtC;AAED,UAAU,mBAAmB;IAC3B,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;IAC1D,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC,CAAC;IAC7E,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AA2ND,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAgE7C;AAuvBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAkM9E"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const APP_VERSION = "0.1.85";
1
+ export declare const APP_VERSION = "0.1.87";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwonye/mcpx",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "HTTP-first MCP gateway and cross-client installer",
5
5
  "keywords": [
6
6
  "mcp",