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; } }

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