MCP Tool Server

The GVA VoIP Agent includes an MCP (Model Context Protocol) client that connects to tool servers, extending the AI assistant's capabilities. MCP provides a standardised way for LLMs to interact with external tools, databases, and services.

Overview

graph LR subgraph "VoIP Agent" LLM[Ollama LLM] MCP[MCP Client] end subgraph "MCP Servers" WX[Weather Server] FS[Filesystem Server] FT[Fetch Server] MEM[Memory Server] MIL[Military Tools
Native C++] end LLM <--> MCP MCP <-->|stdio| WX MCP <-->|stdio| FS MCP <-->|stdio| FT MCP <-->|stdio| MEM MCP <--> MIL style MCP fill:#9C27B0 style MIL fill:#4CAF50

Architecture

MCP uses JSON-RPC 2.0 over stdio for communication between the client and tool servers. Each server runs as a separate process, providing isolation and allowing servers written in any language.

Protocol Flow

sequenceDiagram participant LLM as Ollama LLM participant MCP as MCP Client participant SRV as Tool Server Note over MCP,SRV: Initialization MCP->>SRV: initialize (protocol version) SRV->>MCP: capabilities MCP->>SRV: notifications/initialized MCP->>SRV: tools/list SRV->>MCP: available tools Note over LLM,SRV: Tool Call LLM->>MCP: call tool "get_weather" MCP->>SRV: tools/call {name, arguments} SRV->>MCP: result MCP->>LLM: tool result

Built-in Native Tools

The MCP client includes native C++ tools that don't require external servers:

Military Coordinate Tools

Tool Description Parameters
mgrs_to_latlon Convert MGRS to lat/lon mgrs: MGRS string
latlon_to_mgrs Convert lat/lon to MGRS latitude, longitude, precision
calculate_bearing Calculate bearing between points from_lat, from_lon, to_lat, to_lon
calculate_distance Calculate distance (Haversine) from_lat, from_lon, to_lat, to_lon
format_dtg Format Date-Time Group timestamp (optional), timezone

Example Usage

// Register military tools automatically
m_mcp->registerMilitaryTools();

// Call a tool
m_mcp->callTool("calculate_bearing", {
    {"from_lat", -37.8136},
    {"from_lon", 144.9631},
    {"to_lat", -33.8688},
    {"to_lon", 151.2093}
}, [](bool success, const QJsonValue& result) {
    // result: {"bearing_degrees": 54.2, "bearing_mils": 964, ...}
});

External MCP Servers

Weather Server

Provides weather forecasts via Open-Meteo API.

# Install
npm install -g @modelcontextprotocol/server-weather

# Or use npx (auto-downloads)
npx -y @modelcontextprotocol/server-weather

Tools provided:

  • get_forecast - Weather forecast for a location

Filesystem Server

Provides read/write access to a specified directory.

npx -y @modelcontextprotocol/server-filesystem /path/to/allowed/directory

Tools provided:

  • read_file - Read file contents
  • write_file - Write to file
  • list_directory - List directory contents
  • search_files - Search for files

Fetch Server

Makes HTTP requests to external URLs.

npx -y @anthropic-ai/fetch-mcp

Tools provided:

  • fetch - Make HTTP GET/POST requests

Memory Server

Provides persistent key-value storage.

npx -y @modelcontextprotocol/server-memory

Tools provided:

  • store - Store a value
  • retrieve - Retrieve a value
  • delete - Delete a value

Configuration

Connecting from C++ (any Qt6 binary)

Use mcp::McpHttpClient (src/qt6/mcp-lib/McpHttpClient.h) — the same client gva-voip-agent uses internally:

#include "McpHttpClient.h"

auto mcp = std::make_unique<mcp::McpHttpClient>();
mcp->setEndpoint(QUrl("http://127.0.0.1:7077/mcp"));
mcp->setClientInfo("my-app", "1.0.0");

QObject::connect(mcp.get(), &mcp::McpHttpClient::connected, [&] {
    mcp->listTools([](bool ok, const QJsonArray& tools, const QString& err) {
        if (!ok) { qWarning() << "listTools failed:" << err; return; }
        qInfo() << "Discovered" << tools.size() << "tools";
    });
});

mcp->connectToServer();

Tool calls follow the same pattern:

mcp->callTool("get_weather", QJsonObject{{"location", "London"}},
    [](bool ok, const QJsonObject& result, const QString& err) {
        if (!ok) { qWarning() << err; return; }
        // result follows MCP shape: { content: [{type:"text", text:"…"}], isError: bool }
    });

Command line

# stdio (default — for Claude Desktop / single client launches)
./build/bin/gva-mcp-server --config etc/gva-mcp-server-ollama.json

# HTTP daemon mode (what gva-voip-agent and the systemd unit use)
./build/bin/gva-mcp-server \
    --config etc/gva-mcp-server-ollama.json \
    --no-stdio --http --port 7077 --address 127.0.0.1

Both transports can run simultaneously — omit --no-stdio to keep stdio available while the HTTP port is also listening.

Features

Registering tools on the server

Tools live in gva-mcp-server (or mcp-lib) and are exposed to every client automatically. The fastest path is mcp::LambdaTool:

// In gva-mcp-server (e.g. GvaTools.cpp)
server.registerTool(std::make_unique<mcp::LambdaTool>(
    "get_vehicle_status",
    "Get current vehicle status from DDS",
    QJsonObject{
        {"type", "object"},
        {"properties", QJsonObject{
            {"system", QJsonObject{
                {"type", "string"},
                {"description", "System name (engine, transmission, …)"}
            }}
        }},
        {"required", QJsonArray{"system"}}
    },
    [](const QJsonObject& args) -> mcp::ToolResult {
        const QString system = args.value("system").toString();
        // Query DDS for vehicle status…
        return mcp::ToolResult::text(QString("%1: operational, 85.5°C").arg(system));
    }
));

For reusable generic tools, subclass mcp::McpTool in src/qt6/mcp-lib/ and register it from main.cpp behind a config flag.

Tool-call timeouts

Clients (mcp::McpHttpClient) set timeouts per call — the server itself does not impose a global tool timeout, but the bash tool reads timeout_ms from its server-side config.

Client signals

mcp::McpHttpClient emits the following signals for monitoring:

connect(mcp.get(), &mcp::McpHttpClient::connected, []() {
    qDebug() << "MCP HTTP transport connected";
});

connect(mcp.get(), &mcp::McpHttpClient::disconnected, []() {
    qDebug() << "MCP HTTP transport disconnected";
});

connect(mcp.get(), &mcp::McpHttpClient::errorOccurred, [](const QString& err) {
    qWarning() << "MCP error:" << err;
});

Tool discovery and dispatch use callbacks on listTools() / callTool() directly rather than signals.

Creating Custom MCP Servers

Node.js Example

// my-mcp-server.js
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');

const server = new Server({
  name: 'my-tools',
  version: '1.0.0'
}, {
  capabilities: { tools: {} }
});

server.setRequestHandler('tools/list', async () => ({
  tools: [{
    name: 'my_tool',
    description: 'Does something useful',
    inputSchema: {
      type: 'object',
      properties: {
        input: { type: 'string', description: 'Input value' }
      },
      required: ['input']
    }
  }]
}));

server.setRequestHandler('tools/call', async (request) => {
  if (request.params.name === 'my_tool') {
    const input = request.params.arguments.input;
    return {
      content: [{ type: 'text', text: `Processed: ${input}` }]
    };
  }
  throw new Error('Unknown tool');
});

const transport = new StdioServerTransport();
server.connect(transport);

Python Example

# my_mcp_server.py
import json
import sys

def handle_request(request):
    method = request.get('method')

    if method == 'initialize':
        return {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}}

    elif method == 'tools/list':
        return {'tools': [{
            'name': 'my_python_tool',
            'description': 'A Python tool',
            'inputSchema': {'type': 'object', 'properties': {}}
        }]}

    elif method == 'tools/call':
        name = request['params']['name']
        args = request['params'].get('arguments', {})
        return {'content': [{'type': 'text', 'text': f'Called {name}'}]}

    return {'error': {'code': -32601, 'message': 'Method not found'}}

for line in sys.stdin:
    request = json.loads(line)
    response = {'jsonrpc': '2.0', 'id': request.get('id')}
    response['result'] = handle_request(request)
    print(json.dumps(response), flush=True)

Troubleshooting

Server Won't Start

# Check if npx is available
which npx

# Test server manually
npx -y @modelcontextprotocol/server-weather
# Should wait for JSON-RPC input

Tool Not Found

  1. Verify server started: check logs for "Started server: name"
  2. Wait for tool discovery (500ms after init)
  3. Check availableTools() returns expected tools

Timeout Issues

  1. Increase timeout for slow tools
  2. Check network connectivity for remote APIs
  3. Monitor server stderr for errors

Server Crashes

  1. Check server logs via serverLog signal
  2. Enable auto-reconnect with appropriate maxRestarts
  3. Review environment variables and permissions

See Also