Skip to main content
Glama
http-server.tsโ€ข20.8 kB
/** * Nextcloud MCP Server - Streamable HTTP Transport * * This file provides an HTTP server for self-hosting the MCP server on a VPS. * It uses the Streamable HTTP transport for MCP communication. * * Usage: * npm run build * node dist/src/http-server.js * * Or with environment variables: * PORT=8080 node dist/src/http-server.js */ import 'dotenv/config'; import express, { Request, Response } from 'express'; import cors from 'cors'; import fs from 'fs'; import path from 'path'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { setCredentials } from './utils/client-manager.js'; // Import tool registration functions import { registerNotesTools } from './tools/notes.tools.js'; import { registerCalendarTools } from './tools/calendar.tools.js'; import { registerCalendarDebugTools } from './tools/calendar-debug.tools.js'; import { registerContactsTools } from './tools/contacts.tools.js'; import { registerTablesTools } from './tools/tables.tools.js'; import { registerWebDAVTools } from './tools/webdav.tools.js'; import { prefixToolName } from './utils/tool-naming.js'; // Type definition for tool registration functions type ToolRegistrationFn = (server: McpServer) => void; // Configuration const PORT = parseInt(process.env.PORT || '8080', 10); const HOST = process.env.HOST || '0.0.0.0'; const ANALYTICS_FILE = process.env.ANALYTICS_FILE || '/app/data/nextcloud-mcp-analytics.json'; // Analytics tracking interface Analytics { serverStartTime: string; totalRequests: number; totalToolCalls: number; requestsByMethod: Record<string, number>; requestsByEndpoint: Record<string, number>; toolCalls: Record<string, number>; recentToolCalls: Array<{ tool: string; timestamp: string; clientIp: string }>; clientsByIp: Record<string, number>; clientsByUserAgent: Record<string, number>; hourlyRequests: Record<string, number>; } const defaultAnalytics: Analytics = { serverStartTime: new Date().toISOString(), totalRequests: 0, totalToolCalls: 0, requestsByMethod: {}, requestsByEndpoint: {}, toolCalls: {}, recentToolCalls: [], clientsByIp: {}, clientsByUserAgent: {}, hourlyRequests: {}, }; // Load analytics from file or use defaults function loadAnalytics(): Analytics { try { if (fs.existsSync(ANALYTICS_FILE)) { const data = fs.readFileSync(ANALYTICS_FILE, 'utf-8'); const loaded = JSON.parse(data) as Analytics; console.log(`๐Ÿ“Š Loaded analytics from ${ANALYTICS_FILE}`); return loaded; } } catch (error) { console.warn('โš ๏ธ Could not load analytics file, starting fresh:', error); } return { ...defaultAnalytics }; } // Save analytics to file function saveAnalytics(): void { try { const dir = path.dirname(ANALYTICS_FILE); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(ANALYTICS_FILE, JSON.stringify(analytics, null, 2)); } catch (error) { console.warn('โš ๏ธ Could not save analytics file:', error); } } // Auto-save analytics every 5 minutes setInterval(saveAnalytics, 5 * 60 * 1000); // Save on process exit process.on('SIGTERM', () => { console.log('๐Ÿ“Š Saving analytics before shutdown...'); saveAnalytics(); process.exit(0); }); process.on('SIGINT', () => { console.log('๐Ÿ“Š Saving analytics before shutdown...'); saveAnalytics(); process.exit(0); }); const analytics: Analytics = loadAnalytics(); function getUptime(): string { const start = new Date(analytics.serverStartTime).getTime(); const now = Date.now(); const diff = now - start; const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); return `${hours}h ${minutes}m ${seconds}s`; } function trackRequest(req: Request, endpoint: string): void { analytics.totalRequests++; analytics.requestsByMethod[req.method] = (analytics.requestsByMethod[req.method] || 0) + 1; analytics.requestsByEndpoint[endpoint] = (analytics.requestsByEndpoint[endpoint] || 0) + 1; const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.ip || 'unknown'; analytics.clientsByIp[clientIp] = (analytics.clientsByIp[clientIp] || 0) + 1; const userAgent = req.headers['user-agent'] || 'unknown'; const shortAgent = userAgent.split('/')[0] || userAgent.substring(0, 30); analytics.clientsByUserAgent[shortAgent] = (analytics.clientsByUserAgent[shortAgent] || 0) + 1; const hourKey = new Date().toISOString().substring(0, 13) + ':00'; analytics.hourlyRequests[hourKey] = (analytics.hourlyRequests[hourKey] || 0) + 1; } function trackToolCall(toolName: string, clientIp: string): void { analytics.totalToolCalls++; analytics.toolCalls[toolName] = (analytics.toolCalls[toolName] || 0) + 1; analytics.recentToolCalls.unshift({ tool: toolName, timestamp: new Date().toISOString(), clientIp, }); if (analytics.recentToolCalls.length > 100) { analytics.recentToolCalls.pop(); } } // Create MCP server const mcpServer = new McpServer({ name: 'Nextcloud MCP Server', version: '1.0.0', }); // Initialize credentials from environment or query params function initializeCredentials(req?: Request): void { // Check for query params first (user-provided) const host = req?.query.nextcloudHost as string || process.env.NEXTCLOUD_HOST; const username = req?.query.nextcloudUsername as string || process.env.NEXTCLOUD_USERNAME; const password = req?.query.nextcloudPassword as string || process.env.NEXTCLOUD_PASSWORD; if (host && username && password) { setCredentials(host, username, password); } } // Register all tool sets const toolSets: ToolRegistrationFn[] = [ registerNotesTools, registerCalendarTools, registerCalendarDebugTools, registerContactsTools, registerTablesTools, registerWebDAVTools, ]; // Register all tools toolSets.forEach((toolSet) => toolSet(mcpServer)); // Register hello tool for testing mcpServer.tool( prefixToolName('hello'), 'A simple test tool to verify that the MCP server is working correctly', {}, async () => { return { content: [ { type: 'text', text: JSON.stringify({ message: 'Hello from Nextcloud MCP!', timestamp: new Date().toISOString(), transport: 'streamable-http', available_tools: [ 'Notes: nextcloud_notes_create_note, nextcloud_notes_update_note, nextcloud_notes_append_content, nextcloud_notes_search_notes, nextcloud_notes_delete_note', 'Calendar: nextcloud_calendar_list_calendars, nextcloud_calendar_create_event, nextcloud_calendar_list_events, nextcloud_calendar_get_event, nextcloud_calendar_update_event, nextcloud_calendar_delete_event', 'Contacts: nextcloud_contacts_list_addressbooks, nextcloud_contacts_create_addressbook, nextcloud_contacts_delete_addressbook, nextcloud_contacts_list_contacts, nextcloud_contacts_create_contact, nextcloud_contacts_delete_contact', 'Tables: nextcloud_tables_list_tables, nextcloud_tables_get_schema, nextcloud_tables_read_table, nextcloud_tables_insert_row, nextcloud_tables_update_row, nextcloud_tables_delete_row', 'WebDAV: nextcloud_webdav_list_directory, nextcloud_webdav_read_file, nextcloud_webdav_write_file, nextcloud_webdav_create_directory, nextcloud_webdav_delete_resource' ], total_tools: 29, }, null, 2), }, ], }; } ); // Create Express app const app = express(); // Middleware app.use(cors({ origin: '*', methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Accept', 'Authorization', 'Mcp-Session-Id'], exposedHeaders: ['Mcp-Session-Id'], })); app.use(express.json()); // Health check endpoint app.get('/health', (req: Request, res: Response) => { trackRequest(req, '/health'); res.json({ status: 'healthy', server: 'Nextcloud MCP', version: '1.0.0', transport: 'streamable-http', timestamp: new Date().toISOString(), }); }); // Analytics endpoint - summary app.get('/analytics', (req: Request, res: Response) => { trackRequest(req, '/analytics'); const sortedTools = Object.entries(analytics.toolCalls) .sort(([, a], [, b]) => b - a) .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); const sortedClients = Object.entries(analytics.clientsByIp) .sort(([, a], [, b]) => b - a) .slice(0, 20) .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); const last24Hours = Object.entries(analytics.hourlyRequests) .sort(([a], [b]) => b.localeCompare(a)) .slice(0, 24) .reverse() .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); res.json({ server: 'Nextcloud MCP', uptime: getUptime(), serverStartTime: analytics.serverStartTime, summary: { totalRequests: analytics.totalRequests, totalToolCalls: analytics.totalToolCalls, uniqueClients: Object.keys(analytics.clientsByIp).length, }, breakdown: { byMethod: analytics.requestsByMethod, byEndpoint: analytics.requestsByEndpoint, byTool: sortedTools, }, clients: { byIp: sortedClients, byUserAgent: analytics.clientsByUserAgent, }, hourlyRequests: last24Hours, recentToolCalls: analytics.recentToolCalls.slice(0, 20), }); }); // Analytics dashboard - visual HTML page app.get('/analytics/dashboard', (req: Request, res: Response) => { trackRequest(req, '/analytics/dashboard'); const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Nextcloud MCP - Analytics Dashboard</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #0082c9 0%, #00678c 100%); min-height: 100vh; color: #e4e4e7; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; } header { text-align: center; margin-bottom: 30px; padding: 20px; background: rgba(255,255,255,0.1); border-radius: 16px; backdrop-filter: blur(10px); } header h1 { font-size: 2rem; background: linear-gradient(90deg, #fff, #a0d8ef); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 8px; } header p { color: rgba(255,255,255,0.8); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 24px; text-align: center; border: 1px solid rgba(255,255,255,0.2); transition: transform 0.2s; } .stat-card:hover { transform: translateY(-4px); } .stat-card h3 { font-size: 2.5rem; margin-bottom: 8px; color: #fff; } .stat-card p { color: rgba(255,255,255,0.7); font-size: 0.9rem; } .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 30px; } .chart-card { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 20px; border: 1px solid rgba(255,255,255,0.2); } .chart-card h2 { font-size: 1.2rem; margin-bottom: 16px; color: #fff; } .recent-calls { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 20px; border: 1px solid rgba(255,255,255,0.2); } .recent-calls h2 { margin-bottom: 16px; color: #fff; } .call-item { display: flex; justify-content: space-between; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 8px; margin-bottom: 8px; } .call-tool { font-weight: 600; color: #a0d8ef; } .call-time { color: rgba(255,255,255,0.6); font-size: 0.85rem; } .refresh-note { text-align: center; margin-top: 20px; color: rgba(255,255,255,0.5); font-size: 0.85rem; } </style> </head> <body> <div class="container"> <header> <h1>โ˜๏ธ Nextcloud MCP Analytics</h1> <p>Real-time usage statistics for your Nextcloud MCP server</p> </header> <div class="stats-grid" id="stats-grid"></div> <div class="charts-grid"> <div class="chart-card"> <h2>๐Ÿ“Š Tool Usage Distribution</h2> <canvas id="toolsChart"></canvas> </div> <div class="chart-card"> <h2>๐Ÿ“ˆ Hourly Requests (Last 24h)</h2> <canvas id="hourlyChart"></canvas> </div> <div class="chart-card"> <h2>๐Ÿ“ฑ Clients by User Agent</h2> <canvas id="clientsChart"></canvas> </div> <div class="chart-card"> <h2>๐ŸŒ Top IPs</h2> <div id="clients-list"></div> </div> </div> <div class="recent-calls"> <h2>๐Ÿ”„ Recent Tool Calls</h2> <div id="recent-calls-list"></div> </div> <p class="refresh-note">Auto-refreshes every 30 seconds</p> </div> <script> let toolsChart, hourlyChart, clientsChart; async function fetchData() { // Use relative path that works with nginx reverse proxy const basePath = window.location.pathname.replace('/analytics/dashboard', ''); const res = await fetch(basePath + '/analytics'); return res.json(); } function updateStats(data) { document.getElementById('stats-grid').innerHTML = \` <div class="stat-card"> <h3>\${data.summary.totalRequests.toLocaleString()}</h3> <p>Total Requests</p> </div> <div class="stat-card"> <h3>\${data.summary.totalToolCalls.toLocaleString()}</h3> <p>Tool Calls</p> </div> <div class="stat-card"> <h3>\${data.summary.uniqueClients}</h3> <p>Unique Clients</p> </div> <div class="stat-card"> <h3>\${data.uptime}</h3> <p>Uptime</p> </div> \`; } function updateCharts(data) { const toolLabels = Object.keys(data.breakdown.byTool).slice(0, 10); const toolValues = Object.values(data.breakdown.byTool).slice(0, 10); if (toolsChart) toolsChart.destroy(); toolsChart = new Chart(document.getElementById('toolsChart'), { type: 'doughnut', data: { labels: toolLabels, datasets: [{ data: toolValues, backgroundColor: [ '#0082c9', '#00678c', '#a0d8ef', '#5bc0de', '#4a90d9', '#3498db', '#2980b9', '#1abc9c', '#16a085', '#27ae60' ] }] }, options: { responsive: true, plugins: { legend: { position: 'right', labels: { color: '#fff' } } } } }); const hourlyLabels = Object.keys(data.hourlyRequests).map(h => h.split('T')[1] || h); const hourlyValues = Object.values(data.hourlyRequests); if (hourlyChart) hourlyChart.destroy(); hourlyChart = new Chart(document.getElementById('hourlyChart'), { type: 'line', data: { labels: hourlyLabels, datasets: [{ label: 'Requests', data: hourlyValues, borderColor: '#a0d8ef', backgroundColor: 'rgba(160, 216, 239, 0.2)', fill: true, tension: 0.4 }] }, options: { responsive: true, scales: { x: { ticks: { color: '#fff' }, grid: { color: 'rgba(255,255,255,0.1)' } }, y: { ticks: { color: '#fff' }, grid: { color: 'rgba(255,255,255,0.1)' } } }, plugins: { legend: { labels: { color: '#fff' } } } } }); // Clients by User Agent chart const clientLabels = Object.keys(data.clients.byUserAgent).slice(0, 8); const clientValues = Object.values(data.clients.byUserAgent).slice(0, 8); if (clientsChart) clientsChart.destroy(); clientsChart = new Chart(document.getElementById('clientsChart'), { type: 'bar', data: { labels: clientLabels, datasets: [{ label: 'Requests', data: clientValues, backgroundColor: [ '#0082c9', '#00678c', '#a0d8ef', '#5bc0de', '#4a90d9', '#3498db', '#2980b9', '#1abc9c' ] }] }, options: { responsive: true, indexAxis: 'y', scales: { x: { ticks: { color: '#fff' }, grid: { color: 'rgba(255,255,255,0.1)' } }, y: { ticks: { color: '#fff' }, grid: { color: 'rgba(255,255,255,0.1)' } } }, plugins: { legend: { display: false } } } }); // Top IPs list const ipList = document.getElementById('clients-list'); const topIps = Object.entries(data.clients.byIp).slice(0, 10); ipList.innerHTML = topIps.map(([ip, count]) => \` <div class="call-item"> <span class="call-tool">\${ip}</span> <span class="call-time">\${count} requests</span> </div> \`).join('') || '<p style="color: rgba(255,255,255,0.6);">No data yet</p>'; } function updateRecentCalls(data) { const list = document.getElementById('recent-calls-list'); list.innerHTML = data.recentToolCalls.slice(0, 10).map(call => \` <div class="call-item"> <span class="call-tool">\${call.tool}</span> <span class="call-time">\${new Date(call.timestamp).toLocaleString()}</span> </div> \`).join(''); } async function refresh() { const data = await fetchData(); updateStats(data); updateCharts(data); updateRecentCalls(data); } refresh(); setInterval(refresh, 30000); </script> </body> </html> `; res.send(html); }); // Create Streamable HTTP transport (stateless) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); // MCP endpoint app.all('/mcp', async (req: Request, res: Response) => { trackRequest(req, '/mcp'); // Initialize credentials from query params or env initializeCredentials(req); // Track tool calls if (req.body?.method === 'tools/call' && req.body?.params?.name) { const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.ip || 'unknown'; trackToolCall(req.body.params.name, clientIp); } try { await transport.handleRequest(req, res, req.body); } catch (error) { console.error('MCP request error:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Root endpoint with server info app.get('/', (req: Request, res: Response) => { trackRequest(req, '/'); res.json({ name: 'Nextcloud MCP Server', version: '1.0.0', description: 'MCP server for Nextcloud integration (Notes, Calendar, Contacts, Tables, WebDAV)', transport: 'streamable-http', endpoints: { mcp: '/mcp', health: '/health', analytics: '/analytics', analyticsDashboard: '/analytics/dashboard', }, authentication: { description: 'Provide Nextcloud credentials via query params or environment variables', queryParams: ['nextcloudHost', 'nextcloudUsername', 'nextcloudPassword'], example: '/mcp?nextcloudHost=https://cloud.example.com&nextcloudUsername=user&nextcloudPassword=pass', }, documentation: 'https://github.com/hithereiamaliff/mcp-nextcloud', }); }); // Connect server to transport and start listening mcpServer.server.connect(transport) .then(() => { app.listen(PORT, HOST, () => { console.log('='.repeat(60)); console.log('โ˜๏ธ Nextcloud MCP Server (Streamable HTTP)'); console.log('='.repeat(60)); console.log(`๐Ÿ“ Server running on http://${HOST}:${PORT}`); console.log(`๐Ÿ“ก MCP endpoint: http://${HOST}:${PORT}/mcp`); console.log(`โค๏ธ Health check: http://${HOST}:${PORT}/health`); console.log(`๐Ÿ“Š Analytics: http://${HOST}:${PORT}/analytics/dashboard`); console.log('='.repeat(60)); console.log(''); }); }) .catch((error) => { console.error('Failed to start server:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hithereiamaliff/mcp-nextcloud'

If you have feedback or need assistance with the MCP directory API, please join our Discord server