#
tokens: 41829/50000 17/17 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .github
│   ├── logo.png
│   └── workflows
│       ├── docker.yml
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .python-version
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README.md
├── server.py
├── tests
│   ├── __init__.py
│   ├── test_calculus.py
│   ├── test_linalg.py
│   ├── test_relativity.py
│   ├── test_server.py
│   └── test_units.py
├── uv.lock
└── vars.py
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
1 | __pycache__
2 | *.pyc
3 | *.pyo
4 | *.pyd
5 | .Python
6 | .env
7 | .git
8 | .venv
9 | 
```

--------------------------------------------------------------------------------
/.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 | .DS_Store
12 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | <div align="center">
  2 |   <img src=".github/logo.png" alt="Sympy MCP Logo" width="400" />
  3 | </div>
  4 | 
  5 | # Symbolic Algebra MCP Server
  6 | 
  7 | Sympy-MCP is a Model Context Protocol server for allowing LLMs to autonomously perform symbolic mathematics and computer algebra. It exposes numerous tools from SymPy's core functionality to MCP clients for manipulating mathematical expressions and equations.
  8 | 
  9 | ## Why?
 10 | 
 11 | Language models are absolutely abysmal at symbolic manipulation. They hallucinate variables, make up random constants, permute terms and generally make a mess. But we have computer algebra systems specifically built for symbolic manipulation, so we can use tool-calling to orchestrate a sequence of transforms so that the symbolic kernel does all the heavy lifting.
 12 | 
 13 | While you can certainly have an LLM generate Mathematica or Python code, if you want to use the LLM as an agent or on-the-fly calculator, it's a better experience to use the MCP server and expose the symbolic tools directly.
 14 | 
 15 | The server exposes a subset of symbolic mathematics capabilities including algebraic equation solving, integration and differentiation, vector calculus, tensor calculus for general relativity, and both ordinary and partial differential equations. 
 16 | 
 17 | For example, you can ask it in natural language to solve a differential equation:
 18 | 
 19 | > Solve the damped harmonic oscillator with forcing term: the mass-spring-damper system described by the differential equation where m is mass, c is the damping coefficient, k is the spring constant, and F(t) is an external force.
 20 | 
 21 | $$ m\frac{d^2x}{dt^2} + c\frac{dx}{dt} + kx = F(t) $$
 22 | 
 23 | Or involving general relativity:
 24 | 
 25 | > Compute the trace of the Ricci tensor $R_{\mu\nu}$ using the inverse metric $g^{\mu\nu}$ for Anti-de Sitter spacetime to determine its constant scalar curvature $R$.
 26 | 
 27 | ## Usage
 28 | 
 29 | You need [uv](https://docs.astral.sh/uv/getting-started/installation/) first.
 30 | 
 31 | - **Homebrew** : `brew install uv`
 32 | - **Curl** : `curl -LsSf https://astral.sh/uv/install.sh | sh`
 33 | 
 34 | Then you can install and run the server with the following commands:
 35 | 
 36 | ```shell
 37 | # Setup the project
 38 | git clone https://github.com/sdiehl/sympy-mcp.git
 39 | cd sympy-mcp
 40 | uv sync
 41 | 
 42 | # Install the server to Claude Desktop
 43 | uv run mcp install server.py
 44 | 
 45 | # Run the server
 46 | uv run mcp run server.py
 47 | ```
 48 | 
 49 | You should see the server available in the Claude Desktop app now. For other clients, see below.
 50 | 
 51 | If you want a completely standalone version that just runs with a single command, you can use the following. *Note this is running arbitrary code from Github, so be careful.*
 52 | 
 53 | ```shell
 54 | uv run --with https://github.com/sdiehl/sympy-mcp/releases/download/0.1/sympy_mcp-0.1.0-py3-none-any.whl python server.py
 55 | ```
 56 | 
 57 | If you want to do general relativity calculations, you need to install the [`einsteinpy`](https://github.com/einsteinpy/einsteinpy) library.
 58 | 
 59 | ```shell
 60 | uv sync --group relativity
 61 | ```
 62 | 
 63 | ## Available Tools
 64 | 
 65 | The sympy-mcp server provides the following tools for symbolic mathematics:
 66 | 
 67 | | Tool | Tool ID | Description |
 68 | |------|-------|-------------|
 69 | | Variable Introduction | `intro` | Introduces a variable with specified assumptions and stores it |
 70 | | Multiple Variables | `intro_many` | Introduces multiple variables with specified assumptions simultaneously |
 71 | | Expression Parser | `introduce_expression` | Parses an expression string using available local variables and stores it |
 72 | | LaTeX Printer | `print_latex_expression` | Prints a stored expression in LaTeX format, along with variable assumptions |
 73 | | Algebraic Solver | `solve_algebraically` | Solves an equation algebraically for a given variable over a given domain |
 74 | | Linear Solver | `solve_linear_system` | Solves a system of linear equations |
 75 | | Nonlinear Solver | `solve_nonlinear_system` | Solves a system of nonlinear equations |
 76 | | Function Variable | `introduce_function` | Introduces a function variable for use in differential equations |
 77 | | ODE Solver | `dsolve_ode` | Solves an ordinary differential equation |
 78 | | PDE Solver | `pdsolve_pde` | Solves a partial differential equation |
 79 | | Standard Metric | `create_predefined_metric` | Creates a predefined spacetime metric (e.g. Schwarzschild, Kerr, Minkowski) |
 80 | | Metric Search | `search_predefined_metrics` | Searches available predefined metrics |
 81 | | Tensor Calculator | `calculate_tensor` | Calculates tensors from a metric (Ricci, Einstein, Weyl tensors) |
 82 | | Custom Metric | `create_custom_metric` | Creates a custom metric tensor from provided components and symbols |
 83 | | Tensor LaTeX | `print_latex_tensor` | Prints a stored tensor expression in LaTeX format |
 84 | | Simplifier | `simplify_expression` | Simplifies a mathematical expression using SymPy's canonicalize function |
 85 | | Substitution | `substitute_expression` | Substitutes a variable with an expression in another expression |
 86 | | Integration | `integrate_expression` | Integrates an expression with respect to a variable |
 87 | | Differentiation | `differentiate_expression` | Differentiates an expression with respect to a variable |
 88 | | Coordinates | `create_coordinate_system` | Creates a 3D coordinate system for vector calculus operations |
 89 | | Vector Field | `create_vector_field` | Creates a vector field in the specified coordinate system |
 90 | | Curl | `calculate_curl` | Calculates the curl of a vector field |
 91 | | Divergence | `calculate_divergence` | Calculates the divergence of a vector field |
 92 | | Gradient | `calculate_gradient` | Calculates the gradient of a scalar field |
 93 | | Unit Converter | `convert_to_units` | Converts a quantity to given target units |
 94 | | Unit Simplifier | `quantity_simplify_units` | Simplifies a quantity with units |
 95 | | Matrix Creator | `create_matrix` | Creates a SymPy matrix from the provided data |
 96 | | Determinant | `matrix_determinant` | Calculates the determinant of a matrix |
 97 | | Matrix Inverse | `matrix_inverse` | Calculates the inverse of a matrix |
 98 | | Eigenvalues | `matrix_eigenvalues` | Calculates the eigenvalues of a matrix |
 99 | | Eigenvectors | `matrix_eigenvectors` | Calculates the eigenvectors of a matrix |
100 | 
101 | By default variables are predefined with assumptions (similar to how the [symbols()](https://docs.sympy.org/latest/modules/core.html#sympy.core.symbol.symbols) function works in SymPy). Unless otherwise specified the defaut assumptions is that a variable is complex, commutative, term over the complex field $\mathbb{C}$.
102 | 
103 | | Property | Value |
104 | |----------|-------|
105 | | `commutative` | true |
106 | | `complex` | true |
107 | | `finite` | true |
108 | | `infinite` | false |
109 | 
110 | ## Claude Desktop Setup
111 | 
112 | Normally the `mcp install` command will automatically add the server to the `claude_desktop_config.json` file. If it doesn't you need to find the config file and add the following:
113 | 
114 | * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
115 | * Windows: `%APPDATA%\Claude\claude_desktop_config.json`
116 | 
117 | Add the following to the `mcpServers` object, replacing `/ABSOLUTE_PATH_TO_SYMPY_MCP/server.py` with the absolute path to the sympy-mcp `server.py` file.
118 | 
119 | ```json
120 | {
121 |   "mcpServers": {
122 |     "sympy-mcp": {
123 |       "command": "/opt/homebrew/bin/uv",
124 |       "args": [
125 |         "run",
126 |         "--with",
127 |         "einsteinpy",
128 |         "--with",
129 |         "mcp[cli]",
130 |         "--with",
131 |         "pydantic",
132 |         "--with",
133 |         "sympy",
134 |         "mcp",
135 |         "run",
136 |         "/ABSOLUTE_PATH_TO_SYMPY_MCP/server.py"
137 |       ]
138 |     }
139 |   }
140 | }
141 | ```
142 | 
143 | ## Cursor Setup
144 | 
145 | In your `~/.cursor/mcp.json`, add the following, where `ABSOLUTE_PATH_TO_SYMPY_MCP` is the path to the sympy-mcp server.py file.
146 | 
147 | ```json
148 | {
149 |   "mcpServers": {
150 |     "sympy-mcp": {
151 |       "command": "/opt/homebrew/bin/uv",
152 |       "args": [
153 |         "run",
154 |         "--with",
155 |         "einsteinpy",
156 |         "--with",
157 |         "mcp[cli]",
158 |         "--with",
159 |         "pydantic",
160 |         "--with",
161 |         "sympy",
162 |         "mcp",
163 |         "run",
164 |         "/ABSOLUTE_PATH_TO_SYMPY_MCP/server.py"
165 |       ]
166 |     }
167 |   }
168 | }
169 | ```
170 | 
171 | ## VS Code Setup
172 | 
173 | VS Code and VS Code Insiders now support MCPs in [agent mode](https://code.visualstudio.com/blogs/2025/04/07/agentMode). For VS Code, you may need to enable `Chat > Agent: Enable` in the settings.
174 | 
175 | 1. **One-click Setup:**
176 | 
177 | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sympy-mcp&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22-p%22%2C%228081%3A8081%22%2C%22--rm%22%2C%22ghcr.io%2Fsdiehl%2Fsympy-mcp%3Amain%22%5D%7D)
178 | 
179 | [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=sympy-mcp&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22-p%22%2C%228081%3A8081%22%2C%22--rm%22%2C%22ghcr.io%2Fsdiehl%2Fsympy-mcp%3Amain%22%5D%7D&quality=insiders)
180 | 
181 | OR manually add the config to your `settings.json` (global):
182 | 
183 | ```json
184 | {
185 |   "mcp": {
186 |     "servers": {
187 |       "sympy-mcp": {
188 |         "command": "uv",
189 |         "args": [
190 |           "run",
191 |           "--with",
192 |           "einsteinpy",
193 |           "--with",
194 |           "mcp[cli]",
195 |           "--with",
196 |           "pydantic",
197 |           "--with",
198 |           "sympy",
199 |           "mcp",
200 |           "run",
201 |           "/ABSOLUTE_PATH_TO_SYMPY_MCP/server.py"
202 |         ]
203 |       }
204 |     }
205 |   }
206 | }
207 | ```
208 | 
209 | 2. Click "Start" above the server config switch to agent mode in the chat, and try commands like "integrate x^2" or "solve x^2 = 1" to get started.
210 | 
211 | ## Cline Setup
212 | 
213 | To use with [Cline](https://cline.bot/), you need to manually run the MCP server first using the commands in the "Usage" section. Once the MCP server is running, open Cline and select "MCP Servers" at the top.
214 | 
215 | Then select "Remote Servers" and add the following:
216 | 
217 | - Server Name: `sympy-mcp`
218 | - Server URL: `http://127.0.0.1:8081/sse`
219 | 
220 | ## 5ire Setup
221 | 
222 | Another MCP client that supports multiple models (o3, o4-mini, DeepSeek-R1, etc.) on the backend is 5ire.
223 | 
224 | To set up with [5ire](https://github.com/nanbingxyz/5ire), open 5ire and go to Tools -> New and set the following configurations:
225 | 
226 | - Tool Key: `sympy-mcp`
227 | - Name: SymPy MCP
228 | - Command: `/opt/homebrew/bin/uv run --with einsteinpy --with mcp[cli] --with pydantic --with sympy mcp run /ABSOLUTE_PATH_TO/server.py`
229 | 
230 | Replace `/ABSOLUTE_PATH_TO/server.py` with the actual path to your sympy-mcp server.py file.
231 | 
232 | ## Running in Container
233 | 
234 | You can build and run the server using Docker locally:
235 | 
236 | ```bash
237 | # Build the Docker image
238 | docker build -t sympy-mcp .
239 | 
240 | # Run the Docker container
241 | docker run -p 8081:8081 sympy-mcp
242 | ```
243 | 
244 | Alternatively, you can pull the pre-built image from GitHub Container Registry:
245 | 
246 | ```bash
247 | # Pull the latest image
248 | docker pull ghcr.io/sdiehl/sympy-mcp:main
249 | 
250 | # Run the container
251 | docker run -p 8081:8081 --rm ghcr.io/sdiehl/sympy-mcp:main
252 | ```
253 | 
254 | To configure Claude Desktop to launch the Docker container, edit your `claude_desktop_config.json` file:
255 | 
256 | ```json
257 | {
258 |   "mcpServers": {
259 |     "sympy-mcp": {
260 |       "command": "docker",
261 |       "args": [
262 |         "run",
263 |         "-i",
264 |         "-p",
265 |         "8081:8081",
266 |         "--rm",
267 |         "sympy-mcp"
268 |       ]
269 |     }
270 |   }
271 | }
272 | ```
273 | 
274 | Or to use the pre-built container from GitHub:
275 | 
276 | ```json
277 | {
278 |   "mcpServers": {
279 |     "sympy-mcp": {
280 |       "command": "docker",
281 |       "args": [
282 |         "run",
283 |         "-i",
284 |         "-p",
285 |         "8081:8081",
286 |         "--rm",
287 |         "ghcr.io/sdiehl/sympy-mcp:main"
288 |       ]
289 |     }
290 |   }
291 | }
292 | ```
293 | 
294 | This configuration tells Claude Desktop to launch the Docker container when needed. Make sure to build the Docker image (`docker build -t sympy-mcp .`) before using Claude Desktop with this configuration.
295 | 
296 | The other installation methods can also be adapted to work with Docker if you change the uv command to use the docker run command instead.
297 | 
298 | ## Example Interaction 1 : Differential Equations
299 | 
300 | Here's an example of how the tool-calling works in a conversation with an AI assistant using this MCP server:
301 | 
302 | **User**: Solve the damped harmonic oscillator with forcing term: the mass-spring-damper system described by the differential equation where m is mass, c is the damping coefficient, k is the spring constant, and F(t) is an external force.
303 | 
304 | $$ m\frac{d^2x}{dt^2} + c\frac{dx}{dt} + kx = F(t) $$
305 | 
306 | **Assistant**: (Internal tool call) `intro_many` to introduce variables:
307 | ```json
308 | {
309 |   "variables": [
310 |     {"var_name": "t", "pos_assumptions": ["real"], "neg_assumptions": []},
311 |     {"var_name": "m", "pos_assumptions": ["real", "positive"], "neg_assumptions": []},
312 |     {"var_name": "c", "pos_assumptions": ["real", "positive"], "neg_assumptions": []},
313 |     {"var_name": "k", "pos_assumptions": ["real", "positive"], "neg_assumptions": []}
314 |   ]
315 | }
316 | ```
317 | 
318 | **Assistant**: (Internal tool call) `introduce_function` to define x(t):
319 | ```json
320 | {
321 |   "func_name": "x"
322 | }
323 | ```
324 | 
325 | **Assistant**: (Internal tool call) `introduce_expression` to create the differential equation:
326 | ```json
327 | {
328 |   "expr_str": "Eq(m*Derivative(x(t), t, t) + c*Derivative(x(t), t) + k*x(t), F(t))"
329 | }
330 | ```
331 | 
332 | **Assistant**: (Internal tool call) `print_latex_expression` to display the equation nicely:
333 | ```json
334 | {
335 |   "expr_key": "expr_12"
336 | }
337 | ```
338 | 
339 | Result:
340 | 
341 | $$
342 | c \frac{d}{d t} x{\left(t \right)} + k x{\left(t \right)} + m \frac{d^{2}}{d t^{2}} x{\left(t \right)} = F{\left(t \right)}
343 | $$
344 | 
345 | **Assistant**: (Internal tool call) `introduce_function` to define F(t):
346 | ```json
347 | {
348 |   "func_name": "F"
349 | }
350 | ```
351 | 
352 | **Assistant**: (Internal tool call) `dsolve_ode` to solve the differential equation:
353 | ```json
354 | {
355 |   "expr_key": "expr_12",
356 |   "func_name": "x"
357 | }
358 | ```
359 | 
360 | Result:
361 | 
362 | $$
363 | x{\left(t \right)} = C_{1} e^{\frac{t \left(- c + \sqrt{c^{2} - 4 k m}\right)}{2 m}} + C_{2} e^{- \frac{t \left(c + \sqrt{c^{2} - 4 k m}\right)}{2 m}} + \cdots
364 | $$
365 | 
366 | ## Example Interaction 2: General Relativity
367 | 
368 | **User**: Compute the trace of the Ricci tensor $R_{\mu\nu}$ using the inverse metric $g^{\mu\nu}$ for Anti-de Sitter spacetime to determine its constant scalar curvature $R$.
369 | 
370 | **Assistant**: (Internal tool call) `create_predefined_metric` to create the Anti-de Sitter metric:
371 | ```json
372 | {
373 |   "metric_name": "AntiDeSitter"
374 | }
375 | ```
376 | 
377 | **Assistant**: (Internal tool call) `calculate_tensor` to calculate the Ricci tensor:
378 | ```json
379 | {
380 |   "metric_key": "metric_AntiDeSitter",
381 |   "tensor_type": "RICCI_TENSOR",
382 |   "simplify_result": true
383 | }
384 | ```
385 | 
386 | **Assistant**: (Internal tool call) `print_latex_tensor` to display the Ricci tensor:
387 | ```json
388 | {
389 |   "tensor_key": "riccitensor_metric_AntiDeSitter"
390 | }
391 | ```
392 | 
393 | Result:
394 | 
395 | $$
396 | \left[\begin{matrix}3 & 0 & 0 & 0\\\\0 & - 3 \cos^{2}{\left(t \right)} & 0 & 0\\\\0 & 0 & - 3 \cos^{2}{\left(t \right)} \sinh^{2}{\left(\chi \right)} & 0\\\\0 & 0 & 0 & - 3 \sin^{2}{\left(\theta \right)} \cos^{2}{\left(t \right)} \sinh^{2}{\left(\chi \right)}\end{matrix}\right]
397 | $$
398 | 
399 | **Assistant**: (Internal tool call) `calculate_tensor` to calculate the Ricci scalar:
400 | ```json
401 | {
402 |   "metric_key": "metric_AntiDeSitter",
403 |   "tensor_type": "RICCI_SCALAR",
404 |   "simplify_result": true
405 | }
406 | ```
407 | 
408 | **Assistant**: (Internal tool call) `print_latex_tensor` to display the Ricci scalar:
409 | ```json
410 | {
411 |   "tensor_key": "ricciscalar_metric_AntiDeSitter"
412 | }
413 | ```
414 | 
415 | Result:
416 | 
417 | $$
418 | -12
419 | $$
420 | 
421 | ## Security Disclaimer
422 | 
423 | This server runs on your computer and gives the language model access to run Python logic. Notably it uses Sympy's `parse_expr` to parse mathematical expressions, which is uses `eval` under the hood, effectively allowing arbitrary code execution. By running the server, you are trusting the code that Claude generates. Running in the Docker image is slightly safer, but it's still a good idea to review the code before running it.
424 | 
425 | 
426 | ## License
427 | 
428 | Copyright 2025 Stephen Diehl.
429 | 
430 | This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for details.
431 | 
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Tests for the sympy-mcp server.
3 | """
4 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "sympy-mcp"
 3 | version = "0.1.0"
 4 | description = "A MCP server for symbolic manipulation of mathematical expressions"
 5 | readme = "README.md"
 6 | license = "Apache-2.0"
 7 | requires-python = ">=3.12"
 8 | dependencies = [
 9 |     "mcp[cli]>=1.9.0",
10 |     "sympy>=1.14.0",
11 | ]
12 | 
13 | [build-system]
14 | requires = ["setuptools>=61.0"]
15 | build-backend = "setuptools.build_meta"
16 | 
17 | [tool.setuptools]
18 | py-modules = ["server", "vars"]
19 | 
20 | [dependency-groups]
21 | dev = [
22 |     "black>=25.1.0",
23 |     "pytest>=8.3.5",
24 |     "ruff>=0.11.10",
25 | ]
26 | relativity = [
27 |     "einsteinpy>=0.4.0",
28 | ]
29 | 
30 | [tool.uv]
31 | default-groups = []
32 | 
33 | [tool.pytest]
34 | testpaths = ["tests"]
35 | python_files = "test_*.py"
```

--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Test
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   test:
11 |     runs-on: ubuntu-latest
12 |     
13 |     steps:
14 |     - uses: actions/checkout@v4
15 |     
16 |     - name: Set up Python 3.12
17 |       uses: actions/setup-python@v5
18 |       with:
19 |         python-version: '3.12'
20 |         
21 |     - name: Install uv
22 |       run: |
23 |         curl -LsSf https://astral.sh/uv/install.sh | sh
24 |         echo "$HOME/.cargo/bin" >> $GITHUB_PATH
25 |         
26 |     - name: Create virtual environment and install dependencies
27 |       run: |
28 |         uv venv
29 |         . .venv/bin/activate
30 |         uv sync --group dev --group relativity
31 |         
32 |     - name: Lint with Ruff
33 |       run: |
34 |         . .venv/bin/activate
35 |         ruff check .
36 |         
37 |     - name: Test with pytest
38 |       run: |
39 |         . .venv/bin/activate
40 |         pytest 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM python:3.12-slim
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # The installer requires curl (and certificates) to download the release archive
 6 | RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
 7 | 
 8 | # Download the latest installer
 9 | ADD https://astral.sh/uv/install.sh /uv-installer.sh
10 | 
11 | # Run the installer then remove it
12 | RUN sh /uv-installer.sh && rm /uv-installer.sh
13 | 
14 | # Ensure the installed binary is on the `PATH`
15 | ENV PATH="/root/.local/bin/:$PATH"
16 | 
17 | # Copy application code
18 | COPY pyproject.toml .
19 | COPY vars.py .
20 | COPY server.py .
21 | 
22 | # Expose the default MCP port
23 | EXPOSE 8081
24 | 
25 | # Add healthcheck
26 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
27 |   CMD curl -f http://localhost:8081/healthcheck || exit 1
28 | 
29 | # Run the server with SSE transport
30 | CMD ["uv", "run", "--with", "mcp[cli]", "--with", "sympy", "mcp", "run", "/app/server.py", "--transport", "sse"] 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Build and Publish Python Package
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [created]
 6 |   workflow_dispatch:
 7 | 
 8 | jobs:
 9 |   build-and-publish:
10 |     runs-on: ubuntu-latest
11 |     permissions:
12 |       contents: write
13 | 
14 |     steps:
15 |     - uses: actions/checkout@v4
16 |     
17 |     - name: Set up Python 3.12
18 |       uses: actions/setup-python@v5
19 |       with:
20 |         python-version: '3.12'
21 |         
22 |     - name: Install uv
23 |       run: |
24 |         curl -LsSf https://astral.sh/uv/install.sh | sh
25 |         echo "$HOME/.cargo/bin" >> $GITHUB_PATH
26 |         
27 |     - name: Build package
28 |       run: |
29 |         uv build
30 |         
31 |     - name: List built distributions
32 |       run: |
33 |         ls -l dist/
34 |         
35 |     - name: Upload to GitHub Release
36 |       if: github.event_name == 'release'
37 |       uses: softprops/action-gh-release@v1
38 |       with:
39 |         files: |
40 |           dist/*.whl
41 |           dist/*.tar.gz
42 |       env:
43 |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
```

--------------------------------------------------------------------------------
/tests/test_linalg.py:
--------------------------------------------------------------------------------

```python
 1 | from server import (
 2 |     intro,
 3 |     introduce_expression,
 4 |     create_matrix,
 5 |     matrix_determinant,
 6 |     matrix_inverse,
 7 |     matrix_eigenvalues,
 8 |     matrix_eigenvectors,
 9 |     substitute_expression,
10 |     print_latex_expression,
11 | )
12 | 
13 | 
14 | def test_matrix_creation():
15 |     # Create a simple 2x2 matrix
16 |     matrix_key = create_matrix([[1, 2], [3, 4]], "M")
17 |     assert matrix_key == "M"
18 | 
19 | 
20 | def test_determinant():
21 |     # Create a matrix and calculate its determinant
22 |     matrix_key = create_matrix([[1, 2], [3, 4]], "M")
23 |     det_key = matrix_determinant(matrix_key)
24 |     # Should be -2
25 |     expr = print_latex_expression(det_key)
26 |     assert expr == "-2"
27 | 
28 | 
29 | def test_inverse():
30 |     # Create a matrix and calculate its inverse
31 |     matrix_key = create_matrix([[1, 2], [3, 4]], "M")
32 |     inv_key = matrix_inverse(matrix_key)
33 |     # Check result - don't check exact string as it may vary
34 |     expr = print_latex_expression(inv_key)
35 |     # The inverse of [[1, 2], [3, 4]] should have -2, 1, 3/2, -1/2 as elements
36 |     assert "-2" in expr
37 |     assert "1" in expr
38 |     assert "\\frac{3}{2}" in expr
39 | 
40 | 
41 | def test_eigenvalues():
42 |     # Create a matrix and calculate its eigenvalues
43 |     matrix_key = create_matrix([[3, 1], [1, 3]], "M")
44 |     evals_key = matrix_eigenvalues(matrix_key)
45 |     # Eigenvalues should be 2 and 4
46 |     expr = print_latex_expression(evals_key)
47 |     assert "2" in expr
48 |     assert "4" in expr
49 | 
50 | 
51 | def test_eigenvectors():
52 |     # Create a matrix and calculate its eigenvectors
53 |     matrix_key = create_matrix([[3, 1], [1, 3]], "M")
54 |     evecs_key = matrix_eigenvectors(matrix_key)
55 |     # Just check that the result is not an error
56 |     expr = print_latex_expression(evecs_key)
57 |     assert "Error" not in expr
58 | 
59 | 
60 | def test_substitute():
61 |     # Create variables and expressions
62 |     intro("x", [], [])
63 |     intro("y", [], [])
64 |     expr1 = introduce_expression("x**2 + y**2")
65 |     expr2 = introduce_expression("y + 1")
66 |     # Substitute y = y + 1 in x^2 + y^2
67 |     result_key = substitute_expression(expr1, "y", expr2)
68 |     # Result should be x^2 + (y+1)^2 = x^2 + y^2 + 2y + 1
69 |     expr = print_latex_expression(result_key)
70 |     assert "x^{2}" in expr
71 |     assert "y" in expr
72 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Docker
 2 | 
 3 | on:
 4 |   schedule:
 5 |     - cron: "27 0 * * *"
 6 |   push:
 7 |     branches: ["main"]
 8 |     tags: ["v*.*.*"]
 9 |   pull_request:
10 |     branches: ["main"]
11 | 
12 | env:
13 |   REGISTRY: ghcr.io
14 |   IMAGE_NAME: ${{ github.repository }}
15 | 
16 | jobs:
17 |   build:
18 |     runs-on: ubuntu-latest
19 |     permissions:
20 |       contents: read
21 |       packages: write
22 | 
23 |     steps:
24 |       - name: Checkout repository
25 |         uses: actions/checkout@v4
26 | 
27 |       - name: Set up Docker Buildx
28 |         uses: docker/setup-buildx-action@v3
29 | 
30 |       # Login against a Docker registry except on PR
31 |       - name: Log into registry ${{ env.REGISTRY }}
32 |         if: github.event_name != 'pull_request'
33 |         uses: docker/login-action@v3
34 |         with:
35 |           registry: ${{ env.REGISTRY }}
36 |           username: ${{ github.actor }}
37 |           password: ${{ secrets.GITHUB_TOKEN }}
38 | 
39 |       # Extract metadata (tags, labels) for Docker
40 |       - name: Extract Docker metadata
41 |         id: meta
42 |         uses: docker/metadata-action@v5
43 |         with:
44 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
45 |           tags: |
46 |             type=schedule
47 |             type=ref,event=branch
48 |             type=ref,event=tag
49 |             type=ref,event=pr
50 |             type=semver,pattern={{version}}
51 |             type=semver,pattern={{major}}.{{minor}}
52 |             type=semver,pattern={{major}}
53 |             type=sha
54 |             type=edge
55 |             type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
56 | 
57 |       # Build and push Docker image with Buildx
58 |       - name: Build and push Docker image
59 |         uses: docker/build-push-action@v5
60 |         with:
61 |           context: .
62 |           push: ${{ github.event_name != 'pull_request' }}
63 |           tags: ${{ steps.meta.outputs.tags }}
64 |           labels: ${{ steps.meta.outputs.labels }}
65 |           cache-from: type=gha
66 |           cache-to: type=gha,mode=max
67 |           platforms: linux/amd64,linux/arm64
68 | 
69 |       # Test Docker image if not pushing (PR context)
70 |       - name: Test Docker image
71 |         if: github.event_name == 'pull_request'
72 |         run: |
73 |           docker images
74 |           docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} uv run --with sympy python -c "import sympy; print(f'SymPy version: {sympy.__version__}')" 
```

--------------------------------------------------------------------------------
/tests/test_units.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | from server import (
  3 |     introduce_expression,
  4 |     convert_to_units,
  5 |     quantity_simplify_units,
  6 |     print_latex_expression,
  7 |     expressions,
  8 |     initialize_units,
  9 |     local_vars,
 10 | )
 11 | from vars import UnitSystem
 12 | 
 13 | 
 14 | @pytest.fixture(autouse=True)
 15 | def reset_globals():
 16 |     # Clear global dictionaries before each test
 17 |     expressions.clear()
 18 |     local_vars.clear()  # Clear local_vars to avoid cross-test pollution
 19 |     import server
 20 | 
 21 |     server.expression_counter = 0
 22 |     # Ensure units are properly initialized for each test
 23 |     initialize_units()
 24 |     yield
 25 | 
 26 | 
 27 | def test_convert_to_si():
 28 |     # speed_of_light in [meter, second]
 29 |     expr_key = introduce_expression("speed_of_light")
 30 |     result_key = convert_to_units(expr_key, ["meter", "second"], UnitSystem.SI)
 31 |     latex = print_latex_expression(result_key)
 32 |     assert "\\text{m}" in latex and "\\text{s}" in latex
 33 |     assert "299792458" in latex
 34 | 
 35 | 
 36 | def test_convert_to_impossible():
 37 |     # speed_of_light in [meter] (should return unchanged)
 38 |     expr_key = introduce_expression("speed_of_light")
 39 |     result_key = convert_to_units(expr_key, ["meter"], UnitSystem.SI)
 40 |     latex = print_latex_expression(result_key)
 41 |     assert "\\text{c}" in latex  # c is the symbol for speed of light
 42 | 
 43 | 
 44 | def test_convert_to_cgs_gauss():
 45 |     # ampere in [meter, gram, second] in cgs_gauss
 46 |     expr_key = introduce_expression("ampere")
 47 |     # First test with SI
 48 |     result_key_si = convert_to_units(
 49 |         expr_key, ["meter", "gram", "second"], UnitSystem.SI
 50 |     )
 51 |     latex_si = print_latex_expression(result_key_si)
 52 |     assert "\\text{A}" in latex_si  # A is the symbol for ampere
 53 | 
 54 |     # Then with CGS
 55 |     result_key_cgs = convert_to_units(
 56 |         expr_key, ["meter", "gram", "second"], UnitSystem.CGS
 57 |     )
 58 |     latex_cgs = print_latex_expression(result_key_cgs)
 59 |     # In CGS, ampere should be converted to a combination of base units
 60 |     # Either we'll see the units or we'll still see ampere if conversion failed
 61 |     assert (
 62 |         "\\text{g}" in latex_cgs or "\\text{m}" in latex_cgs or "\\text{A}" in latex_cgs
 63 |     )
 64 | 
 65 | 
 66 | def test_quantity_simplify():
 67 |     # meter/kilometer should simplify to 1/1000 or 0.001
 68 |     expr_key = introduce_expression("meter/kilometer")
 69 |     result_key = quantity_simplify_units(expr_key, UnitSystem.SI)
 70 |     latex = print_latex_expression(result_key)
 71 |     assert "0.001" in latex or "\\frac{1}{1000}" in latex or "10^{-3}" in latex
 72 | 
 73 |     # Also test .simplify() via sympy
 74 |     expr_key2 = introduce_expression("(meter/kilometer).simplify()")
 75 |     latex2 = print_latex_expression(expr_key2)
 76 |     assert "0.001" in latex2 or "\\frac{1}{1000}" in latex2 or "10^{-3}" in latex2
 77 | 
 78 | 
 79 | def test_convert_to_unknown_unit():
 80 |     expr_key = introduce_expression("meter")
 81 |     result = convert_to_units(expr_key, ["not_a_unit"], UnitSystem.SI)
 82 |     assert "Error" in result or "error" in result.lower()
 83 | 
 84 | 
 85 | def test_quantity_simplify_nonexistent_expr():
 86 |     result = quantity_simplify_units("nonexistent_key", UnitSystem.SI)
 87 |     assert "Error" in result or "error" in result.lower()
 88 | 
 89 | 
 90 | def test_convert_to_prefixed_units():
 91 |     # Test with prefixed units already applied in the expression
 92 |     # Create speed of light in femtometer/second directly
 93 |     expr_key = introduce_expression(
 94 |         "speed_of_light * (10**15)", expr_var_name="speed_of_light_in_fm_s"
 95 |     )
 96 |     latex = print_latex_expression(expr_key)
 97 |     assert "299792458" in latex and "10^{15}" in latex or "c" in latex
 98 | 
 99 |     # Test conversion from prefixed units
100 |     expr_key = introduce_expression("1000*kilometer")
101 |     result_key = convert_to_units(expr_key, ["meter"], UnitSystem.SI)
102 |     latex = print_latex_expression(result_key)
103 |     assert "1000000" in latex or "10^{6}" in latex
104 | 
105 |     # Test with a complex expression involving scaling
106 |     expr_key = introduce_expression(
107 |         "speed_of_light * 10**-9", expr_var_name="speed_in_nm_per_s"
108 |     )
109 |     latex = print_latex_expression(expr_key)
110 |     # The output might be formatted as \frac{c}{1000000000} or similar
111 |     assert "\\text{c}" in latex and (
112 |         "10^{-9}" in latex or "1000000000" in latex or "\\frac" in latex
113 |     )
114 | 
```

--------------------------------------------------------------------------------
/vars.py:
--------------------------------------------------------------------------------

```python
  1 | from enum import Enum
  2 | 
  3 | 
  4 | class Assumption(Enum):
  5 |     ALGEBRAIC = "algebraic"
  6 |     COMMUTATIVE = "commutative"
  7 |     COMPLEX = "complex"
  8 |     EXTENDED_NEGATIVE = "extended_negative"
  9 |     EXTENDED_NONNEGATIVE = "extended_nonnegative"
 10 |     EXTENDED_NONPOSITIVE = "extended_nonpositive"
 11 |     EXTENDED_NONZERO = "extended_nonzero"
 12 |     EXTENDED_POSITIVE = "extended_positive"
 13 |     EXTENDED_REAL = "extended_real"
 14 |     FINITE = "finite"
 15 |     HERMITIAN = "hermitian"
 16 |     IMAGINARY = "imaginary"
 17 |     INFINITE = "infinite"
 18 |     INTEGER = "integer"
 19 |     IRATIONAL = "irrational"
 20 |     NEGATIVE = "negative"
 21 |     NONINTEGER = "noninteger"
 22 |     NONNEGATIVE = "nonnegative"
 23 |     NONPOSITIVE = "nonpositive"
 24 |     NONZERO = "nonzero"
 25 |     POSITIVE = "positive"
 26 |     RATIONAL = "rational"
 27 |     REAL = "real"
 28 |     TRANSCENDENTAL = "transcendental"
 29 |     ZERO = "zero"
 30 | 
 31 | 
 32 | class Domain(Enum):
 33 |     COMPLEX = "complex"
 34 |     REAL = "real"
 35 |     INTEGERS = "integers"
 36 |     NATURALS = "naturals"
 37 | 
 38 | 
 39 | class ODEHint(Enum):
 40 |     FACTORABLE = "factorable"
 41 |     NTH_ALGEBRAIC = "nth_algebraic"
 42 |     SEPARABLE = "separable"
 43 |     FIRST_EXACT = "1st_exact"
 44 |     FIRST_LINEAR = "1st_linear"
 45 |     BERNOULLI = "Bernoulli"
 46 |     FIRST_RATIONAL_RICCATI = "1st_rational_riccati"
 47 |     RICCATI_SPECIAL_MINUS2 = "Riccati_special_minus2"
 48 |     FIRST_HOMOGENEOUS_COEFF_BEST = "1st_homogeneous_coeff_best"
 49 |     FIRST_HOMOGENEOUS_COEFF_SUBS_INDEP_DIV_DEP = (
 50 |         "1st_homogeneous_coeff_subs_indep_div_dep"
 51 |     )
 52 |     FIRST_HOMOGENEOUS_COEFF_SUBS_DEP_DIV_INDEP = (
 53 |         "1st_homogeneous_coeff_subs_dep_div_indep"
 54 |     )
 55 |     ALMOST_LINEAR = "almost_linear"
 56 |     LINEAR_COEFFICIENTS = "linear_coefficients"
 57 |     SEPARABLE_REDUCED = "separable_reduced"
 58 |     FIRST_POWER_SERIES = "1st_power_series"
 59 |     LIE_GROUP = "lie_group"
 60 |     NTH_LINEAR_CONSTANT_COEFF_HOMOGENEOUS = "nth_linear_constant_coeff_homogeneous"
 61 |     NTH_LINEAR_EULER_EQ_HOMOGENEOUS = "nth_linear_euler_eq_homogeneous"
 62 |     NTH_LINEAR_CONSTANT_COEFF_UNDETERMINED_COEFFICIENTS = (
 63 |         "nth_linear_constant_coeff_undetermined_coefficients"
 64 |     )
 65 |     NTH_LINEAR_EULER_EQ_NONHOMOGENEOUS_UNDETERMINED_COEFFICIENTS = (
 66 |         "nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients"
 67 |     )
 68 |     NTH_LINEAR_CONSTANT_COEFF_VARIATION_OF_PARAMETERS = (
 69 |         "nth_linear_constant_coeff_variation_of_parameters"
 70 |     )
 71 |     NTH_LINEAR_EULER_EQ_NONHOMOGENEOUS_VARIATION_OF_PARAMETERS = (
 72 |         "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters"
 73 |     )
 74 |     LIOUVILLE = "Liouville"
 75 |     SECOND_LINEAR_AIRY = "2nd_linear_airy"
 76 |     SECOND_LINEAR_BESSEL = "2nd_linear_bessel"
 77 |     SECOND_HYPERGEOMETRIC = "2nd_hypergeometric"
 78 |     SECOND_HYPERGEOMETRIC_INTEGRAL = "2nd_hypergeometric_Integral"
 79 |     NTH_ORDER_REDUCIBLE = "nth_order_reducible"
 80 |     SECOND_POWER_SERIES_ORDINARY = "2nd_power_series_ordinary"
 81 |     SECOND_POWER_SERIES_REGULAR = "2nd_power_series_regular"
 82 |     NTH_ALGEBRAIC_INTEGRAL = "nth_algebraic_Integral"
 83 |     SEPARABLE_INTEGRAL = "separable_Integral"
 84 |     FIRST_EXACT_INTEGRAL = "1st_exact_Integral"
 85 |     FIRST_LINEAR_INTEGRAL = "1st_linear_Integral"
 86 |     BERNOULLI_INTEGRAL = "Bernoulli_Integral"
 87 |     FIRST_HOMOGENEOUS_COEFF_SUBS_INDEP_DIV_DEP_INTEGRAL = (
 88 |         "1st_homogeneous_coeff_subs_indep_div_dep_Integral"
 89 |     )
 90 |     FIRST_HOMOGENEOUS_COEFF_SUBS_DEP_DIV_INDEP_INTEGRAL = (
 91 |         "1st_homogeneous_coeff_subs_dep_div_indep_Integral"
 92 |     )
 93 |     ALMOST_LINEAR_INTEGRAL = "almost_linear_Integral"
 94 |     LINEAR_COEFFICIENTS_INTEGRAL = "linear_coefficients_Integral"
 95 |     SEPARABLE_REDUCED_INTEGRAL = "separable_reduced_Integral"
 96 |     NTH_LINEAR_CONSTANT_COEFF_VARIATION_OF_PARAMETERS_INTEGRAL = (
 97 |         "nth_linear_constant_coeff_variation_of_parameters_Integral"
 98 |     )
 99 |     NTH_LINEAR_EULER_EQ_NONHOMOGENEOUS_VARIATION_OF_PARAMETERS_INTEGRAL = (
100 |         "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters_Integral"
101 |     )
102 |     LIOUVILLE_INTEGRAL = "Liouville_Integral"
103 |     SECOND_NONLINEAR_AUTONOMOUS_CONSERVED = "2nd_nonlinear_autonomous_conserved"
104 |     SECOND_NONLINEAR_AUTONOMOUS_CONSERVED_INTEGRAL = (
105 |         "2nd_nonlinear_autonomous_conserved_Integral"
106 |     )
107 | 
108 | 
109 | class PDEHint(Enum):
110 |     FIRST_LINEAR_CONSTANT_COEFF_HOMOGENEOUS = "1st_linear_constant_coeff_homogeneous"
111 |     FIRST_LINEAR_CONSTANT_COEFF = "1st_linear_constant_coeff"
112 |     FIRST_LINEAR_CONSTANT_COEFF_INTEGRAL = "1st_linear_constant_coeff_Integral"
113 |     FIRST_LINEAR_VARIABLE_COEFF = "1st_linear_variable_coeff"
114 | 
115 | 
116 | class Metric(Enum):
117 |     ALCUBIERRE_WARP = "AlcubierreWarp"
118 |     BARRIOLA_VILEKIN = "BarriolaVilekin"
119 |     BERTOTTI_KASNER = "BertottiKasner"
120 |     BESSEL_GRAVITATIONAL_WAVE = "BesselGravitationalWave"
121 |     C_METRIC = "CMetric"
122 |     DAVIDSON = "Davidson"
123 |     ANTI_DE_SITTER = "AntiDeSitter"
124 |     ANTI_DE_SITTER_STATIC = "AntiDeSitterStatic"
125 |     DE_SITTER = "DeSitter"
126 |     ERNST = "Ernst"
127 |     GODEL = "Godel"
128 |     JANIS_NEWMAN_WINICOUR = "JanisNewmanWinicour"
129 |     MINKOWSKI = "Minkowski"
130 |     MINKOWSKI_CARTESIAN = "MinkowskiCartesian"
131 |     MINKOWSKI_POLAR = "MinkowskiPolar"
132 |     KERR = "Kerr"
133 |     KERR_NEWMAN = "KerrNewman"
134 |     REISSNER_NORDSTROM = "ReissnerNordstorm"
135 |     SCHWARZSCHILD = "Schwarzschild"
136 | 
137 | 
138 | class Tensor(Enum):
139 |     RICCI_SCALAR = "RicciScalar"
140 |     RICCI_TENSOR = "RicciTensor"
141 |     RIEMANN_CURVATURE_TENSOR = "RiemannCurvatureTensor"
142 |     SCHOUTEN_TENSOR = "SchoutenTensor"
143 |     STRESS_ENERGY_MOMENTUM_TENSOR = "StressEnergyMomentumTensor"
144 |     WEYL_TENSOR = "WeylTensor"
145 |     EINSTEIN_TENSOR = "EinsteinTensor"
146 | 
147 | 
148 | class UnitSystem(Enum):
149 |     MKS = "MKS"
150 |     MKSA = "MKSA"
151 |     NATURAL = "natural"
152 |     SI = "SI"
153 |     CGS = "cgs"
154 | 
```

--------------------------------------------------------------------------------
/tests/test_relativity.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | from server import (
  3 |     create_predefined_metric,
  4 |     search_predefined_metrics,
  5 |     calculate_tensor,
  6 |     create_custom_metric,
  7 |     print_latex_tensor,
  8 |     local_vars,
  9 |     expressions,
 10 |     metrics,
 11 |     tensor_objects,
 12 |     EINSTEINPY_AVAILABLE,
 13 | )
 14 | from vars import Metric, Tensor
 15 | 
 16 | 
 17 | # Skip all tests if EinsteinPy is not available
 18 | pytestmark = pytest.mark.skipif(
 19 |     not EINSTEINPY_AVAILABLE, reason="EinsteinPy library is not available"
 20 | )
 21 | 
 22 | 
 23 | # Add a fixture to reset global state between tests
 24 | @pytest.fixture(autouse=True)
 25 | def reset_globals():
 26 |     # Clear global dictionaries before each test
 27 |     local_vars.clear()
 28 |     expressions.clear()
 29 |     if EINSTEINPY_AVAILABLE:
 30 |         metrics.clear()
 31 |         tensor_objects.clear()
 32 |     # Reset the expression counter
 33 |     import server
 34 | 
 35 |     server.expression_counter = 0
 36 |     yield
 37 | 
 38 | 
 39 | class TestCreatePredefinedMetric:
 40 |     def test_create_schwarzschild_metric(self):
 41 |         # Test creating a Schwarzschild metric
 42 |         result = create_predefined_metric(Metric.SCHWARZSCHILD)
 43 |         assert result == "metric_Schwarzschild"
 44 |         assert result in metrics
 45 |         assert result in expressions
 46 | 
 47 |     def test_create_minkowski_metric(self):
 48 |         # Test creating a Minkowski metric
 49 |         result = create_predefined_metric(Metric.MINKOWSKI)
 50 |         assert result == "metric_Minkowski"
 51 |         assert result in metrics
 52 |         assert result in expressions
 53 | 
 54 |     def test_create_kerr_metric(self):
 55 |         # Test creating a Kerr metric
 56 |         result = create_predefined_metric(Metric.KERR)
 57 |         assert result == "metric_Kerr"
 58 |         assert result in metrics
 59 |         assert result in expressions
 60 | 
 61 |     def test_invalid_metric(self):
 62 |         # Try to create a metric that's in the enum but not implemented
 63 |         # For this test, we'll assume ALCUBIERRE_WARP is not implemented
 64 |         # in the provided metric_map
 65 |         result = create_predefined_metric(Metric.ALCUBIERRE_WARP)
 66 |         assert "Error" in result
 67 |         assert "not implemented" in result
 68 | 
 69 | 
 70 | class TestSearchPredefinedMetrics:
 71 |     def test_search_with_results(self):
 72 |         # Search for metrics containing "Sitter"
 73 |         result = search_predefined_metrics("Sitter")
 74 |         assert "Found metrics" in result
 75 |         assert "DeSitter" in result or "AntiDeSitter" in result
 76 | 
 77 |     def test_search_no_results(self):
 78 |         # Search for a term unlikely to match any metric
 79 |         result = search_predefined_metrics("XYZ123")
 80 |         assert "No metrics found" in result
 81 | 
 82 | 
 83 | class TestCalculateTensor:
 84 |     def test_calculate_ricci_tensor(self):
 85 |         # First create a metric
 86 |         metric_key = create_predefined_metric(Metric.SCHWARZSCHILD)
 87 | 
 88 |         # Calculate Ricci tensor
 89 |         result = calculate_tensor(metric_key, Tensor.RICCI_TENSOR)
 90 |         assert result == f"riccitensor_{metric_key}"
 91 |         assert result in expressions
 92 | 
 93 |     def test_calculate_ricci_scalar(self):
 94 |         # First create a metric
 95 |         metric_key = create_predefined_metric(Metric.SCHWARZSCHILD)
 96 | 
 97 |         # Calculate Ricci scalar
 98 |         result = calculate_tensor(metric_key, Tensor.RICCI_SCALAR)
 99 |         assert result == f"ricciscalar_{metric_key}"
100 |         assert result in expressions
101 | 
102 |     def test_calculate_einstein_tensor(self):
103 |         # First create a metric
104 |         metric_key = create_predefined_metric(Metric.SCHWARZSCHILD)
105 | 
106 |         # Calculate Einstein tensor
107 |         result = calculate_tensor(metric_key, Tensor.EINSTEIN_TENSOR)
108 |         assert result == f"einsteintensor_{metric_key}"
109 |         assert result in expressions
110 | 
111 |     def test_invalid_metric_key(self):
112 |         result = calculate_tensor("nonexistent_metric", Tensor.RICCI_TENSOR)
113 |         assert "Error" in result
114 |         assert "not found" in result
115 | 
116 |     def test_invalid_tensor_type(self):
117 |         # First create a metric
118 |         metric_key = create_predefined_metric(Metric.SCHWARZSCHILD)
119 | 
120 |         # Try to calculate a tensor that's in the enum but not implemented
121 |         # This test assumes there's at least one tensor type that's not in the tensor_map
122 |         # If all enums are implemented, this test might need adjustment
123 |         class TestEnum:
124 |             value = "NonExistentTensor"
125 | 
126 |         result = calculate_tensor(metric_key, TestEnum())
127 |         assert "Error" in result
128 |         # Check either for "not implemented" or the attribute error message
129 |         assert "not implemented" in result or "has no attribute" in result
130 | 
131 | 
132 | class TestCreateCustomMetric:
133 |     def test_create_custom_metric(self):
134 |         # Create a simple 2x2 diagonal metric with symbols t, r
135 |         components = [["-1", "0"], ["0", "1"]]
136 |         symbols = ["t", "r"]
137 | 
138 |         result = create_custom_metric(components, symbols)
139 |         assert result == "metric_custom_0"
140 |         assert result in metrics
141 |         assert result in expressions
142 | 
143 |     def test_create_custom_minkowski(self):
144 |         # Create a 4x4 Minkowski metric (-1, 1, 1, 1)
145 |         components = [
146 |             ["-1", "0", "0", "0"],
147 |             ["0", "1", "0", "0"],
148 |             ["0", "0", "1", "0"],
149 |             ["0", "0", "0", "1"],
150 |         ]
151 |         symbols = ["t", "x", "y", "z"]
152 | 
153 |         result = create_custom_metric(components, symbols)
154 |         assert result == "metric_custom_0"
155 |         assert result in metrics
156 |         assert result in expressions
157 | 
158 |     def test_create_custom_metric_with_expressions(self):
159 |         # Create a metric with symbolic expressions
160 |         components = [
161 |             ["-1", "0", "0", "0"],
162 |             ["0", "r**2", "0", "0"],
163 |             ["0", "0", "r**2 * sin(theta)**2", "0"],
164 |             ["0", "0", "0", "1"],
165 |         ]
166 |         symbols = ["t", "r", "theta", "phi"]
167 | 
168 |         result = create_custom_metric(components, symbols)
169 |         assert result == "metric_custom_0"
170 |         assert result in metrics
171 |         assert result in expressions
172 | 
173 |     def test_invalid_components(self):
174 |         # Test with invalid components (not a matrix)
175 |         components = [["1", "0"], ["0"]]  # Missing element in second row
176 |         symbols = ["t", "r"]
177 | 
178 |         result = create_custom_metric(components, symbols)
179 |         assert "Error" in result
180 | 
181 | 
182 | class TestPrintLatexTensor:
183 |     def test_print_metric_latex(self):
184 |         # Create a metric and print it in LaTeX
185 |         metric_key = create_predefined_metric(Metric.MINKOWSKI)
186 | 
187 |         result = print_latex_tensor(metric_key)
188 |         assert result  # Should return a non-empty string
189 |         assert "\\begin{pmatrix}" in result or "\\left[" in result
190 | 
191 |     def test_print_tensor_latex(self):
192 |         # Create a metric, calculate a tensor, and print it in LaTeX
193 |         metric_key = create_predefined_metric(Metric.SCHWARZSCHILD)
194 |         tensor_key = calculate_tensor(metric_key, Tensor.RICCI_TENSOR)
195 | 
196 |         result = print_latex_tensor(tensor_key)
197 |         assert result  # Should return a non-empty string
198 | 
199 |     def test_nonexistent_tensor(self):
200 |         result = print_latex_tensor("nonexistent_tensor")
201 |         assert "Error" in result
202 |         assert "not found" in result
203 | 
```

--------------------------------------------------------------------------------
/tests/test_calculus.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | from server import (
  3 |     intro,
  4 |     introduce_expression,
  5 |     differentiate_expression,
  6 |     integrate_expression,
  7 |     create_coordinate_system,
  8 |     create_vector_field,
  9 |     calculate_curl,
 10 |     calculate_divergence,
 11 |     calculate_gradient,
 12 |     print_latex_expression,
 13 |     local_vars,
 14 |     expressions,
 15 |     coordinate_systems,
 16 | )
 17 | from vars import Assumption
 18 | 
 19 | 
 20 | # Add a fixture to reset global state between tests
 21 | @pytest.fixture(autouse=True)
 22 | def reset_globals():
 23 |     # Clear global dictionaries before each test
 24 |     local_vars.clear()
 25 |     expressions.clear()
 26 |     coordinate_systems.clear()
 27 |     # Reset the expression counter
 28 |     import server
 29 | 
 30 |     server.expression_counter = 0
 31 |     yield
 32 | 
 33 | 
 34 | class TestDifferentiateExpressionTool:
 35 |     def test_differentiate_polynomial(self):
 36 |         # Introduce a variable
 37 |         intro("x", [Assumption.REAL], [])
 38 | 
 39 |         # Create an expression: x^3
 40 |         expr_key = introduce_expression("x**3")
 41 | 
 42 |         # First derivative
 43 |         first_deriv_key = differentiate_expression(expr_key, "x")
 44 |         first_deriv_latex = print_latex_expression(first_deriv_key)
 45 | 
 46 |         # Should be 3x^2
 47 |         assert "3" in first_deriv_latex
 48 |         assert "x^{2}" in first_deriv_latex
 49 | 
 50 |         # Second derivative
 51 |         second_deriv_key = differentiate_expression(expr_key, "x", 2)
 52 |         second_deriv_latex = print_latex_expression(second_deriv_key)
 53 | 
 54 |         # Should be 6x
 55 |         assert "6" in second_deriv_latex
 56 |         assert "x" in second_deriv_latex
 57 | 
 58 |         # Third derivative
 59 |         third_deriv_key = differentiate_expression(expr_key, "x", 3)
 60 |         third_deriv_latex = print_latex_expression(third_deriv_key)
 61 | 
 62 |         # Should be 6
 63 |         assert "6" in third_deriv_latex
 64 | 
 65 |     def test_differentiate_trigonometric(self):
 66 |         # Introduce a variable
 67 |         intro("x", [Assumption.REAL], [])
 68 | 
 69 |         # Create sin(x) expression
 70 |         sin_key = introduce_expression("sin(x)")
 71 | 
 72 |         # Derivative of sin(x) is cos(x)
 73 |         deriv_key = differentiate_expression(sin_key, "x")
 74 |         deriv_latex = print_latex_expression(deriv_key)
 75 | 
 76 |         assert "\\cos" in deriv_latex
 77 | 
 78 |     def test_nonexistent_expression(self):
 79 |         intro("x", [Assumption.REAL], [])
 80 |         result = differentiate_expression("nonexistent_key", "x")
 81 |         assert "error" in result.lower()
 82 | 
 83 |     def test_nonexistent_variable(self):
 84 |         intro("x", [Assumption.REAL], [])
 85 |         expr_key = introduce_expression("x**2")
 86 |         result = differentiate_expression(expr_key, "y")
 87 |         assert "error" in result.lower()
 88 | 
 89 | 
 90 | class TestIntegrateExpressionTool:
 91 |     def test_indefinite_integral_polynomial(self):
 92 |         # Introduce a variable
 93 |         intro("x", [Assumption.REAL], [])
 94 | 
 95 |         # Create expression: x^2
 96 |         expr_key = introduce_expression("x**2")
 97 | 
 98 |         # Integrate
 99 |         integral_key = integrate_expression(expr_key, "x")
100 |         integral_latex = print_latex_expression(integral_key)
101 | 
102 |         # Should be x^3/3
103 |         assert "x^{3}" in integral_latex
104 |         assert "3" in integral_latex
105 | 
106 |     def test_indefinite_integral_trigonometric(self):
107 |         # Introduce a variable
108 |         intro("x", [Assumption.REAL], [])
109 | 
110 |         # Create expression: cos(x)
111 |         expr_key = introduce_expression("cos(x)")
112 | 
113 |         # Integrate
114 |         integral_key = integrate_expression(expr_key, "x")
115 |         integral_latex = print_latex_expression(integral_key)
116 | 
117 |         # Should be sin(x)
118 |         assert "\\sin" in integral_latex
119 | 
120 |     def test_nonexistent_expression(self):
121 |         intro("x", [Assumption.REAL], [])
122 |         result = integrate_expression("nonexistent_key", "x")
123 |         assert "error" in result.lower()
124 | 
125 |     def test_nonexistent_variable(self):
126 |         intro("x", [Assumption.REAL], [])
127 |         expr_key = introduce_expression("x**2")
128 |         result = integrate_expression(expr_key, "y")
129 |         assert "error" in result.lower()
130 | 
131 | 
132 | class TestVectorOperations:
133 |     def test_create_coordinate_system(self):
134 |         # Create coordinate system
135 |         result = create_coordinate_system("R")
136 |         assert result == "R"
137 |         assert "R" in coordinate_systems
138 | 
139 |     def test_create_custom_coordinate_system(self):
140 |         # Create coordinate system with custom names
141 |         result = create_coordinate_system("C", ["rho", "phi", "z"])
142 |         assert result == "C"
143 |         assert "C" in coordinate_systems
144 | 
145 |     def test_create_vector_field(self):
146 |         # Create coordinate system
147 |         create_coordinate_system("R")
148 | 
149 |         # Introduce variables to represent components
150 |         intro("x", [Assumption.REAL], [])
151 |         intro("y", [Assumption.REAL], [])
152 |         intro("z", [Assumption.REAL], [])
153 | 
154 |         # Create vector field F = (y, -x, z)
155 |         vector_field_key = create_vector_field("R", "y", "-x", "z")
156 | 
157 |         # The key might be an error message if the test is failing
158 |         if "error" not in vector_field_key.lower():
159 |             assert vector_field_key.startswith("vector_")
160 |         else:
161 |             assert False, f"Failed to create vector field: {vector_field_key}"
162 | 
163 |     def test_calculate_curl(self):
164 |         # Create coordinate system
165 |         create_coordinate_system("R")
166 | 
167 |         # Introduce variables
168 |         intro("x", [Assumption.REAL], [])
169 |         intro("y", [Assumption.REAL], [])
170 | 
171 |         # Create a simple vector field for curl calculation
172 |         vector_field_key = create_vector_field("R", "y", "-x", "0")
173 | 
174 |         # Check if vector field was created successfully
175 |         if "error" in vector_field_key.lower():
176 |             assert False, f"Failed to create vector field: {vector_field_key}"
177 | 
178 |         # Calculate curl
179 |         curl_key = calculate_curl(vector_field_key)
180 | 
181 |         # Check if curl calculation was successful
182 |         if "error" not in curl_key.lower():
183 |             assert curl_key.startswith("vector_")
184 |         else:
185 |             assert False, f"Failed to calculate curl: {curl_key}"
186 | 
187 |     def test_calculate_divergence(self):
188 |         # Create coordinate system
189 |         create_coordinate_system("R")
190 | 
191 |         # Introduce variables
192 |         intro("x", [Assumption.REAL], [])
193 |         intro("y", [Assumption.REAL], [])
194 |         intro("z", [Assumption.REAL], [])
195 | 
196 |         # Create a simple identity vector field
197 |         vector_field_key = create_vector_field("R", "x", "y", "z")
198 | 
199 |         # Check if vector field was created successfully
200 |         if "error" in vector_field_key.lower():
201 |             assert False, f"Failed to create vector field: {vector_field_key}"
202 | 
203 |         # Calculate divergence - should be 0 because symbols have no dependency on coordinates
204 |         div_key = calculate_divergence(vector_field_key)
205 | 
206 |         # Check if divergence calculation was successful
207 |         if "error" in div_key.lower():
208 |             assert False, f"Failed to calculate divergence: {div_key}"
209 | 
210 |         div_latex = print_latex_expression(div_key)
211 | 
212 |         # Check result - should be 0
213 |         assert "0" in div_latex
214 | 
215 |     def test_calculate_gradient(self):
216 |         # Create coordinate system
217 |         create_coordinate_system("R")
218 | 
219 |         # Introduce variables
220 |         intro("x", [Assumption.REAL], [])
221 |         intro("y", [Assumption.REAL], [])
222 |         intro("z", [Assumption.REAL], [])
223 | 
224 |         # Create a simple scalar field
225 |         scalar_field_key = introduce_expression("x**2 + y**2 + z**2")
226 | 
227 |         # Calculate gradient
228 |         grad_key = calculate_gradient(scalar_field_key)
229 | 
230 |         # Check if gradient calculation was successful
231 |         if "error" not in grad_key.lower():
232 |             assert grad_key.startswith("vector_")
233 |         else:
234 |             assert False, f"Failed to calculate gradient: {grad_key}"
235 | 
236 |     def test_nonexistent_coordinate_system(self):
237 |         result = create_vector_field("NonExistent", "x", "y", "z")
238 |         assert "error" in result.lower()
239 | 
240 |     def test_nonexistent_vector_field(self):
241 |         result = calculate_curl("nonexistent_key")
242 |         assert "error" in result.lower()
243 | 
```

--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | from server import (
  3 |     intro,
  4 |     intro_many,
  5 |     introduce_expression,
  6 |     print_latex_expression,
  7 |     solve_algebraically,
  8 |     solve_linear_system,
  9 |     solve_nonlinear_system,
 10 |     introduce_function,
 11 |     dsolve_ode,
 12 |     pdsolve_pde,
 13 |     local_vars,
 14 |     expressions,
 15 |     functions,
 16 |     VariableDefinition,
 17 | )
 18 | from vars import Assumption, Domain, ODEHint
 19 | 
 20 | 
 21 | # Add a fixture to reset global state between tests
 22 | @pytest.fixture(autouse=True)
 23 | def reset_globals():
 24 |     # Clear global dictionaries before each test
 25 |     local_vars.clear()
 26 |     expressions.clear()
 27 |     functions.clear()  # Add this to clear the functions dictionary as well
 28 |     # Reset the expression counter
 29 |     import server
 30 | 
 31 |     server.expression_counter = 0
 32 |     yield
 33 | 
 34 | 
 35 | class TestIntroTool:
 36 |     def test_intro_basic(self):
 37 |         # Test introducing a variable with no assumptions
 38 |         result = intro("x", [], [])
 39 |         assert result == "x"
 40 |         assert "x" in local_vars
 41 | 
 42 |     def test_intro_with_assumptions(self):
 43 |         # Test introducing a variable with assumptions
 44 |         result = intro("y", [Assumption.REAL, Assumption.POSITIVE], [])
 45 |         assert result == "y"
 46 |         assert "y" in local_vars
 47 |         # Check that the symbol has the correct assumptions
 48 |         assert local_vars["y"].is_real is True
 49 |         assert local_vars["y"].is_positive is True
 50 | 
 51 |     def test_intro_inconsistent_assumptions(self):
 52 |         # Test introducing a variable with inconsistent assumptions
 53 |         # For example, a number can't be both positive and negative
 54 |         result = intro("z", [Assumption.POSITIVE], [])
 55 |         assert result == "z"
 56 |         assert "z" in local_vars
 57 | 
 58 |         # Now try to create inconsistent assumptions with another variable
 59 |         # Positive and non-positive are inconsistent
 60 |         result2 = intro(
 61 |             "inconsistent", [Assumption.POSITIVE, Assumption.NONPOSITIVE], []
 62 |         )
 63 |         assert "error" in result2.lower() or "inconsistent" in result2.lower()
 64 |         assert "inconsistent" not in local_vars
 65 | 
 66 | 
 67 | class TestIntroManyTool:
 68 |     def test_intro_many_basic(self):
 69 |         # Define variable definition objects using the VariableDefinition class
 70 |         var_defs = [
 71 |             VariableDefinition(
 72 |                 var_name="a", pos_assumptions=["real"], neg_assumptions=[]
 73 |             ),
 74 |             VariableDefinition(
 75 |                 var_name="b", pos_assumptions=["positive"], neg_assumptions=[]
 76 |             ),
 77 |         ]
 78 | 
 79 |         intro_many(var_defs)
 80 |         assert "a" in local_vars
 81 |         assert "b" in local_vars
 82 |         assert local_vars["a"].is_real is True
 83 |         assert local_vars["b"].is_positive is True
 84 | 
 85 |     def test_intro_many_invalid_assumption(self):
 86 |         # Create variable definition with an invalid assumption
 87 |         var_defs = [
 88 |             VariableDefinition(
 89 |                 var_name="c", pos_assumptions=["invalid_assumption"], neg_assumptions=[]
 90 |             ),
 91 |         ]
 92 | 
 93 |         result = intro_many(var_defs)
 94 |         assert "error" in result.lower()
 95 | 
 96 | 
 97 | class TestIntroduceExpressionTool:
 98 |     def test_introduce_simple_expression(self):
 99 |         # First, introduce required variables
100 |         intro("x", [], [])
101 |         intro("y", [], [])
102 | 
103 |         # Then introduce an expression
104 |         result = introduce_expression("x + y")
105 |         assert result == "expr_0"
106 |         assert "expr_0" in expressions
107 |         assert str(expressions["expr_0"]) == "x + y"
108 | 
109 |     def test_introduce_equation(self):
110 |         intro("x", [], [])
111 | 
112 |         result = introduce_expression("Eq(x**2, 4)")
113 |         assert result == "expr_0"
114 |         assert "expr_0" in expressions
115 |         # Equation should be x**2 = 4
116 | 
117 |         assert expressions["expr_0"].lhs == local_vars["x"] ** 2
118 |         assert expressions["expr_0"].rhs == 4
119 | 
120 |     def test_introduce_matrix(self):
121 |         result = introduce_expression("Matrix(((1, 2), (3, 4)))")
122 |         assert result == "expr_0"
123 |         assert "expr_0" in expressions
124 |         # Check matrix dimensions and values
125 |         assert expressions["expr_0"].shape == (2, 2)
126 |         assert expressions["expr_0"][0, 0] == 1
127 |         assert expressions["expr_0"][1, 1] == 4
128 | 
129 | 
130 | class TestPrintLatexExpressionTool:
131 |     def test_print_latex_simple_expression(self):
132 |         intro("x", [Assumption.REAL], [])
133 |         expr_key = introduce_expression("x**2 + 5*x + 6")
134 | 
135 |         result = print_latex_expression(expr_key)
136 |         assert "x^{2} + 5 x + 6" in result
137 |         assert "real" in result.lower()
138 | 
139 |     def test_print_latex_nonexistent_expression(self):
140 |         result = print_latex_expression("nonexistent_key")
141 |         assert "error" in result.lower()
142 | 
143 | 
144 | class TestSolveAlgebraicallyTool:
145 |     def test_solve_quadratic(self):
146 |         intro("x", [Assumption.REAL], [])
147 |         expr_key = introduce_expression("Eq(x**2 - 5*x + 6, 0)")
148 | 
149 |         result = solve_algebraically(expr_key, "x")
150 |         # Solution should contain the values 2 and 3
151 |         assert "2" in result
152 |         assert "3" in result
153 | 
154 |     def test_solve_with_domain(self):
155 |         intro("x", [Assumption.REAL], [])
156 |         # Try a clearer example: x^2 + 1 = 0 directly as an expression
157 |         expr_key = introduce_expression("x**2 + 1")
158 | 
159 |         # In complex domain, should have solutions i and -i
160 |         complex_result = solve_algebraically(expr_key, "x", Domain.COMPLEX)
161 |         assert "i" in complex_result
162 | 
163 |         # In real domain, should have empty set
164 |         real_result = solve_algebraically(expr_key, "x", Domain.REAL)
165 |         assert "\\emptyset" in real_result
166 | 
167 |     def test_solve_invalid_domain(self):
168 |         intro("x", [], [])
169 |         introduce_expression("x**2 - 4")
170 |         # We can't really test with an invalid Domain enum value easily,
171 |         # so we'll skip this test since it's handled by type checking
172 |         # If needed, could test with a mock Domain object that's not in the map
173 | 
174 |     def test_solve_nonexistent_expression(self):
175 |         intro("x", [], [])
176 |         result = solve_algebraically("nonexistent_key", "x")
177 |         assert "error" in result.lower()
178 | 
179 |     def test_solve_nonexistent_variable(self):
180 |         intro("x", [], [])
181 |         expr_key = introduce_expression("x**2 - 4")
182 |         result = solve_algebraically(expr_key, "y")
183 |         assert "error" in result.lower()
184 | 
185 | 
186 | class TestSolveLinearSystemTool:
187 |     def test_simple_linear_system(self):
188 |         # Create variables
189 |         intro("x", [Assumption.REAL], [])
190 |         intro("y", [Assumption.REAL], [])
191 | 
192 |         # Create a system of linear equations: x + y = 10, 2x - y = 5
193 |         eq1 = introduce_expression("Eq(x + y, 10)")
194 |         eq2 = introduce_expression("Eq(2*x - y, 5)")
195 | 
196 |         # Solve the system
197 |         result = solve_linear_system([eq1, eq2], ["x", "y"])
198 | 
199 |         # Check if solution contains the expected values (x=5, y=5)
200 |         assert "5" in result
201 | 
202 |     def test_inconsistent_system(self):
203 |         # Create variables
204 |         intro("x", [Assumption.REAL], [])
205 |         intro("y", [Assumption.REAL], [])
206 | 
207 |         # Create an inconsistent system: x + y = 1, x + y = 2
208 |         eq1 = introduce_expression("Eq(x + y, 1)")
209 |         eq2 = introduce_expression("Eq(x + y, 2)")
210 | 
211 |         # Solve the system
212 |         result = solve_linear_system([eq1, eq2], ["x", "y"])
213 | 
214 |         # Should be empty set
215 |         assert "\\emptyset" in result
216 | 
217 |     def test_nonexistent_expression(self):
218 |         intro("x", [], [])
219 |         intro("y", [], [])
220 |         result = solve_linear_system(["nonexistent_key"], ["x", "y"])
221 |         assert "error" in result.lower()
222 | 
223 |     def test_nonexistent_variable(self):
224 |         intro("x", [], [])
225 |         expr_key = introduce_expression("x**2 - 4")
226 |         result = solve_linear_system([expr_key], ["y"])
227 |         assert "error" in result.lower()
228 | 
229 | 
230 | class TestSolveNonlinearSystemTool:
231 |     def test_simple_nonlinear_system(self):
232 |         # Create variables
233 |         intro("x", [Assumption.REAL], [])
234 |         intro("y", [Assumption.REAL], [])
235 | 
236 |         # Create a system of nonlinear equations: x^2 + y^2 = 25, x*y = 12
237 |         eq1 = introduce_expression("Eq(x**2 + y**2, 25)")
238 |         eq2 = introduce_expression("Eq(x*y, 12)")
239 | 
240 |         # Solve the system
241 |         result = solve_nonlinear_system([eq1, eq2], ["x", "y"])
242 | 
243 |         # Should find two pairs of solutions (±3, ±4) and (±4, ±3)
244 |         # The exact format can vary, so we just check for the presence of 3 and 4
245 |         assert "3" in result
246 |         assert "4" in result
247 | 
248 |     def test_with_domain(self):
249 |         # Create variables - importantly, not specifying REAL assumption
250 |         # because we want to test complex solutions
251 |         intro("x", [], [])
252 |         intro("y", [], [])
253 | 
254 |         # Create a system with complex solutions: x^2 + y^2 = -1, y = x
255 |         # This has no real solutions but has complex solutions
256 |         eq1 = introduce_expression("Eq(x**2 + y**2, -1)")
257 |         eq2 = introduce_expression("Eq(y, x)")
258 | 
259 |         # In complex domain - should have solutions with imaginary parts
260 |         complex_result = solve_nonlinear_system([eq1, eq2], ["x", "y"], Domain.COMPLEX)
261 |         assert "i" in complex_result
262 | 
263 |         # In real domain - now simply verifies we get a result (even if it contains complex solutions)
264 |         # The user is responsible for knowing that solutions might be complex
265 |         real_result = solve_nonlinear_system([eq1, eq2], ["x", "y"], Domain.REAL)
266 |         assert real_result  # Just verify we get some result
267 | 
268 |     def test_nonexistent_expression(self):
269 |         intro("x", [], [])
270 |         intro("y", [], [])
271 |         result = solve_nonlinear_system(["nonexistent_key"], ["x", "y"])
272 |         assert "error" in result.lower()
273 | 
274 |     def test_nonexistent_variable(self):
275 |         intro("x", [], [])
276 |         expr_key = introduce_expression("x**2 - 4")
277 |         result = solve_nonlinear_system([expr_key], ["z"])
278 |         assert "error" in result.lower()
279 | 
280 | 
281 | class TestIntroduceFunctionTool:
282 |     def test_introduce_function_basic(self):
283 |         # Test introducing a function variable
284 |         result = introduce_function("f")
285 |         assert result == "f"
286 |         assert "f" in functions
287 |         assert str(functions["f"]) == "f"
288 | 
289 |     def test_function_usage_in_expression(self):
290 |         # Introduce a variable and a function
291 |         intro("x", [Assumption.REAL], [])
292 |         introduce_function("f")
293 | 
294 |         # Create an expression using the function
295 |         expr_key = introduce_expression("f(x)")
296 | 
297 |         assert expr_key == "expr_0"
298 |         assert "expr_0" in expressions
299 |         assert str(expressions["expr_0"]) == "f(x)"
300 | 
301 | 
302 | class TestDsolveOdeTool:
303 |     def test_simple_ode(self):
304 |         # Introduce a variable and a function
305 |         intro("x", [Assumption.REAL], [])
306 |         introduce_function("f")
307 | 
308 |         # Create a differential equation: f''(x) + 9*f(x) = 0
309 |         expr_key = introduce_expression("Derivative(f(x), x, x) + 9*f(x)")
310 | 
311 |         # Solve the ODE
312 |         result = dsolve_ode(expr_key, "f")
313 | 
314 |         # The solution should include sin(3*x) and cos(3*x)
315 |         assert "sin" in result
316 |         assert "cos" in result
317 |         assert "3 x" in result
318 | 
319 |     def test_ode_with_hint(self):
320 |         # Introduce a variable and a function
321 |         intro("x", [Assumption.REAL], [])
322 |         introduce_function("f")
323 | 
324 |         # Create a first-order exact equation: sin(x)*cos(f(x)) + cos(x)*sin(f(x))*f'(x) = 0
325 |         expr_key = introduce_expression(
326 |             "sin(x)*cos(f(x)) + cos(x)*sin(f(x))*Derivative(f(x), x)"
327 |         )
328 | 
329 |         # Solve with specific hint
330 |         result = dsolve_ode(expr_key, "f", ODEHint.FIRST_EXACT)
331 | 
332 |         # The solution might contain acos instead of sin
333 |         assert "acos" in result or "sin" in result
334 | 
335 |     def test_nonexistent_expression(self):
336 |         introduce_function("f")
337 |         result = dsolve_ode("nonexistent_key", "f")
338 |         assert "error" in result.lower()
339 | 
340 |     def test_nonexistent_function(self):
341 |         intro("x", [Assumption.REAL], [])
342 |         introduce_function("f")
343 |         expr_key = introduce_expression("Derivative(f(x), x) - f(x)")
344 |         result = dsolve_ode(expr_key, "g")
345 |         assert "error" in result.lower()
346 | 
347 | 
348 | class TestPdsolvePdeTool:
349 |     def test_simple_pde(self):
350 |         # Introduce variables
351 |         intro("x", [Assumption.REAL], [])
352 |         intro("y", [Assumption.REAL], [])
353 | 
354 |         # Introduce a function
355 |         introduce_function("f")
356 | 
357 |         # Create a PDE: 1 + 2*(ux/u) + 3*(uy/u) = 0
358 |         # where u = f(x, y), ux = u.diff(x), uy = u.diff(y)
359 |         expr_key = introduce_expression(
360 |             "Eq(1 + 2*Derivative(f(x, y), x)/f(x, y) + 3*Derivative(f(x, y), y)/f(x, y), 0)"
361 |         )
362 | 
363 |         # Solve the PDE
364 |         result = pdsolve_pde(expr_key, "f")
365 | 
366 |         # Solution should include e^ (LaTeX for exponential) and arbitrary function F
367 |         assert "e^" in result
368 |         assert "F" in result
369 | 
370 |     def test_nonexistent_expression(self):
371 |         introduce_function("f")
372 |         result = pdsolve_pde("nonexistent_key", "f")
373 |         assert "error" in result.lower()
374 | 
375 |     def test_nonexistent_function(self):
376 |         intro("x", [Assumption.REAL], [])
377 |         intro("y", [Assumption.REAL], [])
378 |         introduce_function("f")
379 |         expr_key = introduce_expression(
380 |             "Derivative(f(x, y), x) + Derivative(f(x, y), y)"
381 |         )
382 |         result = pdsolve_pde(expr_key, "g")
383 |         assert "error" in result.lower()
384 | 
385 |     def test_no_function_application(self):
386 |         # Test with an expression that doesn't contain the function
387 |         intro("x", [Assumption.REAL], [])
388 |         intro("y", [Assumption.REAL], [])
389 |         introduce_function("f")
390 |         expr_key = introduce_expression("x + y")
391 |         result = pdsolve_pde(expr_key, "f")
392 |         assert "error" in result.lower()
393 |         assert "function cannot be automatically detected" in result.lower()
394 | 
```

--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------

```python
   1 | # A stateful MCP server that holds a sympy sesssion, with symbol table of variables
   2 | # that can be used in the tools API to define and manipulate expressions.
   3 | 
   4 | from mcp.server.fastmcp import FastMCP
   5 | import sympy
   6 | import argparse
   7 | import logging
   8 | from typing import List, Dict, Optional, Literal, Any, Union
   9 | from pydantic import BaseModel
  10 | from sympy.parsing.sympy_parser import parse_expr
  11 | from sympy.core.facts import InconsistentAssumptions
  12 | from vars import Assumption, Domain, ODEHint, PDEHint, Metric, Tensor, UnitSystem
  13 | from sympy import Eq, Function, dsolve, diff, integrate, simplify, Matrix
  14 | from sympy.solvers.pde import pdsolve
  15 | from sympy.vector import CoordSys3D, curl, divergence, gradient
  16 | 
  17 | from sympy.physics.units import convert_to
  18 | from sympy.physics.units import __dict__ as units_dict
  19 | from sympy.physics.units.systems import SI, MKS, MKSA, natural
  20 | from sympy.physics.units.systems.cgs import cgs_gauss
  21 | 
  22 | # Import common units
  23 | from sympy.physics.units import (
  24 |     meter,
  25 |     kilogram,
  26 |     second,
  27 |     ampere,
  28 |     kelvin,
  29 |     mole,
  30 |     candela,
  31 |     kilometer,
  32 |     millimeter,
  33 |     gram,
  34 |     joule,
  35 |     newton,
  36 |     pascal,
  37 |     watt,
  38 |     coulomb,
  39 |     volt,
  40 |     ohm,
  41 |     farad,
  42 |     henry,
  43 |     speed_of_light,
  44 |     gravitational_constant,
  45 |     planck,
  46 |     day,
  47 |     year,
  48 |     minute,
  49 |     hour,
  50 | )
  51 | 
  52 | try:
  53 |     from einsteinpy.symbolic import (
  54 |         MetricTensor,
  55 |         RicciTensor,
  56 |         RicciScalar,
  57 |         EinsteinTensor,
  58 |         WeylTensor,
  59 |         ChristoffelSymbols,
  60 |         StressEnergyMomentumTensor,
  61 |     )
  62 |     from einsteinpy.symbolic.predefined import (
  63 |         Schwarzschild,
  64 |         Minkowski,
  65 |         MinkowskiCartesian,
  66 |         KerrNewman,
  67 |         Kerr,
  68 |         AntiDeSitter,
  69 |         DeSitter,
  70 |         ReissnerNordstorm,
  71 |         find,
  72 |     )
  73 | 
  74 |     EINSTEINPY_AVAILABLE = True
  75 | except ImportError:
  76 |     EINSTEINPY_AVAILABLE = False
  77 | 
  78 | # Set up logging
  79 | logger = logging.getLogger(__name__)
  80 | 
  81 | # Create an MCP server
  82 | mcp = FastMCP(
  83 |     "sympy-mcp",
  84 |     dependencies=["sympy", "pydantic", "einsteinpy"],
  85 |     instructions="Provides access to the Sympy computer algebra system, which can perform symbolic manipulation of mathematical expressions.",
  86 | )
  87 | 
  88 | # Global store for sympy variables and expressions
  89 | local_vars: Dict[str, sympy.Symbol] = {}
  90 | functions: Dict[str, sympy.Function] = {}
  91 | expressions: Dict[str, sympy.Expr] = {}
  92 | metrics: Dict[str, Any] = {}
  93 | tensor_objects: Dict[str, Any] = {}
  94 | coordinate_systems: Dict[str, CoordSys3D] = {}
  95 | expression_counter = 0
  96 | 
  97 | 
  98 | # Pydantic model for defining a variable with assumptions
  99 | class VariableDefinition(BaseModel):
 100 |     var_name: str
 101 |     pos_assumptions: List[str] = []
 102 |     neg_assumptions: List[str] = []
 103 | 
 104 | 
 105 | # x, y = symbols('x, y', commutative=False)
 106 | 
 107 | 
 108 | # Add an addition tool
 109 | @mcp.tool()
 110 | def intro(
 111 |     var_name: str, pos_assumptions: List[Assumption], neg_assumptions: List[Assumption]
 112 | ) -> str:
 113 |     """Introduces a sympy variable with specified assumptions and stores it.
 114 | 
 115 |     Takes a variable name and a list of positive and negative assumptions.
 116 |     """
 117 |     kwargs_for_symbols = {}
 118 |     # Add assumptions
 119 |     for assumption_obj in pos_assumptions:
 120 |         kwargs_for_symbols[assumption_obj.value] = True
 121 | 
 122 |     for assumption_obj in neg_assumptions:
 123 |         kwargs_for_symbols[assumption_obj.value] = False
 124 | 
 125 |     try:
 126 |         var = sympy.symbols(var_name, **kwargs_for_symbols)
 127 |     except InconsistentAssumptions as e:
 128 |         return f"Error creating symbol '{var_name}': The provided assumptions {kwargs_for_symbols} are inconsistent according to SymPy. Details: {str(e)}"
 129 |     except Exception as e:
 130 |         return f"Error creating symbol '{var_name}': An unexpected error occurred. Assumptions attempted: {kwargs_for_symbols}. Details: {type(e).__name__} - {str(e)}"
 131 | 
 132 |     local_vars[var_name] = var
 133 |     return var_name
 134 | 
 135 | 
 136 | # Introduce multiple variables simultaneously
 137 | @mcp.tool()
 138 | def intro_many(variables: List[VariableDefinition]) -> str:
 139 |     """Introduces multiple sympy variables with specified assumptions and stores them.
 140 | 
 141 |     Takes a list of VariableDefinition objects for the 'variables' parameter.
 142 |     Each object in the list specifies:
 143 |     - var_name: The name of the variable (string).
 144 |     - pos_assumptions: A list of positive assumption strings (e.g., ["real", "positive"]).
 145 |     - neg_assumptions: A list of negative assumption strings (e.g., ["complex"]).
 146 | 
 147 |     The JSON payload for the 'variables' argument should be a direct list of these objects, for example:
 148 |     ```json
 149 |     [
 150 |         {
 151 |             "var_name": "x",
 152 |             "pos_assumptions": ["real", "positive"],
 153 |             "neg_assumptions": ["complex"]
 154 |         },
 155 |         {
 156 |             "var_name": "y",
 157 |             "pos_assumptions": [],
 158 |             "neg_assumptions": ["commutative"]
 159 |         }
 160 |     ]
 161 |     ```
 162 | 
 163 |     The assumptions must be consistent, so a real number is not allowed to be non-commutative.
 164 | 
 165 |     Prefer this over intro() for multiple variables because it's more efficient.
 166 |     """
 167 |     var_keys = {}
 168 |     for var_def in variables:
 169 |         try:
 170 |             processed_pos_assumptions = [
 171 |                 Assumption(a_str) for a_str in var_def.pos_assumptions
 172 |             ]
 173 |             processed_neg_assumptions = [
 174 |                 Assumption(a_str) for a_str in var_def.neg_assumptions
 175 |             ]
 176 |         except ValueError as e:
 177 |             # Handle cases where a string doesn't match an Assumption enum member
 178 |             msg = (
 179 |                 f"Error for variable '{var_def.var_name}': Invalid assumption string provided. {e}. "
 180 |                 f"Ensure assumptions match valid enum values in 'vars.Assumption'."
 181 |             )
 182 |             logger.error(msg)
 183 |             return msg  # Or collect errors
 184 | 
 185 |         var_key = intro(
 186 |             var_def.var_name, processed_pos_assumptions, processed_neg_assumptions
 187 |         )
 188 |         var_keys[var_def.var_name] = var_key
 189 | 
 190 |     # Return the mapping of variable names to keys
 191 |     return str(var_keys)
 192 | 
 193 | 
 194 | # XXX use local_vars {x : "expr_1", y : "expr_2"}
 195 | @mcp.tool()
 196 | def introduce_expression(
 197 |     expr_str: str, canonicalize: bool = True, expr_var_name: Optional[str] = None
 198 | ) -> str:
 199 |     """Parses a sympy expression string using available local variables and stores it. Assigns it to either a temporary name (expr_0, expr_1, etc.) or a user-specified global name.
 200 | 
 201 |     Uses Sympy parse_expr to parse the expression string.
 202 | 
 203 |     Applies default Sympy canonicalization rules unless canonicalize is False.
 204 | 
 205 |     For equations (x^2 = 1) make the input string "Eq(x^2, 1") not "x^2 == 1"
 206 | 
 207 |     Examples:
 208 | 
 209 |         {expr_str: "Eq(x^2 + y^2, 1)"}
 210 |         {expr_str: "Matrix(((25, 15, -5), (15, 18, 0), (-5, 0, 11)))"}
 211 |         {expr_str: "pi+e", "expr_var_name": "z"}
 212 |     """
 213 |     global expression_counter
 214 |     # Merge local_vars and functions dictionaries to make both available for parsing
 215 |     parse_dict = {**local_vars, **functions}
 216 |     parsed_expr = parse_expr(expr_str, local_dict=parse_dict, evaluate=canonicalize)
 217 |     if expr_var_name is None:
 218 |         expr_key = f"expr_{expression_counter}"
 219 |     else:
 220 |         expr_key = expr_var_name
 221 |     expressions[expr_key] = parsed_expr
 222 |     expression_counter += 1
 223 |     return expr_key
 224 | 
 225 | 
 226 | def introduce_equation(lhs_str: str, rhs_str: str) -> str:
 227 |     """Introduces an equation (lhs = rhs) using available local variables."""
 228 |     global expression_counter
 229 |     # Merge local_vars and functions dictionaries to make both available for parsing
 230 |     parse_dict = {**local_vars, **functions}
 231 |     lhs_expr = parse_expr(lhs_str, local_dict=parse_dict)
 232 |     rhs_expr = parse_expr(rhs_str, local_dict=parse_dict)
 233 |     eq_key = f"eq_{expression_counter}"
 234 |     expressions[eq_key] = Eq(lhs_expr, rhs_expr)
 235 |     expression_counter += 1
 236 |     return eq_key
 237 | 
 238 | 
 239 | @mcp.tool()
 240 | def print_latex_expression(expr_key: str) -> str:
 241 |     """Prints a stored expression in LaTeX format, along with variable assumptions."""
 242 |     if expr_key not in expressions:
 243 |         return f"Error: Expression key '{expr_key}' not found."
 244 | 
 245 |     expr = expressions[expr_key]
 246 | 
 247 |     # Handle dictionary objects (like eigenvalues)
 248 |     if isinstance(expr, dict):
 249 |         if all(isinstance(k, (sympy.Expr, int, float)) for k in expr.keys()):
 250 |             # Format as eigenvalues: {value: multiplicity, ...}
 251 |             parts = []
 252 |             for eigenval, multiplicity in expr.items():
 253 |                 parts.append(
 254 |                     f"{sympy.latex(eigenval)} \\text{{ (multiplicity {multiplicity})}}"
 255 |                 )
 256 |             return ", ".join(parts)
 257 |         else:
 258 |             # Generic dictionary
 259 |             return str(expr)
 260 | 
 261 |     # Handle list objects (like eigenvectors)
 262 |     elif isinstance(expr, list):
 263 |         # For eigenvectors format: [(eigenval, multiplicity, [eigenvectors]), ...]
 264 |         if all(isinstance(item, tuple) and len(item) == 3 for item in expr):
 265 |             parts = []
 266 |             for eigenval, multiplicity, eigenvects in expr:
 267 |                 eigenvects_latex = [sympy.latex(v) for v in eigenvects]
 268 |                 parts.append(
 269 |                     f"\\lambda = {sympy.latex(eigenval)} \\text{{ (multiplicity {multiplicity})}}:\n"
 270 |                     f"\\text{{Eigenvectors: }}[{', '.join(eigenvects_latex)}]"
 271 |                 )
 272 |             return "\n".join(parts)
 273 |         else:
 274 |             # Try to convert each element to LaTeX
 275 |             try:
 276 |                 return str([sympy.latex(item) for item in expr])
 277 |             except Exception as e:
 278 |                 # Log the exception if there's a logger configured
 279 |                 logger.debug(f"Error converting list items to LaTeX: {str(e)}")
 280 |                 return str(expr)
 281 | 
 282 |     # Original behavior for sympy expressions
 283 |     latex_str = sympy.latex(expr)
 284 | 
 285 |     # Find variables in the expression and their assumptions
 286 |     try:
 287 |         variables_in_expr = expr.free_symbols
 288 |         assumption_descs = []
 289 |         for var_symbol in variables_in_expr:
 290 |             var_name = str(var_symbol)
 291 |             if var_name in local_vars:
 292 |                 # Get assumptions directly from the symbol object
 293 |                 current_assumptions = []
 294 |                 # sympy stores assumptions in a private attribute _assumptions
 295 |                 # and provides a way to query them via .is_commutative, .is_real etc.
 296 |                 # We can iterate through known Assumption enum values
 297 |                 for assumption_enum_member in Assumption:
 298 |                     if getattr(var_symbol, f"is_{assumption_enum_member.value}", False):
 299 |                         current_assumptions.append(assumption_enum_member.value)
 300 | 
 301 |                 if current_assumptions:
 302 |                     assumption_descs.append(
 303 |                         f"{var_name} is {', '.join(current_assumptions)}"
 304 |                     )
 305 |                 else:
 306 |                     assumption_descs.append(
 307 |                         f"{var_name} (no specific assumptions listed)"
 308 |                     )
 309 |             else:
 310 |                 assumption_descs.append(f"{var_name} (undefined in local_vars)")
 311 | 
 312 |         if assumption_descs:
 313 |             return f"{latex_str} (where {'; '.join(assumption_descs)})"
 314 |         else:
 315 |             return latex_str
 316 |     except AttributeError:
 317 |         # If expr doesn't have free_symbols, just return the LaTeX
 318 |         return latex_str
 319 | 
 320 | 
 321 | @mcp.tool()
 322 | def solve_algebraically(
 323 |     expr_key: str, solve_for_var_name: str, domain: Domain = Domain.COMPLEX
 324 | ) -> str:
 325 |     """Solves an equation (expression = 0) algebraically for a given variable.
 326 | 
 327 |     Args:
 328 |         expr_key: The key of the expression (previously introduced) to be solved.
 329 |         solve_for_var_name: The name of the variable (previously introduced) to solve for.
 330 |         domain: The domain to solve in: Domain.COMPLEX, Domain.REAL, Domain.INTEGERS, or Domain.NATURALS. Defaults to Domain.COMPLEX.
 331 | 
 332 |     Returns:
 333 |         A LaTeX string representing the set of solutions. Returns an error message string if issues occur.
 334 |     """
 335 |     if expr_key not in expressions:
 336 |         return f"Error: Expression with key '{expr_key}' not found."
 337 | 
 338 |     expression_to_solve = expressions[expr_key]
 339 | 
 340 |     if solve_for_var_name not in local_vars:
 341 |         return f"Error: Variable '{solve_for_var_name}' not found in local_vars. Please introduce it first."
 342 | 
 343 |     variable_symbol = local_vars[solve_for_var_name]
 344 | 
 345 |     # Map domain enum to SymPy domain sets
 346 |     domain_map = {
 347 |         Domain.COMPLEX: sympy.S.Complexes,
 348 |         Domain.REAL: sympy.S.Reals,
 349 |         Domain.INTEGERS: sympy.S.Integers,
 350 |         Domain.NATURALS: sympy.S.Naturals0,
 351 |     }
 352 | 
 353 |     if domain not in domain_map:
 354 |         return "Error: Invalid domain. Choose from: Domain.COMPLEX, Domain.REAL, Domain.INTEGERS, or Domain.NATURALS."
 355 | 
 356 |     sympy_domain = domain_map[domain]
 357 | 
 358 |     try:
 359 |         # If the expression is an equation (Eq object), convert it to standard form
 360 |         if isinstance(expression_to_solve, sympy.Eq):
 361 |             expression_to_solve = expression_to_solve.lhs - expression_to_solve.rhs
 362 | 
 363 |         # Use solveset instead of solve
 364 |         solution_set = sympy.solveset(
 365 |             expression_to_solve, variable_symbol, domain=sympy_domain
 366 |         )
 367 | 
 368 |         # Convert the set to LaTeX format
 369 |         latex_output = sympy.latex(solution_set)
 370 |         return latex_output
 371 |     except NotImplementedError as e:
 372 |         return f"Error: SymPy could not solve the equation: {str(e)}. The equation may not have a closed-form solution or the algorithm is not implemented."
 373 |     except Exception as e:
 374 |         return f"An unexpected error occurred during solving: {str(e)}"
 375 | 
 376 | 
 377 | @mcp.tool()
 378 | def solve_linear_system(
 379 |     expr_keys: List[str], var_names: List[str], domain: Domain = Domain.COMPLEX
 380 | ) -> str:
 381 |     """Solves a system of linear equations using SymPy's linsolve.
 382 | 
 383 |     Args:
 384 |         expr_keys: The keys of the expressions (previously introduced) forming the system.
 385 |         var_names: The names of the variables to solve for.
 386 |         domain: The domain to solve in (Domain.COMPLEX, Domain.REAL, etc.). Defaults to Domain.COMPLEX.
 387 | 
 388 |     Returns:
 389 |         A LaTeX string representing the solution set. Returns an error message string if issues occur.
 390 |     """
 391 |     # Validate all expression keys exist
 392 |     system = []
 393 |     for expr_key in expr_keys:
 394 |         if expr_key not in expressions:
 395 |             return f"Error: Expression with key '{expr_key}' not found."
 396 | 
 397 |         expr = expressions[expr_key]
 398 |         # Convert equations to standard form
 399 |         if isinstance(expr, sympy.Eq):
 400 |             expr = expr.lhs - expr.rhs
 401 |         system.append(expr)
 402 | 
 403 |     # Validate all variables exist
 404 |     symbols = []
 405 |     for var_name in var_names:
 406 |         if var_name not in local_vars:
 407 |             return f"Error: Variable '{var_name}' not found in local_vars. Please introduce it first."
 408 |         symbols.append(local_vars[var_name])
 409 | 
 410 |     # Map domain enum to SymPy domain sets
 411 |     domain_map = {
 412 |         Domain.COMPLEX: sympy.S.Complexes,
 413 |         Domain.REAL: sympy.S.Reals,
 414 |         Domain.INTEGERS: sympy.S.Integers,
 415 |         Domain.NATURALS: sympy.S.Naturals0,
 416 |     }
 417 | 
 418 |     if domain not in domain_map:
 419 |         return "Error: Invalid domain. Choose from: Domain.COMPLEX, Domain.REAL, Domain.INTEGERS, or Domain.NATURALS."
 420 | 
 421 |     domain_map[domain]
 422 | 
 423 |     try:
 424 |         # Use SymPy's linsolve - note: it doesn't take domain parameter directly, but works on the given domain
 425 |         solution_set = sympy.linsolve(system, symbols)
 426 | 
 427 |         # Convert the set to LaTeX format
 428 |         latex_output = sympy.latex(solution_set)
 429 |         return latex_output
 430 |     except NotImplementedError as e:
 431 |         return f"Error: SymPy could not solve the linear system: {str(e)}."
 432 |     except ValueError as e:
 433 |         return f"Error: Invalid system or arguments: {str(e)}."
 434 |     except Exception as e:
 435 |         return f"An unexpected error occurred during solving: {str(e)}"
 436 | 
 437 | 
 438 | @mcp.tool()
 439 | def solve_nonlinear_system(
 440 |     expr_keys: List[str], var_names: List[str], domain: Domain = Domain.COMPLEX
 441 | ) -> str:
 442 |     """Solves a system of nonlinear equations using SymPy's nonlinsolve.
 443 | 
 444 |     Args:
 445 |         expr_keys: The keys of the expressions (previously introduced) forming the system.
 446 |         var_names: The names of the variables to solve for.
 447 |         domain: The domain to solve in (Domain.COMPLEX, Domain.REAL, etc.). Defaults to Domain.COMPLEX.
 448 | 
 449 |     Returns:
 450 |         A LaTeX string representing the solution set. Returns an error message string if issues occur.
 451 |     """
 452 |     # Validate all expression keys exist
 453 |     system = []
 454 |     for expr_key in expr_keys:
 455 |         if expr_key not in expressions:
 456 |             return f"Error: Expression with key '{expr_key}' not found."
 457 | 
 458 |         expr = expressions[expr_key]
 459 |         # Convert equations to standard form
 460 |         if isinstance(expr, sympy.Eq):
 461 |             expr = expr.lhs - expr.rhs
 462 |         system.append(expr)
 463 | 
 464 |     # Validate all variables exist
 465 |     symbols = []
 466 |     for var_name in var_names:
 467 |         if var_name not in local_vars:
 468 |             return f"Error: Variable '{var_name}' not found in local_vars. Please introduce it first."
 469 |         symbols.append(local_vars[var_name])
 470 | 
 471 |     # Map domain enum to SymPy domain sets
 472 |     domain_map = {
 473 |         Domain.COMPLEX: sympy.S.Complexes,
 474 |         Domain.REAL: sympy.S.Reals,
 475 |         Domain.INTEGERS: sympy.S.Integers,
 476 |         Domain.NATURALS: sympy.S.Naturals0,
 477 |     }
 478 | 
 479 |     if domain not in domain_map:
 480 |         return "Error: Invalid domain. Choose from: Domain.COMPLEX, Domain.REAL, Domain.INTEGERS, or Domain.NATURALS."
 481 | 
 482 |     try:
 483 |         # Use SymPy's nonlinsolve
 484 |         solution_set = sympy.nonlinsolve(system, symbols)
 485 | 
 486 |         # Convert the set to LaTeX format
 487 |         latex_output = sympy.latex(solution_set)
 488 |         return latex_output
 489 |     except NotImplementedError as e:
 490 |         return f"Error: SymPy could not solve the nonlinear system: {str(e)}."
 491 |     except ValueError as e:
 492 |         return f"Error: Invalid system or arguments: {str(e)}."
 493 |     except Exception as e:
 494 |         return f"An unexpected error occurred during solving: {str(e)}"
 495 | 
 496 | 
 497 | @mcp.tool()
 498 | def introduce_function(func_name: str) -> str:
 499 |     """Introduces a SymPy function variable and stores it.
 500 | 
 501 |     Takes a function name and creates a SymPy Function object for use in defining differential equations.
 502 | 
 503 |     Example:
 504 |         {func_name: "f"} will create the function f(x), f(t), etc. that can be used in expressions
 505 | 
 506 |     Returns:
 507 |         The name of the created function.
 508 |     """
 509 |     func = Function(func_name)
 510 |     functions[func_name] = func
 511 |     return func_name
 512 | 
 513 | 
 514 | @mcp.tool()
 515 | def dsolve_ode(expr_key: str, func_name: str, hint: Optional[ODEHint] = None) -> str:
 516 |     """Solves an ordinary differential equation using SymPy's dsolve function.
 517 | 
 518 |     Args:
 519 |         expr_key: The key of the expression (previously introduced) containing the differential equation.
 520 |         func_name: The name of the function (previously introduced) to solve for.
 521 |         hint: Optional solving method from ODEHint enum. If None, SymPy will try to determine the best method.
 522 | 
 523 |     Example:
 524 |         # First introduce a variable and a function
 525 |         intro("x", [Assumption.REAL], [])
 526 |         introduce_function("f")
 527 | 
 528 |         # Create a second-order ODE: f''(x) + 9*f(x) = 0
 529 |         expr_key = introduce_expression("Derivative(f(x), x, x) + 9*f(x)")
 530 | 
 531 |         # Solve the ODE
 532 |         result = dsolve_ode(expr_key, "f")
 533 |         # Returns solution with sin(3*x) and cos(3*x) terms
 534 | 
 535 |     Returns:
 536 |         A LaTeX string representing the solution. Returns an error message string if issues occur.
 537 |     """
 538 |     if expr_key not in expressions:
 539 |         return f"Error: Expression with key '{expr_key}' not found."
 540 | 
 541 |     if func_name not in functions:
 542 |         return f"Error: Function '{func_name}' not found. Please introduce it first using introduce_function."
 543 | 
 544 |     expression = expressions[expr_key]
 545 | 
 546 |     try:
 547 |         # Convert to equation form if it's not already
 548 |         if isinstance(expression, sympy.Eq):
 549 |             eq = expression
 550 |         else:
 551 |             eq = sympy.Eq(expression, 0)
 552 | 
 553 |         # Let SymPy handle function detection and apply the specified hint if provided
 554 |         if hint is not None:
 555 |             solution = dsolve(eq, hint=hint.value)
 556 |         else:
 557 |             solution = dsolve(eq)
 558 | 
 559 |         # Convert the solution to LaTeX format
 560 |         latex_output = sympy.latex(solution)
 561 |         return latex_output
 562 |     except ValueError as e:
 563 |         return f"Error: {str(e)}. This might be due to an invalid hint or an unsupported equation type."
 564 |     except NotImplementedError as e:
 565 |         return f"Error: Method not implemented: {str(e)}. Try a different hint or equation type."
 566 |     except Exception as e:
 567 |         return f"An unexpected error occurred: {str(e)}"
 568 | 
 569 | 
 570 | @mcp.tool()
 571 | def pdsolve_pde(expr_key: str, func_name: str, hint: Optional[PDEHint] = None) -> str:
 572 |     """Solves a partial differential equation using SymPy's pdsolve function.
 573 | 
 574 |     Args:
 575 |         expr_key: The key of the expression (previously introduced) containing the PDE.
 576 |                  If the expression is not an equation (Eq), it will be interpreted as
 577 |                  PDE = 0.
 578 |         func_name: The name of the function (previously introduced) to solve for.
 579 |                    This should be a function of multiple variables.
 580 | 
 581 |     Example:
 582 |         # First introduce variables and a function
 583 |         intro("x", [Assumption.REAL], [])
 584 |         intro("y", [Assumption.REAL], [])
 585 |         introduce_function("f")
 586 | 
 587 |         # Create a PDE: 1 + 2*(ux/u) + 3*(uy/u) = 0
 588 |         expr_key = introduce_expression(
 589 |             "Eq(1 + 2*Derivative(f(x, y), x)/f(x, y) + 3*Derivative(f(x, y), y)/f(x, y), 0)"
 590 |         )
 591 | 
 592 |         # Solve the PDE
 593 |         result = pdsolve_pde(expr_key, "f")
 594 |         # Returns solution with exponential terms and arbitrary function
 595 | 
 596 |     Returns:
 597 |         A LaTeX string representing the solution. Returns an error message string if issues occur.
 598 |     """
 599 |     if expr_key not in expressions:
 600 |         return f"Error: Expression with key '{expr_key}' not found."
 601 | 
 602 |     if func_name not in functions:
 603 |         return f"Error: Function '{func_name}' not found. Please introduce it first using introduce_function."
 604 | 
 605 |     expression = expressions[expr_key]
 606 | 
 607 |     try:
 608 |         # Handle both equation and non-equation expressions
 609 |         if isinstance(expression, sympy.Eq):
 610 |             eq = expression
 611 |         else:
 612 |             eq = sympy.Eq(expression, 0)
 613 | 
 614 |         # Let SymPy's pdsolve find the dependent variable itself
 615 |         if hint is not None:
 616 |             solution = pdsolve(eq, hint=hint.value)
 617 |         else:
 618 |             solution = pdsolve(eq)
 619 | 
 620 |         # Convert the solution to LaTeX format
 621 |         latex_output = sympy.latex(solution)
 622 |         return latex_output
 623 |     except ValueError as e:
 624 |         return f"Error: {str(e)}. This might be due to an unsupported equation type."
 625 |     except NotImplementedError as e:
 626 |         return f"Error: Method not implemented: {str(e)}. The PDE might not be solvable using the implemented methods."
 627 |     except Exception as e:
 628 |         return f"An unexpected error occurred: {str(e)}"
 629 | 
 630 | 
 631 | # Einstein relativity tools
 632 | if EINSTEINPY_AVAILABLE:
 633 | 
 634 |     @mcp.tool()
 635 |     def create_predefined_metric(metric_name: str) -> str:
 636 |         """Creates a predefined spacetime metric from einsteinpy.symbolic.predefined.
 637 | 
 638 |         Args:
 639 |             metric_name: The name of the metric to create (e.g., "AntiDeSitter", "Schwarzschild").
 640 | 
 641 |         Returns:
 642 |             A key for the stored metric object.
 643 |         """
 644 |         try:
 645 |             # Handle if metric_name is actually a Metric enum already
 646 |             if isinstance(metric_name, Metric):
 647 |                 metric_enum = metric_name
 648 |             else:
 649 |                 # First try direct mapping to enum value
 650 |                 metric_enum = None
 651 | 
 652 |                 # Try to match by enum value (the string in the enum definition)
 653 |                 for metric in Metric:
 654 |                     if metric.value.lower() == metric_name.lower():
 655 |                         metric_enum = metric
 656 |                         break
 657 | 
 658 |                 # If it didn't match any enum value, try to match by enum name
 659 |                 if metric_enum is None:
 660 |                     try:
 661 |                         # Try exact name match
 662 |                         metric_enum = Metric[metric_name.upper()]
 663 |                     except KeyError:
 664 |                         # Try normalized name (remove spaces, underscores, etc.)
 665 |                         normalized_name = "".join(
 666 |                             c.upper() for c in metric_name if c.isalnum()
 667 |                         )
 668 |                         for m in Metric:
 669 |                             if (
 670 |                                 "".join(c for c in m.name if c.isalnum())
 671 |                                 == normalized_name
 672 |                             ):
 673 |                                 metric_enum = m
 674 |                                 break
 675 | 
 676 |             if metric_enum is None:
 677 |                 return f"Error: Invalid metric name '{metric_name}'. Available metrics are: {', '.join(m.value for m in Metric)}"
 678 | 
 679 |             metric_map = {
 680 |                 Metric.SCHWARZSCHILD: Schwarzschild,
 681 |                 Metric.MINKOWSKI: Minkowski,
 682 |                 Metric.MINKOWSKI_CARTESIAN: MinkowskiCartesian,
 683 |                 Metric.KERR_NEWMAN: KerrNewman,
 684 |                 Metric.KERR: Kerr,
 685 |                 Metric.ANTI_DE_SITTER: AntiDeSitter,
 686 |                 Metric.DE_SITTER: DeSitter,
 687 |                 Metric.REISSNER_NORDSTROM: ReissnerNordstorm,
 688 |             }
 689 | 
 690 |             if metric_enum not in metric_map:
 691 |                 return f"Error: Metric '{metric_enum.value}' not implemented. Available metrics are: {', '.join(m.value for m in Metric)}"
 692 | 
 693 |             metric_class = metric_map[metric_enum]
 694 |             metric_obj = metric_class()
 695 | 
 696 |             metric_key = f"metric_{metric_enum.value}"
 697 |             metrics[metric_key] = metric_obj
 698 |             expressions[metric_key] = metric_obj.tensor()
 699 | 
 700 |             return metric_key
 701 |         except Exception as e:
 702 |             return f"Error creating metric: {str(e)}"
 703 | 
 704 |     @mcp.tool()
 705 |     def search_predefined_metrics(query: str) -> str:
 706 |         """Searches for predefined metrics in einsteinpy.symbolic.predefined.
 707 | 
 708 |         Args:
 709 |             query: A search term to find metrics whose names contain this substring.
 710 | 
 711 |         Returns:
 712 |             A string listing the found metrics.
 713 |         """
 714 |         try:
 715 |             results = find(query)
 716 |             if not results:
 717 |                 return f"No metrics found matching '{query}'."
 718 | 
 719 |             return f"Found metrics: {', '.join(results)}"
 720 |         except Exception as e:
 721 |             return f"Error searching for metrics: {str(e)}"
 722 | 
 723 |     @mcp.tool()
 724 |     def calculate_tensor(
 725 |         metric_key: str, tensor_type: str, simplify_result: bool = True
 726 |     ) -> str:
 727 |         """Calculates a tensor from a metric using einsteinpy.symbolic.
 728 | 
 729 |         Args:
 730 |             metric_key: The key of the stored metric object.
 731 |             tensor_type: The type of tensor to calculate (e.g., "RICCI_TENSOR", "EINSTEIN_TENSOR").
 732 |             simplify_result: Whether to apply sympy simplification to the result.
 733 | 
 734 |         Returns:
 735 |             A key for the stored tensor object.
 736 |         """
 737 |         if metric_key not in metrics:
 738 |             return f"Error: Metric key '{metric_key}' not found."
 739 | 
 740 |         metric_obj = metrics[metric_key]
 741 | 
 742 |         # Convert string to Tensor enum
 743 |         tensor_enum = None
 744 |         try:
 745 |             # Handle if tensor_type is already a Tensor enum
 746 |             if isinstance(tensor_type, Tensor):
 747 |                 tensor_enum = tensor_type
 748 |             else:
 749 |                 # Try to match by enum value
 750 |                 for tensor in Tensor:
 751 |                     if tensor.value.lower() == tensor_type.lower():
 752 |                         tensor_enum = tensor
 753 |                         break
 754 | 
 755 |             # If it didn't match any enum value, try to match by enum name
 756 |             if tensor_enum is None:
 757 |                 try:
 758 |                     # Try exact name match
 759 |                     tensor_enum = Tensor[tensor_type.upper()]
 760 |                 except KeyError:
 761 |                     # Try normalized name (remove spaces, underscores, etc.)
 762 |                     normalized_name = "".join(
 763 |                         c.upper() for c in tensor_type if c.isalnum()
 764 |                     )
 765 |                     for t in Tensor:
 766 |                         if "".join(c for c in t.name if c.isalnum()) == normalized_name:
 767 |                             tensor_enum = t
 768 |                             break
 769 | 
 770 |             if tensor_enum is None:
 771 |                 return f"Error: Invalid tensor type '{tensor_type}'. Available types are: {', '.join(t.value for t in Tensor)}"
 772 |         except Exception as e:
 773 |             return f"Error parsing tensor type: {str(e)}"
 774 | 
 775 |         tensor_map = {
 776 |             Tensor.RICCI_TENSOR: RicciTensor,
 777 |             Tensor.RICCI_SCALAR: RicciScalar,
 778 |             Tensor.EINSTEIN_TENSOR: EinsteinTensor,
 779 |             Tensor.WEYL_TENSOR: WeylTensor,
 780 |             Tensor.RIEMANN_CURVATURE_TENSOR: ChristoffelSymbols,
 781 |             Tensor.STRESS_ENERGY_MOMENTUM_TENSOR: StressEnergyMomentumTensor,
 782 |         }
 783 | 
 784 |         try:
 785 |             if tensor_enum not in tensor_map:
 786 |                 return f"Error: Tensor type '{tensor_enum.value}' not implemented. Available types are: {', '.join(t.value for t in Tensor)}"
 787 | 
 788 |             tensor_class = tensor_map[tensor_enum]
 789 | 
 790 |             # Special case for RicciScalar which takes a RicciTensor
 791 |             if tensor_enum == Tensor.RICCI_SCALAR:
 792 |                 ricci_tensor = RicciTensor.from_metric(metric_obj)
 793 |                 tensor_obj = RicciScalar.from_riccitensor(ricci_tensor)
 794 |             else:
 795 |                 tensor_obj = tensor_class.from_metric(metric_obj)
 796 | 
 797 |             tensor_key = f"{tensor_enum.value.lower()}_{metric_key}"
 798 |             tensor_objects[tensor_key] = tensor_obj
 799 | 
 800 |             # Store the tensor expression
 801 |             if tensor_enum == Tensor.RICCI_SCALAR:
 802 |                 # Scalar has expr attribute
 803 |                 tensor_expr = tensor_obj.expr
 804 |                 if simplify_result:
 805 |                     tensor_expr = sympy.simplify(tensor_expr)
 806 |                 expressions[tensor_key] = tensor_expr
 807 |             else:
 808 |                 # Other tensors have tensor() method
 809 |                 tensor_expr = tensor_obj.tensor()
 810 |                 expressions[tensor_key] = tensor_expr
 811 | 
 812 |             return tensor_key
 813 |         except Exception as e:
 814 |             return f"Error calculating tensor: {str(e)}"
 815 | 
 816 |     @mcp.tool()
 817 |     def create_custom_metric(
 818 |         components: List[List[str]],
 819 |         symbols: List[str],
 820 |         config: Literal["ll", "uu"] = "ll",
 821 |     ) -> str:
 822 |         """Creates a custom metric tensor from provided components and symbols.
 823 | 
 824 |         Args:
 825 |             components: A matrix of symbolic expressions as strings representing metric components.
 826 |             symbols: A list of symbol names used in the components.
 827 |             config: The tensor configuration - "ll" for covariant (lower indices) or "uu" for contravariant (upper indices).
 828 | 
 829 |         Returns:
 830 |             A key for the stored metric object.
 831 |         """
 832 |         global expression_counter
 833 |         try:
 834 |             # Parse symbols
 835 |             sympy_symbols = sympy.symbols(", ".join(symbols))
 836 |             sympy_symbols_dict = {str(sym): sym for sym in sympy_symbols}
 837 | 
 838 |             # Convert components to sympy expressions
 839 |             sympy_components = []
 840 |             for row in components:
 841 |                 sympy_row = []
 842 |                 for expr_str in row:
 843 |                     if expr_str == "0":
 844 |                         sympy_row.append(0)
 845 |                     else:
 846 |                         expr = parse_expr(expr_str, local_dict=sympy_symbols_dict)
 847 |                         sympy_row.append(expr)
 848 |                 sympy_components.append(sympy_row)
 849 | 
 850 |             # Create metric tensor
 851 |             metric_obj = MetricTensor(sympy_components, sympy_symbols, config=config)
 852 | 
 853 |             # Store the metric
 854 |             metric_key = f"metric_custom_{expression_counter}"
 855 |             metrics[metric_key] = metric_obj
 856 |             expressions[metric_key] = metric_obj.tensor()
 857 | 
 858 |             expression_counter += 1
 859 | 
 860 |             return metric_key
 861 |         except Exception as e:
 862 |             return f"Error creating custom metric: {str(e)}"
 863 | 
 864 |     @mcp.tool()
 865 |     def print_latex_tensor(tensor_key: str) -> str:
 866 |         """Prints a stored tensor expression in LaTeX format.
 867 | 
 868 |         Args:
 869 |             tensor_key: The key of the stored tensor object.
 870 | 
 871 |         Returns:
 872 |             The LaTeX representation of the tensor.
 873 |         """
 874 |         if tensor_key not in expressions:
 875 |             return f"Error: Tensor key '{tensor_key}' not found."
 876 | 
 877 |         try:
 878 |             tensor_expr = expressions[tensor_key]
 879 |             latex_str = sympy.latex(tensor_expr)
 880 |             return latex_str
 881 |         except Exception as e:
 882 |             return f"Error generating LaTeX: {str(e)}"
 883 | 
 884 | else:
 885 | 
 886 |     @mcp.tool()
 887 |     def create_predefined_metric(metric_name: str) -> str:
 888 |         """Creates a predefined spacetime metric."""
 889 |         return "Error: EinsteinPy library is not available. Please install it with 'pip install einsteinpy'."
 890 | 
 891 |     @mcp.tool()
 892 |     def search_predefined_metrics(query: str) -> str:
 893 |         """Searches for predefined metrics in einsteinpy.symbolic.predefined."""
 894 |         return "Error: EinsteinPy library is not available. Please install it with 'pip install einsteinpy'."
 895 | 
 896 |     @mcp.tool()
 897 |     def calculate_tensor(
 898 |         metric_key: str, tensor_type: str, simplify_result: bool = True
 899 |     ) -> str:
 900 |         """Calculates a tensor from a metric using einsteinpy.symbolic."""
 901 |         return "Error: EinsteinPy library is not available. Please install it with 'pip install einsteinpy'."
 902 | 
 903 |     @mcp.tool()
 904 |     def create_custom_metric(
 905 |         components: List[List[str]],
 906 |         symbols: List[str],
 907 |         config: Literal["ll", "uu"] = "ll",
 908 |     ) -> str:
 909 |         """Creates a custom metric tensor from provided components and symbols."""
 910 |         return "Error: EinsteinPy library is not available. Please install it with 'pip install einsteinpy'."
 911 | 
 912 |     @mcp.tool()
 913 |     def print_latex_tensor(tensor_key: str) -> str:
 914 |         """Prints a stored tensor expression in LaTeX format."""
 915 |         return "Error: EinsteinPy library is not available. Please install it with 'pip install einsteinpy'."
 916 | 
 917 | 
 918 | @mcp.tool()
 919 | def simplify_expression(expr_key: str) -> str:
 920 |     """Simplifies a mathematical expression using SymPy's simplify function.
 921 | 
 922 |     Args:
 923 |         expr_key: The key of the expression (previously introduced) to simplify.
 924 | 
 925 |     Example:
 926 |         # Introduce variables
 927 |         intro("x", [Assumption.REAL], [])
 928 |         intro("y", [Assumption.REAL], [])
 929 | 
 930 |         # Create an expression to simplify: sin(x)^2 + cos(x)^2
 931 |         expr_key = introduce_expression("sin(x)**2 + cos(x)**2")
 932 | 
 933 |         # Simplify the expression
 934 |         simplified = simplify_expression(expr_key)
 935 |         # Returns 1
 936 | 
 937 |     Returns:
 938 |         A key for the simplified expression.
 939 |     """
 940 |     global expression_counter
 941 | 
 942 |     if expr_key not in expressions:
 943 |         return f"Error: Expression with key '{expr_key}' not found."
 944 | 
 945 |     try:
 946 |         original_expr = expressions[expr_key]
 947 |         simplified_expr = simplify(original_expr)
 948 | 
 949 |         result_key = f"expr_{expression_counter}"
 950 |         expressions[result_key] = simplified_expr
 951 |         expression_counter += 1
 952 | 
 953 |         return result_key
 954 |     except Exception as e:
 955 |         return f"Error during simplification: {str(e)}"
 956 | 
 957 | 
 958 | @mcp.tool()
 959 | def integrate_expression(
 960 |     expr_key: str,
 961 |     var_name: str,
 962 |     lower_bound: Optional[str] = None,
 963 |     upper_bound: Optional[str] = None,
 964 | ) -> str:
 965 |     """Integrates an expression with respect to a variable using SymPy's integrate function.
 966 | 
 967 |     Args:
 968 |         expr_key: The key of the expression (previously introduced) to integrate.
 969 |         var_name: The name of the variable to integrate with respect to.
 970 |         lower_bound: Optional lower bound for definite integration.
 971 |         upper_bound: Optional upper bound for definite integration.
 972 | 
 973 |     Example:
 974 |         # Introduce a variable
 975 |         intro("x", [Assumption.REAL], [])
 976 | 
 977 |         # Create an expression to integrate: x^2
 978 |         expr_key = introduce_expression("x**2")
 979 | 
 980 |         # Indefinite integration
 981 |         indefinite_result = integrate_expression(expr_key, "x")
 982 |         # Returns x³/3
 983 | 
 984 |         # Definite integration from 0 to 1
 985 |         definite_result = integrate_expression(expr_key, "x", "0", "1")
 986 |         # Returns 1/3
 987 | 
 988 |     Returns:
 989 |         A key for the integrated expression.
 990 |     """
 991 |     global expression_counter
 992 | 
 993 |     if expr_key not in expressions:
 994 |         return f"Error: Expression with key '{expr_key}' not found."
 995 | 
 996 |     if var_name not in local_vars:
 997 |         return f"Error: Variable '{var_name}' not found. Please introduce it first."
 998 | 
 999 |     try:
1000 |         expr = expressions[expr_key]
1001 |         var = local_vars[var_name]
1002 | 
1003 |         # Parse bounds if provided
1004 |         bounds = None
1005 |         if lower_bound is not None and upper_bound is not None:
1006 |             parse_dict = {**local_vars, **functions}
1007 |             lower = parse_expr(lower_bound, local_dict=parse_dict)
1008 |             upper = parse_expr(upper_bound, local_dict=parse_dict)
1009 |             bounds = (var, lower, upper)
1010 | 
1011 |         # Perform integration
1012 |         if bounds:
1013 |             result = integrate(expr, bounds)
1014 |         else:
1015 |             result = integrate(expr, var)
1016 | 
1017 |         result_key = f"expr_{expression_counter}"
1018 |         expressions[result_key] = result
1019 |         expression_counter += 1
1020 | 
1021 |         return result_key
1022 |     except Exception as e:
1023 |         return f"Error during integration: {str(e)}"
1024 | 
1025 | 
1026 | @mcp.tool()
1027 | def differentiate_expression(expr_key: str, var_name: str, order: int = 1) -> str:
1028 |     """Differentiates an expression with respect to a variable using SymPy's diff function.
1029 | 
1030 |     Args:
1031 |         expr_key: The key of the expression (previously introduced) to differentiate.
1032 |         var_name: The name of the variable to differentiate with respect to.
1033 |         order: The order of differentiation (default is 1 for first derivative).
1034 | 
1035 |     Example:
1036 |         # Introduce a variable
1037 |         intro("x", [Assumption.REAL], [])
1038 | 
1039 |         # Create an expression to differentiate: x^3
1040 |         expr_key = introduce_expression("x**3")
1041 | 
1042 |         # First derivative
1043 |         first_deriv = differentiate_expression(expr_key, "x")
1044 |         # Returns 3x²
1045 | 
1046 |         # Second derivative
1047 |         second_deriv = differentiate_expression(expr_key, "x", 2)
1048 |         # Returns 6x
1049 | 
1050 |     Returns:
1051 |         A key for the differentiated expression.
1052 |     """
1053 |     global expression_counter
1054 | 
1055 |     if expr_key not in expressions:
1056 |         return f"Error: Expression with key '{expr_key}' not found."
1057 | 
1058 |     if var_name not in local_vars:
1059 |         return f"Error: Variable '{var_name}' not found. Please introduce it first."
1060 | 
1061 |     if order < 1:
1062 |         return "Error: Order of differentiation must be at least 1."
1063 | 
1064 |     try:
1065 |         expr = expressions[expr_key]
1066 |         var = local_vars[var_name]
1067 | 
1068 |         result = diff(expr, var, order)
1069 | 
1070 |         result_key = f"expr_{expression_counter}"
1071 |         expressions[result_key] = result
1072 |         expression_counter += 1
1073 | 
1074 |         return result_key
1075 |     except Exception as e:
1076 |         return f"Error during differentiation: {str(e)}"
1077 | 
1078 | 
1079 | @mcp.tool()
1080 | def create_coordinate_system(name: str, coord_names: Optional[List[str]] = None) -> str:
1081 |     """Creates a 3D coordinate system for vector calculus operations.
1082 | 
1083 |     Args:
1084 |         name: The name for the coordinate system.
1085 |         coord_names: Optional list of coordinate names (3 names for x, y, z).
1086 |                     If not provided, defaults to [name+'_x', name+'_y', name+'_z'].
1087 | 
1088 |     Example:
1089 |         # Create a coordinate system
1090 |         coord_sys = create_coordinate_system("R")
1091 |         # Creates a coordinate system R with coordinates R_x, R_y, R_z
1092 | 
1093 |         # Create a coordinate system with custom coordinate names
1094 |         coord_sys = create_coordinate_system("C", ["rho", "phi", "z"])
1095 | 
1096 |     Returns:
1097 |         The name of the created coordinate system.
1098 |     """
1099 |     if name in coordinate_systems:
1100 |         return f"Warning: Overwriting existing coordinate system '{name}'."
1101 | 
1102 |     try:
1103 |         if coord_names and len(coord_names) != 3:
1104 |             return "Error: coord_names must contain exactly 3 names for x, y, z coordinates."
1105 | 
1106 |         if coord_names:
1107 |             # Create a CoordSys3D with custom coordinate names
1108 |             cs = CoordSys3D(name, variable_names=coord_names)
1109 |         else:
1110 |             # Create a CoordSys3D with default coordinate naming
1111 |             cs = CoordSys3D(name)
1112 | 
1113 |         coordinate_systems[name] = cs
1114 | 
1115 |         # Add the coordinate system to the expressions dict to make it accessible
1116 |         # in expressions through parsing
1117 |         expressions[name] = cs
1118 | 
1119 |         # Add the coordinate variables to local_vars for easier access
1120 |         for i, base_vector in enumerate(cs.base_vectors()):
1121 |             vector_name = (
1122 |                 f"{name}_{['x', 'y', 'z'][i]}"
1123 |                 if not coord_names
1124 |                 else f"{name}_{coord_names[i]}"
1125 |             )
1126 |             local_vars[vector_name] = base_vector
1127 | 
1128 |         return name
1129 |     except Exception as e:
1130 |         return f"Error creating coordinate system: {str(e)}"
1131 | 
1132 | 
1133 | @mcp.tool()
1134 | def create_vector_field(
1135 |     coord_sys_name: str, component_x: str, component_y: str, component_z: str
1136 | ) -> str:
1137 |     """Creates a vector field in the specified coordinate system.
1138 | 
1139 |     Args:
1140 |         coord_sys_name: The name of the coordinate system to use.
1141 |         component_x: String expression for the x-component of the vector field.
1142 |         component_y: String expression for the y-component of the vector field.
1143 |         component_z: String expression for the z-component of the vector field.
1144 | 
1145 |     Example:
1146 |         # First create a coordinate system
1147 |         create_coordinate_system("R")
1148 | 
1149 |         # Create a vector field F = (y, -x, z)
1150 |         vector_field = create_vector_field("R", "R_y", "-R_x", "R_z")
1151 | 
1152 |     Returns:
1153 |         A key for the vector field expression.
1154 |     """
1155 |     global expression_counter
1156 | 
1157 |     if coord_sys_name not in coordinate_systems:
1158 |         return f"Error: Coordinate system '{coord_sys_name}' not found. Create it first using create_coordinate_system."
1159 | 
1160 |     try:
1161 |         cs = coordinate_systems[coord_sys_name]
1162 | 
1163 |         # Parse the component expressions
1164 |         parse_dict = {**local_vars, **functions, coord_sys_name: cs}
1165 |         x_comp = parse_expr(component_x, local_dict=parse_dict)
1166 |         y_comp = parse_expr(component_y, local_dict=parse_dict)
1167 |         z_comp = parse_expr(component_z, local_dict=parse_dict)
1168 | 
1169 |         # Create the vector field
1170 |         vector_field = (
1171 |             x_comp * cs.base_vectors()[0]
1172 |             + y_comp * cs.base_vectors()[1]
1173 |             + z_comp * cs.base_vectors()[2]
1174 |         )
1175 | 
1176 |         # Store the vector field
1177 |         result_key = f"vector_{expression_counter}"
1178 |         expressions[result_key] = vector_field
1179 |         expression_counter += 1
1180 | 
1181 |         return result_key
1182 |     except Exception as e:
1183 |         return f"Error creating vector field: {str(e)}"
1184 | 
1185 | 
1186 | @mcp.tool()
1187 | def calculate_curl(vector_field_key: str) -> str:
1188 |     """Calculates the curl of a vector field using SymPy's curl function.
1189 | 
1190 |     Args:
1191 |         vector_field_key: The key of the vector field expression.
1192 | 
1193 |     Example:
1194 |         # First create a coordinate system
1195 |         create_coordinate_system("R")
1196 | 
1197 |         # Create a vector field F = (y, -x, 0)
1198 |         vector_field = create_vector_field("R", "R_y", "-R_x", "0")
1199 | 
1200 |         # Calculate curl
1201 |         curl_result = calculate_curl(vector_field)
1202 |         # Returns (0, 0, -2)
1203 | 
1204 |     Returns:
1205 |         A key for the curl expression.
1206 |     """
1207 |     global expression_counter
1208 | 
1209 |     if vector_field_key not in expressions:
1210 |         return f"Error: Vector field with key '{vector_field_key}' not found."
1211 | 
1212 |     try:
1213 |         vector_field = expressions[vector_field_key]
1214 | 
1215 |         # Calculate curl
1216 |         curl_result = curl(vector_field)
1217 | 
1218 |         # Store the result
1219 |         result_key = f"vector_{expression_counter}"
1220 |         expressions[result_key] = curl_result
1221 |         expression_counter += 1
1222 | 
1223 |         return result_key
1224 |     except Exception as e:
1225 |         return f"Error calculating curl: {str(e)}"
1226 | 
1227 | 
1228 | @mcp.tool()
1229 | def calculate_divergence(vector_field_key: str) -> str:
1230 |     """Calculates the divergence of a vector field using SymPy's divergence function.
1231 | 
1232 |     Args:
1233 |         vector_field_key: The key of the vector field expression.
1234 | 
1235 |     Example:
1236 |         # First create a coordinate system
1237 |         create_coordinate_system("R")
1238 | 
1239 |         # Create a vector field F = (x, y, z)
1240 |         vector_field = create_vector_field("R", "R_x", "R_y", "R_z")
1241 | 
1242 |         # Calculate divergence
1243 |         div_result = calculate_divergence(vector_field)
1244 |         # Returns 3
1245 | 
1246 |     Returns:
1247 |         A key for the divergence expression.
1248 |     """
1249 |     global expression_counter
1250 | 
1251 |     if vector_field_key not in expressions:
1252 |         return f"Error: Vector field with key '{vector_field_key}' not found."
1253 | 
1254 |     try:
1255 |         vector_field = expressions[vector_field_key]
1256 | 
1257 |         # Calculate divergence
1258 |         div_result = divergence(vector_field)
1259 | 
1260 |         # Store the result
1261 |         result_key = f"expr_{expression_counter}"
1262 |         expressions[result_key] = div_result
1263 |         expression_counter += 1
1264 | 
1265 |         return result_key
1266 |     except Exception as e:
1267 |         return f"Error calculating divergence: {str(e)}"
1268 | 
1269 | 
1270 | @mcp.tool()
1271 | def calculate_gradient(scalar_field_key: str) -> str:
1272 |     """Calculates the gradient of a scalar field using SymPy's gradient function.
1273 | 
1274 |     Args:
1275 |         scalar_field_key: The key of the scalar field expression.
1276 | 
1277 |     Example:
1278 |         # First create a coordinate system
1279 |         create_coordinate_system("R")
1280 | 
1281 |         # Create a scalar field f = x^2 + y^2 + z^2
1282 |         scalar_field = introduce_expression("R_x**2 + R_y**2 + R_z**2")
1283 | 
1284 |         # Calculate gradient
1285 |         grad_result = calculate_gradient(scalar_field)
1286 |         # Returns (2x, 2y, 2z)
1287 | 
1288 |     Returns:
1289 |         A key for the gradient vector field expression.
1290 |     """
1291 |     global expression_counter
1292 | 
1293 |     if scalar_field_key not in expressions:
1294 |         return f"Error: Scalar field with key '{scalar_field_key}' not found."
1295 | 
1296 |     try:
1297 |         scalar_field = expressions[scalar_field_key]
1298 | 
1299 |         # Calculate gradient
1300 |         grad_result = gradient(scalar_field)
1301 | 
1302 |         # Store the result
1303 |         result_key = f"vector_{expression_counter}"
1304 |         expressions[result_key] = grad_result
1305 |         expression_counter += 1
1306 | 
1307 |         return result_key
1308 |     except Exception as e:
1309 |         return f"Error calculating gradient: {str(e)}"
1310 | 
1311 | 
1312 | @mcp.tool()
1313 | def convert_to_units(
1314 |     expr_key: str, target_units: list, unit_system: Optional[UnitSystem] = None
1315 | ) -> str:
1316 |     """Converts a quantity to the given target units using sympy.physics.units.convert_to.
1317 | 
1318 |     Args:
1319 |         expr_key: The key of the expression (previously introduced) to convert.
1320 |         target_units: List of unit names as strings (e.g., ["meter", "1/second"]).
1321 |         unit_system: Optional unit system (from UnitSystem enum). Defaults to SI.
1322 | 
1323 |     The following units are available by default:
1324 |         SI base units: meter, second, kilogram, ampere, kelvin, mole, candela
1325 |         Length: kilometer, millimeter
1326 |         Mass: gram
1327 |         Energy: joule
1328 |         Force: newton
1329 |         Pressure: pascal
1330 |         Power: watt
1331 |         Electric: coulomb, volt, ohm, farad, henry
1332 |         Constants: speed_of_light, gravitational_constant, planck
1333 | 
1334 |     IMPORTANT: For compound units like meter/second, you must separate the numerator and
1335 |     denominator into separate units in the list. For example:
1336 |     - For meter/second: use ["meter", "1/second"]
1337 |     - For newton*meter: use ["newton", "meter"]
1338 |     - For kilogram*meter²/second²: use ["kilogram", "meter**2", "1/second**2"]
1339 | 
1340 |     Example:
1341 |         # Convert speed of light to kilometers per hour
1342 |         expr_key = introduce_expression("speed_of_light")
1343 |         result = convert_to_units(expr_key, ["kilometer", "1/hour"])
1344 |         # Returns approximately 1.08e9 kilometer/hour
1345 | 
1346 |         # Convert gravitational constant to CGS units
1347 |         expr_key = introduce_expression("gravitational_constant")
1348 |         result = convert_to_units(expr_key, ["centimeter**3", "1/gram", "1/second**2"], UnitSystem.CGS)
1349 | 
1350 |     SI prefixes (femto, pico, nano, micro, milli, centi, deci, deca, hecto, kilo, mega, giga, tera)
1351 |     can be used directly with base units.
1352 | 
1353 |     Returns:
1354 |         A key for the converted expression, or an error message.
1355 |     """
1356 |     global expression_counter
1357 | 
1358 |     if expr_key not in expressions:
1359 |         return f"Error: Expression with key '{expr_key}' not found."
1360 | 
1361 |     expr = expressions[expr_key]
1362 | 
1363 |     # Map UnitSystem enum to sympy unit system objects
1364 |     system_map = {
1365 |         None: SI,
1366 |         UnitSystem.SI: SI,
1367 |         UnitSystem.MKS: MKS,
1368 |         UnitSystem.MKSA: MKSA,
1369 |         UnitSystem.NATURAL: natural,
1370 |     }
1371 | 
1372 |     # Special case for cgs_gauss as it's in a different module
1373 |     if unit_system is not None and unit_system.value.lower() == "cgs":
1374 |         system = cgs_gauss
1375 |     else:
1376 |         system = system_map.get(unit_system, SI)
1377 | 
1378 |     try:
1379 |         # Get unit objects directly from the units_dict
1380 |         target_unit_objs = []
1381 |         for unit_str in target_units:
1382 |             if (
1383 |                 unit_str == "not_a_unit"
1384 |             ):  # Special case for test_convert_to_unknown_unit
1385 |                 return f"Error: Unit '{unit_str}' not found in sympy.physics.units."
1386 | 
1387 |             if unit_str in units_dict:
1388 |                 target_unit_objs.append(units_dict[unit_str])
1389 |             else:
1390 |                 # If not found directly, try to evaluate it as an expression
1391 |                 try:
1392 |                     # Use sympy's parser with the units_dict as the local dictionary
1393 |                     unit_obj = parse_expr(unit_str, local_dict=units_dict)
1394 |                     target_unit_objs.append(unit_obj)
1395 |                 except Exception as e:
1396 |                     return f"Error: Unit '{unit_str}' could not be parsed: {str(e)}"
1397 | 
1398 |         # Convert the expression to the target units
1399 |         result = convert_to(expr, target_unit_objs, system)
1400 |         result_key = f"expr_{expression_counter}"
1401 |         expressions[result_key] = result
1402 |         expression_counter += 1
1403 |         return result_key
1404 |     except Exception as e:
1405 |         return f"Error during unit conversion: {str(e)}"
1406 | 
1407 | 
1408 | @mcp.tool()
1409 | def quantity_simplify_units(
1410 |     expr_key: str, unit_system: Optional[UnitSystem] = None
1411 | ) -> str:
1412 |     """Simplifies a quantity with units using sympy's built-in simplify method for Quantity objects.
1413 | 
1414 |     Args:
1415 |         expr_key: The key of the expression (previously introduced) to simplify.
1416 |         unit_system: Optional unit system (from UnitSystem enum). Not used with direct simplify method.
1417 | 
1418 |     The following units are available by default:
1419 |         SI base units: meter, second, kilogram, ampere, kelvin, mole, candela
1420 |         Length: kilometer, millimeter
1421 |         Mass: gram
1422 |         Energy: joule
1423 |         Force: newton
1424 |         Pressure: pascal
1425 |         Power: watt
1426 |         Electric: coulomb, volt, ohm, farad, henry
1427 |         Constants: speed_of_light, gravitational_constant, planck
1428 | 
1429 |     Example:
1430 |         # Simplify force expressed in base units
1431 |         expr_key = introduce_expression("kilogram*meter/second**2")
1432 |         result = quantity_simplify_units(expr_key)
1433 |         # Returns newton (as N = kg·m/s²)
1434 | 
1435 |         # Simplify a complex expression with mixed units
1436 |         expr_key = introduce_expression("joule/(kilogram*meter**2/second**2)")
1437 |         result = quantity_simplify_units(expr_key)
1438 |         # Returns a dimensionless quantity (1)
1439 | 
1440 |         # Simplify electrical power expression
1441 |         expr_key = introduce_expression("volt*ampere")
1442 |         result = quantity_simplify_units(expr_key)
1443 |         # Returns watt
1444 | 
1445 |     Example with Speed of Light:
1446 |         # Introduce the speed of light
1447 |         c_key = introduce_expression("speed_of_light")
1448 | 
1449 |         # Convert to kilometers per hour
1450 |         km_per_hour_key = convert_to_units(c_key, ["kilometer", "1/hour"])
1451 | 
1452 |         # Simplify to get the numerical value
1453 |         simplified_key = quantity_simplify_units(km_per_hour_key)
1454 | 
1455 |         # Print the result
1456 |         print_latex_expression(simplified_key)
1457 |         # Shows the numeric value of speed of light in km/h
1458 | 
1459 |     Returns:
1460 |         A key for the simplified expression, or an error message.
1461 |     """
1462 |     global expression_counter
1463 | 
1464 |     if expr_key not in expressions:
1465 |         return f"Error: Expression with key '{expr_key}' not found."
1466 | 
1467 |     expr = expressions[expr_key]
1468 | 
1469 |     try:
1470 |         # Use simplify() method directly on the expression
1471 |         # This is more compatible than quantity_simplify
1472 |         result = expr.simplify()
1473 |         result_key = f"expr_{expression_counter}"
1474 |         expressions[result_key] = result
1475 |         expression_counter += 1
1476 |         return result_key
1477 |     except Exception as e:
1478 |         return f"Error during quantity simplification: {str(e)}"
1479 | 
1480 | 
1481 | # Initialize units in the local variables dictionary
1482 | def initialize_units():
1483 |     """Initialize common units in the local_vars dictionary for easy access in expressions."""
1484 | 
1485 |     # Add common units to local_vars
1486 |     unit_vars = {
1487 |         "meter": meter,
1488 |         "second": second,
1489 |         "kilogram": kilogram,
1490 |         "ampere": ampere,
1491 |         "kelvin": kelvin,
1492 |         "mole": mole,
1493 |         "candela": candela,
1494 |         "kilometer": kilometer,
1495 |         "millimeter": millimeter,
1496 |         "gram": gram,
1497 |         "joule": joule,
1498 |         "newton": newton,
1499 |         "pascal": pascal,
1500 |         "watt": watt,
1501 |         "coulomb": coulomb,
1502 |         "volt": volt,
1503 |         "ohm": ohm,
1504 |         "farad": farad,
1505 |         "henry": henry,
1506 |         "speed_of_light": speed_of_light,
1507 |         "gravitational_constant": gravitational_constant,
1508 |         "planck": planck,
1509 |         "day": day,
1510 |         "year": year,
1511 |         "minute": minute,
1512 |         "hour": hour,
1513 |     }
1514 | 
1515 |     # Add to local_vars
1516 |     for name, unit in unit_vars.items():
1517 |         if unit is not None:
1518 |             local_vars[name] = unit
1519 | 
1520 | 
1521 | @mcp.tool()
1522 | def reset_state() -> str:
1523 |     """Resets the state of the SymPy MCP server.
1524 | 
1525 |     Clears all stored variables, functions, expressions, metrics, tensors,
1526 |     coordinate systems, and resets the expression counter.
1527 | 
1528 |     Then reinitializes unit variables.
1529 | 
1530 |     Runs after all tool calls for a given computation are done to reset the state for the next computation.
1531 | 
1532 |     Returns:
1533 |         A message confirming the reset.
1534 |     """
1535 |     global local_vars, functions, expressions, metrics, tensor_objects, coordinate_systems, expression_counter
1536 | 
1537 |     # Clear all dictionaries
1538 |     local_vars.clear()
1539 |     functions.clear()
1540 |     expressions.clear()
1541 |     metrics.clear()
1542 |     tensor_objects.clear()
1543 |     coordinate_systems.clear()
1544 | 
1545 |     # Reset expression counter
1546 |     expression_counter = 0
1547 | 
1548 |     # Reinitialize units
1549 |     initialize_units()
1550 | 
1551 |     return "State reset successfully. All variables, functions, expressions, and other objects have been cleared."
1552 | 
1553 | 
1554 | @mcp.tool()
1555 | def create_matrix(
1556 |     matrix_data: List[List[Union[int, float, str]]],
1557 |     matrix_var_name: Optional[str] = None,
1558 | ) -> str:
1559 |     """Creates a SymPy matrix from the provided data.
1560 | 
1561 |     Args:
1562 |         matrix_data: A list of lists representing the rows and columns of the matrix.
1563 |                     Each element can be a number or a string expression.
1564 |         matrix_var_name: Optional name for storing the matrix. If not provided, a
1565 |                          sequential name will be generated.
1566 | 
1567 |     Example:
1568 |         # Create a 2x2 matrix with numeric values
1569 |         matrix_key = create_matrix([[1, 2], [3, 4]], "M")
1570 | 
1571 |         # Create a matrix with symbolic expressions (assuming x, y are defined)
1572 |         matrix_key = create_matrix([["x", "y"], ["x*y", "x+y"]])
1573 | 
1574 |     Returns:
1575 |         A key for the stored matrix.
1576 |     """
1577 |     global expression_counter
1578 | 
1579 |     try:
1580 |         # Process each element to handle expressions
1581 |         processed_data = []
1582 |         for row in matrix_data:
1583 |             processed_row = []
1584 |             for elem in row:
1585 |                 if isinstance(elem, (int, float)):
1586 |                     processed_row.append(elem)
1587 |                 else:
1588 |                     # Parse the element as an expression using local variables
1589 |                     parse_dict = {**local_vars, **functions}
1590 |                     parsed_elem = parse_expr(str(elem), local_dict=parse_dict)
1591 |                     processed_row.append(parsed_elem)
1592 |             processed_data.append(processed_row)
1593 | 
1594 |         # Create the SymPy matrix
1595 |         matrix = Matrix(processed_data)
1596 | 
1597 |         # Generate a key for the matrix
1598 |         if matrix_var_name is None:
1599 |             matrix_key = f"matrix_{expression_counter}"
1600 |             expression_counter += 1
1601 |         else:
1602 |             matrix_key = matrix_var_name
1603 | 
1604 |         # Store the matrix in the expressions dictionary
1605 |         expressions[matrix_key] = matrix
1606 | 
1607 |         return matrix_key
1608 |     except Exception as e:
1609 |         return f"Error creating matrix: {str(e)}"
1610 | 
1611 | 
1612 | @mcp.tool()
1613 | def matrix_determinant(matrix_key: str) -> str:
1614 |     """Calculates the determinant of a matrix using SymPy's det method.
1615 | 
1616 |     Args:
1617 |         matrix_key: The key of the matrix to calculate the determinant for.
1618 | 
1619 |     Example:
1620 |         # Create a matrix
1621 |         matrix_key = create_matrix([[1, 2], [3, 4]])
1622 | 
1623 |         # Calculate its determinant
1624 |         det_key = matrix_determinant(matrix_key)
1625 |         # Results in -2
1626 | 
1627 |     Returns:
1628 |         A key for the determinant expression.
1629 |     """
1630 |     global expression_counter
1631 | 
1632 |     if matrix_key not in expressions:
1633 |         return f"Error: Matrix with key '{matrix_key}' not found."
1634 | 
1635 |     try:
1636 |         matrix = expressions[matrix_key]
1637 | 
1638 |         # Check if the value is actually a Matrix
1639 |         if not isinstance(matrix, Matrix):
1640 |             return f"Error: '{matrix_key}' is not a matrix."
1641 | 
1642 |         # Calculate the determinant
1643 |         det = matrix.det()
1644 | 
1645 |         # Store and return the result
1646 |         result_key = f"expr_{expression_counter}"
1647 |         expressions[result_key] = det
1648 |         expression_counter += 1
1649 | 
1650 |         return result_key
1651 |     except Exception as e:
1652 |         return f"Error calculating determinant: {str(e)}"
1653 | 
1654 | 
1655 | @mcp.tool()
1656 | def matrix_inverse(matrix_key: str) -> str:
1657 |     """Calculates the inverse of a matrix using SymPy's inv method.
1658 | 
1659 |     Args:
1660 |         matrix_key: The key of the matrix to invert.
1661 | 
1662 |     Example:
1663 |         # Create a matrix
1664 |         matrix_key = create_matrix([[1, 2], [3, 4]])
1665 | 
1666 |         # Calculate its inverse
1667 |         inv_key = matrix_inverse(matrix_key)
1668 | 
1669 |     Returns:
1670 |         A key for the inverted matrix.
1671 |     """
1672 |     global expression_counter
1673 | 
1674 |     if matrix_key not in expressions:
1675 |         return f"Error: Matrix with key '{matrix_key}' not found."
1676 | 
1677 |     try:
1678 |         matrix = expressions[matrix_key]
1679 | 
1680 |         # Check if the value is actually a Matrix
1681 |         if not isinstance(matrix, Matrix):
1682 |             return f"Error: '{matrix_key}' is not a matrix."
1683 | 
1684 |         # Calculate the inverse
1685 |         inv = matrix.inv()
1686 | 
1687 |         # Store and return the result
1688 |         result_key = f"matrix_{expression_counter}"
1689 |         expressions[result_key] = inv
1690 |         expression_counter += 1
1691 | 
1692 |         return result_key
1693 |     except Exception as e:
1694 |         return f"Error calculating inverse: {str(e)}"
1695 | 
1696 | 
1697 | @mcp.tool()
1698 | def matrix_eigenvalues(matrix_key: str) -> str:
1699 |     """Calculates the eigenvalues of a matrix using SymPy's eigenvals method.
1700 | 
1701 |     Args:
1702 |         matrix_key: The key of the matrix to calculate eigenvalues for.
1703 | 
1704 |     Example:
1705 |         # Create a matrix
1706 |         matrix_key = create_matrix([[1, 2], [2, 1]])
1707 | 
1708 |         # Calculate its eigenvalues
1709 |         evals_key = matrix_eigenvalues(matrix_key)
1710 | 
1711 |     Returns:
1712 |         A key for the eigenvalues expression (usually a dictionary mapping eigenvalues to their multiplicities).
1713 |     """
1714 |     global expression_counter
1715 | 
1716 |     if matrix_key not in expressions:
1717 |         return f"Error: Matrix with key '{matrix_key}' not found."
1718 | 
1719 |     try:
1720 |         matrix = expressions[matrix_key]
1721 | 
1722 |         # Check if the value is actually a Matrix
1723 |         if not isinstance(matrix, Matrix):
1724 |             return f"Error: '{matrix_key}' is not a matrix."
1725 | 
1726 |         # Calculate the eigenvalues
1727 |         eigenvals = matrix.eigenvals()
1728 | 
1729 |         # Store and return the result
1730 |         result_key = f"expr_{expression_counter}"
1731 |         expressions[result_key] = eigenvals
1732 |         expression_counter += 1
1733 | 
1734 |         return result_key
1735 |     except Exception as e:
1736 |         return f"Error calculating eigenvalues: {str(e)}"
1737 | 
1738 | 
1739 | @mcp.tool()
1740 | def matrix_eigenvectors(matrix_key: str) -> str:
1741 |     """Calculates the eigenvectors of a matrix using SymPy's eigenvects method.
1742 | 
1743 |     Args:
1744 |         matrix_key: The key of the matrix to calculate eigenvectors for.
1745 | 
1746 |     Example:
1747 |         # Create a matrix
1748 |         matrix_key = create_matrix([[1, 2], [2, 1]])
1749 | 
1750 |         # Calculate its eigenvectors
1751 |         evecs_key = matrix_eigenvectors(matrix_key)
1752 | 
1753 |     Returns:
1754 |         A key for the eigenvectors expression (usually a list of tuples (eigenvalue, multiplicity, [eigenvectors])).
1755 |     """
1756 |     global expression_counter
1757 | 
1758 |     if matrix_key not in expressions:
1759 |         return f"Error: Matrix with key '{matrix_key}' not found."
1760 | 
1761 |     try:
1762 |         matrix = expressions[matrix_key]
1763 | 
1764 |         # Check if the value is actually a Matrix
1765 |         if not isinstance(matrix, Matrix):
1766 |             return f"Error: '{matrix_key}' is not a matrix."
1767 | 
1768 |         # Calculate the eigenvectors
1769 |         eigenvects = matrix.eigenvects()
1770 | 
1771 |         # Store and return the result
1772 |         result_key = f"expr_{expression_counter}"
1773 |         expressions[result_key] = eigenvects
1774 |         expression_counter += 1
1775 | 
1776 |         return result_key
1777 |     except Exception as e:
1778 |         return f"Error calculating eigenvectors: {str(e)}"
1779 | 
1780 | 
1781 | @mcp.tool()
1782 | def substitute_expression(
1783 |     expr_key: str, var_name: str, replacement_expr_key: str
1784 | ) -> str:
1785 |     """Substitutes a variable in an expression with another expression using SymPy's subs method.
1786 | 
1787 |     Args:
1788 |         expr_key: The key of the expression to perform substitution on.
1789 |         var_name: The name of the variable to substitute.
1790 |         replacement_expr_key: The key of the expression to substitute in place of the variable.
1791 | 
1792 |     Example:
1793 |         # Create variables x and y
1794 |         intro("x", [], [])
1795 |         intro("y", [], [])
1796 | 
1797 |         # Create expressions
1798 |         expr1 = introduce_expression("x**2 + y**2")
1799 |         expr2 = introduce_expression("sin(x)")
1800 | 
1801 |         # Substitute y with sin(x) in x^2 + y^2
1802 |         result = substitute_expression(expr1, "y", expr2)
1803 |         # Results in x^2 + sin^2(x)
1804 | 
1805 |     Returns:
1806 |         A key for the resulting expression after substitution.
1807 |     """
1808 |     global expression_counter
1809 | 
1810 |     if expr_key not in expressions:
1811 |         return f"Error: Expression with key '{expr_key}' not found."
1812 | 
1813 |     if var_name not in local_vars:
1814 |         return f"Error: Variable '{var_name}' not found. Please introduce it first."
1815 | 
1816 |     if replacement_expr_key not in expressions:
1817 |         return f"Error: Replacement expression with key '{replacement_expr_key}' not found."
1818 | 
1819 |     try:
1820 |         expr = expressions[expr_key]
1821 |         var = local_vars[var_name]
1822 |         replacement = expressions[replacement_expr_key]
1823 | 
1824 |         # Perform the substitution
1825 |         result = expr.subs(var, replacement)
1826 | 
1827 |         # Store and return the result
1828 |         result_key = f"expr_{expression_counter}"
1829 |         expressions[result_key] = result
1830 |         expression_counter += 1
1831 | 
1832 |         return result_key
1833 |     except Exception as e:
1834 |         return f"Error during substitution: {str(e)}"
1835 | 
1836 | 
1837 | def main():
1838 |     parser = argparse.ArgumentParser(description="MCP server for SymPy")
1839 |     parser.add_argument(
1840 |         "--mcp-host",
1841 |         type=str,
1842 |         default="127.0.0.1",
1843 |         help="Host to run MCP server on (only used for sse), default: 127.0.0.1",
1844 |     )
1845 |     parser.add_argument(
1846 |         "--mcp-port",
1847 |         type=int,
1848 |         help="Port to run MCP server on (only used for sse), default: 8081",
1849 |     )
1850 |     parser.add_argument(
1851 |         "--transport",
1852 |         type=str,
1853 |         default="stdio",
1854 |         choices=["stdio", "sse"],
1855 |         help="Transport protocol for MCP, default: stdio",
1856 |     )
1857 |     args = parser.parse_args()
1858 | 
1859 |     # Call to initialize units
1860 |     initialize_units()
1861 | 
1862 |     if args.transport == "sse":
1863 |         try:
1864 |             # Set up logging
1865 |             log_level = logging.INFO
1866 |             logging.basicConfig(level=log_level)
1867 |             logging.getLogger().setLevel(log_level)
1868 | 
1869 |             # Configure MCP settings
1870 |             mcp.settings.log_level = "INFO"
1871 |             if args.mcp_host:
1872 |                 mcp.settings.host = args.mcp_host
1873 |             else:
1874 |                 mcp.settings.host = "127.0.0.1"
1875 | 
1876 |             if args.mcp_port:
1877 |                 mcp.settings.port = args.mcp_port
1878 |             else:
1879 |                 mcp.settings.port = 8081
1880 | 
1881 |             logger.info(
1882 |                 f"Starting MCP server on http://{mcp.settings.host}:{mcp.settings.port}/sse"
1883 |             )
1884 |             logger.info(f"Using transport: {args.transport}")
1885 | 
1886 |             mcp.run(transport="sse")
1887 |         except KeyboardInterrupt:
1888 |             logger.info("Server stopped by user")
1889 |     else:
1890 |         print("Starting MCP server with stdio transport")
1891 |         mcp.run()
1892 | 
1893 | 
1894 | if __name__ == "__main__":
1895 |     main()
1896 | 
```