mirror of
https://github.com/wassname/HumanAgent-MCP.git
synced 2026-09-10 11:40:39 +08:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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' : '';
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user