calculator_server.py•9.93 kB
#!/usr/bin/env python3
"""
Calculator MCP Server
A Model Context Protocol server that provides calculator tools.
"""
import asyncio
import sys
from typing import Any, Sequence
try:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
except ImportError:
print("Error: mcp package not found. Please install it with: pip install mcp", file=sys.stderr)
sys.exit(1)
# Create the MCP server instance
server = Server("calculator-mcp-server")
@server.list_tools()
async def list_tools() -> list[Tool]:
"""List available calculator tools."""
return [
Tool(
name="add",
description="Add two or more numbers together",
inputSchema={
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"},
"description": "Array of numbers to add"
}
},
"required": ["numbers"]
}
),
Tool(
name="subtract",
description="Subtract numbers. Subtracts all subsequent numbers from the first",
inputSchema={
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"},
"description": "Array of numbers. First number is minuend, rest are subtrahends"
}
},
"required": ["numbers"]
}
),
Tool(
name="multiply",
description="Multiply two or more numbers together",
inputSchema={
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"},
"description": "Array of numbers to multiply"
}
},
"required": ["numbers"]
}
),
Tool(
name="divide",
description="Divide numbers. Divides first number by all subsequent numbers",
inputSchema={
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"},
"description": "Array of numbers. First number is dividend, rest are divisors"
}
},
"required": ["numbers"]
}
),
Tool(
name="power",
description="Raise a number to a power",
inputSchema={
"type": "object",
"properties": {
"base": {
"type": "number",
"description": "The base number"
},
"exponent": {
"type": "number",
"description": "The exponent"
}
},
"required": ["base", "exponent"]
}
),
Tool(
name="sqrt",
description="Calculate the square root of a number",
inputSchema={
"type": "object",
"properties": {
"number": {
"type": "number",
"description": "The number to calculate square root of"
}
},
"required": ["number"]
}
),
Tool(
name="evaluate",
description="Evaluate a mathematical expression safely",
inputSchema={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate (e.g., '2 + 2 * 3')"
}
},
"required": ["expression"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any] | None) -> Sequence[TextContent]:
"""Handle tool calls."""
if arguments is None:
arguments = {}
try:
if name == "add":
numbers = arguments.get("numbers", [])
if not numbers:
return [TextContent(type="text", text="Error: No numbers provided")]
result = sum(float(n) for n in numbers)
return [TextContent(type="text", text=str(result))]
elif name == "subtract":
numbers = arguments.get("numbers", [])
if not numbers:
return [TextContent(type="text", text="Error: No numbers provided")]
if len(numbers) < 2:
return [TextContent(type="text", text="Error: At least two numbers required for subtraction")]
result = float(numbers[0])
for n in numbers[1:]:
result -= float(n)
return [TextContent(type="text", text=str(result))]
elif name == "multiply":
numbers = arguments.get("numbers", [])
if not numbers:
return [TextContent(type="text", text="Error: No numbers provided")]
result = 1
for n in numbers:
result *= float(n)
return [TextContent(type="text", text=str(result))]
elif name == "divide":
numbers = arguments.get("numbers", [])
if not numbers:
return [TextContent(type="text", text="Error: No numbers provided")]
if len(numbers) < 2:
return [TextContent(type="text", text="Error: At least two numbers required for division")]
result = float(numbers[0])
for n in numbers[1:]:
divisor = float(n)
if divisor == 0:
return [TextContent(type="text", text="Error: Division by zero")]
result /= divisor
return [TextContent(type="text", text=str(result))]
elif name == "power":
base = arguments.get("base")
exponent = arguments.get("exponent")
if base is None or exponent is None:
return [TextContent(type="text", text="Error: Both base and exponent are required")]
result = float(base) ** float(exponent)
return [TextContent(type="text", text=str(result))]
elif name == "sqrt":
number = arguments.get("number")
if number is None:
return [TextContent(type="text", text="Error: Number is required")]
num = float(number)
if num < 0:
return [TextContent(type="text", text="Error: Cannot calculate square root of negative number")]
import math
result = math.sqrt(num)
return [TextContent(type="text", text=str(result))]
elif name == "evaluate":
expression = arguments.get("expression", "")
if not expression:
return [TextContent(type="text", text="Error: Expression is required")]
# Safe evaluation using eval with limited globals and locals
# Only allow basic math operations
allowed_names = {
"__builtins__": {},
"abs": abs,
"round": round,
"min": min,
"max": max,
"sum": sum,
"pow": pow,
}
# Import math functions
import math
math_functions = {
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"asin": math.asin,
"acos": math.acos,
"atan": math.atan,
"sinh": math.sinh,
"cosh": math.cosh,
"tanh": math.tanh,
"log": math.log,
"log10": math.log10,
"exp": math.exp,
"sqrt": math.sqrt,
"ceil": math.ceil,
"floor": math.floor,
"pi": math.pi,
"e": math.e,
}
allowed_names.update(math_functions)
try:
# Compile to AST first for safety
code = compile(expression, "<string>", "eval", flags=0)
result = eval(code, allowed_names)
return [TextContent(type="text", text=str(result))]
except Exception as e:
return [TextContent(type="text", text=f"Error evaluating expression: {str(e)}")]
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]
except Exception as e:
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def main():
"""Main entry point for the MCP server."""
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())