Skip to main content
Glama
http-server.ts19.8 kB
#!/usr/bin/env node /** * Skolverket MCP Server - HTTP/SSE Transport * * Denna server exponerar skolverket-mcp via HTTP med Server-Sent Events * så att den kan användas från webbaserade AI-chatbotar. * * Starta servern: * npm run start:http * * Använd från MCP-klient: * URL: http://localhost:3000/sse */ import express, { Request, Response } from 'express'; import cors from 'cors'; import { v4 as uuidv4 } from 'uuid'; import { log, createRequestLogger } from './logger.js'; // Importera alla verktyg import { searchSubjects, getSubjectDetails, getSubjectVersions } from './tools/syllabus/subjects.js'; import { searchCourses, getCourseDetails, getCourseVersions } from './tools/syllabus/courses.js'; import { searchPrograms, getProgramDetails, getProgramVersions } from './tools/syllabus/programs.js'; import { searchCurriculums, getCurriculumDetails, getCurriculumVersions } from './tools/syllabus/curriculums.js'; import { getSchoolTypes, getTypesOfSyllabus, getSubjectAndCourseCodes, getStudyPathCodes, getApiInfo } from './tools/syllabus/valuestore.js'; import { searchSchoolUnits, getSchoolUnitDetails, getSchoolUnitsByStatus, searchSchoolUnitsByName } from './tools/school-units/search.js'; import { searchAdultEducation, getAdultEducationDetails, filterAdultEducationByDistance, filterAdultEducationByPace } from './tools/planned-education/adult-education.js'; import { getEducationAreas, getDirections } from './tools/planned-education/support-data.js'; import { healthCheck } from './tools/health.js'; const PORT = process.env.PORT || 3000; const ENABLE_CORS = process.env.ENABLE_CORS !== 'false'; const SSE_TIMEOUT_MS = parseInt(process.env.SSE_TIMEOUT_MS || '600000'); // 10 minutes default const SSE_KEEPALIVE_MS = parseInt(process.env.SSE_KEEPALIVE_MS || '30000'); // 30 seconds default const app = express(); // Middleware app.use(express.json({ limit: '10mb' })); if (ENABLE_CORS) { app.use(cors({ origin: '*', methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], })); } // Tool registry const tools: Record<string, (args: any) => Promise<any>> = { // Syllabus API search_subjects: searchSubjects, get_subject_details: getSubjectDetails, get_subject_versions: getSubjectVersions, search_courses: searchCourses, get_course_details: getCourseDetails, get_course_versions: getCourseVersions, search_programs: searchPrograms, get_program_details: getProgramDetails, get_program_versions: getProgramVersions, search_curriculums: searchCurriculums, get_curriculum_details: getCurriculumDetails, get_curriculum_versions: getCurriculumVersions, get_school_types: getSchoolTypes, get_types_of_syllabus: getTypesOfSyllabus, get_subject_and_course_codes: getSubjectAndCourseCodes, get_study_path_codes: getStudyPathCodes, get_api_info: getApiInfo, // School Units API search_school_units: searchSchoolUnits, get_school_unit_details: getSchoolUnitDetails, get_school_units_by_status: getSchoolUnitsByStatus, search_school_units_by_name: searchSchoolUnitsByName, // Planned Education API search_adult_education: searchAdultEducation, get_adult_education_details: getAdultEducationDetails, filter_adult_education_by_distance: filterAdultEducationByDistance, filter_adult_education_by_pace: filterAdultEducationByPace, get_education_areas: getEducationAreas, get_directions: getDirections, // Diagnostics health_check: healthCheck, }; // Root endpoint with documentation app.get('/', (req: Request, res: Response) => { const html = ` <!DOCTYPE html> <html lang="sv"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Skolverket MCP Server</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; text-align: center; } .header h1 { font-size: 2.5em; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); } .header p { font-size: 1.2em; opacity: 0.95; } .content { padding: 40px; } .section { margin-bottom: 40px; } .section h2 { color: #667eea; font-size: 1.8em; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 3px solid #667eea; } .section h3 { color: #764ba2; font-size: 1.3em; margin: 20px 0 10px 0; } .endpoint { background: #f7f7f7; padding: 20px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #667eea; } .endpoint code { background: #333; color: #0f0; padding: 2px 8px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 0.9em; } .endpoint .method { display: inline-block; padding: 4px 12px; border-radius: 4px; font-weight: bold; font-size: 0.85em; margin-right: 10px; } .method.get { background: #10b981; color: white; } .method.post { background: #3b82f6; color: white; } .tool-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; margin-top: 15px; } .tool-item { background: #f0f4ff; padding: 10px 15px; border-radius: 6px; font-size: 0.9em; border-left: 3px solid #667eea; } .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0; } .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 10px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .stat-card h3 { color: white; font-size: 2.5em; margin-bottom: 5px; } .stat-card p { opacity: 0.9; font-size: 1em; } .code-block { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 8px; overflow-x: auto; margin: 15px 0; font-family: 'Courier New', monospace; font-size: 0.9em; line-height: 1.5; } .code-block .keyword { color: #569cd6; } .code-block .string { color: #ce9178; } .code-block .comment { color: #6a9955; } a { color: #667eea; text-decoration: none; font-weight: 500; } a:hover { text-decoration: underline; } .footer { background: #f7f7f7; padding: 20px 40px; text-align: center; color: #666; border-top: 1px solid #e0e0e0; } .badge { display: inline-block; padding: 4px 12px; background: #10b981; color: white; border-radius: 12px; font-size: 0.85em; font-weight: bold; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎓 Skolverket MCP Server</h1> <p>Model Context Protocol Server för Skolverkets öppna API:er</p> <p style="margin-top: 10px;"><span class="badge">v2.6.0</span> <span class="badge" style="background: #3b82f6;">HTTP/SSE</span></p> </div> <div class="content"> <div class="stats"> <div class="stat-card"> <h3>${Object.keys(tools).length}</h3> <p>Tillgängliga Verktyg</p> </div> <div class="stat-card"> <h3>3</h3> <p>Skolverket API:er</p> </div> <div class="stat-card"> <h3>100%</h3> <p>Gratis & Open Source</p> </div> </div> <div class="section"> <h2>📚 Om Tjänsten</h2> <p> Denna MCP-server ger AI-assistenter tillgång till Skolverkets öppna data via Model Context Protocol. Servern kan användas i webbaserade chatbotar, Claude Code, och andra MCP-kompatibla klienter. </p> <p style="margin-top: 10px;"> <strong>Funktioner:</strong> Sök i läroplaner, kurser, program, skolenheter, och vuxenutbildningar. Med avancerad retry-logik, caching, och strukturerad logging. </p> </div> <div class="section"> <h2>🔌 API Endpoints</h2> <div class="endpoint"> <span class="method get">GET</span> <code>/health</code> <p style="margin-top: 10px;">Kontrollera serverns status och hälsa.</p> <a href="/health" target="_blank">Testa →</a> </div> <div class="endpoint"> <span class="method get">GET</span> <code>/tools</code> <p style="margin-top: 10px;">Lista alla tillgängliga MCP-verktyg (${Object.keys(tools).length} st).</p> <a href="/tools" target="_blank">Testa →</a> </div> <div class="endpoint"> <span class="method get">GET</span> <code>/sse</code> <p style="margin-top: 10px;">Server-Sent Events endpoint för real-time MCP-kommunikation.</p> </div> <div class="endpoint"> <span class="method post">POST</span> <code>/execute</code> <p style="margin-top: 10px;">Kör ett specifikt verktyg med givna argument.</p> <div class="code-block"> <span class="comment">// Exempel request:</span> POST /execute Content-Type: application/json { <span class="string">"tool"</span>: <span class="string">"search_subjects"</span>, <span class="string">"arguments"</span>: { <span class="string">"name"</span>: <span class="string">"matematik"</span> } } </div> </div> </div> <div class="section"> <h2>🛠️ Tillgängliga Verktyg</h2> <p>Servern erbjuder ${Object.keys(tools).length} verktyg uppdelade i kategorier:</p> <h3>📖 Läroplan & Kurser (Syllabus API)</h3> <div class="tool-grid"> <div class="tool-item">search_subjects</div> <div class="tool-item">get_subject_details</div> <div class="tool-item">get_subject_versions</div> <div class="tool-item">search_courses</div> <div class="tool-item">get_course_details</div> <div class="tool-item">get_course_versions</div> <div class="tool-item">search_programs</div> <div class="tool-item">get_program_details</div> <div class="tool-item">get_program_versions</div> <div class="tool-item">search_curriculums</div> <div class="tool-item">get_curriculum_details</div> <div class="tool-item">get_curriculum_versions</div> </div> <h3>🏫 Skolenheter (School Units API)</h3> <div class="tool-grid"> <div class="tool-item">search_school_units</div> <div class="tool-item">get_school_unit_details</div> <div class="tool-item">get_school_units_by_status</div> <div class="tool-item">search_school_units_by_name</div> </div> <h3>👨‍🎓 Vuxenutbildning (Planned Education API)</h3> <div class="tool-grid"> <div class="tool-item">search_adult_education</div> <div class="tool-item">get_adult_education_details</div> <div class="tool-item">filter_adult_education_by_distance</div> <div class="tool-item">filter_adult_education_by_pace</div> </div> <h3>🔧 Support & Diagnostik</h3> <div class="tool-grid"> <div class="tool-item">get_school_types</div> <div class="tool-item">get_types_of_syllabus</div> <div class="tool-item">get_subject_and_course_codes</div> <div class="tool-item">get_study_path_codes</div> <div class="tool-item">get_api_info</div> <div class="tool-item">get_education_areas</div> <div class="tool-item">get_directions</div> <div class="tool-item">health_check</div> </div> </div> <div class="section"> <h2>🚀 Kom Igång</h2> <h3>Använd i Claude Code</h3> <div class="code-block"> <span class="comment"># Lägg till MCP-server i Claude Code:</span> claude mcp add --transport http skolverket \\ https://skolverket-mcp.onrender.com/sse </div> <h3>Använd i Webbaserad Chatbot</h3> <div class="code-block"> { <span class="string">"mcpServers"</span>: [ { <span class="string">"name"</span>: <span class="string">"skolverket"</span>, <span class="string">"url"</span>: <span class="string">"https://skolverket-mcp.onrender.com"</span>, <span class="string">"transport"</span>: <span class="string">"http"</span>, <span class="string">"endpoints"</span>: { <span class="string">"tools"</span>: <span class="string">"/tools"</span>, <span class="string">"execute"</span>: <span class="string">"/execute"</span>, <span class="string">"health"</span>: <span class="string">"/health"</span> } } ] } </div> <h3>Curl Exempel</h3> <div class="code-block"> <span class="comment"># Sök efter ämnen:</span> curl -X POST https://skolverket-mcp.onrender.com/execute \\ -H <span class="string">"Content-Type: application/json"</span> \\ -d <span class="string">'{ "tool": "search_subjects", "arguments": { "name": "matematik" } }'</span> </div> </div> <div class="section"> <h2>📖 Resurser</h2> <p> <a href="https://github.com/KSAklfszf921/skolverket-mcp" target="_blank">📦 GitHub Repository</a> • <a href="https://api.skolverket.se" target="_blank">🔗 Skolverkets API</a> • <a href="https://modelcontextprotocol.io" target="_blank">📚 MCP Dokumentation</a> </p> </div> </div> <div class="footer"> <p> Skolverket MCP Server v2.6.0 • Byggd med Node.js, TypeScript, Express & MCP SDK </p> <p style="margin-top: 5px; font-size: 0.9em;"> Deployad på Render • ${new Date().getFullYear()} • Open Source MIT License </p> </div> </div> </body> </html> `; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(html); }); // Health check endpoint app.get('/health', (req: Request, res: Response) => { const requestId = uuidv4(); const reqLog = createRequestLogger(requestId); reqLog.info('Health check requested'); res.json({ status: 'healthy', service: 'skolverket-mcp', version: '2.6.0', timestamp: new Date().toISOString(), transport: 'http-sse', endpoints: { health: '/health', sse: '/sse', tools: '/tools', }, toolCount: Object.keys(tools).length, }); }); // List tools endpoint app.get('/tools', (req: Request, res: Response) => { const requestId = uuidv4(); const reqLog = createRequestLogger(requestId); reqLog.info('Tools list requested'); const toolList = Object.keys(tools).map(name => ({ name, description: `Skolverket MCP tool: ${name}`, })); res.json({ tools: toolList, count: toolList.length, }); }); // SSE endpoint for MCP communication app.get('/sse', async (req: Request, res: Response) => { const requestId = uuidv4(); const reqLog = createRequestLogger(requestId); reqLog.info('SSE connection established'); // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Send initial connection message res.write(`event: connected\n`); res.write(`data: ${JSON.stringify({ type: 'connection', requestId, timestamp: new Date().toISOString(), service: 'skolverket-mcp', version: '2.6.0', })}\n\n`); // Keepalive ping const keepalive = setInterval(() => { res.write(`: keepalive\n\n`); }, SSE_KEEPALIVE_MS); // Connection timeout const connectionTimeout = setTimeout(() => { reqLog.info('SSE connection timeout', { timeoutMs: SSE_TIMEOUT_MS }); clearInterval(keepalive); res.end(); }, SSE_TIMEOUT_MS); // Cleanup on disconnect req.on('close', () => { clearInterval(keepalive); clearTimeout(connectionTimeout); reqLog.info('SSE connection closed'); }); }); // Execute tool endpoint app.post('/execute', async (req: Request, res: Response) => { const requestId = uuidv4(); const reqLog = createRequestLogger(requestId); try { const { tool, arguments: args } = req.body; if (!tool) { return res.status(400).json({ error: 'Missing required parameter: tool', requestId, }); } reqLog.info('Tool execution requested', { tool, args }); const toolFunction = tools[tool]; if (!toolFunction) { return res.status(404).json({ error: `Unknown tool: ${tool}`, availableTools: Object.keys(tools), requestId, }); } // Execute tool const result = await toolFunction(args || {}); reqLog.info('Tool execution completed', { tool }); res.json({ success: true, tool, result, requestId, timestamp: new Date().toISOString(), }); } catch (error: any) { const reqLog = createRequestLogger(requestId); reqLog.error('Tool execution failed', { error: error.message, stack: error.stack, }); res.status(500).json({ error: error.message || 'Internal server error', code: error.code || 'INTERNAL_ERROR', requestId, timestamp: new Date().toISOString(), }); } }); // Error handling middleware app.use((error: Error, req: Request, res: Response, next: any) => { log.error('Unhandled error', { error: error.message, stack: error.stack, url: req.url, method: req.method, }); res.status(500).json({ error: 'Internal server error', message: error.message, timestamp: new Date().toISOString(), }); }); // 404 handler app.use((req: Request, res: Response) => { res.status(404).json({ error: 'Endpoint not found', path: req.path, availableEndpoints: ['/health', '/tools', '/sse', '/execute'], }); }); // Start server app.listen(PORT, () => { log.info(`Skolverket MCP Server (HTTP/SSE) started`, { port: PORT, endpoints: { health: `http://localhost:${PORT}/health`, tools: `http://localhost:${PORT}/tools`, sse: `http://localhost:${PORT}/sse`, execute: `http://localhost:${PORT}/execute`, }, cors: ENABLE_CORS, environment: process.env.NODE_ENV || 'development', }); console.error(`\n✅ Skolverket MCP Server (HTTP/SSE) is running!`); console.error(`📍 Health check: http://localhost:${PORT}/health`); console.error(`🛠️ Tools list: http://localhost:${PORT}/tools`); console.error(`📡 SSE endpoint: http://localhost:${PORT}/sse`); console.error(`⚡ Execute tool: POST http://localhost:${PORT}/execute`); console.error(`\nFor use with AI chatbots, provide the base URL: http://localhost:${PORT}\n`); }); // Graceful shutdown process.on('SIGTERM', () => { log.info('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { log.info('SIGINT received, shutting down gracefully'); process.exit(0); });

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/isakskogstad/skolverket-syllabus-mcp'

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