Tax MCP Agent
MCP server for VAT/tax calculation based on country and price, supporting integration with LLM clients like Claude and Ollama for natural language queries.
README
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:
- 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.
- Claude Desktop client - The MCP server is connected directly to Claude Desktop, which acts as a native MCP host.
- 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.
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 asllama3.1,mistral-nemo(12B), orqwen2.5tend 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
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.