Calculator Server Example

A complete MCP server implementing basic arithmetic operations.

Overview

The Calculator server demonstrates:

  • ✅ Basic tool definition - Simple arithmetic operations
  • ✅ Typed parameters - Using TypedJsonRpc<T>
  • ✅ Error handling - Division by zero handling
  • ✅ Parameter validation - Required parameter checking
  • ✅ Structured output - JSON responses with operation details

Complete Code

Program.cs

using Mcp.Gateway.Tools;

var builder = WebApplication.CreateBuilder(args);

// Register MCP Gateway
builder.AddToolsService();

var app = builder.Build();

// stdio mode for GitHub Copilot
if (args.Contains("--stdio"))
{
    await ToolInvoker.RunStdioModeAsync(app.Services);
    return;
}

// HTTP mode
app.UseWebSockets();
app.UseProtocolVersionValidation();
app.MapStreamableHttpEndpoint("/mcp");

app.Run();

CalculatorTools.cs

using Mcp.Gateway.Tools;

namespace CalculatorMcpServer.Tools;

public class CalculatorTools
{
    [McpTool("add_numbers",
        Title = "Add Numbers",
        Description = "Adds two numbers and returns result. Example: 5 + 3 = 8")]
    public JsonRpcMessage AddNumbers(TypedJsonRpc<AddNumbersRequest> request)
    {
        var args = request.GetParams()
            ?? throw new ToolInvalidParamsException(
                "Parameters 'number1' and 'number2' are required.");

        var result = args.Number1 + args.Number2;
        
        return ToolResponse.Success(
            request.Id,
            new
            {
                result,
                operation = "addition"
            });
    }

    [McpTool("multiply_numbers",
        Title = "Multiply Numbers",
        Description = "Multiplies two numbers. Example: 5 * 3 = 15")]
    public JsonRpcMessage Multiply(TypedJsonRpc<MultiplyRequest> request)
    {
        var args = request.GetParams()
            ?? throw new ToolInvalidParamsException(
                "Parameters 'number1' and 'number2' are required.");
        
        var result = args.Number1 * args.Number2;
        
        return ToolResponse.Success(
            request.Id,
            new MultiplyResponse(result));
    }

    [McpTool("divide_numbers",
        Title = "Divide Numbers",
        Description = "Divides two numbers. Example: 10 / 2 = 5")]
    public JsonRpcMessage Divide(TypedJsonRpc<DivideRequest> request)
    {
        var args = request.GetParams()
            ?? throw new ToolInvalidParamsException(
                "Parameters 'number1' and 'number2' are required.");

        if (args.Number2 == 0)
        {
            throw new ToolInvalidParamsException("Cannot divide by zero.");
        }
        
        var result = args.Number1 / args.Number2;
        
        return ToolResponse.Success(
            request.Id,
            new { result, operation = "division" });
    }
}

// Request/Response models
public record AddNumbersRequest(double Number1, double Number2);
public record MultiplyRequest(double Number1, double Number2);
public record MultiplyResponse(double Result);
public record DivideRequest(double Number1, double Number2);

Running the Server

HTTP Mode

dotnet run

Server runs at: http://localhost:5000/mcp

stdio Mode (GitHub Copilot)

dotnet run -- --stdio

Testing

Using curl

List tools:

curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/list",
    "id": 1
  }'

Call add_numbers:

curl -X POST http://localhost:5000/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "add_numbers",
      "arguments": {
        "Number1": 5,
        "Number2": 3
      }
    },
    "id": 2
  }'

Response:

{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"result\":8,\"operation\":\"addition\"}"
      }
    ]
  },
  "id": 2
}

Using GitHub Copilot

Configure .mcp.json:

{
  "mcpServers": {
    "calculator": {
      "command": "dotnet",
      "args": [
        "run",
        "--project",
        "C:\\path\\to\\CalculatorMcpServer",
        "--",
        "--stdio"
      ]
    }
  }
}

Then in Copilot Chat:

@calculator add 5 and 3
@calculator multiply 10 by 7
@calculator divide 100 by 4

Key Concepts

1. TypedJsonRpc

Type-safe parameter handling:

public JsonRpcMessage AddNumbers(TypedJsonRpc<AddNumbersRequest> request)
{
    var args = request.GetParams();  // Returns AddNumbersRequest
    // args.Number1 and args.Number2 are strongly typed
}

2. Parameter Validation

Always validate required parameters:

var args = request.GetParams()
    ?? throw new ToolInvalidParamsException(
        "Parameters 'number1' and 'number2' are required.");

3. Error Handling

Use ToolInvalidParamsException for invalid input:

if (args.Number2 == 0)
{
    throw new ToolInvalidParamsException("Cannot divide by zero.");
}

This automatically returns a proper JSON-RPC error:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "detail": "Cannot divide by zero."
    }
  },
  "id": 3
}

Integration Tests

CalculatorToolsTests.cs

using Xunit;
using Mcp.Gateway.Tests;

public class CalculatorToolsTests
{
    [Fact]
    public async Task AddNumbers_ValidInput_ReturnsSum()
    {
        // Arrange
        using var server = new McpGatewayFixture();
        var client = server.CreateClient();
        
        var request = new
        {
            jsonrpc = "2.0",
            method = "tools/call",
            @params = new
            {
                name = "add_numbers",
                arguments = new { Number1 = 5, Number2 = 3 }
            },
            id = 1
        };
        
        // Act
        var response = await client.PostAsJsonAsync("/mcp", request);
        var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
        
        // Assert
        Assert.Equal(8, result.RootElement
            .GetProperty("result")
            .GetProperty("content")[0]
            .GetProperty("text")
            .GetString()
            .Deserialize<dynamic>().result);
    }
    
    [Fact]
    public async Task Divide_ByZero_ReturnsError()
    {
        // Arrange
        using var server = new McpGatewayFixture();
        var client = server.CreateClient();
        
        var request = new
        {
            jsonrpc = "2.0",
            method = "tools/call",
            @params = new
            {
                name = "divide_numbers",
                arguments = new { Number1 = 10, Number2 = 0 }
            },
            id = 2
        };
        
        // Act
        var response = await client.PostAsJsonAsync("/mcp", request);
        var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
        
        // Assert
        Assert.True(result.RootElement.TryGetProperty("error", out var error));
        Assert.Equal(-32602, error.GetProperty("code").GetInt32());
        Assert.Contains("Cannot divide by zero", 
            error.GetProperty("data").GetProperty("detail").GetString());
    }
}

Enhancements

1. Add More Operations

[McpTool("subtract_numbers")]
public JsonRpcMessage Subtract(TypedJsonRpc<SubtractRequest> request)
{
    var args = request.GetParams()!;
    return ToolResponse.Success(request.Id, 
        new { result = args.Number1 - args.Number2 });
}

[McpTool("power")]
public JsonRpcMessage Power(TypedJsonRpc<PowerRequest> request)
{
    var args = request.GetParams()!;
    return ToolResponse.Success(request.Id, 
        new { result = Math.Pow(args.Base, args.Exponent) });
}

2. Add History Tracking

public class CalculatorTools
{
    private readonly List<CalculationHistory> _history = new();
    
    [McpTool("add_numbers")]
    public JsonRpcMessage AddNumbers(TypedJsonRpc<AddNumbersRequest> request)
    {
        var args = request.GetParams()!;
        var result = args.Number1 + args.Number2;
        
        _history.Add(new CalculationHistory
        {
            Operation = "add",
            Operand1 = args.Number1,
            Operand2 = args.Number2,
            Result = result,
            Timestamp = DateTime.UtcNow
        });
        
        return ToolResponse.Success(request.Id, new { result });
    }
    
    [McpTool("get_history")]
    public JsonRpcMessage GetHistory(JsonRpcMessage request)
    {
        return ToolResponse.Success(request.Id, _history);
    }
}

3. Add Lifecycle Hooks

// In Program.cs
builder.AddToolLifecycleHook<LoggingToolLifecycleHook>();
builder.AddToolLifecycleHook<MetricsToolLifecycleHook>();

// Expose metrics endpoint
app.MapGet("/metrics", (IEnumerable<IToolLifecycleHook> hooks) =>
{
    var metricsHook = hooks.OfType<MetricsToolLifecycleHook>().FirstOrDefault();
    return Results.Json(metricsHook?.GetMetrics());
});

Source Code

Full source code available at:

See Also