Matrix logo

Writing a Tool Bridge

Add a new MCP server to a Matrix agent: declare the server in the manifest, enumerate its tools, pin the package digest, wire credentials, and verify the bijection.

Adding a capability to an agent means adding an MCP server to its manifest. Matrix never calls undeclared tools, so the manifest is the single source of truth.

1
Declare the server

Add an entry to the manifest's servers array with an alias, transport, and how to launch it:

{
  "alias": "github",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-github"],
  "env": ["GITHUB_TOKEN=$env:GITHUB_TOKEN"],
  "version": "2025.1.0",
  "package_digest": "sha256:...",
  "tools": []
}

For http servers, use url and optional headers instead of command and args:

{
  "alias": "vercel",
  "transport": "http",
  "url": "https://mcp.vercel.com",
  "headers": ["Authorization=$env:VERCEL_TOKEN"],
  "version": "2025.1.0",
  "package_digest": "sha256:...",
  "tools": []
}
2
Enumerate every tool

The tools array must list exactly what the server advertises: name, description, and side_effect_class. The manager rejects any drift at boot.

"tools": [
  {
    "name": "create_issue",
    "description": "Open a new GitHub issue",
    "side_effect_class": "network"
  },
  {
    "name": "list_issues",
    "description": "List issues in a repository",
    "side_effect_class": "network"
  },
  {
    "name": "create_pull_request",
    "description": "Open a new pull request",
    "side_effect_class": "network"
  }
]

Each tool must declare one of read, write, network, or shell as its side_effect_class. Choose the lightest class that accurately describes the tool's behavior.

3
Pin the package digest

Compute the sha256 of the published package and set package_digest to sha256:<64-hex>. Bump version to match; it forms the tool URI pin.

sha256sum package.tgz
4
Wire credentials safely

Reference secrets as $env:NAME, never as literal values. The executor resolves them from its own environment at spawn time.

"env": ["GITHUB_TOKEN=$env:GITHUB_TOKEN"]

For http servers, credentials go in headers:

"headers": ["Authorization=$env:API_KEY"]
5
Verify the bijection

Run the verification tool against your manifest. It spawns each server and asserts that declared tools equal discovered tools. Any mismatch is a boot-time fatal error, so catching it early saves debugging time.

6
Grant the tool to a skill

A plan can only call tools that a skill declares. Add the version-pinned URI to the skill's §TOOLS section:

§TOOLS
matrix://tool/mcp/github/create_issue@2025.1.0
matrix://tool/mcp/github/list_issues@2025.1.0

The side-effect class of every tool must be covered by the manifest's allowed_side_effects. A shell tool on an agent that only allows read and network will be refused by the capability gate.

How tool bridges work internally

A tool bridge is a process that speaks the MCP wire protocol over stdio (newline-delimited JSON-RPC) or http (Streamable HTTP). The bridge handles three methods:

MethodPurpose
initializeHandshake: return server name, version, and capabilities.
tools/listReturn the full list of tools with names, descriptions, and input schemas.
tools/callExecute a tool by name with arguments, return the result.

For stdio bridges, the daemon spawns the process and communicates over stdin/stdout. For http bridges, the daemon acts as an HTTP client.

A typical stdio bridge in Node.js:

import { createInterface } from 'node:readline'

const tools = [
  {
    name: 'my_tool',
    description: 'Does something useful',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'What to look up' }
      },
      required: ['query']
    }
  }
]

const handlers = {
  initialize: (params) => ({
    protocolVersion: params?.protocolVersion ?? '2024-11-05',
    serverInfo: { name: 'my-server', version: '0.1.0' },
    capabilities: { tools: {} },
  }),
  'tools/list': () => ({ tools }),
  'tools/call': async (params) => {
    const { name, arguments: args } = params
    // dispatch to your tool implementation
    const result = await handleTool(name, args)
    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
      isError: false,
    }
  },
}

function send(obj) { process.stdout.write(JSON.stringify(obj) + '\n') }

const rl = createInterface({ input: process.stdin })
rl.on('line', async (line) => {
  const req = JSON.parse(line)
  const fn = handlers[req.method]
  if (!fn) return
  const result = await fn(req.params)
  if (req.id !== undefined && result !== null) {
    send({ jsonrpc: '2.0', id: req.id, result })
  }
})
Server templates

Ready-to-copy server definitions for popular services.

MCP overview

The resolution, bijection, and gating model in full.