Skip to content

Commit 47680ef

Browse files
committed
feat: relay logs from connectToRemoteServer to proxy local transport
1 parent e642a56 commit 47680ef

File tree

3 files changed

+76
-51
lines changed

3 files changed

+76
-51
lines changed

src/lib/types.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { EventEmitter } from 'events'
2-
import { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'
32

43
/**
54
* Options for creating an OAuth client provider
@@ -36,10 +35,6 @@ export interface OAuthCallbackServerOptions {
3635
}
3736

3837
/*
39-
* Message sending helper type
38+
* Connection status types used for logging (via local transport, in proxy mode)
4039
*/
41-
export interface MCPLogMessageParams {
42-
level: LoggingLevel
43-
logger: string
44-
data: Record<string, any>
45-
}
40+
export type ConnStatus = 'connected' | 'connecting' | 'reconnecting' | 'authenticating' | 'error' | 'error_final'

src/lib/utils.ts

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
33
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
44
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
55
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
6-
import type { MCPLogMessageParams } from './types'
6+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7+
import { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'
8+
import { ConnStatus } from './types'
79

810
// Connection constants
911
export const REASON_AUTH_NEEDED = 'authentication-needed'
@@ -74,6 +76,29 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
7476
}
7577
}
7678

79+
/**
80+
* Extended StdioServerTransport class
81+
*/
82+
export class StdioServerTransportExt extends StdioServerTransport {
83+
/**
84+
* Send a log message through the transport
85+
* @param level The log level ('error' | 'debug' | 'info' | 'notice' | 'warning' | 'critical' | 'alert' | 'emergency')
86+
* @param data The data object to send (should be JSON serializable)
87+
* @param logger Optional logger name, defaults to "default"
88+
*/
89+
sendMessage(level: LoggingLevel, data: any, logger: string = 'mcp-remote') {
90+
return this.send({
91+
jsonrpc: '2.0',
92+
method: 'notifications/message',
93+
params: {
94+
level,
95+
logger,
96+
data,
97+
},
98+
})
99+
}
100+
}
101+
77102
/**
78103
* Type for the auth initialization function
79104
*/
@@ -100,9 +125,21 @@ export async function connectToRemoteServer(
100125
headers: Record<string, string>,
101126
authInitializer: AuthInitializer,
102127
transportStrategy: TransportStrategy = 'http-first',
128+
localTransport: StdioServerTransportExt | null = null,
103129
recursionReasons: Set<string> = new Set(),
104130
): Promise<Transport> {
105-
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
131+
const _log = (level: LoggingLevel, message: any, status: ConnStatus) => {
132+
// If localTransport is provided (proxy mode), send the message to it
133+
if (localTransport) {
134+
localTransport.sendMessage(level, {
135+
status,
136+
message,
137+
})
138+
}
139+
log(message)
140+
}
141+
142+
_log('info', `[${pid}] Connecting to remote server: ${serverUrl}`, 'connecting')
106143
const url = new URL(serverUrl)
107144

108145
// Create transport with eventSourceInit to pass Authorization header if present
@@ -122,7 +159,7 @@ export async function connectToRemoteServer(
122159
},
123160
}
124161

125-
log(`Using transport strategy: ${transportStrategy}`)
162+
_log('info', `Using transport strategy: ${transportStrategy}`, 'connecting')
126163
// Determine if we should attempt to fallback on error
127164
// Choose transport based on user strategy and recursion history
128165
const shouldAttemptFallback = transportStrategy === 'http-first' || transportStrategy === 'sse-first'
@@ -155,7 +192,7 @@ export async function connectToRemoteServer(
155192
await testClient.connect(testTransport)
156193
}
157194
}
158-
log(`Connected to remote server using ${transport.constructor.name}`)
195+
_log('info', `Connected to remote server using ${transport.constructor.name}`, 'connected')
159196

160197
return transport
161198
} catch (error) {
@@ -168,16 +205,16 @@ export async function connectToRemoteServer(
168205
error.message.includes('404') ||
169206
error.message.includes('Not Found'))
170207
) {
171-
log(`Received error: ${error.message}`)
208+
_log('error', `Received error: ${error.message}`, 'error')
172209

173210
// If we've already tried falling back once, throw an error
174211
if (recursionReasons.has(REASON_TRANSPORT_FALLBACK)) {
175212
const errorMessage = `Already attempted transport fallback. Giving up.`
176-
log(errorMessage)
213+
_log('error', errorMessage, 'error_final')
177214
throw new Error(errorMessage)
178215
}
179216

180-
log(`Recursively reconnecting for reason: ${REASON_TRANSPORT_FALLBACK}`)
217+
_log('info', `Recursively reconnecting for reason: ${REASON_TRANSPORT_FALLBACK}`, 'reconnecting')
181218

182219
// Add to recursion reasons set
183220
recursionReasons.add(REASON_TRANSPORT_FALLBACK)
@@ -190,45 +227,55 @@ export async function connectToRemoteServer(
190227
headers,
191228
authInitializer,
192229
sseTransport ? 'http-only' : 'sse-only',
230+
localTransport,
193231
recursionReasons,
194232
)
195233
} else if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
196-
log('Authentication required. Initializing auth...')
234+
_log('info', 'Authentication required. Initializing auth...', 'authenticating')
197235

198236
// Initialize authentication on-demand
199237
const { waitForAuthCode, skipBrowserAuth } = await authInitializer()
200238

201239
if (skipBrowserAuth) {
202-
log('Authentication required but skipping browser auth - using shared auth')
240+
_log('info', 'Authentication required but skipping browser auth - using shared auth', 'authenticating')
203241
} else {
204-
log('Authentication required. Waiting for authorization...')
242+
_log('info', 'Authentication required. Waiting for authorization...', 'authenticating')
205243
}
206244

207245
// Wait for the authorization code from the callback
208246
const code = await waitForAuthCode()
209247

210248
try {
211-
log('Completing authorization...')
249+
_log('info', 'Completing authorization...', 'authenticating')
212250
await transport.finishAuth(code)
213251

214252
if (recursionReasons.has(REASON_AUTH_NEEDED)) {
215253
const errorMessage = `Already attempted reconnection for reason: ${REASON_AUTH_NEEDED}. Giving up.`
216-
log(errorMessage)
254+
_log('error', errorMessage, 'error_final')
217255
throw new Error(errorMessage)
218256
}
219257

220258
// Track this reason for recursion
221259
recursionReasons.add(REASON_AUTH_NEEDED)
222-
log(`Recursively reconnecting for reason: ${REASON_AUTH_NEEDED}`)
260+
_log('info', `Recursively reconnecting for reason: ${REASON_AUTH_NEEDED}`, 'reconnecting')
223261

224262
// Recursively call connectToRemoteServer with the updated recursion tracking
225-
return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, recursionReasons)
263+
return connectToRemoteServer(
264+
client,
265+
serverUrl,
266+
authProvider,
267+
headers,
268+
authInitializer,
269+
transportStrategy,
270+
localTransport,
271+
recursionReasons,
272+
)
226273
} catch (authError) {
227-
log('Authorization error:', authError)
274+
_log('error', `Authorization error: ${authError}`, 'error_final')
228275
throw authError
229276
}
230277
} else {
231-
log('Connection error:', error)
278+
_log('error', `Connection error: ${error}`, 'error_final')
232279
throw error
233280
}
234281
}
@@ -488,19 +535,3 @@ export function setupSignalHandlers(cleanup: () => Promise<void>) {
488535
export function getServerUrlHash(serverUrl: string): string {
489536
return crypto.createHash('md5').update(serverUrl).digest('hex')
490537
}
491-
492-
/**
493-
* Helper function to send log messages through stdio MCP transport, for proxy logging purposes
494-
* @param transport The transport to send the message through
495-
* @param params Message parameters including level, logger name, and data
496-
*
497-
* In accordance with the official MCP specification
498-
* @see https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging#log-message-notifications
499-
*/
500-
export function sendLog(transport: Transport, params: MCPLogMessageParams) {
501-
return transport.send({
502-
jsonrpc: '2.0',
503-
method: 'notifications/message',
504-
params: { ...params },
505-
})
506-
}

src/proxy.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import { EventEmitter } from 'events'
13-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
13+
import { StdioServerTransportExt } from './lib/utils'
1414
import {
1515
connectToRemoteServer,
1616
log,
@@ -20,7 +20,6 @@ import {
2020
getServerUrlHash,
2121
MCP_REMOTE_VERSION,
2222
TransportStrategy,
23-
sendLog,
2423
} from './lib/utils'
2524
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
2625
import { createLazyAuthCoordinator } from './lib/coordination'
@@ -51,7 +50,7 @@ async function runProxy(
5150
})
5251

5352
// Create the STDIO transport for local connections
54-
const localTransport = new StdioServerTransport()
53+
const localTransport = new StdioServerTransportExt()
5554

5655
// Keep track of the server instance for cleanup
5756
let server: any = null
@@ -77,17 +76,17 @@ async function runProxy(
7776
}
7877
}
7978

80-
sendLog(localTransport, {
81-
level: 'debug',
82-
logger: 'mcp-remote',
83-
data: {
84-
message: 'Connecting to remote server...',
85-
},
86-
})
87-
8879
try {
8980
// Connect to remote server with lazy authentication
90-
const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy)
81+
const remoteTransport = await connectToRemoteServer(
82+
null,
83+
serverUrl,
84+
authProvider,
85+
headers,
86+
authInitializer,
87+
transportStrategy,
88+
localTransport,
89+
)
9190

9291
// Set up bidirectional proxy between local and remote transports
9392
mcpProxy({

0 commit comments

Comments
 (0)