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;
      }
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses key behavioral traits: the response format (structured JSON with chronological and grouped lists), sorting (by date, most imminent first), and an optional parameter for limiting results. However, it lacks details on error handling, rate limits, or authentication needs, which are important for a tool with no annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded, starting with the core purpose and response details. Every sentence adds value, such as usage guidelines and sorting behavior, but it could be slightly more concise by integrating some details more tightly without losing clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description does a decent job covering purpose, usage, and basic behavior. However, it lacks information on error cases, pagination, or the exact structure of the JSON response, which would be helpful for an agent to handle the tool effectively in more complex scenarios.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, with the 'limit' parameter documented as 'Maximum number of activities to return (default: 20, max: 100)'. The description adds value by explaining the purpose of limiting results ('You can limit the number of results using the optional 'limit' parameter') and implying its optionality, but doesn't provide additional semantics beyond what the schema already covers.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('Retrieves') and resource ('list of upcoming parliamentary activities'), specifying the types (debates, committee meetings, other events) and distinguishing it from siblings like get_committee_details or get_voting_results by focusing on upcoming activities rather than details of specific entities.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states when to 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.' It also provides context on usefulness for planning or overview, though it doesn't explicitly name alternatives, the guidance is clear and actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

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