mathmethods-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mathmethods/__init__.py +7 -0
- mathmethods/__main__.py +6 -0
- mathmethods/compiler.py +202 -0
- mathmethods/core/__init__.py +36 -0
- mathmethods/core/differentiation.py +122 -0
- mathmethods/core/dynamic_1d.py +567 -0
- mathmethods/core/dynamic_2d_conservative.py +283 -0
- mathmethods/core/dynamic_2d_lanchester.py +264 -0
- mathmethods/core/dynamic_2d_linear.py +269 -0
- mathmethods/core/dynamic_2d_non_homogeneous.py +710 -0
- mathmethods/core/dynamic_2d_nonlinear.py +298 -0
- mathmethods/core/integration.py +396 -0
- mathmethods/core/interpolation.py +126 -0
- mathmethods/core/monte_carlo.py +312 -0
- mathmethods/core/ode.py +154 -0
- mathmethods/core/root_finding.py +339 -0
- mathmethods/core/utils.py +226 -0
- mathmethods/server.py +1414 -0
- mathmethods_mcp-0.1.0.dist-info/METADATA +436 -0
- mathmethods_mcp-0.1.0.dist-info/RECORD +24 -0
- mathmethods_mcp-0.1.0.dist-info/WHEEL +5 -0
- mathmethods_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- mathmethods_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- mathmethods_mcp-0.1.0.dist-info/top_level.txt +1 -0
mathmethods/__init__.py
ADDED
mathmethods/__main__.py
ADDED
mathmethods/compiler.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Hardened math expression validation for the MCP server.
|
|
2
|
+
|
|
3
|
+
The vendored math core trusts the strings it receives. Since MCP tools are
|
|
4
|
+
driven by an LLM (which can be tricked by prompt injection), this module adds
|
|
5
|
+
defense in depth before any string reaches the math core:
|
|
6
|
+
|
|
7
|
+
- Length cap (mitigates denial-of-service on pathological inputs)
|
|
8
|
+
- Symbol whitelist (no undeclared variables)
|
|
9
|
+
- Function whitelist (only standard math functions)
|
|
10
|
+
- Notation normalization (``e^x`` -> ``E**x``, ``sen`` -> ``sin``, ``^`` -> ``**``)
|
|
11
|
+
- Strict lexical check BEFORE parsing (this is what stops code execution)
|
|
12
|
+
|
|
13
|
+
**Important:** SymPy's ``sympify`` evaluates strings with ``eval`` internally
|
|
14
|
+
and, on some versions, allows attribute access and ``__import__`` (verified:
|
|
15
|
+
``sympify("__import__('os').system('echo HACK')")`` executes the command).
|
|
16
|
+
Therefore the lexical whitelist below runs FIRST and rejects any token that is
|
|
17
|
+
not a number, an allowed variable/constant or an allowed function name, as well
|
|
18
|
+
as any ``.`` or ``__``. Only after that gate is it safe to call ``sympify``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
25
|
+
|
|
26
|
+
import sympy as sp
|
|
27
|
+
|
|
28
|
+
MAX_EXPRESSION_LENGTH = 200
|
|
29
|
+
|
|
30
|
+
# Reject integer literals with more than this many digits to keep sympify's
|
|
31
|
+
# eager evaluation (e.g. factorial(99999999)) from exhausting memory.
|
|
32
|
+
MAX_INTEGER_DIGITS = 6
|
|
33
|
+
|
|
34
|
+
# SymPy canonical function names allowed inside expressions.
|
|
35
|
+
ALLOWED_FUNCTIONS: frozenset[str] = frozenset(
|
|
36
|
+
{
|
|
37
|
+
# trigonometric
|
|
38
|
+
"sin", "cos", "tan", "cot", "sec", "csc",
|
|
39
|
+
"asin", "acos", "atan", "acot", "asec", "acsc", "atan2",
|
|
40
|
+
"sinh", "cosh", "tanh", "coth", "sech", "csch",
|
|
41
|
+
"asinh", "acosh", "atanh",
|
|
42
|
+
# exponential / logarithmic / roots
|
|
43
|
+
"exp", "log", "ln", "sqrt", "cbrt", "root",
|
|
44
|
+
# absolute value / sign / rounding
|
|
45
|
+
"Abs", "sign", "floor", "ceiling", "frac",
|
|
46
|
+
# gamma family
|
|
47
|
+
"gamma", "loggamma", "factorial", "binomial", "subfactorial",
|
|
48
|
+
# special functions
|
|
49
|
+
"erf", "erfc", "erfi",
|
|
50
|
+
"Min", "Max",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Numeric constants always available regardless of the allowed symbol list.
|
|
55
|
+
_BUILTIN_CONSTANTS = {"e": sp.E, "pi": sp.pi, "E": sp.E}
|
|
56
|
+
|
|
57
|
+
_SINGLE_VAR = re.compile(r"(?<![A-Za-z0-9_])[eE]\^")
|
|
58
|
+
# Unicode letters (incl. Greek symbols like α, β) start identifiers; digits
|
|
59
|
+
# and leading underscores are rejected by the lexical gate.
|
|
60
|
+
_IDENTIFIER_RE = re.compile(r"[^\W\d_]\w*", re.UNICODE)
|
|
61
|
+
_NUMBER_RE = re.compile(r"(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
|
|
62
|
+
_ALLOWED_PUNCTUATION = set("()+-,*/%<>!=:")
|
|
63
|
+
|
|
64
|
+
# Logic keywords SymPy understands; harmless in expressions.
|
|
65
|
+
_LOGIC_KEYWORDS = frozenset({"and", "or", "not"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize(expr: str) -> str:
|
|
69
|
+
"""Normalize common math notations to SymPy syntax.
|
|
70
|
+
|
|
71
|
+
Converts ``e^x`` / ``e^(-x)`` into ``E**x`` / ``E**(-x)`` (the old
|
|
72
|
+
``exp(`` replacement left unbalanced parentheses), ``sen`` into ``sin``,
|
|
73
|
+
``ln`` into ``log`` and every remaining ``^`` into ``**``.
|
|
74
|
+
"""
|
|
75
|
+
out = expr.replace("sen", "sin")
|
|
76
|
+
out = out.replace("ln", "log")
|
|
77
|
+
out = _SINGLE_VAR.sub("E**", out)
|
|
78
|
+
out = out.replace("^", "**")
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_lexical(expr: str, allowed_identifiers: set[str]) -> None:
|
|
83
|
+
"""Reject any token that is not a number, variable, constant or allowed
|
|
84
|
+
function, and any attribute access (``.`` / ``__``).
|
|
85
|
+
|
|
86
|
+
Runs *before* ``sympify`` so arbitrary Python code can never be evaluated.
|
|
87
|
+
"""
|
|
88
|
+
allowed_words = (
|
|
89
|
+
allowed_identifiers
|
|
90
|
+
| set(ALLOWED_FUNCTIONS)
|
|
91
|
+
| set(_BUILTIN_CONSTANTS)
|
|
92
|
+
| _LOGIC_KEYWORDS
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
i, n = 0, len(expr)
|
|
96
|
+
while i < n:
|
|
97
|
+
ch = expr[i]
|
|
98
|
+
if ch.isspace():
|
|
99
|
+
i += 1
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
number = _NUMBER_RE.match(expr, i)
|
|
103
|
+
if number:
|
|
104
|
+
text = number.group()
|
|
105
|
+
int_part = text.split(".")[0].split("e")[0].split("E")[0].lstrip("+-")
|
|
106
|
+
if len(int_part) > MAX_INTEGER_DIGITS:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Integer literal too large ({len(int_part)} digits, max {MAX_INTEGER_DIGITS})."
|
|
109
|
+
)
|
|
110
|
+
i = number.end()
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
ident = _IDENTIFIER_RE.match(expr, i)
|
|
114
|
+
if ident:
|
|
115
|
+
token = ident.group()
|
|
116
|
+
if token not in allowed_words:
|
|
117
|
+
raise ValueError(f"Unknown or forbidden token: {token}")
|
|
118
|
+
i = ident.end()
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if ch in _ALLOWED_PUNCTUATION:
|
|
122
|
+
i += 1
|
|
123
|
+
continue
|
|
124
|
+
if ch == ".":
|
|
125
|
+
raise ValueError("Attribute access is not allowed.")
|
|
126
|
+
if ch == "_":
|
|
127
|
+
raise ValueError("Underscore identifiers are not allowed.")
|
|
128
|
+
raise ValueError(f"Invalid character: {ch!r}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate(expr_str: str, variables: Sequence[str] = ("x",)) -> str:
|
|
132
|
+
"""Validate a math expression string and return its normalized form.
|
|
133
|
+
|
|
134
|
+
Raises ``ValueError`` if the expression is too long, malformed, uses
|
|
135
|
+
forbidden functions or references symbols outside ``variables``.
|
|
136
|
+
"""
|
|
137
|
+
if not expr_str or not expr_str.strip():
|
|
138
|
+
raise ValueError("Expression cannot be empty.")
|
|
139
|
+
if len(expr_str) > MAX_EXPRESSION_LENGTH:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Expression too long ({len(expr_str)} chars, max {MAX_EXPRESSION_LENGTH})."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
normalized = normalize(expr_str)
|
|
145
|
+
|
|
146
|
+
_check_lexical(normalized, {str(name) for name in variables})
|
|
147
|
+
|
|
148
|
+
local_dict: dict[str, sp.Symbol] = {}
|
|
149
|
+
for name in variables:
|
|
150
|
+
local_dict[str(name)] = sp.Symbol(str(name), real=True)
|
|
151
|
+
local_dict.update(_BUILTIN_CONSTANTS)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
expr = sp.sympify(normalized, locals=local_dict)
|
|
155
|
+
except Exception as exc: # SymPy raises a variety of parse errors
|
|
156
|
+
raise ValueError(f"Invalid math expression: {exc}") from exc
|
|
157
|
+
|
|
158
|
+
allowed_symbols = {str(name) for name in variables}
|
|
159
|
+
extra = {str(sym) for sym in expr.free_symbols} - allowed_symbols
|
|
160
|
+
if extra:
|
|
161
|
+
raise ValueError(f"Unknown symbol(s): {sorted(extra)}")
|
|
162
|
+
|
|
163
|
+
for fn in expr.atoms(sp.Function):
|
|
164
|
+
name = fn.func.__name__
|
|
165
|
+
if name not in ALLOWED_FUNCTIONS:
|
|
166
|
+
raise ValueError(f"Function not allowed: {name}")
|
|
167
|
+
|
|
168
|
+
return normalized
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compile_callable(
|
|
172
|
+
expr_str: str,
|
|
173
|
+
variables: Sequence[str] = ("x",),
|
|
174
|
+
modules: Sequence[str] = ("numpy",),
|
|
175
|
+
) -> Callable:
|
|
176
|
+
"""Validate an expression and return a vectorized NumPy callable.
|
|
177
|
+
|
|
178
|
+
The returned callable accepts the variable(s) in the same order as
|
|
179
|
+
``variables`` (a single callable for one variable, multiple positional
|
|
180
|
+
arguments for several variables).
|
|
181
|
+
"""
|
|
182
|
+
normalized = validate(expr_str, variables)
|
|
183
|
+
symbols = [sp.Symbol(str(name), real=True) for name in variables]
|
|
184
|
+
local_dict = {str(name): sym for name, sym in zip(variables, symbols, strict=True)}
|
|
185
|
+
local_dict.update(_BUILTIN_CONSTANTS)
|
|
186
|
+
expr = sp.sympify(normalized, locals=local_dict)
|
|
187
|
+
return sp.lambdify(symbols, expr, modules=list(modules))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def sanitize_and_check(
|
|
191
|
+
expr_str: str, variables: Iterable[str] = ("x",)
|
|
192
|
+
) -> tuple[str, sp.Expr]:
|
|
193
|
+
"""Return ``(normalized_str, parsed_expr)`` after validation.
|
|
194
|
+
|
|
195
|
+
Useful for tests and for callers that need the symbolic expression too.
|
|
196
|
+
"""
|
|
197
|
+
normalized = validate(expr_str, list(variables))
|
|
198
|
+
symbols = [sp.Symbol(str(name), real=True) for name in variables]
|
|
199
|
+
local_dict = {str(name): sym for name, sym in zip(variables, symbols, strict=True)}
|
|
200
|
+
local_dict.update(_BUILTIN_CONSTANTS)
|
|
201
|
+
expr = sp.sympify(normalized, locals=local_dict)
|
|
202
|
+
return normalized, expr
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Vendored numerical-methods core.
|
|
2
|
+
|
|
3
|
+
These modules are vendored from ``modeladoYsimulacion-web/backend/app/methods``
|
|
4
|
+
so this MCP server is fully self-contained and publishable. Keep them in sync
|
|
5
|
+
with the upstream repo when that code changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
differentiation,
|
|
10
|
+
dynamic_1d,
|
|
11
|
+
dynamic_2d_conservative,
|
|
12
|
+
dynamic_2d_lanchester,
|
|
13
|
+
dynamic_2d_linear,
|
|
14
|
+
dynamic_2d_non_homogeneous,
|
|
15
|
+
dynamic_2d_nonlinear,
|
|
16
|
+
integration,
|
|
17
|
+
interpolation,
|
|
18
|
+
monte_carlo,
|
|
19
|
+
ode,
|
|
20
|
+
root_finding,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"differentiation",
|
|
25
|
+
"dynamic_1d",
|
|
26
|
+
"dynamic_2d_conservative",
|
|
27
|
+
"dynamic_2d_lanchester",
|
|
28
|
+
"dynamic_2d_linear",
|
|
29
|
+
"dynamic_2d_non_homogeneous",
|
|
30
|
+
"dynamic_2d_nonlinear",
|
|
31
|
+
"integration",
|
|
32
|
+
"interpolation",
|
|
33
|
+
"monte_carlo",
|
|
34
|
+
"ode",
|
|
35
|
+
"root_finding",
|
|
36
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Derivación Numérica - Diferencias Finitas
|
|
3
|
+
- Primera derivada
|
|
4
|
+
- Segunda derivada
|
|
5
|
+
"""
|
|
6
|
+
import sympy as sp
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import Dict, Callable
|
|
10
|
+
from .utils import safe_sympify
|
|
11
|
+
|
|
12
|
+
class DifferentiationService:
|
|
13
|
+
|
|
14
|
+
# --- TUS MÉTODOS ORIGINALES (INTACTOS POR COMPATIBILIDAD) ---
|
|
15
|
+
@staticmethod
|
|
16
|
+
def compilar_funcion(texto_funcion: str) -> Callable:
|
|
17
|
+
"""Convierte string a función callable."""
|
|
18
|
+
try:
|
|
19
|
+
x = sp.Symbol('x')
|
|
20
|
+
expr = safe_sympify(texto_funcion, allowed_symbols=['x'])
|
|
21
|
+
return sp.lambdify(x, expr, 'numpy'), expr
|
|
22
|
+
except Exception as e:
|
|
23
|
+
raise ValueError(f"Error compilando función: {str(e)}")
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def diferencias_finitas(f: Callable, x_val: float, h: float = 1e-5, precision: int = 8) -> Dict:
|
|
27
|
+
"""Calcula primera y segunda derivada con diferencias centrales."""
|
|
28
|
+
f1_aprox = (f(x_val + h) - f(x_val - h)) / (2 * h)
|
|
29
|
+
f2_aprox = (f(x_val + h) - 2*f(x_val) + f(x_val - h)) / (h**2)
|
|
30
|
+
return {
|
|
31
|
+
"x": round(x_val, precision), "h": round(h, precision),
|
|
32
|
+
"primera_derivada": {"aproximada": round(f1_aprox, precision), "formula": "(f(x+h) - f(x-h)) / (2h)"},
|
|
33
|
+
"segunda_derivada": {"aproximada": round(f2_aprox, precision), "formula": "(f(x+h) - 2f(x) + f(x-h)) / h²"}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def diferencias_finitas_con_exacto(f_str: str, x_val: float, h: float = 1e-5, precision: int = 8) -> Dict:
|
|
38
|
+
"""Calcula derivadas numéricas Y las compara con exactas."""
|
|
39
|
+
f, expr = DifferentiationService.compilar_funcion(f_str)
|
|
40
|
+
x = sp.Symbol('x')
|
|
41
|
+
df1_expr = sp.diff(expr, x)
|
|
42
|
+
df2_expr = sp.diff(df1_expr, x)
|
|
43
|
+
f1_exacta = float(df1_expr.subs(x, x_val).evalf())
|
|
44
|
+
f2_exacta = float(df2_expr.subs(x, x_val).evalf())
|
|
45
|
+
f1_aprox = (f(x_val + h) - f(x_val - h)) / (2 * h)
|
|
46
|
+
f2_aprox = (f(x_val + h) - 2*f(x_val) + f(x_val - h)) / (h**2)
|
|
47
|
+
# Aquí también deberíamos usar .evalf() si queremos máxima precisión, pero el problema
|
|
48
|
+
# estaba en el pre-redondeo que hacíamos para la nueva web.
|
|
49
|
+
error_1 = abs(f1_exacta - f1_aprox)
|
|
50
|
+
error_2 = abs(f2_exacta - f2_aprox)
|
|
51
|
+
return {
|
|
52
|
+
"x": round(x_val, precision), "h": round(h, precision),
|
|
53
|
+
"primera_derivada": {"numerica": round(f1_aprox, precision), "exacta": round(f1_exacta, precision), "error": round(error_1, 8), "formula_numerica": "(f(x+h) - f(x-h)) / (2h)"},
|
|
54
|
+
"segunda_derivada": {"numerica": round(f2_aprox, precision), "exacta": round(f2_exacta, precision), "error": round(error_2, 8), "formula_numerica": "(f(x+h) - 2f(x) + f(x-h)) / h²"}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# --- NUESTRO MÉTODO NUEVO UNIFICADO (ACTUALIZADO PARA FORMATOS) ---
|
|
58
|
+
@staticmethod
|
|
59
|
+
def calcular_diferencias_completas(func_str: str, x_val: float, h: float, precision: int = 8) -> Dict:
|
|
60
|
+
"""Calcula diferencias Progresivas, Regresivas y Centrales enviando error crudo."""
|
|
61
|
+
try:
|
|
62
|
+
# Tratamiento especial para 'log' en SymPy (por defecto es ln)
|
|
63
|
+
# si el usuario escribe 'log10', SymPy lo manejará. 'ln' es el estándar.
|
|
64
|
+
|
|
65
|
+
x_sym = sp.Symbol('x')
|
|
66
|
+
diccionario_local = {'e': sp.E, 'pi': sp.pi}
|
|
67
|
+
expr = safe_sympify(func_str, local_dict=diccionario_local, allowed_symbols=['x'])
|
|
68
|
+
f = sp.lambdify(x_sym, expr, 'numpy')
|
|
69
|
+
|
|
70
|
+
dev_expr = sp.diff(expr, x_sym)
|
|
71
|
+
dev_exacta = float(dev_expr.subs(x_sym, x_val).evalf())
|
|
72
|
+
|
|
73
|
+
with warnings.catch_warnings():
|
|
74
|
+
warnings.simplefilter("ignore")
|
|
75
|
+
fx = float(f(x_val))
|
|
76
|
+
fx_mas_h = float(f(x_val + h))
|
|
77
|
+
fx_menos_h = float(f(x_val - h))
|
|
78
|
+
|
|
79
|
+
progresiva = (fx_mas_h - fx) / h
|
|
80
|
+
regresiva = (fx - fx_menos_h) / h
|
|
81
|
+
central = (fx_mas_h - fx_menos_h) / (2 * h)
|
|
82
|
+
|
|
83
|
+
dev2_expr = sp.diff(dev_expr, x_sym)
|
|
84
|
+
dev2_exacta = float(dev2_expr.subs(x_sym, x_val).evalf())
|
|
85
|
+
segunda_central = (fx_mas_h - 2*fx + fx_menos_h) / (h**2)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"x_evaluado": round(x_val, precision),
|
|
89
|
+
"h_usado": h,
|
|
90
|
+
"f_x": round(fx, precision),
|
|
91
|
+
"derivada_exacta": round(dev_exacta, precision),
|
|
92
|
+
"segunda_derivada_exacta": round(dev2_exacta, precision),
|
|
93
|
+
"resultados": [
|
|
94
|
+
{
|
|
95
|
+
"metodo": "Diferencia Progresiva",
|
|
96
|
+
"formula": "[f(x+h) - f(x)] / h",
|
|
97
|
+
"valor": round(progresiva, precision),
|
|
98
|
+
# CONEXIÓN: Enviamos el error crudo sin redondear
|
|
99
|
+
"error_raw": abs(dev_exacta - progresiva)
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"metodo": "Diferencia Regresiva",
|
|
103
|
+
"formula": "[f(x) - f(x-h)] / h",
|
|
104
|
+
"valor": round(regresiva, precision),
|
|
105
|
+
"error_raw": abs(dev_exacta - regresiva)
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"metodo": "Diferencia Central",
|
|
109
|
+
"formula": "[f(x+h) - f(x-h)] / 2h",
|
|
110
|
+
"valor": round(central, precision),
|
|
111
|
+
"error_raw": abs(dev_exacta - central)
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"metodo": "Segunda Derivada (Central)",
|
|
115
|
+
"formula": "[f(x+h) - 2f(x) + f(x-h)] / h²",
|
|
116
|
+
"valor": round(segunda_central, precision),
|
|
117
|
+
"error_raw": abs(dev2_exacta - segunda_central)
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise ValueError(f"Error evaluando la función: {str(e)}")
|