Skip to main content
Glama
r-huijts

OpenTK Model Context Protocol Server

by r-huijts

search_by_category

Filter and retrieve parliamentary documents by specific categories (questions, motions, or all types) using advanced search syntax. Returns structured JSON results sorted by date for targeted research.

Instructions

Performs a search specifically for documents of a certain category, such as questions, motions, or letters. The response contains a structured JSON object with paginated results and metadata. Use this tool when a user wants to find documents of a specific type that match certain keywords or when they need more targeted search results than the general search provides. The 'category' parameter lets you filter by document type: 'vragen' for parliamentary questions, 'moties' for motions, or 'alles' for all document types. The search syntax supports advanced queries: 'Joe Biden' finds documents with both terms anywhere, '"Joe Biden"' (with quotes) finds exact phrases, 'Hubert NOT Bruls' finds documents with 'Hubert' but not 'Bruls' (capital NOT is required), and you can use 'OR' for alternatives. Results are sorted by date with the most recent documents first. This tool is particularly useful for finding specific types of parliamentary documents on a given topic.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
categoryYesDocument category: 'vragen' for questions, 'moties' for motions, 'alles' for all document types
limitNoMaximum number of results to return per page (default: 20, max: 100)
pageNoPage number for paginated results (default: 1)
queryYesSearch term - any keyword, name, policy area, or quote you want to find in parliamentary records

Implementation Reference

  • The handler function executes the tool logic: validates input, calls apiService.search with {soorten: category} for category-specific search, sorts results by recency, paginates, formats to summary (id, title, category, date, url), returns structured JSON with pagination metadata or error handling.
    async ({ query, category, page = 1, limit = 20 }) => {
      try {
        // Validate and cap the limit
        const validatedLimit = Math.min(Math.max(1, limit), 100);
        const validatedPage = Math.max(1, page);
    
        const data = await apiService.search<{ results: any[], error?: string }>(query, { soorten: category });
    
        // Check if there's an error message in the response
        if (data.error) {
          return {
            content: [
              { type: "text", text: data.error },
              { type: "text", text: JSON.stringify(data.results || [], null, 2) }
            ]
          };
        }
    
        // If no results were found
        if (!data.results || data.results.length === 0) {
          return {
            content: [{
              type: "text",
              text: `No results found for query: ${query} with category: ${category}. Try using different keywords or a different category.`
            }]
          };
        }
    
        // Sort results by date (most recent first)
        const sortedResults = [...data.results].sort((a, b) => {
          // Parse dates from the 'datum' field (format: YYYY-MM-DDT00:00:00)
          const dateA = new Date(a.datum);
          const dateB = new Date(b.datum);
          return dateB.getTime() - dateA.getTime(); // Descending order (newest first)
        });
    
        // Calculate pagination
        const totalResults = sortedResults.length;
        const totalPages = Math.ceil(totalResults / validatedLimit);
        const startIndex = (validatedPage - 1) * validatedLimit;
        const endIndex = Math.min(startIndex + validatedLimit, totalResults);
        const paginatedResults = sortedResults.slice(startIndex, endIndex);
    
        // Create pagination info
        const paginationInfo = {
          query,
          category,
          totalResults,
          page: validatedPage,
          limit: validatedLimit,
          totalPages,
          hasNextPage: validatedPage < totalPages,
          hasPreviousPage: validatedPage > 1
        };
    
        // Create a summary version with only essential fields
        const formattedResults = paginatedResults.map(item => ({
          id: item.id,
          title: item.title,
          category: item.category,
          datum: item.datum,
          url: item.url
        }));
    
        // Return the paginated results with pagination info
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              pagination: paginationInfo,
              results: formattedResults
            }, null, 2)
          }]
        };
      } catch (error: any) {
        return {
          content: [{
            type: "text",
            text: `Error searching by category: ${error.message || 'Unknown error'}`
          }]
        };
      }
    }
  • src/index.ts:673-673 (registration)
    Registers the 'search_by_category' tool with the MCP server via mcp.tool(), providing name, long description, input schema, and handler reference.
    mcp.tool(
  • Zod schema defining input parameters: required query (string), category enum ['vragen','moties','alles'], optional page and limit (numbers).
      query: z.string().describe("Search term - any keyword, name, policy area, or quote you want to find in parliamentary records"),
      category: z.enum(["vragen", "moties", "alles"]).describe("Document category: 'vragen' for questions, 'moties' for motions, 'alles' for all document types"),
      page: z.number().optional().describe("Page number for paginated results (default: 1)"),
      limit: z.number().optional().describe("Maximum number of results to return per page (default: 20, max: 100)")
    },
  • apiService.search helper: performs HTTP POST to /search endpoint with FormData including 'soorten' for category filtering ('vragen', 'moties', 'alles'), handles complex error cases with retry on 500, returns {results, error?}.
    async search<T>(query: string, options: { twomonths?: boolean, soorten?: string } = {}): Promise<T> {
      try {
        // Don't sanitize quotes as they're important for exact phrase searches
        // Only sanitize backslashes which could cause issues
        const sanitizedQuery = query.replace(/\\/g, ' ').trim();
    
        // Use FormData for multipart/form-data
        const formData = new FormData();
        formData.append('q', sanitizedQuery);
        formData.append('twomonths', options.twomonths ? "true" : "false");
        formData.append('soorten', options.soorten || "alles"); // Default to 'alles'
    
        const res = await fetch(`${BASE_URL}/search`, {
          method: "POST",
          headers: {
            // Remove explicit Content-Type, fetch will set it for FormData
            'Accept': '*/*',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
            'Referer': `${BASE_URL}/search.html?q=${encodeURIComponent(sanitizedQuery)}&twomonths=${options.twomonths ? "true" : "false"}&soorten=${options.soorten || "alles"}`,
            'Origin': BASE_URL,
            'Host': 'berthub.eu',
            'Connection': 'keep-alive',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'sec-ch-ua': '"Chromium";v="135", "Not-A.Brand";v="8"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"'
          },
          body: formData, // Pass FormData object directly
          agent: ApiService.agent
        } as NodeRequestInit);
    
        if (!res.ok) {
          // If we get a 500 error, try to use a simplified query instead
          if (res.status === 500) {
            // API returned 500 error - let's investigate before simplifying
            let actualErrorContent = await res.text().catch(() => "Could not read error response body");
            let errorMessage = `Initial request failed with 500: ${res.statusText}. Response: ${actualErrorContent.substring(0, 200)}...`; // Limit length
    
            // Optional: Try simplifying only if the error content suggests it,
            // otherwise, maybe just report the initial 500 error.
            // For now, we'll still try simplifying but provide better final message.
    
            const simplifiedQuery = sanitizedQuery.split(/\\s+/)[0];
    
            // Initialize retry error message
            let retryErrorMessage = "Simplification/Retry not attempted or failed.";
    
            if (simplifiedQuery && simplifiedQuery !== sanitizedQuery) {
              // Retry with simplified query
    
              // Use FormData for the retry as well
              const simplifiedFormData = new FormData();
              simplifiedFormData.append('q', simplifiedQuery);
              simplifiedFormData.append('twomonths', options.twomonths ? "true" : "false");
              simplifiedFormData.append('soorten', options.soorten || "alles"); // Default to 'alles'
    
              try {
                const retryRes = await fetch(`${BASE_URL}/search`, {
                  method: "POST",
                  headers: { // Use same headers as initial, without Content-Type
                    'Accept': '*/*',
                    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
                    'Referer': `${BASE_URL}/search.html?q=${encodeURIComponent(simplifiedQuery)}&twomonths=${options.twomonths ? "true" : "false"}&soorten=${options.soorten || "alles"}`,
                    'Origin': BASE_URL,
                    'Host': 'berthub.eu',
                    'Connection': 'keep-alive',
                    'Sec-Fetch-Dest': 'empty',
                    'Sec-Fetch-Mode': 'cors',
                    'Sec-Fetch-Site': 'same-origin',
                    'sec-ch-ua': '"Chromium";v="135", "Not-A.Brand";v="8"',
                    'sec-ch-ua-mobile': '?0',
                    'sec-ch-ua-platform': '"macOS"'
                  },
                  body: simplifiedFormData, // Pass FormData object
                  agent: ApiService.agent
                } as NodeRequestInit);
    
                if (retryRes.ok) {
                  const retryText = await retryRes.text();
                  // Check for HTML in retry response too
                  if (retryText.trim().startsWith('<!DOCTYPE')) {
                     retryErrorMessage = "Retry attempt returned HTML instead of JSON.";
                  } else {
                    try {
                      const retryData = JSON.parse(retryText);
                      // Return only the parsed data, matching the expected type T
                      return retryData as T;
                    } catch (e) {
                      retryErrorMessage = `Retry succeeded but failed to parse JSON response: ${e instanceof Error ? e.message : String(e)}. Response: ${retryText.substring(0, 200)}...`;
                    }
                  }
                } else {
                  // Capture retry failure details
                  const retryErrorBody = await retryRes.text().catch(() => "Could not read retry error body");
                  retryErrorMessage = `Retry with simplified query failed with status: ${retryRes.status} ${retryRes.statusText}. Response: ${retryErrorBody.substring(0, 200)}...`;
                }
              } catch (retryFetchError) {
                   retryErrorMessage = `Fetch error during retry attempt: ${retryFetchError instanceof Error ? retryFetchError.message : String(retryFetchError)}`;
              }
            } else {
               retryErrorMessage = "Original query was already simple; retry not attempted.";
            }
    
            // If retry failed or wasn't applicable, return empty results with a more informative error
            // Combine initial error reason and retry outcome.
            const finalErrorMessage = `Search failed for query '${sanitizedQuery}'. Initial error: ${res.status} ${res.statusText}. ${retryErrorMessage}`;
            return {
              results: [],
              error: finalErrorMessage
            } as T;
          }
          // Handle non-500 errors
          const errorBody = await res.text().catch(() => "Could not read error response body");
          throw new Error(`API error: ${res.status} ${res.statusText}. Response: ${errorBody.substring(0, 200)}...`);
        }
    
        // Check if the response is HTML
        const text = await res.text();
        if (text.trim().startsWith('<!DOCTYPE')) {
          // The API returned HTML instead of JSON
          return { results: [] } as T;
        }
    
        // Parse JSON
        try {
          return JSON.parse(text) as T;
        } catch (error) {
          // Failed to parse JSON
          return { results: [] } as T;
        }
      } catch (error) {
        // Unexpected error in search
        throw error;
      }
    }
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes key behaviors: the response format ('structured JSON object with paginated results and metadata'), sorting behavior ('sorted by date with the most recent documents first'), and search syntax details. It doesn't mention rate limits or authentication requirements, but covers most operational aspects well.

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 with the core purpose in the first sentence. Each subsequent sentence adds valuable information about usage, parameters, search syntax, and sorting. While comprehensive, it maintains good flow without unnecessary repetition, though it could be slightly more concise in the syntax explanation section.

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

Completeness4/5

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

Given the tool's complexity (search with advanced syntax, pagination, filtering) and no output schema, the description provides substantial context: response format, sorting behavior, parameter usage, and search syntax. With no annotations, it adequately covers operational aspects, though it doesn't specify error conditions or exact response structure details that an output schema would provide.

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?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds some value by explaining the 'category' parameter options with examples and detailing the 'query' parameter's advanced syntax, but doesn't add significant meaning beyond what the schema provides. The baseline of 3 is appropriate given the comprehensive schema documentation.

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 tool's purpose: 'Performs a search specifically for documents of a certain category' with specific examples (questions, motions, letters). It distinguishes from siblings by mentioning 'more targeted search results than the general search provides' and lists sibling tools like 'search_tk' and 'search_tk_filtered' that likely provide broader searches.

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?

The description provides explicit guidance on when to use this tool: 'when a user wants to find documents of a specific type that match certain keywords' and 'when they need more targeted search results than the general search provides.' It distinguishes from alternatives by contrasting with 'general search' and the sibling tools suggest broader search capabilities.

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