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().

Available Tools

2 tools
get_upcoming_deadlinesA

Find South African tax deadlines falling within the next N days.

Use this to answer questions like "what SARS deadlines are coming up?" or "is anything due for VAT in the next month?". Dates come from a curated dataset of SARS filing-season notices and the underlying legislation, not from a live SARS feed.

Args: within_days: How many days ahead to look, counting from today. Both today and the final day are included. For example, 30 covers the next month. category: Optional filter. Must be one of: - provisional_tax: IRP6 provisional payments, provisional taxpayer and trust income tax returns - filing_season: annual individual filing season milestones (auto-assessments, opening dates, ITR12 deadlines) - vat: VAT201 return and payment deadlines - paye: EMP201 monthly PAYE/UIF/SDL declarations - emp501: employer annual and interim PAYE reconciliations Omit it to search across every category.

Returns: One line per matching deadline, sorted earliest first, in the form "YYYY-MM-DD - Title (sars.gov.za, verified YYYY-MM-DD)", each followed by an indented "Note:" line covering edge cases such as weekend or public-holiday shifts. Returns a plain sentence saying so when nothing falls in the window, and an error message listing the valid categories if the category argument is not recognised.

ParametersJSON Schema
NameRequiredDescriptionDefault
categoryNo
within_daysYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden and meets it: it discloses the data provenance (curated SARS notices, not a live feed), exact date-counting semantics, output format and sorting, edge-case notes for weekend/holiday shifts, and error behavior for invalid categories. This gives the agent a rich behavioral picture beyond a bare read-only implication.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well structured with clear Args and Returns sections and front-loads the purpose. It is slightly longer than necessary because the Returns section partly duplicates information an output schema would already provide, but the added format and edge-case details are still useful.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the absence of annotations, a 0% schema coverage, and an existing output schema, the description supplies everything needed to invoke the tool correctly: parameter semantics, data source, output formatting, sorting, and error cases. It is complete for correct use, with only sibling routing left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate fully and does: it defines within_days precisely (counting from today, inclusive endpoints, example of 30 days) and explains the category enum values in detail, including what omitting it does. No parameter meaning is left to the schema alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Find South African tax deadlines falling within the next N days.' The scope and data source are made clear, so an agent can tell it returns upcoming deadlines. However, it does not name or distinguish itself from the sibling tool list_deadlines, which the rubric requires for a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear usage context with example questions like 'what SARS deadlines are coming up?' and explains that omitting category searches all categories. It lacks explicit when-not guidance or a named alternative (e.g., list_deadlines), so it falls short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_deadlinesA

List every known South African tax deadline, optionally by category.

Use this when the question is not time-bounded - for example "what are the provisional tax dates for this cycle?" or "show me all SARS deadlines you know about". Results include dates that have already passed; use get_upcoming_deadlines instead when only future dates matter.

Args: category: Optional filter. Must be one of: - provisional_tax: IRP6 provisional payments, provisional taxpayer and trust income tax returns - filing_season: annual individual filing season milestones (auto-assessments, opening dates, ITR12 deadlines) - vat: VAT201 return and payment deadlines - paye: EMP201 monthly PAYE/UIF/SDL declarations - emp501: employer annual and interim PAYE reconciliations Omit it to list every category.

Returns: One line per deadline, sorted earliest first, in the form "YYYY-MM-DD - Title (sars.gov.za, verified YYYY-MM-DD)", each followed by an indented "Note:" line covering edge cases such as weekend or public-holiday shifts. Recurring obligations (VAT201, EMP201) appear as a single concrete upcoming example whose note explains the recurrence rule. Returns an error message listing the valid categories if the category argument is not recognised.

ParametersJSON Schema
NameRequiredDescriptionDefault
categoryNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses that past dates are included, that output is one line per deadline sorted earliest first, how recurrence is represented, and error behavior for an unrecognised category. It omits only non-behavioral operational details such as auth or rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads purpose and routing decision before the Args/Returns blocks, and every sentence earns its place - the category list and the returns/note format are all actionable. No filler or repetition of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-optional-param read tool with no annotations, the description covers use case, alternatives, all parameter values, output shape, edge cases (weekend/holiday shifts, recurrence), and error handling. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% and the enum values carry no schema descriptions, so the description compensates fully by enumerating all five categories with concrete semantics (e.g. 'provisional_tax: IRP6 provisional payments...', 'emp501: employer annual and interim PAYE reconciliations') and stating the omit-to-list-all behaviour. This is meaning well beyond the bare enum.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('List every known South African tax deadline') with an explicit scope qualifier (optionally by category), and names the sibling get_upcoming_deadlines as the contrasting tool. An agent can distinguish it from the sibling without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ('when the question is not time-bounded') with two concrete example queries, plus an explicit when-not ('use get_upcoming_deadlines instead when only future dates matter') and reinforces it by noting results include past dates. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.1.0
    • First observedget_upcoming_deadlines
    • First observedlist_deadlines

TDQS

A4.5/5.0

Scored across 2 tools

Disambiguation5/5

Both tools retrieve SARS deadlines but with distinctly scoped behavior: get_upcoming_deadlines is explicitly time-bounded to the next N days, while list_deadlines returns all known deadlines including past ones. The descriptions clearly state when to use each, eliminating misselection risk.

Naming Consistency5/5

Both names use snake_case with a verb_noun structure (get_upcoming_deadlines, list_deadlines). The retrieval verbs get and list are consistent and clearly indicate the action, with no mixed conventions.

Tool Count3/5

Two tools cover the server's narrow purpose of querying a curated deadline dataset, but this is on the thin side. The dimension considers 1–2 tools borderline, as a few more targeted queries (e.g., arbitrary date range, keyword search) could exist.

Completeness4/5

The surface covers core retrieval needs: all deadlines and upcoming deadlines, each with optional category filtering and detailed notes. However, it lacks a way to query an arbitrary date range or search by title, which agents could only work around by fetching and filtering the full list.

Maintenance

ActivityMaintained
ResponsivenessNo issues

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
    33 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
    411
    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.
    -