search
Find Canadian cases, legislation, and commentary by searching with specific legal keywords. Results include ranked citations and titles. Refine by jurisdiction. Confirm sources with provided CanLII citations and URLs.
Instructions
Search CanLII for cases, legislation, and commentary by keyword. This is the primary entry point for legal research. Returns case citations and titles ranked by relevance — does NOT include keywords, dates, or URLs. Call get_case_metadata on promising results to get full details before citing a case. Search is keyword-based, not semantic — use specific legal terms rather than natural language. Common terms: 'best interests of the child', 'material change in circumstances', 'standard of review', 'duty to consult', 'reasonable expectation of privacy'. Include jurisdiction to narrow results (e.g., 'Ontario', 'Alberta'). Date filters are NOT supported on search. Always cite the CanLII citation and provide the case URL so the user can verify the source.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Full-text search query. Can include case names, legal concepts, legislation references, or keywords. | |
| language | No | Language: 'en' for English (default), 'fr' for French | en |
| resultCount | No | Number of results to return (1-100, default 10). Keep low for AI context efficiency. | |
| offset | No | Pagination offset (default 0). Use to page through results. |
Implementation Reference
- src/index.ts:116-164 (registration)Registration of the 'search' tool via server.tool(...) including its name, description, input schemas (Zod), and handler callback.
// ============================================================ // TOOL: search // ============================================================ server.tool( "search", "Search CanLII for cases, legislation, and commentary by keyword. This is the primary entry point for legal research. " + "Returns case citations and titles ranked by relevance — does NOT include keywords, dates, or URLs. " + "Call get_case_metadata on promising results to get full details before citing a case. " + "Search is keyword-based, not semantic — use specific legal terms rather than natural language. " + "Common terms: 'best interests of the child', 'material change in circumstances', 'standard of review', " + "'duty to consult', 'reasonable expectation of privacy'. Include jurisdiction to narrow results (e.g., 'Ontario', 'Alberta'). " + "Date filters are NOT supported on search. Always cite the CanLII citation and provide the case URL so the user can verify the source.", { query: z.string() .describe("Full-text search query. Can include case names, legal concepts, legislation references, or keywords."), language: z.enum(["en", "fr"]).default("en") .describe("Language: 'en' for English (default), 'fr' for French"), resultCount: z.number().min(1).max(100).default(10) .describe("Number of results to return (1-100, default 10). Keep low for AI context efficiency."), offset: z.number().min(0).default(0) .describe("Pagination offset (default 0). Use to page through results."), }, async ({ query, language, resultCount, offset }) => { try { const params = new URLSearchParams({ api_key: apiKey, fullText: query, resultCount: resultCount.toString(), offset: offset.toString(), }); const response = await apiFetch( `https://api.canlii.org/v1/search/${language}/?${params.toString()}` ); if (!response.ok) { return errorResponse(`Error: Search failed (${response.status}). The search endpoint may not be available for your API key.`); } const data = await response.json(); const parsed = SearchResponseSchema.parse(data); return jsonResponse(parsed); } catch (error) { return errorResponse( `Error: ${error instanceof Error ? error.message : "Unknown error"}` ); } } ); - src/index.ts:138-163 (handler)The handler function for the 'search' tool. Receives query, language, resultCount, offset; builds URLSearchParams; calls the CanLII API via apiFetch; parses response with SearchResponseSchema; returns JSON response or error.
async ({ query, language, resultCount, offset }) => { try { const params = new URLSearchParams({ api_key: apiKey, fullText: query, resultCount: resultCount.toString(), offset: offset.toString(), }); const response = await apiFetch( `https://api.canlii.org/v1/search/${language}/?${params.toString()}` ); if (!response.ok) { return errorResponse(`Error: Search failed (${response.status}). The search endpoint may not be available for your API key.`); } const data = await response.json(); const parsed = SearchResponseSchema.parse(data); return jsonResponse(parsed); } catch (error) { return errorResponse( `Error: ${error instanceof Error ? error.message : "Unknown error"}` ); } } - src/schema.ts:129-132 (schema)SearchResponseSchema - the Zod schema used to validate the API response for search results (resultCount + array of search results).
export const SearchResponseSchema = z.object({ resultCount: z.number(), results: z.array(SearchResultSchema), }).passthrough(); - src/schema.ts:122-127 (schema)SearchResultSchema - union of SearchCaseResultSchema, SearchCommentaryResultSchema, SearchLegislationResultSchema, and SearchUnknownResultSchema for parsing heterogeneous search results.
export const SearchResultSchema = z.union([ SearchCaseResultSchema, SearchCommentaryResultSchema, SearchLegislationResultSchema, SearchUnknownResultSchema, ]); - src/index.ts:32-53 (helper)apiFetch helper - the rate-limited fetch wrapper used by the search handler to make API calls (2 req/sec, 1 concurrent, 5000/day limit).
async function apiFetch(url: string): Promise<Response> { return new Promise((resolve, reject) => { requestQueue = requestQueue.then(async () => { const today = new Date().toDateString(); if (today !== dailyResetDate) { dailyCount = 0; dailyResetDate = today; } if (dailyCount >= 5000) { throw new Error("Daily API limit reached (5,000 queries). Try again tomorrow."); } const now = Date.now(); const elapsed = now - lastRequestTime; if (elapsed < MIN_INTERVAL_MS) { await new Promise(r => setTimeout(r, MIN_INTERVAL_MS - elapsed)); } lastRequestTime = Date.now(); dailyCount++; return fetch(url); }).then(resolve, reject); }); }