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:
B Harper
2025-10-25 11:29:05 +11:00
parent 59be50e9d9
commit d846f0895b
10 changed files with 823 additions and 128 deletions
BIN
View File
Binary file not shown.
+54 -4
View File
@@ -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)
);
+1 -92
View File
@@ -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
View File
@@ -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',
+8 -1
View File
@@ -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');