# Directory Structure
```
├── .github
│ └── workflows
│ └── CI.yml
├── .gitignore
├── .python-version
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│ └── mcp_server_calculator
│ ├── __init__.py
│ ├── __main__.py
│ └── calculator.py
├── tests
│ └── test_calculator.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.10
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Calculator MCP Server
A Model Context Protocol server for calculating. This server enables LLMs to use calculator for precise numerical calculations.
### Available Tools
- `calculate` - Calculates/evaluates the given expression.
- `expression` (string, required): Expression to be calculated
## Installation
### Using uv (recommended)
When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-calculator*.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### Using PIP
Alternatively you can install `mcp-server-calculator` via pip:
```bash
pip install mcp-server-calculator
```
After installation, you can run it as a script using:
```bash
python -m mcp_server_calculator
```
## Configuration
### Using uv (recommended)
Add this to your MCP client settings:
```json
"mcpServers": {
"calculator": {
"command": "uvx",
"args": ["mcp-server-calculator"]
}
}
```
### Using PIP
Alternatively add this to your MCP client settings:
```json
"mcpServers": {
"calculator": {
"command": "python",
"args": ["-m", "mcp_server_calculator"]
}
}
```
## License
mcp-server-calculator is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/__main__.py:
--------------------------------------------------------------------------------
```python
from mcp_server_calculator import main
main()
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/__init__.py:
--------------------------------------------------------------------------------
```python
from .calculator import main
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: .python-version
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: uv sync
- name: Build package
run: uv build
- name: Run tests
run: uv run python -m unittest discover -s tests
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev --no-editable
ADD . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable
FROM python:3.12-slim-bookworm
WORKDIR /app
COPY --from=uv /root/.local /root/.local
COPY --from=uv --chown=app:app /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENTRYPOINT ["mcp-server-calculator"]
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "mcp-server-calculator"
version = "0.2.0"
description = "A Model Context Protocol server for calculating"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "He Jie", email = "[email protected]" }]
keywords = ["mcp", "llm", "math", "calculator"]
license = { text = "MIT" }
urls = { Source = "https://github.com/githejie/mcp-server-calculator" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"mcp>=1.4.1",
]
[project.scripts]
mcp-server-calculator = "mcp_server_calculator:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/calculator.py:
--------------------------------------------------------------------------------
```python
import ast
import operator
import math
from mcp.server.fastmcp import FastMCP
def evaluate(expression: str) -> str:
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
}
allowed_names = {
k: getattr(math, k)
for k in dir(math)
if not k.startswith("__")
}
allowed_names.update({
"pi": math.pi,
"e": math.e,
})
def eval_expr(node):
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.Name):
if node.id in allowed_names:
return allowed_names[node.id]
raise ValueError(f"Unknown identifier: {node.id}")
elif isinstance(node, ast.BinOp):
left = eval_expr(node.left)
right = eval_expr(node.right)
if type(node.op) in allowed_operators:
return allowed_operators[type(node.op)](left, right)
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -eval_expr(node.operand)
elif isinstance(node, ast.Call):
func = eval_expr(node.func)
args = [eval_expr(arg) for arg in node.args]
return func(*args)
raise ValueError(f"Unsupported operation: {ast.dump(node)}")
expression = expression.replace('^', '**').replace('×', '*').replace('÷', '/')
parsed_expr = ast.parse(expression, mode='eval')
result = eval_expr(parsed_expr.body)
return str(result)
mcp = FastMCP("calculator")
@mcp.tool()
async def calculate(expression: str) -> str:
"""Calculates/evaluates the given expression."""
return evaluate(expression)
def main():
mcp.run()
```
--------------------------------------------------------------------------------
/tests/test_calculator.py:
--------------------------------------------------------------------------------
```python
import math
import unittest
from src.mcp_server_calculator.calculator import evaluate
class TestCalculator(unittest.TestCase):
def test_addition(self):
self.assertEqual(evaluate("1 + 1"), "2")
def test_subtraction(self):
self.assertEqual(evaluate("5 - 3"), "2")
def test_multiplication(self):
self.assertEqual(evaluate("2 * 3"), "6")
self.assertEqual(evaluate("2 × 3"), "6")
def test_division(self):
self.assertEqual(evaluate("8 / 2"), "4.0")
self.assertEqual(evaluate("8 ÷ 2"), "4.0")
def test_floor_division(self):
self.assertEqual(evaluate("7 // 2"), "3")
def test_modulus(self):
self.assertEqual(evaluate("10 % 3"), "1")
def test_power(self):
self.assertEqual(evaluate("2 ** 3"), "8")
self.assertEqual(evaluate("2 ^ 3"), "8")
def test_unary_minus(self):
self.assertEqual(evaluate("-5"), "-5")
def test_complex_expression(self):
self.assertEqual(evaluate("2 + 3 * (4 - 1) / 2 ** 2"), "4.25")
def test_parentheses_expression(self):
self.assertEqual(evaluate("(2 + 3) * 4"), "20")
def test_negative_numbers(self):
self.assertEqual(evaluate("-2 + 3"), "1")
self.assertEqual(evaluate("4 * -2"), "-8")
self.assertEqual(evaluate("-6 / 2"), "-3.0")
def test_floating_point_operations(self):
self.assertEqual(evaluate("0.5 + 0.25"), "0.75")
self.assertEqual(evaluate("2.5 * 2"), "5.0")
self.assertEqual(evaluate("5.0 / 2"), "2.5")
def test_large_numbers(self):
self.assertEqual(evaluate("123456789 * 987654321"), str(123456789 * 987654321))
def test_floating_point_precision(self):
self.assertAlmostEqual(float(evaluate("0.1 + 0.2")), 0.3, places=7)
def test_unsupported_operation(self):
with self.assertRaises(ValueError):
evaluate("unknown")
def test_empty_string(self):
with self.assertRaises(SyntaxError):
evaluate("")
def test_whitespace_string(self):
with self.assertRaises(SyntaxError):
evaluate(" ")
def test_invalid_expression(self):
with self.assertRaises(SyntaxError):
evaluate("2 +")
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
evaluate("1 / 0")
def test_floor_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
evaluate("1 // 0")
def test_modulus_by_zero(self):
with self.assertRaises(ZeroDivisionError):
evaluate("1 % 0")
def test_math_functions(self):
self.assertAlmostEqual(float(evaluate("sin(pi/2)")), math.sin(math.pi/2), places=7)
self.assertAlmostEqual(float(evaluate("cos(0)")), math.cos(0), places=7)
self.assertAlmostEqual(float(evaluate("sqrt(16)")), math.sqrt(16), places=7)
self.assertAlmostEqual(float(evaluate("log(e)")), math.log(math.e), places=7)
self.assertAlmostEqual(float(evaluate("exp(1)")), math.exp(1), places=7)
self.assertAlmostEqual(float(evaluate("tan(0)")), math.tan(0), places=7)
self.assertAlmostEqual(float(evaluate("fabs(-5)")), math.fabs(-5), places=7)
self.assertAlmostEqual(float(evaluate("factorial(5)")), math.factorial(5), places=7)
self.assertAlmostEqual(float(evaluate("pow(2, 5)")), math.pow(2, 5), places=7)
self.assertAlmostEqual(float(evaluate("degrees(pi)")), math.degrees(math.pi), places=7)
self.assertAlmostEqual(float(evaluate("radians(180)")), math.radians(180), places=7)
self.assertAlmostEqual(float(evaluate("pi")), math.pi, places=7)
self.assertAlmostEqual(float(evaluate("e")), math.e, places=7)
if __name__ == '__main__':
unittest.main()
```