Skip to main content
Glama

sars-dates-mcp

An MCP (Model Context Protocol) server exposing a curated dataset of South African tax deadlines — provisional tax, individual filing season, VAT, PAYE, and employer reconciliations. Built to demonstrate tool-contract design, not just to answer a date question: typed schemas, error messages that teach the calling agent what to do next, and a response-size budget applied before it was ever needed.

Status: Complete for its current scope. Two tools, 41 tests, strict mypy, verified against MCP Inspector CLI.

~745 lines across server.py (172 — the two MCP tools and their formatting), deadlines.py (191 — the data layer, with no MCP dependency at all), and __init__.py, plus 41 tests covering both layers, including an explicit clock seam so date-dependent tests don't depend on when they're run.

Why this exists

MCP servers are harness components — tool interfaces with contracts, the same way a REST endpoint or a function signature is a contract. This one is deliberately small in scope (13 curated deadlines) so the engineering discipline is the point: schema design, error handling, response budgeting, and a documented security posture, not dataset size.

Related MCP server: OpenAccountants

The two tools

get_upcoming_deadlines(within_days, category=None) — deadlines falling within the next N days from today. Use when the question is time-bounded: "what's due in the next 30 days?"

list_deadlines(category=None) — every known deadline, optionally filtered by category, including ones already past. Use when the question isn't time-bounded: "what are all the VAT dates you know about?"

Both accept an optional category, typed as a Literal rather than a bare string:

Category: TypeAlias = Literal[
    "provisional_tax", "filing_season", "vat", "paye", "emp501"
]

This isn't cosmetic. FastMCP turns a Literal into an enum in the tool's JSON schema, so a calling agent sees the valid values up front instead of guessing at a string and finding out it was wrong. A test asserts this Literal and the runtime validation list (VALID_CATEGORIES) can't drift apart.

Errors teach, they don't just fail. An invalid category doesn't return a bare exception — it returns a message listing the valid categories and how to omit the filter entirely. That's the same principle used in bare-agent's permission-denial messages: a tool response is part of the prompt, so it should steer the next action, not just report failure.

Data model

Each deadline is a Deadline: id, category, title, date, recurrence, notes, source_url, source_detail, last_verified. The dataset lives in data/deadlines.json, loaded and strictly validated at startup — a missing or wrong-typed field fails loudly (ValueError) rather than silently producing a malformed Deadline.

Every deadline carries its own provenance: citation() renders it as (sars.gov.za, verified YYYY-MM-DD), appended to every formatted result. The data is a curated, hand-maintained dataset, not a live SARS feed — it reflects filing-season notices and legislation as read and recorded on the last_verified date, not a real-time source. Both tool docstrings say this explicitly, because "hallucinated dates presented as authoritative" is the single worst failure mode a tool like this could have.

Response-size discipline

Added ahead of MCP server #2, so the pair demonstrates a standard rather than two one-off designs. _format_deadlines — the formatter shared by both tools — caps output at 50 entries. If a result set is ever larger, the response is truncated with a line stating how many were omitted and how to narrow the query using the category filter, rather than either silently truncating or growing unbounded.

At the current dataset size (13 deadlines total) this cap never fires — the honest state of things is "no query has ever hit this limit." It exists on principle: Anthropic's tool-writing guidance treats response-size budgeting as a first-class design concern, not something to retrofit once a dataset happens to grow large enough to hurt.

Security posture

  • Read-only, no state mutation, no external network calls. Both tools query an in-memory list built once from a local JSON file at startup. There is no code path in this server that writes anything, calls out to a network service, or executes a subprocess. This is the smallest possible attack surface for an MCP server and it's worth stating plainly rather than leaving it implicit.

  • No user-supplied string reaches a file path, a query, or a shell. The only argument that varies per call (category) is validated against a closed five-value enum before it's used for anything. There's no way for a calling agent's input to be interpreted as anything other than one of those five literal strings.

  • Tool descriptions contain no dynamic content. Both docstrings are static, hand-written text, not assembled from the dataset or any external source. Since tool descriptions are loaded into the calling agent's context (OWASP MCP Top 10 — tool poisoning), this closes off the most direct version of that risk for this server: there's nothing in the dataset that could get concatenated into a description an agent trusts.

  • A dependency was found unpinned during this session — dependencies = ["mcp"], no version constraint. A routine pip install -e . pulled mcp 2.1.1, which renamed the FastMCP API this server is built on (MCPServer in 2.x) and broke the import outright. Fixed by pinning mcp<2 in pyproject.toml. Included here rather than left in a commit message: an unpinned dependency in a published MCP server is exactly the agentic-supply-chain risk OWASP ASI04 names — anyone installing this server inherits whatever version resolution pip does that day, and this incident is a concrete example of that going wrong on the very machine that built it.

  • What this doesn't cover: no rate limiting beyond what an MCP client enforces; no automated check that data/deadlines.json hasn't been tampered with between commits (it's a plain tracked file, verified by normal git history rather than a checksum).

Known limitations

Stated honestly, not hidden — a passing test suite proves the code does what it says, nothing about whether the underlying claim is true.

  • Citations are source-supported, not independently verified at subsection level. During this dataset's construction, some legal citations were initially written at a more specific level (exact subsection references) than the source material actually supported, and were caught and softened to the general provision the source could substantiate. Every citation in the dataset reflects that correction, but it means "verified" here means "the date and provision are supported by the cited source," not "independently checked against the exact subsection of the Act." Worth stating precisely rather than letting "verified" imply more rigor than was actually applied.

  • The dataset requires manual updates. There's no mechanism that detects when a SARS deadline changes or a new filing season is announced. last_verified tells you when a human last checked, not that the date is still current today.

  • Small dataset, so the response-size cap is currently unexercised. The 50-result truncation is tested via the code path, not via a real query that actually triggers it — worth flagging rather than implying it's been proven under load. Live verification via the MCP Inspector CLI confirmed the cap doesn't fire early against the current 13-entry dataset — it does not confirm the cap fires correctly when a result set actually exceeds 50, since nothing in the current dataset does.

  • The custom "teaching" error for an invalid category is unreachable through the live MCP protocol. Because category is typed as a Literal, FastMCP's own schema validation rejects an invalid value with a Pydantic error before the tool function body — including its custom error message — ever runs. Confirmed live via the MCP Inspector CLI: calling either tool with an invalid category returns FastMCP's own literal_error message (which does list the valid enum values), not the app's own "Errors teach, they don't just fail" text described above. That custom error-handling code still exists and is still covered by unit tests that call the function directly, bypassing protocol-level schema validation — but a real MCP client can never actually reach it for this field. Worth stating plainly rather than letting the tools section imply the app's own error path is what a live caller sees.

Development

pip install -e ".[dev]"
pytest
mypy src

Requires Python 3.11+. mcp<2 is a hard pin — see Security posture above for why.

Verification

Protocol wiring verified against the MCP Inspector CLI. Full test suite: 41 tests across the data layer (test_deadlines.py) and the MCP tool layer (test_server.py), the latter using an explicit clock seam so get_upcoming_deadlines can be tested against fixed dates rather than datetime.now().

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Provides access to official Spanish fiscal data and tools based on AEAT and BOE sources, covering income tax, VAT, and regional deductions. It enables AI assistants to answer tax-related queries and verify filing deadlines using verified information.
    10
    11 npm
    13
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Open-source, accountant-verified tax computation skills for AI agents. 261+ skills across 172+ jurisdictions covering income tax, VAT/GST, payroll, corporate tax, crypto, and cross-border planning. Every skill is verified section-by-section by licensed CPAs and chartered accountants. 3 tools (list_skills, get_skill, get_skill_sections) and 1 prompt (skill-review).
    3
    391
    AGPL 3.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables natural-language queries about US federal and New York privacy law obligations, returning citation-anchored answers with computed deadlines, usable from Claude Desktop or the command line.
    1
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides AI agents with verified Vietnamese tax and legal retrieval capabilities, including searching legal documents, fetching document details, evaluating effective tax rules at a specified date, and tracking recent legal updates.
    -