up-mcp-bridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/dist/chunk-EOYXIWZ7.js +21532 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +140 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +246 -0
- package/package.json +70 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
ListResourcesResultSchema,
|
|
5
|
+
ListToolsResultSchema,
|
|
6
|
+
NodeOAuthClientProvider,
|
|
7
|
+
connectToRemoteServer,
|
|
8
|
+
createLazyAuthCoordinator,
|
|
9
|
+
debugLog,
|
|
10
|
+
fetchAuthorizationServerMetadata,
|
|
11
|
+
log,
|
|
12
|
+
parseCommandLineArgs,
|
|
13
|
+
setupSignalHandlers,
|
|
14
|
+
version
|
|
15
|
+
} from "./chunk-EOYXIWZ7.js";
|
|
16
|
+
|
|
17
|
+
// src/client.ts
|
|
18
|
+
import { EventEmitter } from "events";
|
|
19
|
+
async function runClient(serverUrl, callbackPort, headers, transportStrategy = "http-first", host, staticOAuthClientMetadata, staticOAuthClientInfo, authTimeoutMs, serverUrlHash) {
|
|
20
|
+
const events = new EventEmitter();
|
|
21
|
+
const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events, authTimeoutMs);
|
|
22
|
+
let authorizationServerMetadata;
|
|
23
|
+
try {
|
|
24
|
+
authorizationServerMetadata = await fetchAuthorizationServerMetadata(serverUrl);
|
|
25
|
+
if (authorizationServerMetadata?.scopes_supported) {
|
|
26
|
+
debugLog("Pre-fetched authorization server metadata", {
|
|
27
|
+
scopes_supported: authorizationServerMetadata.scopes_supported
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
debugLog("Failed to pre-fetch authorization server metadata", error);
|
|
32
|
+
}
|
|
33
|
+
const authProvider = new NodeOAuthClientProvider({
|
|
34
|
+
serverUrl,
|
|
35
|
+
callbackPort,
|
|
36
|
+
host,
|
|
37
|
+
clientName: "MCP CLI Client",
|
|
38
|
+
staticOAuthClientMetadata,
|
|
39
|
+
staticOAuthClientInfo,
|
|
40
|
+
serverUrlHash,
|
|
41
|
+
authorizationServerMetadata
|
|
42
|
+
});
|
|
43
|
+
const client = new Client(
|
|
44
|
+
{
|
|
45
|
+
name: "mcp-remote",
|
|
46
|
+
version
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
capabilities: {}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
let server = null;
|
|
53
|
+
const authInitializer = async () => {
|
|
54
|
+
const authState = await authCoordinator.initializeAuth();
|
|
55
|
+
server = authState.server;
|
|
56
|
+
if (authState.skipBrowserAuth) {
|
|
57
|
+
log("Authentication was completed by another instance - will use tokens from disk...");
|
|
58
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
waitForAuthCode: authState.waitForAuthCode,
|
|
62
|
+
skipBrowserAuth: authState.skipBrowserAuth
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const transport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy);
|
|
67
|
+
transport.onmessage = (message) => {
|
|
68
|
+
log("Received message:", JSON.stringify(message, null, 2));
|
|
69
|
+
};
|
|
70
|
+
transport.onerror = (error) => {
|
|
71
|
+
log("Transport error:", error);
|
|
72
|
+
};
|
|
73
|
+
transport.onclose = () => {
|
|
74
|
+
log("Connection closed.");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
const cleanup = async () => {
|
|
78
|
+
log("\nClosing connection...");
|
|
79
|
+
await client.close();
|
|
80
|
+
if (server) {
|
|
81
|
+
server.close();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
setupSignalHandlers(cleanup);
|
|
85
|
+
log("Connected successfully!");
|
|
86
|
+
try {
|
|
87
|
+
log("Requesting tools list...");
|
|
88
|
+
const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema);
|
|
89
|
+
log("Tools:", JSON.stringify(tools, null, 2));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
log("Error requesting tools list:", e);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
log("Requesting resource list...");
|
|
95
|
+
const resources = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
96
|
+
log("Resources:", JSON.stringify(resources, null, 2));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
log("Error requesting resources list:", e);
|
|
99
|
+
}
|
|
100
|
+
log("Exiting OK...");
|
|
101
|
+
if (server) {
|
|
102
|
+
server.close();
|
|
103
|
+
}
|
|
104
|
+
process.exit(0);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
log("Fatal error:", error);
|
|
107
|
+
if (server) {
|
|
108
|
+
server.close();
|
|
109
|
+
}
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
parseCommandLineArgs(process.argv.slice(2), "Usage: npx tsx client.ts <https://server-url> [callback-port] [--debug]").then(
|
|
114
|
+
({
|
|
115
|
+
serverUrl,
|
|
116
|
+
callbackPort,
|
|
117
|
+
headers,
|
|
118
|
+
transportStrategy,
|
|
119
|
+
host,
|
|
120
|
+
staticOAuthClientMetadata,
|
|
121
|
+
staticOAuthClientInfo,
|
|
122
|
+
authTimeoutMs,
|
|
123
|
+
serverUrlHash
|
|
124
|
+
}) => {
|
|
125
|
+
return runClient(
|
|
126
|
+
serverUrl,
|
|
127
|
+
callbackPort,
|
|
128
|
+
headers,
|
|
129
|
+
transportStrategy,
|
|
130
|
+
host,
|
|
131
|
+
staticOAuthClientMetadata,
|
|
132
|
+
staticOAuthClientInfo,
|
|
133
|
+
authTimeoutMs,
|
|
134
|
+
serverUrlHash
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
).catch((error) => {
|
|
138
|
+
console.error("Fatal error:", error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
JSONRPCMessageSchema,
|
|
4
|
+
NodeOAuthClientProvider,
|
|
5
|
+
connectToRemoteServer,
|
|
6
|
+
createLazyAuthCoordinator,
|
|
7
|
+
debugLog,
|
|
8
|
+
fetchAuthorizationServerMetadata,
|
|
9
|
+
log,
|
|
10
|
+
mcpProxy,
|
|
11
|
+
parseCommandLineArgs,
|
|
12
|
+
setupSignalHandlers
|
|
13
|
+
} from "./chunk-EOYXIWZ7.js";
|
|
14
|
+
|
|
15
|
+
// src/proxy.ts
|
|
16
|
+
import { EventEmitter } from "events";
|
|
17
|
+
|
|
18
|
+
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
19
|
+
import process2 from "process";
|
|
20
|
+
|
|
21
|
+
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
22
|
+
var ReadBuffer = class {
|
|
23
|
+
append(chunk) {
|
|
24
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
25
|
+
}
|
|
26
|
+
readMessage() {
|
|
27
|
+
if (!this._buffer) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const index = this._buffer.indexOf("\n");
|
|
31
|
+
if (index === -1) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
35
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
36
|
+
return deserializeMessage(line);
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this._buffer = void 0;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function deserializeMessage(line) {
|
|
43
|
+
return JSONRPCMessageSchema.parse(JSON.parse(line));
|
|
44
|
+
}
|
|
45
|
+
function serializeMessage(message) {
|
|
46
|
+
return JSON.stringify(message) + "\n";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
50
|
+
var StdioServerTransport = class {
|
|
51
|
+
constructor(_stdin = process2.stdin, _stdout = process2.stdout) {
|
|
52
|
+
this._stdin = _stdin;
|
|
53
|
+
this._stdout = _stdout;
|
|
54
|
+
this._readBuffer = new ReadBuffer();
|
|
55
|
+
this._started = false;
|
|
56
|
+
this._ondata = (chunk) => {
|
|
57
|
+
this._readBuffer.append(chunk);
|
|
58
|
+
this.processReadBuffer();
|
|
59
|
+
};
|
|
60
|
+
this._onerror = (error) => {
|
|
61
|
+
var _a;
|
|
62
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Starts listening for messages on stdin.
|
|
67
|
+
*/
|
|
68
|
+
async start() {
|
|
69
|
+
if (this._started) {
|
|
70
|
+
throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
|
|
71
|
+
}
|
|
72
|
+
this._started = true;
|
|
73
|
+
this._stdin.on("data", this._ondata);
|
|
74
|
+
this._stdin.on("error", this._onerror);
|
|
75
|
+
}
|
|
76
|
+
processReadBuffer() {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
while (true) {
|
|
79
|
+
try {
|
|
80
|
+
const message = this._readBuffer.readMessage();
|
|
81
|
+
if (message === null) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, message);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async close() {
|
|
91
|
+
var _a;
|
|
92
|
+
this._stdin.off("data", this._ondata);
|
|
93
|
+
this._stdin.off("error", this._onerror);
|
|
94
|
+
const remainingDataListeners = this._stdin.listenerCount("data");
|
|
95
|
+
if (remainingDataListeners === 0) {
|
|
96
|
+
this._stdin.pause();
|
|
97
|
+
}
|
|
98
|
+
this._readBuffer.clear();
|
|
99
|
+
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
100
|
+
}
|
|
101
|
+
send(message) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const json = serializeMessage(message);
|
|
104
|
+
if (this._stdout.write(json)) {
|
|
105
|
+
resolve();
|
|
106
|
+
} else {
|
|
107
|
+
this._stdout.once("drain", resolve);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/proxy.ts
|
|
114
|
+
async function runProxy(serverUrl, callbackPort, headers, transportStrategy = "http-first", host, staticOAuthClientMetadata, staticOAuthClientInfo, authorizeResource, ignoredTools, authTimeoutMs, serverUrlHash, reconnectOptions) {
|
|
115
|
+
const events = new EventEmitter();
|
|
116
|
+
const authCoordinator = createLazyAuthCoordinator(serverUrlHash, callbackPort, events, authTimeoutMs);
|
|
117
|
+
let authorizationServerMetadata;
|
|
118
|
+
try {
|
|
119
|
+
authorizationServerMetadata = await fetchAuthorizationServerMetadata(serverUrl);
|
|
120
|
+
if (authorizationServerMetadata?.scopes_supported) {
|
|
121
|
+
debugLog("Pre-fetched authorization server metadata", {
|
|
122
|
+
scopes_supported: authorizationServerMetadata.scopes_supported
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
debugLog("Failed to pre-fetch authorization server metadata", error);
|
|
127
|
+
}
|
|
128
|
+
const authProvider = new NodeOAuthClientProvider({
|
|
129
|
+
serverUrl,
|
|
130
|
+
callbackPort,
|
|
131
|
+
host,
|
|
132
|
+
clientName: "MCP CLI Proxy",
|
|
133
|
+
staticOAuthClientMetadata,
|
|
134
|
+
staticOAuthClientInfo,
|
|
135
|
+
authorizeResource,
|
|
136
|
+
serverUrlHash,
|
|
137
|
+
authorizationServerMetadata
|
|
138
|
+
});
|
|
139
|
+
const localTransport = new StdioServerTransport();
|
|
140
|
+
let server = null;
|
|
141
|
+
const authInitializer = async () => {
|
|
142
|
+
const authState = await authCoordinator.initializeAuth();
|
|
143
|
+
server = authState.server;
|
|
144
|
+
if (authState.skipBrowserAuth) {
|
|
145
|
+
log("Authentication was completed by another instance - will use tokens from disk");
|
|
146
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
waitForAuthCode: authState.waitForAuthCode,
|
|
150
|
+
skipBrowserAuth: authState.skipBrowserAuth
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy);
|
|
155
|
+
const reconnectFn = async () => {
|
|
156
|
+
log("Creating new connection to remote server...");
|
|
157
|
+
return connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy);
|
|
158
|
+
};
|
|
159
|
+
mcpProxy({
|
|
160
|
+
transportToClient: localTransport,
|
|
161
|
+
transportToServer: remoteTransport,
|
|
162
|
+
ignoredTools,
|
|
163
|
+
reconnectFn,
|
|
164
|
+
reconnectOptions,
|
|
165
|
+
serverUrl
|
|
166
|
+
});
|
|
167
|
+
await localTransport.start();
|
|
168
|
+
log("Local STDIO server running");
|
|
169
|
+
log(`Proxy established successfully between local STDIO and remote ${remoteTransport.constructor.name}`);
|
|
170
|
+
if (reconnectOptions.enabled) {
|
|
171
|
+
log(`Auto-reconnect enabled (max ${reconnectOptions.maxAttempts} attempts, base delay ${reconnectOptions.baseDelayMs}ms)`);
|
|
172
|
+
}
|
|
173
|
+
log("Press Ctrl+C to exit");
|
|
174
|
+
const cleanup = async () => {
|
|
175
|
+
await remoteTransport.close();
|
|
176
|
+
await localTransport.close();
|
|
177
|
+
if (server) {
|
|
178
|
+
server.close();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
setupSignalHandlers(cleanup);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
log("Fatal error:", error);
|
|
184
|
+
if (error instanceof Error && error.message.includes("self-signed certificate in certificate chain")) {
|
|
185
|
+
log(`You may be behind a VPN!
|
|
186
|
+
|
|
187
|
+
If you are behind a VPN, you can try setting the NODE_EXTRA_CA_CERTS environment variable to point
|
|
188
|
+
to the CA certificate file. If using claude_desktop_config.json, this might look like:
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
"mcpServers": {
|
|
192
|
+
"\${mcpServerName}": {
|
|
193
|
+
"command": "npx",
|
|
194
|
+
"args": [
|
|
195
|
+
"mcp-remote",
|
|
196
|
+
"https://remote.mcp.server/sse"
|
|
197
|
+
],
|
|
198
|
+
"env": {
|
|
199
|
+
"NODE_EXTRA_CA_CERTS": "\${your CA certificate file path}.pem"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
if (server) {
|
|
207
|
+
server.close();
|
|
208
|
+
}
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
parseCommandLineArgs(process.argv.slice(2), "Usage: npx tsx proxy.ts <https://server-url> [callback-port] [--debug]").then(
|
|
213
|
+
({
|
|
214
|
+
serverUrl,
|
|
215
|
+
callbackPort,
|
|
216
|
+
headers,
|
|
217
|
+
transportStrategy,
|
|
218
|
+
host,
|
|
219
|
+
debug,
|
|
220
|
+
staticOAuthClientMetadata,
|
|
221
|
+
staticOAuthClientInfo,
|
|
222
|
+
authorizeResource,
|
|
223
|
+
ignoredTools,
|
|
224
|
+
authTimeoutMs,
|
|
225
|
+
serverUrlHash,
|
|
226
|
+
reconnectOptions
|
|
227
|
+
}) => {
|
|
228
|
+
return runProxy(
|
|
229
|
+
serverUrl,
|
|
230
|
+
callbackPort,
|
|
231
|
+
headers,
|
|
232
|
+
transportStrategy,
|
|
233
|
+
host,
|
|
234
|
+
staticOAuthClientMetadata,
|
|
235
|
+
staticOAuthClientInfo,
|
|
236
|
+
authorizeResource,
|
|
237
|
+
ignoredTools,
|
|
238
|
+
authTimeoutMs,
|
|
239
|
+
serverUrlHash,
|
|
240
|
+
reconnectOptions
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
).catch((error) => {
|
|
244
|
+
log("Fatal error:", error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "up-mcp-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote proxy for MCP with auto-reconnect support. Fork of mcp-remote with transparent server restart handling.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"stdio",
|
|
8
|
+
"sse",
|
|
9
|
+
"remote",
|
|
10
|
+
"oauth",
|
|
11
|
+
"auto-reconnect",
|
|
12
|
+
"reconnection"
|
|
13
|
+
],
|
|
14
|
+
"author": "César Obach <cesar.obach@ultrabase.net>",
|
|
15
|
+
"contributors": [
|
|
16
|
+
"Glen Maddern <glen@glenmaddern.com>"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": "https://github.com/grupoultra/up-mcp-bridge",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"bin": {
|
|
28
|
+
"up-mcp-bridge": "dist/proxy.js",
|
|
29
|
+
"up-mcp-bridge-client": "dist/client.js"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"build:watch": "tsup --watch",
|
|
34
|
+
"check": "prettier --check . && tsc",
|
|
35
|
+
"lint-fix": "prettier --check . --write",
|
|
36
|
+
"test:unit": "vitest run",
|
|
37
|
+
"test:unit:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"express": "^4.21.2",
|
|
41
|
+
"open": "^10.1.0",
|
|
42
|
+
"strict-url-sanitise": "^0.0.1",
|
|
43
|
+
"undici": "^7.12.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
47
|
+
"zod": "^4.0.0",
|
|
48
|
+
"@types/express": "^5.0.0",
|
|
49
|
+
"@types/node": "^22.13.10",
|
|
50
|
+
"prettier": "^3.5.3",
|
|
51
|
+
"tsup": "^8.4.0",
|
|
52
|
+
"tsx": "^4.19.3",
|
|
53
|
+
"typescript": "^5.8.2",
|
|
54
|
+
"vitest": "^3.2.3"
|
|
55
|
+
},
|
|
56
|
+
"tsup": {
|
|
57
|
+
"entry": [
|
|
58
|
+
"src/client.ts",
|
|
59
|
+
"src/proxy.ts"
|
|
60
|
+
],
|
|
61
|
+
"format": [
|
|
62
|
+
"esm"
|
|
63
|
+
],
|
|
64
|
+
"dts": true,
|
|
65
|
+
"clean": true,
|
|
66
|
+
"outDir": "dist",
|
|
67
|
+
"external": []
|
|
68
|
+
},
|
|
69
|
+
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
|
70
|
+
}
|