Fix sessionless HumanAgent chat functionality

- Remove session management complexity from MCP server and UI
- Update chatWebviewProvider for direct messaging without sessions
- Fix MCP tool response format to use content array as per specification
- Restore cog icon for MCP configuration (install global/local)
- Add safety checks for message content handling
- Resolve 'o.content is not iterable' error by using proper MCP format
This commit is contained in:
B Harper
2025-10-22 11:49:17 +11:00
commit a414a4f70d
32 changed files with 8179 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
import * as vscode from 'vscode';
import { McpServer } from './mcp/server';
import { ChatTreeProvider } from './providers/chatTreeProvider';
import { ChatWebviewProvider } from './webview/chatWebviewProvider';
import { McpConfigManager } from './mcp/mcpConfigManager';
let mcpServer: McpServer;
let chatTreeProvider: ChatTreeProvider;
let mcpConfigManager: McpConfigManager;
export async function activate(context: vscode.ExtensionContext) {
console.log('HumanAgent MCP extension is now active!');
// Initialize MCP Configuration Manager
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
mcpConfigManager = new McpConfigManager(workspaceRoot, context.extensionPath);
// Initialize MCP Server (internal to extension)
mcpServer = new McpServer();
await mcpServer.start();
// Initialize Tree View Provider
chatTreeProvider = new ChatTreeProvider();
const treeView = vscode.window.createTreeView('humanagent-mcp.chatSessions', {
treeDataProvider: chatTreeProvider,
showCollapseAll: true
});
// Initialize Chat Webview Provider
const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, mcpServer, mcpConfigManager);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ChatWebviewProvider.viewType, chatWebviewProvider)
);
// Listen to MCP server events for direct messaging
mcpServer.on('human-agent-request', (data: any) => {
// Update tree view to show active chat
chatTreeProvider.updateActiveChat(true);
// Ensure chat webview displays the message and sets up response handling
chatWebviewProvider.displayHumanAgentMessage(data.message, data.context, data.requestId);
});
// Register Commands
const openChatCommand = vscode.commands.registerCommand('humanagent-mcp.openChat', () => {
// Focus the chat webview
vscode.commands.executeCommand('humanagent-mcp.chatView.focus');
});
const createSessionCommand = vscode.commands.registerCommand('humanagent-mcp.createSession', async () => {
// In sessionless mode, just open the chat view
vscode.commands.executeCommand('humanagent-mcp.chatView.focus');
vscode.window.showInformationMessage(`Chat interface ready for HumanAgent communication`);
});
const refreshSessionsCommand = vscode.commands.registerCommand('humanagent-mcp.refreshSessions', () => {
// In sessionless mode, just update the tree view
chatTreeProvider.refresh();
});
const showStatusCommand = vscode.commands.registerCommand('humanagent-mcp.showStatus', () => {
const tools = mcpServer.getAvailableTools();
const pendingRequests = mcpServer.getPendingRequests();
const isRegistered = mcpConfigManager?.isMcpServerRegistered() ?? false;
vscode.window.showInformationMessage(
`HumanAgent MCP Server Status:
- Running: ✅
- Available tools: ${tools.length}
- Pending requests: ${pendingRequests.length}
- Registered with VS Code: ${isRegistered ? '✅' : '❌'}`
);
});
const configureMcpCommand = vscode.commands.registerCommand('humanagent-mcp.configureMcp', async () => {
const hasWorkspace = mcpConfigManager?.hasWorkspace() ?? false;
const isWorkspaceRegistered = mcpConfigManager?.isMcpServerRegistered(false) ?? false;
const isGlobalRegistered = mcpConfigManager?.isMcpServerRegistered(true) ?? false;
const options = [];
if (hasWorkspace) {
if (isWorkspaceRegistered) {
options.push('🗑️ Unregister from This Workspace');
} else {
options.push('📝 Register for This Workspace');
}
}
if (isGlobalRegistered) {
options.push('🗑️ Unregister Globally');
} else {
options.push('🌐 Register Globally');
}
if (hasWorkspace) {
options.push('📄 Open Workspace Configuration');
}
options.push('📊 Show Status');
const action = await vscode.window.showQuickPick(options, {
placeHolder: 'Choose MCP Server configuration action:'
});
if (!action) {
return;
}
try {
switch (action) {
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.');
break;
case '🌐 Register Globally':
await mcpConfigManager!.ensureMcpServerRegistered(true);
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':
const tools = mcpServer.getAvailableTools();
const pendingRequests = mcpServer.getPendingRequests();
vscode.window.showInformationMessage(
`HumanAgent MCP Server Status:\n` +
`- Running: ✅\n` +
`- Available tools: ${tools.length}\n` +
`- Pending requests: ${pendingRequests.length}\n` +
`- Workspace registration: ${isWorkspaceRegistered ? '✅' : '❌'}\n` +
`- Global registration: ${isGlobalRegistered ? '✅' : '❌'}`
);
break;
}
} catch (error) {
vscode.window.showErrorMessage(`MCP configuration failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
// Add all disposables to context
context.subscriptions.push(
treeView,
openChatCommand,
createSessionCommand,
refreshSessionsCommand,
showStatusCommand,
configureMcpCommand
);
// Show welcome message
vscode.window.showInformationMessage('HumanAgent MCP extension activated successfully!');
}
export async function deactivate() {
if (mcpServer) {
await mcpServer.stop();
}
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Extension Bridge for MCP
* This script acts as a bridge between VS Code's MCP client and the extension's internal MCP server
* It uses VS Code's extension API to communicate with the running extension
*/
import * as vscode from 'vscode';
class ExtensionBridge {
async start() {
// This script will be executed when VS Code connects to the MCP server
// We need to find a way to communicate with the extension's internal McpServer
// For now, use stdio communication
process.stdin.on('data', async (data) => {
try {
const input = data.toString().trim();
if (!input) {
return;
}
const message = JSON.parse(input);
// Try to get the extension and forward the message
const extension = vscode.extensions.getExtension('your-extension-id');
if (extension && extension.isActive) {
// This won't work because this script runs in a separate process
// We need a different approach
}
// For now, return an error
const errorResponse = {
id: message.id,
type: 'response',
error: {
code: -32603,
message: 'Extension bridge not implemented yet'
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
} catch (error) {
const errorResponse = {
id: null,
type: 'response',
error: {
code: -32700,
message: 'Parse error',
data: error instanceof Error ? error.message : String(error)
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
}
}
// Start the bridge
const bridge = new ExtensionBridge();
bridge.start().catch((error) => {
console.error('Failed to start extension bridge:', error);
process.exit(1);
});
+233
View File
@@ -0,0 +1,233 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
export interface McpServerConfig {
type: 'stdio' | 'sse' | 'http';
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
}
export interface McpConfiguration {
servers: Record<string, McpServerConfig>;
inputs?: any[];
}
export class McpConfigManager {
private static readonly MCP_CONFIG_FILE = '.vscode/mcp.json';
private static readonly SERVER_NAME = 'humanagent-mcp';
private static readonly GLOBAL_CONFIG_KEY = 'mcp.servers';
constructor(private workspaceRoot?: string, private extensionPath?: string) {
if (!extensionPath) {
throw new Error('Extension path is required');
}
}
async ensureMcpServerRegistered(global: boolean = false): Promise<boolean> {
if (global) {
return this.registerGlobally();
} else {
return this.registerInWorkspace();
}
}
private async registerInWorkspace(): Promise<boolean> {
const currentWorkspaceRoot = this.getCurrentWorkspaceRoot();
if (!currentWorkspaceRoot) {
throw new Error('No workspace folder available for workspace registration - this is a blank workspace');
}
if (!this.extensionPath) {
throw new Error('Extension path not provided');
}
try {
const mcpConfigPath = path.join(currentWorkspaceRoot, McpConfigManager.MCP_CONFIG_FILE);
// Ensure .vscode directory exists
const vscodeDirPath = path.dirname(mcpConfigPath);
if (!fs.existsSync(vscodeDirPath)) {
fs.mkdirSync(vscodeDirPath, { recursive: true });
}
// Read existing config or create new one
let config: McpConfiguration = { servers: {}, inputs: [] };
if (fs.existsSync(mcpConfigPath)) {
try {
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
config = JSON.parse(configContent);
} catch (error) {
console.warn('Failed to parse existing mcp.json, creating new one', error);
}
}
// Check if our server is already registered
if (config.servers[McpConfigManager.SERVER_NAME]) {
return true; // Already configured
}
// Use the extension path passed during construction
// Configure our MCP server (HTTP transport)
const serverConfig: McpServerConfig = {
type: 'http',
url: 'http://127.0.0.1:3737/mcp'
};
// Add our server to the config
config.servers[McpConfigManager.SERVER_NAME] = serverConfig;
// Write the updated config
fs.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2));
return true;
} catch (error) {
console.error('Failed to register MCP server in workspace:', error);
throw error;
}
}
private async registerGlobally(): Promise<boolean> {
if (!this.extensionPath) {
throw new Error('Extension path not provided');
}
try {
// Use the extension path passed during construction
// Configure our MCP server (HTTP transport)
const serverConfig: McpServerConfig = {
type: 'http',
url: 'http://127.0.0.1:3737/mcp'
};
// Get current global MCP servers configuration
const config = vscode.workspace.getConfiguration();
const mcpServers = config.get<Record<string, McpServerConfig>>(McpConfigManager.GLOBAL_CONFIG_KEY) || {};
// Add our server
mcpServers[McpConfigManager.SERVER_NAME] = serverConfig;
// Update global configuration
await config.update(McpConfigManager.GLOBAL_CONFIG_KEY, mcpServers, vscode.ConfigurationTarget.Global);
return true;
} catch (error) {
console.error('Failed to register MCP server globally:', error);
throw error;
}
}
async removeMcpServerRegistration(global: boolean = false): Promise<boolean> {
if (global) {
return this.unregisterGlobally();
} else {
return this.unregisterFromWorkspace();
}
}
private async unregisterFromWorkspace(): Promise<boolean> {
const currentWorkspaceRoot = this.getCurrentWorkspaceRoot();
if (!currentWorkspaceRoot) {
throw new Error('No workspace folder available for workspace unregistration');
}
try {
const mcpConfigPath = path.join(currentWorkspaceRoot, McpConfigManager.MCP_CONFIG_FILE);
if (!fs.existsSync(mcpConfigPath)) {
return true; // Nothing to remove
}
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
const config: McpConfiguration = JSON.parse(configContent);
if (config.servers[McpConfigManager.SERVER_NAME]) {
delete config.servers[McpConfigManager.SERVER_NAME];
fs.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2));
}
return true;
} catch (error) {
console.error('Failed to remove MCP server registration from workspace:', error);
throw error;
}
}
private async unregisterGlobally(): Promise<boolean> {
try {
const config = vscode.workspace.getConfiguration();
const mcpServers = config.get<Record<string, McpServerConfig>>(McpConfigManager.GLOBAL_CONFIG_KEY) || {};
if (mcpServers[McpConfigManager.SERVER_NAME]) {
// Create a new object without the server instead of mutating the original
const updatedServers = { ...mcpServers };
delete updatedServers[McpConfigManager.SERVER_NAME];
await config.update(McpConfigManager.GLOBAL_CONFIG_KEY, updatedServers, vscode.ConfigurationTarget.Global);
}
return true;
} catch (error) {
console.error('Failed to remove MCP server registration globally:', error);
throw error;
}
}
isMcpServerRegistered(global: boolean = false): boolean {
if (global) {
return this.isRegisteredGlobally();
} else {
return this.isRegisteredInWorkspace();
}
}
private isRegisteredInWorkspace(): boolean {
const currentWorkspaceRoot = this.getCurrentWorkspaceRoot();
if (!currentWorkspaceRoot) {
return false;
}
try {
const mcpConfigPath = path.join(currentWorkspaceRoot, McpConfigManager.MCP_CONFIG_FILE);
if (!fs.existsSync(mcpConfigPath)) {
return false;
}
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
const config: McpConfiguration = JSON.parse(configContent);
return !!config.servers[McpConfigManager.SERVER_NAME];
} catch (error) {
console.error('Failed to check MCP server registration in workspace:', error);
return false;
}
}
private isRegisteredGlobally(): boolean {
try {
// Explicitly check ONLY global configuration, not workspace-merged config
const config = vscode.workspace.getConfiguration();
const globalServers = config.inspect<Record<string, McpServerConfig>>(McpConfigManager.GLOBAL_CONFIG_KEY);
// Only check the globalValue, not the merged value
const mcpServers = globalServers?.globalValue || {};
return !!mcpServers[McpConfigManager.SERVER_NAME];
} catch (error) {
console.error('Failed to check global MCP server registration:', error);
return false;
}
}
hasWorkspace(): boolean {
// Check current workspace state dynamically
return !!vscode.workspace.workspaceFolders?.[0];
}
private getCurrentWorkspaceRoot(): string | undefined {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
}
+233
View File
@@ -0,0 +1,233 @@
import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as path from 'path';
import { EventEmitter } from 'events';
import { HumanAgentSession, ChatMessage, McpTool } from './types';
export class McpServerClient extends EventEmitter {
private serverProcess: cp.ChildProcess | null = null;
private isConnected = false;
private extensionPath: string;
private pendingRequests = new Map<string, { resolve: Function; reject: Function }>();
private requestId = 1;
constructor(extensionPath: string) {
super();
this.extensionPath = extensionPath;
}
async start(): Promise<void> {
if (this.isConnected) {
return;
}
try {
const serverPath = path.join(this.extensionPath, 'dist', 'mcpStandalone.js');
console.log('Starting MCP server client:', serverPath);
this.serverProcess = cp.spawn('node', [serverPath], {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
env: {
...process.env,
NODE_ENV: 'production'
}
});
this.serverProcess.on('spawn', () => {
console.log('MCP server process spawned for client connection');
this.isConnected = true;
this.emit('connected');
});
this.serverProcess.on('error', (error) => {
console.error('MCP server client process error:', error);
this.isConnected = false;
this.emit('error', error);
});
this.serverProcess.on('exit', (code, signal) => {
console.log(`MCP server client process exited with code ${code}, signal ${signal}`);
this.isConnected = false;
this.emit('disconnected');
});
// Handle server responses
if (this.serverProcess.stdout) {
this.serverProcess.stdout.on('data', (data) => {
this.handleServerResponse(data.toString());
});
}
// Give the process a moment to start
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error('Failed to start MCP server client:', error);
this.isConnected = false;
throw error;
}
}
async stop(): Promise<void> {
if (!this.isConnected || !this.serverProcess) {
return;
}
try {
console.log('Stopping MCP server client...');
// Try graceful shutdown first
this.serverProcess.kill('SIGTERM');
// Wait for graceful shutdown
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
// Force kill if graceful shutdown didn't work
if (this.serverProcess && !this.serverProcess.killed) {
console.log('Force killing MCP server client process...');
this.serverProcess.kill('SIGKILL');
}
resolve();
}, 5000);
if (this.serverProcess) {
this.serverProcess.on('exit', () => {
clearTimeout(timeout);
resolve();
});
} else {
clearTimeout(timeout);
resolve();
}
});
this.isConnected = false;
this.serverProcess = null;
} catch (error) {
console.error('Failed to stop MCP server client:', error);
throw error;
}
}
private handleServerResponse(data: string): void {
try {
const lines = data.trim().split('\n');
for (const line of lines) {
if (line.trim()) {
const response = JSON.parse(line);
if (response.id && this.pendingRequests.has(response.id)) {
const { resolve, reject } = this.pendingRequests.get(response.id)!;
this.pendingRequests.delete(response.id);
if (response.error) {
reject(new Error(response.error.message || 'Server error'));
} else {
resolve(response.result);
}
} else {
// Handle server events/notifications
this.handleServerEvent(response);
}
}
}
} catch (error) {
console.error('Failed to parse server response:', error);
}
}
private handleServerEvent(event: any): void {
// Handle server-sent events (like session updates, new messages, etc.)
switch (event.method) {
case 'session/created':
this.emit('session-created', event.params);
break;
case 'message/received':
this.emit('message-received', event.params);
break;
case 'message/sent':
this.emit('message-sent', event.params);
break;
case 'human/awaiting-response':
this.emit('awaiting-human-response', event.params);
break;
case 'server/started':
console.log('MCP server started:', event.params);
break;
default:
console.log('Unknown server event:', event);
}
}
private async sendRequest(method: string, params?: any): Promise<any> {
if (!this.isConnected || !this.serverProcess?.stdin) {
throw new Error('MCP server client not connected');
}
const id = (this.requestId++).toString();
const request = {
jsonrpc: '2.0',
id,
method,
params: params || {}
};
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
// Set timeout for request
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error(`Request timeout: ${method}`));
}
}, 10000);
this.serverProcess!.stdin!.write(JSON.stringify(request) + '\n');
});
}
// Public API methods using MCP protocol
async getAllSessions(): Promise<HumanAgentSession[]> {
const response = await this.sendRequest('chat/list-sessions', {});
return response.sessions || [];
}
async createSession(name: string): Promise<HumanAgentSession> {
const response = await this.sendRequest('chat/create-session', { name });
return response.session;
}
async sendMessage(sessionId: string, content: string): Promise<ChatMessage> {
const response = await this.sendRequest('chat/send', { sessionId, content });
return response.message;
}
async sendToHuman(message: string, context?: string, sessionId?: string): Promise<string> {
const response = await this.sendRequest('tools/call', {
name: 'HumanAgent_Chat',
arguments: { message, context, sessionId }
});
return response.result?.response || '';
}
async getAvailableTools(): Promise<McpTool[]> {
const response = await this.sendRequest('tools/list', {});
return response.tools || [];
}
async getPendingRequests(): Promise<any[]> {
// This would need to be implemented in the server if needed
return [];
}
isServerConnected(): boolean {
return this.isConnected;
}
getServerPid(): number | undefined {
return this.serverProcess?.pid;
}
}
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env node
/**
* Standalone MCP Server Entry Point
* This script runs the HumanAgent MCP server as a standalone process
* that can be connected to by VS Code's MCP client
*/
import { McpServer } from './server';
class StandaloneMcpServer {
private server: McpServer;
constructor() {
this.server = new McpServer();
this.setupProcessHandlers();
}
private setupProcessHandlers(): void {
// Handle STDIO communication for MCP protocol
process.stdin.on('data', async (data) => {
try {
const input = data.toString().trim();
if (!input) {
return;
}
const message = JSON.parse(input);
const response = await this.server.handleMessage(message);
if (response) {
process.stdout.write(JSON.stringify(response) + '\n');
}
} catch (error) {
const errorResponse = {
id: null,
type: 'response',
error: {
code: -32700,
message: 'Parse error',
data: error instanceof Error ? error.message : String(error)
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
// Handle server events and forward them as notifications
this.server.on('session-created', (session) => {
const notification = {
type: 'notification',
method: 'session/created',
params: { session }
};
process.stdout.write(JSON.stringify(notification) + '\n');
});
this.server.on('message-received', (data) => {
const notification = {
type: 'notification',
method: 'message/received',
params: data
};
process.stdout.write(JSON.stringify(notification) + '\n');
});
this.server.on('message-sent', (data) => {
const notification = {
type: 'notification',
method: 'message/sent',
params: data
};
process.stdout.write(JSON.stringify(notification) + '\n');
});
this.server.on('awaiting-human-response', (data) => {
const notification = {
type: 'notification',
method: 'human/awaiting-response',
params: data
};
process.stdout.write(JSON.stringify(notification) + '\n');
});
// Graceful shutdown
process.on('SIGINT', () => this.shutdown());
process.on('SIGTERM', () => this.shutdown());
process.on('exit', () => this.shutdown());
}
async start(): Promise<void> {
try {
await this.server.start();
// Send initialization notification
const initNotification = {
type: 'notification',
method: 'server/started',
params: {
name: 'HumanAgent MCP Server',
version: '1.0.0',
capabilities: {
chat: true,
tools: true,
resources: false
}
}
};
process.stdout.write(JSON.stringify(initNotification) + '\n');
console.error('HumanAgent MCP Server started successfully'); // Use stderr for logging
} catch (error) {
console.error('Failed to start HumanAgent MCP Server:', error);
process.exit(1);
}
}
private async shutdown(): Promise<void> {
try {
await this.server.stop();
console.error('HumanAgent MCP Server stopped');
process.exit(0);
} catch (error) {
console.error('Error during shutdown:', error);
process.exit(1);
}
}
}
// Start the standalone server
const standaloneServer = new StandaloneMcpServer();
standaloneServer.start().catch((error) => {
console.error('Failed to start standalone MCP server:', error);
process.exit(1);
});
+600
View File
@@ -0,0 +1,600 @@
import { EventEmitter } from 'events';
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import { McpMessage, McpServerConfig, HumanAgentSession, ChatMessage, McpTool, HumanAgentChatToolParams, HumanAgentChatToolResult } from './types';
// File logging utility
class DebugLogger {
private logPath: string = '';
private logStream: fs.WriteStream | null = null;
private logBuffer: string[] = [];
constructor(workspaceRoot: string = '/Users/benharper/Coding/HumanAgent-MCP') {
try {
this.logPath = path.join(workspaceRoot, 'mcp-debug.log');
console.log(`[LOGGER] Attempting to create log file at: ${this.logPath}`);
// Clear previous log file
if (fs.existsSync(this.logPath)) {
fs.unlinkSync(this.logPath);
console.log(`[LOGGER] Cleared existing log file`);
}
// Ensure directory exists
const logDir = path.dirname(this.logPath);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
this.logStream = fs.createWriteStream(this.logPath, { flags: 'a' });
this.logStream.on('error', (error) => {
console.error(`[LOGGER] File stream error:`, error);
});
this.log('DEBUG', `Debug logging started at ${new Date().toISOString()}`);
this.log('DEBUG', `Current system time: ${new Date()}`);
this.log('DEBUG', `Log file: ${this.logPath}`);
this.log('DEBUG', `Working directory: ${process.cwd()}`);
console.log(`[LOGGER] Debug logger initialized successfully`);
} catch (error) {
console.error(`[LOGGER] Failed to initialize debug logger:`, error);
this.logStream = null;
}
}
log(level: string, message: string, data?: any): void {
const timestamp = new Date().toISOString();
const logLine = `[${timestamp}] [${level}] ${message}${data ? '\n' + JSON.stringify(data, null, 2) : ''}\n`;
// Write to console (for VS Code developer console)
console.log(`[${level}] ${message}`, data || '');
// Write to file if stream is available
if (this.logStream) {
try {
this.logStream.write(logLine);
} catch (error) {
console.error(`[LOGGER] Error writing to log file:`, error);
}
} else {
// Buffer logs if stream not available
this.logBuffer.push(logLine);
}
}
close(): void {
try {
this.log('DEBUG', 'Closing debug logger');
if (this.logStream) {
this.logStream.end();
this.logStream = null;
}
} catch (error) {
console.error(`[LOGGER] Error closing debug logger:`, error);
}
}
}
export class McpServer extends EventEmitter {
private config: McpServerConfig;
private isRunning: boolean = false;
private tools: Map<string, McpTool> = new Map();
private httpServer?: http.Server;
private port: number = 3737;
private debugLogger: DebugLogger;
private pendingHumanRequests: Map<string, {
resolve: (value: string) => void;
reject: (error: Error) => void;
startTime: number;
params: HumanAgentChatToolParams;
}> = new Map();
constructor() {
super();
this.debugLogger = new DebugLogger();
this.config = {
name: 'HumanAgent MCP Server',
description: 'MCP server for chatting with human agents',
version: '1.0.0',
capabilities: {
chat: true,
tools: true,
resources: false
}
};
this.debugLogger.log('INFO', 'McpServer initialized');
this.initializeTools();
}
private initializeTools(): void {
// Define the HumanAgent_Chat tool
const humanAgentChatTool: McpTool = {
name: 'HumanAgent_Chat',
description: 'Allows AI agents to initiate interactive conversations with human agents. The human will receive the message and can respond in real-time through the chat interface.',
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']
}
};
this.tools.set('HumanAgent_Chat', humanAgentChatTool);
}
async start(): Promise<void> {
if (this.isRunning) {
return;
}
// Force clear log file on server start
const logPath = '/Users/benharper/Coding/HumanAgent-MCP/mcp-debug.log';
try {
if (fs.existsSync(logPath)) {
fs.unlinkSync(logPath);
console.log('[SERVER] Cleared log file on server start');
}
} catch (error) {
console.log('[SERVER] Could not clear log file:', error);
}
this.debugLogger.log('INFO', '=== MCP SERVER STARTING ===');
await this.startHttpServer();
this.isRunning = true;
this.emit('server-started', this.config);
}
async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
try {
this.debugLogger.log('INFO', 'Stopping MCP server...');
if (this.httpServer) {
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
this.debugLogger.log('WARN', 'HTTP server close timeout, forcing closure');
resolve();
}, 5000);
this.httpServer!.close((error) => {
clearTimeout(timeout);
if (error) {
this.debugLogger.log('WARN', 'HTTP server close error:', error);
}
resolve();
});
});
this.httpServer = undefined;
}
// Clear pending requests with proper cancellation
for (const [requestId, request] of this.pendingHumanRequests.entries()) {
try {
request.reject(new Error('Server shutting down'));
} catch (error) {
// Ignore rejection errors during shutdown
}
}
this.pendingHumanRequests.clear();
this.isRunning = false;
this.debugLogger.close();
this.emit('server-stopped');
this.debugLogger.log('INFO', 'MCP server stopped successfully');
} catch (error) {
console.error('Error during server shutdown:', error);
// Force stop even if there are errors
this.isRunning = false;
this.httpServer = undefined;
this.pendingHumanRequests.clear();
this.debugLogger.close();
}
}
private async startHttpServer(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.debugLogger.log('INFO', `Starting HTTP server on port ${this.port}...`);
this.httpServer = http.createServer((req, res) => {
this.handleHttpRequest(req, res).catch(error => {
this.debugLogger.log('ERROR', 'HTTP request handling error:', error);
});
});
this.httpServer.on('error', (error) => {
this.debugLogger.log('ERROR', 'HTTP server error:', error);
reject(error);
});
this.httpServer.on('close', () => {
this.debugLogger.log('INFO', 'HTTP server closed');
});
this.httpServer.listen(this.port, '127.0.0.1', () => {
this.debugLogger.log('INFO', `MCP HTTP server running on http://127.0.0.1:${this.port}/mcp`);
resolve();
});
} catch (error) {
this.debugLogger.log('ERROR', 'Failed to start HTTP server:', error);
reject(error);
}
});
}
private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
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');
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
this.debugLogger.log('HTTP', 'Handling OPTIONS preflight request');
res.statusCode = 200;
res.end();
return;
}
// Only handle requests to /mcp endpoint
if (req.url !== '/mcp') {
this.debugLogger.log('HTTP', `404 - Invalid endpoint: ${req.url}`);
res.statusCode = 404;
res.end('Not Found');
return;
}
if (req.method === 'POST') {
this.debugLogger.log('HTTP', 'Handling POST request to /mcp');
await this.handleHttpPost(req, res);
} else if (req.method === 'GET') {
this.debugLogger.log('HTTP', 'Handling GET request to /mcp');
await this.handleHttpGet(req, res);
} else if (req.method === 'DELETE') {
this.debugLogger.log('HTTP', 'Handling DELETE request to /mcp');
await this.handleHttpDelete(req, res);
} else {
this.debugLogger.log('HTTP', `405 - Method not allowed: ${req.method}`);
res.statusCode = 405;
res.end('Method Not Allowed');
}
}
private async handleHttpPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
try {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
this.debugLogger.log('HTTP', `Received chunk: ${chunk.length} bytes`);
});
req.on('end', async () => {
this.debugLogger.log('HTTP', `Complete request body received (${body.length} bytes)`);
this.debugLogger.log('HTTP', 'Request Body:', body);
try {
const message = JSON.parse(body);
this.debugLogger.log('HTTP', 'Parsed JSON message:', message);
const response = await this.handleMessage(message);
this.debugLogger.log('HTTP', 'Response from handleMessage:', response);
if (response) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
const responseJson = JSON.stringify(response);
this.debugLogger.log('HTTP', `Sending 200 response (${responseJson.length} bytes)`);
res.end(responseJson);
} else {
this.debugLogger.log('HTTP', 'Sending 202 response (no content)');
res.statusCode = 202;
res.end();
}
} catch (error) {
this.debugLogger.log('ERROR', 'Error parsing JSON or handling message:', error);
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32700,
message: 'Parse error',
data: error instanceof Error ? error.message : String(error)
}
}));
}
});
} catch (error) {
res.statusCode = 500;
res.end('Internal Server Error');
}
}
private async handleHttpGet(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
this.debugLogger.log('HTTP', 'Setting up SSE stream for GET request');
// Set up Server-Sent Events (SSE) stream
res.statusCode = 200;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Send initial connection acknowledgment
res.write('data: {"type":"connection","status":"established"}\n\n');
// Keep connection alive with heartbeat
const heartbeat = setInterval(() => {
if (!res.destroyed) {
res.write('data: {"type":"heartbeat","timestamp":"' + new Date().toISOString() + '"}\n\n');
} else {
clearInterval(heartbeat);
}
}, 30000); // Send heartbeat every 30 seconds
// Handle client disconnect
req.on('close', () => {
this.debugLogger.log('HTTP', 'SSE connection closed');
clearInterval(heartbeat);
});
req.on('end', () => {
this.debugLogger.log('HTTP', 'SSE connection ended');
clearInterval(heartbeat);
});
}
private async handleHttpDelete(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
// Session termination - could be implemented if needed
res.statusCode = 405;
res.end('Method Not Allowed');
}
async handleMessage(message: McpMessage): Promise<McpMessage | null> {
this.debugLogger.log('MCP', 'Handling message:', message);
try {
switch (message.method) {
case 'initialize':
this.debugLogger.log('MCP', 'Processing initialize request');
return this.handleInitialize(message);
case 'tools/list':
this.debugLogger.log('MCP', 'Processing tools/list request');
return this.handleToolsList(message);
case 'tools/call':
this.debugLogger.log('MCP', `Processing tools/call request for tool: ${message.params?.name}`);
return await this.handleToolCall(message);
case 'notifications/initialized':
this.debugLogger.log('MCP', 'Processing notifications/initialized (ignoring)');
return null;
default:
this.debugLogger.log('MCP', `Unknown method: ${message.method}`);
return {
id: message.id,
type: 'response',
error: {
code: -32601,
message: `Method ${message.method} not found`
}
};
}
} catch (error) {
return {
id: message.id,
type: 'response',
error: {
code: -32603,
message: 'Internal error',
data: error instanceof Error ? error.message : String(error)
}
};
}
}
private handleInitialize(message: McpMessage): McpMessage {
return {
id: message.id,
type: 'response',
result: {
protocolVersion: '2024-11-05',
capabilities: this.config.capabilities,
serverInfo: {
name: this.config.name,
version: this.config.version
}
}
};
}
private handleToolsList(message: McpMessage): McpMessage {
const tools = Array.from(this.tools.values());
return {
id: message.id,
type: 'response',
result: { tools }
};
}
private async handleToolCall(message: McpMessage): Promise<McpMessage> {
const { name, arguments: args } = message.params;
this.debugLogger.log('MCP', `Tool call - name: "${name}"`, { name, args });
this.debugLogger.log('MCP', 'Available tools:', Array.from(this.tools.keys()));
if (name === 'HumanAgent_Chat') {
this.debugLogger.log('MCP', 'Executing HumanAgent_Chat tool');
return await this.handleHumanAgentChatTool(message.id, args);
}
this.debugLogger.log('MCP', `Tool not found: ${name}`);
return {
id: message.id,
type: 'response',
error: {
code: -32601,
message: `Tool ${name} not found`
}
};
}
private async handleHumanAgentChatTool(messageId: string, params: HumanAgentChatToolParams): Promise<McpMessage> {
this.debugLogger.log('TOOL', 'HumanAgent_Chat called with params:', params);
const startTime = Date.now();
const timeout = (params.timeout || 300) * 1000; // Convert to milliseconds
this.debugLogger.log('TOOL', `Using timeout: ${timeout}ms (${timeout/1000}s)`);
// Generate unique request ID for tracking this specific request
const requestId = `${messageId}-${Date.now()}`;
this.debugLogger.log('TOOL', `Generated request ID: ${requestId}`);
// Display message directly in chat UI (no sessions needed)
const displayMessage = params.context ? `${params.context}\n\n${params.message}` : params.message;
this.debugLogger.log('TOOL', 'Displaying message in chat UI:', displayMessage);
// Emit event to show message in chat UI immediately
this.emit('human-agent-request', {
requestId,
message: params.message,
context: params.context,
priority: params.priority || 'normal',
timestamp: new Date().toISOString()
});
// Wait for human response
return new Promise((resolve) => {
// Set up timeout
const timeoutHandle = setTimeout(() => {
this.pendingHumanRequests.delete(requestId);
this.debugLogger.log('TOOL', `Request ${requestId} timed out after ${timeout/1000}s`);
resolve({
id: messageId,
type: 'response',
error: {
code: -32603,
message: `Human response timeout after ${params.timeout || 300} seconds`
}
});
}, timeout);
// Store the pending request
this.pendingHumanRequests.set(requestId, {
resolve: (response: string) => {
clearTimeout(timeoutHandle);
const responseTime = Date.now() - startTime;
this.debugLogger.log('TOOL', `Request ${requestId} completed with response:`, response);
const result: HumanAgentChatToolResult = {
content: [{
type: 'text',
text: response
}]
};
resolve({
id: messageId,
type: 'response',
result
});
},
reject: (error: Error) => {
clearTimeout(timeoutHandle);
this.debugLogger.log('TOOL', `Request ${requestId} rejected:`, error);
resolve({
id: messageId,
type: 'response',
error: {
code: -32603,
message: error.message
}
});
},
startTime,
params
});
this.debugLogger.log('TOOL', `Request ${requestId} waiting for human response...`);
});
}
// Method to handle human responses (called by webview)
public respondToHumanRequest(requestId: string, response: string): boolean {
this.debugLogger.log('SERVER', `Received human response for request ${requestId}:`, response);
const pendingRequest = this.pendingHumanRequests.get(requestId);
if (pendingRequest) {
this.pendingHumanRequests.delete(requestId);
pendingRequest.resolve(response);
return true;
}
this.debugLogger.log('SERVER', `No pending request found for ID: ${requestId}`);
return false;
}
// Simplified API - no sessions needed
getAvailableTools(): McpTool[] {
return Array.from(this.tools.values());
}
getPendingRequests(): Array<{id: string, params: HumanAgentChatToolParams, startTime: number}> {
return Array.from(this.pendingHumanRequests.entries()).map(([id, req]) => ({
id,
params: req.params,
startTime: req.startTime
}));
}
// Method to manually resolve a pending request (for testing)
resolvePendingRequest(requestId: string, response: string): boolean {
const request = this.pendingHumanRequests.get(requestId);
if (request) {
this.pendingHumanRequests.delete(requestId);
request.resolve(response);
return true;
}
return false;
}
isServerRunning(): boolean {
return this.isRunning;
}
getServerUrl(): string {
return `http://127.0.0.1:${this.port}/mcp`;
}
getPort(): number {
return this.port;
}
}
+64
View File
@@ -0,0 +1,64 @@
export interface McpMessage {
id: string;
type: 'request' | 'response' | 'notification';
method?: string;
params?: any;
result?: any;
error?: {
code: number;
message: string;
data?: any;
};
}
export interface ChatMessage {
id: string;
sender: 'user' | 'agent';
content: string;
timestamp: Date;
type: 'text' | 'system';
}
export interface McpServerConfig {
name: string;
description: string;
version: string;
capabilities: {
chat: boolean;
tools: boolean;
resources: boolean;
};
}
export interface HumanAgentSession {
id: string;
name: string;
isActive: boolean;
lastActivity: Date;
messages: ChatMessage[];
}
export interface McpTool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
export interface HumanAgentChatToolParams {
message: string;
context?: string;
sessionId?: string;
priority?: 'low' | 'normal' | 'high' | 'urgent';
timeout?: number;
}
export interface HumanAgentChatToolResult {
content: Array<{
type: 'text';
text: string;
}>;
}
+56
View File
@@ -0,0 +1,56 @@
import * as vscode from 'vscode';
export class ChatTreeProvider implements vscode.TreeDataProvider<ChatTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<ChatTreeItem | undefined | null | void> = new vscode.EventEmitter<ChatTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<ChatTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
private hasActiveChat: boolean = false;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
updateActiveChat(isActive: boolean): void {
this.hasActiveChat = isActive;
this.refresh();
}
getTreeItem(element: ChatTreeItem): vscode.TreeItem {
return element;
}
getChildren(element?: ChatTreeItem): Thenable<ChatTreeItem[]> {
if (!element) {
// Root level - return chat status
const chatItem = new ChatTreeItem(
this.hasActiveChat ? 'HumanAgent Chat (Active)' : 'HumanAgent Chat',
'chat',
vscode.TreeItemCollapsibleState.None,
'chat',
{
command: 'humanagent-mcp.openChat',
title: 'Open Chat',
arguments: []
}
);
return Promise.resolve([chatItem]);
}
return Promise.resolve([]);
}
}
export class ChatTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly itemId: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly contextValue: string,
public readonly command?: vscode.Command
) {
super(label, collapsibleState);
this.tooltip = `${this.label}`;
this.description = contextValue === 'chat' ? 'MCP Communication' : '';
}
}
+15
View File
@@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});
+466
View File
@@ -0,0 +1,466 @@
import * as vscode from 'vscode';
import { McpServer } from '../mcp/server';
import { ChatMessage } from '../mcp/types';
import { McpConfigManager } from '../mcp/mcpConfigManager';
export class ChatWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'humanagent-mcp.chatView';
private _view?: vscode.WebviewView;
private mcpServer: McpServer;
private mcpConfigManager?: McpConfigManager;
private extensionPath: string;
private messages: ChatMessage[] = [];
private currentRequestId?: string;
constructor(
private readonly _extensionUri: vscode.Uri,
mcpServer: McpServer,
mcpConfigManager?: McpConfigManager
) {
this.mcpServer = mcpServer;
this.mcpConfigManager = mcpConfigManager;
this.extensionPath = _extensionUri.fsPath;
}
public displayHumanAgentMessage(message: string, context?: string, requestId?: string) {
// Store the current request ID for response handling
this.currentRequestId = requestId;
// Combine context and message if context exists
const fullMessage = context ? `${context}\n\n${message}` : message;
// Add AI message to chat
const aiMessage: ChatMessage = {
id: Date.now().toString(),
content: fullMessage,
sender: 'agent',
timestamp: new Date(),
type: 'text'
};
this.messages.push(aiMessage);
this.updateWebview();
// Focus the chat webview
if (this._view) {
this._view.show?.(true);
}
}
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
this._extensionUri
]
};
this.updateWebview();
webviewView.webview.onDidReceiveMessage(async (data) => {
switch (data.type) {
case 'sendMessage':
await this.sendHumanResponse(data.content);
break;
case 'mcpAction':
await this.handleMcpAction(data.action);
break;
case 'requestServerStatus':
this.updateServerStatus();
break;
}
});
}
private async sendHumanResponse(content: string) {
try {
console.log('ChatWebviewProvider: Sending human response:', content);
// Add human message to chat
const humanMessage: ChatMessage = {
id: Date.now().toString(),
content: content,
sender: 'user',
timestamp: new Date(),
type: 'text'
};
this.messages.push(humanMessage);
this.updateWebview();
// Send response back to MCP server
if (this.currentRequestId) {
console.log('ChatWebviewProvider: Responding to request ID:', this.currentRequestId);
this.mcpServer.respondToHumanRequest(this.currentRequestId, content);
this.currentRequestId = undefined;
}
} catch (error) {
console.error('ChatWebviewProvider: Error in sendHumanResponse:', error);
}
}
public async waitForHumanResponse(): Promise<string> {
// This method is no longer needed since we use direct callbacks
throw new Error('waitForHumanResponse is deprecated - use direct response handling');
}
private updateWebview() {
if (this._view) {
this._view.webview.html = this._getHtmlForWebview(this._view.webview);
}
}
private updateServerStatus() {
if (!this._view) {
return;
}
const tools = this.mcpServer.getAvailableTools();
const pendingRequests = this.mcpServer.getPendingRequests();
const isRegistered = this.mcpConfigManager?.isMcpServerRegistered() ?? false;
this._view.webview.postMessage({
type: 'serverStatus',
data: {
running: true,
tools: tools.length,
pendingRequests: pendingRequests.length,
registered: isRegistered
}
});
}
private async handleMcpAction(action: string) {
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':
await this.mcpServer.stop();
await this.mcpServer.start();
vscode.window.showInformationMessage('MCP Server restarted');
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');
break;
case 'configure':
vscode.commands.executeCommand('humanagent-mcp.configureMcp');
break;
}
// Update status after action
this.updateServerStatus();
} catch (error) {
vscode.window.showErrorMessage(`MCP action failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
private _getHtmlForWebview(webview: vscode.Webview) {
const messagesHtml = this.messages.map(message => {
const messageClass = message.sender === 'agent' ? 'ai-message' : 'human-message';
const timestamp = message.timestamp.toLocaleTimeString();
const senderLabel = message.sender === 'agent' ? 'AI' : 'Human';
return `
<div class="message ${messageClass}">
<div class="message-header">
<span class="sender">${senderLabel}</span>
<span class="timestamp">${timestamp}</span>
</div>
<div class="message-content">${this._escapeHtml(String(message.content || ''))}</div>
</div>
`;
}).join('');
const hasPendingResponse = this.currentRequestId ? 'waiting' : '';
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HumanAgent Chat</title>
<style>
body {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
line-height: 1.4;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
padding: 10px;
border-bottom: 1px solid var(--vscode-panel-border);
background-color: var(--vscode-panel-background);
}
.status {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 5px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--vscode-charts-green);
}
.control-buttons {
display: flex;
gap: 5px;
}
.cog-button {
padding: 4px 8px;
font-size: 14px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 3px;
cursor: pointer;
}
.cog-button:hover {
background-color: var(--vscode-button-hoverBackground);
}
.control-button {
padding: 4px 8px;
font-size: 11px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 3px;
cursor: pointer;
}
.control-button:hover {
background-color: var(--vscode-button-hoverBackground);
}
.messages {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
border-left: 3px solid;
}
.ai-message {
background-color: var(--vscode-editor-selectionBackground);
border-left-color: var(--vscode-charts-blue);
}
.human-message {
background-color: var(--vscode-editor-hoverHighlightBackground);
border-left-color: var(--vscode-charts-green);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
font-size: 12px;
opacity: 0.8;
}
.sender {
font-weight: bold;
}
.timestamp {
font-size: 11px;
}
.message-content {
white-space: pre-wrap;
word-wrap: break-word;
}
.input-area {
padding: 10px;
border-top: 1px solid var(--vscode-panel-border);
background-color: var(--vscode-panel-background);
}
.input-container {
display: flex;
gap: 5px;
}
.message-input {
flex: 1;
padding: 8px;
border: 1px solid var(--vscode-input-border);
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border-radius: 3px;
font-family: inherit;
font-size: inherit;
}
.send-button {
padding: 8px 16px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 3px;
cursor: pointer;
}
.send-button:hover {
background-color: var(--vscode-button-hoverBackground);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.waiting-indicator {
text-align: center;
padding: 10px;
font-style: italic;
color: var(--vscode-descriptionForeground);
}
.empty-state {
text-align: center;
padding: 20px;
color: var(--vscode-descriptionForeground);
}
</style>
</head>
<body>
<div class="header">
<div class="status">
<div class="status-indicator">
<div class="status-dot"></div>
<span>HumanAgent MCP Server</span>
</div>
<div class="control-buttons">
<button class="control-button" onclick="requestServerStatus()">Status</button>
<button class="cog-button" onclick="showConfigMenu()" title="Configure MCP">⚙️</button>
</div>
</div>
</div>
<div class="messages" id="messages">
${messagesHtml || '<div class="empty-state">Waiting for AI messages...</div>'}
${hasPendingResponse ? '<div class="waiting-indicator">⏳ Waiting for your response...</div>' : ''}
</div>
<div class="input-area">
<div class="input-container">
<input type="text" class="message-input" id="messageInput" placeholder="Type your response..." ${hasPendingResponse ? '' : 'disabled'}>
<button class="send-button" id="sendButton" onclick="sendMessage()" ${hasPendingResponse ? '' : 'disabled'}>Send</button>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
function showConfigMenu() {
vscode.postMessage({
type: 'mcpAction',
action: 'configure'
});
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
vscode.postMessage({
type: 'sendMessage',
content: message
});
input.value = '';
}
}
function handleMcpAction(action) {
vscode.postMessage({
type: 'mcpAction',
action: action
});
}
function requestServerStatus() {
vscode.postMessage({
type: 'requestServerStatus'
});
}
// Auto-scroll to bottom
const messagesContainer = document.getElementById('messages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Listen for server status updates
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'serverStatus') {
// Could update UI with server status if needed
console.log('Server status:', message.data);
}
});
</script>
</body>
</html>
`;
}
private _escapeHtml(text: string): string {
if (typeof text !== 'string') {
text = String(text || '');
}
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
}