Developer blog
How to use the MCP server
A practical guide to connecting clients to the movahedi.ca MCP server, calling its five privacy tools, and running the open-source data-mapping executor over stdio.
On this page
Key takeaways
- Point any modern MCP client at
https://movahedi.ca/mcpover Streamable HTTP. No account needed, 2,000 calls a day free.- Five read-only privacy tools: search and fetch enforcement actions, look up or browse the privacy glossary, pull the Quebec Law 25 checklist. These exist only on the hosted endpoint.
- A separate stdio MCP server in the open-source AI-data-map repo validates and tickets data-mapping recipes. Those recipe tools exist only there, not on the hosted endpoint.
The short version
movahedi.ca hosts an MCP server at https://movahedi.ca/mcp. It speaks Streamable HTTP: your client declares a protocol version on initialize, the server accepts it or errors, and every tool call returns structured output. Everything is read-only. You get 2,000 calls per day per IP with no key, or 10,000 a day with a free key from https://movahedi.ca/developers/keys/ (a Cloudflare Turnstile challenge stands between you and the key, nothing else).
The data behind the tools: verified enforcement actions from the CAI and the OPC, a plain-language privacy glossary, and the Quebec Law 25 compliance checklist. The API data regenerates from the site’s source data on every build; check https://movahedi.ca/api/v1/health for the current data version and counts, and the developers page at https://movahedi.ca/developers/ for the full reference.
Five tools, all read-only, all only on this endpoint:
privacy_search_enforcement_actions: find cases by keyword, regulator (caioropc), or yearprivacy_get_enforcement_case: full record for one case ID (use the id from search results, e.g.opc-2026-06-11-x-corp-and-x-ai-llc-xai)privacy_lookup_glossary_term: definition for one term slug (kebab-case, e.g.meaningful-consent)privacy_list_glossary_terms: browse or search the glossary, paginatedprivacy_law25_requirements: the Law 25 checklist, optionally by category
That matters for one reason: an agent with these tools works from the actual decisions and the actual definitions, not from whatever the model half-remembers about Canadian privacy law.
Connect a client
Seven ways in, each verified against the developers page. Pick yours. After any file edit, restart the client (or reload its MCP server list) and confirm the server shows as connected in settings before you go further. That restart step is the single most common reason these setups appear broken.
Claude Code CLI:
claude mcp add --transport http movahedi-privacy https://movahedi.ca/mcp
With an API key, add --header "Authorization: Bearer <key>".
Claude Desktop: open Settings, Connectors, Add custom connector. Name it Movahedi Privacy API, URL https://movahedi.ca/mcp. The developers page also offers a one-click install link for this flow.
On older builds without custom connectors, add this to claude_desktop_config.json instead (needs Node.js). Create the file if it does not exist: on macOS it lives at ~/Library/Application Support/Claude/claude_desktop_config.json, on Windows at %APPDATA%\Claude\claude_desktop_config.json.
{"mcpServers": {"movahedi-ca-privacy": {"command": "npx", "args": ["-y", "mcp-remote", "https://movahedi.ca/mcp"]}}}
Cursor (~/.cursor/mcp.json or .cursor/mcp.json):
{"mcpServers": {"movahedi-ca-privacy": {"url": "https://movahedi.ca/mcp"}}}
With a key, add "headers": {"Authorization": "Bearer <key>"} next to "url".
VS Code (.vscode/mcp.json):
{"servers": {"movahedi-ca-privacy": {"type": "http", "url": "https://movahedi.ca/mcp"}}}
Windsurf (~/.codeium/windsurf/mcp_config.json):
{"mcpServers": {"movahedi-ca-privacy": {"serverUrl": "https://movahedi.ca/mcp"}}}
If your client cannot set request headers, the key path is REST-only (next section covers that); the anonymous 2,000-a-day quota still applies over MCP.
Verify the connection with one cheap call before anything else: list glossary terms with privacy_list_glossary_terms and confirm structured results come back. Then try these prompts:
- “Search CAI enforcement actions involving video surveillance and summarize the outcomes.”
- “What is the privacy glossary definition of meaningful consent?”
- “List the Law 25 requirements on consent and data retention.”
A worked example: search, then fetch
Say you want to know how the CAI has treated video surveillance. In MCP wire format, the first call is a tools/call JSON-RPC request (shown here as the params object; your client builds the envelope):
{
"name": "privacy_search_enforcement_actions",
"arguments": {
"q": "video surveillance",
"regulator": "cai"
}
}
The response is a list of matches, each with an id, date, title, and short summary. Skim the summaries before you fetch; the search output is usually enough to tell whether you have the right matter. Pick the one that matches your question and feed its id into the second call:
{
"name": "privacy_get_enforcement_case",
"arguments": {
"id": "<id from the search results>"
}
}
The full record comes back: findings, the provisions cited, the outcome, any corrective orders. Hand that to your model and ask for the summary.
This two-step pattern, search then fetch, is the intended way to use the server. Search is cheap and broad. Fetch is where the detail lives.
If you only need definitions, skip the cases. privacy_list_glossary_terms takes a query and a letter filter and pages through the whole glossary; privacy_lookup_glossary_term takes a slug and returns the definition. For compliance work, privacy_law25_requirements takes an optional category and returns the checklist items.
Errors and validation
Tool calls fail for predictable reasons. Unknown case IDs and slugs return a not-found error that points you back at the search tools. Bad parameters, say a regulator value other than cai or opc, return an invalid-parameter error naming the parameter and its allowed values. Fix the argument, call again.
On the REST side the same failures map to HTTP codes: 400 invalid_parameter, 404 not_found, 405 method_not_allowed, 429 quota_exhausted, 500 internal_error. The full table is on the developers page at https://movahedi.ca/developers/.
Rate limits
Anonymous quota is 2,000 calls per IP per UTC day; with a key it is 10,000 per key per UTC day. When the quota is spent you get a 429 with a Retry-After header and a link to get a key. If you are scripting against the endpoint, back off before you hit it. (On REST responses, ETag/If-None-Match caching is supported for GETs; that is a REST feature, not an MCP one.)
The REST API, briefly
Everything the hosted MCP server exposes is also plain REST at https://movahedi.ca/api/v1:
GET /enforcement(filters:regulator,year,q,per_page)GET /enforcement/:idGET /glossary(filters:q,letter)GET /glossary/:termGET /law25/checklist(filter:category)GET /openapi.json(OpenAPI 3.1)
Responses wrap in a small envelope: data holds the payload, meta holds pagination, lang is "en" (the API serves English; the French tool pages are a separate front end). A one-liner:
curl "https://movahedi.ca/api/v1/enforcement?regulator=cai&per_page=5"
Honestly, if you are writing a script that fetches data once and renders it, use REST. MCP pays off when an agent needs to decide at runtime which lookups to run and in what order. Pick the interface that matches the caller.
The other MCP server: the data-mapping executor
Separate from the hosted privacy server, the open-source repo github.com/movahedi-ca/AI-data-map (MIT) ships its own MCP server: mcp/server.js, Node standard library only (no dependencies, any recent Node), MCP over stdio. To be explicit: the five privacy tools exist only on https://movahedi.ca/mcp; the three recipe tools below exist only in this stdio server. Do not go hunting for one in the other.
Quickstart:
git clone https://github.com/movahedi-ca/AI-data-map
node AI-data-map/mcp/server.js
The client sends one JSON-RPC 2.0 object per line on stdin and reads one per line on stdout. A minimal client entry:
{"command": "node", "args": ["/path/to/AI-data-map/mcp/server.js"]}
Handshake (from the repo’s README):
// client -> server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"demo","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
// server -> client
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"ai-data-map-mcp","version":"1.0.0"}}}
Three tools:
execute_mapping_workflow(recipe): validates a recipe JSON against the recipe schema and the action catalogue, returns a session ticketget_session: takes the ticket, returns the ticket plus any step events streamed backcancel_session: takes the ticket, cancels it
The ticket looks like this:
{
"ok": true,
"session_id": "s1-a3f9c21be04d",
"recipe_id": "cafe-nord-sample-01",
"item_count": 3,
"status": "ticketed"
}
A recipe is {recipe_id, schema_version, items[]}, and each item is {intent, params}. The authoring intents: add_node, add_collection_point, connect, set_field, set_retention, run_check, export, confirm_node, flag_for_review. The recovery intents (undo_last, skip_recipe_item, abort_session) belong to the executor itself and are rejected if you put them in a recipe. That is a deliberate split: the recipe describes what to build, not how to recover. Limits: 200 items max, a 64KB recipe cap, 4KB per item params. schema_version follows 1.0.x.
A minimal recipe (real params from the repo’s contract):
{
"recipe_id": "cafe-nord-sample-01",
"schema_version": "1.0.0",
"name": "Café Nord sample: checkout form to Shopify to Klaviyo",
"items": [
{"intent": "add_collection_point", "params": {"node_id": "n1", "label": "Online checkout form", "x": 80, "y": 80}},
{"intent": "add_node", "params": {"node_id": "n2", "label": "Shopify", "type": "system", "x": 310, "y": 80}},
{"intent": "connect", "params": {"a": "n1", "b": "n2", "cat": "contact"}}
]
}
Send a recipe with an unknown intent and the server rejects it before anything runs. A malformed recipe returns validation errors describing what failed, not a silent acceptance. The executor refuses to guess.
The same contract runs in the browser. The data-mapping assistant on movahedi.ca exposes window.execute_mapping_workflow(recipe, opts) (opts.onStep fires per executed step so the page can narrate and paint), which takes the same recipe shape and executes it in-page. Same validation, same limits, same session model. The difference is the transport: stdio for the repo server, a page seam for the assistant. The recipe executes locally in the page; no recipe data is sent to a server.
One honest limitation: sessions live in the page or the stdio process. Reload the page and the session is gone. Whether persistence is worth the complexity is still an open question.