calc-mcp-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. calc_mcp_server-0.1.0/.github/dependabot.yml +21 -0
  2. calc_mcp_server-0.1.0/.github/workflows/auto-release.yml +90 -0
  3. calc_mcp_server-0.1.0/.github/workflows/release.yml +90 -0
  4. calc_mcp_server-0.1.0/.github/workflows/validate.yml +89 -0
  5. calc_mcp_server-0.1.0/.gitignore +22 -0
  6. calc_mcp_server-0.1.0/.python-version +1 -0
  7. calc_mcp_server-0.1.0/AGENTS.md +80 -0
  8. calc_mcp_server-0.1.0/CHANGELOG.md +31 -0
  9. calc_mcp_server-0.1.0/CLAUDE.md +3 -0
  10. calc_mcp_server-0.1.0/LICENSE +21 -0
  11. calc_mcp_server-0.1.0/PKG-INFO +131 -0
  12. calc_mcp_server-0.1.0/README.md +107 -0
  13. calc_mcp_server-0.1.0/docs/README.md +81 -0
  14. calc_mcp_server-0.1.0/docs/domain/OVERVIEW.md +115 -0
  15. calc_mcp_server-0.1.0/docs/domain/README.md +8 -0
  16. calc_mcp_server-0.1.0/docs/superpowers/specs/2026-07-30-calc-mcp-server-design.md +93 -0
  17. calc_mcp_server-0.1.0/docs/tech/ARCHITECTURE.md +106 -0
  18. calc_mcp_server-0.1.0/docs/tech/CONVENTIONS.md +83 -0
  19. calc_mcp_server-0.1.0/docs/tech/README.md +11 -0
  20. calc_mcp_server-0.1.0/docs/tech/RELEASING.md +82 -0
  21. calc_mcp_server-0.1.0/docs/tech/SAFE-EVALUATION.md +122 -0
  22. calc_mcp_server-0.1.0/docs/tech/TECH-STACK.md +101 -0
  23. calc_mcp_server-0.1.0/docs/tech/TESTING.md +117 -0
  24. calc_mcp_server-0.1.0/pyproject.toml +81 -0
  25. calc_mcp_server-0.1.0/scripts/changelog_release.py +160 -0
  26. calc_mcp_server-0.1.0/server.json +22 -0
  27. calc_mcp_server-0.1.0/src/calc_mcp_server/__init__.py +15 -0
  28. calc_mcp_server-0.1.0/src/calc_mcp_server/_version.py +24 -0
  29. calc_mcp_server-0.1.0/src/calc_mcp_server/const.py +96 -0
  30. calc_mcp_server-0.1.0/src/calc_mcp_server/evaluator.py +236 -0
  31. calc_mcp_server-0.1.0/src/calc_mcp_server/py.typed +0 -0
  32. calc_mcp_server-0.1.0/src/calc_mcp_server/server.py +61 -0
  33. calc_mcp_server-0.1.0/tests/__init__.py +1 -0
  34. calc_mcp_server-0.1.0/tests/conftest.py +13 -0
  35. calc_mcp_server-0.1.0/tests/test_changelog_release.py +177 -0
  36. calc_mcp_server-0.1.0/tests/test_evaluator.py +174 -0
  37. calc_mcp_server-0.1.0/tests/test_server.py +44 -0
  38. calc_mcp_server-0.1.0/tests/test_stdio.py +140 -0
  39. calc_mcp_server-0.1.0/uv.lock +834 -0
@@ -0,0 +1,21 @@
1
+ version: 2
2
+ updates:
3
+ # Python dependencies (managed via uv / uv.lock)
4
+ - package-ecosystem: "uv"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ python-dependencies:
10
+ patterns:
11
+ - "*"
12
+
13
+ # GitHub Actions used in workflows
14
+ - package-ecosystem: "github-actions"
15
+ directory: "/"
16
+ schedule:
17
+ interval: "weekly"
18
+ groups:
19
+ github-actions:
20
+ patterns:
21
+ - "*"
@@ -0,0 +1,90 @@
1
+ name: Auto Release (Dependabot)
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ workflow_dispatch:
7
+ inputs:
8
+ version:
9
+ description: 'Version to release, e.g. v0.3.0. Leave empty to bump the patch.'
10
+ required: false
11
+ type: string
12
+
13
+ jobs:
14
+ tag:
15
+ # Runs for a manual dispatch, or for a merged Dependabot PR from the uv
16
+ # (Python dependency) ecosystem — those change the published package.
17
+ # github-actions bumps do not, so they merge without cutting a release.
18
+ if: >-
19
+ github.event_name == 'workflow_dispatch' ||
20
+ (github.event.pull_request.merged == true &&
21
+ github.event.pull_request.user.login == 'dependabot[bot]' &&
22
+ startsWith(github.event.pull_request.head.ref, 'dependabot/uv/'))
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - name: Generate GitHub App token
26
+ id: app-token
27
+ uses: actions/create-github-app-token@v3
28
+ with:
29
+ app-id: ${{ secrets.GH_ACTION_APP_ID }}
30
+ private-key: ${{ secrets.GH_ACTION_APP_PRIVATE_KEY }}
31
+ - uses: actions/checkout@v7
32
+ with:
33
+ ref: main
34
+ fetch-depth: 0 # need all tags + the merge commit
35
+ token: ${{ steps.app-token.outputs.token }}
36
+ - name: Resolve release version
37
+ id: ver
38
+ env:
39
+ REQUESTED: ${{ inputs.version }}
40
+ run: |
41
+ if [ -n "$REQUESTED" ]; then
42
+ if ! printf '%s' "$REQUESTED" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
43
+ echo "::error::version input '$REQUESTED' must look like v1.2.3"
44
+ exit 1
45
+ fi
46
+ next="$REQUESTED"
47
+ echo "Releasing requested version: $next"
48
+ else
49
+ latest=$(git tag -l 'v*' --sort=-v:refname | head -n1)
50
+ latest=${latest:-v0.0.0}
51
+ IFS=. read -r major minor patch <<< "${latest#v}"
52
+ next="v${major}.${minor}.$((patch + 1))"
53
+ echo "Releasing next patch: $next (from ${latest})"
54
+ fi
55
+ if git rev-parse -q --verify "refs/tags/$next" >/dev/null; then
56
+ echo "::error::tag $next already exists"
57
+ exit 1
58
+ fi
59
+ echo "next=$next" >> "$GITHUB_OUTPUT"
60
+
61
+ # Files the release section under the version being cut, so no published
62
+ # version can end up without one. Stdlib-only, so no project install.
63
+ - name: Prepare CHANGELOG
64
+ env:
65
+ NEXT: ${{ steps.ver.outputs.next }}
66
+ run: python3 scripts/changelog_release.py --version "$NEXT"
67
+
68
+ - name: Commit, tag and push
69
+ env:
70
+ NEXT: ${{ steps.ver.outputs.next }}
71
+ REASON: ${{ github.event_name == 'workflow_dispatch' && 'manual dispatch' || format('Dependabot PR {0}', github.event.pull_request.number) }}
72
+ run: |
73
+ git config user.name "github-actions[bot]"
74
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
75
+
76
+ # The script is a no-op when the section already exists (a human may
77
+ # have written it in the release PR), and committing nothing would
78
+ # fail and abort an otherwise valid release.
79
+ if git diff --quiet -- CHANGELOG.md; then
80
+ echo "CHANGELOG.md already had a $NEXT section; nothing to commit."
81
+ else
82
+ git add CHANGELOG.md
83
+ git commit -m "docs: changelog for $NEXT ($REASON)"
84
+ git push origin HEAD:main
85
+ fi
86
+
87
+ # Tag AFTER the commit so the tagged tree contains its own changelog
88
+ # section; tagging first would ship every release without one.
89
+ git tag -a "$NEXT" -m "Automated release $NEXT ($REASON)"
90
+ git push origin "$NEXT"
@@ -0,0 +1,90 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build distributions
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ with:
14
+ fetch-depth: 0 # hatch-vcs needs full history + tags to derive the version
15
+ - uses: astral-sh/setup-uv@v9.0.0
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Build sdist and wheel
19
+ run: uv build
20
+ - name: Verify tag matches built version
21
+ run: |
22
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
23
+ BUILT=$(ls dist/calc_mcp_server-*.tar.gz | sed -E 's|.*/calc_mcp_server-(.*)\.tar\.gz|\1|')
24
+ echo "tag=$TAG_VERSION built=$BUILT"
25
+ if [ "$TAG_VERSION" != "$BUILT" ]; then
26
+ echo "::error::Tag $TAG_VERSION does not match built version $BUILT"
27
+ exit 1
28
+ fi
29
+ - uses: actions/upload-artifact@v7
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ pypi-publish:
35
+ name: Publish to PyPI
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment: pypi
39
+ permissions:
40
+ id-token: write # OIDC for PyPI Trusted Publishing
41
+ steps:
42
+ - uses: actions/download-artifact@v8
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+
48
+ github-release:
49
+ name: Create GitHub Release
50
+ needs: pypi-publish
51
+ runs-on: ubuntu-latest
52
+ permissions:
53
+ contents: write
54
+ steps:
55
+ - uses: actions/download-artifact@v8
56
+ with:
57
+ name: dist
58
+ path: dist/
59
+ - name: Create release
60
+ env:
61
+ GH_TOKEN: ${{ github.token }}
62
+ run: |
63
+ gh release create "$GITHUB_REF_NAME" \
64
+ --repo "$GITHUB_REPOSITORY" \
65
+ --title "$GITHUB_REF_NAME" \
66
+ --generate-notes \
67
+ dist/*
68
+
69
+ mcp-registry:
70
+ name: Publish to MCP Registry
71
+ needs: pypi-publish
72
+ runs-on: ubuntu-latest
73
+ permissions:
74
+ id-token: write # OIDC proves ownership of the io.github.<owner> namespace
75
+ contents: read
76
+ steps:
77
+ - uses: actions/checkout@v7
78
+ - name: Set server.json version from tag
79
+ run: |
80
+ VERSION="${GITHUB_REF_NAME#v}"
81
+ jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' \
82
+ server.json > server.tmp && mv server.tmp server.json
83
+ cat server.json
84
+ - name: Install mcp-publisher
85
+ run: |
86
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
87
+ - name: Authenticate to MCP Registry
88
+ run: ./mcp-publisher login github-oidc
89
+ - name: Publish to MCP Registry
90
+ run: ./mcp-publisher publish
@@ -0,0 +1,89 @@
1
+ name: Validate
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ruff:
10
+ name: Ruff (lint + format)
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ - uses: astral-sh/setup-uv@v9.0.0
15
+ with:
16
+ python-version: "3.12"
17
+ enable-cache: true
18
+ # ruff comes from the dev group in the committed uv.lock, so CI lints with
19
+ # the exact version used locally. `pip install ruff` was unpinned, meaning
20
+ # a ruff release could fail the build without any change to this repo.
21
+ # --only-group keeps the project itself out of the env (linting does not
22
+ # need it installed); --no-sync then stops `uv run` from putting it back,
23
+ # which would build via hatch-vcs and so need the full git history.
24
+ - run: uv sync --locked --only-group dev
25
+ - run: uv run --no-sync ruff check .
26
+ - run: uv run --no-sync ruff format . --check
27
+
28
+ test:
29
+ name: Tests
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ with:
34
+ fetch-depth: 0 # hatch-vcs derives the version from git history + tags
35
+ - uses: astral-sh/setup-uv@v9.0.0
36
+ with:
37
+ python-version: "3.12"
38
+ enable-cache: true
39
+ # --locked installs the committed uv.lock exactly and fails if it has
40
+ # drifted from pyproject.toml, so dependency bumps are actually tested.
41
+ - run: uv sync --locked
42
+ # The whole suite runs here: unlike the sibling servers this one contacts
43
+ # nothing external, so there is no `integration` marker to deselect. That
44
+ # includes the stdio round trip, which is the check that would have caught
45
+ # the import break this repo exists to prevent.
46
+ - run: uv run pytest tests/ -v
47
+
48
+ # Advisory only. Deliberately absent from `gate`'s needs so it can never block
49
+ # a merge — the goal is to surface a forgotten entry while it is still cheap to
50
+ # add, not to add ceremony to dependency bumps or docs-only PRs.
51
+ changelog:
52
+ name: Changelog reminder
53
+ if: >-
54
+ github.event_name == 'pull_request' &&
55
+ github.event.pull_request.user.login != 'dependabot[bot]' &&
56
+ !contains(github.event.pull_request.labels.*.name, 'no-changelog')
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - uses: actions/checkout@v7
60
+ with:
61
+ fetch-depth: 0 # need the base ref to diff against
62
+ - name: Warn when src changed without a changelog entry
63
+ env:
64
+ BASE: ${{ github.event.pull_request.base.sha }}
65
+ HEAD: ${{ github.event.pull_request.head.sha }}
66
+ run: |
67
+ changed=$(git diff --name-only "$BASE" "$HEAD")
68
+ if grep -q '^src/' <<< "$changed" && ! grep -qx 'CHANGELOG.md' <<< "$changed"; then
69
+ echo "::warning::This PR changes src/ but not CHANGELOG.md. Add an entry under '## Unreleased', or label the PR 'no-changelog'."
70
+ else
71
+ echo "Changelog check satisfied."
72
+ fi
73
+
74
+ gate:
75
+ name: gate
76
+ needs: [ruff, test]
77
+ if: always()
78
+ runs-on: ubuntu-latest
79
+ steps:
80
+ - name: Check validation results
81
+ run: |
82
+ if [[ "${{ needs.ruff.result }}" == "success" && \
83
+ "${{ needs.test.result }}" == "success" ]]; then
84
+ echo "All checks passed"
85
+ exit 0
86
+ else
87
+ echo "One or more checks failed"
88
+ exit 1
89
+ fi
@@ -0,0 +1,22 @@
1
+ # Python
2
+ _version.py
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.pyo
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+
13
+ # Environment
14
+ .venv/
15
+
16
+ # macOS
17
+ .DS_Store
18
+ ._*
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,80 @@
1
+ # Calculator MCP Server
2
+ > MCP server for exact arithmetic, usable by LLMs via the Model Context Protocol. One `calculate` tool over a hand-rolled AST allowlist — no `eval()`, no unbounded dependencies.
3
+
4
+ > **Editing this guide:** `AGENTS.md` is the single source of truth for project context, read by all AI
5
+ > coding agents and humans. Keep it concise — put detail in `docs/` and link it. When you change code that
6
+ > alters documented behavior, update the matching `docs/` file in the **same PR** (CodeRabbit enforces
7
+ > this — see [docs/README.md](docs/README.md)).
8
+
9
+ ## Quick Reference
10
+ - **Lint**: `ruff check .`
11
+ - **Format**: `ruff format .`
12
+ - **Test**: `pytest tests/ -v`
13
+ - **Run server**: `uvx --from . calc-mcp-server` or `python -m calc_mcp_server.server`
14
+ - **Validate (CI)**: Ruff + pytest (all must pass via the `gate` job)
15
+ - **Release**: run the Auto Release workflow; the version comes from the git tag -- see [RELEASING.md](docs/tech/RELEASING.md)
16
+
17
+ ## Where to Find Things
18
+ | I need to... | Read |
19
+ |--------------|------|
20
+ | Understand the architecture | [ARCHITECTURE.md](docs/tech/ARCHITECTURE.md) |
21
+ | Change the allowlist, a cap, or anything security-relevant | [SAFE-EVALUATION.md](docs/tech/SAFE-EVALUATION.md) |
22
+ | Write code that fits conventions | [CONVENTIONS.md](docs/tech/CONVENTIONS.md) |
23
+ | Know the tech stack | [TECH-STACK.md](docs/tech/TECH-STACK.md) |
24
+ | Write or run tests | [TESTING.md](docs/tech/TESTING.md) |
25
+ | Cut a release, or add a changelog entry | [RELEASING.md](docs/tech/RELEASING.md) |
26
+ | Understand the domain and why this repo exists | [OVERVIEW.md](docs/domain/OVERVIEW.md) |
27
+
28
+ ## Architecture Overview
29
+ MCP presentation layer over a pure, dependency-free evaluator. Purely functional — no classes outside
30
+ the `MCPServer` instance and the exception hierarchy. All code lives in `src/calc_mcp_server/`.
31
+
32
+ - `server.py` -- MCPServer tool registration (1 tool), stdio entry point, turns `CalculatorError` into an `Error:` line
33
+ - `evaluator.py` -- parses, walks the AST against the allowlist, enforces the caps, renders the result
34
+ - `const.py` -- operator aliases, name/function allowlists, the four caps, float precision
35
+
36
+ Data flow: MCP tool call -> `server.py:calculate` -> `evaluator.evaluate()` (normalize aliases ->
37
+ `ast.parse` -> allowlist walk) -> `evaluator.format_result()` -> string.
38
+
39
+ See [Architecture](docs/tech/ARCHITECTURE.md) for module boundaries and data flow detail.
40
+
41
+ ## Tech Stack
42
+ - Python 3.12+, `from __future__ import annotations` in every file
43
+ - `mcp[cli]` (`mcp.server.MCPServer`) for MCP server framework -- v2 line, pinned `>=2,<3`
44
+ - **Exactly one runtime dependency**, upper-bounded. This is the point of the repo -- see Structural Risks
45
+ - `ruff` for linting/formatting, `pytest` + `pytest-asyncio` for testing
46
+ - `uv` for environment management, `hatchling` + `hatch-vcs` build backend
47
+ - GitHub Actions CI (validate on push/PR)
48
+
49
+ See [Tech Stack](docs/tech/TECH-STACK.md) for full detail.
50
+
51
+ ## Core Conventions
52
+ - Constants centralized in `const.py` -- no inline magic values, and the allowlists are written out rather than derived from `math`'s contents
53
+ - Logger: `_LOGGER = logging.getLogger(__name__)` with `%s` formatting (not f-strings)
54
+ - Import order: `__future__` -> stdlib -> third-party -> local
55
+ - `evaluator.py` raises typed `CalculatorError` subclasses; `server.py` catches them and returns a short `Error:` line -- the tool never raises
56
+ - Never coerce integers to float
57
+
58
+ See [Conventions](docs/tech/CONVENTIONS.md) for naming tables and full rules.
59
+
60
+ ## Business Domain
61
+ A calculator MCP server for LLM voice agents, replacing the abandoned and broken
62
+ `mcp-server-calculator`. One tool, `calculate`, taking an expression string. Integer results are exact
63
+ at any size; floats render at 12 significant digits. Evaluation is guarded against both code execution
64
+ and resource exhaustion.
65
+
66
+ See [Domain Overview](docs/domain/OVERVIEW.md) for the tool contract, the supported expression surface,
67
+ and why this repo exists.
68
+
69
+ ## Structural Risks
70
+ - **The server key `calculator` and the tool name `calculate` are load-bearing.** Home Assistant agent prompts route to `calculator__calculate` and an existing HA config entry points at `/servers/calculator/sse`. Renaming either breaks live voice agents -- see [OVERVIEW.md](docs/domain/OVERVIEW.md)
71
+ - **Never let a dependency go unbounded.** An unbounded `mcp>=1.4.1` in the package this replaces is what caused the outage this repo exists to prevent; `uv.lock` must be regenerated in the same commit as any `pyproject.toml` dependency change or `uv sync --locked` fails CI
72
+ - Widening `ALLOWED_FUNCTIONS` widens the attack surface -- any addition needs a cap review, since `factorial` needed one and `pow` needed another
73
+ - The caps in `const.py` are tuned to CPython's 4300-digit int-to-string limit; a runtime that changes that limit makes `MAX_RESULT_DIGITS` arbitrary rather than principled
74
+ - `MathError` wraps the stdlib's own message, whose wording differs between Python versions (3.12 says "math domain error", 3.14 says "expected a nonnegative input") -- tests assert the type, not the text
75
+ - Tool functions are called directly in tests, which relies on `@mcp.tool()` returning the function undecorated. That held across the v1 -> v2 migration; a future SDK returning a wrapper breaks the tests loudly rather than silently
76
+
77
+ ## Detailed Guides
78
+ - [Technical Context](docs/tech/README.md) -- architecture, safe evaluation, tech stack, conventions, testing
79
+ - [Domain Context](docs/domain/README.md) -- tool contract, expression surface, why this exists
80
+ - [Documentation Guide](docs/README.md) -- how to maintain these docs
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ The version of a release is derived from its git tag by `hatch-vcs`; there is no version string in the
4
+ source tree. Add entries under `## Unreleased` as you go — the release workflow moves them under the
5
+ version being cut, so you never rename that heading by hand. See
6
+ [docs/tech/RELEASING.md](docs/tech/RELEASING.md).
7
+
8
+ ## Unreleased
9
+
10
+ ## 0.1.0 - 2026-07-30
11
+
12
+ - Added: initial release. A single `calculate` tool evaluating arithmetic expressions against a
13
+ hand-rolled AST allowlist, replacing the abandoned `mcp-server-calculator` (last commit 2025-05-10),
14
+ whose unbounded `mcp>=1.4.1` made `uvx …@latest` resolve the v2 SDK, crash on the removed
15
+ `mcp.server.fastmcp`, and take the whole MCP proxy down with it.
16
+ - Added: resource caps the incumbent lacked — expression length, AST nesting depth, exponentiation
17
+ result size and factorial argument. `9**9**9` hangs the incumbent for over five seconds; here it is
18
+ rejected before any bignum work, from an estimate on the operands. See
19
+ [SAFE-EVALUATION.md](docs/tech/SAFE-EVALUATION.md).
20
+ - Added: integer results stay exact at any size. The only actively-maintained alternative (the npm
21
+ `@cyanheads/calculator-mcp-server`) computes in float64 and returns `121932631112635260` for
22
+ `123456789 * 987654321` — off by 9, in a tool that exists so the model does not have to approximate.
23
+ - Added: an end-to-end stdio round-trip test that spawns the real entry point and speaks JSON-RPC to it.
24
+ A unit-tested evaluator behind broken MCP wiring is precisely the failure that motivated this repo.
25
+ - Changed: floats render at 12 significant digits, so `0.1 + 0.2` reads `0.3` rather than
26
+ `0.30000000000000004`. Whole floats keep their `.0`, so `8 / 2` stays visibly distinct from `4`.
27
+ - Fixed: the CI test step no longer passes `-m "not integration"`. This repo contacts nothing external
28
+ and so defines no `integration` marker, and the flag disagreed with the `pytest tests/ -v` documented
29
+ in `AGENTS.md` and `TESTING.md` — the kind of drift that makes CI look like it covers more than it
30
+ does.
31
+
@@ -0,0 +1,3 @@
1
+ @AGENTS.md
2
+
3
+ Only update @AGENTS.md. Never edit @CLAUDE.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stefan Lettmayer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: calc-mcp-server
3
+ Version: 0.1.0
4
+ Summary: MCP server for exact arithmetic: a single safe calculator tool over an AST allowlist, with no unbounded dependencies.
5
+ Project-URL: Homepage, https://github.com/slettmayer/calc-mcp-server
6
+ Project-URL: Repository, https://github.com/slettmayer/calc-mcp-server
7
+ Project-URL: Issues, https://github.com/slettmayer/calc-mcp-server/issues
8
+ Project-URL: Changelog, https://github.com/slettmayer/calc-mcp-server/blob/main/CHANGELOG.md
9
+ Author: Stefan Lettmayer
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: arithmetic,calculator,home-assistant,math,mcp,model-context-protocol,safe-eval
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: mcp[cli]<3,>=2
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Calculator MCP Server
26
+
27
+ An MCP server that does arithmetic, exactly — so an LLM does not have to do it mentally.
28
+
29
+ One tool, `calculate`, evaluating expressions against a hand-rolled AST allowlist. No `eval()`, no
30
+ `sympy`, no unbounded dependencies.
31
+
32
+ ```
33
+ 2 + 3 * (4 - 1) / 2 ** 2 -> 4.25
34
+ sqrt(16) + sin(pi/2) -> 5.0
35
+ 123456789 * 987654321 -> 121932631112635269
36
+ ```
37
+
38
+ ## Why this exists
39
+
40
+ The widely-used `mcp-server-calculator` package is abandoned (last commit May 2025) and declares
41
+ `mcp>=1.4.1` with no upper bound. When the MCP Python SDK released v2 and removed
42
+ `mcp.server.fastmcp`, every `uvx …@latest` install of it started crashing on import — which took down
43
+ the entire MCP proxy hosting it, and every other server alongside it.
44
+
45
+ No maintained Python replacement exists. The one actively-maintained npm calculator computes in
46
+ float64, so `123456789 * 987654321` comes back as `121932631112635260` — off by 9. That is a poor
47
+ trait in a tool whose whole purpose is that the model should not be doing the arithmetic itself.
48
+
49
+ So this server:
50
+
51
+ - **bounds its one dependency** (`mcp[cli]>=2,<3`) — the failure above cannot recur here;
52
+ - **keeps integers exact** at any size, never coercing to float;
53
+ - **bounds resource use**, not just code execution — see below.
54
+
55
+ ## Safety
56
+
57
+ Two problems, and most calculator servers only solve the first.
58
+
59
+ **Code execution.** Expressions are parsed with `ast.parse` and walked against an explicit allowlist of
60
+ node types. `Attribute` is not on it, so `(1).__class__.__bases__` is rejected. A `Call` is only
61
+ evaluated when its target is a bare name in the function allowlist, so `__import__('os').system(…)` is
62
+ rejected before any argument is even evaluated.
63
+
64
+ **Resource exhaustion.** An allowlist alone still lets `9**9**9` occupy the process for minutes on
65
+ unbounded bignum exponentiation — the incumbent hangs for over five seconds on it. Four caps close
66
+ that: expression length (500 chars), nesting depth (32), result size (4300 digits, checked on the
67
+ operands *before* exponentiating), and factorial argument (1000).
68
+
69
+ Full detail in [docs/tech/SAFE-EVALUATION.md](docs/tech/SAFE-EVALUATION.md).
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ uvx calc-mcp-server
75
+ ```
76
+
77
+ Pin it. Do not add `@latest` — that is how the package this replaces broke.
78
+
79
+ ## Configure
80
+
81
+ As a stdio MCP server:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "calculator": {
87
+ "command": "uvx",
88
+ "args": ["calc-mcp-server"]
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## The `calculate` tool
95
+
96
+ | Argument | Type | Description |
97
+ |---|---|---|
98
+ | `expression` | `str` | The expression to evaluate |
99
+
100
+ Returns the result as a string, or a line starting with `Error: ` explaining why the expression was
101
+ rejected. The tool never raises, so a bad expression is an answer the agent can read back rather than a
102
+ tool-call failure.
103
+
104
+ **Operators** — `+` `-` `*` `/` `//` `%` `**`, parentheses, unary `+`/`-`. `^` is accepted as a power
105
+ operator, and `×` `·` `÷` `−` are accepted as their ASCII equivalents (speech-to-text produces them).
106
+
107
+ **Constants** — `pi`, `e`, `tau`.
108
+
109
+ **Functions** — `abs` `round` `min` `max` `sqrt` `exp` `log` `log2` `log10` `sin` `cos` `tan` `asin`
110
+ `acos` `atan` `atan2` `degrees` `radians` `hypot` `floor` `ceil` `factorial` `gcd` `lcm`.
111
+
112
+ **Results** — integer arithmetic returns an exact integer of any size. Floats are rendered at 12
113
+ significant digits, which removes IEEE-754 representation noise (`0.1 + 0.2` reads `0.3`, not
114
+ `0.30000000000000004`) while keeping far more precision than a calculator result is used at. A whole
115
+ float keeps its `.0`, so `8 / 2` reads `4.0` and stays distinct from the exact integer `4`.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ uv sync
121
+ uv run pytest tests/ -v
122
+ uv run ruff check .
123
+ uv run ruff format .
124
+ ```
125
+
126
+ See [AGENTS.md](AGENTS.md) for the project guide and [docs/](docs/README.md) for the full
127
+ documentation set.
128
+
129
+ ## License
130
+
131
+ MIT — see [LICENSE](LICENSE).