Fix MCP message monitor layout and add collapsible sections
- Made MCP tab sections (Summary, Add Servers, Server List) collapsible to save space - Expanded sidebar width limit from 600px to 80% of window width for better debugging - Fixed message monitor overflow by establishing proper height constraint chain - Removed auto-scroll and show/hide toggle for message monitor - always visible now - Server list now has fixed height (max-h-60) with internal scroll - Message monitor takes remaining space with proper internal scrolling - Used h-full chain instead of flex-1 to establish explicit height constraints - Message monitor now properly scrolls within its container instead of at app level 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1ab5496e0a
commit
91608a0b97
2 changed files with 409 additions and 101 deletions
|
|
@ -18,6 +18,7 @@ import type {
|
|||
} from '@/types/mcp';
|
||||
import type { Tool } from '@/types/inference';
|
||||
import { normalizeServerName } from '@/utils/mcpUtils';
|
||||
import { DebugTransport } from './debugTransport';
|
||||
|
||||
interface MCPOAuthState {
|
||||
codeVerifier: string;
|
||||
|
|
@ -107,7 +108,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
|
|||
}
|
||||
|
||||
async saveClientInformation(clientInformation: OAuthClientInformation): Promise<void> {
|
||||
console.log('Registered OAuth client for MCP server');
|
||||
const serverKey = this.getServerKey();
|
||||
localStorage.setItem(`mcp_oauth_client_${serverKey}`, JSON.stringify(clientInformation));
|
||||
}
|
||||
|
|
@ -118,12 +118,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
|
|||
}
|
||||
|
||||
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
||||
console.log('OAuth tokens saved successfully');
|
||||
localStorage.setItem(`mcp_oauth_tokens_${this.connectionId}`, JSON.stringify(tokens));
|
||||
}
|
||||
|
||||
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
||||
console.log('🔐 Starting OAuth flow in popup window...');
|
||||
|
||||
// Open popup for OAuth flow
|
||||
const popup = window.open(
|
||||
|
|
@ -149,13 +147,10 @@ class MCPOAuthProvider implements OAuthClientProvider {
|
|||
popup.close();
|
||||
|
||||
if (event.data.error) {
|
||||
console.error('❌ OAuth authorization failed:', event.data.error);
|
||||
this.authError = event.data.error;
|
||||
} else if (event.data.code) {
|
||||
console.log('✅ OAuth authorization successful, exchanging code for tokens...');
|
||||
this.processAuthorizationCode(event.data.code);
|
||||
} else {
|
||||
console.error('❌ OAuth callback missing authorization code');
|
||||
this.authError = 'No authorization code received';
|
||||
}
|
||||
};
|
||||
|
|
@ -206,7 +201,6 @@ class MCPOAuthProvider implements OAuthClientProvider {
|
|||
});
|
||||
|
||||
if (result === 'AUTHORIZED') {
|
||||
console.log('🎉 OAuth authentication completed! Connecting to MCP server...');
|
||||
this.notifyOAuthComplete();
|
||||
} else {
|
||||
console.error('❌ OAuth token exchange failed');
|
||||
|
|
@ -241,6 +235,7 @@ export class MCPConnectionManager {
|
|||
private healthCheckInterval?: NodeJS.Timeout;
|
||||
private oauthProvider?: MCPOAuthProvider;
|
||||
private onConnectionUpdate?: () => void;
|
||||
private onMessage?: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
|
||||
|
||||
constructor(id: string, config: MCPServerConfig) {
|
||||
this.connection = {
|
||||
|
|
@ -275,6 +270,12 @@ export class MCPConnectionManager {
|
|||
this.onConnectionUpdate = callback;
|
||||
}
|
||||
|
||||
// Set callback for message monitoring
|
||||
setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
|
||||
console.log(`Setting message callback for connection ${this.connection.id}`); // Debug log
|
||||
this.onMessage = callback;
|
||||
}
|
||||
|
||||
// Notify about connection state changes
|
||||
private notifyConnectionUpdate(): void {
|
||||
if (this.onConnectionUpdate) {
|
||||
|
|
@ -287,19 +288,6 @@ export class MCPConnectionManager {
|
|||
this.connection.error = undefined;
|
||||
|
||||
try {
|
||||
// Validate URL format
|
||||
console.log('Validating URL:', this.connection.url);
|
||||
try {
|
||||
const url = new URL(this.connection.url);
|
||||
console.log('URL parsed successfully:', {
|
||||
protocol: url.protocol,
|
||||
hostname: url.hostname,
|
||||
pathname: url.pathname,
|
||||
port: url.port
|
||||
});
|
||||
} catch (urlError) {
|
||||
throw new Error(`Invalid URL format: ${this.connection.url}`);
|
||||
}
|
||||
|
||||
// Clear any existing connections
|
||||
await this.disconnect();
|
||||
|
|
@ -343,7 +331,6 @@ export class MCPConnectionManager {
|
|||
if (this.connection.config.autoReconnect !== false &&
|
||||
this.connection.connectionAttempts < (this.connection.config.maxReconnectAttempts || 5)) {
|
||||
const delay = this.getBackoffDelay();
|
||||
console.log(`Scheduling reconnect attempt ${this.connection.connectionAttempts} in ${delay}ms`);
|
||||
this.reconnectTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await this.reconnect();
|
||||
|
|
@ -397,14 +384,11 @@ export class MCPConnectionManager {
|
|||
throw new Error('OAuth provider not initialized');
|
||||
}
|
||||
|
||||
console.log('Processing OAuth callback with authorization code...');
|
||||
|
||||
try {
|
||||
// If we have an active transport, use its finishAuth method
|
||||
if (this.transport && typeof (this.transport as any).finishAuth === 'function') {
|
||||
console.log('Calling transport.finishAuth...');
|
||||
await (this.transport as any).finishAuth(authorizationCode);
|
||||
console.log('OAuth authorization completed via transport');
|
||||
|
||||
// Now attempt to connect - the transport should be authenticated
|
||||
await this.connect();
|
||||
|
|
@ -412,11 +396,9 @@ export class MCPConnectionManager {
|
|||
// If no transport yet, store the authorization code and try connecting
|
||||
// The transport creation will handle the auth flow
|
||||
this.oauthProvider.pendingAuthorizationCode = authorizationCode;
|
||||
console.log('Stored authorization code, attempting connection...');
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
console.log('OAuth callback processed successfully');
|
||||
} catch (error) {
|
||||
console.error('OAuth callback processing failed:', error);
|
||||
throw new Error(`OAuth callback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
|
|
@ -450,7 +432,6 @@ export class MCPConnectionManager {
|
|||
|
||||
// Handle successful OAuth completion
|
||||
private async handleOAuthSuccess(): Promise<void> {
|
||||
console.log('OAuth completed successfully, retrying connection...');
|
||||
|
||||
try {
|
||||
// Reset connection state and retry
|
||||
|
|
@ -461,7 +442,6 @@ export class MCPConnectionManager {
|
|||
// Attempt to connect now that we have valid tokens
|
||||
await this.connect();
|
||||
|
||||
console.log('Post-OAuth connection successful!');
|
||||
this.notifyConnectionUpdate();
|
||||
} catch (error) {
|
||||
console.error('Post-OAuth connection failed:', error);
|
||||
|
|
@ -474,7 +454,6 @@ export class MCPConnectionManager {
|
|||
|
||||
private async tryStreamableHttp(): Promise<void> {
|
||||
try {
|
||||
console.log('Attempting StreamableHTTP connection to:', this.connection.url);
|
||||
|
||||
// Create transport options with OAuth provider if configured
|
||||
const transportOptions: any = {};
|
||||
|
|
@ -485,7 +464,6 @@ export class MCPConnectionManager {
|
|||
|
||||
const transport = new StreamableHTTPClientTransport(new URL(this.connection.url), transportOptions);
|
||||
await this.initializeClient(transport);
|
||||
console.log('StreamableHTTP connection successful');
|
||||
} catch (error) {
|
||||
console.error('StreamableHTTP connection failed:', error);
|
||||
throw error;
|
||||
|
|
@ -494,7 +472,6 @@ export class MCPConnectionManager {
|
|||
|
||||
private async trySSE(): Promise<void> {
|
||||
try {
|
||||
console.log('Attempting SSE connection to:', this.connection.url);
|
||||
|
||||
// Create transport options with OAuth provider if configured
|
||||
const transportOptions: any = {};
|
||||
|
|
@ -504,25 +481,17 @@ export class MCPConnectionManager {
|
|||
}
|
||||
|
||||
const transport = new SSEClientTransport(new URL(this.connection.url), transportOptions);
|
||||
console.log('SSE transport created, attempting client connection...');
|
||||
await this.initializeClient(transport);
|
||||
console.log('SSE connection successful');
|
||||
} catch (error) {
|
||||
console.error('SSE connection failed:', error);
|
||||
console.error('Error details:', {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
url: this.connection.url
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeClient(transport: Transport): Promise<void> {
|
||||
try {
|
||||
console.log('Initializing MCP client...');
|
||||
this.transport = transport;
|
||||
this.transport.onmessage = console.log.bind(console, 'MCP Client message received:');
|
||||
const debugTransport = new DebugTransport(transport);
|
||||
this.transport = debugTransport;
|
||||
|
||||
this.client = new Client(
|
||||
{
|
||||
name: 'example-remote-client',
|
||||
|
|
@ -532,19 +501,23 @@ export class MCPConnectionManager {
|
|||
capabilities: {},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Connecting client to transport...');
|
||||
console.log('Transport type:', transport.constructor.name);
|
||||
console.log('Transport details:', transport);
|
||||
|
||||
await this.client.connect(transport);
|
||||
// Set up message callbacks to broadcast to UI after client is created
|
||||
debugTransport.onsendmessage_ = async (message, options) => {
|
||||
if (this.onMessage && this.client) {
|
||||
this.onMessage(this.connection.id, this.client, message, 'sent', { options });
|
||||
}
|
||||
};
|
||||
|
||||
debugTransport.onreceivemessage_ = (message, extra) => {
|
||||
if (this.onMessage && this.client) {
|
||||
this.onMessage(this.connection.id, this.client, message, 'received', extra);
|
||||
}
|
||||
};
|
||||
|
||||
await this.client.connect(debugTransport);
|
||||
this.connection.client = this.client;
|
||||
console.log('Client connected successfully');
|
||||
} catch (error) {
|
||||
console.error('Client initialization failed:', error);
|
||||
console.error('Error type:', error?.constructor?.name);
|
||||
console.error('Error message:', error instanceof Error ? error.message : error);
|
||||
console.error('Full error object:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -683,7 +656,6 @@ export class MCPConnectionManager {
|
|||
await this.performHealthCheck();
|
||||
}, 30000);
|
||||
|
||||
console.log(`Started health check monitoring for ${this.connection.name}`);
|
||||
}
|
||||
|
||||
private async performHealthCheck(): Promise<void> {
|
||||
|
|
@ -695,7 +667,6 @@ export class MCPConnectionManager {
|
|||
try {
|
||||
// Try to list tools as a health check - this is a lightweight operation
|
||||
await this.client.listTools();
|
||||
console.log(`Health check passed for ${this.connection.name}`);
|
||||
} catch (error) {
|
||||
console.warn(`Health check failed for ${this.connection.name}:`, error);
|
||||
await this.handleHealthCheckFailure(error);
|
||||
|
|
@ -703,7 +674,6 @@ export class MCPConnectionManager {
|
|||
}
|
||||
|
||||
private async handleHealthCheckFailure(error: any): Promise<void> {
|
||||
console.log(`Connection health check failed for ${this.connection.name}, attempting reconnection...`);
|
||||
|
||||
// Stop health check during reconnection attempt
|
||||
if (this.healthCheckInterval) {
|
||||
|
|
@ -719,7 +689,6 @@ export class MCPConnectionManager {
|
|||
try {
|
||||
// Attempt reconnection
|
||||
await this.connect();
|
||||
console.log(`Health check reconnection successful for ${this.connection.name}`);
|
||||
} catch (reconnectError) {
|
||||
console.error(`Health check reconnection failed for ${this.connection.name}:`, reconnectError);
|
||||
// The connect method will handle scheduling retry attempts
|
||||
|
|
|
|||
63
src/mcp/debugTransport.ts
Normal file
63
src/mcp/debugTransport.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
||||
import type { Transport, TransportSendOptions } from "@modelcontextprotocol/sdk/shared/transport.d.ts";
|
||||
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
|
||||
/**
|
||||
* Debug transport for MCP that logs messages to the console.
|
||||
* This is useful for debugging purposes and does not implement any actual transport logic.
|
||||
*/
|
||||
export class DebugTransport implements Transport {
|
||||
|
||||
// Allow observability of the transport lifecycle
|
||||
onclose_?: () => void;
|
||||
onerror_?: (error: Error) => void;
|
||||
onreceivemessage_?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
|
||||
onsendmessage_?: (message: JSONRPCMessage, options?: TransportSendOptions) => Promise<void>;
|
||||
|
||||
private innerTransport: Transport;
|
||||
constructor(innerTransport: Transport) {
|
||||
this.innerTransport = innerTransport;
|
||||
this.innerTransport.onclose = () => {
|
||||
if (this.onclose_) {
|
||||
this.onclose_();
|
||||
}
|
||||
if (this.onclose) {
|
||||
this.onclose();
|
||||
}
|
||||
};
|
||||
this.innerTransport.onerror = (error: Error) => {
|
||||
if (this.onerror_) {
|
||||
this.onerror_(error);
|
||||
}
|
||||
if (this.onerror) {
|
||||
this.onerror(error);
|
||||
}
|
||||
}
|
||||
this.innerTransport.onmessage = (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => {
|
||||
if (this.onreceivemessage_) {
|
||||
this.onreceivemessage_(message, extra);
|
||||
}
|
||||
if (this.onmessage) {
|
||||
this.onmessage(message, extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
async start(): Promise<void> {
|
||||
await this.innerTransport.start();
|
||||
}
|
||||
async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
|
||||
if (this.onsendmessage_) {
|
||||
await this.onsendmessage_(message, options);
|
||||
}
|
||||
await this.innerTransport.send(message, options);
|
||||
}
|
||||
async close(): Promise<void> {
|
||||
await this.innerTransport.close();
|
||||
}
|
||||
|
||||
// These are taken over by the MCP client
|
||||
onclose?: () => void;
|
||||
onerror?: (error: Error) => void;
|
||||
onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
|
||||
}
|
||||
Loading…
Reference in a new issue