@mcp-shark/mcp-shark
Version:
Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.
201 lines (166 loc) • 7.25 kB
JavaScript
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'url';
import { openDb } from 'mcp-shark-common/db/init.js';
import {
getDatabaseFile,
prepareAppDataSpaces,
getMcpConfigPath,
} from 'mcp-shark-common/configs/index.js';
import { queryRequests } from 'mcp-shark-common/db/query.js';
import { restoreOriginalConfig } from './server/utils/config.js';
import { createRequestsRoutes } from './server/routes/requests.js';
import { createConversationsRoutes } from './server/routes/conversations.js';
import { createSessionsRoutes } from './server/routes/sessions.js';
import { createStatisticsRoutes } from './server/routes/statistics.js';
import { createLogsRoutes } from './server/routes/logs.js';
import { createConfigRoutes } from './server/routes/config.js';
import { createBackupRoutes } from './server/routes/backups.js';
import { createCompositeRoutes } from './server/routes/composite.js';
import { createHelpRoutes } from './server/routes/help.js';
import { createPlaygroundRoutes } from './server/routes/playground.js';
import { createSmartScanRoutes } from './server/routes/smartscan.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MAX_LOG_LINES = 10000;
export function createUIServer() {
prepareAppDataSpaces();
const db = openDb(getDatabaseFile());
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
app.use(express.json());
const clients = new Set();
const mcpSharkLogs = [];
const processState = { mcpSharkProcess: null };
const setMcpSharkProcess = (process) => {
processState.mcpSharkProcess = process;
};
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('close', () => clients.delete(ws));
});
const broadcastLogUpdate = (logEntry) => {
const message = JSON.stringify({ type: 'log', data: logEntry });
clients.forEach((client) => {
if (client.readyState === 1) {
client.send(message);
}
});
};
const restoreConfig = () => {
return restoreOriginalConfig(mcpSharkLogs, broadcastLogUpdate);
};
const requestsRoutes = createRequestsRoutes(db);
const conversationsRoutes = createConversationsRoutes(db);
const sessionsRoutes = createSessionsRoutes(db);
const statisticsRoutes = createStatisticsRoutes(db);
const logsRoutes = createLogsRoutes(mcpSharkLogs, broadcastLogUpdate);
const configRoutes = createConfigRoutes();
const backupRoutes = createBackupRoutes();
const getMcpSharkProcess = () => processState.mcpSharkProcess;
const compositeRoutes = createCompositeRoutes(
getMcpSharkProcess,
setMcpSharkProcess,
mcpSharkLogs,
broadcastLogUpdate
);
const helpRoutes = createHelpRoutes();
const playgroundRoutes = createPlaygroundRoutes();
const smartScanRoutes = createSmartScanRoutes();
app.get('/api/requests', requestsRoutes.getRequests);
app.get('/api/packets', requestsRoutes.getRequests);
app.get('/api/requests/:frameNumber', requestsRoutes.getRequest);
app.get('/api/packets/:frameNumber', requestsRoutes.getRequest);
app.get('/api/requests/export', requestsRoutes.exportRequests);
app.post('/api/requests/clear', requestsRoutes.clearRequests);
app.get('/api/conversations', conversationsRoutes.getConversations);
app.get('/api/sessions', sessionsRoutes.getSessions);
app.get('/api/sessions/:sessionId/requests', sessionsRoutes.getSessionRequests);
app.get('/api/sessions/:sessionId/packets', sessionsRoutes.getSessionRequests);
app.get('/api/statistics', statisticsRoutes.getStatistics);
app.get('/api/composite/logs', logsRoutes.getLogs);
app.post('/api/composite/logs/clear', logsRoutes.clearLogs);
app.get('/api/composite/logs/export', logsRoutes.exportLogs);
app.post('/api/config/services', configRoutes.extractServices);
app.get('/api/config/read', configRoutes.readConfig);
app.get('/api/config/detect', configRoutes.detectConfig);
app.get('/api/config/backups', backupRoutes.listBackups);
app.get('/api/config/backup/view', backupRoutes.viewBackup);
app.post('/api/config/restore', (req, res) => {
backupRoutes.restoreBackup(req, res, mcpSharkLogs, broadcastLogUpdate);
});
app.post('/api/config/backup/delete', (req, res) => {
backupRoutes.deleteBackup(req, res, mcpSharkLogs, broadcastLogUpdate);
});
app.post('/api/composite/setup', compositeRoutes.setup);
app.post('/api/composite/stop', (req, res) => {
compositeRoutes.stop(req, res, restoreConfig);
});
app.get('/api/composite/status', compositeRoutes.getStatus);
app.get('/api/composite/servers', compositeRoutes.getServers);
app.get('/api/help/state', helpRoutes.getState);
app.post('/api/help/dismiss', helpRoutes.dismiss);
app.post('/api/help/reset', helpRoutes.reset);
app.post('/api/playground/proxy', playgroundRoutes.proxyRequest);
app.post('/api/smartscan/scans', smartScanRoutes.createScan);
app.get('/api/smartscan/scans', smartScanRoutes.listScans);
app.get('/api/smartscan/scans/:scanId', smartScanRoutes.getScan);
app.get('/api/smartscan/token', smartScanRoutes.getToken);
app.post('/api/smartscan/token', smartScanRoutes.saveToken);
app.get('/api/smartscan/discover', smartScanRoutes.discoverServers);
app.post('/api/smartscan/scans/batch', smartScanRoutes.createBatchScans);
app.post('/api/smartscan/cached-results', smartScanRoutes.getCachedResults);
app.post('/api/smartscan/cache/clear', smartScanRoutes.clearCache);
const cleanup = () => {
if (processState.mcpSharkProcess) {
processState.mcpSharkProcess.kill();
processState.mcpSharkProcess = null;
}
restoreConfig();
};
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
process.on('exit', () => {
restoreConfig();
});
const staticPath = path.join(__dirname, 'dist');
app.use(express.static(staticPath));
app.get('*', (req, res) => {
res.sendFile(path.join(staticPath, 'index.html'));
});
const notifyClients = async () => {
const requests = queryRequests(db, { limit: 100 });
const { serializeBigInt } = await import('./server/utils/serialization.js');
const message = JSON.stringify({ type: 'update', data: serializeBigInt(requests) });
clients.forEach((client) => {
if (client.readyState === 1) {
client.send(message);
}
});
};
const timestampState = { lastTs: 0 };
setInterval(() => {
const lastCheck = db.prepare('SELECT MAX(timestamp_ns) as max_ts FROM packets').get();
if (lastCheck && lastCheck.max_ts > timestampState.lastTs) {
timestampState.lastTs = lastCheck.max_ts;
notifyClients();
}
}, 500);
return { server, cleanup };
}
export async function runUIServer() {
const port = parseInt(process.env.UI_PORT) || 9853;
const { server, cleanup } = createUIServer();
server.listen(port, '0.0.0.0', () => {
console.log(`UI server listening on http://localhost:${port}`);
});
server.on('close', () => {
cleanup();
});
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
runUIServer().catch(console.error);
}