mirror of
https://github.com/wassname/HumanAgent-MCP.git
synced 2026-10-04 12:10:11 +08:00
Fix SSE connection conflicts between webview and extension
- Added separate /mcp-tools endpoint for VS Code extension MCP protocol - Maintained /mcp endpoint for webview SSE connections (heartbeats, messages) - Eliminated competing SSE connections that caused message delivery failures - Enhanced SSE connection health monitoring with detailed diagnostics - Added connection state management to prevent race conditions - Fixed sendToSession() to use proper health checking instead of raw writes - Improved logging to track connection conflicts and message delivery - Maintains proper session separation between different VS Code workspaces This resolves the issue where VS Code webview received heartbeats but not tool call messages due to competing SSE connections overwriting each other.
This commit is contained in:
1 parent
2de7ad2db1
commit
60ea857b54
10 files changed
+585
-1604
No files matched your search
+133
-120
@@ -21,10 +21,11 @@ class HumanAgentMcpProvider implements vscode.McpServerDefinitionProvider {
|
||||
constructor(private sessionId: string) {}
|
||||
|
||||
provideMcpServerDefinitions(token: vscode.CancellationToken): vscode.ProviderResult<vscode.McpHttpServerDefinition[]> {
|
||||
// Return our HumanAgent MCP server definition with current sessionId
|
||||
const serverUrl = `http://127.0.0.1:3737/mcp?sessionId=${this.sessionId}`;
|
||||
// Use separate endpoint for MCP tools to avoid SSE conflicts with webview
|
||||
const serverUrl = `http://127.0.0.1:3737/mcp-tools?sessionId=${this.sessionId}`;
|
||||
const serverUri = vscode.Uri.parse(serverUrl);
|
||||
const server = new vscode.McpHttpServerDefinition('HumanAgent MCP', serverUri);
|
||||
console.log('HumanAgent MCP: Using separate MCP tools endpoint to avoid SSE conflicts');
|
||||
return [server];
|
||||
}
|
||||
|
||||
@@ -144,8 +145,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
serverManager = ServerManager.getInstance(serverOptions);
|
||||
|
||||
// Auto-detect and start standalone MCP server if already configured
|
||||
await autoStartMcpServer(mcpConfigManager, workspaceSessionId);
|
||||
// Auto-start server and register session (no mcp.json dependency)
|
||||
await ensureServerAndRegisterSession(workspaceSessionId);
|
||||
|
||||
// Restore the persisted session name after server is running (with retry)
|
||||
setTimeout(async () => {
|
||||
@@ -191,9 +192,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Create dedicated status command
|
||||
const showStatusCommand = vscode.commands.registerCommand('humanagent-mcp.showStatus', async () => {
|
||||
const isWorkspaceRegistered = mcpConfigManager?.isMcpServerRegistered(false) ?? false;
|
||||
const isGlobalRegistered = mcpConfigManager?.isMcpServerRegistered(true) ?? false;
|
||||
|
||||
// Get detailed server status
|
||||
const serverStatus = await serverManager.getServerStatus();
|
||||
|
||||
@@ -204,8 +202,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
`- Port: ${serverStatus.port}\n` +
|
||||
`- Host: ${serverStatus.host}\n` +
|
||||
`- Session: ${workspaceSessionId}\n` +
|
||||
`- Workspace registration: ${isWorkspaceRegistered ? '✅' : '❌'}\n` +
|
||||
`- Global registration: ${isGlobalRegistered ? '✅' : '❌'}`
|
||||
`- Registration: Native Provider ✅`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -252,16 +249,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
const configureMcpCommand = vscode.commands.registerCommand('humanagent-mcp.configureMcp', async () => {
|
||||
// Refresh state checks each time the command is executed
|
||||
const hasWorkspace = mcpConfigManager?.hasWorkspace() ?? false;
|
||||
const isWorkspaceRegistered = mcpConfigManager?.isMcpServerRegistered(false) ?? false;
|
||||
const isGlobalRegistered = mcpConfigManager?.isMcpServerRegistered(true) ?? false;
|
||||
|
||||
console.log(`HumanAgent MCP: Configure command - hasWorkspace: ${hasWorkspace}, workspaceRegistered: ${isWorkspaceRegistered}, globalRegistered: ${isGlobalRegistered}`);
|
||||
|
||||
const options = [];
|
||||
|
||||
// Server management options
|
||||
// Server management options only (registration handled automatically by native provider)
|
||||
const serverStatus = await serverManager.getServerStatus();
|
||||
if (serverStatus.isRunning) {
|
||||
options.push('🔴 Stop Server');
|
||||
@@ -270,33 +260,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
options.push('▶️ Start Server');
|
||||
}
|
||||
|
||||
if (hasWorkspace) {
|
||||
if (isWorkspaceRegistered) {
|
||||
options.push('🗑️ Unregister from This Workspace');
|
||||
console.log('HumanAgent MCP: Added workspace UNREGISTER option');
|
||||
} else {
|
||||
options.push('📝 Register for This Workspace');
|
||||
console.log('HumanAgent MCP: Added workspace REGISTER option');
|
||||
}
|
||||
}
|
||||
|
||||
if (isGlobalRegistered) {
|
||||
options.push('🗑️ Unregister Globally');
|
||||
console.log('HumanAgent MCP: Added global UNREGISTER option');
|
||||
} else {
|
||||
options.push('🌐 Register Globally');
|
||||
console.log('HumanAgent MCP: Added global REGISTER option');
|
||||
}
|
||||
|
||||
if (hasWorkspace) {
|
||||
options.push('📄 Open Workspace Configuration');
|
||||
}
|
||||
options.push('📊 Show Status');
|
||||
|
||||
console.log(`HumanAgent MCP: Final options: ${options.join(', ')}`);
|
||||
|
||||
const action = await vscode.window.showQuickPick(options, {
|
||||
placeHolder: 'Choose MCP Server configuration action:'
|
||||
placeHolder: 'Choose MCP Server action:'
|
||||
});
|
||||
|
||||
if (!action) {
|
||||
@@ -314,36 +281,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
case '🔄 Restart Server':
|
||||
await vscode.commands.executeCommand('humanagent-mcp.restartServer');
|
||||
break;
|
||||
case '📝 Register for This Workspace':
|
||||
await mcpConfigManager!.ensureMcpServerRegistered(false, workspaceSessionId);
|
||||
vscode.window.showInformationMessage('MCP server registered for this workspace! Restart VS Code to enable Copilot integration.');
|
||||
break;
|
||||
case '🌐 Register Globally':
|
||||
await mcpConfigManager!.ensureMcpServerRegistered(true, workspaceSessionId);
|
||||
vscode.window.showInformationMessage('MCP server registered globally! Restart VS Code to enable Copilot integration.');
|
||||
break;
|
||||
case '🗑️ Unregister from This Workspace':
|
||||
await mcpConfigManager!.removeMcpServerRegistration(false);
|
||||
vscode.window.showInformationMessage('MCP server unregistered from this workspace. Restart VS Code to apply changes.');
|
||||
break;
|
||||
case '🗑️ Unregister Globally':
|
||||
await mcpConfigManager!.removeMcpServerRegistration(true);
|
||||
vscode.window.showInformationMessage('MCP server unregistered globally. Restart VS Code to apply changes.');
|
||||
break;
|
||||
case '📄 Open Workspace Configuration':
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspaceRoot) {
|
||||
const configPath = vscode.Uri.file(workspaceRoot + '/.vscode/mcp.json');
|
||||
vscode.commands.executeCommand('vscode.open', configPath);
|
||||
}
|
||||
break;
|
||||
case '📊 Show Status':
|
||||
// Call the dedicated status command
|
||||
vscode.commands.executeCommand('humanagent-mcp.showStatus');
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`MCP configuration failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
vscode.window.showErrorMessage(`Server action failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -364,37 +307,38 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
//vscode.window.showInformationMessage('HumanAgent MCP extension activated successfully!');
|
||||
}
|
||||
|
||||
// Auto-detect MCP server configuration and guide user if needed
|
||||
async function autoStartMcpServer(configManager: McpConfigManager, sessionId: string): Promise<void> {
|
||||
// Simplified server startup and session registration (no mcp.json dependency)
|
||||
async function ensureServerAndRegisterSession(sessionId: string): Promise<void> {
|
||||
try {
|
||||
// Check for workspace configuration first (higher priority)
|
||||
if (configManager.isMcpServerRegistered(false)) {
|
||||
console.log(`HumanAgent MCP: Found workspace configuration for session ${sessionId}, checking server status...`);
|
||||
await ensureServerAccessibleAndRegister(sessionId, 'workspace');
|
||||
return;
|
||||
}
|
||||
console.log(`HumanAgent MCP: Starting server and registering session ${sessionId}...`);
|
||||
|
||||
// Check for global configuration
|
||||
if (configManager.isMcpServerRegistered(true)) {
|
||||
console.log(`HumanAgent MCP: Found global configuration for session ${sessionId}, checking server status...`);
|
||||
await ensureServerAccessibleAndRegister(sessionId, 'global');
|
||||
return;
|
||||
}
|
||||
|
||||
// No configuration found - guide user to setup
|
||||
console.log(`HumanAgent MCP: No MCP configuration found for session ${sessionId}`);
|
||||
vscode.window.showInformationMessage(
|
||||
'HumanAgent MCP Server not configured. Use the Configure MCP command to set up the server.',
|
||||
'Configure Now'
|
||||
).then(selection => {
|
||||
if (selection === 'Configure Now') {
|
||||
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
|
||||
// Check if server is accessible, if not start it
|
||||
const serverAccessible = await isServerAccessible();
|
||||
if (!serverAccessible) {
|
||||
console.log('HumanAgent MCP: Server not accessible, starting server...');
|
||||
const serverStarted = await serverManager.ensureServerRunning();
|
||||
if (!serverStarted) {
|
||||
console.error('HumanAgent MCP: Failed to start server');
|
||||
vscode.window.showWarningMessage('HumanAgent MCP Server could not be started. Some features may not work.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
console.log('HumanAgent MCP: Server started successfully');
|
||||
}
|
||||
|
||||
// Register session with the server
|
||||
const sessionExists = await validateSessionWithServer(sessionId);
|
||||
if (!sessionExists) {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} not found on server, registering new session...`);
|
||||
await registerSessionWithStandaloneServer(sessionId, false);
|
||||
} else {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} exists on server, re-registering with override data...`);
|
||||
await registerSessionWithStandaloneServer(sessionId, true);
|
||||
}
|
||||
console.log(`HumanAgent MCP: Session registration complete for ${sessionId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('HumanAgent MCP: Failed to check server configuration:', error);
|
||||
vscode.window.showErrorMessage('Failed to check HumanAgent MCP Server configuration');
|
||||
console.error('HumanAgent MCP: Failed to start server or register session:', error);
|
||||
vscode.window.showWarningMessage('HumanAgent MCP Server could not be initialized. Please check the server status and try reloading the workspace.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +358,7 @@ async function isPortInUse(port: number): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if MCP server is accessible and responding
|
||||
// Check if MCP server is accessible and responding with retry
|
||||
async function isServerAccessible(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:3737/sessions', {
|
||||
@@ -423,12 +367,30 @@ async function isServerAccessible(): Promise<boolean> {
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.log('HumanAgent MCP: Server accessibility check failed:', error);
|
||||
console.log('HumanAgent MCP: Server accessibility check failed, retrying in 3 seconds...', error);
|
||||
|
||||
// Wait 3 seconds and try once more
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
try {
|
||||
const retryResponse = await fetch('http://127.0.0.1:3737/sessions', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (retryResponse.ok) {
|
||||
console.log('HumanAgent MCP: Server accessible on retry');
|
||||
return true;
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.log('HumanAgent MCP: Server accessibility retry failed:', retryError);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if session exists on server by testing a simple MCP call
|
||||
// Check if session exists on server by testing a simple MCP call with retry
|
||||
async function validateSessionWithServer(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:3737/mcp', {
|
||||
@@ -449,49 +411,100 @@ async function validateSessionWithServer(sessionId: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validation failed:`, error);
|
||||
return false;
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validation failed, retrying in 3 seconds...`, error);
|
||||
|
||||
// Wait 3 seconds and try once more
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
try {
|
||||
const retryResponse = await fetch('http://127.0.0.1:3737/mcp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
method: 'tools/list'
|
||||
})
|
||||
});
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const result = await retryResponse.json() as any;
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validated on server (retry)`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} not found on server (${retryResponse.status}) (retry)`);
|
||||
return false;
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validation retry failed:`, retryError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register session with standalone server via HTTP (always sends override data)
|
||||
// Register session with standalone server via HTTP (always sends override data) with retry
|
||||
async function registerSessionWithStandaloneServer(sessionId: string, forceReregister: boolean = false): Promise<void> {
|
||||
try {
|
||||
// Read workspace override file if it exists
|
||||
let overrideData = null;
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspaceRoot) {
|
||||
const overrideFilePath = path.join(workspaceRoot, '.vscode', 'HumanAgentOverride.json');
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(overrideFilePath)) {
|
||||
const overrideContent = fs.readFileSync(overrideFilePath, 'utf8');
|
||||
overrideData = JSON.parse(overrideContent);
|
||||
console.log(`HumanAgent MCP: Loaded override data for session ${sessionId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`HumanAgent MCP: Error reading override file:`, error);
|
||||
// Read workspace override file if it exists
|
||||
let overrideData = null;
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspaceRoot) {
|
||||
const overrideFilePath = path.join(workspaceRoot, '.vscode', 'HumanAgentOverride.json');
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(overrideFilePath)) {
|
||||
const overrideContent = fs.readFileSync(overrideFilePath, 'utf8');
|
||||
overrideData = JSON.parse(overrideContent);
|
||||
console.log(`HumanAgent MCP: Loaded override data for session ${sessionId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`HumanAgent MCP: Error reading override file:`, error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
sessionId,
|
||||
overrideData: overrideData,
|
||||
forceReregister: forceReregister
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:3737/sessions/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
overrideData: overrideData,
|
||||
forceReregister: forceReregister
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json() as any;
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} registered successfully. Total sessions: ${result.totalSessions}`);
|
||||
return;
|
||||
} else {
|
||||
console.error(`HumanAgent MCP: Failed to register session ${sessionId}: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`HumanAgent MCP: Error registering session ${sessionId}:`, error);
|
||||
console.error(`HumanAgent MCP: Error registering session ${sessionId}, retrying in 3 seconds...`, error);
|
||||
|
||||
// Wait 3 seconds and try once more
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
try {
|
||||
const retryResponse = await fetch('http://127.0.0.1:3737/sessions/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const result = await retryResponse.json() as any;
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} registered successfully on retry. Total sessions: ${result.totalSessions}`);
|
||||
return;
|
||||
} else {
|
||||
console.error(`HumanAgent MCP: Failed to register session ${sessionId} on retry: ${retryResponse.status}`);
|
||||
throw new Error(`Registration failed: ${retryResponse.status}`);
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error(`HumanAgent MCP: Session registration retry failed:`, retryError);
|
||||
throw new Error(`Registration failed after retry: ${retryError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,15 +68,10 @@ export class McpConfigManager {
|
||||
|
||||
// Use the extension path passed during construction
|
||||
|
||||
// Configure our MCP server (HTTP transport)
|
||||
const mcpUrl = sessionId ? `http://127.0.0.1:3737/mcp?sessionId=${sessionId}` : 'http://127.0.0.1:3737/mcp';
|
||||
// Configure our MCP server (HTTP transport) - no sessionId in URL
|
||||
const serverConfig: any = {
|
||||
type: 'http',
|
||||
url: mcpUrl,
|
||||
notifications: {
|
||||
enableSound: true,
|
||||
enableFlashing: true
|
||||
}
|
||||
url: 'http://127.0.0.1:3737/mcp'
|
||||
};
|
||||
|
||||
// Add our server to the config
|
||||
@@ -100,15 +95,10 @@ export class McpConfigManager {
|
||||
try {
|
||||
// Use the extension path passed during construction
|
||||
|
||||
// Configure our MCP server (HTTP transport)
|
||||
const mcpUrl = sessionId ? `http://127.0.0.1:3737/mcp?sessionId=${sessionId}` : 'http://127.0.0.1:3737/mcp';
|
||||
// Configure our MCP server (HTTP transport) - no sessionId in URL
|
||||
const serverConfig: any = {
|
||||
type: 'http',
|
||||
url: mcpUrl,
|
||||
notifications: {
|
||||
enableSound: true,
|
||||
enableFlashing: true
|
||||
}
|
||||
url: 'http://127.0.0.1:3737/mcp'
|
||||
};
|
||||
|
||||
// Get current global MCP servers configuration
|
||||
|
||||
+293
-100
@@ -17,8 +17,8 @@ class DebugLogger {
|
||||
|
||||
constructor(workspaceRoot?: string) {
|
||||
// Check environment variables for logging configuration
|
||||
this.loggingEnabled = process.env.HUMANAGENT_LOGGING_ENABLED === 'true';
|
||||
this.loggingLevel = process.env.HUMANAGENT_LOGGING_LEVEL || 'INFO';
|
||||
this.loggingEnabled = process.env.HUMANAGENT_LOGGING_ENABLED === 'true' || true; // Enable by default for debugging
|
||||
this.loggingLevel = process.env.HUMANAGENT_LOGGING_LEVEL || 'DEBUG'; // Debug level by default
|
||||
|
||||
// If logging is disabled, just log to console for important messages
|
||||
if (!this.loggingEnabled) {
|
||||
@@ -27,20 +27,9 @@ class DebugLogger {
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine log path based on workspace or fallback to temp directory
|
||||
if (workspaceRoot) {
|
||||
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
||||
this.logPath = path.join(vscodeDir, 'HumanAgent-server.log');
|
||||
|
||||
// Ensure .vscode directory exists
|
||||
if (!fs.existsSync(vscodeDir)) {
|
||||
fs.mkdirSync(vscodeDir, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
// Fallback for standalone server or when no workspace available
|
||||
const tempDir = os.tmpdir();
|
||||
this.logPath = path.join(tempDir, 'HumanAgent-server.log');
|
||||
}
|
||||
// Always log to system temp directory - server is workspace-independent
|
||||
const tempDir = os.tmpdir();
|
||||
this.logPath = path.join(tempDir, 'HumanAgent-server.log');
|
||||
|
||||
console.log(`[LOGGER] Attempting to create log file at: ${this.logPath}`);
|
||||
|
||||
@@ -134,9 +123,9 @@ export class McpServer extends EventEmitter {
|
||||
private requestResolvers: Map<string, { resolve: (response: string) => void; reject: (error: Error) => void }> = new Map();
|
||||
private activeSessions: Set<string> = new Set();
|
||||
private sseConnections: Set<http.ServerResponse> = new Set();
|
||||
private conversationToSession: Map<string, string> = new Map(); // Map VS Code conversation IDs to registered session IDs
|
||||
private chatManager: ChatManager; // Centralized chat and session management
|
||||
private sseClients: Map<string, http.ServerResponse> = new Map(); // Per-session SSE connections
|
||||
private sseClients: Map<string, http.ServerResponse> = new Map(); // Per-session SSE connections (VS Code webviews)
|
||||
private webInterfaceConnections: Set<http.ServerResponse> = new Set(); // Web interface connections (all browsers)
|
||||
|
||||
constructor(private sessionId?: string, private workspacePath?: string) {
|
||||
super();
|
||||
@@ -169,32 +158,62 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private setupEventForwarding(): void {
|
||||
this.on('request-state-change', (data) => {
|
||||
this.debugLogger.log('SSE', 'Forwarding request-state-change to SSE connections');
|
||||
this.broadcastToSSE('request-state-change', data);
|
||||
this.debugLogger.log('SSE', 'Forwarding request-state-change to target session and web interface');
|
||||
this.sendToSessionAndWeb(data.sessionId, 'request-state-change', data);
|
||||
});
|
||||
}
|
||||
|
||||
private broadcastToSSE(eventType: string, data: any): void {
|
||||
// Send event to web interface connections only
|
||||
private sendToWebInterface(eventType: string, data: any): void {
|
||||
const message = JSON.stringify({ type: eventType, data });
|
||||
const eventData = `data: ${message}\n\n`;
|
||||
|
||||
this.debugLogger.log('SSE', `Broadcasting to ${this.sseConnections.size} SSE connections:`, message);
|
||||
this.debugLogger.log('SSE', `Sending to ${this.webInterfaceConnections.size} web interface connections:`, message);
|
||||
|
||||
// Send to all active SSE connections
|
||||
for (const connection of this.sseConnections) {
|
||||
for (const connection of this.webInterfaceConnections) {
|
||||
if (!connection.destroyed) {
|
||||
try {
|
||||
connection.write(eventData);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('SSE', 'Failed to write to SSE connection:', error);
|
||||
this.debugLogger.log('SSE', 'Failed to write to web interface connection:', error);
|
||||
this.webInterfaceConnections.delete(connection);
|
||||
this.sseConnections.delete(connection);
|
||||
}
|
||||
} else {
|
||||
this.webInterfaceConnections.delete(connection);
|
||||
this.sseConnections.delete(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send event to specific session only
|
||||
private sendToSession(sessionId: string, eventType: string, data: any): void {
|
||||
const message = { type: eventType, data };
|
||||
|
||||
const sessionConnection = this.sseClients.get(sessionId);
|
||||
if (!sessionConnection) {
|
||||
this.debugLogger.log('WARN', `❌ No SSE connection found for session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: Compare connection objects to see if they match the heartbeat connection
|
||||
this.debugLogger.log('SSE', `🔍 Sending to session ${sessionId} - Connection destroyed: ${sessionConnection.destroyed}, writable: ${sessionConnection.writable}`);
|
||||
|
||||
// Use the enhanced sendSSEMessage method with health checking
|
||||
this.sendSSEMessage(sessionConnection, message);
|
||||
this.debugLogger.log('SSE', `Sent to session ${sessionId}:`, JSON.stringify(message));
|
||||
}
|
||||
|
||||
// Send event to specific session AND web interface
|
||||
private sendToSessionAndWeb(sessionId: string, eventType: string, data: any): void {
|
||||
// Send to specific session
|
||||
this.sendToSession(sessionId, eventType, data);
|
||||
// Also send to web interface
|
||||
this.sendToWebInterface(eventType, data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private sendMcpNotification(method: string, params?: any, sessionId?: string): void {
|
||||
this.debugLogger.log('MCP', `Sending SSE notification: ${method}`, params);
|
||||
|
||||
@@ -229,11 +248,27 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private sendSSEMessage(response: http.ServerResponse, message: any): void {
|
||||
try {
|
||||
// Check connection health before sending
|
||||
if (response.destroyed) {
|
||||
this.debugLogger.log('ERROR', `Cannot send SSE message: connection is destroyed`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.writable) {
|
||||
this.debugLogger.log('ERROR', `Cannot send SSE message: connection is not writable`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.stringify(message);
|
||||
response.write(`data: ${data}\n\n`);
|
||||
this.debugLogger.log('SSE', `Sent SSE message: ${message.method || 'response'}`);
|
||||
const success = response.write(`data: ${data}\n\n`);
|
||||
|
||||
if (success) {
|
||||
this.debugLogger.log('SSE', `✅ Successfully sent SSE message: ${message.type || message.method || 'response'}`);
|
||||
} else {
|
||||
this.debugLogger.log('ERROR', `❌ Failed to write SSE message (buffer full): ${message.type || message.method || 'response'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', `Failed to send SSE message:`, error);
|
||||
this.debugLogger.log('ERROR', `❌ Exception sending SSE message:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,10 +292,7 @@ export class McpServer extends EventEmitter {
|
||||
type: 'string',
|
||||
description: 'Optional context or background information for the human agent'
|
||||
},
|
||||
sessionId: {
|
||||
type: 'string',
|
||||
description: 'Optional specific session ID to use. If not provided, a new session will be created.'
|
||||
},
|
||||
|
||||
priority: {
|
||||
type: 'string',
|
||||
enum: ['low', 'normal', 'high', 'urgent'],
|
||||
@@ -512,7 +544,11 @@ export class McpServer extends EventEmitter {
|
||||
const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
|
||||
|
||||
if (reqUrl.pathname === '/mcp') {
|
||||
// Main MCP protocol endpoint
|
||||
// Main MCP protocol endpoint (webview SSE)
|
||||
} else if (reqUrl.pathname === '/mcp-tools') {
|
||||
// MCP tools endpoint (extension only, no SSE conflicts)
|
||||
await this.handleMcpToolsEndpoint(req, res, reqUrl);
|
||||
return;
|
||||
} else if (req.url === '/HumanAgent') {
|
||||
// Web interface for multi-session chat
|
||||
await this.handleWebInterface(req, res);
|
||||
@@ -626,22 +662,31 @@ export class McpServer extends EventEmitter {
|
||||
this.debugLogger.log('SSE', 'Headers:', req.headers);
|
||||
this.debugLogger.log('SSE', 'URL:', req.url);
|
||||
|
||||
// Extract sessionId from query params in URL or Mcp-Session-Id header
|
||||
// Extract sessionId and clientType from query params in URL or headers
|
||||
const url = new URL(req.url!, `http://${req.headers.host}`);
|
||||
let sessionId = url.searchParams.get('sessionId');
|
||||
const clientType = url.searchParams.get('clientType');
|
||||
|
||||
// If not in URL, try the Mcp-Session-Id header (per MCP spec)
|
||||
if (!sessionId) {
|
||||
sessionId = req.headers['mcp-session-id'] as string;
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
this.debugLogger.log('SSE', 'No sessionId in SSE request (tried URL param and Mcp-Session-Id header)');
|
||||
// Validate connection parameters
|
||||
if (!sessionId && clientType !== 'web') {
|
||||
this.debugLogger.log('ERROR', 'SSE connection rejected: sessionId required for VS Code connections, or use clientType=web for web interface');
|
||||
this.debugLogger.log('SSE', `Request headers: ${JSON.stringify(req.headers)}`);
|
||||
this.debugLogger.log('SSE', `Request URL: ${req.url}`);
|
||||
// For now, let's allow connection without sessionId and use a default
|
||||
sessionId = 'default-sse-session';
|
||||
this.debugLogger.log('SSE', `Using default sessionId: ${sessionId}`);
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'sessionId required for VS Code connections, or use clientType=web for web interface' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// For web interface connections, use a placeholder sessionId for logging
|
||||
if (clientType === 'web') {
|
||||
sessionId = 'web-interface';
|
||||
this.debugLogger.log('SSE', 'Web interface SSE connection detected via clientType=web');
|
||||
}
|
||||
|
||||
// Set up Server-Sent Events (SSE) stream
|
||||
@@ -657,11 +702,21 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
this.debugLogger.log('SSE', `SSE headers set for session ${sessionId}, adding to connections...`);
|
||||
|
||||
// Add this connection to our session-based SSE connections
|
||||
this.sseClients.set(sessionId, res);
|
||||
this.sseConnections.add(res); // Keep old connections for cleanup
|
||||
this.debugLogger.log('HTTP', `Added SSE connection for session ${sessionId}. Total connections: ${this.sseConnections.size}`);
|
||||
this.debugLogger.log('SSE', `SSE connection added for session ${sessionId}. Total: ${this.sseConnections.size}`);
|
||||
// Detect connection type: web interface via clientType=web, VS Code via sessionId
|
||||
const isWebInterface = clientType === 'web';
|
||||
const isVSCodeWebview = !isWebInterface && sessionId !== 'web-interface';
|
||||
|
||||
if (isWebInterface) {
|
||||
// Web interface connection - add to web interface connections
|
||||
this.webInterfaceConnections.add(res);
|
||||
this.sseConnections.add(res); // Keep old connections for cleanup
|
||||
this.debugLogger.log('SSE', `Added web interface SSE connection. Total web connections: ${this.webInterfaceConnections.size}`);
|
||||
} else {
|
||||
// VS Code webview connection - add to session-specific connections
|
||||
this.sseClients.set(sessionId!, res);
|
||||
this.sseConnections.add(res); // Keep old connections for cleanup
|
||||
this.debugLogger.log('SSE', `Added VS Code SSE connection for session ${sessionId}. Total session connections: ${this.sseClients.size}`);
|
||||
}
|
||||
|
||||
// Send initial connection acknowledgment
|
||||
const initialMessage = 'data: {"type":"connection","status":"established","sessionId":"' + sessionId + '"}\n\n';
|
||||
@@ -676,15 +731,22 @@ export class McpServer extends EventEmitter {
|
||||
clearInterval(heartbeat);
|
||||
this.sseConnections.delete(res);
|
||||
}
|
||||
}, 30000); // Send heartbeat every 30 seconds
|
||||
}, 10000); // Send heartbeat every 10 seconds
|
||||
|
||||
// Handle client disconnect
|
||||
const cleanup = () => {
|
||||
this.debugLogger.log('HTTP', `SSE connection closed for session ${sessionId}`);
|
||||
clearInterval(heartbeat);
|
||||
this.sseConnections.delete(res);
|
||||
this.sseClients.delete(sessionId);
|
||||
this.debugLogger.log('HTTP', `Removed SSE connection for session ${sessionId}. Total connections: ${this.sseConnections.size}`);
|
||||
|
||||
// Remove from appropriate connection type
|
||||
if (isWebInterface) {
|
||||
this.webInterfaceConnections.delete(res);
|
||||
this.debugLogger.log('HTTP', `Removed web interface SSE connection. Total web connections: ${this.webInterfaceConnections.size}`);
|
||||
} else {
|
||||
this.sseClients.delete(sessionId);
|
||||
this.debugLogger.log('HTTP', `Removed VS Code SSE connection for session ${sessionId}. Total session connections: ${this.sseClients.size}`);
|
||||
}
|
||||
};
|
||||
|
||||
req.on('close', cleanup);
|
||||
@@ -698,6 +760,33 @@ export class McpServer extends EventEmitter {
|
||||
res.end('Method Not Allowed');
|
||||
}
|
||||
|
||||
private async handleMcpToolsEndpoint(req: http.IncomingMessage, res: http.ServerResponse, reqUrl: URL): Promise<void> {
|
||||
// MCP tools endpoint - handles MCP protocol for VS Code extension without SSE conflicts
|
||||
this.debugLogger.log('HTTP', `MCP Tools: ${req.method} ${req.url}`);
|
||||
|
||||
// Extract sessionId from query params
|
||||
const sessionId = reqUrl.searchParams.get('sessionId');
|
||||
if (!sessionId) {
|
||||
this.debugLogger.log('ERROR', 'MCP Tools: sessionId required');
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'sessionId required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
// Handle MCP protocol messages (initialize, tools/list, tools/call)
|
||||
await this.handleHttpPost(req, res);
|
||||
} else if (req.method === 'GET') {
|
||||
// Reject GET requests to prevent SSE conflicts - tools only via POST
|
||||
this.debugLogger.log('WARN', 'MCP Tools: GET requests not allowed (use /mcp for SSE)');
|
||||
res.statusCode = 405;
|
||||
res.end(JSON.stringify({ error: 'GET not allowed on /mcp-tools - use /mcp for SSE' }));
|
||||
} else {
|
||||
res.statusCode = 405;
|
||||
res.end('Method Not Allowed');
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSessionEndpoint(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const url = new URL(req.url || '', `http://localhost:${this.port}`);
|
||||
|
||||
@@ -769,9 +858,9 @@ export class McpServer extends EventEmitter {
|
||||
this.sessionNames.set(sessionId, name);
|
||||
this.debugLogger.log('INFO', `Session ${sessionId} named: "${name}"`);
|
||||
|
||||
// Broadcast name change via SSE to all connected clients
|
||||
this.broadcastToSSE('session-name-changed', { sessionId, name });
|
||||
this.debugLogger.log('SSE', 'Broadcasting session name change to all clients');
|
||||
// Send name change to target session and web interface
|
||||
this.sendToSessionAndWeb(sessionId, 'session-name-changed', { sessionId, name });
|
||||
this.debugLogger.log('SSE', 'Sent session name change to target session and web interface');
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
@@ -1417,6 +1506,95 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
function addSessionTab(sessionId) {
|
||||
// Check if tab already exists
|
||||
const existingTab = document.querySelector(\`[data-session="\${sessionId}"].tab\`);
|
||||
if (existingTab) {
|
||||
console.log(\`Tab for session \${sessionId} already exists\`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabsContainer = document.getElementById('tabs');
|
||||
const contentDiv = document.querySelector('.content');
|
||||
|
||||
if (!tabsContainer || !contentDiv) {
|
||||
console.error('Could not find tabs container or content div');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new tab
|
||||
const tabElement = document.createElement('div');
|
||||
tabElement.className = 'tab';
|
||||
tabElement.setAttribute('data-session', sessionId);
|
||||
tabElement.textContent = \`Session: \${sessionId.substring(0, 8)}\`;
|
||||
tabElement.onclick = () => switchToSession(sessionId);
|
||||
|
||||
// Add tab to container
|
||||
tabsContainer.appendChild(tabElement);
|
||||
|
||||
// Create new chat container
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.className = 'chat-container';
|
||||
chatContainer.setAttribute('data-session', sessionId);
|
||||
chatContainer.innerHTML = \`
|
||||
<div class="messages" id="messages-\${sessionId}">
|
||||
<!-- Messages will be loaded dynamically -->
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<textarea class="input-box" placeholder="Type your message..." data-session="\${sessionId}"></textarea>
|
||||
<button class="send-button" data-session="\${sessionId}">Send</button>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// Add chat container to content
|
||||
contentDiv.appendChild(chatContainer);
|
||||
|
||||
// Set up event listeners for new input elements
|
||||
const textarea = chatContainer.querySelector('textarea');
|
||||
const button = chatContainer.querySelector('button');
|
||||
|
||||
if (textarea) {
|
||||
textarea.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendResponse(sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.addEventListener('click', () => sendResponse(sessionId));
|
||||
}
|
||||
|
||||
console.log(\`Added new session tab: \${sessionId}\`);
|
||||
}
|
||||
|
||||
function removeSessionTab(sessionId) {
|
||||
// Remove tab
|
||||
const tabElement = document.querySelector(\`[data-session="\${sessionId}"].tab\`);
|
||||
if (tabElement) {
|
||||
tabElement.remove();
|
||||
}
|
||||
|
||||
// Remove chat container
|
||||
const chatContainer = document.querySelector(\`[data-session="\${sessionId}"].chat-container\`);
|
||||
if (chatContainer) {
|
||||
chatContainer.remove();
|
||||
}
|
||||
|
||||
// If this was the active session, switch to first available
|
||||
const remainingTabs = document.querySelectorAll('.tab');
|
||||
if (remainingTabs.length > 0) {
|
||||
const firstTab = remainingTabs[0];
|
||||
const firstSessionId = firstTab.getAttribute('data-session');
|
||||
if (firstSessionId) {
|
||||
switchToSession(firstSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(\`Removed session tab: \${sessionId}\`);
|
||||
}
|
||||
|
||||
// Load existing messages for all sessions
|
||||
async function loadExistingMessages() {
|
||||
const sessions = ['${sessions.map(s => s.id).join("', '")}'];
|
||||
@@ -1478,7 +1656,7 @@ export class McpServer extends EventEmitter {
|
||||
// WebSocket connection for real-time updates
|
||||
function setupRealtimeUpdates() {
|
||||
console.log('Setting up SSE connection to /mcp...');
|
||||
const eventSource = new EventSource('/mcp');
|
||||
const eventSource = new EventSource('/mcp?clientType=web');
|
||||
|
||||
eventSource.onopen = function(event) {
|
||||
console.log('SSE connection opened successfully:', event);
|
||||
@@ -1548,6 +1726,14 @@ export class McpServer extends EventEmitter {
|
||||
} else if (data.type === 'session_update') {
|
||||
// Refresh the page to show new sessions
|
||||
window.location.reload();
|
||||
} else if (data.type === 'session-registered' && data.data) {
|
||||
// Add new session tab dynamically
|
||||
console.log('New session registered:', data.data);
|
||||
addSessionTab(data.data.sessionId);
|
||||
} else if (data.type === 'session-unregistered' && data.data) {
|
||||
// Remove session tab dynamically
|
||||
console.log('Session unregistered:', data.data);
|
||||
removeSessionTab(data.data.sessionId);
|
||||
} else if (data.type === 'session-name-changed' && data.data) {
|
||||
// Handle session name changes
|
||||
console.log('Session name changed:', data.data);
|
||||
@@ -1646,11 +1832,10 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private async handleToolCall(message: McpMessage): Promise<McpMessage> {
|
||||
const { name, arguments: args } = message.params;
|
||||
// Extract session ID from _meta.vscode.conversationId (VS Code) or fallback to args.sessionId (web)
|
||||
const rawSessionId = message.params._meta?.['vscode.conversationId'] || args?.sessionId;
|
||||
const sessionId = this.mapToRegisteredSession(rawSessionId);
|
||||
// Use actual session ID from MCP message context, not tool argument
|
||||
const sessionId = message.params.sessionId;
|
||||
|
||||
this.debugLogger.log('MCP', `Tool call - name: "${name}", raw session: ${rawSessionId}, mapped session: ${sessionId}`, { name, args });
|
||||
this.debugLogger.log('MCP', `Tool call - name: "${name}", sessionId: ${sessionId}`, { name, args });
|
||||
|
||||
// Check session-specific tools first, then default tools
|
||||
const sessionTools = sessionId ? this.sessionTools.get(sessionId) : null;
|
||||
@@ -1676,6 +1861,21 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private async handleHumanAgentChatTool(messageId: string, params: HumanAgentChatToolParams, sessionId?: string, toolName?: string): Promise<McpMessage> {
|
||||
this.debugLogger.log('TOOL', 'HumanAgent_Chat called with params:', params);
|
||||
|
||||
// Require valid session ID - no default fallback allowed
|
||||
const actualSessionId = sessionId || params.sessionId;
|
||||
if (!actualSessionId) {
|
||||
this.debugLogger.log('ERROR', 'HumanAgent_Chat tool called without session ID - rejecting');
|
||||
return {
|
||||
id: messageId,
|
||||
type: 'response',
|
||||
error: {
|
||||
code: -32602,
|
||||
message: 'Invalid parameters: sessionId is required for HumanAgent_Chat tool'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = params.timeout ? params.timeout * 1000 : null; // Convert to milliseconds or null for no timeout
|
||||
this.debugLogger.log('TOOL', `Using timeout: ${timeoutMs ? `${timeoutMs}ms (${timeoutMs/1000}s)` : 'no timeout (wait indefinitely)'}`);
|
||||
@@ -1711,9 +1911,8 @@ export class McpServer extends EventEmitter {
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
// Store the pending request using the extracted session ID
|
||||
const sessionToUse = sessionId || params.sessionId || 'default';
|
||||
this.debugLogger.log('TOOL', `Adding pending request ${requestId} to session: ${sessionToUse}`);
|
||||
// Use the validated session ID
|
||||
this.debugLogger.log('TOOL', `Adding pending request ${requestId} to session: ${actualSessionId}`);
|
||||
|
||||
// Store the AI's message (this IS the AI communication - it talks by calling the tool)
|
||||
const aiMessage: ChatMessage = {
|
||||
@@ -1723,21 +1922,21 @@ export class McpServer extends EventEmitter {
|
||||
timestamp: new Date(),
|
||||
type: 'text'
|
||||
};
|
||||
this.chatManager.addMessage(sessionToUse, aiMessage);
|
||||
this.debugLogger.log('CHAT', `Stored AI message in ChatManager for session ${sessionToUse}: ${aiMessage.content.substring(0, 50)}...`);
|
||||
this.broadcastMessageToClients(sessionToUse, aiMessage);
|
||||
this.chatManager.addMessage(actualSessionId, aiMessage);
|
||||
this.debugLogger.log('CHAT', `Stored AI message in ChatManager for session ${actualSessionId}: ${aiMessage.content.substring(0, 50)}...`);
|
||||
this.broadcastMessageToClients(actualSessionId, aiMessage);
|
||||
|
||||
// Emit request state to enable input controls and show waiting indicator
|
||||
this.emit('request-state-change', {
|
||||
requestId,
|
||||
sessionId: sessionToUse,
|
||||
sessionId: actualSessionId,
|
||||
state: 'waiting_for_response',
|
||||
message: params.message,
|
||||
context: params.context,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
this.chatManager.addPendingRequest(sessionToUse, requestId, { ...params, toolName: toolName || 'HumanAgent_Chat' });
|
||||
this.chatManager.addPendingRequest(actualSessionId, requestId, { ...params, toolName: toolName || 'HumanAgent_Chat' });
|
||||
this.requestResolvers.set(requestId, {
|
||||
resolve: (response: string) => {
|
||||
if (timeoutHandle) { clearTimeout(timeoutHandle); }
|
||||
@@ -1747,7 +1946,7 @@ export class McpServer extends EventEmitter {
|
||||
// Emit request completed state to disable input controls and hide waiting indicator
|
||||
this.emit('request-state-change', {
|
||||
requestId,
|
||||
sessionId: sessionToUse,
|
||||
sessionId: actualSessionId,
|
||||
state: 'completed',
|
||||
response: response,
|
||||
timestamp: new Date().toISOString()
|
||||
@@ -1872,38 +2071,12 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.debugLogger.log('INFO', `Session registered: ${sessionId} (${this.activeSessions.size} total sessions)`);
|
||||
|
||||
// Send session registration to web interface only (for web UI tab creation)
|
||||
this.sendToWebInterface('session-registered', { sessionId, totalSessions: this.activeSessions.size });
|
||||
}
|
||||
|
||||
private mapToRegisteredSession(conversationId?: string): string | undefined {
|
||||
if (!conversationId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if it's already a registered session ID
|
||||
if (this.activeSessions.has(conversationId)) {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
// Check if it's a conversation ID we've seen before
|
||||
const mappedSession = this.conversationToSession.get(conversationId);
|
||||
if (mappedSession && this.activeSessions.has(mappedSession)) {
|
||||
this.debugLogger.log('MCP', `Mapped conversation ${conversationId} to session ${mappedSession}`);
|
||||
return mappedSession;
|
||||
}
|
||||
|
||||
// If not found, try to map to the first available registered session
|
||||
// This handles the case where VS Code conversation ID needs to be linked to a web-registered session
|
||||
const activeSessions = Array.from(this.activeSessions);
|
||||
if (activeSessions.length > 0) {
|
||||
const targetSession = activeSessions[0]; // Use first available session
|
||||
this.conversationToSession.set(conversationId, targetSession);
|
||||
this.debugLogger.log('MCP', `Auto-mapped conversation ${conversationId} to session ${targetSession}`);
|
||||
return targetSession;
|
||||
}
|
||||
|
||||
this.debugLogger.log('MCP', `No registered session found for conversation ${conversationId}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
unregisterSession(sessionId: string): void {
|
||||
this.activeSessions.delete(sessionId);
|
||||
@@ -1912,6 +2085,9 @@ export class McpServer extends EventEmitter {
|
||||
this.sessionWorkspacePaths.delete(sessionId);
|
||||
this.sessionMessageSettings.delete(sessionId);
|
||||
this.debugLogger.log('INFO', `Session unregistered and cleaned up: ${sessionId} (${this.activeSessions.size} total sessions)`);
|
||||
|
||||
// Send session unregistration to web interface only (for web UI tab removal)
|
||||
this.sendToWebInterface('session-unregistered', { sessionId, totalSessions: this.activeSessions.size });
|
||||
}
|
||||
|
||||
getActiveSessions(): string[] {
|
||||
@@ -1975,7 +2151,7 @@ export class McpServer extends EventEmitter {
|
||||
// Removed: storeMessage and getSessionMessages wrapper methods - call ChatManager directly
|
||||
|
||||
private broadcastMessageToClients(sessionId: string, message: ChatMessage): void {
|
||||
// Broadcast message to all connected SSE clients
|
||||
// Send message to specific session AND all web interface connections
|
||||
const messageEvent = {
|
||||
type: 'chat_message',
|
||||
sessionId: sessionId,
|
||||
@@ -1990,15 +2166,32 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
const sseData = `data: ${JSON.stringify(messageEvent)}\n\n`;
|
||||
|
||||
// Send to all connected SSE clients (VS Code webviews and web interfaces)
|
||||
for (const connection of this.sseConnections) {
|
||||
// Send to the specific session's SSE connection (VS Code webview)
|
||||
const sessionConnection = this.sseClients.get(sessionId);
|
||||
if (sessionConnection) {
|
||||
try {
|
||||
connection.write(sseData);
|
||||
this.debugLogger.log('CHAT', `Broadcasted message to SSE client for session ${sessionId}`);
|
||||
sessionConnection.write(sseData);
|
||||
this.debugLogger.log('CHAT', `Sent message to VS Code session ${sessionId}`);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', 'Failed to broadcast message to SSE client:', error);
|
||||
this.debugLogger.log('ERROR', `Failed to send message to VS Code session ${sessionId}:`, error);
|
||||
// Remove failed connection
|
||||
this.sseConnections.delete(connection);
|
||||
this.sseClients.delete(sessionId);
|
||||
this.sseConnections.delete(sessionConnection);
|
||||
}
|
||||
} else {
|
||||
this.debugLogger.log('WARN', `No VS Code SSE connection found for session ${sessionId}`);
|
||||
}
|
||||
|
||||
// Also send to all web interface connections
|
||||
for (const webConnection of this.webInterfaceConnections) {
|
||||
try {
|
||||
webConnection.write(sseData);
|
||||
this.debugLogger.log('CHAT', `Sent message to web interface for session ${sessionId}`);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', `Failed to send message to web interface:`, error);
|
||||
// Remove failed connection
|
||||
this.webInterfaceConnections.delete(webConnection);
|
||||
this.sseConnections.delete(webConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,23 +51,12 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
private loadNotificationSettings() {
|
||||
try {
|
||||
// Try to load settings from mcp.json
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (workspaceRoot) {
|
||||
const mcpConfigPath = `${workspaceRoot}/.vscode/mcp.json`;
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(mcpConfigPath)) {
|
||||
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
|
||||
// Check if humanagent-mcp server has notifications settings
|
||||
if (mcpConfig.servers && mcpConfig.servers['humanagent-mcp'] && mcpConfig.servers['humanagent-mcp'].notifications) {
|
||||
const notifications = mcpConfig.servers['humanagent-mcp'].notifications;
|
||||
this.notificationSettings = {
|
||||
enableSound: notifications.enableSound ?? true,
|
||||
enableFlashing: notifications.enableFlashing ?? true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load settings from VS Code configuration
|
||||
const config = vscode.workspace.getConfiguration('humanagent-mcp');
|
||||
this.notificationSettings = {
|
||||
enableSound: config.get<boolean>('notifications.enableSound', true),
|
||||
enableFlashing: config.get<boolean>('notifications.enableFlashing', true)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('ChatWebviewProvider: Error loading notification settings:', error);
|
||||
// Use defaults on error
|
||||
@@ -248,18 +237,14 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRegisteredWorkspace = this.mcpConfigManager?.isMcpServerRegistered(false) ?? false;
|
||||
const isRegisteredGlobal = this.mcpConfigManager?.isMcpServerRegistered(true) ?? false;
|
||||
const configType = isRegisteredWorkspace ? 'workspace' : (isRegisteredGlobal ? 'global' : 'none');
|
||||
|
||||
this._view.webview.postMessage({
|
||||
type: 'serverStatus',
|
||||
data: {
|
||||
running: true, // Assume standalone server is running if configured
|
||||
tools: 1, // Default tool count
|
||||
pendingRequests: 0, // Can't get from standalone server easily
|
||||
registered: isRegisteredWorkspace || isRegisteredGlobal,
|
||||
configType: configType
|
||||
registered: true, // Always true with native provider
|
||||
configType: 'native' // Native provider registration
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -488,11 +473,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
vscode.window.showInformationMessage('MCP Server management not available - using standalone server');
|
||||
break;
|
||||
case 'register':
|
||||
// Use the MCP configuration from the parent command
|
||||
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
|
||||
break;
|
||||
case 'unregister':
|
||||
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
|
||||
// Registration handled automatically by native provider
|
||||
vscode.window.showInformationMessage('HumanAgent MCP registration is handled automatically by the native provider.');
|
||||
break;
|
||||
case 'configure':
|
||||
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
|
||||
@@ -501,6 +484,10 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
// Test notification sound by triggering a fake notification
|
||||
await this.displayHumanAgentMessage('🔊 Audio test - this is a test notification sound!', 'Testing audio notifications', 'test-audio');
|
||||
break;
|
||||
case 'requestServerStatus':
|
||||
// Show the same notification popup as the main status command
|
||||
vscode.commands.executeCommand('humanagent-mcp.showStatus');
|
||||
break;
|
||||
case 'overridePrompt':
|
||||
await this.createPromptOverrideFile();
|
||||
break;
|
||||
@@ -850,7 +837,8 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
// Set global flag for override file existence
|
||||
// Set session ID and override file flag
|
||||
const sessionId = '${this.workspaceSessionId}';
|
||||
window.overrideFileExists = ${overrideFileExists};
|
||||
|
||||
// Play notification beep sound
|
||||
@@ -955,47 +943,15 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
let currentServerStatus = null;
|
||||
|
||||
function getDynamicMenuOptions() {
|
||||
const options = [];
|
||||
// Simplified menu options (no registration needed with native provider)
|
||||
const options = [
|
||||
{ text: '📊 Show Status', action: 'requestServerStatus' },
|
||||
{ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' },
|
||||
{ text: '📝 Name This Chat', action: 'nameSession' },
|
||||
{ text: '🌐 Open Web View', action: 'openWebView' }
|
||||
];
|
||||
|
||||
if (!currentServerStatus) {
|
||||
// Default options when status unknown
|
||||
const defaultOptions = [
|
||||
{ text: '📦 Install Globally', action: 'register' },
|
||||
{ text: '📁 Install in Workspace', action: 'register' },
|
||||
{ text: '📊 Show Status', action: 'requestServerStatus' },
|
||||
{ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' },
|
||||
{ text: '📝 Name This Chat', action: 'nameSession' },
|
||||
{ text: '🌐 Open Web View', action: 'openWebView' }
|
||||
];
|
||||
|
||||
// Check for override file existence even when status unknown
|
||||
if (window.overrideFileExists) {
|
||||
defaultOptions.push({ text: '🔄 Reload Override File', action: 'reloadOverride' });
|
||||
}
|
||||
|
||||
defaultOptions.push({ text: '⚙️ Configure MCP', action: 'configure' });
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
// Dynamic options based on current registration status
|
||||
if (currentServerStatus.configType === 'workspace') {
|
||||
options.push({ text: '🗑️ Uninstall from Workspace', action: 'unregister' });
|
||||
options.push({ text: '📦 Install Globally', action: 'register' });
|
||||
} else if (currentServerStatus.configType === 'global') {
|
||||
options.push({ text: '📁 Install in Workspace', action: 'register' });
|
||||
options.push({ text: '🗑️ Uninstall Globally', action: 'unregister' });
|
||||
} else {
|
||||
// Not installed anywhere
|
||||
options.push({ text: '📦 Install Globally', action: 'register' });
|
||||
options.push({ text: '📁 Install in Workspace', action: 'register' });
|
||||
}
|
||||
|
||||
options.push({ text: '📊 Show Status', action: 'requestServerStatus' });
|
||||
options.push({ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' });
|
||||
options.push({ text: '📝 Name This Chat', action: 'nameSession' });
|
||||
options.push({ text: '🌐 Open Web View', action: 'openWebView' });
|
||||
|
||||
// Check for HumanAgentOverride.json file existence (passed from extension)
|
||||
// Check for override file existence
|
||||
if (window.overrideFileExists) {
|
||||
options.push({ text: '🔄 Reload Override File', action: 'reloadOverride' });
|
||||
}
|
||||
@@ -1059,19 +1015,78 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
|
||||
// Set up SSE connection for real-time server events
|
||||
let currentEventSource = null;
|
||||
let connectionInProgress = false;
|
||||
|
||||
function setupSSEConnection() {
|
||||
if (connectionInProgress) {
|
||||
console.log('⚠️ SSE connection already in progress, skipping duplicate attempt');
|
||||
return;
|
||||
}
|
||||
|
||||
connectionInProgress = true;
|
||||
|
||||
try {
|
||||
console.log('Setting up SSE connection to MCP server...');
|
||||
const eventSource = new EventSource('http://localhost:3737/mcp');
|
||||
// Close existing connection if present
|
||||
if (currentEventSource && currentEventSource.readyState !== 2) { // 2 = CLOSED
|
||||
console.log('🔄 Closing existing EventSource before creating new one');
|
||||
currentEventSource.close();
|
||||
}
|
||||
|
||||
console.log('Setting up SSE connection to MCP server for session:', sessionId);
|
||||
const eventSource = new EventSource(\`http://localhost:3737/mcp?sessionId=\${sessionId}\`);
|
||||
currentEventSource = eventSource;
|
||||
|
||||
// Enhanced connection health monitoring
|
||||
let lastHeartbeat = Date.now();
|
||||
let heartbeatTimeout = null;
|
||||
let messageCount = 0;
|
||||
let connectionStartTime = Date.now();
|
||||
|
||||
// Monitor heartbeat to detect stale connections
|
||||
function resetHeartbeatTimeout() {
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
console.error('❌ SSE Connection Analysis:');
|
||||
console.error(' - No heartbeat for 25 seconds, EventSource appears stale');
|
||||
console.error(' - Last heartbeat:', new Date(lastHeartbeat).toISOString());
|
||||
console.error(' - Connection duration:', Math.round((Date.now() - connectionStartTime) / 1000), 'seconds');
|
||||
console.error(' - Messages received:', messageCount);
|
||||
console.error(' - EventSource readyState:', eventSource.readyState);
|
||||
console.error(' - Reconnecting...');
|
||||
eventSource.close();
|
||||
connectionInProgress = false;
|
||||
setTimeout(setupSSEConnection, 1000);
|
||||
}, 25000); // Timeout after 25 seconds (2.5x heartbeat interval)
|
||||
}
|
||||
|
||||
eventSource.onopen = function(event) {
|
||||
console.log('SSE connection opened:', event);
|
||||
console.log('✅ SSE connection opened for session:', sessionId);
|
||||
console.log(' EventSource readyState:', eventSource.readyState);
|
||||
console.log(' Connection time:', new Date().toISOString());
|
||||
lastHeartbeat = Date.now();
|
||||
connectionStartTime = Date.now();
|
||||
messageCount = 0;
|
||||
connectionInProgress = false; // Connection successful, allow future connections
|
||||
resetHeartbeatTimeout();
|
||||
};
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
try {
|
||||
messageCount++;
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE event received:', data);
|
||||
|
||||
// Handle heartbeat for connection monitoring
|
||||
if (data.type === 'heartbeat') {
|
||||
lastHeartbeat = Date.now();
|
||||
resetHeartbeatTimeout();
|
||||
console.log('💓 Heartbeat received at', new Date().toISOString(), '(msg #' + messageCount + ')');
|
||||
return; // Don't log heartbeats further
|
||||
}
|
||||
|
||||
console.log('📨 SSE event received (msg #' + messageCount + '):', data);
|
||||
|
||||
if (data.type === 'request-state-change') {
|
||||
handleRequestStateChange(data.data);
|
||||
@@ -1087,13 +1102,30 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
};
|
||||
|
||||
eventSource.onerror = function(error) {
|
||||
console.error('SSE connection error:', error);
|
||||
// Try to reconnect after 5 seconds
|
||||
console.error('❌ SSE connection error detected:');
|
||||
console.error(' Error object:', error);
|
||||
console.error(' EventSource readyState:', eventSource.readyState);
|
||||
console.error(' Connection duration:', Math.round((Date.now() - connectionStartTime) / 1000), 'seconds');
|
||||
console.error(' Messages received before error:', messageCount);
|
||||
console.error(' Last heartbeat:', new Date(lastHeartbeat).toISOString());
|
||||
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
}
|
||||
|
||||
// Log readyState meaning
|
||||
const stateNames = ['CONNECTING', 'OPEN', 'CLOSED'];
|
||||
console.error(' EventSource state:', stateNames[eventSource.readyState] || 'UNKNOWN');
|
||||
|
||||
// Reset connection state and try to reconnect after 5 seconds
|
||||
connectionInProgress = false;
|
||||
console.log('🔄 Reconnecting SSE in 5 seconds...');
|
||||
setTimeout(setupSSEConnection, 5000);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to setup SSE connection:', error);
|
||||
connectionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user