Skip to main content

What is MCP?

The Model Context Protocol (MCP) is an open standard for connecting large language models to external tools, APIs, and data sources. An MCP server exposes a catalogue of callable tools — each with a name, description, and JSON Schema for its parameters — and the LLM decides at runtime which tool to call and with what arguments. MCP servers run as stdio subprocesses: the client spawns the server binary, communicates over stdin/stdout, and tears it down when the session ends. This model keeps servers lightweight and language-agnostic — you can use npx-hosted community servers or ship your own binary.

How Voxray uses MCP

At pipeline startup, Voxray’s MCP client connects to the configured server subprocess, calls ListTools to enumerate available tools, and converts each MCP tool schema into a FunctionSchema that the LLM service understands. Those schemas are registered on the LLM provider before the first user turn. During a conversation, when the LLM decides to call a tool, it produces a structured tool-call that Voxray’s ToolHandler intercepts. The handler opens a fresh subprocess session, calls session.CallTool() with the LLM-supplied arguments, extracts the text content from the result, optionally applies a per-tool output transform, and returns the result string back to the LLM — which then continues the conversation with that context.
Pipeline.Setup()
    └── MCP Client.RegisterTools(ctx, llm)
            ├── connect → MCP subprocess
            ├── ListTools → [tool1, tool2, ...]
            ├── filter by ToolsFilter (if set)
            ├── convert to FunctionSchema
            └── llm.RegisterTool(schema, handler)

User speaks → STT → LLM thinks → tool call
    └── ToolHandler
            ├── connect → MCP subprocess
            ├── CallTool(name, arguments)
            ├── extract text content
            ├── apply ToolsOutputFilter (optional)
            └── return result → LLM continues

Pipeline.Cleanup()
    └── MCP subprocess exits

Configuration

Add an mcp block to your config.json:
{
  "mcp": {
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
    "tools_filter": ["read_file", "list_directory"]
  }
}
FieldTypeRequiredDescription
commandstringyesExecutable to launch as the MCP server (e.g. npx, go, python3).
argsstring[]noArguments forwarded to the server executable.
tools_filterstring[]noWhitelist of tool names to register. When empty or omitted, all tools from the server are registered.
tools_filter is a whitelist, not a blacklist. If you specify any entries, only those exact tool names are registered — all others are silently skipped. Leave it empty to expose every tool the server advertises.

Tool discovery

Client.GetToolsSchema(ctx) handles discovery:
  1. Spawns the MCP server via mcp.CommandTransport{Command: ...}.
  2. Calls session.ListTools() to retrieve the full tool catalogue.
  3. Iterates each tool, skipping any not present in ToolsFilter (when set).
  4. Converts MCP’s InputSchema (a JSON Schema object) into Voxray’s FunctionSchema, preserving properties and required arrays.
  5. Returns a ToolsSchema with all accepted tools.
RegisterTools calls GetToolsSchema and then calls llm.RegisterTool(schema, handler) for each result, binding a ToolHandler closure that knows which tool name to forward to the subprocess.

Tool execution

Each tool handler is a closure over the tool name. When invoked:
// Simplified from pkg/mcp/client.go
func (c *Client) toolWrapper(toolName string) llmapi.ToolHandler {
    return func(ctx context.Context, name string, arguments map[string]any) (string, error) {
        session, _ := client.Connect(ctx, transport, nil)
        defer session.Close()

        res, _ := session.CallTool(ctx, &mcp.CallToolParams{
            Name:      name,
            Arguments: arguments,
        })
        response := extractTextFromResult(res)
        if fn := c.ToolsOutputFilters[name]; fn != nil {
            response = toString(fn(response))
        }
        return response, nil
    }
}
Each tool call opens its own subprocess connection. This is intentional: MCP stdio servers are stateless per invocation, and sharing a single long-lived session across concurrent tool calls would require additional synchronization.

Output filters

ToolsOutputFilters is a map[string]func(any) any set directly on the Client struct in Go code (not configurable via JSON). Use it when the raw MCP tool output needs to be trimmed, reformatted, or redacted before the LLM sees it:
client := mcp.NewClient(params, filter, map[string]func(any) any{
    "read_file": func(raw any) any {
        // Truncate large files so the LLM context stays within limits.
        if s, ok := raw.(string); ok && len(s) > 4000 {
            return s[:4000] + "\n[truncated]"
        }
        return raw
    },
})
Output filters run in the tool handler goroutine and block the LLM response. Keep them fast. For heavy processing (parsing, summarisation), consider a background worker with a channel.

LLM provider compatibility

MCP tool integration requires the LLM service to implement the LLMServiceWithTools interface. Currently supported providers:
ProviderTools Supported
OpenAI (GPT-4o, GPT-4 Turbo, etc.)Yes
Google (Gemini)Yes
Anthropic (Claude)Not yet
GroqNot yet
If you configure mcp but your llm_provider does not implement LLMServiceWithTools, tool registration is silently skipped and the agent will run without tools. Check startup logs for mcp: entries to confirm tools were registered.

Common MCP servers

MCP ServerPackageUse Case
Filesystem@modelcontextprotocol/server-filesystemRead and write files within an allowed directory tree
Brave Search@modelcontextprotocol/server-brave-searchReal-time web search during calls
SQLite@modelcontextprotocol/server-sqliteQuery a local SQLite database
Custom server(your binary)Internal APIs, CRM lookups, ticketing systems

Filesystem server example

{
  "mcp": {
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/var/data/knowledge-base"],
    "tools_filter": ["read_file", "list_directory", "search_files"]
  }
}

Brave Search example

{
  "mcp": {
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-brave-search"],
    "tools_filter": ["brave_web_search"]
  }
}
Set BRAVE_API_KEY in your environment before starting Voxray.

Custom Go MCP server

{
  "mcp": {
    "command": "go",
    "args": ["run", "./cmd/my-mcp-server"],
    "tools_filter": ["lookup_customer", "create_ticket"]
  }
}

Lifecycle

EventWhat happens
Pipeline.Setup()MCP client connects to subprocess, discovers tools, registers them with the LLM provider.
During conversationEach tool call spawns a short-lived subprocess session, executes, and closes.
Pipeline.Cleanup()The MCP subprocess exits. No explicit teardown is needed for individual tool sessions.
Tool discovery at Pipeline.Setup() adds a round-trip to the MCP server before the first call is answered. For latency-sensitive deployments, pre-warm pipelines or use tools_filter to limit the number of tools that need to be introspected.