@aiwerk/mcp-bridge 2.5.2 → 2.6.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/README.md +36 -2
- package/dist/bin/mcp-bridge.js +145 -0
- package/dist/src/cli-auth.d.ts +41 -0
- package/dist/src/cli-auth.js +300 -0
- package/dist/src/config.js +10 -2
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +5 -1
- package/dist/src/mcp-router.d.ts +11 -2
- package/dist/src/mcp-router.js +57 -31
- package/dist/src/oauth2-token-manager.d.ts +31 -1
- package/dist/src/oauth2-token-manager.js +171 -1
- package/dist/src/security.d.ts +4 -0
- package/dist/src/security.js +88 -1
- package/dist/src/standalone-server.js +4 -3
- package/dist/src/token-store.d.ts +30 -0
- package/dist/src/token-store.js +69 -0
- package/dist/src/transport-base.d.ts +15 -3
- package/dist/src/transport-base.js +67 -9
- package/dist/src/transport-sse.d.ts +2 -1
- package/dist/src/transport-sse.js +8 -3
- package/dist/src/transport-stdio.js +1 -1
- package/dist/src/transport-streamable-http.d.ts +2 -1
- package/dist/src/transport-streamable-http.js +47 -16
- package/dist/src/types.d.ts +16 -0
- package/package.json +2 -2
|
@@ -181,25 +181,83 @@ export function resolveAuthHeaders(config, extraEnv, envFallback) {
|
|
|
181
181
|
}
|
|
182
182
|
throw new Error("[mcp-bridge] OAuth2 auth requires async header resolution via resolveAuthHeadersAsync");
|
|
183
183
|
}
|
|
184
|
+
/** Check whether an oauth2 auth config uses the authorization_code grant type. */
|
|
185
|
+
export function isAuthCodeOAuth2(auth) {
|
|
186
|
+
return auth.grantType === "authorization_code";
|
|
187
|
+
}
|
|
188
|
+
/** Check whether an oauth2 auth config uses the device_code grant type. */
|
|
189
|
+
export function isDeviceCodeOAuth2(auth) {
|
|
190
|
+
return auth.grantType === "device_code";
|
|
191
|
+
}
|
|
184
192
|
export function resolveOAuth2Config(config, extraEnv, envFallback) {
|
|
185
193
|
if (!config.auth || config.auth.type !== "oauth2") {
|
|
186
194
|
throw new Error("[mcp-bridge] resolveOAuth2Config called for non-oauth2 auth config");
|
|
187
195
|
}
|
|
188
|
-
|
|
196
|
+
if (isAuthCodeOAuth2(config.auth)) {
|
|
197
|
+
throw new Error("[mcp-bridge] resolveOAuth2Config called for authorization_code config — use resolveAuthCodeOAuth2Config instead");
|
|
198
|
+
}
|
|
199
|
+
if (isDeviceCodeOAuth2(config.auth)) {
|
|
200
|
+
throw new Error("[mcp-bridge] resolveOAuth2Config called for device_code config — use resolveDeviceCodeOAuth2Config instead");
|
|
201
|
+
}
|
|
202
|
+
const auth = config.auth;
|
|
203
|
+
const scopes = auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
|
|
189
204
|
return {
|
|
190
|
-
clientId: resolveEnvVars(
|
|
191
|
-
clientSecret: resolveEnvVars(
|
|
192
|
-
tokenUrl: resolveEnvVars(
|
|
205
|
+
clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback),
|
|
206
|
+
clientSecret: resolveEnvVars(auth.clientSecret, "oauth2 clientSecret", extraEnv, envFallback),
|
|
207
|
+
tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
|
|
193
208
|
...(scopes && scopes.length > 0 ? { scopes } : {}),
|
|
194
|
-
...(
|
|
195
|
-
? { audience: resolveEnvVars(
|
|
209
|
+
...(auth.audience
|
|
210
|
+
? { audience: resolveEnvVars(auth.audience, "oauth2 audience", extraEnv, envFallback) }
|
|
196
211
|
: {}),
|
|
197
212
|
};
|
|
198
213
|
}
|
|
199
|
-
export
|
|
214
|
+
export function resolveAuthCodeOAuth2Config(config, extraEnv, envFallback) {
|
|
215
|
+
if (!config.auth || config.auth.type !== "oauth2" || !isAuthCodeOAuth2(config.auth)) {
|
|
216
|
+
throw new Error("[mcp-bridge] resolveAuthCodeOAuth2Config called for non-authorization_code auth config");
|
|
217
|
+
}
|
|
218
|
+
const auth = config.auth;
|
|
219
|
+
const scopes = auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
|
|
220
|
+
return {
|
|
221
|
+
grantType: "authorization_code",
|
|
222
|
+
tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
|
|
223
|
+
...(auth.clientId ? { clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback) } : {}),
|
|
224
|
+
...(auth.clientSecret ? { clientSecret: resolveEnvVars(auth.clientSecret, "oauth2 clientSecret", extraEnv, envFallback) } : {}),
|
|
225
|
+
...(scopes && scopes.length > 0 ? { scopes } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function resolveDeviceCodeOAuth2Config(config, extraEnv, envFallback) {
|
|
229
|
+
if (!config.auth || config.auth.type !== "oauth2" || !isDeviceCodeOAuth2(config.auth)) {
|
|
230
|
+
throw new Error("[mcp-bridge] resolveDeviceCodeOAuth2Config called for non-device_code auth config");
|
|
231
|
+
}
|
|
232
|
+
const auth = config.auth;
|
|
233
|
+
const scopes = auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
|
|
234
|
+
return {
|
|
235
|
+
grantType: "device_code",
|
|
236
|
+
tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
|
|
237
|
+
clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback),
|
|
238
|
+
...(scopes && scopes.length > 0 ? { scopes } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export async function resolveAuthHeadersAsync(config, tokenManager, extraEnv, envFallback, serverName) {
|
|
200
242
|
if (!config.auth)
|
|
201
243
|
return {};
|
|
202
244
|
if (config.auth.type === "oauth2") {
|
|
245
|
+
if (isAuthCodeOAuth2(config.auth)) {
|
|
246
|
+
if (!serverName) {
|
|
247
|
+
throw new Error("[mcp-bridge] serverName is required for authorization_code OAuth2 flow");
|
|
248
|
+
}
|
|
249
|
+
const authCodeConfig = resolveAuthCodeOAuth2Config(config, extraEnv, envFallback);
|
|
250
|
+
const token = await tokenManager.getTokenForAuthCode(serverName, authCodeConfig);
|
|
251
|
+
return { Authorization: `Bearer ${token}` };
|
|
252
|
+
}
|
|
253
|
+
if (isDeviceCodeOAuth2(config.auth)) {
|
|
254
|
+
if (!serverName) {
|
|
255
|
+
throw new Error("[mcp-bridge] serverName is required for device_code OAuth2 flow");
|
|
256
|
+
}
|
|
257
|
+
const deviceCodeConfig = resolveDeviceCodeOAuth2Config(config, extraEnv, envFallback);
|
|
258
|
+
const token = await tokenManager.getTokenForDeviceCode(serverName, deviceCodeConfig);
|
|
259
|
+
return { Authorization: `Bearer ${token}` };
|
|
260
|
+
}
|
|
203
261
|
const oauth2Config = resolveOAuth2Config(config, extraEnv, envFallback);
|
|
204
262
|
const token = await tokenManager.getToken(oauth2Config);
|
|
205
263
|
return { Authorization: `Bearer ${token}` };
|
|
@@ -214,9 +272,9 @@ export function resolveServerHeaders(config, extraEnv, envFallback) {
|
|
|
214
272
|
const auth = resolveAuthHeaders(config, extraEnv, envFallback);
|
|
215
273
|
return { ...base, ...auth };
|
|
216
274
|
}
|
|
217
|
-
export async function resolveServerHeadersAsync(config, tokenManager, extraEnv, envFallback) {
|
|
275
|
+
export async function resolveServerHeadersAsync(config, tokenManager, extraEnv, envFallback, serverName) {
|
|
218
276
|
const base = resolveEnvRecord(config.headers || {}, "header", extraEnv, envFallback);
|
|
219
|
-
const auth = await resolveAuthHeadersAsync(config, tokenManager, extraEnv, envFallback);
|
|
277
|
+
const auth = await resolveAuthHeadersAsync(config, tokenManager, extraEnv, envFallback, serverName);
|
|
220
278
|
return { ...base, ...auth };
|
|
221
279
|
}
|
|
222
280
|
/**
|
|
@@ -7,8 +7,9 @@ export declare class SseTransport extends BaseTransport {
|
|
|
7
7
|
private resolvedHeaders;
|
|
8
8
|
private pendingRequestControllers;
|
|
9
9
|
private readonly tokenManager;
|
|
10
|
+
private readonly serverName?;
|
|
10
11
|
protected get transportName(): string;
|
|
11
|
-
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator);
|
|
12
|
+
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator, serverName?: string);
|
|
12
13
|
connect(): Promise<void>;
|
|
13
14
|
private _onEndpointReceived;
|
|
14
15
|
private getBaseHeaders;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
2
|
-
import { BaseTransport, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
2
|
+
import { BaseTransport, isAuthCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
3
3
|
export class SseTransport extends BaseTransport {
|
|
4
4
|
endpointUrl = null;
|
|
5
5
|
sseAbortController = null;
|
|
6
6
|
resolvedHeaders = null;
|
|
7
7
|
pendingRequestControllers = new Map();
|
|
8
8
|
tokenManager;
|
|
9
|
+
serverName;
|
|
9
10
|
get transportName() { return "SSE"; }
|
|
10
|
-
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator) {
|
|
11
|
+
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator, serverName) {
|
|
11
12
|
super(config, clientConfig, logger, onReconnected, requestIdGenerator);
|
|
12
13
|
this.tokenManager = tokenManager ?? new OAuth2TokenManager(logger);
|
|
14
|
+
this.serverName = serverName;
|
|
13
15
|
}
|
|
14
16
|
async connect() {
|
|
15
17
|
if (!this.config.url) {
|
|
@@ -52,7 +54,7 @@ export class SseTransport extends BaseTransport {
|
|
|
52
54
|
}
|
|
53
55
|
async refreshResolvedHeaders() {
|
|
54
56
|
if (this.config.auth?.type === "oauth2") {
|
|
55
|
-
this.resolvedHeaders = await resolveServerHeadersAsync(this.config, this.tokenManager, undefined, this.clientConfig.envFallback);
|
|
57
|
+
this.resolvedHeaders = await resolveServerHeadersAsync(this.config, this.tokenManager, undefined, this.clientConfig.envFallback, this.serverName);
|
|
56
58
|
}
|
|
57
59
|
else {
|
|
58
60
|
this.resolvedHeaders = resolveServerHeaders(this.config, undefined, this.clientConfig.envFallback);
|
|
@@ -63,6 +65,9 @@ export class SseTransport extends BaseTransport {
|
|
|
63
65
|
if (this.config.auth?.type !== "oauth2") {
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
68
|
+
if (isAuthCodeOAuth2(this.config.auth)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
66
71
|
const oauth2Config = resolveOAuth2Config(this.config, undefined, this.clientConfig.envFallback);
|
|
67
72
|
this.tokenManager.invalidate(oauth2Config.tokenUrl, oauth2Config.clientId);
|
|
68
73
|
}
|
|
@@ -63,7 +63,7 @@ export class StdioTransport extends BaseTransport {
|
|
|
63
63
|
this.process.stdout.on("data", (data) => {
|
|
64
64
|
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, data]);
|
|
65
65
|
// Safety limit: prevent unbounded buffer growth from misbehaving servers
|
|
66
|
-
const MAX_BUFFER =
|
|
66
|
+
const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
|
|
67
67
|
if (this.stdoutBuffer.length > MAX_BUFFER) {
|
|
68
68
|
this.logger.error(`[mcp-bridge] Stdio buffer exceeded ${MAX_BUFFER} bytes, killing process`);
|
|
69
69
|
this.process?.kill();
|
|
@@ -6,8 +6,9 @@ export declare class StreamableHttpTransport extends BaseTransport {
|
|
|
6
6
|
private resolvedHeaders;
|
|
7
7
|
private pendingRequestControllers;
|
|
8
8
|
private readonly tokenManager;
|
|
9
|
+
private readonly serverName?;
|
|
9
10
|
protected get transportName(): string;
|
|
10
|
-
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator);
|
|
11
|
+
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator, serverName?: string);
|
|
11
12
|
connect(): Promise<void>;
|
|
12
13
|
private getBaseHeaders;
|
|
13
14
|
private refreshResolvedHeaders;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
2
|
-
import { BaseTransport, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
2
|
+
import { BaseTransport, isAuthCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
3
3
|
export class StreamableHttpTransport extends BaseTransport {
|
|
4
4
|
sessionId;
|
|
5
5
|
resolvedHeaders = null;
|
|
6
6
|
pendingRequestControllers = new Map();
|
|
7
7
|
tokenManager;
|
|
8
|
+
serverName;
|
|
8
9
|
get transportName() { return "streamable-http"; }
|
|
9
|
-
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator) {
|
|
10
|
+
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator, serverName) {
|
|
10
11
|
super(config, clientConfig, logger, onReconnected, requestIdGenerator);
|
|
11
12
|
this.tokenManager = tokenManager ?? new OAuth2TokenManager(logger);
|
|
13
|
+
this.serverName = serverName;
|
|
12
14
|
}
|
|
13
15
|
async connect() {
|
|
14
16
|
if (!this.config.url) {
|
|
@@ -35,7 +37,7 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
35
37
|
}
|
|
36
38
|
async refreshResolvedHeaders() {
|
|
37
39
|
if (this.config.auth?.type === "oauth2") {
|
|
38
|
-
this.resolvedHeaders = await resolveServerHeadersAsync(this.config, this.tokenManager, undefined, this.clientConfig.envFallback);
|
|
40
|
+
this.resolvedHeaders = await resolveServerHeadersAsync(this.config, this.tokenManager, undefined, this.clientConfig.envFallback, this.serverName);
|
|
39
41
|
}
|
|
40
42
|
else {
|
|
41
43
|
this.resolvedHeaders = resolveServerHeaders(this.config, undefined, this.clientConfig.envFallback);
|
|
@@ -46,6 +48,10 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
46
48
|
if (this.config.auth?.type !== "oauth2") {
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
51
|
+
// authorization_code tokens are managed via TokenStore, not the in-memory cache
|
|
52
|
+
if (isAuthCodeOAuth2(this.config.auth)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
49
55
|
const oauth2Config = resolveOAuth2Config(this.config, undefined, this.clientConfig.envFallback);
|
|
50
56
|
this.tokenManager.invalidate(oauth2Config.tokenUrl, oauth2Config.clientId);
|
|
51
57
|
}
|
|
@@ -111,10 +117,17 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
111
117
|
try {
|
|
112
118
|
const contentType = response.headers.get("content-type") || "";
|
|
113
119
|
if (contentType.includes("text/event-stream")) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
120
|
+
// Stream SSE response incrementally using ReadableStream
|
|
121
|
+
// (previous implementation used response.text() which blocked until
|
|
122
|
+
// the entire response was received, causing timeouts on long-running calls)
|
|
123
|
+
if (!response.body) {
|
|
124
|
+
throw new Error("SSE response has no body stream");
|
|
125
|
+
}
|
|
126
|
+
const reader = response.body.getReader();
|
|
127
|
+
const decoder = new TextDecoder();
|
|
128
|
+
let partial = "";
|
|
117
129
|
let dataBuffer = [];
|
|
130
|
+
let hasData = false;
|
|
118
131
|
const dispatch = () => {
|
|
119
132
|
if (dataBuffer.length === 0)
|
|
120
133
|
return;
|
|
@@ -127,19 +140,37 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
127
140
|
// skip malformed events
|
|
128
141
|
}
|
|
129
142
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (done)
|
|
147
|
+
break;
|
|
148
|
+
partial += decoder.decode(value, { stream: true });
|
|
149
|
+
const lines = partial.split("\n");
|
|
150
|
+
// Keep the last (potentially incomplete) line in partial
|
|
151
|
+
partial = lines.pop() || "";
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
if (trimmed.startsWith("data:")) {
|
|
155
|
+
dataBuffer.push(trimmed.substring(5).trimStart());
|
|
156
|
+
hasData = true;
|
|
157
|
+
}
|
|
158
|
+
else if (trimmed === "" && dataBuffer.length > 0) {
|
|
159
|
+
dispatch();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
136
162
|
}
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
// Process any remaining partial line
|
|
164
|
+
if (partial.trim().startsWith("data:")) {
|
|
165
|
+
dataBuffer.push(partial.trim().substring(5).trimStart());
|
|
166
|
+
hasData = true;
|
|
139
167
|
}
|
|
168
|
+
// Dispatch any trailing data (server may omit final empty line)
|
|
169
|
+
dispatch();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
reader.releaseLock();
|
|
140
173
|
}
|
|
141
|
-
// Dispatch any trailing data (server may omit final empty line)
|
|
142
|
-
dispatch();
|
|
143
174
|
if (!hasData) {
|
|
144
175
|
throw new Error("No data lines in SSE response");
|
|
145
176
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -17,6 +17,22 @@ export type HttpAuthConfig = {
|
|
|
17
17
|
tokenUrl: string;
|
|
18
18
|
scopes?: string[];
|
|
19
19
|
audience?: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "oauth2";
|
|
22
|
+
grantType: "authorization_code";
|
|
23
|
+
authorizationUrl: string;
|
|
24
|
+
tokenUrl: string;
|
|
25
|
+
clientId?: string;
|
|
26
|
+
clientSecret?: string;
|
|
27
|
+
scopes?: string[];
|
|
28
|
+
callbackPort?: number;
|
|
29
|
+
} | {
|
|
30
|
+
type: "oauth2";
|
|
31
|
+
grantType: "device_code";
|
|
32
|
+
deviceAuthorizationUrl: string;
|
|
33
|
+
tokenUrl: string;
|
|
34
|
+
clientId: string;
|
|
35
|
+
scopes?: string[];
|
|
20
36
|
};
|
|
21
37
|
export interface RetryConfig {
|
|
22
38
|
maxAttempts?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwerk/mcp-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"build": "tsc",
|
|
45
45
|
"test": "node --import tsx --test tests/*.test.ts",
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
|
47
|
-
"prepublishOnly": "bash scripts/validate-recipes.sh",
|
|
47
|
+
"prepublishOnly": "tsc && bash scripts/validate-recipes.sh",
|
|
48
48
|
"validate-recipe": "npx tsx bin/validate-recipe.ts",
|
|
49
49
|
"lint": "eslint src/",
|
|
50
50
|
"format": "prettier --write src/",
|