Skip to main content

Developer blog

The privacy API, finished as an agent interface: search, execute, and a contract you can pin

The movahedi.ca Privacy API and MCP server now run 11 tools, a discovery search, a multi-operation execute plan, breach rules, resources, and prompts, on the Cloudflare Free plan, with a published tool contract hash.

ShareLinkedIn
APIMCPdevelopersprivacyagents
On this page
  1. It is live, and here is the receipt
  2. Why the old shape was not enough
  3. One call instead of three
  4. Breach rules, without a date the law does not give
  5. What stayed in the browser, and why
  6. Resources, prompts, and the hash
  7. Two failures on the way out
  8. How to use it in ten minutes

Key takeaways

  • Eleven MCP tools are live at https://movahedi.ca/mcp: the nine data tools, plus privacy_search_api for discovery and privacy_execute_plan for multi-step work.
  • A fixed eval set of 24 plain questions scores top-1 at 23 of 24 and top-3 at 24 of 24 for discovery. The one miss is the _ga question, which returns the cookies list first and classify second.
  • Breach rules are now a REST endpoint and an execute operation, with no invented dates: PIPEDA says as soon as feasible, Law 25 says with diligence, Alberta PIPA says without unreasonable delay.
  • Resources, prompts, and a tool contract hash are published. Pin the hash, recheck it before each call, and refuse on drift until a person reviews the diff.
  • The PII redactor, policy grader, PIA builder, scope checker, and cookie auditor paste mode stay in the browser. Nothing pasted leaves your machine for those tools.

It is live, and here is the receipt

tools/list returns 11 names. resources/list returns five URIs. prompts/list returns four prompts. Health reported 10 enforcement actions, 11 Law 25 checks, 202 cookie patterns, and 51 crawlers at data version 2026-10-25, when the glossary count stood at 320. The tool contract hash is 90f07f530204002c72b87a5775e2b4be9b9c3a4943708da1caeedba7359eb90e.

That list is the point. An agent that connects to this server should not have to guess which endpoint fits a question, or make five round trips to answer one that touches three datasets. It should search, run one plan, and verify the server it got is the server it approved.

Why the old shape was not enough

The server started as nine tools, one per dataset operation. That shape works when a person already knows the operation name. It fails in the normal case, which is a person asking a question in plain words.

“Has anyone been fined for facial recognition?” is not an operation name. It is a question that should land on enforcement search, not on a glossary definition. “What does PIPEDA mean?” should land on a glossary term, not on a case search. The old server left that mapping to the model, and models guess. Sometimes they guess well. Sometimes they call the wrong tool with confidence and build an answer on the wrong record.

So the first new tool is a search tool. privacy_search_api takes a plain question, up to 120 characters, and returns matching operations with method, path, summary, parameters, a use-when line, a do-not-use line, and one example REST query each. The scoring is deterministic and lexical: path words count most, then summary, then parameter names, then description, with a small synonym map at half weight. No model call, no embedding call, no vector database. The Free plan stays at zero dollars, and the answer is the same every time for the same question.

The eval set is fixed and committed in the repo. Twenty-four rows, at least two phrasings per operation, with traps. “What does PIPEDA mean” goes to a glossary term. “Has anyone been fined” goes to enforcement. One row still misses top-1 and lands second: the _ga question returns the cookies list first and classify second. The gate is top-3 at 100 percent and top-1 at 90 percent or better, and the run scores 23 of 24 on top-1. I would rather ship that number honestly than tune the scoring until a made-up set reads 24 of 24.

One call instead of three

The second new tool is privacy_execute_plan. Take the worked example the docs now carry: enforcement search for facial recognition with regulator cai, the Law 25 checklist for Automated decisions, and the glossary term privacy-by-design, in one call, with selected fields only. Before, that was three tool calls and three model round trips. Now it is one execute call, with a log per operation: id, path, status, milliseconds, and count.

The rules inside execute are narrow on purpose. At most 10 operations. GET only, and the plan format has no method field at all, so a plan cannot express a write. Only allowlisted path templates, resolved in process by the same query functions the REST API uses. No HTTP subrequest, no fetch, no new data path. A parameter can use $ref:<operation-id>:<field> to take a value from an earlier operation’s first result, and nothing else: no expressions, no arithmetic, no string building. A failing operation returns its error in place and does not abort later independent operations, unless it is a $ref source, in which case its dependents are marked skipped, not silently filled with a wrong value.

Every operation counts as one call against the daily quota. A 10-operation plan costs 10 calls, checked before the plan runs, so a plan larger than the remaining quota is refused before it starts, not halfway through. The flag behind both new tools is CODEMODE_ENABLED, default false in code, true in the launch configuration, and setting it back to false removes both tools from tools/list while the nine native tools keep working. That is the rollback. No data or quota state changes when you flip it.

Breach rules, without a date the law does not give

The breach deadline calculator on the site was never a numeric calculator, and the API version does not pretend otherwise. Canadian private-sector law has no fixed breach deadline. The new endpoint, GET /api/v1/breach-deadlines, returns regime rules, trigger, clock wording, who to notify, and retention, with a no_fixed_deadline flag on every regime.

PIPEDA: report to the Privacy Commissioner of Canada and notify each affected individual as soon as feasible after the organization determines the breach occurred, where it is reasonable to believe the breach creates a real risk of significant harm. Records of every breach, threshold or not, kept for 24 months.

Quebec Law 25: notify the Commission d’acces a l’information and each affected person with diligence where an incident presents a risk of serious injury, and keep a register of all confidentiality incidents, kept for five years from awareness, available to the Commission on request.

Alberta PIPA: notify the Information and Privacy Commissioner without unreasonable delay where a reasonable person would consider there is a real risk of significant harm. The Commissioner may then require notification of affected individuals.

The 72-hour figure belongs to the EU GDPR. The 7-day material-breach rule belongs to federal government institutions under Canada’s Privacy Act. Neither applies to a private-sector incident under PIPEDA or Law 25, and the endpoint says so by returning no date at all. If you want a date from this API, you will not get one, because the law does not give one.

Breach rules are available as REST and as an execute-plan operation, not as a twelfth native tool. Eleven tools already cross the practitioner-reported 8 to 10 threshold for a visible catalog, and the mitigations are in the docs: short descriptions with one use line and one do-not-use line, server instructions that send single lookups to native tools, multi-step work to execute, and unsure work to search first. No twelfth native tool without a profile plan or a search-first justification, and this build did not need one.

What stayed in the browser, and why

Not every site tool became an API tool. The test for that was simple: deterministic, public, and safe on public inputs, or it stays where it is.

The PII redactor stays client-only forever in this build. Its whole promise is that nothing leaves the browser, and a server version would break that promise in the act of offering it.

The policy grader, nutrition label scoring, PIA builder, and scope checker stay client-only too. They take pasted text or screening answers, and a server version would need a no-storage proof that this build does not claim: no storage path, no logging of input text, only counts and scores in logs, and a terms line stating pasted text is processed and not stored. The client versions already work, and they already keep the text in the browser. Shipping a server version on a weaker proof would have traded a working privacy promise for a wider surface.

The cookie auditor is gated the same way. It already has a server fetch at /api/cookie-audit with SSRF hardening, an 8-second timeout, and a 5 MB cap, plus a paste-HTML mode that runs entirely in the browser. MCP exposure would need a separate quota bucket from data lookups and tested blocks on private IP ranges, localhost, and metadata endpoints, and this build does not widen an arbitrary-fetch surface to get there. The browser auditor and the existing API route stay, and the paste mode stays the fallback.

That is the awkward tradeoff, stated plainly: the build is less complete on paper than a version that shipped everything as an API tool, and more honest about what it did not prove.

Resources, prompts, and the hash

Resources are application-controlled and read-only: the OpenAPI document, the terms, dataset metadata, crawler registry metadata, and the Law 25 checklist guide, each versioned by data version and capped at the same 50 KB output cap. Prompts are user-invoked and auditable, with declared arguments: a Law 25 gap check that returns a gap table template without filling gaps or claiming compliance, an enforcement briefing that searches and returns the top cases with official source URLs, a cookie audit summary that classifies up to 20 cookie names by pattern and says so, and a crawler check that returns operator, purpose, robots.txt behavior, and citation for one token.

Prompts evolve additively. A breaking change gets a new name, and the old name stays for 90 days or is removed only with a changelog line and a developers page note. The same rule covers tools: names are stable, and the contract hash is the enforcement mechanism. Compute it over tool names, descriptions, input schemas, and output schemas, publish it on the developers page and in the repo, and update it in the same commit as any schema change, or the drift test fails. Consumers should pin it at approval and recheck it before each call. Registry scans, for the same reason, only see what the server served the crawler at scan time. A live endpoint can vary by client, key, region, or hour, and the hash is how you notice.

Two failures on the way out

The first deploy failed in the build, in under five seconds, on a script I had not touched: the search index generator keeps a hardcoded description map for MCP tools and throws when a tool has no entry. The two new tools had no entries. The fix was two lines in that map, and the lesson is the one this whole build keeps teaching: every list of tools in the repo is a second source of truth until a test proves otherwise. The sync test now covers the route allowlist too, because the second failure was the same shape in a different place.

The second failure was quieter. The breach endpoint deployed, the execute operation worked, and the REST route returned a 404 page. The route dispatch existed, but the router’s allowlist of data routes did not include the new path, so the request fell through to static assets before it ever reached the handler. The fix was one line in the allowlist, plus a test that every non-templated OpenAPI path appears in that allowlist, plus an integration test that actually calls the breach endpoint. That test would have caught it before the deploy. It exists now because it did not exist then.

How to use it in ten minutes

Install from the developers page alone, for Claude, Cursor, Windsurf, VS Code, or ChatGPT connectors. List tools and confirm 11 names and the contract hash. Ask a plain question through privacy_search_api and call the operation it returns. Run the worked execute example and check the log. Read one resource and call one prompt. Then check the same record three ways: site search, REST, and MCP, for the same glossary term, and confirm the definition and data version match.

One thread stays open. Site chat now grounds data questions in the same query data before it answers, with record sources in the answer, but a live chat answer still needs a browser session to prove end to end, and the policy grader server version is still unproven rather than proven safe. Both are recorded as open, not as done. If you use the server and find a record that disagrees with its official source, that is a bug, and the developers page is where the contract lives to check it against.