Repository: geelen/mcp-remote
Pull request #32: Streamable http support
Author: login: geelen
Created: 2025-04-17T06:34:58Z | Merged: 2025-04-30T11:58:02Z

**Update:** this is testable! Use `mcp-remote@next` in your config and it should pull `v0.1.0-0`. I'll continue publishing prereleases on the `next` tag to get some real world feedback until it's ready.

This depends on the changes in https://github.com/modelcontextprotocol/typescript-sdk/pull/351 which aren't released yet, so marking this as draft for now. It's also extremely under-tested, since there's now _a lot_ of combinations of servers/transports/auth schemes

The new API all comes under the `--transport` CLI flag, which has 4 values: `http-first` (the default), `sse-first`, `http-only` and `sse-only`. For `-first` flags, we try one type of transport then fall back to the other. That's done by looking for a `405` for sse-first and a `404` for http-first, then trying the other. It's not exactly airtight but it appears to work for the first few things i've tested.

<img width="544" alt="image" src="https://github.com/user-attachments/assets/87297555-a9cf-4d4f-8d56-fc560f435fbb" />

![image](https://github.com/user-attachments/assets/e00d4b36-a6ce-40fa-9c0f-e7eb7e2ffe48)

The tricky thing is combining this with auth, of course, where a transport can fail either because it's unauthed _or_ it's using the wrong transport. Claude has done its best with some guidance from me, and I'd almost believe it was done if I had a good test setup to verify. Will work on that next.


Diff:
diff --git a/package.json b/package.json
index 47cfa99..eeb95b5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mcp-remote",
-  "version": "0.0.22",
+  "version": "0.1.0",
   "description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
   "keywords": [
     "mcp",
@@ -28,11 +28,11 @@
     "check": "prettier --check . && tsc"
   },
   "dependencies": {
-    "@modelcontextprotocol/sdk": "^1.9.0",
     "express": "^4.21.2",
     "open": "^10.1.0"
   },
   "devDependencies": {
+    "@modelcontextprotocol/sdk": "^1.10.2",
     "@types/express": "^5.0.0",
     "@types/node": "^22.13.10",
     "@types/react": "^19.0.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e550c3e..48d8491 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,9 +8,6 @@ importers:
 
   .:
     dependencies:
-      '@modelcontextprotocol/sdk':
-        specifier: ^1.9.0
-        version: 1.9.0
       express:
         specifier: ^4.21.2
         version: 4.21.2
@@ -18,6 +15,9 @@ importers:
         specifier: ^10.1.0
         version: 10.1.0
     devDependencies:
+      '@modelcontextprotocol/sdk':
+        specifier: ^1.10.2
+        version: 1.10.2
       '@types/express':
         specifier: ^5.0.0
         version: 5.0.0
@@ -217,8 +217,8 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
-  '@modelcontextprotocol/sdk@1.9.0':
-    resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
+  '@modelcontextprotocol/sdk@1.10.2':
+    resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
     engines: {node: '>=18'}
 
   '@pkgjs/parseargs@0.11.0':
@@ -396,8 +396,8 @@ packages:
     resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
 
-  body-parser@2.1.0:
-    resolution: {integrity: sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==}
+  body-parser@2.2.0:
+    resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
     engines: {node: '>=18'}
 
   brace-expansion@2.0.1:
@@ -490,15 +490,6 @@ packages:
       supports-color:
         optional: true
 
-  debug@4.3.6:
-    resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
   debug@4.4.0:
     resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
     engines: {node: '>=6.0'}
@@ -576,12 +567,12 @@ packages:
     resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
     engines: {node: '>= 0.6'}
 
-  eventsource-parser@3.0.0:
-    resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
+  eventsource-parser@3.0.1:
+    resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
     engines: {node: '>=18.0.0'}
 
-  eventsource@3.0.5:
-    resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==}
+  eventsource@3.0.6:
+    resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==}
     engines: {node: '>=18.0.0'}
 
   express-rate-limit@7.5.0:
@@ -594,8 +585,8 @@ packages:
     resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
     engines: {node: '>= 0.10.0'}
 
-  express@5.0.1:
-    resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==}
+  express@5.1.0:
+    resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
     engines: {node: '>= 18'}
 
   fdir@6.4.3:
@@ -673,10 +664,6 @@ packages:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
 
-  iconv-lite@0.5.2:
-    resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==}
-    engines: {node: '>=0.10.0'}
-
   iconv-lite@0.6.3:
     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
     engines: {node: '>=0.10.0'}
@@ -771,8 +758,8 @@ packages:
     resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
     engines: {node: '>= 0.6'}
 
-  mime-types@3.0.0:
-    resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==}
+  mime-types@3.0.1:
+    resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
     engines: {node: '>= 0.6'}
 
   mime@1.6.0:
@@ -791,9 +778,6 @@ packages:
   ms@2.0.0:
     resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
 
-  ms@2.1.2:
-    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
-
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
@@ -935,8 +919,8 @@ packages:
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
-  router@2.1.0:
-    resolution: {integrity: sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==}
+  router@2.2.0:
+    resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
     engines: {node: '>= 18'}
 
   run-applescript@7.0.0:
@@ -953,16 +937,16 @@ packages:
     resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
     engines: {node: '>= 0.8.0'}
 
-  send@1.1.0:
-    resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==}
+  send@1.2.0:
+    resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
     engines: {node: '>= 18'}
 
   serve-static@1.16.2:
     resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
     engines: {node: '>= 0.8.0'}
 
-  serve-static@2.1.0:
-    resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==}
+  serve-static@2.2.0:
+    resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
     engines: {node: '>= 18'}
 
   setprototypeof@1.2.0:
@@ -1081,8 +1065,8 @@ packages:
     resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
     engines: {node: '>= 0.6'}
 
-  type-is@2.0.0:
-    resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==}
+  type-is@2.0.1:
+    resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
     engines: {node: '>= 0.6'}
 
   typescript@5.8.2:
@@ -1132,8 +1116,8 @@ packages:
     peerDependencies:
       zod: ^3.24.1
 
-  zod@3.24.2:
-    resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
+  zod@3.24.3:
+    resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
 
 snapshots:
 
@@ -1238,18 +1222,18 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
-  '@modelcontextprotocol/sdk@1.9.0':
+  '@modelcontextprotocol/sdk@1.10.2':
     dependencies:
       content-type: 1.0.5
       cors: 2.8.5
       cross-spawn: 7.0.6
-      eventsource: 3.0.5
-      express: 5.0.1
-      express-rate-limit: 7.5.0(express@5.0.1)
+      eventsource: 3.0.6
+      express: 5.1.0
+      express-rate-limit: 7.5.0(express@5.1.0)
       pkce-challenge: 5.0.0
       raw-body: 3.0.0
-      zod: 3.24.2
-      zod-to-json-schema: 3.24.5(zod@3.24.2)
+      zod: 3.24.3
+      zod-to-json-schema: 3.24.5(zod@3.24.3)
     transitivePeerDependencies:
       - supports-color
 
@@ -1372,7 +1356,7 @@ snapshots:
 
   accepts@2.0.0:
     dependencies:
-      mime-types: 3.0.0
+      mime-types: 3.0.1
       negotiator: 1.0.0
 
   ansi-regex@5.0.1: {}
@@ -1408,17 +1392,17 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  body-parser@2.1.0:
+  body-parser@2.2.0:
     dependencies:
       bytes: 3.1.2
       content-type: 1.0.5
       debug: 4.4.0
       http-errors: 2.0.0
-      iconv-lite: 0.5.2
+      iconv-lite: 0.6.3
       on-finished: 2.4.1
       qs: 6.14.0
       raw-body: 3.0.0
-      type-is: 2.0.0
+      type-is: 2.0.1
     transitivePeerDependencies:
       - supports-color
 
@@ -1496,10 +1480,6 @@ snapshots:
     dependencies:
       ms: 2.0.0
 
-  debug@4.3.6:
-    dependencies:
-      ms: 2.1.2
-
   debug@4.4.0:
     dependencies:
       ms: 2.1.3
@@ -1575,15 +1555,15 @@ snapshots:
 
   etag@1.8.1: {}
 
-  eventsource-parser@3.0.0: {}
+  eventsource-parser@3.0.1: {}
 
-  eventsource@3.0.5:
+  eventsource@3.0.6:
     dependencies:
-      eventsource-parser: 3.0.0
+      eventsource-parser: 3.0.1
 
-  express-rate-limit@7.5.0(express@5.0.1):
+  express-rate-limit@7.5.0(express@5.1.0):
     dependencies:
-      express: 5.0.1
+      express: 5.1.0
 
   express@4.21.2:
     dependencies:
@@ -1621,16 +1601,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  express@5.0.1:
+  express@5.1.0:
     dependencies:
       accepts: 2.0.0
-      body-parser: 2.1.0
+      body-parser: 2.2.0
       content-disposition: 1.0.0
       content-type: 1.0.5
       cookie: 0.7.1
       cookie-signature: 1.2.2
-      debug: 4.3.6
-      depd: 2.0.0
+      debug: 4.4.0
       encodeurl: 2.0.0
       escape-html: 1.0.3
       etag: 1.8.1
@@ -1638,22 +1617,18 @@ snapshots:
       fresh: 2.0.0
       http-errors: 2.0.0
       merge-descriptors: 2.0.0
-      methods: 1.1.2
-      mime-types: 3.0.0
+      mime-types: 3.0.1
       on-finished: 2.4.1
       once: 1.4.0
       parseurl: 1.3.3
       proxy-addr: 2.0.7
-      qs: 6.13.0
+      qs: 6.14.0
       range-parser: 1.2.1
-      router: 2.1.0
-      safe-buffer: 5.2.1
-      send: 1.1.0
-      serve-static: 2.1.0
-      setprototypeof: 1.2.0
+      router: 2.2.0
+      send: 1.2.0
+      serve-static: 2.2.0
       statuses: 2.0.1
-      type-is: 2.0.0
-      utils-merge: 1.0.1
+      type-is: 2.0.1
       vary: 1.1.2
     transitivePeerDependencies:
       - supports-color
@@ -1752,10 +1727,6 @@ snapshots:
     dependencies:
       safer-buffer: 2.1.2
 
-  iconv-lite@0.5.2:
-    dependencies:
-      safer-buffer: 2.1.2
-
   iconv-lite@0.6.3:
     dependencies:
       safer-buffer: 2.1.2
@@ -1818,7 +1789,7 @@ snapshots:
     dependencies:
       mime-db: 1.52.0
 
-  mime-types@3.0.0:
+  mime-types@3.0.1:
     dependencies:
       mime-db: 1.54.0
 
@@ -1832,8 +1803,6 @@ snapshots:
 
   ms@2.0.0: {}
 
-  ms@2.1.2: {}
-
   ms@2.1.3: {}
 
   mz@2.7.0:
@@ -1960,11 +1929,15 @@ snapshots:
       '@rollup/rollup-win32-x64-msvc': 4.35.0
       fsevents: 2.3.3
 
-  router@2.1.0:
+  router@2.2.0:
     dependencies:
+      debug: 4.4.0
+      depd: 2.0.0
       is-promise: 4.0.0
       parseurl: 1.3.3
       path-to-regexp: 8.2.0
+    transitivePeerDependencies:
+      - supports-color
 
   run-applescript@7.0.0: {}
 
@@ -1990,16 +1963,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  send@1.1.0:
+  send@1.2.0:
     dependencies:
       debug: 4.4.0
-      destroy: 1.2.0
       encodeurl: 2.0.0
       escape-html: 1.0.3
       etag: 1.8.1
-      fresh: 0.5.2
+      fresh: 2.0.0
       http-errors: 2.0.0
-      mime-types: 2.1.35
+      mime-types: 3.0.1
       ms: 2.1.3
       on-finished: 2.4.1
       range-parser: 1.2.1
@@ -2016,12 +1988,12 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  serve-static@2.1.0:
+  serve-static@2.2.0:
     dependencies:
       encodeurl: 2.0.0
       escape-html: 1.0.3
       parseurl: 1.3.3
-      send: 1.1.0
+      send: 1.2.0
     transitivePeerDependencies:
       - supports-color
 
@@ -2162,11 +2134,11 @@ snapshots:
       media-typer: 0.3.0
       mime-types: 2.1.35
 
-  type-is@2.0.0:
+  type-is@2.0.1:
     dependencies:
       content-type: 1.0.5
       media-typer: 1.1.0
-      mime-types: 3.0.0
+      mime-types: 3.0.1
 
   typescript@5.8.2: {}
 
@@ -2204,8 +2176,8 @@ snapshots:
 
   wrappy@1.0.2: {}
 
-  zod-to-json-schema@3.24.5(zod@3.24.2):
+  zod-to-json-schema@3.24.5(zod@3.24.3):
     dependencies:
-      zod: 3.24.2
+      zod: 3.24.3
 
-  zod@3.24.2: {}
+  zod@3.24.3: {}
diff --git a/src/client.ts b/src/client.ts
index d620884..4e0c14c 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -11,25 +11,36 @@
 
 import { EventEmitter } from 'events'
 import { Client } from '@modelcontextprotocol/sdk/client/index.js'
-import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
 import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
-import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
 import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
-import { parseCommandLineArgs, setupSignalHandlers, log, MCP_REMOTE_VERSION, getServerUrlHash } from './lib/utils'
-import { coordinateAuth } from './lib/coordination'
+import {
+  parseCommandLineArgs,
+  setupSignalHandlers,
+  log,
+  MCP_REMOTE_VERSION,
+  getServerUrlHash,
+  connectToRemoteServer,
+  TransportStrategy,
+} from './lib/utils'
+import { createLazyAuthCoordinator } from './lib/coordination'
 
 /**
  * Main function to run the client
  */
-async function runClient(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
+async function runClient(
+  serverUrl: string,
+  callbackPort: number,
+  headers: Record<string, string>,
+  transportStrategy: TransportStrategy = 'http-first',
+) {
   // Set up event emitter for auth flow
   const events = new EventEmitter()
 
   // Get the server URL hash for lockfile operations
   const serverUrlHash = getServerUrlHash(serverUrl)
 
-  // Coordinate authentication with other instances
-  const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events)
+  // Create a lazy auth coordinator
+  const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events)
 
   // Create the OAuth client provider
   const authProvider = new NodeOAuthClientProvider({
@@ -38,14 +49,6 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
     clientName: 'MCP CLI Client',
   })
 
-  // If auth was completed by another instance, just log that we'll use the auth from disk
-  if (skipBrowserAuth) {
-    log('Authentication was completed by another instance - will use tokens from disk...')
-    // TODO: remove, the callback is happening before the tokens are exchanged
-    //  so we're slightly too early
-    await new Promise((res) => setTimeout(res, 1_000))
-  }
-
   // Create the client
   const client = new Client(
     {
@@ -57,10 +60,33 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
     },
   )
 
-  // Create the transport factory
-  const url = new URL(serverUrl)
-  function initTransport() {
-    const transport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
+  // Keep track of the server instance for cleanup
+  let server: any = null
+
+  // Define an auth initializer function
+  const authInitializer = async () => {
+    const authState = await authCoordinator.initializeAuth()
+
+    // Store server in outer scope for cleanup
+    server = authState.server
+
+    // If auth was completed by another instance, just log that we'll use the auth from disk
+    if (authState.skipBrowserAuth) {
+      log('Authentication was completed by another instance - will use tokens from disk...')
+      // TODO: remove, the callback is happening before the tokens are exchanged
+      //  so we're slightly too early
+      await new Promise((res) => setTimeout(res, 1_000))
+    }
+
+    return {
+      waitForAuthCode: authState.waitForAuthCode,
+      skipBrowserAuth: authState.skipBrowserAuth,
+    }
+  }
+
+  try {
+    // Connect to remote server with lazy authentication
+    const transport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy)
 
     // Set up message and error handlers
     transport.onmessage = (message) => {
@@ -75,89 +101,59 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
       log('Connection closed.')
       process.exit(0)
     }
-    return transport
-  }
-
-  const transport = initTransport()
 
-  // Set up cleanup handler
-  const cleanup = async () => {
-    log('\nClosing connection...')
-    await client.close()
-    server.close()
-  }
-  setupSignalHandlers(cleanup)
+    // Set up cleanup handler
+    const cleanup = async () => {
+      log('\nClosing connection...')
+      await client.close()
+      // If auth was initialized and server was created, close it
+      if (server) {
+        server.close()
+      }
+    }
+    setupSignalHandlers(cleanup)
 
-  // Try to connect
-  try {
-    log('Connecting to server...')
-    await client.connect(transport)
     log('Connected successfully!')
-  } catch (error) {
-    if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
-      log('Authentication required. Waiting for authorization...')
 
-      // Wait for the authorization code from the callback or another instance
-      const code = await waitForAuthCode()
-
-      try {
-        log('Completing authorization...')
-        await transport.finishAuth(code)
-
-        // Reconnect after authorization with a new transport
-        log('Connecting after authorization...')
-        await client.connect(initTransport())
-
-        log('Connected successfully!')
-
-        // Request tools list after auth
-        log('Requesting tools list...')
-        const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
-        log('Tools:', JSON.stringify(tools, null, 2))
+    try {
+      // Request tools list
+      log('Requesting tools list...')
+      const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
+      log('Tools:', JSON.stringify(tools, null, 2))
+    } catch (e) {
+      log('Error requesting tools list:', e)
+    }
 
-        // Request resources list after auth
-        log('Requesting resource list...')
-        const resources = await client.request({ method: 'resources/list' }, ListResourcesResultSchema)
-        log('Resources:', JSON.stringify(resources, null, 2))
+    try {
+      // Request resources list
+      log('Requesting resource list...')
+      const resources = await client.request({ method: 'resources/list' }, ListResourcesResultSchema)
+      log('Resources:', JSON.stringify(resources, null, 2))
+    } catch (e) {
+      log('Error requesting resources list:', e)
+    }
 
-        log('Listening for messages. Press Ctrl+C to exit.')
-      } catch (authError) {
-        log('Authorization error:', authError)
-        server.close()
-        process.exit(1)
-      }
-    } else {
-      log('Connection error:', error)
+    // log('Listening for messages. Press Ctrl+C to exit.')
+    log('Exiting OK...')
+    // Only close the server if it was initialized
+    if (server) {
       server.close()
-      process.exit(1)
     }
+    process.exit(0)
+  } catch (error) {
+    log('Fatal error:', error)
+    // Only close the server if it was initialized
+    if (server) {
+      server.close()
+    }
+    process.exit(1)
   }
-
-  try {
-    // Request tools list
-    log('Requesting tools list...')
-    const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
-    log('Tools:', JSON.stringify(tools, null, 2))
-  } catch (e) {
-    log('Error requesting tools list:', e)
-  }
-
-  try {
-    // Request resources list
-    log('Requesting resource list...')
-    const resources = await client.request({ method: 'resources/list' }, ListResourcesResultSchema)
-    log('Resources:', JSON.stringify(resources, null, 2))
-  } catch (e) {
-    log('Error requesting resources list:', e)
-  }
-
-  log('Listening for messages. Press Ctrl+C to exit.')
 }
 
 // Parse command-line arguments and run the client
 parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
-  .then(({ serverUrl, callbackPort, headers }) => {
-    return runClient(serverUrl, callbackPort, headers)
+  .then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
+    return runClient(serverUrl, callbackPort, headers, transportStrategy)
   })
   .catch((error) => {
     console.error('Fatal error:', error)
diff --git a/src/lib/coordination.ts b/src/lib/coordination.ts
index ad1d2f6..ffe0c5b 100644
--- a/src/lib/coordination.ts
+++ b/src/lib/coordination.ts
@@ -5,6 +5,10 @@ import express from 'express'
 import { AddressInfo } from 'net'
 import { log, setupOAuthCallbackServerWithLongPoll } from './utils'
 
+export type AuthCoordinator = {
+  initializeAuth: () => Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }>
+}
+
 /**
  * Checks if a process with the given PID is running
  * @param pid The process ID to check
@@ -88,6 +92,36 @@ export async function waitForAuthentication(port: number): Promise<boolean> {
   }
 }
 
+/**
+ * Creates a lazy auth coordinator that will only initiate auth when needed
+ * @param serverUrlHash The hash of the server URL
+ * @param callbackPort The port to use for the callback server
+ * @param events The event emitter to use for signaling
+ * @returns An AuthCoordinator object with an initializeAuth method
+ */
+export function createLazyAuthCoordinator(
+  serverUrlHash: string,
+  callbackPort: number,
+  events: EventEmitter
+): AuthCoordinator {
+  let authState: { server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean } | null = null
+
+  return {
+    initializeAuth: async () => {
+      // If auth has already been initialized, return the existing state
+      if (authState) {
+        return authState
+      }
+
+      log('Initializing auth coordination on-demand')
+      
+      // Initialize auth using the existing coordinateAuth logic
+      authState = await coordinateAuth(serverUrlHash, callbackPort, events)
+      return authState
+    }
+  }
+}
+
 /**
  * Coordinates authentication between multiple instances of the client/proxy
  * @param serverUrlHash The hash of the server URL
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 40c744d..1b68bef 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,15 @@
 import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
+import { Client } from '@modelcontextprotocol/sdk/client/index.js'
 import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
+import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
 import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
+
+// Connection constants
+export const REASON_AUTH_NEEDED = 'authentication-needed'
+export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport'
+
+// Transport strategy types
+export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
 import { OAuthCallbackServerOptions } from './types'
 import express from 'express'
 import net from 'net'
@@ -65,21 +74,33 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
 }
 
 /**
- * Creates and connects to a remote SSE server with OAuth authentication
+ * Type for the auth initialization function
+ */
+export type AuthInitializer = () => Promise<{
+  waitForAuthCode: () => Promise<string>
+  skipBrowserAuth: boolean
+}>
+
+/**
+ * Creates and connects to a remote server with OAuth authentication
+ * @param client The client to connect with
  * @param serverUrl The URL of the remote server
  * @param authProvider The OAuth client provider
  * @param headers Additional headers to send with the request
- * @param waitForAuthCode Function to wait for the auth code
- * @param skipBrowserAuth Whether to skip browser auth and use shared auth
- * @returns The connected SSE client transport
+ * @param authInitializer Function to initialize authentication when needed
+ * @param transportStrategy Strategy for selecting transport type ('sse-only', 'http-only', 'sse-first', 'http-first')
+ * @param recursionReasons Set of reasons for recursive calls (internal use)
+ * @returns The connected transport
  */
 export async function connectToRemoteServer(
+  client: Client | null,
   serverUrl: string,
   authProvider: OAuthClientProvider,
   headers: Record<string, string>,
-  waitForAuthCode: () => Promise<string>,
-  skipBrowserAuth: boolean = false,
-): Promise<SSEClientTransport> {
+  authInitializer: AuthInitializer,
+  transportStrategy: TransportStrategy = 'http-first',
+  recursionReasons: Set<string> = new Set(),
+): Promise<Transport> {
   log(`[${pid}] Connecting to remote server: ${serverUrl}`)
   const url = new URL(serverUrl)
 
@@ -93,25 +114,88 @@ export async function connectToRemoteServer(
             ...(init?.headers as Record<string, string> | undefined),
             ...headers,
             ...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}),
-            Accept: "text/event-stream",
+            Accept: 'text/event-stream',
           } as Record<string, string>,
-        })
-      );
+        }),
+      )
     },
-  };
+  }
 
-  const transport = new SSEClientTransport(url, {
-    authProvider,
-    requestInit: { headers },
-    eventSourceInit,
-  })
+  log(`Using transport strategy: ${transportStrategy}`)
+  // Determine if we should attempt to fallback on error
+  // Choose transport based on user strategy and recursion history
+  const shouldAttemptFallback = transportStrategy === 'http-first' || transportStrategy === 'sse-first'
+
+  // Create transport instance based on the strategy
+  const sseTransport = transportStrategy === 'sse-only' || transportStrategy === 'sse-first'
+  const transport = sseTransport
+    ? new SSEClientTransport(url, {
+        authProvider,
+        requestInit: { headers },
+        eventSourceInit,
+      })
+    : new StreamableHTTPClientTransport(url, {
+        authProvider,
+        requestInit: { headers },
+      })
 
   try {
-    await transport.start()
-    log('Connected to remote server')
+    if (client) {
+      await client.connect(transport)
+    } else {
+      await transport.start()
+      if (!sseTransport) {
+        // Extremely hacky, but we didn't actually send a request when calling transport.start() above, so we don't
+        // know if we're even talking to an HTTP server. But if we forced that now we'd get an error later saying that
+        // the client is already connected. So let's just create a one-off client to make a single request and figure
+        // out if we're actually talking to an HTTP server or not.
+        const testTransport = new StreamableHTTPClientTransport(url, { authProvider, requestInit: { headers } })
+        const testClient = new Client({ name: 'mcp-remote-fallback-test', version: '0.0.0' }, { capabilities: {} })
+        await testClient.connect(testTransport)
+      }
+    }
+    log(`Connected to remote server using ${transport.constructor.name}`)
+
     return transport
   } catch (error) {
-    if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
+    // Check if it's a protocol error and we should attempt fallback
+    if (
+      error instanceof Error &&
+      shouldAttemptFallback &&
+      (sseTransport
+        ? error.message.includes('405') || error.message.includes('Method Not Allowed')
+        : error.message.includes('404') || error.message.includes('Not Found'))
+    ) {
+      log(`Received error: ${error.message}`)
+
+      // If we've already tried falling back once, throw an error
+      if (recursionReasons.has(REASON_TRANSPORT_FALLBACK)) {
+        const errorMessage = `Already attempted transport fallback. Giving up.`
+        log(errorMessage)
+        throw new Error(errorMessage)
+      }
+
+      log(`Recursively reconnecting for reason: ${REASON_TRANSPORT_FALLBACK}`)
+
+      // Add to recursion reasons set
+      recursionReasons.add(REASON_TRANSPORT_FALLBACK)
+
+      // Recursively call connectToRemoteServer with the updated recursion tracking
+      return connectToRemoteServer(
+        client,
+        serverUrl,
+        authProvider,
+        headers,
+        authInitializer,
+        sseTransport ? 'http-only' : 'sse-only',
+        recursionReasons,
+      )
+    } else if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
+      log('Authentication required. Initializing auth...')
+
+      // Initialize authentication on-demand
+      const { waitForAuthCode, skipBrowserAuth } = await authInitializer()
+
       if (skipBrowserAuth) {
         log('Authentication required but skipping browser auth - using shared auth')
       } else {
@@ -125,11 +209,18 @@ export async function connectToRemoteServer(
         log('Completing authorization...')
         await transport.finishAuth(code)
 
-        // Create a new transport after auth
-        const newTransport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
-        await newTransport.start()
-        log('Connected to remote server after authentication')
-        return newTransport
+        if (recursionReasons.has(REASON_AUTH_NEEDED)) {
+          const errorMessage = `Already attempted reconnection for reason: ${REASON_AUTH_NEEDED}. Giving up.`
+          log(errorMessage)
+          throw new Error(errorMessage)
+        }
+
+        // Track this reason for recursion
+        recursionReasons.add(REASON_AUTH_NEEDED)
+        log(`Recursively reconnecting for reason: ${REASON_AUTH_NEEDED}`)
+
+        // Recursively call connectToRemoteServer with the updated recursion tracking
+        return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, recursionReasons)
       } catch (authError) {
         log('Authorization error:', authError)
         throw authError
@@ -301,6 +392,19 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
   const specifiedPort = args[1] ? parseInt(args[1]) : undefined
   const allowHttp = args.includes('--allow-http')
 
+  // Parse transport strategy
+  let transportStrategy: TransportStrategy = 'http-first' // Default
+  const transportIndex = args.indexOf('--transport')
+  if (transportIndex !== -1 && transportIndex < args.length - 1) {
+    const strategy = args[transportIndex + 1]
+    if (strategy === 'sse-only' || strategy === 'http-only' || strategy === 'sse-first' || strategy === 'http-first') {
+      transportStrategy = strategy as TransportStrategy
+      log(`Using transport strategy: ${transportStrategy}`)
+    } else {
+      log(`Warning: Ignoring invalid transport strategy: ${strategy}. Valid values are: sse-only, http-only, sse-first, http-first`)
+    }
+  }
+
   if (!serverUrl) {
     log(usage)
     process.exit(1)
@@ -343,7 +447,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
     })
   }
 
-  return { serverUrl, callbackPort, headers }
+  return { serverUrl, callbackPort, headers, transportStrategy }
 }
 
 /**
diff --git a/src/proxy.ts b/src/proxy.ts
index 9fd87d1..7263a95 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -11,22 +11,36 @@
 
 import { EventEmitter } from 'events'
 import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
-import { connectToRemoteServer, log, mcpProxy, parseCommandLineArgs, setupSignalHandlers, getServerUrlHash } from './lib/utils'
+import {
+  connectToRemoteServer,
+  log,
+  mcpProxy,
+  parseCommandLineArgs,
+  setupSignalHandlers,
+  getServerUrlHash,
+  MCP_REMOTE_VERSION,
+  TransportStrategy,
+} from './lib/utils'
 import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
-import { coordinateAuth } from './lib/coordination'
+import { createLazyAuthCoordinator } from './lib/coordination'
 
 /**
  * Main function to run the proxy
  */
-async function runProxy(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
+async function runProxy(
+  serverUrl: string,
+  callbackPort: number,
+  headers: Record<string, string>,
+  transportStrategy: TransportStrategy = 'http-first',
+) {
   // Set up event emitter for auth flow
   const events = new EventEmitter()
 
   // Get the server URL hash for lockfile operations
   const serverUrlHash = getServerUrlHash(serverUrl)
 
-  // Coordinate authentication with other instances
-  const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events)
+  // Create a lazy auth coordinator
+  const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events)
 
   // Create the OAuth client provider
   const authProvider = new NodeOAuthClientProvider({
@@ -35,20 +49,36 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
     clientName: 'MCP CLI Proxy',
   })
 
-  // If auth was completed by another instance, just log that we'll use the auth from disk
-  if (skipBrowserAuth) {
-    log('Authentication was completed by another instance - will use tokens from disk')
-    // TODO: remove, the callback is happening before the tokens are exchanged
-    //  so we're slightly too early
-    await new Promise((res) => setTimeout(res, 1_000))
-  }
-
   // Create the STDIO transport for local connections
   const localTransport = new StdioServerTransport()
 
+  // Keep track of the server instance for cleanup
+  let server: any = null
+
+  // Define an auth initializer function
+  const authInitializer = async () => {
+    const authState = await authCoordinator.initializeAuth()
+
+    // Store server in outer scope for cleanup
+    server = authState.server
+
+    // If auth was completed by another instance, just log that we'll use the auth from disk
+    if (authState.skipBrowserAuth) {
+      log('Authentication was completed by another instance - will use tokens from disk')
+      // TODO: remove, the callback is happening before the tokens are exchanged
+      //  so we're slightly too early
+      await new Promise((res) => setTimeout(res, 1_000))
+    }
+
+    return {
+      waitForAuthCode: authState.waitForAuthCode,
+      skipBrowserAuth: authState.skipBrowserAuth,
+    }
+  }
+
   try {
-    // Connect to remote server with authentication
-    const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth)
+    // Connect to remote server with lazy authentication
+    const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy)
 
     // Set up bidirectional proxy between local and remote transports
     mcpProxy({
@@ -59,14 +89,17 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
     // Start the local STDIO server
     await localTransport.start()
     log('Local STDIO server running')
-    log('Proxy established successfully between local STDIO and remote SSE')
+    log(`Proxy established successfully between local STDIO and remote ${remoteTransport.constructor.name}`)
     log('Press Ctrl+C to exit')
 
     // Setup cleanup handler
     const cleanup = async () => {
       await remoteTransport.close()
       await localTransport.close()
-      server.close()
+      // Only close the server if it was initialized
+      if (server) {
+        server.close()
+      }
     }
     setupSignalHandlers(cleanup)
   } catch (error) {
@@ -93,15 +126,18 @@ to the CA certificate file. If using claude_desktop_config.json, this might look
 }
         `)
     }
-    server.close()
+    // Only close the server if it was initialized
+    if (server) {
+      server.close()
+    }
     process.exit(1)
   }
 }
 
 // Parse command-line arguments and run the proxy
 parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
-  .then(({ serverUrl, callbackPort, headers }) => {
-    return runProxy(serverUrl, callbackPort, headers)
+  .then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
+    return runProxy(serverUrl, callbackPort, headers, transportStrategy)
   })
   .catch((error) => {
     log('Fatal error:', error)


Reviews:
user: login: kjosk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T03:37:46Z
user: login: kjosk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T13:53:58Z
user: login: geelen
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-24T06:24:08Z

Review comments:
diff_hunk: @@ -35,20 +44,45 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
     clientName: 'MCP CLI Proxy',
   })
 
-  // If auth was completed by another instance, just log that we'll use the auth from disk
-  if (skipBrowserAuth) {
-    log('Authentication was completed by another instance - will use tokens from disk')
-    // TODO: remove, the callback is happening before the tokens are exchanged
-    //  so we're slightly too early
-    await new Promise((res) => setTimeout(res, 1_000))
-  }
-
   // Create the STDIO transport for local connections
   const localTransport = new StdioServerTransport()
 
+  // Keep track of the server instance for cleanup
+  let server: any = null
+
+  // Define an auth initializer function
+  const authInitializer = async () => {
+    const authState = await authCoordinator.initializeAuth()
+    
+    // Store server in outer scope for cleanup
+    server = authState.server
+    
+    // If auth was completed by another instance, just log that we'll use the auth from disk
+    if (authState.skipBrowserAuth) {
+      log('Authentication was completed by another instance - will use tokens from disk')
+      // TODO: remove, the callback is happening before the tokens are exchanged
+      //  so we're slightly too early
+      await new Promise((res) => setTimeout(res, 1_000))
+    }
+    
+    return { 
+      waitForAuthCode: authState.waitForAuthCode, 
+      skipBrowserAuth: authState.skipBrowserAuth 
+    }
+  }
+
   try {
-    // Connect to remote server with authentication
-    const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth)
+    const client = new Client(
+      {
+        name: 'mcp-remote',
+        version: MCP_REMOTE_VERSION,
+      },
+      {
+        capabilities: {},
+      },
+    )
+    // Connect to remote server with lazy authentication
+    const remoteTransport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer)
path: src/proxy.ts
user: login: kjosk
body: Hey, shouldn't you pass `transportStrategy` here like you do in `client.ts`?

```typescript
// client.ts
const transport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy)

// proxy.ts
const remoteTransport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer) // <- missing transportStrategy?
```

created_at: 2025-04-18T03:37:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +44,45 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
     clientName: 'MCP CLI Proxy',
   })
 
-  // If auth was completed by another instance, just log that we'll use the auth from disk
-  if (skipBrowserAuth) {
-    log('Authentication was completed by another instance - will use tokens from disk')
-    // TODO: remove, the callback is happening before the tokens are exchanged
-    //  so we're slightly too early
-    await new Promise((res) => setTimeout(res, 1_000))
-  }
-
   // Create the STDIO transport for local connections
   const localTransport = new StdioServerTransport()
 
+  // Keep track of the server instance for cleanup
+  let server: any = null
+
+  // Define an auth initializer function
+  const authInitializer = async () => {
+    const authState = await authCoordinator.initializeAuth()
+    
+    // Store server in outer scope for cleanup
+    server = authState.server
+    
+    // If auth was completed by another instance, just log that we'll use the auth from disk
+    if (authState.skipBrowserAuth) {
+      log('Authentication was completed by another instance - will use tokens from disk')
+      // TODO: remove, the callback is happening before the tokens are exchanged
+      //  so we're slightly too early
+      await new Promise((res) => setTimeout(res, 1_000))
+    }
+    
+    return { 
+      waitForAuthCode: authState.waitForAuthCode, 
+      skipBrowserAuth: authState.skipBrowserAuth 
+    }
+  }
+
   try {
-    // Connect to remote server with authentication
-    const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth)
+    const client = new Client(
+      {
+        name: 'mcp-remote',
+        version: MCP_REMOTE_VERSION,
+      },
+      {
+        capabilities: {},
+      },
+    )
+    // Connect to remote server with lazy authentication
+    const remoteTransport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer)
path: src/proxy.ts
user: login: kjosk
body: Hi @geelen, any thoughts on this?
created_at: 2025-04-21T13:53:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +44,45 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
     clientName: 'MCP CLI Proxy',
   })
 
-  // If auth was completed by another instance, just log that we'll use the auth from disk
-  if (skipBrowserAuth) {
-    log('Authentication was completed by another instance - will use tokens from disk')
-    // TODO: remove, the callback is happening before the tokens are exchanged
-    //  so we're slightly too early
-    await new Promise((res) => setTimeout(res, 1_000))
-  }
-
   // Create the STDIO transport for local connections
   const localTransport = new StdioServerTransport()
 
+  // Keep track of the server instance for cleanup
+  let server: any = null
+
+  // Define an auth initializer function
+  const authInitializer = async () => {
+    const authState = await authCoordinator.initializeAuth()
+    
+    // Store server in outer scope for cleanup
+    server = authState.server
+    
+    // If auth was completed by another instance, just log that we'll use the auth from disk
+    if (authState.skipBrowserAuth) {
+      log('Authentication was completed by another instance - will use tokens from disk')
+      // TODO: remove, the callback is happening before the tokens are exchanged
+      //  so we're slightly too early
+      await new Promise((res) => setTimeout(res, 1_000))
+    }
+    
+    return { 
+      waitForAuthCode: authState.waitForAuthCode, 
+      skipBrowserAuth: authState.skipBrowserAuth 
+    }
+  }
+
   try {
-    // Connect to remote server with authentication
-    const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth)
+    const client = new Client(
+      {
+        name: 'mcp-remote',
+        version: MCP_REMOTE_VERSION,
+      },
+      {
+        capabilities: {},
+      },
+    )
+    // Connect to remote server with lazy authentication
+    const remoteTransport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer)
path: src/proxy.ts
user: login: geelen
body: Yes, actually. Fixed.
created_at: 2025-04-24T06:24:07Z
author_association: OWNER

PR comments:
user: login: geelen
created_at: 2025-04-24T12:28:19Z
body: Fixed a bug, defaulting now to the SSE transport, able to mount the same server using both SSE and HTTP:

```json
{
  "mcpServers": {
    "streamable-no-auth": {
      "command": "node",
      "args": [
        "mcp-remote@next",
        "http://localhost:8788/mcp"
      ]
    },
    "sse-fallback": {
      "command": "npx",
      "args": [
        "mcp-remote@next",
        "http://localhost:8788/sse"
      ]
    }
  }
}
```

That pairs with the updates here to make it easy to serve both: https://github.com/geelen/mcp-remote-examples/pull/17

```ts
export default new OAuthProvider({
  apiHandlers: {
    '/sse': MyMCP.serveSSE('/sse'),
    '/mcp': MyMCP.serve('/mcp'),
  },
  defaultHandler: GitHubHandler,
  authorizeEndpoint: '/authorize',
  tokenEndpoint: '/token',
  clientRegistrationEndpoint: '/register',
})
```

This is feeling a lot better now, I think it's likely ready to release on Monday but I'd still love to put in some more tests.
author_association: OWNER
user: login: liriID
created_at: 2025-04-29T07:53:35Z
body: note the SSE fallback doesn't work well with this example where the endpoint does support GET http method:

https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#streamable-http

because we expect to get 405 in case of trying SSE on a server working with streamable http
author_association: NONE
user: login: zxkane
created_at: 2025-04-30T04:49:50Z
body: Tested on proxying this [streamable HTTP-only server](https://github.com/zxkane/quip-mcp-server-typescript), works really well!
author_association: NONE
user: login: geelen
created_at: 2025-04-30T05:58:49Z
body: > note the SSE fallback doesn't work well with this example where the endpoint does support GET http method:
> 
> https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#streamable-http
> 
> because we expect to get 405 in case of trying SSE on a server working with streamable http

Ugh of course, that's annoying. You'll have to use `--transport http-first` then.

I actually wanted to default to the new transport anyway but the `.start` method [doesn't do anything](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts#L341-L349), so there's nothing to trigger the fallback logic. But if the SSE GET doesn't reliably trigger a 405 then... 🤔 

Maybe instead of expecting the transport to explode with a known error, I just fire a separate POST to figure out what kind of server it is? That might... be way better, damn.
author_association: OWNER