Skip to main content
Glama
index.js14.8 kB
#!/usr/bin/env node import dotenv from 'dotenv'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import express from 'express'; import axios from 'axios'; // Import Bookstack tool handlers and definitions import { handleCreateBook, createBookToolDefinition } from './src/tools/create-book.js'; import { handleReadBook, readBookToolDefinition } from './src/tools/read-book.js'; import { handleUpdateBook, updateBookToolDefinition } from './src/tools/update-book.js'; import { handleDeleteBook, deleteBookToolDefinition } from './src/tools/delete-book.js'; import { handleListBooks, listBooksToolDefinition } from './src/tools/list-books.js'; import { handleCreateBookshelf, createBookshelfToolDefinition } from './src/tools/create-bookshelf.js'; import { handleReadBookshelf, readBookshelfToolDefinition } from './src/tools/read-bookshelf.js'; import { handleUpdateBookshelf, updateBookshelfToolDefinition } from './src/tools/update-bookshelf.js'; import { handleDeleteBookshelf, deleteBookshelfToolDefinition } from './src/tools/delete-bookshelf.js'; import { handleListBookshelves, listBookshelvesToolDefinition } from './src/tools/list-bookshelves.js'; import { handleCreateChapter, createChapterToolDefinition } from './src/tools/create-chapter.js'; import { handleReadChapter, readChapterToolDefinition } from './src/tools/read-chapter.js'; import { handleUpdateChapter, updateChapterToolDefinition } from './src/tools/update-chapter.js'; import { handleDeleteChapter, deleteChapterToolDefinition } from './src/tools/delete-chapter.js'; import { handleListChapters, listChaptersToolDefinition } from './src/tools/list-chapters.js'; import { handleCreatePage, createPageToolDefinition } from './src/tools/create-page.js'; import { handleReadPage, readPageToolDefinition } from './src/tools/read-page.js'; import { handleUpdatePage, updatePageToolDefinition } from './src/tools/update-page.js'; import { handleDeletePage, deletePageToolDefinition } from './src/tools/delete-page.js'; import { handleListPages, listPagesToolDefinition } from './src/tools/list-pages.js'; // Load environment variables dotenv.config(); class BookstackServer { constructor() { // Initialize MCP server this.server = new Server({ name: 'bookstack-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up error handler this.server.onerror = (error) => console.error('[MCP Error]', error); // Set up tool handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ createBookToolDefinition, readBookToolDefinition, updateBookToolDefinition, deleteBookToolDefinition, listBooksToolDefinition, createBookshelfToolDefinition, readBookshelfToolDefinition, updateBookshelfToolDefinition, deleteBookshelfToolDefinition, listBookshelvesToolDefinition, createChapterToolDefinition, readChapterToolDefinition, updateChapterToolDefinition, deleteChapterToolDefinition, listChaptersToolDefinition, createPageToolDefinition, readPageToolDefinition, updatePageToolDefinition, deletePageToolDefinition, listPagesToolDefinition, ], })); // Set up CallTool handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case 'create_book': return handleCreateBook(this, request.params.arguments); case 'read_book': return handleReadBook(this, request.params.arguments); case 'update_book': return handleUpdateBook(this, request.params.arguments); case 'delete_book': return handleDeleteBook(this, request.params.arguments); case 'list_books': return handleListBooks(this, request.params.arguments); case 'create_bookshelf': return handleCreateBookshelf(this, request.params.arguments); case 'read_bookshelf': return handleReadBookshelf(this, request.params.arguments); case 'update_bookshelf': return handleUpdateBookshelf(this, request.params.arguments); case 'delete_bookshelf': return handleDeleteBookshelf(this, request.params.arguments); case 'list_bookshelves': return handleListBookshelves(this, request.params.arguments); case 'create_chapter': return handleCreateChapter(this, request.params.arguments); case 'read_chapter': return handleReadChapter(this, request.params.arguments); case 'update_chapter': return handleUpdateChapter(this, request.params.arguments); case 'delete_chapter': return handleDeleteChapter(this, request.params.arguments); case 'list_chapters': return handleListChapters(this, request.params.arguments); case 'create_page': return handleCreatePage(this, request.params.arguments); case 'read_page': return handleReadPage(this, request.params.arguments); case 'update_page': return handleUpdatePage(this, request.params.arguments); case 'delete_page': return handleDeletePage(this, request.params.arguments); case 'list_pages': return handleListPages(this, request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); // Initialize API configuration this.baseUrl = process.env.BS_URL || 'https://knowledge.oculair.ca'; this.tokenId = process.env.BS_TOKEN_ID; this.tokenSecret = process.env.BS_TOKEN_SECRET; if (!this.tokenId || !this.tokenSecret) { console.warn('Warning: BS_TOKEN_ID or BS_TOKEN_SECRET environment variables not set'); } // Initialize axios instance for API requests this.api = axios.create({ baseURL: this.baseUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, }); } // Helper method to get API headers with authentication getApiHeaders() { return { 'Authorization': `Token ${this.tokenId}:${this.tokenSecret}`, 'Content-Type': 'application/json', 'Accept': 'application/json', }; } // Helper method to create error responses createErrorResponse(error) { console.error('Error in tool handler:', error); return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message, details: error.response?.data || error, }, null, 2), }], isError: true, }; } async runStdio() { try { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Bookstack MCP server running on stdio'); const cleanup = async () => { await this.server.close(); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('uncaughtException', async (error) => { console.error('Uncaught exception:', error); await cleanup(); }); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); console.error('Failed to start server:', error); process.exit(1); } } async runSSE() { try { const app = express(); let transport; let activeConnections = new Map(); app.get('/sse', async (req, res) => { console.log('Received SSE connection'); // Set headers for SSE res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Generate client ID const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; transport = new SSEServerTransport('/message', res); await this.server.connect(transport); // Store connection info activeConnections.set(clientId, { transport, connectedAt: new Date(), req, res }); console.log(`SSE connection established for client ${clientId}`); console.log(`Active connections: ${activeConnections.size}`); // Send initial connected message res.write(': connected\n\n'); req.on('close', () => { console.log(`SSE connection closed for client ${clientId}`); activeConnections.delete(clientId); console.log(`Connection lost, waiting for new client connection to reconnect...`); }); this.server.onclose = async () => { console.log('Server closing...'); await this.server.close(); }; }); app.post('/message', async (req, res) => { try { console.log('Received message'); if (!transport) { console.error('No active SSE connection'); res.status(503).json({ error: 'No active SSE connection' }); return; } await transport.handlePostMessage(req, res); } catch (error) { console.error('Error handling message:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Add a health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', activeConnections: activeConnections.size, uptime: process.uptime() }); }); // Set up a ping interval to keep connections alive const pingInterval = setInterval(() => { if (activeConnections.size > 0) { console.log(`Sending ping to ${activeConnections.size} active connections`); for (const [clientId, connection] of activeConnections.entries()) { try { if (connection.res && !connection.res.finished) { connection.res.write(': ping\n\n'); } } catch (error) { console.error(`Error sending ping to client ${clientId}:`, error); activeConnections.delete(clientId); } } } }, 30000); // Send ping every 30 seconds const PORT = process.env.PORT || 3001; const httpServer = app.listen(PORT, () => { console.log(`Bookstack MCP server running on port ${PORT}`); console.log(`Bookstack API: ${this.baseUrl}`); console.log(`Connection tracking: enabled with ping interval (30s)`); }); const cleanup = async () => { console.log('Starting cleanup process...'); // Clear the ping interval console.log('Clearing ping interval'); clearInterval(pingInterval); // Clean up all active connections console.log(`Cleaning up ${activeConnections.size} active connections`); for (const [clientId, connection] of activeConnections.entries()) { console.log(`Closing connection for client ${clientId}`); activeConnections.delete(clientId); } // Close the HTTP server console.log('Closing HTTP server...'); httpServer.close(); // Close the MCP server console.log('Closing MCP server...'); await this.server.close(); console.log('Cleanup complete, exiting process'); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('uncaughtException', async (error) => { console.error('Uncaught exception:', error); await cleanup(); }); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); console.error('Failed to start SSE server:', error); process.exit(1); } } } const server = new BookstackServer(); const useSSE = process.argv.includes('--sse'); if (useSSE) { server.runSSE().catch(console.error); } else { server.runStdio().catch(console.error); }

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/oculairmedia/Ghost-MCP'

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