search_tk_filtered
Refine search queries within Dutch parliamentary data by filtering results to specific content types such as documents, activities, or cases. Use advanced syntax for exact phrases, exclusions, alternatives, or proximity searches to obtain precise results.
Instructions
Performs a targeted search within a specific category of parliamentary data. Unlike the general search, this tool allows you to limit results to only documents, activities, or cases. Use this when you need more focused search results within a particular content type. Search syntax: Searching for 'Joe Biden' finds documents containing both 'Joe' and 'Biden' anywhere in the text. Searching for "Joe Biden" (with quotes) finds only documents where these words appear next to each other. Searching for 'Hubert NOT Bruls' finds documents containing 'Hubert' but not 'Bruls'. The capital letters in 'NOT' are important. You can also use 'OR' and 'NEAR()' operators.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| format | No | Format of the results: 'full' for complete data or 'summary' for a condensed version (default: 'summary') | |
| limit | No | Maximum number of results to return per page (default: 20, max: 100) | |
| page | No | Page number for paginated results (default: 1) | |
| query | Yes | Search term - any keyword, name, policy area, or quote you want to find in parliamentary records. Use quotes for exact phrases, 'NOT' to exclude terms, 'OR' for alternatives, and 'NEAR()' for proximity searches. | |
| type | Yes | Category filter: 'Document' for official papers, reports and letters; 'Activiteit' for debates and committee meetings; 'Zaak' for legislative cases and motions |
Implementation Reference
- src/index.ts:209-315 (registration)Registration of the 'search_tk_filtered' tool using mcp.tool(), including description, input schema with Zod, and inline handler function./** 7. Search filtered by type */ mcp.tool( "search_tk_filtered", "Performs a search within a specific category of parliamentary data, allowing results to be limited to only documents, activities, or cases. Returns paginated results sorted by date (most recent first). Search syntax supports: keyword searches ('Joe Biden' finds both terms), exact phrase searches (\"Joe Biden\" with quotes finds the exact phrase), exclusion ('Hubert NOT Bruls' excludes documents with 'Bruls'), and boolean operators ('OR', 'NEAR()'). Results can be returned in 'full' or 'summary' format.", { query: z.string().describe("Search term - any keyword, name, policy area, or quote you want to find in parliamentary records. Use quotes for exact phrases, 'NOT' to exclude terms, 'OR' for alternatives, and 'NEAR()' for proximity searches."), type: z .enum(["Document", "Activiteit", "Zaak"]) .describe("Category filter: 'Document' for official papers, reports and letters; 'Activiteit' for debates and committee meetings; 'Zaak' for legislative cases and motions"), 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)"), format: z.enum(["full", "summary"]).optional().describe("Format of the results: 'full' for complete data or 'summary' for a condensed version (default: 'summary')") }, async ({ query, type, page = 1, limit = 20, format = "summary" }) => { 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); // Check if there's an error message in the response if (data.error) { return { content: [ { type: "text", text: data.error }, { type: "text", text: "[]" } ] }; } // Filter the results by category const filtered = data.results ? data.results.filter((r: any) => r.category === type) : []; // If no results were found after filtering if (filtered.length === 0) { return { content: [{ type: "text", text: `No results found for query: ${query} with filter: ${type}. Try using different keywords or a different filter.` }] }; } // Sort filtered results by date (most recent first) const sortedResults = [...filtered].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, type, totalResults, page: validatedPage, limit: validatedLimit, totalPages, hasNextPage: validatedPage < totalPages, hasPreviousPage: validatedPage > 1 }; // Format the results based on the requested format let formattedResults; if (format === "summary") { // Create a summary version with only essential fields formattedResults = paginatedResults.map(item => ({ id: item.id, title: item.title, category: item.category, datum: item.datum, url: item.url })); } else { // Use the full data formattedResults = paginatedResults; } // 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 with filter: ${error.message || 'Unknown error'}` }] }; } } );
- src/index.ts:222-314 (handler)Handler function that performs the search using apiService.search, filters results by the specified 'type' category, sorts by date (newest first), paginates, formats as 'summary' or 'full', and returns JSON-structured text content.async ({ query, type, page = 1, limit = 20, format = "summary" }) => { 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); // Check if there's an error message in the response if (data.error) { return { content: [ { type: "text", text: data.error }, { type: "text", text: "[]" } ] }; } // Filter the results by category const filtered = data.results ? data.results.filter((r: any) => r.category === type) : []; // If no results were found after filtering if (filtered.length === 0) { return { content: [{ type: "text", text: `No results found for query: ${query} with filter: ${type}. Try using different keywords or a different filter.` }] }; } // Sort filtered results by date (most recent first) const sortedResults = [...filtered].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, type, totalResults, page: validatedPage, limit: validatedLimit, totalPages, hasNextPage: validatedPage < totalPages, hasPreviousPage: validatedPage > 1 }; // Format the results based on the requested format let formattedResults; if (format === "summary") { // Create a summary version with only essential fields formattedResults = paginatedResults.map(item => ({ id: item.id, title: item.title, category: item.category, datum: item.datum, url: item.url })); } else { // Use the full data formattedResults = paginatedResults; } // 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 with filter: ${error.message || 'Unknown error'}` }] }; } }
- src/index.ts:213-221 (schema)Input schema defined using Zod validators for query, type (enum), page, limit, format parameters.{ query: z.string().describe("Search term - any keyword, name, policy area, or quote you want to find in parliamentary records. Use quotes for exact phrases, 'NOT' to exclude terms, 'OR' for alternatives, and 'NEAR()' for proximity searches."), type: z .enum(["Document", "Activiteit", "Zaak"]) .describe("Category filter: 'Document' for official papers, reports and letters; 'Activiteit' for debates and committee meetings; 'Zaak' for legislative cases and motions"), 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)"), format: z.enum(["full", "summary"]).optional().describe("Format of the results: 'full' for complete data or 'summary' for a condensed version (default: 'summary')") },
- src/services/api.ts:151-287 (helper)apiService.search method called by the handler to perform the actual API search request, which the handler then filters client-side by category.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; } }