@ruifung/codemode-bridge 1.0.3-1

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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +378 -0
  3. package/dist/cli/commands.d.ts +70 -0
  4. package/dist/cli/commands.js +436 -0
  5. package/dist/cli/config-manager.d.ts +53 -0
  6. package/dist/cli/config-manager.js +142 -0
  7. package/dist/cli/index.d.ts +19 -0
  8. package/dist/cli/index.js +165 -0
  9. package/dist/executor/container-executor.d.ts +81 -0
  10. package/dist/executor/container-executor.js +351 -0
  11. package/dist/executor/executor-test-suite.d.ts +22 -0
  12. package/dist/executor/executor-test-suite.js +395 -0
  13. package/dist/executor/isolated-vm-executor.d.ts +78 -0
  14. package/dist/executor/isolated-vm-executor.js +368 -0
  15. package/dist/executor/vm2-executor.d.ts +21 -0
  16. package/dist/executor/vm2-executor.js +109 -0
  17. package/dist/executor/wrap-code.d.ts +52 -0
  18. package/dist/executor/wrap-code.js +80 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/mcp/config.d.ts +44 -0
  22. package/dist/mcp/config.js +102 -0
  23. package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
  24. package/dist/mcp/e2e-bridge-test-suite.js +429 -0
  25. package/dist/mcp/executor.d.ts +31 -0
  26. package/dist/mcp/executor.js +121 -0
  27. package/dist/mcp/mcp-adapter.d.ts +12 -0
  28. package/dist/mcp/mcp-adapter.js +49 -0
  29. package/dist/mcp/mcp-client.d.ts +85 -0
  30. package/dist/mcp/mcp-client.js +441 -0
  31. package/dist/mcp/oauth-handler.d.ts +33 -0
  32. package/dist/mcp/oauth-handler.js +138 -0
  33. package/dist/mcp/server.d.ts +25 -0
  34. package/dist/mcp/server.js +322 -0
  35. package/dist/mcp/token-persistence.d.ts +57 -0
  36. package/dist/mcp/token-persistence.js +131 -0
  37. package/dist/utils/logger.d.ts +44 -0
  38. package/dist/utils/logger.js +123 -0
  39. package/package.json +56 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * MCP Client - Wrapper around official MCP SDK for connecting to upstream MCP servers
3
+ *
4
+ * This client:
5
+ * 1. Uses the official @modelcontextprotocol/sdk Client API
6
+ * 2. Supports stdio, HTTP (Streamable HTTP), and SSE transports
7
+ * 3. Returns tools in native MCP format (JSON Schema for inputSchema)
8
+ * 4. Provides tool execution via callTool()
9
+ */
10
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
11
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
12
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
14
+ import { tokenPersistence } from "./token-persistence.js";
15
+ import { OAuthCallbackServer } from "./oauth-handler.js";
16
+ import { logDebug, logInfo } from "../utils/logger.js";
17
+ /**
18
+ * Simple in-memory OAuth provider implementation for basic OAuth flows
19
+ * Supports both pre-registered clients and dynamic client registration (RFC 7591)
20
+ */
21
+ class SimpleOAuthProvider {
22
+ constructor(config, serverUrl) {
23
+ this.config = config;
24
+ this.serverUrl = serverUrl;
25
+ logDebug(`Creating provider for ${serverUrl}`, { component: 'OAuth' });
26
+ // Load tokens from persistence on initialization
27
+ const persistedTokens = tokenPersistence.getTokens(serverUrl);
28
+ if (persistedTokens) {
29
+ this._tokens = persistedTokens;
30
+ logDebug(`Loaded persisted tokens for ${serverUrl}`, { component: 'OAuth' });
31
+ }
32
+ else {
33
+ logDebug(`No persisted tokens found for ${serverUrl}`, { component: 'OAuth' });
34
+ }
35
+ // Load client information from persistence
36
+ const persistedClientInfo = tokenPersistence.getClientInformation(serverUrl);
37
+ if (persistedClientInfo) {
38
+ this._clientInfo = persistedClientInfo;
39
+ logDebug(`Loaded persisted client info for ${serverUrl}`, { component: 'OAuth' });
40
+ }
41
+ else {
42
+ logDebug(`No persisted client info found for ${serverUrl}`, { component: 'OAuth' });
43
+ }
44
+ }
45
+ /**
46
+ * Set the callback to be invoked when authorization code is received.
47
+ * This should be called by MCPClient with the transport's finishAuth method.
48
+ */
49
+ setFinishAuthCallback(callback) {
50
+ this._finishAuthCallback = callback;
51
+ }
52
+ get redirectUrl() {
53
+ // If using dynamic port assignment, return the server's actual URL
54
+ if (this._callbackServer) {
55
+ return this._callbackServer.getRedirectUrl();
56
+ }
57
+ // Otherwise use config or default
58
+ return this.config.redirectUrl || SimpleOAuthProvider.DEFAULT_REDIRECT_URL;
59
+ }
60
+ get clientMetadata() {
61
+ const redirectUrl = this.redirectUrl || SimpleOAuthProvider.DEFAULT_REDIRECT_URL;
62
+ const metadata = {
63
+ redirect_uris: [String(redirectUrl)],
64
+ client_name: 'CodeMode Bridge',
65
+ };
66
+ if (this.config.grantType) {
67
+ metadata.grant_types = [this.config.grantType];
68
+ }
69
+ return metadata;
70
+ }
71
+ clientInformation() {
72
+ // If client info was dynamically registered and saved, return it
73
+ if (this._clientInfo) {
74
+ return this._clientInfo;
75
+ }
76
+ // If clientId was provided in config (pre-registered), return static info
77
+ if (this.config.clientId) {
78
+ const info = {
79
+ client_id: this.config.clientId,
80
+ };
81
+ if (this.config.clientSecret) {
82
+ info.client_secret = this.config.clientSecret;
83
+ }
84
+ return info;
85
+ }
86
+ // Return undefined to trigger dynamic registration
87
+ return undefined;
88
+ }
89
+ saveClientInformation(clientInformation) {
90
+ // Save dynamically registered client information in memory
91
+ this._clientInfo = clientInformation;
92
+ // Also persist to disk
93
+ tokenPersistence.saveClientInformation(this.serverUrl, clientInformation);
94
+ logDebug(`Client dynamically registered: ${clientInformation.client_id}`, { component: 'OAuth' });
95
+ }
96
+ tokens() {
97
+ // First try in-memory tokens
98
+ if (this._tokens) {
99
+ logDebug('tokens() returning in-memory tokens', { component: 'OAuth' });
100
+ return this._tokens;
101
+ }
102
+ // Fallback to persistence if memory is empty
103
+ logDebug('tokens() checking persistence...', { component: 'OAuth' });
104
+ const persistedTokens = tokenPersistence.getTokens(this.serverUrl);
105
+ if (persistedTokens) {
106
+ this._tokens = persistedTokens;
107
+ logDebug(`tokens() returning persisted tokens for ${this.serverUrl}`, { component: 'OAuth' });
108
+ return persistedTokens;
109
+ }
110
+ logDebug('tokens() returning undefined - no tokens available', { component: 'OAuth' });
111
+ return undefined;
112
+ }
113
+ saveTokens(tokens) {
114
+ logDebug(`saveTokens() called for ${this.serverUrl}`, { component: 'OAuth' });
115
+ this._tokens = tokens;
116
+ // Also persist tokens to disk
117
+ tokenPersistence.saveTokens(this.serverUrl, tokens);
118
+ logDebug('Tokens saved to persistence', { component: 'OAuth' });
119
+ }
120
+ async redirectToAuthorization(authorizationUrl) {
121
+ logDebug('=== OAuth Authorization Required ===', { component: 'OAuth' });
122
+ logDebug(`Server: ${this.serverUrl}`, { component: 'OAuth' });
123
+ logDebug('Opening browser for authorization...', { component: 'OAuth' });
124
+ logDebug(`URL: ${authorizationUrl.toString()}`, { component: 'OAuth' });
125
+ // Start the callback server to listen for the authorization code
126
+ const redirectUrl = this.config.redirectUrl || SimpleOAuthProvider.DEFAULT_REDIRECT_URL;
127
+ this._callbackServer = new OAuthCallbackServer(redirectUrl);
128
+ try {
129
+ // Start listening for callback in the background
130
+ // Don't await this - it will resolve when code is received
131
+ const authCodePromise = this._callbackServer.waitForAuthorizationCode(300000); // 5 min timeout
132
+ // Open in default browser using dynamic import
133
+ import('open').then((module) => {
134
+ const open = module.default;
135
+ open(authorizationUrl.toString()).catch((err) => {
136
+ logDebug('Could not open browser automatically.', { component: 'OAuth' });
137
+ logDebug('Please visit this URL to authorize:', { component: 'OAuth' });
138
+ logDebug(authorizationUrl.toString(), { component: 'OAuth' });
139
+ });
140
+ }).catch((err) => {
141
+ logDebug('Could not import open module.', { component: 'OAuth' });
142
+ logDebug('Please visit this URL to authorize:', { component: 'OAuth' });
143
+ logDebug(authorizationUrl.toString(), { component: 'OAuth' });
144
+ });
145
+ // Wait for the authorization code
146
+ const authorizationCode = await authCodePromise;
147
+ logDebug(`Authorization code received: ${authorizationCode.substring(0, 10)}...`, { component: 'OAuth' });
148
+ // Call the finish auth callback if set
149
+ if (this._finishAuthCallback) {
150
+ logDebug('Calling finishAuth with authorization code', { component: 'OAuth' });
151
+ await this._finishAuthCallback(authorizationCode);
152
+ }
153
+ else {
154
+ logDebug('WARNING: finishAuthCallback not set - authorization code cannot be exchanged', { component: 'OAuth' });
155
+ }
156
+ }
157
+ catch (error) {
158
+ const errorMsg = error instanceof Error ? error.message : String(error);
159
+ logDebug(`OAuth authorization failed: ${errorMsg}`, { component: 'OAuth' });
160
+ throw error;
161
+ }
162
+ finally {
163
+ // Clean up callback server
164
+ if (this._callbackServer) {
165
+ await this._callbackServer.stop();
166
+ this._callbackServer = undefined;
167
+ }
168
+ }
169
+ }
170
+ saveCodeVerifier(codeVerifier) {
171
+ this._codeVerifier = codeVerifier;
172
+ }
173
+ codeVerifier() {
174
+ if (!this._codeVerifier) {
175
+ throw new Error('Code verifier not available');
176
+ }
177
+ return this._codeVerifier;
178
+ }
179
+ saveDiscoveryState(state) {
180
+ this._discoveryState = state;
181
+ }
182
+ discoveryState() {
183
+ return this._discoveryState;
184
+ }
185
+ // Prepare token request for client_credentials grant
186
+ prepareTokenRequest(scope) {
187
+ if (this.config.grantType === 'client_credentials') {
188
+ const params = new URLSearchParams({
189
+ grant_type: 'client_credentials',
190
+ });
191
+ if (scope || this.config.scope) {
192
+ params.set('scope', scope || this.config.scope);
193
+ }
194
+ return params;
195
+ }
196
+ return undefined;
197
+ }
198
+ }
199
+ SimpleOAuthProvider.DEFAULT_REDIRECT_URL = 'http://localhost:3000/oauth/callback';
200
+ /**
201
+ * MCP Client wrapper using official SDK
202
+ */
203
+ export class MCPClient {
204
+ constructor(config) {
205
+ this.config = config;
206
+ this.transport = null;
207
+ this.connected = false;
208
+ this.client = new Client({
209
+ name: `codemode-bridge-client-${config.name}`,
210
+ version: "1.0.0",
211
+ }, {
212
+ capabilities: {},
213
+ });
214
+ // Create OAuth provider if config is present
215
+ if (config.oauth && config.url) {
216
+ this.oauthProvider = new SimpleOAuthProvider(config.oauth, config.url);
217
+ }
218
+ }
219
+ /**
220
+ * Authenticate with OAuth server and obtain tokens without connecting the full client.
221
+ * This is useful for CLI auth flows where we just want to get tokens without initializing
222
+ * the full MCP client.
223
+ */
224
+ async authenticateOAuth() {
225
+ if (!this.config.oauth || !this.config.url) {
226
+ throw new Error("OAuth not configured for this server");
227
+ }
228
+ if (this.config.type !== "http") {
229
+ throw new Error("OAuth is only supported for HTTP servers");
230
+ }
231
+ // Create OAuth provider if not already created
232
+ if (!this.oauthProvider) {
233
+ this.oauthProvider = new SimpleOAuthProvider(this.config.oauth, this.config.url);
234
+ }
235
+ const provider = this.oauthProvider;
236
+ const currentTokens = provider.tokens();
237
+ if (currentTokens) {
238
+ logDebug('OAuth tokens already available, skipping OAuth flow', { component: 'HTTP' });
239
+ return;
240
+ }
241
+ logDebug('No tokens available, initiating OAuth flow', { component: 'OAuth' });
242
+ // Create HTTP transport for OAuth flow
243
+ const httpTransport = new StreamableHTTPClientTransport(new URL(this.config.url), { authProvider: provider });
244
+ try {
245
+ // Set up the callback for OAuth code exchange BEFORE connecting
246
+ provider.setFinishAuthCallback(async (authorizationCode) => {
247
+ logDebug('Exchanging authorization code for tokens...', { component: 'OAuth' });
248
+ logDebug(`Authorization code: ${authorizationCode.substring(0, 20)}...`, { component: 'OAuth' });
249
+ try {
250
+ await httpTransport.finishAuth(authorizationCode);
251
+ logDebug('Authorization code exchanged successfully', { component: 'OAuth' });
252
+ }
253
+ catch (error) {
254
+ logDebug(`Token exchange failed: ${error instanceof Error ? error.message : String(error)}`, { component: 'OAuth' });
255
+ throw error;
256
+ }
257
+ });
258
+ // Try to make a request to trigger OAuth
259
+ // This will fail with 401, which should trigger the auth provider's OAuth flow
260
+ try {
261
+ await httpTransport.send({
262
+ jsonrpc: '2.0',
263
+ id: 1,
264
+ method: 'initialize',
265
+ params: {
266
+ protocolVersion: '2024-11-05',
267
+ capabilities: {},
268
+ clientInfo: {
269
+ name: `codemode-bridge-oauth-${this.config.name}`,
270
+ version: '1.0.0',
271
+ },
272
+ },
273
+ });
274
+ }
275
+ catch (error) {
276
+ // Expected - will trigger OAuth or be a real error
277
+ logDebug(`Initial request result: ${error instanceof Error ? error.message : String(error)}`, { component: 'OAuth' });
278
+ // Don't rethrow - the OAuth flow may have been triggered
279
+ }
280
+ // Wait a moment for OAuth flow to complete
281
+ // If tokens were saved, we're done
282
+ const tokensAfterOAuth = provider.tokens();
283
+ if (tokensAfterOAuth) {
284
+ logDebug('Tokens obtained successfully via OAuth', { component: 'OAuth' });
285
+ return;
286
+ }
287
+ throw new Error('OAuth authentication did not produce tokens');
288
+ }
289
+ finally {
290
+ // Clean up the transport
291
+ if (httpTransport) {
292
+ try {
293
+ await httpTransport.close();
294
+ }
295
+ catch (e) {
296
+ logDebug(`Error closing transport: ${e instanceof Error ? e.message : String(e)}`, { component: 'OAuth' });
297
+ }
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * Connect to the upstream MCP server
303
+ */
304
+ async connect() {
305
+ if (this.connected) {
306
+ return;
307
+ }
308
+ if (this.config.type === "stdio") {
309
+ if (!this.config.command) {
310
+ throw new Error(`stdio type requires "command" field`);
311
+ }
312
+ // Merge provided env vars with current process env, filtering out undefined values
313
+ const baseEnv = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
314
+ const env = this.config.env ? { ...baseEnv, ...this.config.env } : baseEnv;
315
+ // Create stdio transport with stderr piped so we can capture and log it
316
+ const stdioTransport = new StdioClientTransport({
317
+ command: this.config.command,
318
+ args: this.config.args || [],
319
+ env,
320
+ stderr: 'pipe',
321
+ });
322
+ // Attach stderr handler to capture tool output and pass through logger
323
+ if (stdioTransport.stderr) {
324
+ stdioTransport.stderr.on('data', (data) => {
325
+ const lines = data.toString().split('\n').filter(line => line.trim());
326
+ for (const line of lines) {
327
+ logInfo(line, { component: this.config.name, suppressEarly: true });
328
+ }
329
+ });
330
+ stdioTransport.stderr.on('error', (error) => {
331
+ logDebug(`stderr error from ${this.config.name}: ${error.message}`, {
332
+ component: this.config.name
333
+ });
334
+ });
335
+ }
336
+ this.transport = stdioTransport;
337
+ await this.client.connect(this.transport);
338
+ this.connected = true;
339
+ }
340
+ else if (this.config.type === "http") {
341
+ if (!this.config.url) {
342
+ throw new Error(`http type requires "url" field`);
343
+ }
344
+ // Create HTTP transport using Streamable HTTP with optional OAuth
345
+ if (this.oauthProvider) {
346
+ logDebug(`Connecting with OAuth provider to ${this.config.url}`, { component: 'HTTP' });
347
+ const currentTokens = this.oauthProvider.tokens();
348
+ if (currentTokens) {
349
+ logDebug('OAuth tokens available', { component: 'HTTP' });
350
+ }
351
+ else {
352
+ logDebug('WARNING: No OAuth tokens available', { component: 'HTTP' });
353
+ }
354
+ }
355
+ else {
356
+ logDebug(`Connecting without OAuth to ${this.config.url}`, { component: 'HTTP' });
357
+ }
358
+ // Create HTTP transport
359
+ const httpTransport = new StreamableHTTPClientTransport(new URL(this.config.url), this.oauthProvider ? { authProvider: this.oauthProvider } : undefined);
360
+ // Set up the callback for OAuth code exchange
361
+ if (this.oauthProvider) {
362
+ this.oauthProvider.setFinishAuthCallback(async (authorizationCode) => {
363
+ logDebug('Exchanging authorization code for tokens...', { component: 'OAuth' });
364
+ await httpTransport.finishAuth(authorizationCode);
365
+ logDebug('Authorization code exchanged successfully', { component: 'OAuth' });
366
+ });
367
+ }
368
+ this.transport = httpTransport;
369
+ await this.client.connect(this.transport);
370
+ this.connected = true;
371
+ }
372
+ else if (this.config.type === "sse") {
373
+ if (!this.config.url) {
374
+ throw new Error(`sse type requires "url" field`);
375
+ }
376
+ // Create SSE transport (deprecated but still supported) with optional OAuth
377
+ this.transport = new SSEClientTransport(new URL(this.config.url), this.oauthProvider ? { authProvider: this.oauthProvider } : undefined);
378
+ await this.client.connect(this.transport);
379
+ this.connected = true;
380
+ }
381
+ else {
382
+ throw new Error(`Unsupported transport type: ${this.config.type}`);
383
+ }
384
+ }
385
+ /**
386
+ * List all tools from the upstream server
387
+ * Returns tools in native MCP format with JSON Schema
388
+ */
389
+ async listTools() {
390
+ if (!this.connected) {
391
+ throw new Error("Client not connected. Call connect() first.");
392
+ }
393
+ const response = await this.client.listTools();
394
+ return response.tools;
395
+ }
396
+ /**
397
+ * Call a tool on the upstream server
398
+ */
399
+ async callTool(name, args) {
400
+ if (!this.connected) {
401
+ throw new Error("Client not connected. Call connect() first.");
402
+ }
403
+ logDebug(`Invoking upstream tool: ${name}`, {
404
+ component: 'MCP Client',
405
+ transport: this.config.type,
406
+ server: this.config.name,
407
+ argsSize: JSON.stringify(args).length
408
+ });
409
+ try {
410
+ const response = await this.client.callTool({
411
+ name,
412
+ arguments: args,
413
+ });
414
+ logDebug(`Upstream tool completed: ${name}`, {
415
+ component: 'MCP Client',
416
+ transport: this.config.type,
417
+ server: this.config.name,
418
+ resultType: typeof response
419
+ });
420
+ return response;
421
+ }
422
+ catch (error) {
423
+ logDebug(`Upstream tool failed: ${name}`, {
424
+ component: 'MCP Client',
425
+ transport: this.config.type,
426
+ server: this.config.name,
427
+ error: error instanceof Error ? error.message : String(error)
428
+ });
429
+ throw error;
430
+ }
431
+ }
432
+ /**
433
+ * Close the connection to the upstream server
434
+ */
435
+ async close() {
436
+ if (this.connected && this.client) {
437
+ await this.client.close();
438
+ this.connected = false;
439
+ }
440
+ }
441
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * OAuth2 Authorization Handler
3
+ *
4
+ * Implements a loopback HTTP server to handle OAuth2 redirect callbacks.
5
+ * When user completes authorization on the OAuth provider, the browser redirects
6
+ * to our loopback server with the authorization code, which we capture and use
7
+ * to complete the authorization flow.
8
+ */
9
+ /**
10
+ * Loopback HTTP server that listens for OAuth2 redirect callbacks
11
+ */
12
+ export declare class OAuthCallbackServer {
13
+ private server?;
14
+ private port;
15
+ private host;
16
+ private pendingAuthorization?;
17
+ constructor(redirectUrl?: string);
18
+ /**
19
+ * Start the callback server and wait for authorization code
20
+ * Returns a promise that resolves when the authorization code is received
21
+ * or rejects if timeout occurs or server fails to start
22
+ */
23
+ waitForAuthorizationCode(timeoutMs?: number): Promise<string>;
24
+ /**
25
+ * Stop the callback server
26
+ */
27
+ stop(): Promise<void>;
28
+ /**
29
+ * Get the redirect URL for this server
30
+ * After waitForAuthorizationCode is called, returns the actual URL with the assigned port
31
+ */
32
+ getRedirectUrl(): string;
33
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * OAuth2 Authorization Handler
3
+ *
4
+ * Implements a loopback HTTP server to handle OAuth2 redirect callbacks.
5
+ * When user completes authorization on the OAuth provider, the browser redirects
6
+ * to our loopback server with the authorization code, which we capture and use
7
+ * to complete the authorization flow.
8
+ */
9
+ import { createServer } from 'http';
10
+ import { URL } from 'url';
11
+ /**
12
+ * Loopback HTTP server that listens for OAuth2 redirect callbacks
13
+ */
14
+ export class OAuthCallbackServer {
15
+ constructor(redirectUrl) {
16
+ this.port = 0; // 0 means OS will assign an available port
17
+ this.host = 'localhost';
18
+ // Parse redirect URL to extract host and port
19
+ if (redirectUrl) {
20
+ try {
21
+ const url = new URL(redirectUrl);
22
+ this.host = url.hostname || 'localhost';
23
+ // If a specific port is provided in the URL, use it
24
+ // Otherwise use 0 (OS will assign available port)
25
+ this.port = url.port ? parseInt(url.port) : 0;
26
+ }
27
+ catch {
28
+ this.port = 0;
29
+ }
30
+ }
31
+ else {
32
+ this.port = 0;
33
+ }
34
+ }
35
+ /**
36
+ * Start the callback server and wait for authorization code
37
+ * Returns a promise that resolves when the authorization code is received
38
+ * or rejects if timeout occurs or server fails to start
39
+ */
40
+ async waitForAuthorizationCode(timeoutMs = 300000) {
41
+ return new Promise((resolve, reject) => {
42
+ // Create HTTP server
43
+ this.server = createServer((req, res) => {
44
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
45
+ const code = url.searchParams.get('code');
46
+ const error = url.searchParams.get('error');
47
+ const errorDescription = url.searchParams.get('error_description');
48
+ if (error) {
49
+ // OAuth error from authorization server
50
+ const errorMsg = errorDescription
51
+ ? `${error}: ${errorDescription}`
52
+ : `OAuth error: ${error}`;
53
+ res.writeHead(400, { 'Content-Type': 'text/html' });
54
+ res.end(`
55
+ <html>
56
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
57
+ <h1>Authorization Failed</h1>
58
+ <p>${errorMsg}</p>
59
+ <p>You can close this window.</p>
60
+ </body>
61
+ </html>
62
+ `);
63
+ if (this.pendingAuthorization) {
64
+ this.pendingAuthorization.reject(new Error(errorMsg));
65
+ }
66
+ return;
67
+ }
68
+ if (code) {
69
+ // Success - authorization code received
70
+ res.writeHead(200, { 'Content-Type': 'text/html' });
71
+ res.end(`
72
+ <html>
73
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
74
+ <h1>Authorization Successful</h1>
75
+ <p>You have successfully authorized the Code Mode Bridge.</p>
76
+ <p>You can close this window and return to your terminal.</p>
77
+ </body>
78
+ </html>
79
+ `);
80
+ if (this.pendingAuthorization) {
81
+ this.pendingAuthorization.resolve(code);
82
+ }
83
+ return;
84
+ }
85
+ // Unknown request
86
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
87
+ res.end('Invalid OAuth callback request');
88
+ });
89
+ // Set up timeout
90
+ const timeout = setTimeout(() => {
91
+ reject(new Error(`Authorization timeout after ${timeoutMs}ms`));
92
+ this.stop();
93
+ }, timeoutMs);
94
+ // Start listening on available port
95
+ this.server.listen(this.port, this.host, () => {
96
+ // Get the actual port assigned by the OS (in case we used 0)
97
+ const addr = this.server.address();
98
+ if (addr && typeof addr === 'object') {
99
+ this.port = addr.port;
100
+ }
101
+ // Store the promise handlers for when request arrives
102
+ this.pendingAuthorization = {
103
+ resolve,
104
+ reject: (error) => {
105
+ clearTimeout(timeout);
106
+ reject(error);
107
+ this.stop();
108
+ },
109
+ timeout,
110
+ };
111
+ });
112
+ this.server.on('error', (error) => {
113
+ clearTimeout(timeout);
114
+ reject(error);
115
+ });
116
+ });
117
+ }
118
+ /**
119
+ * Stop the callback server
120
+ */
121
+ async stop() {
122
+ if (this.server) {
123
+ return new Promise((resolve) => {
124
+ this.server.close(() => {
125
+ this.server = undefined;
126
+ resolve();
127
+ });
128
+ });
129
+ }
130
+ }
131
+ /**
132
+ * Get the redirect URL for this server
133
+ * After waitForAuthorizationCode is called, returns the actual URL with the assigned port
134
+ */
135
+ getRedirectUrl() {
136
+ return `http://${this.host}:${this.port}/oauth/callback`;
137
+ }
138
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MCP Server - Exposes the Code Mode bridge as an MCP server
3
+ *
4
+ * Architecture:
5
+ * - Upstream: Use official MCP SDK's Client to connect to and collect tools from other MCP servers
6
+ * - Orchestration: Pass collected tools to codemode SDK's createCodeTool()
7
+ * - Downstream: Use MCP SDK to expose the codemode tool via MCP protocol (stdio transport)
8
+ *
9
+ * This server:
10
+ * 1. Connects to upstream MCP servers using official MCP SDK Client
11
+ * 2. Collects tools from all upstream servers in native MCP format (JSON Schema)
12
+ * 3. Converts tools to ToolDescriptor format (with Zod schemas)
13
+ * 4. Uses @cloudflare/codemode SDK to create the "codemode" tool with those tools
14
+ * 5. Adapts the codemode SDK's AI SDK Tool to MCP protocol using a shim layer
15
+ * 6. Exposes the "codemode" tool via MCP protocol downstream
16
+ */
17
+ import { z } from "zod";
18
+ import { type MCPServerConfig } from "./mcp-client.js";
19
+ export type { MCPServerConfig };
20
+ /**
21
+ * Convert JSON Schema to Zod schema
22
+ * MCP tools use JSON Schema, but createCodeTool expects Zod schemas
23
+ */
24
+ export declare function jsonSchemaToZod(schema: any): z.ZodType<any>;
25
+ export declare function startCodeModeBridgeServer(serverConfigs: MCPServerConfig[]): Promise<void>;