All files / src/tools search.ts

96.42% Statements 27/28
93.54% Branches 29/31
100% Functions 4/4
96.29% Lines 26/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116            2x 5x 5x   2x 1x 1x     1x     1x 1x 1x           2x 14x                                                       8x 8x   8x 1x     7x                                 6x   8x 2x     4x 5x 5x             5x     4x                         1x          
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod/v4";
import type { KvkClient } from "../kvk-client.js";
import type { SearchResult } from "../types.js";
import { toTextResult, toErrorResult } from "../tool-result.js";
 
const formatSearchResultAddress = (result: SearchResult): string | null => {
  const adres = result.adres;
  if (!adres) return null;
 
  if (adres.binnenlandsAdres) {
    const a = adres.binnenlandsAdres;
    const street = [a.straatnaam, a.huisnummer !== undefined ? String(a.huisnummer) : null, a.huisletter]
      .filter(Boolean)
      .join(" ");
    return [street, a.postcode, a.plaats].filter(Boolean).join(", ");
  }
 
  Eif (adres.buitenlandsAdres) {
    const a = adres.buitenlandsAdres;
    return [a.straatHuisnummer, a.postcodeWoonplaats, a.land].filter(Boolean).join(", ");
  }
 
  return null;
};
 
export const registerSearchTools = (server: McpServer, client: KvkClient): void => {
  server.registerTool(
    "search_companies",
    {
      title: "Search KVK Business Register",
      description:
        "Search the KVK (Kamer van Koophandel) business register by company name, KVK number, RSIN, vestigingsnummer, street name, city, postcode, or type. " +
        "Returns a list of matching companies with basic information including KVK number, name, address, and active status. " +
        "At least one search parameter must be provided. Uses whole-text matching — partial names will not return results.",
      annotations: { readOnlyHint: true, openWorldHint: true },
 
      inputSchema: z.object({
        naam: z.string().optional().describe("Company or establishment name to search for. Uses whole-text matching."),
        kvkNummer: z.string().regex(/^\d{8}$/).optional().describe("KVK number (8 digits)."),
        rsin: z.string().regex(/^\d{9}$/).optional().describe("RSIN number (9 digits) — Legal Entities and Partnerships Identification Number."),
        vestigingsnummer: z.string().regex(/^\d{12}$/).optional().describe("Vestigingsnummer / location number (12 digits)."),
        straatnaam: z.string().optional().describe("Street name."),
        plaats: z.string().optional().describe("City name."),
        postcode: z.string().regex(/^[0-9]{4}[a-zA-Z]{2}$/).optional().describe("Postal code (e.g. 1234AB). Only in combination with huisnummer."),
        huisnummer: z.number().int().min(1).optional().describe("House number. Only in combination with postcode."),
        huisletter: z.string().max(1).optional().describe("House letter suffix. Only in combination with huisnummer."),
        postbusnummer: z.number().int().min(1).optional().describe("PO box number. Only in combination with postcode."),
        type: z.array(z.enum(["hoofdvestiging", "nevenvestiging", "rechtspersoon"])).optional().describe("Type of entity: hoofdvestiging (main location), nevenvestiging (branch), or rechtspersoon (legal entity)."),
        inclusiefInactieveRegistraties: z.boolean().optional().describe("Include inactive registrations. Default is false."),
        pagina: z.number().int().min(1).max(1000).default(1).describe("Page number (starts at 1, max 1000)."),
        resultatenPerPagina: z.number().int().min(1).max(100).default(10).describe("Results per page (max 100)."),
      }),
    },
    async ({ naam, kvkNummer, rsin, vestigingsnummer, straatnaam, plaats, postcode, huisnummer, huisletter, postbusnummer, type, inclusiefInactieveRegistraties, pagina, resultatenPerPagina }) => {
      try {
        const hasParam = naam ?? kvkNummer ?? rsin ?? vestigingsnummer ?? straatnaam ?? plaats ?? postcode ?? type;
 
        if (!hasParam && huisnummer === undefined && postbusnummer === undefined) {
          return toErrorResult(new Error("At least one search parameter must be provided (naam, kvkNummer, rsin, vestigingsnummer, straatnaam, plaats, postcode, or type)."));
        }
 
        const response = await client.search({
          naam,
          kvkNummer,
          rsin,
          vestigingsnummer,
          straatnaam,
          plaats,
          postcode,
          huisnummer,
          huisletter,
          postbusnummer,
          type,
          inclusiefInactieveRegistraties,
          pagina,
          resultatenPerPagina,
        });
 
        const results = response.resultaten ?? [];
 
        if (results.length === 0) {
          return toTextResult("No companies found matching the search criteria.");
        }
 
        const lines = results.map((r) => {
          const address = formatSearchResultAddress(r);
          const parts = [
            `KVK: ${r.kvkNummer}`,
            r.naam,
            address,
            r.type ? `(${r.type})` : null,
            r.actief === "Nee" ? "[INACTIEF]" : null,
          ].filter(Boolean);
          return `  - ${parts.join(", ")}`;
        });
 
        return toTextResult(
          [
            `Found ${response.totaal} result${response.totaal !== 1 ? "s" : ""} (page ${response.pagina}, ${response.resultatenPerPagina} per page)`,
            ...lines,
          ].join("\n"),
          {
            totaal: response.totaal,
            pagina: response.pagina,
            resultatenPerPagina: response.resultatenPerPagina,
            resultaten: results,
          } as Record<string, unknown>,
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
};