mcpdog
Version:
MCPDog - Universal MCP Server Manager with Web Interface
1,046 lines • 45.7 kB
JavaScript
/**
* Daemon Web Server
* Connects directly to the daemon instance instead of creating a separate MCPServer
*/
import express from 'express';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import { globalLogManager } from '../logging/server-log-manager.js';
import { ServerNameValidator } from '../utils/server-name-validator.js';
import { createExpressAuthMiddleware } from '../middleware/auth.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class DaemonWebServer {
app;
server;
io;
daemon;
port;
configManager; // Add configManager property
constructor(daemon, port) {
this.daemon = daemon;
this.port = port;
this.configManager = daemon.getConfigManager(); // Use daemon's configManager
// Create Express application
this.app = express();
this.server = createServer(this.app);
this.io = new SocketIOServer(this.server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
this.setupMiddleware();
this.setupRoutes();
this.setupWebSocket();
this.setupDaemonEvents();
}
setupMiddleware() {
// CORS support
this.app.use(cors());
// JSON parsing
this.app.use(express.json());
// Authentication middleware (if token is configured)
const authToken = process.env.MCPDOG_AUTH_TOKEN;
if (authToken) {
console.log('[DAEMON-WEB] Authentication enabled');
// Add authentication check endpoint (before auth middleware)
this.app.get('/api/auth/status', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.json({ authenticated: false, required: true });
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return res.json({ authenticated: false, required: true });
}
const token = parts[1];
const isAuthorized = Buffer.compare(Buffer.from(token), Buffer.from(authToken)) === 0;
if (!isAuthorized) {
return res.json({ authenticated: false, required: true });
}
res.json({ authenticated: true, required: true });
});
// Add login endpoint (before auth middleware)
this.app.post('/api/auth/login', (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(400).json({ error: 'Token is required' });
}
const isAuthorized = Buffer.compare(Buffer.from(token), Buffer.from(authToken)) === 0;
if (!isAuthorized) {
return res.status(401).json({ error: 'Invalid token' });
}
res.json({ success: true, message: 'Login successful' });
});
this.app.use(createExpressAuthMiddleware(authToken));
}
else {
// When auth is disabled, return auth not required
this.app.get('/api/auth/status', (req, res) => {
res.json({ authenticated: true, required: false });
});
}
// Static file serving
const staticPath = path.join(__dirname, '../../web/dist');
this.app.use(express.static(staticPath));
// API route prefix
this.app.use('/api', this.createAPIRouter());
}
createAPIRouter() {
const router = express.Router();
// System status API
router.get('/status', this.handleGetStatus.bind(this));
// Server management API
router.get('/servers', this.handleGetServers.bind(this));
router.post('/servers', this.handleAddServer.bind(this));
router.put('/servers/:name', this.handleUpdateServer.bind(this));
router.delete('/servers/:name', this.handleRemoveServer.bind(this));
router.post('/servers/:name/toggle', this.handleToggleServer.bind(this));
// Tool-level control API
router.get('/servers/:name/tools', this.handleGetServerTools.bind(this));
router.post('/servers/:name/tools/:tool/toggle', this.handleToggleServerTool.bind(this));
router.put('/servers/:name/tools', this.handleUpdateServerTools.bind(this));
// Tool management API
router.get('/tools', this.handleGetTools.bind(this));
router.post('/tools/:name/call', this.handleCallTool.bind(this));
// Config management API
router.get('/config', this.handleGetConfig.bind(this));
router.put('/config', this.handleUpdateConfig.bind(this));
// Daemon-specific API
router.get('/daemon/clients', this.handleGetClients.bind(this));
router.post('/daemon/reload', this.handleReloadConfig.bind(this));
// Log management API
router.get('/logs', this.handleGetAllLogs.bind(this));
router.get('/logs/:serverName', this.handleGetServerLogs.bind(this));
router.delete('/logs/:serverName', this.handleClearServerLogs.bind(this));
router.get('/logs/:serverName/stats', this.handleGetServerLogStats.bind(this));
return router;
}
setupRoutes() {
// SPA route support - all non-API routes return index.html
this.app.get('*', (req, res) => {
if (!req.path.startsWith('/api')) {
const indexPath = path.join(__dirname, '../../web/dist/index.html');
res.sendFile(indexPath);
}
});
}
setupWebSocket() {
this.io.on('connection', (socket) => {
console.log('[DAEMON-WEB] Web client connected:', socket.id);
// Send initial status
this.sendStatusUpdate(socket);
// Client requests status update
socket.on('request-status', () => {
this.sendStatusUpdate(socket);
});
socket.on('disconnect', () => {
console.log('[DAEMON-WEB] Web client disconnected:', socket.id);
});
});
}
setupDaemonEvents() {
// Listen for daemon events, push to web clients in real-time
this.daemon.on('server-started', (data) => {
console.log(`[DAEMON-WEB] Server started event received: ${data.serverName}`);
// Delay sending composite event to ensure adapter status is fully updated
setTimeout(() => {
const systemStatus = this.getSystemStatus();
if (systemStatus) {
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
this.io.emit('server-status-changed', {
event: 'server-started',
serverName: data.serverName,
systemStatus: systemStatus,
originalData: data,
timestamp: data.timestamp || new Date().toISOString()
});
}
else {
// If status retrieval fails, fall back to original method
this.io.emit('server-started', data);
this.broadcastStatusUpdate();
}
}, 1000); // Delay to ensure status synchronization
});
this.daemon.on('server-stopped', (data) => {
console.log(`[DAEMON-WEB] Server stopped event received: ${data.serverName}`);
// Immediately get and send status
const systemStatus = this.getSystemStatus();
if (systemStatus) {
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
this.io.emit('server-status-changed', {
event: 'server-stopped',
serverName: data.serverName,
systemStatus: systemStatus,
originalData: data,
timestamp: data.timestamp || new Date().toISOString()
});
}
else {
// If status retrieval fails, fall back to original method
this.io.emit('server-stopped', data);
this.broadcastStatusUpdate();
}
});
this.daemon.on('routes-updated', (data) => {
this.io.emit('server-updated', {
serverName: data.serverName,
toolCount: data.toolCount,
timestamp: new Date().toISOString()
});
// Delay broadcasting status update to ensure tool routes are fully updated
setTimeout(() => {
console.log(`[DAEMON-WEB] Broadcasting delayed status update after routes-updated for: ${data.serverName}`);
this.broadcastStatusUpdate();
}, 500); // 500ms delay
});
this.daemon.on('server-connected', (data) => {
console.log(`[DAEMON-WEB] Server connected event received: ${data.serverName}`);
// Delay sending composite event to ensure adapter status is fully updated
setTimeout(() => {
const systemStatus = this.getSystemStatus();
if (systemStatus) {
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
this.io.emit('server-status-changed', {
event: 'server-connected',
serverName: data.serverName,
systemStatus: systemStatus,
originalData: data,
timestamp: data.timestamp || new Date().toISOString()
});
}
else {
// If status retrieval fails, fall back to original method
this.io.emit('server-connected', data);
this.broadcastStatusUpdate();
}
}, 1000); // Delay to ensure status synchronization
});
this.daemon.on('server-disconnected', (data) => {
console.log(`[DAEMON-WEB] Server disconnected event received: ${data.serverName}`);
// Immediately get and send status, as disconnection status changes are immediate
const systemStatus = this.getSystemStatus();
if (systemStatus) {
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
this.io.emit('server-status-changed', {
event: 'server-disconnected',
serverName: data.serverName,
systemStatus: systemStatus,
originalData: data,
timestamp: data.timestamp || new Date().toISOString()
});
}
else {
// If status retrieval fails, fall back to original method
this.io.emit('server-disconnected', data);
this.broadcastStatusUpdate();
}
});
this.daemon.on('server-error', (data) => {
this.io.emit('server-error', data);
});
this.daemon.on('server-log', (data) => {
this.io.emit('server-log', data);
});
this.daemon.on('tool-called', (data) => {
this.io.emit('tool-called', {
serverName: data.serverName,
toolName: data.toolName,
duration: data.duration,
timestamp: new Date().toISOString()
});
});
this.daemon.on('error', (data) => {
this.io.emit('error', {
error: data.error,
context: data.context,
timestamp: new Date().toISOString()
});
});
// Listen for config change events - this is the critical part for fixing!
this.daemon.on('config-changed', (config) => {
console.log('[DAEMON-WEB] Config changed event received, broadcasting to WebSocket clients');
this.io.emit('config-changed', {
config,
timestamp: new Date().toISOString()
});
// Broadcast latest status after config change
this.broadcastStatusUpdate();
});
// Listen for log manager events, push logs to web clients in real-time
globalLogManager.on('log-added', (data) => {
this.io.emit('enhanced-log-added', {
serverName: data.serverName,
logEntry: data.logEntry,
timestamp: data.logEntry.timestamp
});
});
globalLogManager.on('server-error', (data) => {
this.io.emit('server-log-error', data);
});
globalLogManager.on('connection-status-changed', (data) => {
this.io.emit('server-connection-status', data);
});
this.daemon.on('server-toggled', (data) => {
console.log(`[DAEMON-WEB] Server toggled event received: ${data.name} enabled: ${data.enabled}`);
const systemStatus = this.getSystemStatus();
if (systemStatus) {
this.io.emit('server-status-changed', {
event: data.enabled ? 'server-enabled' : 'server-disabled',
serverName: data.name,
systemStatus: systemStatus,
originalData: data,
timestamp: new Date().toISOString()
});
}
else {
this.broadcastStatusUpdate();
}
});
}
// API handlers
async handleGetStatus(req, res) {
try {
const status = this.daemon['getFullStatus'](); // Access private method of daemon
res.json(status);
}
catch (error) {
res.status(500).json({
error: 'Failed to get status',
message: error.message
});
}
}
async handleGetServers(req, res) {
try {
// Get all servers from config file to merge configuration information
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const configServers = config.servers || {};
// Get runtime status
const status = this.daemon['getFullStatus']();
const runtimeServers = status.servers || [];
const toolRouter = this.daemon['mcpServer'].getToolRouter();
// Create runtime status map
const runtimeMap = new Map();
(status.servers || []).forEach((server) => {
runtimeMap.set(server.name, server);
});
// Merge configuration and runtime information
const serversWithTools = Object.entries(configServers).map(([serverName, serverConfig]) => {
const runtimeInfo = runtimeMap.get(serverName);
const isConnected = !!runtimeInfo?.connected;
const tools = isConnected ? toolRouter.getToolsByServer(serverName) : [];
const enabledTools = isConnected ? toolRouter.getEnabledToolsByServer(serverName) : [];
return {
// Include all ServerConfig properties and ensure name is set correctly
...serverConfig,
name: serverName, // Always use the key as the definitive name
// Add server runtime status
connected: isConnected,
toolCount: tools.length,
enabledToolCount: enabledTools.length,
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
enabled: this.isToolEnabled(serverConfig, tool.name),
inputSchema: tool.inputSchema,
settings: {}
}))
};
});
res.json(serversWithTools);
}
catch (error) {
res.status(500).json({
error: 'Failed to get servers',
message: error.message
});
}
}
async handleGetTools(req, res) {
try {
const mcpServer = this.daemon['mcpServer'];
const toolRouter = mcpServer.getToolRouter();
const tools = await toolRouter.getAllTools(true);
const toolsWithServer = tools.map(tool => {
const route = toolRouter.findToolRoute(tool.name);
return {
...tool,
serverName: route?.serverName || 'unknown'
};
});
res.json(toolsWithServer);
}
catch (error) {
res.status(500).json({
error: 'Failed to get tools',
message: error.message
});
}
}
async handleCallTool(req, res) {
try {
const { name } = req.params;
const { args } = req.body;
const mcpServer = this.daemon['mcpServer'];
const toolRouter = mcpServer.getToolRouter();
const result = await toolRouter.callTool(name, args || {});
res.json(result);
}
catch (error) {
res.status(500).json({
error: 'Failed to call tool',
message: error.message
});
}
}
async handleGetConfig(req, res) {
try {
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
res.json(config);
}
catch (error) {
res.status(500).json({
error: 'Failed to get config',
message: error.message
});
}
}
async handleGetClients(req, res) {
try {
const clients = this.daemon['clients'];
const clientList = Array.from(clients.values()).map(c => ({
id: c.id,
type: c.type,
lastSeen: c.lastSeen
}));
res.json(clientList);
}
catch (error) {
res.status(500).json({
error: 'Failed to get clients',
message: error.message
});
}
}
async handleReloadConfig(req, res) {
try {
await this.daemon['reloadConfig']();
res.json({ success: true, message: 'Config reloaded' });
}
catch (error) {
res.status(500).json({
error: 'Failed to reload config',
message: error.message
});
}
}
// WebSocket helper methods
async sendStatusUpdate(socket) {
try {
const status = this.daemon['getFullStatus']();
// Get all servers from config file to merge configuration information
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const configServers = config.servers || {};
const toolRouter = this.daemon['mcpServer'].getToolRouter();
// Get latest adapter status directly from toolRouter to avoid caching issues
const allAdapters = toolRouter.getAllAdapters();
const runtimeMap = new Map();
allAdapters.forEach((adapter) => {
runtimeMap.set(adapter.name, {
connected: adapter.isConnected,
toolCount: toolRouter.getToolsByServer(adapter.name).length,
enabledToolCount: toolRouter.getEnabledToolsByServer(adapter.name).length
});
});
// Merge configuration and runtime information
const serversWithTools = Object.entries(configServers).map(([serverName, serverConfig]) => {
const runtimeInfo = runtimeMap.get(serverName);
const isConnected = runtimeInfo ? !!runtimeInfo.connected : false;
const toolCount = runtimeInfo ? runtimeInfo.toolCount : 0;
const enabledToolCount = runtimeInfo ? runtimeInfo.enabledToolCount : 0;
const tools = isConnected ? toolRouter.getToolsByServer(serverName) : [];
return {
// Include all ServerConfig properties and ensure name is set correctly
...serverConfig,
name: serverName, // Always use the key as the definitive name
// Add server runtime status
connected: isConnected,
toolCount: toolCount,
enabledToolCount: enabledToolCount,
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
enabled: this.isToolEnabled(serverConfig, tool.name),
inputSchema: tool.inputSchema,
settings: {}
}))
};
});
// Send status update with structure consistent with /api/servers
const enhancedStatus = {
...status,
servers: serversWithTools
};
console.log(`[DAEMON-WEB] Broadcasting status update: ${serversWithTools.map(s => `${s.name}(connected:${s.connected}, tools:${s.toolCount})`).join(', ')}`);
socket.emit('status-update', enhancedStatus);
}
catch (error) {
console.error('[DAEMON-WEB] Error sending status update:', error);
socket.emit('error', {
error: 'Failed to get status',
message: error.message
});
}
}
broadcastStatusUpdate() {
this.io.sockets.sockets.forEach(socket => {
this.sendStatusUpdate(socket);
});
}
// Get complete system status
getSystemStatus() {
try {
const toolRouter = this.daemon['mcpServer'].getToolRouter();
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const serversConfig = config?.servers || {};
const enabledServers = Object.keys(serversConfig).filter(name => serversConfig[name].enabled !== false);
// Get latest adapter status directly from toolRouter to avoid caching issues
const allAdapters = toolRouter.getAllAdapters();
const runtimeMap = new Map();
allAdapters.forEach((adapter) => {
runtimeMap.set(adapter.name, {
connected: adapter.isConnected,
toolCount: toolRouter.getToolsByServer(adapter.name).length,
enabledToolCount: toolRouter.getEnabledToolsByServer(adapter.name).length
});
});
const connectedCount = allAdapters.filter((adapter) => adapter.isConnected).length;
const totalTools = allAdapters.reduce((sum, adapter) => {
return sum + toolRouter.getToolsByServer(adapter.name).length;
}, 0);
const totalEnabledTools = allAdapters.reduce((sum, adapter) => {
return sum + toolRouter.getEnabledToolsByServer(adapter.name).length;
}, 0);
const serversWithTools = enabledServers.map(serverName => {
const serverConfig = { ...serversConfig[serverName], name: serverName };
const runtimeInfo = runtimeMap.get(serverName);
const isConnected = runtimeInfo ? runtimeInfo.connected : false;
const toolCount = runtimeInfo ? runtimeInfo.toolCount : 0;
const enabledToolCount = runtimeInfo ? runtimeInfo.enabledToolCount : 0;
const tools = isConnected ? toolRouter.getToolsByServer(serverConfig.name) : [];
return {
...serverConfig,
connected: isConnected,
toolCount: toolCount,
enabledToolCount: enabledToolCount,
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
enabled: this.isToolEnabled(serverConfig, tool.name),
inputSchema: tool.inputSchema,
settings: {}
}))
};
});
return {
total: enabledServers.length,
connected: connectedCount,
totalTools: totalTools, // This should be total tools
enabledTools: totalEnabledTools, // This should be enabled tools
servers: serversWithTools
};
}
catch (error) {
console.error('[DAEMON-WEB] Error getting system status:', error);
return null;
}
}
async handleAddServer(req, res) {
try {
const { name, config } = req.body;
const configManager = this.daemon['configManager'];
// Validate server name
const nameValidation = ServerNameValidator.validateServerName(name);
if (!nameValidation.valid) {
return res.status(400).json({
error: 'Invalid server name',
details: nameValidation.error,
suggestions: nameValidation.suggestions
});
}
// Check for name conflicts
if (configManager.checkServerNameConflict(name)) {
return res.status(409).json({
error: 'Server name already exists',
existingName: name
});
}
try {
// Add the server
configManager.addServer(name, config);
await configManager.saveConfig();
await configManager.loadConfig();
// Only start the server if it's enabled, without reloading all config
if (config.enabled) {
console.log(`[DAEMON-WEB] Server ${name} is enabled, starting it directly`);
// Use configManager's toggleServer method to start the server
configManager.toggleServer(name, true);
}
else {
console.log(`[DAEMON-WEB] Server ${name} is disabled, skipping start`);
}
// Emit a server-added event for the specific server
const systemStatus = this.getSystemStatus();
if (systemStatus) {
this.io.emit('server-status-changed', {
event: 'server-added',
serverName: name,
systemStatus: systemStatus,
originalData: { name, ...config },
timestamp: new Date().toISOString()
});
}
res.json({
success: true,
message: `Server ${name} added successfully`,
server: { name, ...config }
});
}
catch (error) {
res.status(500).json({
error: 'Failed to add server',
message: error.message
});
}
}
catch (error) {
res.status(500).json({
error: 'Failed to add server',
message: error.message
});
}
}
async handleUpdateServer(req, res) {
try {
const { name } = req.params;
const serverConfig = req.body;
const configManager = this.daemon['configManager'];
// If name is being updated, validate the new name
if (serverConfig.name && serverConfig.name !== name) {
const nameValidation = ServerNameValidator.validateServerName(serverConfig.name);
if (!nameValidation.valid) {
return res.status(400).json({
error: 'Invalid server name',
details: nameValidation.error,
suggestions: nameValidation.suggestions
});
}
// Check for name conflicts with other servers
if (configManager.checkServerNameConflict(serverConfig.name)) {
return res.status(409).json({
error: 'Server name already exists',
existingName: serverConfig.name
});
}
}
try {
// Get the old server config to check if it was enabled
const oldConfig = configManager.getServerConfig(name);
const wasEnabled = oldConfig?.enabled || false;
const nameChanged = serverConfig.name && serverConfig.name !== name;
// Update the server configuration
configManager.updateServer(name, serverConfig);
await configManager.saveConfig();
await configManager.loadConfig();
// Get the new server config
const newConfig = configManager.getServerConfig(serverConfig.name || name);
const isEnabled = newConfig?.enabled || false;
// Always restart the server if it was enabled, regardless of what changed
// This ensures any config change (command, args, env, etc.) takes effect
if (wasEnabled) {
console.log(`[DAEMON-WEB] Server ${name} config updated, restarting server`);
if (nameChanged) {
// Name changed: disable old server and enable new server
console.log(`[DAEMON-WEB] Disabling old server: ${name}`);
configManager.toggleServer(name, false);
console.log(`[DAEMON-WEB] Enabling new server: ${serverConfig.name}`);
configManager.toggleServer(serverConfig.name, true);
}
else {
// Config changed but name is the same: restart the server
console.log(`[DAEMON-WEB] Restarting server ${name} due to config change`);
configManager.toggleServer(name, false);
configManager.toggleServer(name, true);
}
}
else if (wasEnabled !== isEnabled) {
// Only enabled status changed
console.log(`[DAEMON-WEB] Toggling server ${serverConfig.name || name} to ${isEnabled}`);
configManager.toggleServer(serverConfig.name || name, isEnabled);
}
else {
console.log(`[DAEMON-WEB] Server ${name} updated but was not enabled, no restart needed`);
}
// Emit a server-updated event for the specific server
const systemStatus = this.getSystemStatus();
if (systemStatus) {
this.io.emit('server-status-changed', {
event: 'server-updated',
serverName: serverConfig.name || name,
systemStatus: systemStatus,
originalData: { name: serverConfig.name || name, ...serverConfig },
timestamp: new Date().toISOString()
});
}
// Rely on daemon events to broadcast status update
res.json({
success: true,
message: `Server ${name} updated successfully`,
server: { name: serverConfig.name || name, ...serverConfig }
});
}
catch (error) {
res.status(500).json({
error: 'Failed to update server',
message: error.message
});
}
}
catch (error) {
res.status(500).json({
error: 'Failed to update server',
message: error.message
});
}
}
async handleRemoveServer(req, res) {
try {
const { name } = req.params;
const configManager = this.daemon['configManager'];
// Get the server config before removing it to check if it was enabled
const serverConfig = configManager.getServerConfig(name);
const wasEnabled = serverConfig?.enabled || false;
await configManager.removeServer(name);
await configManager.saveConfig();
await configManager.loadConfig();
// Only stop the server if it was enabled, without reloading all config
if (wasEnabled) {
console.log(`[DAEMON-WEB] Server ${name} was enabled, stopping it directly`);
// Use configManager's toggleServer method to stop the server
configManager.toggleServer(name, false);
}
else {
console.log(`[DAEMON-WEB] Server ${name} was disabled, skipping stop`);
}
// Emit a server-removed event for the specific server
const systemStatus = this.getSystemStatus();
if (systemStatus) {
this.io.emit('server-status-changed', {
event: 'server-removed',
serverName: name,
systemStatus: systemStatus,
originalData: { name },
timestamp: new Date().toISOString()
});
}
// Rely on daemon events to broadcast status update
res.json({ success: true, message: `Server ${name} removed successfully` });
}
catch (error) {
res.status(500).json({
error: 'Failed to remove server',
message: error.message
});
}
}
async handleToggleServer(req, res) {
try {
const { name } = req.params;
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const serverConfig = config.servers[name];
if (!serverConfig) {
return res.status(404).json({ error: 'Server not found' });
}
// Use configManager's toggleServer method, which automatically emits events
const oldEnabled = serverConfig.enabled;
await configManager.toggleServer(name, !oldEnabled);
// Save configuration to persist the change
await configManager.saveConfig();
console.log(`[DAEMON-WEB] Server ${name} toggled: ${oldEnabled} -> ${!oldEnabled}`);
// ConfigManager's toggleServer method emits a server-toggled event
// MCPDogServer already listens to this event and automatically handles server start/stop
// No need to manually call reloadConfig() or manually connect/disconnect servers
// Send composite event, including toggle operation and latest status
setTimeout(() => {
const systemStatus = this.getSystemStatus();
if (systemStatus) {
console.log(`[DAEMON-WEB] Sending server-status-changed event for toggle: ${name}`);
this.io.emit('server-status-changed', {
event: serverConfig.enabled ? 'server-enabled' : 'server-disabled',
serverName: name,
systemStatus: systemStatus,
originalData: { enabled: serverConfig.enabled },
timestamp: new Date().toISOString()
});
}
else {
// If status retrieval fails, fall back to original method
this.broadcastStatusUpdate();
}
}, 100); // Short delay to ensure config is saved
res.json({
success: true,
server: name,
enabled: serverConfig.enabled,
message: `Server ${name} ${serverConfig.enabled ? 'enabled' : 'disabled'}`
});
}
catch (error) {
res.status(500).json({
error: 'Failed to toggle server',
message: error.message
});
}
}
async handleUpdateConfig(req, res) {
try {
const newConfig = req.body;
const configManager = this.daemon['configManager'];
// Update config
configManager['config'] = newConfig; // Directly set config
await configManager.saveConfig();
// Reload daemon configuration
await this.daemon['reloadConfig']();
res.json({ success: true, message: 'Configuration updated successfully' });
}
catch (error) {
res.status(500).json({
error: 'Failed to update config',
message: error.message
});
}
}
// New tool-level control API handler
async handleGetServerTools(req, res) {
try {
const { name } = req.params;
const mcpServer = this.daemon['mcpServer'];
const configManager = this.daemon['configManager'];
const serverConfig = configManager.getServerConfig(name);
if (!serverConfig) {
return res.status(404).json({ error: 'Server not found' });
}
const toolRouter = mcpServer.getToolRouter();
const allTools = toolRouter.getToolsByServer(name);
// Apply tool filtering config
const toolsWithConfig = allTools.map(tool => ({
...tool,
enabled: this.isToolEnabled(serverConfig, tool.name),
settings: serverConfig.toolsConfig?.toolSettings?.[tool.name] || {}
}));
res.json({
serverName: name,
toolsConfig: serverConfig.toolsConfig || { mode: 'all' },
tools: toolsWithConfig
});
}
catch (error) {
res.status(500).json({
error: 'Failed to get server tools',
message: error.message
});
}
}
async handleToggleServerTool(req, res) {
try {
const { name, tool } = req.params;
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const serverConfig = config.servers[name];
if (!serverConfig) {
return res.status(404).json({ error: 'Server not found' });
}
// Initialize tool config
if (!serverConfig.toolsConfig) {
serverConfig.toolsConfig = { mode: 'all' };
}
if (!serverConfig.toolsConfig.toolSettings) {
serverConfig.toolsConfig.toolSettings = {};
}
// Toggle tool status
const currentEnabled = this.isToolEnabled(serverConfig, tool);
serverConfig.toolsConfig.toolSettings[tool] = {
...serverConfig.toolsConfig.toolSettings[tool],
enabled: !currentEnabled
};
// If mode is 'all', switch to 'blacklist' or 'whitelist' mode
if (serverConfig.toolsConfig.mode === 'all') {
serverConfig.toolsConfig.mode = currentEnabled ? 'blacklist' : 'whitelist';
}
// Save configuration with tool-toggle context to avoid server reconnection
await configManager.saveConfig();
// Just broadcast status update instead of full reload
this.broadcastStatusUpdate();
res.json({
success: true,
tool,
enabled: !currentEnabled,
message: `Tool ${tool} ${!currentEnabled ? 'enabled' : 'disabled'}`
});
}
catch (error) {
res.status(500).json({
error: 'Failed to toggle tool',
message: error.message
});
}
}
async handleUpdateServerTools(req, res) {
try {
const { name } = req.params;
const { toolsConfig } = req.body;
const configManager = this.daemon['configManager'];
const config = configManager.getConfig();
const serverConfig = config.servers[name];
if (!serverConfig) {
return res.status(404).json({ error: 'Server not found' });
}
// Update tool config
serverConfig.toolsConfig = toolsConfig;
// Save configuration with tool-config-update context to avoid server reconnection
await configManager.saveConfig();
// Just broadcast status update instead of full reload
this.broadcastStatusUpdate();
res.json({ success: true, message: 'Server tools configuration updated' });
}
catch (error) {
res.status(500).json({
error: 'Failed to update server tools',
message: error.message
});
}
}
// Helper method: check if tool is enabled
isToolEnabled(serverConfig, toolName) {
const toolsConfig = serverConfig.toolsConfig;
if (!toolsConfig) {
return true; // Default to all enabled
}
// Check specific tool settings
const toolSettings = toolsConfig.toolSettings?.[toolName];
if (toolSettings !== undefined) {
return toolSettings.enabled;
}
// Determine based on mode
switch (toolsConfig.mode) {
case 'all':
return true;
case 'whitelist':
return toolsConfig.enabledTools?.includes(toolName) || false;
case 'blacklist':
return !toolsConfig.disabledTools?.includes(toolName);
default:
return true;
}
}
// Log API handlers
async handleGetAllLogs(req, res) {
try {
const limit = parseInt(req.query.limit) || 100;
const logs = globalLogManager.getAllRecentLogs(limit);
res.json(logs);
}
catch (error) {
res.status(500).json({
error: 'Failed to get logs',
message: error.message
});
}
}
async handleGetServerLogs(req, res) {
try {
const { serverName } = req.params;
const limit = parseInt(req.query.limit) || 100;
const level = req.query.level;
const source = req.query.source;
const search = req.query.search;
let logs;
if (search) {
logs = globalLogManager.searchLogs(serverName, search, { level: level, source: source, limit });
}
else {
logs = globalLogManager.getLogs(serverName, limit);
}
res.json(logs);
}
catch (error) {
res.status(500).json({
error: 'Failed to get server logs',
message: error.message
});
}
}
async handleClearServerLogs(req, res) {
try {
const { serverName } = req.params;
globalLogManager.clearLogs(serverName);
res.json({ success: true, message: `Logs cleared for ${serverName}` });
}
catch (error) {
res.status(500).json({
error: 'Failed to clear server logs',
message: error.message
});
}
}
async handleGetServerLogStats(req, res) {
try {
const { serverName } = req.params;
const stats = globalLogManager.getStats(serverName);
if (!stats) {
return res.status(404).json({ error: 'Server not found' });
}
res.json(stats);
}
catch (error) {
res.status(500).json({
error: 'Failed to get server log stats',
message: error.message
});
}
}
// Server control
async start() {
return new Promise((resolve, reject) => {
this.server.listen(this.port, () => {
console.log(`[DAEMON-WEB] Web interface started on port ${this.port}`);
console.log(`[DAEMON-WEB] Dashboard: http://localhost:${this.port}`);
console.log(`[DAEMON-WEB] WebSocket: ws://localhost:${this.port}`);
resolve();
});
this.server.on('error', (error) => {
reject(error);
});
});
}
async stop() {
return new Promise((resolve) => {
this.server.close(() => {
console.log('[DAEMON-WEB] Web server stopped');
resolve();
});
});
}
}
//# sourceMappingURL=daemon-web-server.js.map