Skip to content

MCP: initialize response returns capabilities.tools as array instead of object #3496

@brettjenkins

Description

@brettjenkins

Describe the bug
The MCP initialize response returns capabilities.tools as an array of tool objects instead of an object (capabilities descriptor). The MCP specification requires capabilities.tools to be an object like {} or {"listChanged": true}. The tool list itself belongs in responses to tools/list requests, not in the capabilities field.

Any strict MCP client fails immediately with a Zod schema validation error on connection:

Connection error: $ZodError: [
  {
    "expected": "object",
    "code": "invalid_type",
    "path": ["capabilities", "tools"],
    "message": "Invalid input: expected object, received array"
  }
]

This makes the server unusable with Claude Desktop via mcp-remote (0.1.x), which uses @modelcontextprotocol/sdk and validates strictly against the spec.

Expected behaviour
The initialize response capabilities.tools field should be an object:

{
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": { "name": "Predbat MCP Server", "version": "1.0.1" }
  }
}

Workaround
For those wanting to connect Claude Desktop to Predbat MCP today, the following proxy script works around all current MCP bugs (see also related issues for get_plan and get_entities). Save as ~/.claude/predbat-mcp-proxy.js and configure Claude Desktop to use it:

"predbat-mcp": {
  "command": "/opt/homebrew/bin/node",
  "args": ["/Users/YOUR_USER/.claude/predbat-mcp-proxy.js"]
}
#!/usr/bin/env node
'use strict';

const SERVER_URL = 'https://YOUR_PREDBAT_HOST/mcp';
const AUTH_HEADER = 'Bearer YOUR_TOKEN';

const { createInterface } = require('readline');
let sessionId = null;

function patchMsg(msg, requestId) {
  if (msg?.result?.capabilities && Array.isArray(msg.result.capabilities.tools)) {
    msg.result.capabilities.tools = {};
  }
  if (msg.id === null && requestId != null) {
    msg.id = requestId;
  }
  return msg;
}

function writeOut(msg, requestId) {
  msg = patchMsg(msg, requestId);
  const isResponse = 'result' in msg || 'error' in msg;
  if (isResponse && typeof msg.id !== 'string' && typeof msg.id !== 'number') return;
  process.stdout.write(JSON.stringify(msg) + '\n');
}

async function send(request) {
  const requestId = request.id;
  const headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream',
    'Authorization': AUTH_HEADER,
  };
  if (sessionId) headers['Mcp-Session-Id'] = sessionId;
  let res;
  try {
    res = await fetch(SERVER_URL, { method: 'POST', headers, body: JSON.stringify(request) });
  } catch (e) { process.stderr.write(`Fetch error: ${e.message}\n`); return; }
  const sid = res.headers.get('mcp-session-id');
  if (sid) sessionId = sid;
  if (res.status === 202) return;
  const ct = res.headers.get('content-type') || '';
  if (ct.includes('text/event-stream')) {
    const reader = res.body.getReader();
    const dec = new TextDecoder();
    let buf = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += dec.decode(value, { stream: true });
      const lines = buf.split('\n'); buf = lines.pop();
      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        const data = line.slice(6).trim();
        if (!data || data === '[DONE]') continue;
        try { writeOut(JSON.parse(data), requestId); } catch {}
      }
    }
  } else {
    const body = await res.text();
    if (!body.trim()) return;
    try {
      const parsed = JSON.parse(body);
      if (Array.isArray(parsed)) parsed.forEach(m => writeOut(m, requestId));
      else writeOut(parsed, requestId);
    } catch (e) { process.stderr.write(`Parse error: ${e.message}\n`); }
  }
}

const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on('line', async line => {
  line = line.trim(); if (!line) return;
  try { await send(JSON.parse(line)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); }
});
process.stdin.on('end', () => process.exit(0));

Predbat version
v8.33.6 (MCP Server 1.0.1)

Environment details

  • Claude Desktop (macOS) connecting via mcp-remote proxy
  • VSCode MCP client is lenient and works around this bug; strict clients do not

Screenshots
N/A

Log file
N/A — this is a protocol-level issue visible in the MCP client logs, not Predbat logs.

Predbat debug yaml file
N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions