mirror of
https://github.com/wassname/HumanAgent-MCP.git
synced 2026-09-11 11:50:43 +08:00
Implement VS Code native MCP event system for automatic tool refreshing
- Added HumanAgentMcpProvider class with onDidChangeMcpServerDefinitions event - Registered MCP provider with VS Code lm.registerMcpServerDefinitionProvider - Added mcpServerDefinitionProviders contribution point to package.json - Implemented startup event firing when override files exist - Fixed session name loading timing by moving after server startup with retry - Chat webview now fires MCP events when override files are reloaded - VS Code Configure Tools will now automatically refresh tool descriptions
This commit is contained in:
Vendored
BIN
Binary file not shown.
+54
-4
@@ -13,6 +13,36 @@ let mcpConfigManager: McpConfigManager;
|
||||
let workspaceSessionId: string;
|
||||
let serverManager: ServerManager;
|
||||
|
||||
// MCP Server Definition Provider for VS Code native MCP integration
|
||||
class HumanAgentMcpProvider implements vscode.McpServerDefinitionProvider {
|
||||
private _onDidChangeMcpServerDefinitions = new vscode.EventEmitter<void>();
|
||||
readonly onDidChangeMcpServerDefinitions = this._onDidChangeMcpServerDefinitions.event;
|
||||
|
||||
constructor(private sessionId: string) {}
|
||||
|
||||
provideMcpServerDefinitions(token: vscode.CancellationToken): vscode.ProviderResult<vscode.McpHttpServerDefinition[]> {
|
||||
// Return our HumanAgent MCP server definition with current sessionId
|
||||
const serverUrl = `http://127.0.0.1:3737/mcp?sessionId=${this.sessionId}`;
|
||||
const serverUri = vscode.Uri.parse(serverUrl);
|
||||
const server = new vscode.McpHttpServerDefinition('HumanAgent MCP', serverUri);
|
||||
return [server];
|
||||
}
|
||||
|
||||
// Method to fire the change event when override files are reloaded
|
||||
notifyServerDefinitionsChanged(): void {
|
||||
console.log('HumanAgent MCP: Firing onDidChangeMcpServerDefinitions event');
|
||||
this._onDidChangeMcpServerDefinitions.fire();
|
||||
}
|
||||
|
||||
// Update session ID when it changes
|
||||
updateSessionId(newSessionId: string): void {
|
||||
this.sessionId = newSessionId;
|
||||
this.notifyServerDefinitionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
let mcpProvider: HumanAgentMcpProvider;
|
||||
|
||||
// Generate or retrieve persistent workspace session ID
|
||||
function getWorkspaceSessionId(context: vscode.ExtensionContext): string {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
@@ -72,13 +102,24 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Generate or retrieve persistent workspace session ID
|
||||
workspaceSessionId = getWorkspaceSessionId(context);
|
||||
|
||||
// Restore the persisted session name for this session ID
|
||||
await restoreSessionName(context, workspaceSessionId);
|
||||
|
||||
// Initialize MCP Configuration Manager
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
mcpConfigManager = new McpConfigManager(workspaceRoot, context.extensionPath);
|
||||
|
||||
// Initialize and register VS Code native MCP provider
|
||||
mcpProvider = new HumanAgentMcpProvider(workspaceSessionId);
|
||||
context.subscriptions.push(vscode.lm.registerMcpServerDefinitionProvider('humanagent-mcp.server', mcpProvider));
|
||||
console.log('HumanAgent MCP: Registered MCP server definition provider');
|
||||
|
||||
// Fire startup event if override file exists to refresh VS Code tools
|
||||
if (workspaceRoot) {
|
||||
const overrideFilePath = path.join(workspaceRoot, '.vscode', 'HumanAgentOverride.json');
|
||||
if (require('fs').existsSync(overrideFilePath)) {
|
||||
console.log('HumanAgent MCP: Override file detected on startup, firing onDidChangeMcpServerDefinitions');
|
||||
mcpProvider.notifyServerDefinitionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Server Manager
|
||||
const serverPath = path.join(context.extensionPath, 'dist', 'mcpStandalone.js');
|
||||
|
||||
@@ -106,6 +147,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Auto-detect and start standalone MCP server if already configured
|
||||
await autoStartMcpServer(mcpConfigManager, workspaceSessionId);
|
||||
|
||||
// Restore the persisted session name after server is running (with retry)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await restoreSessionName(context, workspaceSessionId);
|
||||
} catch (error) {
|
||||
console.log('HumanAgent MCP: Could not restore session name on startup (server may not be ready yet):', error);
|
||||
}
|
||||
}, 1000); // Wait 1 second for server to fully start
|
||||
|
||||
// Initialize Tree View Provider
|
||||
chatTreeProvider = new ChatTreeProvider();
|
||||
const treeView = vscode.window.createTreeView('humanagent-mcp.chatSessions', {
|
||||
@@ -114,7 +164,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
// Initialize Chat Webview Provider (no internal server dependency)
|
||||
const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, null, mcpConfigManager, workspaceSessionId, context);
|
||||
const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, null, mcpConfigManager, workspaceSessionId, context, mcpProvider);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(ChatWebviewProvider.viewType, chatWebviewProvider)
|
||||
);
|
||||
|
||||
@@ -16,103 +16,12 @@ class StandaloneMcpServer {
|
||||
// This ensures log file is created where it can be properly cleared
|
||||
const workspacePath = require('path').resolve(__dirname, '..');
|
||||
this.server = new McpServer(undefined, workspacePath);
|
||||
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 handlers removed - server should remain independent
|
||||
// and only shut down when explicitly requested via API endpoints
|
||||
// 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
|
||||
console.error('HumanAgent MCP Server started successfully (HTTP-only)'); // Use stderr for logging
|
||||
} catch (error) {
|
||||
console.error('Failed to start HumanAgent MCP Server:', error);
|
||||
process.exit(1);
|
||||
|
||||
+98
-26
@@ -3,6 +3,7 @@ import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
import { McpMessage, McpServerConfig, HumanAgentSession, ChatMessage, McpTool, HumanAgentChatToolParams, HumanAgentChatToolResult } from './types';
|
||||
import { ChatManager } from './chatManager';
|
||||
|
||||
@@ -135,6 +136,7 @@ export class McpServer extends EventEmitter {
|
||||
private sseConnections: Set<http.ServerResponse> = new Set();
|
||||
private conversationToSession: Map<string, string> = new Map(); // Map VS Code conversation IDs to registered session IDs
|
||||
private chatManager: ChatManager; // Centralized chat and session management
|
||||
private sseClients: Map<string, http.ServerResponse> = new Map(); // Per-session SSE connections
|
||||
|
||||
constructor(private sessionId?: string, private workspacePath?: string) {
|
||||
super();
|
||||
@@ -193,21 +195,45 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private sendMcpNotification(method: string, params?: any): void {
|
||||
// Check if we're in standalone mode (connected via stdio)
|
||||
if (process.stdout && process.stdout.writable) {
|
||||
const notification = {
|
||||
type: 'notification',
|
||||
method: method,
|
||||
params: params || {}
|
||||
};
|
||||
|
||||
try {
|
||||
process.stdout.write(JSON.stringify(notification) + '\n');
|
||||
this.debugLogger.log('MCP', `Sent MCP notification: ${method}`, params);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', `Failed to send MCP notification: ${method}`, error);
|
||||
private sendMcpNotification(method: string, params?: any, sessionId?: string): void {
|
||||
this.debugLogger.log('MCP', `Sending SSE notification: ${method}`, params);
|
||||
|
||||
const notification = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params: params || {}
|
||||
};
|
||||
|
||||
if (sessionId) {
|
||||
// Send to specific session
|
||||
const sseResponse = this.sseClients.get(sessionId);
|
||||
if (sseResponse) {
|
||||
this.debugLogger.log('SSE', `Sending notification to session: ${sessionId}`);
|
||||
this.sendSSEMessage(sseResponse, notification);
|
||||
} else {
|
||||
this.debugLogger.log('SSE', `No SSE connection for session: ${sessionId}`);
|
||||
}
|
||||
} else {
|
||||
// Send to all active sessions with SSE connections
|
||||
for (const activeSessionId of this.activeSessions) {
|
||||
const sseResponse = this.sseClients.get(activeSessionId);
|
||||
if (sseResponse) {
|
||||
this.debugLogger.log('SSE', `Sending notification to active session: ${activeSessionId}`);
|
||||
this.sendSSEMessage(sseResponse, notification);
|
||||
} else {
|
||||
this.debugLogger.log('SSE', `No SSE connection for active session: ${activeSessionId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendSSEMessage(response: http.ServerResponse, message: any): void {
|
||||
try {
|
||||
const data = JSON.stringify(message);
|
||||
response.write(`data: ${data}\n\n`);
|
||||
this.debugLogger.log('SSE', `Sent SSE message: ${message.method || 'response'}`);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', `Failed to send SSE message:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,6 +580,18 @@ export class McpServer extends EventEmitter {
|
||||
if (response) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// If this is an initialize response, generate and set session ID
|
||||
if (message.method === 'initialize') {
|
||||
// Generate new session ID if none provided in URL
|
||||
const responseSessionId = sessionId || `session-${crypto.randomUUID()}`;
|
||||
res.setHeader('Mcp-Session-Id', responseSessionId);
|
||||
this.debugLogger.log('HTTP', `Set Mcp-Session-Id header: ${responseSessionId}`);
|
||||
|
||||
// Add this session to active sessions for notifications
|
||||
this.activeSessions.add(responseSessionId);
|
||||
}
|
||||
|
||||
const responseJson = JSON.stringify(response);
|
||||
this.debugLogger.log('HTTP', `Sending 200 response (${responseJson.length} bytes)`);
|
||||
res.end(responseJson);
|
||||
@@ -588,6 +626,24 @@ export class McpServer extends EventEmitter {
|
||||
this.debugLogger.log('SSE', 'Headers:', req.headers);
|
||||
this.debugLogger.log('SSE', 'URL:', req.url);
|
||||
|
||||
// Extract sessionId from query params in URL or Mcp-Session-Id header
|
||||
const url = new URL(req.url!, `http://${req.headers.host}`);
|
||||
let sessionId = url.searchParams.get('sessionId');
|
||||
|
||||
// If not in URL, try the Mcp-Session-Id header (per MCP spec)
|
||||
if (!sessionId) {
|
||||
sessionId = req.headers['mcp-session-id'] as string;
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
this.debugLogger.log('SSE', 'No sessionId in SSE request (tried URL param and Mcp-Session-Id header)');
|
||||
this.debugLogger.log('SSE', `Request headers: ${JSON.stringify(req.headers)}`);
|
||||
this.debugLogger.log('SSE', `Request URL: ${req.url}`);
|
||||
// For now, let's allow connection without sessionId and use a default
|
||||
sessionId = 'default-sse-session';
|
||||
this.debugLogger.log('SSE', `Using default sessionId: ${sessionId}`);
|
||||
}
|
||||
|
||||
// Set up Server-Sent Events (SSE) stream
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
@@ -599,15 +655,16 @@ export class McpServer extends EventEmitter {
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Connection');
|
||||
|
||||
this.debugLogger.log('SSE', 'SSE headers set, adding to connections...');
|
||||
this.debugLogger.log('SSE', `SSE headers set for session ${sessionId}, adding to connections...`);
|
||||
|
||||
// Add this connection to our active SSE connections
|
||||
this.sseConnections.add(res);
|
||||
this.debugLogger.log('HTTP', `Added SSE connection. Total connections: ${this.sseConnections.size}`);
|
||||
this.debugLogger.log('SSE', `SSE connection added. Total: ${this.sseConnections.size}`);
|
||||
// Add this connection to our session-based SSE connections
|
||||
this.sseClients.set(sessionId, res);
|
||||
this.sseConnections.add(res); // Keep old connections for cleanup
|
||||
this.debugLogger.log('HTTP', `Added SSE connection for session ${sessionId}. Total connections: ${this.sseConnections.size}`);
|
||||
this.debugLogger.log('SSE', `SSE connection added for session ${sessionId}. Total: ${this.sseConnections.size}`);
|
||||
|
||||
// Send initial connection acknowledgment
|
||||
const initialMessage = 'data: {"type":"connection","status":"established"}\n\n';
|
||||
const initialMessage = 'data: {"type":"connection","status":"established","sessionId":"' + sessionId + '"}\n\n';
|
||||
res.write(initialMessage);
|
||||
this.debugLogger.log('SSE', 'Sent initial SSE message:', initialMessage.trim());
|
||||
|
||||
@@ -623,10 +680,11 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
// Handle client disconnect
|
||||
const cleanup = () => {
|
||||
this.debugLogger.log('HTTP', 'SSE connection closed');
|
||||
this.debugLogger.log('HTTP', `SSE connection closed for session ${sessionId}`);
|
||||
clearInterval(heartbeat);
|
||||
this.sseConnections.delete(res);
|
||||
this.debugLogger.log('HTTP', `Removed SSE connection. Total connections: ${this.sseConnections.size}`);
|
||||
this.sseClients.delete(sessionId);
|
||||
this.debugLogger.log('HTTP', `Removed SSE connection for session ${sessionId}. Total connections: ${this.sseConnections.size}`);
|
||||
};
|
||||
|
||||
req.on('close', cleanup);
|
||||
@@ -890,23 +948,31 @@ export class McpServer extends EventEmitter {
|
||||
async handleMessage(message: McpMessage): Promise<McpMessage | null> {
|
||||
this.debugLogger.log('MCP', 'Handling message:', message);
|
||||
|
||||
// Extract sessionId from message params if available
|
||||
const sessionId = message.params?.sessionId;
|
||||
|
||||
try {
|
||||
let response: McpMessage | null = null;
|
||||
|
||||
switch (message.method) {
|
||||
case 'initialize':
|
||||
this.debugLogger.log('MCP', 'Processing initialize request');
|
||||
return this.handleInitialize(message);
|
||||
response = this.handleInitialize(message);
|
||||
break;
|
||||
case 'tools/list':
|
||||
this.debugLogger.log('MCP', 'Processing tools/list request');
|
||||
return this.handleToolsList(message);
|
||||
response = this.handleToolsList(message);
|
||||
break;
|
||||
case 'tools/call':
|
||||
this.debugLogger.log('MCP', `Processing tools/call request for tool: ${message.params?.name}`);
|
||||
return await this.handleToolCall(message);
|
||||
response = await this.handleToolCall(message);
|
||||
break;
|
||||
case 'notifications/initialized':
|
||||
this.debugLogger.log('MCP', 'Processing notifications/initialized (ignoring)');
|
||||
return null;
|
||||
default:
|
||||
this.debugLogger.log('MCP', `Unknown method: ${message.method}`);
|
||||
return {
|
||||
response = {
|
||||
id: message.id,
|
||||
type: 'response',
|
||||
error: {
|
||||
@@ -915,6 +981,10 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notifications are now sent via SSE, no need to include in response
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
return {
|
||||
id: message.id,
|
||||
@@ -1526,6 +1596,8 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
private handleInitialize(message: McpMessage): McpMessage {
|
||||
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
type: 'response',
|
||||
|
||||
@@ -26,7 +26,8 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
mcpServer: McpServer | null,
|
||||
mcpConfigManager?: McpConfigManager,
|
||||
private readonly workspaceSessionId?: string,
|
||||
private readonly context?: vscode.ExtensionContext
|
||||
private readonly context?: vscode.ExtensionContext,
|
||||
private readonly mcpProvider?: any
|
||||
) {
|
||||
this.mcpServer = mcpServer;
|
||||
this.mcpConfigManager = mcpConfigManager;
|
||||
@@ -335,6 +336,12 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
console.error('Failed to call /reload endpoint:', reloadError);
|
||||
}
|
||||
|
||||
// Fire VS Code native MCP event to refresh tools
|
||||
if (this.mcpProvider) {
|
||||
this.mcpProvider.notifyServerDefinitionsChanged();
|
||||
console.log('Fired onDidChangeMcpServerDefinitions event to refresh VS Code tools');
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage('Override file reloaded successfully!');
|
||||
} else {
|
||||
vscode.window.showWarningMessage('Failed to get sessions from server');
|
||||
|
||||
Reference in New Issue
Block a user