MCP compliance, safety and math fixes; English tool docs; symbolic dsolve
Fork of https://github.com/thinkitpossible/CalcMCP with: - tools/list now publishes inputSchema (upstream used non-standard 'params' key, leaving clients with no parameter information) - tool descriptions and parameter docs rewritten in English with examples - JSON-RPC notifications are no longer answered; tool failures set isError - raw eval() of tool arguments removed (json.loads/ast.literal_eval for matrices, sympy.lambdify for the numeric ODE right-hand side) - complex eigenvalues serialize as 'a+bi' strings instead of crashing - solve accepts '=' equations; ttest accepts {"popmean": ...} params - new symbolic_tool operation 'dsolve' for symbolic ODE solutions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,18 @@ except Exception:
|
||||
from scipy.integrate import odeint
|
||||
from scipy.fftpack import fft, rfft
|
||||
from typing import Optional
|
||||
import ast
|
||||
import json
|
||||
|
||||
|
||||
def _json_default(obj):
|
||||
"""JSON fallback for numpy/complex values (eigenvalues are often complex)."""
|
||||
if isinstance(obj, complex):
|
||||
return obj.real if obj.imag == 0 else f"{obj.real}{obj.imag:+}i"
|
||||
if hasattr(obj, "item"):
|
||||
return _json_default(obj.item())
|
||||
return str(obj)
|
||||
|
||||
# ============================================================================
|
||||
# SYMBOLIC MATHEMATICS (SymPy)
|
||||
# ============================================================================
|
||||
@@ -91,9 +101,14 @@ def symbolic_limit(expression: str, variable: str, point: str) -> str:
|
||||
|
||||
|
||||
def solve_equation(equation: str, variable: str) -> str:
|
||||
"""解方程"""
|
||||
"""Solve an equation. Accepts 'expr' (treated as expr = 0), 'lhs = rhs' or 'lhs == rhs'."""
|
||||
try:
|
||||
expr = sp.sympify(equation)
|
||||
eq_str = equation.replace("==", "=")
|
||||
if "=" in eq_str:
|
||||
lhs, rhs = eq_str.split("=", 1)
|
||||
expr = sp.Eq(sp.sympify(lhs), sp.sympify(rhs))
|
||||
else:
|
||||
expr = sp.sympify(eq_str)
|
||||
var = sp.Symbol(variable)
|
||||
result = sp.solve(expr, var)
|
||||
return str(result)
|
||||
@@ -101,6 +116,22 @@ def solve_equation(equation: str, variable: str) -> str:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
|
||||
def symbolic_dsolve(equation: str, variable: str = "x") -> str:
|
||||
"""Solve an ODE symbolically. Unknown function must be written as y(x),
|
||||
e.g. 'Derivative(y(x), x, 2) + y(x) = 0' -> y(x) = C1*sin(x) + C2*cos(x)."""
|
||||
try:
|
||||
eq_str = equation.replace("==", "=")
|
||||
if "=" in eq_str:
|
||||
lhs, rhs = eq_str.split("=", 1)
|
||||
eq = sp.Eq(sp.sympify(lhs), sp.sympify(rhs))
|
||||
else:
|
||||
eq = sp.sympify(eq_str)
|
||||
result = sp.dsolve(eq)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
|
||||
def taylor_series(expression: str, variable: str, point: str, n: int) -> str:
|
||||
"""泰勒级数展开"""
|
||||
try:
|
||||
@@ -185,8 +216,8 @@ def numpy_linear_algebra(matrix_a, operation: str, matrix_b=None) -> str:
|
||||
if isinstance(matrix_a, str):
|
||||
try:
|
||||
mat1 = np.array(json.loads(matrix_a), dtype=float)
|
||||
except:
|
||||
mat1 = np.array(eval(matrix_a), dtype=float)
|
||||
except Exception:
|
||||
mat1 = np.array(ast.literal_eval(matrix_a), dtype=float)
|
||||
else:
|
||||
mat1 = np.array(matrix_a, dtype=float)
|
||||
|
||||
@@ -195,27 +226,19 @@ def numpy_linear_algebra(matrix_a, operation: str, matrix_b=None) -> str:
|
||||
if isinstance(matrix_b, str):
|
||||
try:
|
||||
mat2 = np.array(json.loads(matrix_b), dtype=float)
|
||||
except:
|
||||
mat2 = np.array(eval(matrix_b), dtype=float)
|
||||
except Exception:
|
||||
mat2 = np.array(ast.literal_eval(matrix_b), dtype=float)
|
||||
else:
|
||||
mat2 = np.array(matrix_b, dtype=float)
|
||||
|
||||
# 执行操作
|
||||
if operation == "eigenvalues":
|
||||
if operation in ("eigenvalues", "eigenvectors"):
|
||||
eigenvalues, eigenvectors = np.linalg.eig(mat1)
|
||||
result = {
|
||||
"eigenvalues": eigenvalues.tolist(),
|
||||
"eigenvectors": eigenvectors.tolist()
|
||||
}
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
elif operation == "eigenvectors":
|
||||
eigenvalues, eigenvectors = np.linalg.eig(mat1)
|
||||
result = {
|
||||
"eigenvalues": eigenvalues.tolist(),
|
||||
"eigenvectors": eigenvectors.tolist()
|
||||
}
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
return json.dumps(result, ensure_ascii=False, default=_json_default)
|
||||
|
||||
elif operation == "determinant":
|
||||
result = float(np.linalg.det(mat1))
|
||||
@@ -434,21 +457,17 @@ def scipy_solve_ode(expression: str, initial_conditions: str, t_values: str, var
|
||||
else:
|
||||
reverse_order = False
|
||||
|
||||
# 评估 ODE 函数表达式
|
||||
try:
|
||||
# 尝试作为 lambda 函数字符串
|
||||
ode_func = eval(expression)
|
||||
except:
|
||||
# 如果失败,尝试作为符号表达式
|
||||
var_list = [sp.Symbol(v.strip()) for v in variables.split(",")]
|
||||
t_sym = sp.Symbol('t')
|
||||
expr = sp.sympify(expression)
|
||||
|
||||
if len(var_list) == 1:
|
||||
ode_func = sp.lambdify([t_sym, var_list[0]], expr, modules=['numpy'])
|
||||
ode_func_wrapped = lambda y, t: ode_func(t, y)
|
||||
else:
|
||||
return "Error: System ODEs not yet fully supported"
|
||||
# Parse dy/dt = f(t, y) as a SymPy expression (no eval of raw Python)
|
||||
var_list = [sp.Symbol(v.strip()) for v in (variables or "y").split(",")]
|
||||
t_sym = sp.Symbol('t')
|
||||
expr = sp.sympify(expression)
|
||||
|
||||
if len(var_list) == 1:
|
||||
f = sp.lambdify([t_sym, var_list[0]], expr, modules=['numpy'])
|
||||
# odeint calls func(y, t)
|
||||
ode_func = lambda y, t: f(t, y)
|
||||
else:
|
||||
return "Error: System ODEs not yet fully supported"
|
||||
|
||||
# 调用 odeint
|
||||
if isinstance(y0, (int, float)):
|
||||
@@ -489,6 +508,8 @@ def scipy_statistics(operation: str, data: str, params: Optional[str] = None) ->
|
||||
return str(float(np.std(data_arr)))
|
||||
elif operation == "ttest":
|
||||
popmean = json.loads(params) if params else 0
|
||||
if isinstance(popmean, dict):
|
||||
popmean = popmean.get("popmean", 0)
|
||||
result = stats.ttest_1samp(data_arr, popmean)
|
||||
return f"statistic={result.statistic}, pvalue={result.pvalue}"
|
||||
elif operation == "pearsonr" and params:
|
||||
@@ -560,6 +581,8 @@ def symbolic_tool(
|
||||
return symbolic_limit(expression, variable, point or point_value)
|
||||
if op in ("solve", "solve_equation"):
|
||||
return solve_equation(equation or expression, variable)
|
||||
if op in ("dsolve", "ode", "solve_ode", "differential_equation"):
|
||||
return symbolic_dsolve(equation or expression, variable or "x")
|
||||
if op in ("taylor", "taylor_series"):
|
||||
return taylor_series(expression, variable, point or "0", order)
|
||||
if op in ("matrix", "symbolic_matrix_operations"):
|
||||
|
||||
Reference in New Issue
Block a user