/**
* Dynamic tool registry for session-based tool management
*/
import { MCPTool, SessionInfo } from '../types/core.js';
import { EventEmitter } from 'events';
export interface ToolHandler {
(params: any, context: { session_id?: string; request_id: string }): Promise<any>;
}
export interface RegisteredTool {
tool: MCPTool;
handler: ToolHandler;
session_id?: string;
registered_at: string;
}
export interface RegistryEvents {
'tool_registered': (toolName: string, sessionId?: string) => void;
'tool_unregistered': (toolName: string, sessionId?: string) => void;
'session_created': (sessionId: string, type: string) => void;
'session_destroyed': (sessionId: string, type: string) => void;
}
export class ToolRegistry extends EventEmitter {
private tools = new Map<string, RegisteredTool>();
private sessions = new Map<string, SessionInfo>();
private sessionTools = new Map<string, Set<string>>(); // session_id -> tool names
constructor() {
super();
}
/**
* Register a static tool (available always)
*/
registerTool(name: string, tool: MCPTool, handler: ToolHandler): void {
this.tools.set(name, {
tool,
handler,
registered_at: new Date().toISOString()
});
this.emit('tool_registered', name);
}
/**
* Register a session-based tool (only available when session exists)
*/
registerSessionTool(sessionId: string, name: string, tool: MCPTool, handler: ToolHandler): void {
const toolName = `${name}`;
this.tools.set(toolName, {
tool: {
...tool,
name: toolName
},
handler,
session_id: sessionId,
registered_at: new Date().toISOString()
});
if (!this.sessionTools.has(sessionId)) {
this.sessionTools.set(sessionId, new Set());
}
this.sessionTools.get(sessionId)!.add(toolName);
this.emit('tool_registered', toolName, sessionId);
}
/**
* Unregister a specific tool
*/
unregisterTool(name: string): boolean {
const tool = this.tools.get(name);
if (!tool) {
return false;
}
this.tools.delete(name);
if (tool.session_id) {
const sessionTools = this.sessionTools.get(tool.session_id);
if (sessionTools) {
sessionTools.delete(name);
if (sessionTools.size === 0) {
this.sessionTools.delete(tool.session_id);
}
}
}
this.emit('tool_unregistered', name, tool.session_id);
return true;
}
/**
* Create a session and mark it as active
*/
createSession(sessionId: string, type: 'browser' | 'db', metadata?: any): void {
const sessionInfo: SessionInfo = {
id: sessionId,
type,
created_at: new Date().toISOString(),
last_used: new Date().toISOString(),
metadata
};
this.sessions.set(sessionId, sessionInfo);
this.emit('session_created', sessionId, type);
}
/**
* Destroy a session and unregister all its tools
*/
destroySession(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
if (!session) {
return false;
}
// Unregister all tools for this session
const sessionTools = this.sessionTools.get(sessionId);
if (sessionTools) {
for (const toolName of sessionTools) {
this.tools.delete(toolName);
this.emit('tool_unregistered', toolName, sessionId);
}
this.sessionTools.delete(sessionId);
}
this.sessions.delete(sessionId);
this.emit('session_destroyed', sessionId, session.type);
return true;
}
/**
* Update session last_used timestamp
*/
touchSession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (session) {
session.last_used = new Date().toISOString();
}
}
/**
* Get a tool by name
*/
getTool(name: string): RegisteredTool | undefined {
return this.tools.get(name);
}
/**
* Get all available tools
*/
getAllTools(): MCPTool[] {
return Array.from(this.tools.values()).map(rt => rt.tool);
}
/**
* Get tools for a specific session
*/
getSessionTools(sessionId: string): MCPTool[] {
const toolNames = this.sessionTools.get(sessionId);
if (!toolNames) {
return [];
}
return Array.from(toolNames)
.map(name => this.tools.get(name))
.filter((rt): rt is RegisteredTool => rt !== undefined)
.map(rt => rt.tool);
}
/**
* Check if a session exists
*/
hasSession(sessionId: string): boolean {
return this.sessions.has(sessionId);
}
/**
* Get session info
*/
getSession(sessionId: string): SessionInfo | undefined {
return this.sessions.get(sessionId);
}
/**
* Get all active sessions
*/
getAllSessions(): SessionInfo[] {
return Array.from(this.sessions.values());
}
/**
* Execute a tool with proper context
*/
async executeTool(name: string, params: any, requestId: string): Promise<any> {
const registeredTool = this.tools.get(name);
if (!registeredTool) {
throw new Error(`Tool '${name}' not found`);
}
// Update session last_used if this is a session tool
if (registeredTool.session_id) {
this.touchSession(registeredTool.session_id);
}
const context = {
session_id: registeredTool.session_id,
request_id: requestId
};
return await registeredTool.handler(params, context);
}
/**
* Clean up expired sessions (older than ttlMs)
*/
cleanupExpiredSessions(ttlMs: number = 24 * 60 * 60 * 1000): number {
const now = Date.now();
const expired: string[] = [];
for (const [sessionId, session] of this.sessions) {
const lastUsed = new Date(session.last_used).getTime();
if (now - lastUsed > ttlMs) {
expired.push(sessionId);
}
}
for (const sessionId of expired) {
this.destroySession(sessionId);
}
return expired.length;
}
/**
* Get registry statistics
*/
getStats(): {
total_tools: number;
static_tools: number;
session_tools: number;
active_sessions: number;
tools_by_session: Record<string, number>;
} {
const staticTools = Array.from(this.tools.values()).filter(t => !t.session_id).length;
const sessionTools = Array.from(this.tools.values()).filter(t => t.session_id).length;
const toolsBySession: Record<string, number> = {};
for (const [sessionId, toolNames] of this.sessionTools) {
toolsBySession[sessionId] = toolNames.size;
}
return {
total_tools: this.tools.size,
static_tools: staticTools,
session_tools: sessionTools,
active_sessions: this.sessions.size,
tools_by_session: toolsBySession
};
}
}