# 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:
--------------------------------------------------------------------------------
```
1 | 3.10
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Calculator MCP Server
2 |
3 | A Model Context Protocol server for calculating. This server enables LLMs to use calculator for precise numerical calculations.
4 |
5 | ### Available Tools
6 |
7 | - `calculate` - Calculates/evaluates the given expression.
8 | - `expression` (string, required): Expression to be calculated
9 |
10 | ## Installation
11 |
12 | ### Using uv (recommended)
13 |
14 | When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
15 | use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-calculator*.
16 |
17 | ```bash
18 | curl -LsSf https://astral.sh/uv/install.sh | sh
19 | ```
20 |
21 | ### Using PIP
22 |
23 | Alternatively you can install `mcp-server-calculator` via pip:
24 |
25 | ```bash
26 | pip install mcp-server-calculator
27 | ```
28 |
29 | After installation, you can run it as a script using:
30 |
31 | ```bash
32 | python -m mcp_server_calculator
33 | ```
34 |
35 | ## Configuration
36 |
37 | ### Using uv (recommended)
38 |
39 | Add this to your MCP client settings:
40 |
41 | ```json
42 | "mcpServers": {
43 | "calculator": {
44 | "command": "uvx",
45 | "args": ["mcp-server-calculator"]
46 | }
47 | }
48 | ```
49 |
50 | ### Using PIP
51 |
52 | Alternatively add this to your MCP client settings:
53 |
54 | ```json
55 | "mcpServers": {
56 | "calculator": {
57 | "command": "python",
58 | "args": ["-m", "mcp_server_calculator"]
59 | }
60 | }
61 | ```
62 |
63 | ## License
64 |
65 | 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.
66 |
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/__main__.py:
--------------------------------------------------------------------------------
```python
1 | from mcp_server_calculator import main
2 |
3 | main()
4 |
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .calculator import main
2 |
3 | if __name__ == "__main__":
4 | main()
5 |
```
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up Python
20 | uses: actions/setup-python@v5
21 | with:
22 | python-version-file: .python-version
23 |
24 | - name: Install uv
25 | uses: astral-sh/setup-uv@v3
26 |
27 | - name: Install dependencies
28 | run: uv sync
29 |
30 | - name: Build package
31 | run: uv build
32 |
33 | - name: Run tests
34 | run: uv run python -m unittest discover -s tests
35 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
2 |
3 | WORKDIR /app
4 |
5 | ENV UV_COMPILE_BYTECODE=1
6 |
7 | ENV UV_LINK_MODE=copy
8 |
9 | RUN --mount=type=cache,target=/root/.cache/uv \
10 | --mount=type=bind,source=uv.lock,target=uv.lock \
11 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
12 | uv sync --frozen --no-install-project --no-dev --no-editable
13 |
14 | ADD . /app
15 | RUN --mount=type=cache,target=/root/.cache/uv \
16 | uv sync --frozen --no-dev --no-editable
17 |
18 | FROM python:3.12-slim-bookworm
19 |
20 | WORKDIR /app
21 |
22 | COPY --from=uv /root/.local /root/.local
23 | COPY --from=uv --chown=app:app /app/.venv /app/.venv
24 |
25 | ENV PATH="/app/.venv/bin:$PATH"
26 |
27 | ENTRYPOINT ["mcp-server-calculator"]
28 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "mcp-server-calculator"
3 | version = "0.2.0"
4 | description = "A Model Context Protocol server for calculating"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | authors = [{ name = "He Jie", email = "[email protected]" }]
8 | keywords = ["mcp", "llm", "math", "calculator"]
9 | license = { text = "MIT" }
10 | urls = { Source = "https://github.com/githejie/mcp-server-calculator" }
11 | classifiers = [
12 | "Development Status :: 4 - Beta",
13 | "Intended Audience :: Developers",
14 | "License :: OSI Approved :: MIT License",
15 | "Programming Language :: Python :: 3",
16 | "Programming Language :: Python :: 3.10",
17 | ]
18 | dependencies = [
19 | "mcp>=1.4.1",
20 | ]
21 |
22 | [project.scripts]
23 | mcp-server-calculator = "mcp_server_calculator:main"
24 |
25 | [build-system]
26 | requires = ["hatchling"]
27 | build-backend = "hatchling.build"
28 |
```
--------------------------------------------------------------------------------
/src/mcp_server_calculator/calculator.py:
--------------------------------------------------------------------------------
```python
1 | import ast
2 | import operator
3 | import math
4 | from mcp.server.fastmcp import FastMCP
5 |
6 | def evaluate(expression: str) -> str:
7 | allowed_operators = {
8 | ast.Add: operator.add,
9 | ast.Sub: operator.sub,
10 | ast.Mult: operator.mul,
11 | ast.Div: operator.truediv,
12 | ast.FloorDiv: operator.floordiv,
13 | ast.Mod: operator.mod,
14 | ast.Pow: operator.pow,
15 | ast.USub: operator.neg,
16 | }
17 | allowed_names = {
18 | k: getattr(math, k)
19 | for k in dir(math)
20 | if not k.startswith("__")
21 | }
22 | allowed_names.update({
23 | "pi": math.pi,
24 | "e": math.e,
25 | })
26 |
27 | def eval_expr(node):
28 | if isinstance(node, ast.Constant):
29 | return node.value
30 | elif isinstance(node, ast.Name):
31 | if node.id in allowed_names:
32 | return allowed_names[node.id]
33 | raise ValueError(f"Unknown identifier: {node.id}")
34 | elif isinstance(node, ast.BinOp):
35 | left = eval_expr(node.left)
36 | right = eval_expr(node.right)
37 | if type(node.op) in allowed_operators:
38 | return allowed_operators[type(node.op)](left, right)
39 | elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
40 | return -eval_expr(node.operand)
41 | elif isinstance(node, ast.Call):
42 | func = eval_expr(node.func)
43 | args = [eval_expr(arg) for arg in node.args]
44 | return func(*args)
45 | raise ValueError(f"Unsupported operation: {ast.dump(node)}")
46 |
47 | expression = expression.replace('^', '**').replace('×', '*').replace('÷', '/')
48 | parsed_expr = ast.parse(expression, mode='eval')
49 | result = eval_expr(parsed_expr.body)
50 | return str(result)
51 |
52 | mcp = FastMCP("calculator")
53 |
54 | @mcp.tool()
55 | async def calculate(expression: str) -> str:
56 | """Calculates/evaluates the given expression."""
57 | return evaluate(expression)
58 |
59 | def main():
60 | mcp.run()
61 |
```
--------------------------------------------------------------------------------
/tests/test_calculator.py:
--------------------------------------------------------------------------------
```python
1 | import math
2 | import unittest
3 | from src.mcp_server_calculator.calculator import evaluate
4 |
5 | class TestCalculator(unittest.TestCase):
6 | def test_addition(self):
7 | self.assertEqual(evaluate("1 + 1"), "2")
8 |
9 | def test_subtraction(self):
10 | self.assertEqual(evaluate("5 - 3"), "2")
11 |
12 | def test_multiplication(self):
13 | self.assertEqual(evaluate("2 * 3"), "6")
14 | self.assertEqual(evaluate("2 × 3"), "6")
15 |
16 | def test_division(self):
17 | self.assertEqual(evaluate("8 / 2"), "4.0")
18 | self.assertEqual(evaluate("8 ÷ 2"), "4.0")
19 |
20 | def test_floor_division(self):
21 | self.assertEqual(evaluate("7 // 2"), "3")
22 |
23 | def test_modulus(self):
24 | self.assertEqual(evaluate("10 % 3"), "1")
25 |
26 | def test_power(self):
27 | self.assertEqual(evaluate("2 ** 3"), "8")
28 | self.assertEqual(evaluate("2 ^ 3"), "8")
29 |
30 | def test_unary_minus(self):
31 | self.assertEqual(evaluate("-5"), "-5")
32 |
33 | def test_complex_expression(self):
34 | self.assertEqual(evaluate("2 + 3 * (4 - 1) / 2 ** 2"), "4.25")
35 |
36 | def test_parentheses_expression(self):
37 | self.assertEqual(evaluate("(2 + 3) * 4"), "20")
38 |
39 | def test_negative_numbers(self):
40 | self.assertEqual(evaluate("-2 + 3"), "1")
41 | self.assertEqual(evaluate("4 * -2"), "-8")
42 | self.assertEqual(evaluate("-6 / 2"), "-3.0")
43 |
44 | def test_floating_point_operations(self):
45 | self.assertEqual(evaluate("0.5 + 0.25"), "0.75")
46 | self.assertEqual(evaluate("2.5 * 2"), "5.0")
47 | self.assertEqual(evaluate("5.0 / 2"), "2.5")
48 |
49 | def test_large_numbers(self):
50 | self.assertEqual(evaluate("123456789 * 987654321"), str(123456789 * 987654321))
51 |
52 | def test_floating_point_precision(self):
53 | self.assertAlmostEqual(float(evaluate("0.1 + 0.2")), 0.3, places=7)
54 |
55 | def test_unsupported_operation(self):
56 | with self.assertRaises(ValueError):
57 | evaluate("unknown")
58 |
59 | def test_empty_string(self):
60 | with self.assertRaises(SyntaxError):
61 | evaluate("")
62 |
63 | def test_whitespace_string(self):
64 | with self.assertRaises(SyntaxError):
65 | evaluate(" ")
66 |
67 | def test_invalid_expression(self):
68 | with self.assertRaises(SyntaxError):
69 | evaluate("2 +")
70 |
71 | def test_division_by_zero(self):
72 | with self.assertRaises(ZeroDivisionError):
73 | evaluate("1 / 0")
74 |
75 | def test_floor_division_by_zero(self):
76 | with self.assertRaises(ZeroDivisionError):
77 | evaluate("1 // 0")
78 |
79 | def test_modulus_by_zero(self):
80 | with self.assertRaises(ZeroDivisionError):
81 | evaluate("1 % 0")
82 |
83 | def test_math_functions(self):
84 | self.assertAlmostEqual(float(evaluate("sin(pi/2)")), math.sin(math.pi/2), places=7)
85 | self.assertAlmostEqual(float(evaluate("cos(0)")), math.cos(0), places=7)
86 | self.assertAlmostEqual(float(evaluate("sqrt(16)")), math.sqrt(16), places=7)
87 | self.assertAlmostEqual(float(evaluate("log(e)")), math.log(math.e), places=7)
88 | self.assertAlmostEqual(float(evaluate("exp(1)")), math.exp(1), places=7)
89 | self.assertAlmostEqual(float(evaluate("tan(0)")), math.tan(0), places=7)
90 | self.assertAlmostEqual(float(evaluate("fabs(-5)")), math.fabs(-5), places=7)
91 | self.assertAlmostEqual(float(evaluate("factorial(5)")), math.factorial(5), places=7)
92 | self.assertAlmostEqual(float(evaluate("pow(2, 5)")), math.pow(2, 5), places=7)
93 | self.assertAlmostEqual(float(evaluate("degrees(pi)")), math.degrees(math.pi), places=7)
94 | self.assertAlmostEqual(float(evaluate("radians(180)")), math.radians(180), places=7)
95 | self.assertAlmostEqual(float(evaluate("pi")), math.pi, places=7)
96 | self.assertAlmostEqual(float(evaluate("e")), math.e, places=7)
97 |
98 | if __name__ == '__main__':
99 | unittest.main()
100 |
```