Compare commits

...

36 commits

Author SHA1 Message Date
d1cb48f770
a
All checks were successful
Publish Any Commit / build (push) Successful in 30s
2025-05-18 18:01:29 +02:00
a63b93aa5c
wip
All checks were successful
Publish Any Commit / build (push) Successful in 25s
2025-05-18 17:32:42 +02:00
d8ce274506
wip
Some checks failed
Publish Any Commit / build (push) Failing after 36s
2025-05-18 17:23:07 +02:00
a7a76d3f17
wip
Some checks failed
Publish Any Commit / build (push) Failing after 18s
2025-05-18 17:18:21 +02:00
27907a4624
wip
Some checks failed
Publish Any Commit / build (push) Failing after 17s
2025-05-18 17:12:39 +02:00
0213c20d3d
update pnpm
Some checks failed
Publish Any Commit / build (push) Failing after 17s
2025-05-18 17:11:17 +02:00
4f6de14fbc
add packageManager
Some checks failed
Publish Any Commit / build (push) Failing after 15s
2025-05-18 17:10:14 +02:00
675dc6a760
fix tool path
Some checks failed
Publish Any Commit / build (push) Failing after 19s
2025-05-18 17:01:58 +02:00
8f83b18966
adjust ci
Some checks failed
Publish Any Commit / build (push) Failing after 3s
2025-05-18 17:00:50 +02:00
Will
7eecc9ca3f Update README.md
Move `env` into mcpServer configuration. The examples have it placed outside. 

If you don't pay attention, you'll end up wondering why you have empty `env` being passed through.
2025-05-15 07:26:39 +01:00
Glen Maddern
5199279ea7 0.1.5 2025-05-14 12:31:07 +01:00
Glen Maddern
b1dfa9fe5b Picking a default port based on the server hash 2025-05-14 12:31:07 +01:00
Glen Maddern
6f2399bbfb remove client info on conflict 2025-05-14 12:31:07 +01:00
Glen Maddern
e5cdf08bc8 Updated SDK version 2025-05-14 12:31:07 +01:00
Frédéric Barthelet
bd6df4222f Fix schema on clientInformation() 2025-05-14 11:53:08 +01:00
Frédéric Barthelet
b209d98074 Add port sourcing from existing client information 2025-05-14 11:53:08 +01:00
Glen Maddern
bd75a1cdf0 0.1.4 2025-05-12 15:37:49 +10:00
Tomer Zait
767549412f fix issue #64 2025-05-12 06:37:44 +01:00
Glen Maddern
46e3333416 0.1.3 2025-05-12 15:27:57 +10:00
Glen Maddern
63e02eef1c Use 127.0.0.1 everywhere _except_ as a redirect_uri for the client registration 2025-05-12 06:27:47 +01:00
Glen Maddern
45c1739b4c Adding (via mcp-remote <version>) to clientInfo.name on initialize 2025-05-12 06:27:37 +01:00
Glen Maddern
5c71b26869 Added a 2 second delay before closing the browser 2025-05-05 23:54:56 +01:00
dp-rufus
b9105958c1 Attempt auto close 2025-05-05 23:54:56 +01:00
shaun smith
114ee3c4b6 Update README.md 2025-05-05 23:34:01 +01:00
Glen Maddern
c9e082d9e2 Removing traces of react 2025-05-05 23:01:46 +01:00
Glen Maddern
67bd63192f Publishing all commits to pkg.pr.new 2025-05-05 06:21:57 +01:00
Glen Maddern
c4a2d4a242 0.1.2 2025-05-05 12:40:27 +10:00
Fadojutimi Temitayo Olusegun
026caedd3c fix: changed the header argument processing from a forEach loop to a while loop to handle array modifications correctly, preventing index errors. 2025-05-05 03:38:07 +01:00
Glen Maddern
da1330d2aa 0.1.1 2025-05-02 11:38:28 +10:00
shaun smith
15f9c944f6 Update README.md 2025-05-02 02:26:18 +01:00
Glen Maddern
2b2b12decd Treat 404s and 405s as the same regardless of starting with SSE or HTTPs transport
Fixes #47 #48
2025-05-02 02:25:50 +01:00
Glen Maddern
5a38b58f63 0.1.0 2025-04-30 12:58:02 +01:00
Glen Maddern
04e3d255b1 Added Streamable HTTP support
This adds a new CLI argument, --transport, with the following values: http-first (the default), http-only, sse-first, and sse-only. Any of the -first tags attempts to connect to the URL as either an HTTP or SSE server and falls back to the other.
2025-04-30 12:58:02 +01:00
Ola Hungerford
504aa26761 Update link to latest auth spec in README.md 2025-04-17 09:13:11 +01:00
Glen Maddern
b69fdc8ebe 0.0.22 2025-04-16 15:59:57 +10:00
Rune Botten
bb4c03f069 chore: update @modelcontextprotocol/sdk to 1.9.0 to include fix for missing redirect_uri in token exchange 2025-04-16 06:59:49 +01:00
10 changed files with 525 additions and 297 deletions

33
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Publish Any Commit
on:
workflow_dispatch:
pull_request:
push:
branches:
- "**"
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Add git.kvant.cloud scope
run: npm config set @kvant:registry=https://git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/
- name: Login to git.kvant.cloud npm
run: npm config set -- '//git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/:_authToken' "${{ secrets.PHOENIX_PACKAGE_WRITER_TOKEN }}"
- name: Setup pnpm & install
uses: https://github.com/wyvox/action-setup-pnpm@v3
with:
node-version: 22
- name: Build
run: pnpm build
- run: pnpm dlx publish --compact --bin

View file

@ -10,7 +10,7 @@ So far, the majority of MCP servers in the wild are installed locally, using the
But there's a reason most software that _could_ be moved to the web _did_ get moved to the web: it's so much easier to find and fix bugs & iterate on new features when you can push updates to all your users with a single deploy. But there's a reason most software that _could_ be moved to the web _did_ get moved to the web: it's so much easier to find and fix bugs & iterate on new features when you can push updates to all your users with a single deploy.
With the MCP [Authorization specification](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/) nearing completion, we now have a secure way of sharing our MCP servers with the world _without_ running code on user's laptops. Or at least, you would, if all the popular MCP _clients_ supported it yet. Most are stdio-only, and those that _do_ support HTTP+SSE don't yet support the OAuth flows required. With the latest MCP [Authorization specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), we now have a secure way of sharing our MCP servers with the world _without_ running code on user's laptops. Or at least, you would, if all the popular MCP _clients_ supported it yet. Most are stdio-only, and those that _do_ support HTTP+SSE don't yet support the OAuth flows required.
That's where `mcp-remote` comes in. As soon as your chosen MCP client supports remote, authorized servers, you can remove it. Until that time, drop in this one liner and dress for the MCP clients you want! That's where `mcp-remote` comes in. As soon as your chosen MCP client supports remote, authorized servers, you can remove it. Until that time, drop in this one liner and dress for the MCP clients you want!
@ -46,16 +46,16 @@ To bypass authentication, or to emit custom headers on all requests to your remo
"https://remote.mcp.server/sse", "https://remote.mcp.server/sse",
"--header", "--header",
"Authorization: Bearer ${AUTH_TOKEN}" "Authorization: Bearer ${AUTH_TOKEN}"
] ],
},
"env": { "env": {
"AUTH_TOKEN": "..." "AUTH_TOKEN": "..."
} }
},
} }
} }
``` ```
**Note:** Cursor has a bug where spaces inside `args` aren't escaped when it invokes `npx`, which ends up mangling these values. You can work around it using: **Note:** Cursor and Claude Desktop (Windows) have a bug where spaces inside `args` aren't escaped when it invokes `npx`, which ends up mangling these values. You can work around it using:
```jsonc ```jsonc
{ {
@ -65,11 +65,11 @@ To bypass authentication, or to emit custom headers on all requests to your remo
"https://remote.mcp.server/sse", "https://remote.mcp.server/sse",
"--header", "--header",
"Authorization:${AUTH_HEADER}" // note no spaces around ':' "Authorization:${AUTH_HEADER}" // note no spaces around ':'
] ],
}, "env": {
"env": {
"AUTH_HEADER": "Bearer <auth-token>" // spaces OK in env vars "AUTH_HEADER": "Bearer <auth-token>" // spaces OK in env vars
} }
},
``` ```
### Flags ### Flags
@ -114,6 +114,23 @@ To bypass authentication, or to emit custom headers on all requests to your remo
] ]
``` ```
### Transport Strategies
MCP Remote supports different transport strategies when connecting to an MCP server. This allows you to control whether it uses Server-Sent Events (SSE) or HTTP transport, and in what order it tries them.
Specify the transport strategy with the `--transport` flag:
```bash
npx mcp-remote https://example.remote/server --transport sse-only
```
**Available Strategies:**
- `http-first` (default): Tries HTTP transport first, falls back to SSE if HTTP fails with a 404 error
- `sse-first`: Tries SSE transport first, falls back to HTTP if SSE fails with a 405 error
- `http-only`: Only uses HTTP transport, fails if the server doesn't support it
- `sse-only`: Only uses SSE transport, fails if the server doesn't support it
### Claude Desktop ### Claude Desktop
[Official Docs](https://modelcontextprotocol.io/quickstart/user) [Official Docs](https://modelcontextprotocol.io/quickstart/user)

View file

@ -1,6 +1,6 @@
{ {
"name": "mcp-remote", "name": "@kvant/mcp-remote",
"version": "0.0.21", "version": "0.1.5",
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth", "description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
"keywords": [ "keywords": [
"mcp", "mcp",
@ -28,16 +28,15 @@
"check": "prettier --check . && tsc" "check": "prettier --check . && tsc"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"express": "^4.21.2", "express": "^4.21.2",
"open": "^10.1.0" "open": "^10.1.0"
}, },
"packageManager": "pnpm@10.11.0",
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.11.2",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/react": "^19.0.12",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^19.0.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.2" "typescript": "^5.8.2"
@ -53,8 +52,6 @@
"dts": true, "dts": true,
"clean": true, "clean": true,
"outDir": "dist", "outDir": "dist",
"external": [ "external": []
"react"
]
} }
} }

191
pnpm-lock.yaml generated
View file

@ -8,9 +8,6 @@ importers:
.: .:
dependencies: dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.7.0
version: 1.7.0
express: express:
specifier: ^4.21.2 specifier: ^4.21.2
version: 4.21.2 version: 4.21.2
@ -18,21 +15,18 @@ importers:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
devDependencies: devDependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.11.2
version: 1.11.2
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0 version: 5.0.0
'@types/node': '@types/node':
specifier: ^22.13.10 specifier: ^22.13.10
version: 22.13.10 version: 22.13.10
'@types/react':
specifier: ^19.0.12
version: 19.0.12
prettier: prettier:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
react:
specifier: ^19.0.0
version: 19.0.0
tsup: tsup:
specifier: ^8.4.0 specifier: ^8.4.0
version: 8.4.0(tsx@4.19.3)(typescript@5.8.2) version: 8.4.0(tsx@4.19.3)(typescript@5.8.2)
@ -217,8 +211,8 @@ packages:
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@modelcontextprotocol/sdk@1.7.0': '@modelcontextprotocol/sdk@1.11.2':
resolution: {integrity: sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==} resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
@ -350,9 +344,6 @@ packages:
'@types/range-parser@1.2.7': '@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/react@19.0.12':
resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==}
'@types/send@0.17.4': '@types/send@0.17.4':
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
@ -396,8 +387,8 @@ packages:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
body-parser@2.1.0: body-parser@2.2.0:
resolution: {integrity: sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==} resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'} engines: {node: '>=18'}
brace-expansion@2.0.1: brace-expansion@2.0.1:
@ -479,9 +470,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
debug@2.6.9: debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@ -490,15 +478,6 @@ packages:
supports-color: supports-color:
optional: true 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: debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -576,12 +555,12 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
eventsource-parser@3.0.0: eventsource-parser@3.0.1:
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
eventsource@3.0.5: eventsource@3.0.6:
resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
express-rate-limit@7.5.0: express-rate-limit@7.5.0:
@ -594,8 +573,8 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
express@5.0.1: express@5.1.0:
resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
fdir@6.4.3: fdir@6.4.3:
@ -673,10 +652,6 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'} 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: iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -771,8 +746,8 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@3.0.0: mime-types@3.0.1:
resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime@1.6.0: mime@1.6.0:
@ -791,9 +766,6 @@ packages:
ms@2.0.0: ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -860,8 +832,8 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
pkce-challenge@4.1.0: pkce-challenge@5.0.0:
resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==} resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==}
engines: {node: '>=16.20.0'} engines: {node: '>=16.20.0'}
postcss-load-config@6.0.1: postcss-load-config@6.0.1:
@ -915,10 +887,6 @@ packages:
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
readdirp@4.1.2: readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
@ -935,8 +903,8 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
router@2.1.0: router@2.2.0:
resolution: {integrity: sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==} resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
run-applescript@7.0.0: run-applescript@7.0.0:
@ -953,16 +921,16 @@ packages:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
send@1.1.0: send@1.2.0:
resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
serve-static@1.16.2: serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
serve-static@2.1.0: serve-static@2.2.0:
resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
setprototypeof@1.2.0: setprototypeof@1.2.0:
@ -1081,8 +1049,8 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
type-is@2.0.0: type-is@2.0.1:
resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==} resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
typescript@5.8.2: typescript@5.8.2:
@ -1132,8 +1100,8 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.24.1 zod: ^3.24.1
zod@3.24.2: zod@3.24.3:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
snapshots: snapshots:
@ -1238,17 +1206,18 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@modelcontextprotocol/sdk@1.7.0': '@modelcontextprotocol/sdk@1.11.2':
dependencies: dependencies:
content-type: 1.0.5 content-type: 1.0.5
cors: 2.8.5 cors: 2.8.5
eventsource: 3.0.5 cross-spawn: 7.0.6
express: 5.0.1 eventsource: 3.0.6
express-rate-limit: 7.5.0(express@5.0.1) express: 5.1.0
pkce-challenge: 4.1.0 express-rate-limit: 7.5.0(express@5.1.0)
pkce-challenge: 5.0.0
raw-body: 3.0.0 raw-body: 3.0.0
zod: 3.24.2 zod: 3.24.3
zod-to-json-schema: 3.24.5(zod@3.24.2) zod-to-json-schema: 3.24.5(zod@3.24.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1349,10 +1318,6 @@ snapshots:
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
'@types/react@19.0.12':
dependencies:
csstype: 3.1.3
'@types/send@0.17.4': '@types/send@0.17.4':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
@ -1371,7 +1336,7 @@ snapshots:
accepts@2.0.0: accepts@2.0.0:
dependencies: dependencies:
mime-types: 3.0.0 mime-types: 3.0.1
negotiator: 1.0.0 negotiator: 1.0.0
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
@ -1407,17 +1372,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
body-parser@2.1.0: body-parser@2.2.0:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
content-type: 1.0.5 content-type: 1.0.5
debug: 4.4.0 debug: 4.4.0
http-errors: 2.0.0 http-errors: 2.0.0
iconv-lite: 0.5.2 iconv-lite: 0.6.3
on-finished: 2.4.1 on-finished: 2.4.1
qs: 6.14.0 qs: 6.14.0
raw-body: 3.0.0 raw-body: 3.0.0
type-is: 2.0.0 type-is: 2.0.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1489,16 +1454,10 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
csstype@3.1.3: {}
debug@2.6.9: debug@2.6.9:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0
debug@4.3.6:
dependencies:
ms: 2.1.2
debug@4.4.0: debug@4.4.0:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@ -1574,15 +1533,15 @@ snapshots:
etag@1.8.1: {} etag@1.8.1: {}
eventsource-parser@3.0.0: {} eventsource-parser@3.0.1: {}
eventsource@3.0.5: eventsource@3.0.6:
dependencies: 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: dependencies:
express: 5.0.1 express: 5.1.0
express@4.21.2: express@4.21.2:
dependencies: dependencies:
@ -1620,16 +1579,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
express@5.0.1: express@5.1.0:
dependencies: dependencies:
accepts: 2.0.0 accepts: 2.0.0
body-parser: 2.1.0 body-parser: 2.2.0
content-disposition: 1.0.0 content-disposition: 1.0.0
content-type: 1.0.5 content-type: 1.0.5
cookie: 0.7.1 cookie: 0.7.1
cookie-signature: 1.2.2 cookie-signature: 1.2.2
debug: 4.3.6 debug: 4.4.0
depd: 2.0.0
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
etag: 1.8.1 etag: 1.8.1
@ -1637,22 +1595,18 @@ snapshots:
fresh: 2.0.0 fresh: 2.0.0
http-errors: 2.0.0 http-errors: 2.0.0
merge-descriptors: 2.0.0 merge-descriptors: 2.0.0
methods: 1.1.2 mime-types: 3.0.1
mime-types: 3.0.0
on-finished: 2.4.1 on-finished: 2.4.1
once: 1.4.0 once: 1.4.0
parseurl: 1.3.3 parseurl: 1.3.3
proxy-addr: 2.0.7 proxy-addr: 2.0.7
qs: 6.13.0 qs: 6.14.0
range-parser: 1.2.1 range-parser: 1.2.1
router: 2.1.0 router: 2.2.0
safe-buffer: 5.2.1 send: 1.2.0
send: 1.1.0 serve-static: 2.2.0
serve-static: 2.1.0
setprototypeof: 1.2.0
statuses: 2.0.1 statuses: 2.0.1
type-is: 2.0.0 type-is: 2.0.1
utils-merge: 1.0.1
vary: 1.1.2 vary: 1.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1751,10 +1705,6 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
iconv-lite@0.5.2:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.6.3: iconv-lite@0.6.3:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@ -1817,7 +1767,7 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
mime-types@3.0.0: mime-types@3.0.1:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
@ -1831,8 +1781,6 @@ snapshots:
ms@2.0.0: {} ms@2.0.0: {}
ms@2.1.2: {}
ms@2.1.3: {} ms@2.1.3: {}
mz@2.7.0: mz@2.7.0:
@ -1885,7 +1833,7 @@ snapshots:
pirates@4.0.6: {} pirates@4.0.6: {}
pkce-challenge@4.1.0: {} pkce-challenge@5.0.0: {}
postcss-load-config@6.0.1(tsx@4.19.3): postcss-load-config@6.0.1(tsx@4.19.3):
dependencies: dependencies:
@ -1926,8 +1874,6 @@ snapshots:
iconv-lite: 0.6.3 iconv-lite: 0.6.3
unpipe: 1.0.0 unpipe: 1.0.0
react@19.0.0: {}
readdirp@4.1.2: {} readdirp@4.1.2: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@ -1959,11 +1905,15 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.35.0 '@rollup/rollup-win32-x64-msvc': 4.35.0
fsevents: 2.3.3 fsevents: 2.3.3
router@2.1.0: router@2.2.0:
dependencies: dependencies:
debug: 4.4.0
depd: 2.0.0
is-promise: 4.0.0 is-promise: 4.0.0
parseurl: 1.3.3 parseurl: 1.3.3
path-to-regexp: 8.2.0 path-to-regexp: 8.2.0
transitivePeerDependencies:
- supports-color
run-applescript@7.0.0: {} run-applescript@7.0.0: {}
@ -1989,16 +1939,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
send@1.1.0: send@1.2.0:
dependencies: dependencies:
debug: 4.4.0 debug: 4.4.0
destroy: 1.2.0
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
etag: 1.8.1 etag: 1.8.1
fresh: 0.5.2 fresh: 2.0.0
http-errors: 2.0.0 http-errors: 2.0.0
mime-types: 2.1.35 mime-types: 3.0.1
ms: 2.1.3 ms: 2.1.3
on-finished: 2.4.1 on-finished: 2.4.1
range-parser: 1.2.1 range-parser: 1.2.1
@ -2015,12 +1964,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
serve-static@2.1.0: serve-static@2.2.0:
dependencies: dependencies:
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
parseurl: 1.3.3 parseurl: 1.3.3
send: 1.1.0 send: 1.2.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2161,11 +2110,11 @@ snapshots:
media-typer: 0.3.0 media-typer: 0.3.0
mime-types: 2.1.35 mime-types: 2.1.35
type-is@2.0.0: type-is@2.0.1:
dependencies: dependencies:
content-type: 1.0.5 content-type: 1.0.5
media-typer: 1.1.0 media-typer: 1.1.0
mime-types: 3.0.0 mime-types: 3.0.1
typescript@5.8.2: {} typescript@5.8.2: {}
@ -2203,8 +2152,8 @@ snapshots:
wrappy@1.0.2: {} 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: dependencies:
zod: 3.24.2 zod: 3.24.3
zod@3.24.2: {} zod@3.24.3: {}

View file

@ -11,25 +11,36 @@
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { Client } from '@modelcontextprotocol/sdk/client/index.js' 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 { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
import { parseCommandLineArgs, setupSignalHandlers, log, MCP_REMOTE_VERSION, getServerUrlHash } from './lib/utils' import {
import { coordinateAuth } from './lib/coordination' parseCommandLineArgs,
setupSignalHandlers,
log,
MCP_REMOTE_VERSION,
getServerUrlHash,
connectToRemoteServer,
TransportStrategy,
} from './lib/utils'
import { createLazyAuthCoordinator } from './lib/coordination'
/** /**
* Main function to run the client * 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 // Set up event emitter for auth flow
const events = new EventEmitter() const events = new EventEmitter()
// Get the server URL hash for lockfile operations // Get the server URL hash for lockfile operations
const serverUrlHash = getServerUrlHash(serverUrl) const serverUrlHash = getServerUrlHash(serverUrl)
// Coordinate authentication with other instances // Create a lazy auth coordinator
const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events) const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events)
// Create the OAuth client provider // Create the OAuth client provider
const authProvider = new NodeOAuthClientProvider({ const authProvider = new NodeOAuthClientProvider({
@ -38,14 +49,6 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
clientName: 'MCP CLI Client', 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 // Create the client
const client = new Client( const client = new Client(
{ {
@ -57,10 +60,33 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
}, },
) )
// Create the transport factory // Keep track of the server instance for cleanup
const url = new URL(serverUrl) let server: any = null
function initTransport() {
const transport = new SSEClientTransport(url, { authProvider, requestInit: { headers } }) // 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 // Set up message and error handlers
transport.onmessage = (message) => { transport.onmessage = (message) => {
@ -75,63 +101,19 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
log('Connection closed.') log('Connection closed.')
process.exit(0) process.exit(0)
} }
return transport
}
const transport = initTransport()
// Set up cleanup handler // Set up cleanup handler
const cleanup = async () => { const cleanup = async () => {
log('\nClosing connection...') log('\nClosing connection...')
await client.close() await client.close()
// If auth was initialized and server was created, close it
if (server) {
server.close() server.close()
} }
}
setupSignalHandlers(cleanup) setupSignalHandlers(cleanup)
// Try to connect
try {
log('Connecting to server...')
await client.connect(transport)
log('Connected successfully!') 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))
// 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))
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)
server.close()
process.exit(1)
}
}
try { try {
// Request tools list // Request tools list
@ -151,13 +133,27 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
log('Error requesting resources list:', e) log('Error requesting resources list:', e)
} }
log('Listening for messages. Press Ctrl+C to exit.') // 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(0)
} catch (error) {
log('Fatal error:', error)
// Only close the server if it was initialized
if (server) {
server.close()
}
process.exit(1)
}
} }
// Parse command-line arguments and run the client // Parse command-line arguments and run the client
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]') parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, headers }) => { .then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
return runClient(serverUrl, callbackPort, headers) return runClient(serverUrl, callbackPort, headers, transportStrategy)
}) })
.catch((error) => { .catch((error) => {
console.error('Fatal error:', error) console.error('Fatal error:', error)

View file

@ -5,6 +5,10 @@ import express from 'express'
import { AddressInfo } from 'net' import { AddressInfo } from 'net'
import { log, setupOAuthCallbackServerWithLongPoll } from './utils' 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 * Checks if a process with the given PID is running
* @param pid The process ID to check * @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 * Coordinates authentication between multiple instances of the client/proxy
* @param serverUrlHash The hash of the server URL * @param serverUrlHash The hash of the server URL

View file

@ -1,9 +1,8 @@
import open from 'open' import open from 'open'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
import { import {
OAuthClientInformation,
OAuthClientInformationFull, OAuthClientInformationFull,
OAuthClientInformationSchema, OAuthClientInformationFullSchema,
OAuthTokens, OAuthTokens,
OAuthTokensSchema, OAuthTokensSchema,
} from '@modelcontextprotocol/sdk/shared/auth.js' } from '@modelcontextprotocol/sdk/shared/auth.js'
@ -37,7 +36,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
} }
get redirectUrl(): string { get redirectUrl(): string {
return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}` return `http://localhost:${this.options.callbackPort}${this.callbackPath}`
} }
get clientMetadata() { get clientMetadata() {
@ -57,9 +56,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
* Gets the client information if it exists * Gets the client information if it exists
* @returns The client information or undefined * @returns The client information or undefined
*/ */
async clientInformation(): Promise<OAuthClientInformation | undefined> { async clientInformation(): Promise<OAuthClientInformationFull | undefined> {
// log('Reading client info') // log('Reading client info')
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema) return readJsonFile<OAuthClientInformationFull>(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
} }
/** /**

View file

@ -1,10 +1,22 @@
import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' 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 { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js'
import { OAuthCallbackServerOptions } from './types' import { OAuthCallbackServerOptions } from './types'
import { getConfigFilePath, readJsonFile } from './mcp-auth-config'
import express from 'express' import express from 'express'
import net from 'net' import net from 'net'
import crypto from 'crypto' import crypto from 'crypto'
import fs from 'fs/promises'
// 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'
// Package version from package.json // Package version from package.json
export const MCP_REMOTE_VERSION = require('../../package.json').version export const MCP_REMOTE_VERSION = require('../../package.json').version
@ -23,14 +35,21 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
let transportToClientClosed = false let transportToClientClosed = false
let transportToServerClosed = false let transportToServerClosed = false
transportToClient.onmessage = (message) => { transportToClient.onmessage = (_message) => {
// @ts-expect-error TODO // TODO: fix types
const message = _message as any
log('[Local→Remote]', message.method || message.id) log('[Local→Remote]', message.method || message.id)
if (message.method === 'initialize') {
const { clientInfo } = message.params
if (clientInfo) clientInfo.name = `${clientInfo.name} (via mcp-remote ${MCP_REMOTE_VERSION})`
log(JSON.stringify(message, null, 2))
}
transportToServer.send(message).catch(onServerError) transportToServer.send(message).catch(onServerError)
} }
transportToServer.onmessage = (message) => { transportToServer.onmessage = (_message) => {
// @ts-expect-error TODO: fix this type // TODO: fix types
const message = _message as any
log('[Remote→Local]', message.method || message.id) log('[Remote→Local]', message.method || message.id)
transportToClient.send(message).catch(onClientError) transportToClient.send(message).catch(onClientError)
} }
@ -65,21 +84,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 serverUrl The URL of the remote server
* @param authProvider The OAuth client provider * @param authProvider The OAuth client provider
* @param headers Additional headers to send with the request * @param headers Additional headers to send with the request
* @param waitForAuthCode Function to wait for the auth code * @param authInitializer Function to initialize authentication when needed
* @param skipBrowserAuth Whether to skip browser auth and use shared auth * @param transportStrategy Strategy for selecting transport type ('sse-only', 'http-only', 'sse-first', 'http-first')
* @returns The connected SSE client transport * @param recursionReasons Set of reasons for recursive calls (internal use)
* @returns The connected transport
*/ */
export async function connectToRemoteServer( export async function connectToRemoteServer(
client: Client | null,
serverUrl: string, serverUrl: string,
authProvider: OAuthClientProvider, authProvider: OAuthClientProvider,
headers: Record<string, string>, headers: Record<string, string>,
waitForAuthCode: () => Promise<string>, authInitializer: AuthInitializer,
skipBrowserAuth: boolean = false, transportStrategy: TransportStrategy = 'http-first',
): Promise<SSEClientTransport> { recursionReasons: Set<string> = new Set(),
): Promise<Transport> {
log(`[${pid}] Connecting to remote server: ${serverUrl}`) log(`[${pid}] Connecting to remote server: ${serverUrl}`)
const url = new URL(serverUrl) const url = new URL(serverUrl)
@ -93,25 +124,89 @@ export async function connectToRemoteServer(
...(init?.headers as Record<string, string> | undefined), ...(init?.headers as Record<string, string> | undefined),
...headers, ...headers,
...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}), ...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}),
Accept: "text/event-stream", Accept: 'text/event-stream',
} as Record<string, string>, } as Record<string, string>,
}) }),
); )
}, },
}; }
const transport = new SSEClientTransport(url, { 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, authProvider,
requestInit: { headers }, requestInit: { headers },
eventSourceInit, eventSourceInit,
}) })
: new StreamableHTTPClientTransport(url, {
authProvider,
requestInit: { headers },
})
try { try {
if (client) {
await client.connect(transport)
} else {
await transport.start() await transport.start()
log('Connected to remote server') 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 return transport
} catch (error) { } 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 &&
(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) { if (skipBrowserAuth) {
log('Authentication required but skipping browser auth - using shared auth') log('Authentication required but skipping browser auth - using shared auth')
} else { } else {
@ -125,11 +220,18 @@ export async function connectToRemoteServer(
log('Completing authorization...') log('Completing authorization...')
await transport.finishAuth(code) await transport.finishAuth(code)
// Create a new transport after auth if (recursionReasons.has(REASON_AUTH_NEEDED)) {
const newTransport = new SSEClientTransport(url, { authProvider, requestInit: { headers } }) const errorMessage = `Already attempted reconnection for reason: ${REASON_AUTH_NEEDED}. Giving up.`
await newTransport.start() log(errorMessage)
log('Connected to remote server after authentication') throw new Error(errorMessage)
return newTransport }
// 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) { } catch (authError) {
log('Authorization error:', authError) log('Authorization error:', authError)
throw authError throw authError
@ -208,7 +310,16 @@ export function setupOAuthCallbackServerWithLongPoll(options: OAuthCallbackServe
log('Auth code received, resolving promise') log('Auth code received, resolving promise')
authCompletedResolve(code) authCompletedResolve(code)
res.send('Authorization successful! You may close this window and return to the CLI.') res.send(`
Authorization successful!
You may close this window and return to the CLI.
<script>
// If this is a non-interactive session (no manual approval step was required) then
// this should automatically close the window. If not, this will have no effect and
// the user will see the message above.
window.close();
</script>
`)
// Notify main flow that auth code is available // Notify main flow that auth code is available
options.events.emit('auth-code-received', code) options.events.emit('auth-code-received', code)
@ -244,6 +355,27 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
return { server, authCode, waitForAuthCode } return { server, authCode, waitForAuthCode }
} }
async function findExistingClientPort(serverUrlHash: string): Promise<number | undefined> {
const clientInfo = await readJsonFile<OAuthClientInformationFull>(serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
if (!clientInfo) {
return undefined
}
const localhostRedirectUri = clientInfo.redirect_uris.map((uri) => new URL(uri)).find(({ hostname }) => hostname === 'localhost')
if (!localhostRedirectUri) {
throw new Error('Cannot find localhost callback URI from existing client information')
}
return parseInt(localhostRedirectUri.port)
}
function calculateDefaultPort(serverUrlHash: string): number {
// Convert the first 4 bytes of the serverUrlHash into a port offset
const offset = parseInt(serverUrlHash.substring(0, 4), 16)
// Pick a consistent but random-seeming port from 3335 to 49151
return 3335 + (offset % 45816)
}
/** /**
* Finds an available port on the local machine * Finds an available port on the local machine
* @param preferredPort Optional preferred port to try first * @param preferredPort Optional preferred port to try first
@ -277,15 +409,15 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
/** /**
* Parses command line arguments for MCP clients and proxies * Parses command line arguments for MCP clients and proxies
* @param args Command line arguments * @param args Command line arguments
* @param defaultPort Default port for the callback server if specified port is unavailable
* @param usage Usage message to show on error * @param usage Usage message to show on error
* @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers * @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers
*/ */
export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { export async function parseCommandLineArgs(args: string[], usage: string) {
// Process headers // Process headers
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
args.forEach((arg, i) => { let i = 0
if (arg === '--header' && i < args.length - 1) { while (i < args.length) {
if (args[i] === '--header' && i < args.length - 1) {
const value = args[i + 1] const value = args[i + 1]
const match = value.match(/^([A-Za-z0-9_-]+):(.*)$/) const match = value.match(/^([A-Za-z0-9_-]+):(.*)$/)
if (match) { if (match) {
@ -294,13 +426,29 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
log(`Warning: ignoring invalid header argument: ${value}`) log(`Warning: ignoring invalid header argument: ${value}`)
} }
args.splice(i, 2) args.splice(i, 2)
// Do not increment i, as the array has shifted
continue
}
i++
} }
})
const serverUrl = args[0] const serverUrl = args[0]
const specifiedPort = args[1] ? parseInt(args[1]) : undefined const specifiedPort = args[1] ? parseInt(args[1]) : undefined
const allowHttp = args.includes('--allow-http') 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) { if (!serverUrl) {
log(usage) log(usage)
process.exit(1) process.exit(1)
@ -314,14 +462,28 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
log(usage) log(usage)
process.exit(1) process.exit(1)
} }
const serverUrlHash = getServerUrlHash(serverUrl)
const defaultPort = calculateDefaultPort(serverUrlHash)
// Use the specified port, or find an available one // Use the specified port, or the existing client port or fallback to find an available one
const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrlHash), findAvailablePort(defaultPort)])
let callbackPort: number
if (specifiedPort) { if (specifiedPort) {
log(`Using specified callback port: ${callbackPort}`) if (existingClientPort && specifiedPort !== existingClientPort) {
log(
`Warning! Specified callback port of ${specifiedPort}, which conflicts with existing client registration port ${existingClientPort}. Deleting existing client data to force reregistration.`,
)
await fs.rm(getConfigFilePath(serverUrlHash, 'client_info.json'))
}
log(`Using specified callback port: ${specifiedPort}`)
callbackPort = specifiedPort
} else if (existingClientPort) {
log(`Using existing client port: ${existingClientPort}`)
callbackPort = existingClientPort
} else { } else {
log(`Using automatically selected callback port: ${callbackPort}`) log(`Using automatically selected callback port: ${availablePort}`)
callbackPort = availablePort
} }
if (Object.keys(headers).length > 0) { if (Object.keys(headers).length > 0) {
@ -343,7 +505,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
}) })
} }
return { serverUrl, callbackPort, headers } return { serverUrl, callbackPort, headers, transportStrategy }
} }
/** /**
@ -359,6 +521,11 @@ export function setupSignalHandlers(cleanup: () => Promise<void>) {
// Keep the process alive // Keep the process alive
process.stdin.resume() process.stdin.resume()
process.stdin.on('end', async () => {
log('\nShutting down...')
await cleanup()
process.exit(0)
})
} }
/** /**

View file

@ -11,22 +11,36 @@
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' 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 { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
import { coordinateAuth } from './lib/coordination' import { createLazyAuthCoordinator } from './lib/coordination'
/** /**
* Main function to run the proxy * 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 // Set up event emitter for auth flow
const events = new EventEmitter() const events = new EventEmitter()
// Get the server URL hash for lockfile operations // Get the server URL hash for lockfile operations
const serverUrlHash = getServerUrlHash(serverUrl) const serverUrlHash = getServerUrlHash(serverUrl)
// Coordinate authentication with other instances // Create a lazy auth coordinator
const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events) const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events)
// Create the OAuth client provider // Create the OAuth client provider
const authProvider = new NodeOAuthClientProvider({ const authProvider = new NodeOAuthClientProvider({
@ -35,20 +49,36 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
clientName: 'MCP CLI Proxy', clientName: 'MCP CLI Proxy',
}) })
// 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 auth was completed by another instance, just log that we'll use the auth from disk
if (skipBrowserAuth) { if (authState.skipBrowserAuth) {
log('Authentication was completed by another instance - will use tokens from disk') log('Authentication was completed by another instance - will use tokens from disk')
// TODO: remove, the callback is happening before the tokens are exchanged // TODO: remove, the callback is happening before the tokens are exchanged
// so we're slightly too early // so we're slightly too early
await new Promise((res) => setTimeout(res, 1_000)) await new Promise((res) => setTimeout(res, 1_000))
} }
// Create the STDIO transport for local connections return {
const localTransport = new StdioServerTransport() waitForAuthCode: authState.waitForAuthCode,
skipBrowserAuth: authState.skipBrowserAuth,
}
}
try { try {
// Connect to remote server with authentication // Connect to remote server with lazy authentication
const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth) const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy)
// Set up bidirectional proxy between local and remote transports // Set up bidirectional proxy between local and remote transports
mcpProxy({ mcpProxy({
@ -59,15 +89,18 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
// Start the local STDIO server // Start the local STDIO server
await localTransport.start() await localTransport.start()
log('Local STDIO server running') 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') log('Press Ctrl+C to exit')
// Setup cleanup handler // Setup cleanup handler
const cleanup = async () => { const cleanup = async () => {
await remoteTransport.close() await remoteTransport.close()
await localTransport.close() await localTransport.close()
// Only close the server if it was initialized
if (server) {
server.close() server.close()
} }
}
setupSignalHandlers(cleanup) setupSignalHandlers(cleanup)
} catch (error) { } catch (error) {
log('Fatal error:', error) log('Fatal error:', error)
@ -93,15 +126,18 @@ to the CA certificate file. If using claude_desktop_config.json, this might look
} }
`) `)
} }
// Only close the server if it was initialized
if (server) {
server.close() server.close()
}
process.exit(1) process.exit(1)
} }
} }
// Parse command-line arguments and run the proxy // Parse command-line arguments and run the proxy
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]') parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, headers }) => { .then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
return runProxy(serverUrl, callbackPort, headers) return runProxy(serverUrl, callbackPort, headers, transportStrategy)
}) })
.catch((error) => { .catch((error) => {
log('Fatal error:', error) log('Fatal error:', error)

View file

@ -7,7 +7,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"noEmit": true, "noEmit": true,
"lib": ["ES2022", "DOM"], "lib": ["ES2022", "DOM"],
"types": ["node", "react"], "types": ["node"],
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true "resolveJsonModule": true
} }