Skip to main content
Glama
r-huijts

OpenTK Model Context Protocol Server

by r-huijts

get_upcoming_activities

Fetch upcoming Dutch parliamentary activities, including debates and committee meetings, with details like date, time, and location. Helps users track and plan participation in parliamentary sessions. Results are sorted by date and can be limited using the 'limit' parameter.

Instructions

Retrieves a list of upcoming parliamentary activities including debates, committee meetings, and other events. The response contains a structured JSON object with both a chronological list of activities and activities grouped by date. Each activity includes details like date, time, location, committee, type, and a URL for more information. Use this tool when a user asks about the parliamentary agenda, wants to know what events are coming up, or needs information about specific types of parliamentary activities. The results are sorted by date with the most imminent activities first. You can limit the number of results using the optional 'limit' parameter. This tool is particularly useful for helping users plan which parliamentary sessions to follow or for providing an overview of the upcoming parliamentary schedule.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of activities to return (default: 20, max: 100)

Implementation Reference

  • src/index.ts:517-587 (registration)
    MCP tool registration for get_upcoming_activities. Includes tool name, description, input schema (optional limit parameter), and complete handler function that fetches the activities HTML page, extracts activities using helper function, sorts chronologically, limits results, groups by date, and formats as JSON response.
    /** Get upcoming activities */ mcp.tool( "get_upcoming_activities", "Retrieves a list of upcoming parliamentary activities including debates, committee meetings, and other events. The response contains a structured JSON object with both a chronological list of activities and activities grouped by date. Each activity includes details like date, time, location, committee, type, and a URL for more information. The results are sorted by date with the most imminent activities first. The optional 'limit' parameter controls the number of results returned (default: 20, max: 100).", { limit: z.number().optional().describe("Maximum number of activities to return (default: 20, max: 100)") }, async ({ limit = 20 }) => { try { // Validate and cap the limit const validatedLimit = Math.min(Math.max(1, limit), 100); const html = await apiService.fetchHtml("/activiteiten.html"); const activities = extractActivitiesFromHtml(html, BASE_URL); if (activities.length === 0) { // If we couldn't extract activities from the HTML, return a simplified response // This could happen if the page structure changes or uses dynamic content return { content: [{ type: "text", text: JSON.stringify({ error: "No upcoming activities found or there was an error retrieving the activities list.", note: "The activities page may use dynamic content rendering. Please try again later or check the website directly.", url: `${BASE_URL}/activiteiten.html` }, null, 2) }] }; } // Sort activities by date (most recent first) and limit the results const sortedActivities = [...activities].sort((a, b) => { const dateA = new Date(a.date + (a.time ? ` ${a.time}` : '')); const dateB = new Date(b.date + (b.time ? ` ${b.time}` : '')); return dateA.getTime() - dateB.getTime(); // Ascending order (upcoming first) }).slice(0, validatedLimit); // Group activities by date for better organization const groupedActivities: Record<string, any[]> = {}; sortedActivities.forEach(activity => { const date = activity.date || 'unknown'; if (!groupedActivities[date]) { groupedActivities[date] = []; } groupedActivities[date].push(activity); }); return { content: [{ type: "text", text: JSON.stringify({ total: activities.length, limit: validatedLimit, groupedByDate: groupedActivities, activities: sortedActivities }, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: JSON.stringify({ error: `Error fetching upcoming activities: ${error.message || 'Unknown error'}`, url: `${BASE_URL}/activiteiten.html` }, null, 2) }] }; } } );
  • Core parsing handler: extractActivitiesFromHtml parses the HTML table from /activiteiten.html, extracts fields (id, title, date, time, location, committee, type, url) from each table row using regex, constructs Activity objects, returns array. This is the main logic executed by the tool handler.
    export function extractActivitiesFromHtml(html: string, baseUrl: string): Activity[] { if (!html) { return []; } const activities: Activity[] = []; // Extract the table containing activities const tableRegex = /<table[^>]*>[\s\S]*?<tbody>([\s\S]*?)<\/tbody>/i; const tableMatch = html.match(tableRegex); if (!tableMatch || !tableMatch[1]) { return []; } const tableContent = tableMatch[1]; // Extract each row (activity) from the table const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi; let rowMatch; while ((rowMatch = rowRegex.exec(tableContent)) !== null) { if (!rowMatch[1]) continue; const rowContent = rowMatch[1]; // Extract cells const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi; const cells: string[] = []; let cellMatch; while ((cellMatch = cellRegex.exec(rowContent)) !== null) { if (cellMatch[1]) { cells.push(cellMatch[1].trim()); } } if (cells.length < 3) continue; // Extract date and time if (!cells[0]) continue; const dateCell = cells[0]; const dateMatch = dateCell.match(/(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2}))?/); if (!dateMatch || !dateMatch[1]) continue; const date = dateMatch[1]; const time = dateMatch[2] || undefined; // Extract location (zaal) if available let location: string | undefined = undefined; if (cells.length > 1 && cells[1]) { location = cells[1].replace(/<[^>]+>/g, "").trim() || undefined; } // Extract committee if available let committee: string | undefined = undefined; if (cells.length > 2 && cells[2]) { committee = cells[2].replace(/<[^>]+>/g, "").trim() || undefined; // Extract committee name from abbr title if present const abbrMatch = cells[2].match(/<abbr title="([^"]+)">/i); if (abbrMatch && abbrMatch[1]) { committee = abbrMatch[1].trim(); } } // Extract title and link from the subject column (index 3) if (cells.length <= 3 || !cells[3]) continue; const titleCell = cells[3]; const titleMatch = titleCell.match(/<a href="(activiteit\.html\?nummer=([^"]+))">([^<]+)<\/a>/); if (!titleMatch || !titleMatch[1] || !titleMatch[2] || !titleMatch[3]) continue; const id = titleMatch[2]; const url = new URL(titleMatch[1], baseUrl).href; const title = titleMatch[3].trim(); // Extract type/description if available (index 4) let type: string | undefined = undefined; if (cells.length > 4 && cells[4]) { type = cells[4].replace(/<[^>]+>/g, "").trim() || undefined; } activities.push({ id: id, title: title, date: date, time, location, committee, type, url: url }); } return activities; }
  • TypeScript interface defining the schema/structure of each Activity object returned by the parser and tool.
    interface Activity { id: string; title: string; date: string; time?: string; location?: string; committee?: string; url: string; type?: string; }
  • apiService.fetchHtml utility fetches the raw HTML from /activiteiten.html used by the tool handler.
    async fetchHtml(path: string, options: RequestInit = {}): Promise<string> { try { // Ensure the path starts with a slash const normalizedPath = path.startsWith('/') ? path : `/${path}`; // Set default headers if not provided const headers = { 'User-Agent': 'Mozilla/5.0 (compatible; OpenTK-MCP/1.0)', ...options.headers }; const res = await fetch(`${BASE_URL}${normalizedPath}`, { ...options, headers, agent: ApiService.agent } as NodeRequestInit); if (!res.ok) { throw new Error(`API error: ${res.status} ${res.statusText}`); } return await res.text(); } catch (error) { throw 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/r-huijts/opentk-mcp'

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