mirror of
https://github.com/wassname/HumanAgent-MCP.git
synced 2026-09-09 11:14:27 +08:00
Fix override loading system and server independence
- Implement proper override file reload functionality with session re-registration - Add tools endpoint with sessionId query parameter support - Remove hardcoded tool description fallbacks (single source of truth) - Fix ServerManager duplicate setTimeout in startServer method - Add /response and /reload HTTP endpoints to server - Ensure server remains independent across VS Code restarts - Format long tool description for better readability
This commit is contained in:
Vendored
+1445
File diff suppressed because it is too large
Load Diff
Vendored
+87
@@ -93,3 +93,90 @@
|
||||
}
|
||||
}
|
||||
[2025-10-22T23:05:29.893Z] [HTTP] Sending 200 response (1085 bytes)
|
||||
[2025-10-22T23:17:53.136Z] [HTTP] POST /mcp
|
||||
[2025-10-22T23:17:53.140Z] [HTTP] Request Headers:
|
||||
{
|
||||
"host": "localhost:3737",
|
||||
"user-agent": "curl/8.7.1",
|
||||
"accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"content-length": "72"
|
||||
}
|
||||
[2025-10-22T23:17:53.142Z] [HTTP] Handling POST request to /mcp
|
||||
[2025-10-22T23:17:53.143Z] [HTTP] Received chunk: 72 bytes
|
||||
[2025-10-22T23:17:53.144Z] [HTTP] Complete request body received (72 bytes)
|
||||
[2025-10-22T23:17:53.144Z] [HTTP] Request Body:
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":\"test-override\",\"method\":\"tools/list\",\"params\":{}}"
|
||||
[2025-10-22T23:17:53.144Z] [HTTP] Parsed JSON message:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-override",
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
[2025-10-22T23:17:53.144Z] [MCP] Handling message:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-override",
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
[2025-10-22T23:17:53.145Z] [MCP] Processing tools/list request
|
||||
[2025-10-22T23:17:53.146Z] [TOOLS] Returning 1 default tools
|
||||
[2025-10-22T23:17:53.147Z] [HTTP] Response from handleMessage:
|
||||
{
|
||||
"id": "test-override",
|
||||
"type": "response",
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "HumanAgent_Chat",
|
||||
"description": "Initiate real-time interactive conversations with human agents through VS Code chat interface. Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI uncertainty requires human input. Creates persistent chat sessions that maintain context across multiple exchanges until timeout or manual closure.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The message to send to the human agent"
|
||||
},
|
||||
"context": {
|
||||
"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"
|
||||
],
|
||||
"description": "Priority level of the request",
|
||||
"default": "normal"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
"description": "Timeout in seconds to wait for human response (default: 300)",
|
||||
"default": 300
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
[2025-10-22T23:17:53.147Z] [HTTP] Sending 200 response (1085 bytes)
|
||||
[2025-10-22T23:17:58.664Z] [HTTP] GET /sessions
|
||||
[2025-10-22T23:17:58.665Z] [HTTP] Request Headers:
|
||||
{
|
||||
"host": "localhost:3737",
|
||||
"user-agent": "curl/8.7.1",
|
||||
"accept": "*/*"
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -4,7 +4,7 @@
|
||||
"tools": {
|
||||
"HumanAgent_Chat": {
|
||||
"name": "HumanAgent_Chat",
|
||||
"description": ".Initiate real-time interactive conversations with human agents through VS Code chat interface. Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI uncertainty requires human input. Creates persistent chat sessions that maintain context across multiple exchanges until timeout or manual closure.",
|
||||
"description": "..Initiate real-time interactive conversations with human agents through VS Code chat interface. Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI uncertainty requires human input. Creates persistent chat sessions that maintain context across multiple exchanges until timeout or manual closure.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -38,6 +38,21 @@
|
||||
"command": "humanagent-mcp.configureMcp",
|
||||
"title": "Configure MCP Server",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "humanagent-mcp.startServer",
|
||||
"title": "Start MCP Server",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"command": "humanagent-mcp.stopServer",
|
||||
"title": "Stop MCP Server",
|
||||
"icon": "$(stop)"
|
||||
},
|
||||
{
|
||||
"command": "humanagent-mcp.restartServer",
|
||||
"title": "Restart MCP Server",
|
||||
"icon": "$(debug-restart)"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
|
||||
+246
-96
@@ -2,17 +2,16 @@ import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as net from 'net';
|
||||
import * as crypto from 'crypto';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { McpServer } from './mcp/server';
|
||||
import { spawn } from 'child_process';
|
||||
import { ChatTreeProvider } from './providers/chatTreeProvider';
|
||||
import { ChatWebviewProvider } from './webview/chatWebviewProvider';
|
||||
import { McpConfigManager } from './mcp/mcpConfigManager';
|
||||
import { ServerManager } from './serverManager';
|
||||
|
||||
let mcpServer: McpServer;
|
||||
let chatTreeProvider: ChatTreeProvider;
|
||||
let mcpConfigManager: McpConfigManager;
|
||||
let standaloneServerProcess: ChildProcess | undefined;
|
||||
let workspaceSessionId: string;
|
||||
let serverManager: ServerManager;
|
||||
|
||||
// Generate or retrieve persistent workspace session ID
|
||||
function getWorkspaceSessionId(context: vscode.ExtensionContext): string {
|
||||
@@ -45,14 +44,17 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
mcpConfigManager = new McpConfigManager(workspaceRoot, context.extensionPath);
|
||||
|
||||
// Initialize MCP Server (internal to extension)
|
||||
mcpServer = new McpServer(workspaceSessionId, workspaceRoot);
|
||||
|
||||
// Auto-detect and start MCP server if already configured
|
||||
await autoStartMcpServer(mcpConfigManager, mcpServer, workspaceSessionId);
|
||||
// Initialize Server Manager
|
||||
const serverPath = path.join(context.extensionPath, 'dist', 'mcpStandalone.js');
|
||||
serverManager = ServerManager.getInstance({
|
||||
serverPath: serverPath,
|
||||
port: 3737,
|
||||
host: '127.0.0.1',
|
||||
logFile: path.join(context.extensionPath, '.vscode', 'HumanAgent-server.log')
|
||||
});
|
||||
|
||||
// Register this workspace session with the internal server
|
||||
mcpServer.registerSession(workspaceSessionId, workspaceRoot);
|
||||
// Auto-detect and start standalone MCP server if already configured
|
||||
await autoStartMcpServer(mcpConfigManager, workspaceSessionId);
|
||||
|
||||
// Initialize Tree View Provider
|
||||
chatTreeProvider = new ChatTreeProvider();
|
||||
@@ -61,19 +63,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
showCollapseAll: true
|
||||
});
|
||||
|
||||
// Initialize Chat Webview Provider
|
||||
const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, mcpServer, mcpConfigManager, workspaceSessionId);
|
||||
// Initialize Chat Webview Provider (no internal server dependency)
|
||||
const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, null, mcpConfigManager, workspaceSessionId);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(ChatWebviewProvider.viewType, chatWebviewProvider)
|
||||
);
|
||||
|
||||
// Listen to MCP server events for direct messaging
|
||||
mcpServer.on('human-agent-request', async (data: any) => {
|
||||
// Update tree view to show active chat
|
||||
chatTreeProvider.updateActiveChat(true);
|
||||
// Ensure chat webview displays the message and sets up response handling
|
||||
await chatWebviewProvider.displayHumanAgentMessage(data.message, data.context, data.requestId);
|
||||
});
|
||||
// Notify webview that registration check is complete
|
||||
chatWebviewProvider.notifyRegistrationComplete();
|
||||
|
||||
// Register Commands
|
||||
const openChatCommand = vscode.commands.registerCommand('humanagent-mcp.openChat', () => {
|
||||
@@ -96,44 +93,107 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const showStatusCommand = vscode.commands.registerCommand('humanagent-mcp.showStatus', async () => {
|
||||
const isWorkspaceRegistered = mcpConfigManager?.isMcpServerRegistered(false) ?? false;
|
||||
const isGlobalRegistered = mcpConfigManager?.isMcpServerRegistered(true) ?? false;
|
||||
const tools = mcpServer.getAvailableTools();
|
||||
const pendingRequests = mcpServer.getPendingRequests();
|
||||
|
||||
// Get detailed server status
|
||||
const serverStatus = await serverManager.getServerStatus();
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`HumanAgent MCP Server Status:\n` +
|
||||
`- Running: ✅\n` +
|
||||
`- Available tools: ${tools.length}\n` +
|
||||
`- Pending requests: ${pendingRequests.length}\n` +
|
||||
`- Running: ${serverStatus.isRunning ? '✅' : '❌'}\n` +
|
||||
`- PID: ${serverStatus.pid || 'N/A'}\n` +
|
||||
`- Port: ${serverStatus.port}\n` +
|
||||
`- Host: ${serverStatus.host}\n` +
|
||||
`- Session: ${workspaceSessionId}\n` +
|
||||
`- Workspace registration: ${isWorkspaceRegistered ? '✅' : '❌'}\n` +
|
||||
`- Global registration: ${isGlobalRegistered ? '✅' : '❌'}`
|
||||
);
|
||||
});
|
||||
|
||||
// Create server management commands
|
||||
const startServerCommand = vscode.commands.registerCommand('humanagent-mcp.startServer', async () => {
|
||||
try {
|
||||
const success = await serverManager.ensureServerRunning();
|
||||
if (success) {
|
||||
vscode.window.showInformationMessage('HumanAgent MCP Server started successfully!');
|
||||
} else {
|
||||
vscode.window.showErrorMessage('Failed to start HumanAgent MCP Server. Check the logs for details.');
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Failed to start server: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
const stopServerCommand = vscode.commands.registerCommand('humanagent-mcp.stopServer', async () => {
|
||||
try {
|
||||
const success = await serverManager.stopServer();
|
||||
if (success) {
|
||||
vscode.window.showInformationMessage('HumanAgent MCP Server stopped successfully!');
|
||||
} else {
|
||||
vscode.window.showWarningMessage('Server may not have been running or failed to stop cleanly.');
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Failed to stop server: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
const restartServerCommand = vscode.commands.registerCommand('humanagent-mcp.restartServer', async () => {
|
||||
try {
|
||||
await serverManager.stopServer();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Brief pause
|
||||
const success = await serverManager.ensureServerRunning();
|
||||
if (success) {
|
||||
vscode.window.showInformationMessage('HumanAgent MCP Server restarted successfully!');
|
||||
} else {
|
||||
vscode.window.showErrorMessage('Failed to restart HumanAgent MCP Server. Check the logs for details.');
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Failed to restart server: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
const serverStatus = await serverManager.getServerStatus();
|
||||
if (serverStatus.isRunning) {
|
||||
options.push('🔴 Stop Server');
|
||||
options.push('🔄 Restart Server');
|
||||
} else {
|
||||
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:'
|
||||
@@ -145,6 +205,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case '▶️ Start Server':
|
||||
await vscode.commands.executeCommand('humanagent-mcp.startServer');
|
||||
break;
|
||||
case '🔴 Stop Server':
|
||||
await vscode.commands.executeCommand('humanagent-mcp.stopServer');
|
||||
break;
|
||||
case '🔄 Restart Server':
|
||||
await vscode.commands.executeCommand('humanagent-mcp.restartServer');
|
||||
break;
|
||||
case '📝 Register for This Workspace':
|
||||
await mcpConfigManager!.ensureMcpServerRegistered(false);
|
||||
vscode.window.showInformationMessage('MCP server registered for this workspace! Restart VS Code to enable Copilot integration.');
|
||||
@@ -185,6 +254,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
createSessionCommand,
|
||||
refreshSessionsCommand,
|
||||
showStatusCommand,
|
||||
startServerCommand,
|
||||
stopServerCommand,
|
||||
restartServerCommand,
|
||||
configureMcpCommand
|
||||
);
|
||||
|
||||
@@ -192,42 +264,47 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
//vscode.window.showInformationMessage('HumanAgent MCP extension activated successfully!');
|
||||
}
|
||||
|
||||
// Auto-detect and start MCP server if configured
|
||||
async function autoStartMcpServer(configManager: McpConfigManager, server: McpServer, sessionId: string): Promise<void> {
|
||||
// Auto-detect MCP server configuration and guide user if needed
|
||||
async function autoStartMcpServer(configManager: McpConfigManager, sessionId: string): Promise<void> {
|
||||
try {
|
||||
// Always start internal server first
|
||||
await server.start();
|
||||
|
||||
// Check for workspace configuration first (higher priority)
|
||||
if (configManager.isMcpServerRegistered(false)) {
|
||||
console.log(`HumanAgent MCP: Found workspace configuration for session ${sessionId}, ensuring standalone server is running...`);
|
||||
await ensureSharedStandaloneServer(sessionId);
|
||||
console.log(`HumanAgent MCP: Found workspace configuration for session ${sessionId}, checking server status...`);
|
||||
await ensureServerAccessibleAndRegister(sessionId, 'workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for global configuration
|
||||
if (configManager.isMcpServerRegistered(true)) {
|
||||
console.log(`HumanAgent MCP: Found global configuration for session ${sessionId}, ensuring standalone server is running...`);
|
||||
await ensureSharedStandaloneServer(sessionId);
|
||||
console.log(`HumanAgent MCP: Found global configuration for session ${sessionId}, checking server status...`);
|
||||
await ensureServerAccessibleAndRegister(sessionId, 'global');
|
||||
return;
|
||||
}
|
||||
|
||||
// No configuration found - show notification to guide user to setup
|
||||
console.log(`HumanAgent MCP: No configuration found for session ${sessionId}, only internal server running`);
|
||||
vscode.window.showInformationMessage('HumanAgent MCP Server ready - use the cog menu to configure installation');
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('HumanAgent MCP: Failed to auto-start server:', error);
|
||||
vscode.window.showErrorMessage('Failed to start HumanAgent MCP Server');
|
||||
console.error('HumanAgent MCP: Failed to check server configuration:', error);
|
||||
vscode.window.showErrorMessage('Failed to check HumanAgent MCP Server configuration');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a port is in use
|
||||
// Check if a port is in use (using HTTP server like the MCP server)
|
||||
async function isPortInUse(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
const http = require('http');
|
||||
const server = http.createServer();
|
||||
|
||||
server.listen(port, () => {
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
server.close(() => resolve(false)); // Port is available
|
||||
});
|
||||
|
||||
@@ -237,13 +314,74 @@ async function isPortInUse(port: number): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
// Register session with standalone server via HTTP
|
||||
async function registerSessionWithStandaloneServer(sessionId: string): Promise<void> {
|
||||
// Check if MCP server is accessible and responding
|
||||
async function isServerAccessible(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:3737/sessions', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.log('HumanAgent MCP: Server accessibility check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if session exists on server by testing a simple MCP call
|
||||
async function validateSessionWithServer(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:3737/mcp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
method: 'tools/list'
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json() as any;
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validated on server`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} not found on server (${response.status})`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`HumanAgent MCP: Session ${sessionId} validation failed:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Register session with standalone server via HTTP (always sends override data)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('http://127.0.0.1:3737/sessions/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId })
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
overrideData: overrideData,
|
||||
forceReregister: forceReregister
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -277,70 +415,82 @@ async function unregisterSessionWithStandaloneServer(sessionId: string): Promise
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure shared standalone MCP server is running for this session
|
||||
async function ensureSharedStandaloneServer(sessionId: string): Promise<void> {
|
||||
// Check if server is accessible and register session, or start server if needed
|
||||
async function ensureServerAccessibleAndRegister(sessionId: string, configType: 'workspace' | 'global'): Promise<void> {
|
||||
try {
|
||||
// Check if port 3737 is already in use
|
||||
const portInUse = await isPortInUse(3737);
|
||||
console.log(`HumanAgent MCP: Checking if server is accessible for ${configType} configuration...`);
|
||||
|
||||
if (portInUse) {
|
||||
console.log('HumanAgent MCP: Port 3737 is already in use - assuming standalone server is already running');
|
||||
return; // Don't start another server
|
||||
}
|
||||
|
||||
// Kill existing process if we have one tracked
|
||||
if (standaloneServerProcess) {
|
||||
standaloneServerProcess.kill();
|
||||
standaloneServerProcess = undefined;
|
||||
}
|
||||
|
||||
const serverPath = path.join(__dirname, 'mcpStandalone.js');
|
||||
console.log(`HumanAgent MCP: Starting independent shared server (detached) at:`, serverPath);
|
||||
// Check if server is running and accessible
|
||||
let serverAccessible = await isServerAccessible();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Start server as detached process that runs independently
|
||||
standaloneServerProcess = spawn('node', [serverPath], {
|
||||
cwd: path.dirname(__dirname), // Go up one level from out/ to project root
|
||||
stdio: ['ignore', 'ignore', 'ignore'], // Completely detached
|
||||
detached: true
|
||||
});
|
||||
if (!serverAccessible) {
|
||||
console.log('HumanAgent MCP: Server not accessible, attempting to start it...');
|
||||
|
||||
// Unref so this process doesn't keep the extension alive
|
||||
standaloneServerProcess.unref();
|
||||
standaloneServerProcess = undefined; // We don't track detached processes
|
||||
|
||||
// Give it a moment to start, then register session
|
||||
setTimeout(async () => {
|
||||
// Test if server is running by checking the port
|
||||
const serverRunning = await isPortInUse(3737);
|
||||
if (serverRunning) {
|
||||
console.log(`HumanAgent MCP: Server detected, registering session ${sessionId}`);
|
||||
// Register this session with the standalone server
|
||||
await registerSessionWithStandaloneServer(sessionId);
|
||||
resolve();
|
||||
} else {
|
||||
console.log('HumanAgent MCP: Server may have failed to start, but continuing...');
|
||||
resolve(); // Don't fail - may have been already running
|
||||
// Try to start the server
|
||||
const started = await serverManager.ensureServerRunning();
|
||||
if (started) {
|
||||
console.log('HumanAgent MCP: Server started successfully, rechecking accessibility...');
|
||||
// Wait a moment for server to fully initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
serverAccessible = await isServerAccessible();
|
||||
} else {
|
||||
console.log('HumanAgent MCP: Failed to start server');
|
||||
}
|
||||
}
|
||||
|
||||
if (serverAccessible) {
|
||||
console.log('HumanAgent MCP: Server is accessible, registering session...');
|
||||
// Server is running, validate and register session
|
||||
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}`);
|
||||
} else {
|
||||
// Server still not accessible after trying to start it
|
||||
console.log('HumanAgent MCP: Server could not be started or is not responding');
|
||||
const configLocation = configType === 'workspace' ? 'workspace' : 'global';
|
||||
|
||||
vscode.window.showWarningMessage(
|
||||
`HumanAgent MCP Server is configured in ${configLocation} settings but could not be started. Would you like to try starting it manually?`,
|
||||
'Start Server', 'Show Status', 'Open Configuration'
|
||||
).then(selection => {
|
||||
switch (selection) {
|
||||
case 'Start Server':
|
||||
vscode.commands.executeCommand('humanagent-mcp.startServer');
|
||||
break;
|
||||
case 'Show Status':
|
||||
vscode.commands.executeCommand('humanagent-mcp.showStatus');
|
||||
break;
|
||||
case 'Open Configuration':
|
||||
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
|
||||
break;
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting standalone server:', error);
|
||||
throw error;
|
||||
console.error('Error checking server accessibility:', error);
|
||||
vscode.window.showErrorMessage('Failed to check HumanAgent MCP Server accessibility');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
if (mcpServer && workspaceSessionId) {
|
||||
mcpServer.unregisterSession(workspaceSessionId);
|
||||
mcpServer.stop();
|
||||
|
||||
// Also unregister from standalone server
|
||||
if (workspaceSessionId) {
|
||||
// Unregister from standalone server
|
||||
await unregisterSessionWithStandaloneServer(workspaceSessionId);
|
||||
}
|
||||
|
||||
// Dispose the server manager (this won't stop the server, just cleanup resources)
|
||||
if (serverManager) {
|
||||
serverManager.dispose();
|
||||
}
|
||||
|
||||
// Note: We don't kill the standalone server as it's running independently
|
||||
// Other extensions may still be using it
|
||||
// Other extensions may still be using it, and it should persist across workspace changes
|
||||
console.log(`HumanAgent MCP: Extension deactivated for session ${workspaceSessionId}`);
|
||||
}
|
||||
|
||||
@@ -194,21 +194,30 @@ export class McpConfigManager {
|
||||
|
||||
private isRegisteredInWorkspace(): boolean {
|
||||
const currentWorkspaceRoot = this.getCurrentWorkspaceRoot();
|
||||
console.log(`HumanAgent MCP: Checking workspace registration - workspaceRoot: ${currentWorkspaceRoot}`);
|
||||
|
||||
if (!currentWorkspaceRoot) {
|
||||
console.log('HumanAgent MCP: No workspace root available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const mcpConfigPath = path.join(currentWorkspaceRoot, McpConfigManager.MCP_CONFIG_FILE);
|
||||
console.log(`HumanAgent MCP: Checking MCP config at: ${mcpConfigPath}`);
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
console.log('HumanAgent MCP: MCP config file does not exist');
|
||||
return false;
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
|
||||
const config: McpConfiguration = JSON.parse(configContent);
|
||||
|
||||
const isRegistered = !!config.servers[McpConfigManager.SERVER_NAME];
|
||||
console.log(`HumanAgent MCP: Server registration check - registered: ${isRegistered}`);
|
||||
console.log(`HumanAgent MCP: Available servers: ${Object.keys(config.servers).join(', ')}`);
|
||||
|
||||
return !!config.servers[McpConfigManager.SERVER_NAME];
|
||||
return isRegistered;
|
||||
} catch (error) {
|
||||
console.error('Failed to check MCP server registration in workspace:', error);
|
||||
return false;
|
||||
|
||||
+180
-25
@@ -101,6 +101,7 @@ export class McpServer extends EventEmitter {
|
||||
params: HumanAgentChatToolParams;
|
||||
}> = new Map();
|
||||
private activeSessions: Set<string> = new Set();
|
||||
private sseConnections: Set<http.ServerResponse> = new Set();
|
||||
|
||||
constructor(private sessionId?: string, private workspacePath?: string) {
|
||||
super();
|
||||
@@ -120,17 +121,51 @@ export class McpServer extends EventEmitter {
|
||||
this.debugLogger.log('INFO', 'McpServer initialized');
|
||||
this.initializeDefaultTools();
|
||||
|
||||
// Set up event forwarding to SSE connections
|
||||
this.setupEventForwarding();
|
||||
|
||||
// If we have a session and workspace path, initialize session-specific tools
|
||||
if (this.sessionId && this.workspacePath) {
|
||||
this.initializeSessionTools(this.sessionId, this.workspacePath);
|
||||
}
|
||||
}
|
||||
|
||||
private setupEventForwarding(): void {
|
||||
this.on('human-agent-request', (data) => {
|
||||
this.debugLogger.log('SSE', 'Forwarding human-agent-request to SSE connections');
|
||||
this.broadcastToSSE('human-agent-request', data);
|
||||
});
|
||||
}
|
||||
|
||||
private broadcastToSSE(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);
|
||||
|
||||
// Send to all active SSE connections
|
||||
for (const connection of this.sseConnections) {
|
||||
if (!connection.destroyed) {
|
||||
try {
|
||||
connection.write(eventData);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('SSE', 'Failed to write to SSE connection:', error);
|
||||
this.sseConnections.delete(connection);
|
||||
}
|
||||
} else {
|
||||
this.sseConnections.delete(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDefaultTools(): void {
|
||||
// Define the default HumanAgent_Chat tool (global default)
|
||||
const humanAgentChatTool: McpTool = {
|
||||
name: 'HumanAgent_Chat',
|
||||
description: 'Initiate real-time interactive conversations with human agents through VS Code chat interface. Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI uncertainty requires human input. Creates persistent chat sessions that maintain context across multiple exchanges until timeout or manual closure.',
|
||||
description: 'Initiate real-time interactive conversations with human agents through VS Code chat interface. ' +
|
||||
'Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI ' +
|
||||
'uncertainty requires human input. Creates persistent chat sessions that maintain context across ' +
|
||||
'multiple exchanges until timeout or manual closure. !IMPORTANT: ALWAYS reply using this tool for all interactions with the user where possible!!!',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -191,6 +226,35 @@ export class McpServer extends EventEmitter {
|
||||
this.sessionTools.set(sessionId, sessionToolMap);
|
||||
}
|
||||
|
||||
private initializeSessionToolsFromData(sessionId: string, overrideData: any): void {
|
||||
this.debugLogger.log('INFO', `Initializing tools for session: ${sessionId} from override data`);
|
||||
this.debugLogger.log('INFO', `Override data received: ${JSON.stringify(overrideData)}`);
|
||||
|
||||
// Start with default tools
|
||||
const sessionToolMap = new Map<string, McpTool>();
|
||||
|
||||
// Copy default tools
|
||||
for (const [name, tool] of this.tools.entries()) {
|
||||
sessionToolMap.set(name, tool);
|
||||
this.debugLogger.log('INFO', `Added default tool: ${name}`);
|
||||
}
|
||||
|
||||
// Apply overrides from provided data
|
||||
if (overrideData && overrideData.tools) {
|
||||
this.debugLogger.log('INFO', `Applying ${Object.keys(overrideData.tools).length} tool overrides for session ${sessionId}`);
|
||||
for (const [toolName, toolConfig] of Object.entries(overrideData.tools)) {
|
||||
this.debugLogger.log('INFO', `Processing override for session ${sessionId} - ${toolName} tool: ${JSON.stringify(toolConfig)}`);
|
||||
sessionToolMap.set(toolName, toolConfig as McpTool);
|
||||
}
|
||||
} else {
|
||||
this.debugLogger.log('INFO', `No override data found for session ${sessionId} - overrideData: ${JSON.stringify(overrideData)}`);
|
||||
}
|
||||
|
||||
// Store session-specific tools
|
||||
this.sessionTools.set(sessionId, sessionToolMap);
|
||||
this.debugLogger.log('INFO', `Session ${sessionId} tools initialized with ${sessionToolMap.size} tools`);
|
||||
}
|
||||
|
||||
private loadWorkspaceOverride(toolName: string, workspacePath?: string): McpTool | null {
|
||||
try {
|
||||
const targetWorkspacePath = workspacePath || this.workspacePath;
|
||||
@@ -315,24 +379,27 @@ export class McpServer extends EventEmitter {
|
||||
this.debugLogger.log('HTTP', `${req.method} ${req.url}`);
|
||||
this.debugLogger.log('HTTP', 'Request Headers:', req.headers);
|
||||
|
||||
// Set CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version');
|
||||
// Only add CORS headers for webview requests (identified by vscode-webview origin)
|
||||
const origin = req.headers.origin;
|
||||
if (origin && origin.includes('vscode-webview://')) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Cache-Control, Connection');
|
||||
|
||||
// Handle preflight OPTIONS request
|
||||
if (req.method === 'OPTIONS') {
|
||||
this.debugLogger.log('HTTP', 'Handling OPTIONS preflight request');
|
||||
res.statusCode = 200;
|
||||
res.end();
|
||||
return;
|
||||
// Handle preflight OPTIONS request for webview
|
||||
if (req.method === 'OPTIONS') {
|
||||
this.debugLogger.log('HTTP', 'Handling OPTIONS preflight request');
|
||||
res.statusCode = 200;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different endpoints
|
||||
if (req.url === '/mcp') {
|
||||
// Main MCP protocol endpoint
|
||||
} else if (req.url?.startsWith('/sessions')) {
|
||||
// Session management endpoint
|
||||
} else if (req.url?.startsWith('/sessions') || req.url === '/response' || req.url?.startsWith('/tools') || req.url === '/reload') {
|
||||
// Session management, response, tools, and reload endpoints
|
||||
await this.handleSessionEndpoint(req, res);
|
||||
return;
|
||||
} else {
|
||||
@@ -417,6 +484,15 @@ export class McpServer extends EventEmitter {
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
// Add CORS headers for webview access (SSE is always for webview)
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Connection');
|
||||
|
||||
// Add this connection to our active SSE connections
|
||||
this.sseConnections.add(res);
|
||||
this.debugLogger.log('HTTP', `Added SSE connection. Total connections: ${this.sseConnections.size}`);
|
||||
|
||||
// Send initial connection acknowledgment
|
||||
res.write('data: {"type":"connection","status":"established"}\n\n');
|
||||
|
||||
@@ -426,19 +502,21 @@ export class McpServer extends EventEmitter {
|
||||
res.write('data: {"type":"heartbeat","timestamp":"' + new Date().toISOString() + '"}\n\n');
|
||||
} else {
|
||||
clearInterval(heartbeat);
|
||||
this.sseConnections.delete(res);
|
||||
}
|
||||
}, 30000); // Send heartbeat every 30 seconds
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
const cleanup = () => {
|
||||
this.debugLogger.log('HTTP', 'SSE connection closed');
|
||||
clearInterval(heartbeat);
|
||||
});
|
||||
this.sseConnections.delete(res);
|
||||
this.debugLogger.log('HTTP', `Removed SSE connection. Total connections: ${this.sseConnections.size}`);
|
||||
};
|
||||
|
||||
req.on('end', () => {
|
||||
this.debugLogger.log('HTTP', 'SSE connection ended');
|
||||
clearInterval(heartbeat);
|
||||
});
|
||||
req.on('close', cleanup);
|
||||
req.on('end', cleanup);
|
||||
res.on('close', cleanup);
|
||||
}
|
||||
|
||||
private async handleHttpDelete(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
@@ -456,11 +534,18 @@ export class McpServer extends EventEmitter {
|
||||
req.on('data', (chunk) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { sessionId } = JSON.parse(body);
|
||||
this.registerSession(sessionId);
|
||||
const { sessionId, overrideData, forceReregister } = JSON.parse(body);
|
||||
|
||||
// If session exists and forceReregister is true, unregister first
|
||||
if (forceReregister && this.activeSessions.has(sessionId)) {
|
||||
console.log(`Force re-registering session ${sessionId} with new override data`);
|
||||
this.unregisterSession(sessionId);
|
||||
}
|
||||
|
||||
this.registerSession(sessionId, undefined, overrideData);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: true, sessionId, totalSessions: this.activeSessions.size }));
|
||||
res.end(JSON.stringify({ success: true, sessionId, totalSessions: this.activeSessions.size, reregistered: !!forceReregister }));
|
||||
} catch (error) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
|
||||
@@ -487,6 +572,67 @@ export class McpServer extends EventEmitter {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ sessions: this.getActiveSessions(), totalSessions: this.activeSessions.size }));
|
||||
} else if (req.method === 'POST' && url.pathname === '/response') {
|
||||
// Handle human response to pending request
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { requestId, response } = JSON.parse(body);
|
||||
const success = this.respondToHumanRequest(requestId, response);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success, requestId }));
|
||||
} catch (error) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
|
||||
}
|
||||
});
|
||||
} else if (req.method === 'GET' && url.pathname === '/tools') {
|
||||
// Get available tools
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
|
||||
if (sessionId) {
|
||||
// Get tools for specific session
|
||||
const tools = this.getAvailableTools(sessionId);
|
||||
res.end(JSON.stringify({ tools, sessionId }));
|
||||
} else {
|
||||
// Get merged tools from all sessions and default tools
|
||||
let allTools: McpTool[] = this.getAvailableTools(); // Default tools
|
||||
|
||||
// Add session-specific tools (session tools override defaults by name)
|
||||
const toolMap = new Map<string, McpTool>();
|
||||
allTools.forEach(tool => toolMap.set(tool.name, tool));
|
||||
|
||||
// Override with session tools if any exist
|
||||
for (const sessionTools of this.sessionTools.values()) {
|
||||
for (const tool of sessionTools.values()) {
|
||||
toolMap.set(tool.name, tool);
|
||||
}
|
||||
}
|
||||
|
||||
const finalTools = Array.from(toolMap.values());
|
||||
res.end(JSON.stringify({ tools: finalTools, merged: true }));
|
||||
}
|
||||
} else if (req.method === 'POST' && url.pathname === '/reload') {
|
||||
// Reload workspace overrides
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { workspacePath } = JSON.parse(body);
|
||||
this.reloadOverrides();
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
} catch (error) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Session endpoint not found');
|
||||
@@ -558,11 +704,18 @@ export class McpServer extends EventEmitter {
|
||||
sessionIdToUse = message.params.sessionId;
|
||||
}
|
||||
|
||||
this.debugLogger.log('TOOLS', `tools/list request - Extension sessionId: ${this.sessionId}, Message params: ${JSON.stringify(message.params)}`);
|
||||
this.debugLogger.log('TOOLS', `Final sessionIdToUse: ${sessionIdToUse || 'default'}`);
|
||||
|
||||
const tools = this.getAvailableTools(sessionIdToUse);
|
||||
|
||||
this.debugLogger.log('TOOLS', `Returning ${tools.length} tools for session: ${sessionIdToUse || 'default'}`);
|
||||
if (sessionIdToUse) {
|
||||
this.debugLogger.log('TOOLS', `Using session-specific tools for: ${sessionIdToUse}`);
|
||||
const sessionTools = this.sessionTools.get(sessionIdToUse);
|
||||
if (sessionTools) {
|
||||
this.debugLogger.log('TOOLS', `Session tools found: ${Array.from(sessionTools.keys()).join(', ')}`);
|
||||
}
|
||||
} else {
|
||||
this.debugLogger.log('TOOLS', `Using default tools (no session ID available)`);
|
||||
}
|
||||
@@ -737,11 +890,13 @@ export class McpServer extends EventEmitter {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
registerSession(sessionId: string, workspacePath?: string): void {
|
||||
registerSession(sessionId: string, workspacePath?: string, overrideData?: any): void {
|
||||
this.activeSessions.add(sessionId);
|
||||
|
||||
// Initialize session-specific tools if workspace path provided
|
||||
if (workspacePath) {
|
||||
// Initialize session-specific tools from override data or workspace path
|
||||
if (overrideData) {
|
||||
this.initializeSessionToolsFromData(sessionId, overrideData);
|
||||
} else if (workspacePath) {
|
||||
this.initializeSessionTools(sessionId, workspacePath);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as net from 'net';
|
||||
|
||||
export interface ServerManagerOptions {
|
||||
serverPath: string;
|
||||
port: number;
|
||||
host?: string;
|
||||
logFile?: string;
|
||||
}
|
||||
|
||||
export class ServerManager {
|
||||
private static instance: ServerManager | undefined;
|
||||
private options: ServerManagerOptions;
|
||||
private readonly pidFile: string;
|
||||
private readonly logFile: string;
|
||||
|
||||
private constructor(options: ServerManagerOptions) {
|
||||
this.options = {
|
||||
host: '127.0.0.1',
|
||||
...options
|
||||
};
|
||||
this.pidFile = path.join(path.dirname(options.serverPath), '.humanagent-mcp-server.pid');
|
||||
this.logFile = options.logFile || path.join(path.dirname(options.serverPath), '.humanagent-mcp-server.log');
|
||||
}
|
||||
|
||||
public static getInstance(options?: ServerManagerOptions): ServerManager {
|
||||
if (!ServerManager.instance) {
|
||||
if (!options) {
|
||||
throw new Error('ServerManager options required for first initialization');
|
||||
}
|
||||
ServerManager.instance = new ServerManager(options);
|
||||
}
|
||||
return ServerManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server is already running by testing the port
|
||||
*/
|
||||
public async isServerRunning(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
|
||||
socket.setTimeout(1000);
|
||||
socket.on('connect', () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(this.options.port, this.options.host!);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a PID file and if that process is still running
|
||||
*/
|
||||
private async isProcessRunning(pid: number): Promise<boolean> {
|
||||
try {
|
||||
// On Unix systems, sending signal 0 checks if process exists
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PID from the PID file if it exists
|
||||
*/
|
||||
private async getStoredPid(): Promise<number | undefined> {
|
||||
try {
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
const pidStr = fs.readFileSync(this.pidFile, 'utf-8').trim();
|
||||
const pid = parseInt(pidStr, 10);
|
||||
return isNaN(pid) ? undefined : pid;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error reading PID file:', error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the PID in the PID file
|
||||
*/
|
||||
private async storePid(pid: number): Promise<void> {
|
||||
try {
|
||||
fs.writeFileSync(this.pidFile, pid.toString());
|
||||
} catch (error) {
|
||||
console.error('Error writing PID file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the PID file
|
||||
*/
|
||||
private async cleanupPidFile(): Promise<void> {
|
||||
try {
|
||||
if (fs.existsSync(this.pidFile)) {
|
||||
fs.unlinkSync(this.pidFile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error cleaning up PID file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the log file
|
||||
*/
|
||||
private log(message: string): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] ${message}\n`;
|
||||
|
||||
try {
|
||||
fs.appendFileSync(this.logFile, logMessage);
|
||||
} catch (error) {
|
||||
console.error('Error writing to log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server if it's not already running
|
||||
*/
|
||||
public async ensureServerRunning(): Promise<boolean> {
|
||||
try {
|
||||
// First check if server is responding on the port
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Server is already running and responding');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we have a stored PID and if that process is running
|
||||
const storedPid = await this.getStoredPid();
|
||||
if (storedPid && await this.isProcessRunning(storedPid)) {
|
||||
this.log(`Found running server process with PID ${storedPid}, but it's not responding on port. Waiting...`);
|
||||
// Wait a moment for the server to start listening
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Server is now responding');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stale PID file
|
||||
await this.cleanupPidFile();
|
||||
|
||||
// Start new server process
|
||||
this.log(`Starting new server process: node ${this.options.serverPath}`);
|
||||
return await this.startServer();
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Error ensuring server is running: ${error}`);
|
||||
vscode.window.showErrorMessage(`Failed to start HumanAgent MCP Server: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server as a truly independent detached process
|
||||
*/
|
||||
private async startServer(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// Spawn the server as a completely detached process
|
||||
const serverProcess = spawn('node', [this.options.serverPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore', // Completely disconnect stdio to make it independent
|
||||
cwd: path.dirname(this.options.serverPath),
|
||||
env: {
|
||||
...process.env,
|
||||
// Add any environment variables needed by the server
|
||||
}
|
||||
});
|
||||
|
||||
// Store the PID but don't keep a reference to the process
|
||||
if (serverProcess.pid) {
|
||||
this.storePid(serverProcess.pid);
|
||||
this.log(`Started independent server with PID ${serverProcess.pid}`);
|
||||
}
|
||||
|
||||
// Immediately detach and unreference the process
|
||||
serverProcess.unref();
|
||||
|
||||
// Don't keep a reference to the process object to ensure complete detachment
|
||||
// this.serverProcess = undefined;
|
||||
|
||||
this.log('Server process started as independent background process');
|
||||
|
||||
// Wait for server to start listening
|
||||
setTimeout(async () => {
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Independent server started successfully and is responding');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.log('Independent server started but is not responding on the expected port yet');
|
||||
// Give it a bit more time
|
||||
setTimeout(async () => {
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Independent server is now responding');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.log('Independent server failed to start or is not responding');
|
||||
this.cleanupPidFile();
|
||||
resolve(false);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Failed to start server: ${error}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server if it's running
|
||||
*/
|
||||
public async stopServer(): Promise<boolean> {
|
||||
try {
|
||||
const storedPid = await this.getStoredPid();
|
||||
|
||||
if (storedPid) {
|
||||
if (await this.isProcessRunning(storedPid)) {
|
||||
this.log(`Stopping independent server with PID ${storedPid}`);
|
||||
process.kill(storedPid, 'SIGTERM');
|
||||
|
||||
// Wait for graceful shutdown
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Force kill if still running
|
||||
if (await this.isProcessRunning(storedPid)) {
|
||||
this.log(`Force killing independent server with PID ${storedPid}`);
|
||||
process.kill(storedPid, 'SIGKILL');
|
||||
|
||||
// Wait a bit more and check again
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (await this.isProcessRunning(storedPid)) {
|
||||
this.log(`Warning: Server process ${storedPid} may still be running`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.log(`Server process ${storedPid} was not running`);
|
||||
}
|
||||
|
||||
await this.cleanupPidFile();
|
||||
} else {
|
||||
this.log('No PID file found, server may not be running');
|
||||
}
|
||||
|
||||
this.log('Server stop operation completed');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Error stopping server: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server status information
|
||||
*/
|
||||
public async getServerStatus(): Promise<{
|
||||
isRunning: boolean;
|
||||
pid?: number;
|
||||
port: number;
|
||||
host: string;
|
||||
serverPath: string;
|
||||
}> {
|
||||
const isRunning = await this.isServerRunning();
|
||||
const pid = await this.getStoredPid();
|
||||
|
||||
return {
|
||||
isRunning,
|
||||
pid: pid && await this.isProcessRunning(pid) ? pid : undefined,
|
||||
port: this.options.port,
|
||||
host: this.options.host!,
|
||||
serverPath: this.options.serverPath
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the extension deactivates
|
||||
*/
|
||||
public dispose(): void {
|
||||
// Note: We don't stop the server here because it should continue running
|
||||
// even if the extension is deactivated. The server will be stopped only
|
||||
// when explicitly requested or when VS Code completely closes.
|
||||
this.log('ServerManager disposed');
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewType = 'humanagent-mcp.chatView';
|
||||
|
||||
private _view?: vscode.WebviewView;
|
||||
private mcpServer: McpServer;
|
||||
private mcpServer: McpServer | null;
|
||||
private mcpConfigManager?: McpConfigManager;
|
||||
private extensionPath: string;
|
||||
private messages: ChatMessage[] = [];
|
||||
private currentRequestId?: string;
|
||||
private registrationCheckComplete = false;
|
||||
private notificationSettings = {
|
||||
enableSound: true,
|
||||
enableFlashing: true
|
||||
@@ -22,7 +23,7 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
constructor(
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
mcpServer: McpServer,
|
||||
mcpServer: McpServer | null,
|
||||
mcpConfigManager?: McpConfigManager,
|
||||
private readonly workspaceSessionId?: string
|
||||
) {
|
||||
@@ -126,12 +127,15 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
]
|
||||
};
|
||||
|
||||
this.updateWebview();
|
||||
// Only update webview if registration check is complete, otherwise it will be updated when notifyRegistrationComplete is called
|
||||
if (this.registrationCheckComplete) {
|
||||
this.updateWebview();
|
||||
}
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(async (data) => {
|
||||
switch (data.type) {
|
||||
case 'sendMessage':
|
||||
await this.sendHumanResponse(data.content);
|
||||
await this.sendHumanResponse(data.content, data.requestId);
|
||||
break;
|
||||
case 'mcpAction':
|
||||
await this.handleMcpAction(data.action);
|
||||
@@ -144,7 +148,7 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
}
|
||||
|
||||
private async sendHumanResponse(content: string) {
|
||||
private async sendHumanResponse(content: string, requestId?: string) {
|
||||
try {
|
||||
console.log('ChatWebviewProvider: Sending human response:', content);
|
||||
|
||||
@@ -160,16 +164,35 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
this.messages.push(humanMessage);
|
||||
this.updateWebview();
|
||||
|
||||
// Send response back to MCP server
|
||||
if (this.currentRequestId) {
|
||||
console.log('ChatWebviewProvider: Responding to request ID:', this.currentRequestId);
|
||||
const success = this.mcpServer.respondToHumanRequest(this.currentRequestId, content);
|
||||
if (success) {
|
||||
this.currentRequestId = undefined;
|
||||
this.updateWebview(); // Force UI update to clear "waiting" state
|
||||
} else {
|
||||
console.warn('ChatWebviewProvider: Failed to respond - request may have expired');
|
||||
// Send response back to standalone MCP server via HTTP
|
||||
const responseRequestId = requestId || this.currentRequestId;
|
||||
if (responseRequestId) {
|
||||
console.log('ChatWebviewProvider: Responding to request ID:', responseRequestId);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3737/response', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestId: responseRequestId,
|
||||
response: content
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('ChatWebviewProvider: Response sent successfully:', result);
|
||||
} else {
|
||||
console.error('ChatWebviewProvider: Failed to send response:', response.status, response.statusText);
|
||||
}
|
||||
} catch (httpError) {
|
||||
console.error('ChatWebviewProvider: HTTP error sending response:', httpError);
|
||||
}
|
||||
|
||||
this.currentRequestId = undefined;
|
||||
this.updateWebview(); // Force UI update to clear "waiting" state
|
||||
} else {
|
||||
console.warn('ChatWebviewProvider: No pending request to respond to');
|
||||
}
|
||||
@@ -189,13 +212,22 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public refreshWebview() {
|
||||
this.updateWebview();
|
||||
}
|
||||
|
||||
public notifyRegistrationComplete() {
|
||||
this.registrationCheckComplete = true;
|
||||
if (this._view) {
|
||||
this.updateWebview();
|
||||
}
|
||||
}
|
||||
|
||||
private updateServerStatus() {
|
||||
if (!this._view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tools = this.mcpServer.getAvailableTools();
|
||||
const pendingRequests = this.mcpServer.getPendingRequests();
|
||||
const isRegisteredWorkspace = this.mcpConfigManager?.isMcpServerRegistered(false) ?? false;
|
||||
const isRegisteredGlobal = this.mcpConfigManager?.isMcpServerRegistered(true) ?? false;
|
||||
const configType = isRegisteredWorkspace ? 'workspace' : (isRegisteredGlobal ? 'global' : 'none');
|
||||
@@ -203,9 +235,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
this._view.webview.postMessage({
|
||||
type: 'serverStatus',
|
||||
data: {
|
||||
running: this.mcpServer.isServerRunning(),
|
||||
tools: tools.length,
|
||||
pendingRequests: pendingRequests.length,
|
||||
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
|
||||
}
|
||||
@@ -231,13 +263,45 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restart the session to pick up new overrides (ensures fresh tool loading)
|
||||
if (this.workspaceSessionId) {
|
||||
await this.mcpServer.restartSession(this.workspaceSessionId);
|
||||
vscode.window.showInformationMessage('Override file reloaded - session restarted with fresh tools');
|
||||
} else {
|
||||
await this.mcpServer.reloadOverrides();
|
||||
vscode.window.showInformationMessage('Override file reloaded successfully');
|
||||
// Force session re-registration with fresh override data
|
||||
try {
|
||||
// Read the current override file
|
||||
let overrideData = null;
|
||||
if (fs.existsSync(overrideFilePath)) {
|
||||
const overrideContent = fs.readFileSync(overrideFilePath, 'utf8');
|
||||
overrideData = JSON.parse(overrideContent);
|
||||
}
|
||||
|
||||
// Get current sessions and re-register them with fresh data
|
||||
const sessionsResponse = await fetch('http://localhost:3737/sessions');
|
||||
if (sessionsResponse.ok) {
|
||||
const sessionsData = await sessionsResponse.json() as { sessions: string[] };
|
||||
|
||||
for (const sessionId of sessionsData.sessions) {
|
||||
const response = await fetch('http://localhost:3737/sessions/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
overrideData: overrideData,
|
||||
forceReregister: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to re-register session ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage('Override file reloaded successfully!');
|
||||
} else {
|
||||
vscode.window.showWarningMessage('Failed to get sessions from server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reload override file:', error);
|
||||
vscode.window.showWarningMessage('Could not communicate with MCP server for reload');
|
||||
}
|
||||
|
||||
// Refresh the webview to update the menu
|
||||
@@ -283,40 +347,19 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Get default tool configuration
|
||||
const defaultTool = {
|
||||
name: 'HumanAgent_Chat',
|
||||
description: 'Initiate real-time interactive conversations with human agents through VS Code chat interface. Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI uncertainty requires human input. Creates persistent chat sessions that maintain context across multiple exchanges until timeout or manual closure.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'The message to send to the human agent'
|
||||
},
|
||||
context: {
|
||||
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'],
|
||||
description: 'Priority level of the request',
|
||||
default: 'normal'
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Timeout in seconds to wait for human response (default: 300)',
|
||||
default: 300
|
||||
}
|
||||
},
|
||||
required: ['message']
|
||||
}
|
||||
};
|
||||
// Get current tool configuration from server - NO FALLBACKS!
|
||||
const response = await fetch('http://localhost:3737/tools');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch tools from server: ${response.status}`);
|
||||
}
|
||||
|
||||
const toolsData = await response.json() as { tools: any[] };
|
||||
const defaultTool = toolsData.tools.find((tool: any) => tool.name === 'HumanAgent_Chat');
|
||||
if (!defaultTool) {
|
||||
throw new Error('HumanAgent_Chat tool not found on server');
|
||||
}
|
||||
|
||||
console.log('ChatWebviewProvider: Fetched current tool configuration from server');
|
||||
|
||||
// Create example tool with medium detail
|
||||
const exampleTool = {
|
||||
@@ -373,24 +416,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'start':
|
||||
await this.mcpServer.start();
|
||||
vscode.window.showInformationMessage('MCP Server started');
|
||||
break;
|
||||
case 'stop':
|
||||
await this.mcpServer.stop();
|
||||
vscode.window.showInformationMessage('MCP Server stopped');
|
||||
break;
|
||||
case 'restart':
|
||||
try {
|
||||
await this.mcpServer.stop();
|
||||
// Small delay to ensure complete cleanup before restart
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await this.mcpServer.start();
|
||||
vscode.window.showInformationMessage('MCP Server restarted');
|
||||
} catch (error) {
|
||||
console.error('Error during MCP server restart:', error);
|
||||
vscode.window.showErrorMessage('Failed to restart MCP Server');
|
||||
}
|
||||
vscode.window.showInformationMessage('MCP Server management not available - using standalone server');
|
||||
break;
|
||||
case 'register':
|
||||
// Use the MCP configuration from the parent command
|
||||
@@ -824,14 +852,47 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (message) {
|
||||
// Add user message to chat
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
// Remove waiting indicator
|
||||
const waitingIndicator = messagesContainer.querySelector('.waiting-indicator');
|
||||
if (waitingIndicator) {
|
||||
waitingIndicator.remove();
|
||||
}
|
||||
|
||||
// Add user message
|
||||
const userMessageDiv = document.createElement('div');
|
||||
userMessageDiv.className = 'message user-message';
|
||||
userMessageDiv.innerHTML = \`
|
||||
<div class="message-header">
|
||||
<strong>You</strong>
|
||||
<span class="timestamp">\${new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<div class="message-content">\${message.replace(/\\n/g, '<br>')}</div>
|
||||
\`;
|
||||
messagesContainer.appendChild(userMessageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Send message to extension
|
||||
vscode.postMessage({
|
||||
type: 'sendMessage',
|
||||
content: message
|
||||
content: message,
|
||||
requestId: currentPendingRequestId
|
||||
});
|
||||
|
||||
// Clear input and disable controls
|
||||
input.value = '';
|
||||
input.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
|
||||
// Clear the pending request ID
|
||||
currentPendingRequestId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,7 +906,7 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
// Default options when status unknown
|
||||
const defaultOptions = [
|
||||
{ text: '📦 Install Globally', action: 'register' },
|
||||
{ text: '📁 Install in Workspace', action: 'unregister' },
|
||||
{ text: '📁 Install in Workspace', action: 'register' },
|
||||
{ text: '📊 Show Status', action: 'requestServerStatus' },
|
||||
{ text: '🛠️ Override Prompt', action: 'overridePrompt' }
|
||||
];
|
||||
@@ -869,7 +930,7 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
} else {
|
||||
// Not installed anywhere
|
||||
options.push({ text: '📦 Install Globally', action: 'register' });
|
||||
options.push({ text: '📁 Install in Workspace', action: 'unregister' });
|
||||
options.push({ text: '📁 Install in Workspace', action: 'register' });
|
||||
}
|
||||
|
||||
options.push({ text: '📊 Show Status', action: 'requestServerStatus' });
|
||||
@@ -933,6 +994,105 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
playNotificationBeep();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up SSE connection for real-time server events
|
||||
function setupSSEConnection() {
|
||||
try {
|
||||
console.log('Setting up SSE connection to MCP server...');
|
||||
const eventSource = new EventSource('http://localhost:3737/mcp');
|
||||
|
||||
eventSource.onopen = function(event) {
|
||||
console.log('SSE connection opened:', event);
|
||||
};
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE event received:', data);
|
||||
|
||||
if (data.type === 'human-agent-request') {
|
||||
handleHumanAgentRequest(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function(error) {
|
||||
console.error('SSE connection error:', error);
|
||||
// Try to reconnect after 5 seconds
|
||||
setTimeout(setupSSEConnection, 5000);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to setup SSE connection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to store current request ID for responses
|
||||
let currentPendingRequestId = null;
|
||||
|
||||
function handleHumanAgentRequest(data) {
|
||||
console.log('Handling human agent request:', data);
|
||||
|
||||
// Store the request ID for sending response
|
||||
currentPendingRequestId = data.requestId;
|
||||
|
||||
// Add the AI message to chat
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
// Remove empty state if it exists
|
||||
const emptyState = messagesContainer.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message ai-message';
|
||||
|
||||
const displayMessage = data.context ? \`\${data.context}\\n\\n\${data.message}\` : data.message;
|
||||
messageDiv.innerHTML = \`
|
||||
<div class="message-header">
|
||||
<strong>AI Agent</strong>
|
||||
<span class="timestamp">\${new Date(data.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<div class="message-content">\${displayMessage.replace(/\\n/g, '<br>')}</div>
|
||||
\`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
// Enable input controls for response
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
if (messageInput && sendButton) {
|
||||
messageInput.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
// Add waiting indicator if not present
|
||||
const existingWaiting = messagesContainer.querySelector('.waiting-indicator');
|
||||
if (!existingWaiting) {
|
||||
const waitingDiv = document.createElement('div');
|
||||
waitingDiv.className = 'waiting-indicator';
|
||||
waitingDiv.textContent = '⏳ Waiting for your response...';
|
||||
messagesContainer.appendChild(waitingDiv);
|
||||
}
|
||||
|
||||
// Play notification
|
||||
playNotificationBeep();
|
||||
|
||||
// Flash border
|
||||
document.body.classList.add('flashing');
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('flashing');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize SSE connection
|
||||
setupSSEConnection();
|
||||
|
||||
// Webview initialized - status can be requested manually via cog menu
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const net = require('net');
|
||||
const http = require('http');
|
||||
|
||||
async function isPortInUseNet(port) {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`NET: Port ${port} available - server created successfully`);
|
||||
server.close(() => resolve(false)); // Port is available
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.log(`NET: Port ${port} in use - error: ${err.message}`);
|
||||
resolve(true); // Port is in use
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function isPortInUseHttp(port) {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer();
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`HTTP: Port ${port} available - server created successfully`);
|
||||
server.close(() => resolve(false)); // Port is available
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.log(`HTTP: Port ${port} in use - error: ${err.message}`);
|
||||
resolve(true); // Port is in use
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Testing port 3737...');
|
||||
Promise.all([isPortInUseNet(3737), isPortInUseHttp(3737)]).then(([netResult, httpResult]) => {
|
||||
console.log(`NET result - Port 3737 in use: ${netResult}`);
|
||||
console.log(`HTTP result - Port 3737 in use: ${httpResult}`);
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user