Skip to main content
Glama
yehyaabk

Tax MCP Agent

by yehyaabk

Tax MCP Agent

Author: ABOU KHECHFE Yehya

Introduction

This project is built around an MCP (Model Context Protocol) server that exposes a VAT/tax calculation tool. The server can be consumed by three different MCP clients, each demonstrating a different way of interacting with an MCP server:

  1. No-LLM client - A simple, text-only interactive client. The user manually selects an option from a menu and enters the required parameters (price, country) to calculate the tax. No AI/LLM is involved - this client exists purely to test the MCP server itself.

  2. Claude Desktop client - The MCP server is connected directly to Claude Desktop, which acts as a native MCP host.

  3. Ollama client - A local LLM (via Ollama) is connected to the MCP server. Unlike the first client, here the LLM is responsible for deciding whether a tool should be called and for extracting the correct arguments from natural language input.


Related MCP server: euinvoice-mcp

1. No-LLM Client (Test Client)

šŸ“ tax_client.py

This client does not use any LLM. It's a way to directly test the MCP server's capabilities (tools, resources, prompts) through a simple interactive menu, where the user manually provides all required parameters.

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import json

async def main():
    # Start the MCP server as a subprocess and connect to it via stdio
    server_params = StdioServerParameters(command="uv", args=["run", "tax_server.py"])
    async with stdio_client(server_params) as (reader, writer):
        async with ClientSession(reader, writer) as session:
            await session.initialize()  # Handshake with the MCP server

            while True:
                # Simple text menu for the user to choose an action
                print("=== MENU ===")
                print("1. Calculate VAT for a country and price")
                print("2. View VAT rates by country")
                print("3. Get a greeting message")
                print("4. Exit")
                choice = input("Select an option (1/2/3/4): ").strip()

                if choice == "4":
                    break

                elif choice == "1":
                    # Ask the user for the tool's required arguments manually
                    price = float(input("Enter a price: "))
                    country = input("Enter a country: ").strip()

                    # Call the MCP tool directly with the provided arguments
                    tool_response = await session.call_tool(
                        "calculate_tax", {"country": country, "price": price}
                    )
                    result = json.loads(tool_response.content[0].text)
                    print(f"VAT amount: {result['vat_amount']}")
                    print(f"Total price: {result['total_price']}")

                elif choice == "2":
                    # Read an MCP resource exposing all VAT rates
                    resource = await session.read_resource(uri='tax://vat/rates')
                    result = json.loads(resource.contents[0].text)
                    for k, v in result.items():
                        print(f"{k}: {v * 100} %")

                elif choice == "3":
                    # Fetch a pre-written MCP prompt, filled with user-provided values
                    name = input("Enter Your Name: ").strip()
                    country = input("Enter a country: ").strip()
                    prompt = await session.get_prompt(
                        "tax_greeting", {"user_name": name, "country": country}
                    )
                    print(prompt.messages[0].content.text)

if __name__ == "__main__":
    asyncio.run(main())

Testing the server with MCP Inspector

You can also test the MCP server directly, without any client, using the MCP Inspector:

uv run mcp dev tax_server.py

2. Claude Desktop Client

To connect the MCP server to Claude Desktop, edit Claude Desktop's configuration file (claude_desktop_config.json) and register the server as shown below:

{
  "mcpServers": {
    "TaxAssistant": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\\Path\\to\\MCP_Server",
        "run",
        "tax_server.py"
      ]
    }
  }
}

Once added, Claude Desktop automatically discovers the server's tools, resources, and prompts, and handles all tool selection, argument extraction, and execution internally - no extra client code required.


3. Ollama Client

šŸ“ ollama/tax_client_ollama.py

This client connects a locally running Ollama model to the MCP server. The key difference from the no-LLM client is that here, the LLM itself decides whether a tool should be called and extracts the corresponding arguments from natural language input.

Tool selection logic

async def select_tool(session: ClientSession, user_input: str, history: list[dict]) -> dict:
    # 1. Fetch and render the tool classification prompt using the MCP server
    prompt_result = await session.get_prompt("tool_classifier", {"user_input": user_input})
    rendered_prompt = prompt_result.messages[0].content.text

    # 2. Build the message list (system prompt + recent history + current input) and query Ollama
    messages = [{"role": "system", "content": rendered_prompt}]
    context = history[-4:]
    messages.extend(context)
    messages.append({"role": "user", "content": user_input})

    print("Processing your request with Ollama, this may take a moment...")
    ollama_response = ollama.chat(model="llama3", messages=messages)
    response_text = ollama_response.message.content

    # 3. Clean and parse the model's output into a Python dictionary
    clean = response_text.replace("```json", "").replace("```", "").strip()
    try:
        return json.loads(clean)
    except json.JSONDecodeError:
        return {"error": "Invalid JSON from model"}

The classifier prompt (MCP Prompt template)

This is the reusable, pre-written prompt template exposed by the MCP server. It's dynamically filled in with the list of supported countries, and instructs the LLM to output strict JSON indicating which tool to call (if any) and with which arguments:

Your role is to identify which tool should be used and extract its corresponding arguments.

- Available tools:
    - Name: "tax_calculate"
        - Role: Calculates the VAT/tax amount based on the price and country.
        - Arguments:
            - "price": float
            - "country": str (must be one of: {country_list})

- Output format:
    Respond with valid JSON only - no text, no explanation, no extra characters.
    There are only two possible output formats, both JSON:

    - 1st (when a matching tool is found):
        {
            "tool_name": "tool_name_here",
            "args": {
                "arg1": "value1",
                "arg2": "value2"
            }
        }

    - 2nd (when no matching tool is found):
        {
            "tool_name": null
        }

- Examples:

    - User: "What is the tax on 1000 in Saudi Arabia?"
      Output:
        {
            "tool_name": "tax_calculate",
            "args": {
                "price": 1000,
                "country": "Saudi Arabia"
            }
        }

    - User: "What's the weather like today?"
      Output:
        {
            "tool_name": null
        }

    - User: "What is the VAT on 500 dirhams?"
      Output:
        {
            "tool_name": "tax_calculate",
            "args": {
                "price": 500,
                "country": "UAE"
            }
        }

- Guidelines:
    - Always use the country names exactly as written in this list: [{country_list}]
    - If the country is not mentioned explicitly, try to infer it from context
      (e.g., currency terms like "riyals", "dirhams", "pounds", or location words
      like "Riyadh", "Dubai", "Cairo", etc.)
    - Always include both "price" and "country" in the response.
    - If either is missing or unclear, return: {"tool_name": null}
    - Try to infer the country from currency symbols if the country name is not
      clearly mentioned. For example:
        - SAR → Saudi Arabia
        - AED → UAE

Note: For better decision-making results, it's recommended to use a more powerful model than llama3 (8B parameters). Smaller models can struggle with multi-turn reasoning, strict JSON formatting, and context retention, which may lead to inconsistent or incorrect tool selection. Models such as llama3.1, mistral-nemo (12B), or qwen2.5 tend to give noticeably better results for this kind of structured tool-classification task.


Stack

This project uses uv as the Python package/project manager, chosen for its speed and simplicity over traditional pip.


How to Run

Run the no-LLM test client:

uv run tax_client.py

Run the Ollama client:

uv run ./ollama/tax_client_ollama.py

Project Structure

TAX-MCP-AGENT/
ā”œā”€ā”€ __pycache__/
ā”œā”€ā”€ .venv/
ā”œā”€ā”€ ollama/
│   ā”œā”€ā”€ tax_client_ollama.py    # MCP client using Ollama for tool selection
│   └── tax_server_ollama.py    # MCP server variant used by the Ollama client
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ .python-version
ā”œā”€ā”€ main.py
ā”œā”€ā”€ pyproject.toml
ā”œā”€ā”€ README.md
ā”œā”€ā”€ tax_client.py                # No-LLM interactive test client
ā”œā”€ā”€ tax_server.py                # Core MCP server exposing the tax tool/resources/prompts
└── uv.lock

Available Tools

1 tool
calculate_taxA

Calculate the VAT amount and total price for a given country and price.

Args: country: Country name (case-insensitive). price: Price excluding VAT. Must be positive.

Returns: A dict with country, price, vat_rate, vat_amount, and total_price. Returns a dict with an "error" key if the country is unknown or input is invalid.

ParametersJSON Schema
NameRequiredDescriptionDefault
priceYes
countryYes

TDQS

A4.8/5.0
Behavior5/5

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

With no annotations provided, the description fully discloses the tool's behavior: it calculates VAT and total price, returns a structured dict with specific keys, and returns an error dict for unknown countries or invalid input. This covers error handling and return format, which are key behavioral aspects.

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?

The description is concise and well-structured with clear sections for args and returns. It front-loads the primary purpose in the first sentence, and every subsequent line adds necessary detail without redundancy. The length is appropriate for the tool's complexity.

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 tool's simplicity, two parameters, and lack of output schema, the description is complete: it explains inputs, validation rules, return structure, and error behavior. There are no gaps that would leave an agent uncertain about how to invoke or interpret the tool.

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?

The schema only lists parameter names and types (country: string, price: number) with no descriptions. The description adds critical semantics: country is case-insensitive, price is excluding VAT and must be positive. Since schema coverage is 0%, the description fully compensates and enriches both parameters.

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?

The description clearly states the function's purpose: 'Calculate the VAT amount and total price for a given country and price.' This is a specific verb+resource statement. Although there are no sibling tools to differentiate, the description unambiguously defines what the tool does without tautology.

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?

The description specifies the required inputs ('for a given country and price') and notes that the price must be positive. It does not explicitly mention alternative tools, but since there are no sibling tools, this is not a deficiency. The usage context is clear and sufficient for a calculation tool.

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. Dates show when Glama detected each change.

  1. 1 tool updatev0.1.0
    • First observedcalculate_tax

TDQS

A4.6/5.0
Disambiguation5/5

Only one tool exists, so there is no possibility of ambiguity. The tool's purpose is clearly defined as VAT calculation.

Naming Consistency5/5

The single tool name 'calculate_tax' follows a clear verb_noun pattern, which is internally consistent and predictable.

Tool Count3/5

A single tool feels thin for an agent labeled 'Tax MCP Agent'. While it serves a focused purpose, the name suggests a broader scope that could use additional utilities like rate lookup or country validation.

Completeness4/5

The tool covers core VAT calculation for a given country and price, but lacks a way to discover supported countries or retrieve tax rates directly. These are minor gaps that agents can work around via the error messages.

Maintenance

ActivitySlowing
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    An open-source Model Context Protocol (MCP) server for the Open Telekom Cloud (OTC) Price Calculator API. Expose OTC pricing data to Claude and other LLM clients with full observability.
    GPL 3.0
  • A
    license
    Not graded
    quality
    F
    maintenance
    An MCP server for European invoicing rules. Query VAT rates, e-invoicing requirements, format specifications, and compliance rules for EU-27 + EEA countries.
    24
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server that exposes a price calculation tool (calculatePrice) for computing final prices with discount and tax logic, usable by Codex or any MCP client.
    -
  • A
    license
    A
    quality
    B
    maintenance
    Local MCP server for validating EU VAT numbers using the official VIES API. Enables AI to check VAT validity and optionally retrieve consultation numbers for legal proof.
    2
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yehyaabk/mcp_tax_agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server