Skip to content
Traliran edited this page Aug 27, 2026 · 1 revision

MCP in Traliran AI Hub

This is a quick rundown of how MCP (Model Context Protocol) is set up in this project: where to click to connect your own server, and how to actually write a server that the hub understands. All the magic lives in one file, mcp.js; the connection buttons are in index.html (the #mcpModal modal), and the actual tool-calling from chat is in app.js.


How it actually works

The hub hooks into remote MCP servers over the Streamable HTTP (JSON-RPC 2.0) protocol. In other words, your MCP server is just an HTTP endpoint that catches a POST with JSON-RPC and returns a list of tools plus the results of calling them.

What happens under the hood:

  1. On app startup, MCP_MANAGER.connectAll() fires and the hub tries to connect to every saved server (app.js:2461).
  2. With each server it does a JSON-RPC "handshake":
    • initialize → the server replies with the protocol version and its capabilities;
    • notifications/initialized notification (no reply needed);
    • tools/list → the server returns the list of tools.
  3. MCP tools get turned into regular function-tools for the model (MCP_MANAGER.buildToolSet()). The function name = <server_name>_<tool_name> (anything extra gets stripped, length ≤ 64 chars).
  4. In chat, if one model is selected and there are connected MCP tools, the agentic loop runAgenticSingleModel() kicks in (app.js:1647): the model itself decides "should I call a tool", the hub proxies the call to your server, feeds the result back into the conversation, and repeats up to 8 times (MAX_ITER = 8).

What actually matters when writing a server

  • Transport: Streamable HTTP. Every request is a POST to the server URL.
  • Content types: the client sends application/json and accepts either application/json or text/event-stream (SSE). SSE is parsed line by line (mcp.js:39).
  • Headers:
    • Authorization — optional, taken from the "Auth header" field (like Bearer xxx).
    • Mcp-Session-Idimportant: the server MUST return this header after initialize, otherwise the client won't keep the session (mcp.js:104).
  • Protocol version: the client sends 2024-11-05, but it'll accept whatever you return.
  • CORS: the hub is fully browser-based, so your server must emit CORS headers, otherwise the browser blocks everything.

How to connect your own MCP server

Option 1 — click in the UI

  1. In chat, hit the 🧩 MCP Servers button (app.js:376).
  2. In the popup, fill in:
    • Server name — any name, becomes the tool prefix (e.g. Files).
    • URL — the server endpoint, like https://host/mcp.
    • Auth header (optional)Bearer <token> if needed.
  3. Hit ➕ Add & Connect — the client tries to connect immediately (app.js:429).
  4. If all good — a card shows status ● connected (N tools) and the list of tools. A badge "N MCP tools ready" appears at the top of the chat.

The card has (reconnect) and 🗑 (remove) buttons. Everything is saved in LocalStorage under the key gem_mcp_servers and auto-reconnects on the next visit.

Option 2 — from the browser console

Open DevTools → Console:

const entry = MCP_MANAGER.add({
  name: 'MyServer',
  url: 'https://example.com/mcp',
  authHeader: 'Bearer my-token' // can be an empty string
});
await MCP_MANAGER.connectOne(entry.id);

MCP_MANAGER.connectedServers(); // what's connected and which tools
MCP_MANAGER.toolCount();         // total number of tools
MCP_MANAGER.buildToolSet();      // build the set for the model

Remove: MCP_MANAGER.remove(entry.id);

Local server

Tip from the UI (index.html:642): for a local SSE server you can bridge it with an adapter:

npx -y mcp-remote https://your-server/sse

But keep in mind: this client expects Streamable HTTP (POST JSON-RPC), not a bare SSE stream. mcp-remote converts an SSE server into the right shape, so the combo works.


How to write your own MCP server

The server must:

  1. Catch POST with JSON-RPC 2.0.
  2. Handle initialize, notifications/initialized, tools/list, tools/call.
  3. Return the Mcp-Session-Id header.
  4. Emit CORS.
  5. (optionally) be able to reply with SSE.

Bare minimum in Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

// CORS — without it the browser won't let you in
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*'); // better to use a specific domain
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

const TOOLS = [{
  name: 'get_weather',
  description: 'Returns the weather for a city',
  inputSchema: {
    type: 'object',
    properties: { city: { type: 'string', description: 'City name' } },
    required: ['city']
  }
}];

function rpc(res, id, result) {
  res.setHeader('Mcp-Session-Id', 'sess-' + Math.random().toString(36).slice(2));
  res.json({ jsonrpc: '2.0', id, result });
}

app.post('/mcp', (req, res) => {
  const { id, method, params } = req.body;

  if (!id) return res.status(202).end(); // notification — no reply

  if (method === 'initialize') {
    return rpc(res, id, {
      protocolVersion: '2024-11-05',
      capabilities: { tools: {} },
      serverInfo: { name: 'MyServer', version: '1.0.0' }
    });
  }

  if (method === 'tools/list') {
    return rpc(res, id, { tools: TOOLS });
  }

  if (method === 'tools/call') {
    const { name, arguments: args } = params;
    let content;
    if (name === 'get_weather') {
      content = [{ type: 'text', text: `It is +21°C in ${args.city} right now` }];
    } else {
      content = [{ type: 'text', text: 'Unknown tool', isError: true }];
    }
    return rpc(res, id, { content });
  }

  return rpc(res, id, { error: { code: -32601, message: 'Method not found' } });
});

app.listen(3000, () => console.log('MCP server on http://localhost:3000/mcp'));

Connect it in the hub as http://localhost:3000/mcp.

Bare minimum in Python (FastAPI)

from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
import uuid

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # set your app's domain here
    allow_headers=["*"],
    allow_methods=["*"],
)

TOOLS = [{
    "name": "get_weather",
    "description": "Returns the weather for a city",
    "inputSchema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
    },
}]

@app.post("/mcp")
async def mcp(req: Request, res: Response):
    body = await req.json()
    rid = body.get("id")
    method = body.get("method")
    params = body.get("params", {})

    res.headers["Mcp-Session-Id"] = "sess-" + uuid.uuid4().hex[:12]

    if rid is None:  # notification
        return Response(status_code=202)

    if method == "initialize":
        return {"jsonrpc": "2.0", "id": rid, "result": {
            "protocolVersion": "2024-11-05",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "MyServer", "version": "1.0.0"},
        }}

    if method == "tools/list":
        return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}

    if method == "tools/call":
        name = params.get("name")
        args = params.get("arguments", {})
        if name == "get_weather":
            content = [{"type": "text", "text": f"It is +21C in {args.get('city')} right now"}]
        else:
            content = [{"type": "text", "text": "Unknown tool", "isError": True}]
        return {"jsonrpc": "2.0", "id": rid, "result": {"content": content}}

    return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": "Method not found"}}

Tool response format (tools/call)

The client expects a content field — an array of objects (mcp.js:21):

{ "content": [ { "type": "text", "text": "tool result" } ] }

Error — via "isError": true:

{ "content": [ { "type": "text", "text": "oops", "isError": true } ] }

Types: text and resource (resource gets serialized to JSON).

About tool names

  • In chat the name = <sanitize(server_name)>_<tool_name>.
  • Anything that isn't latin letters/digits/_ gets replaced with _ (mcp.js:13).
  • The result is truncated to 64 chars; on a collision a random suffix is added (mcp.js:214).

Bottom line: name your tools simply — latin letters, digits, _ — so they read fine in chat.


What's in MCP_MANAGER (global object on window)

Method Purpose
load() load servers from LocalStorage
save() save the list
add({name, url, authHeader}) add, returns an entry with id
connectOne(id) connect one, returns its tools
connectAll() connect all, returns the count of successes
disconnectOne(id) disconnect (without removing)
remove(id) remove the server and its client
buildToolSet() build the { tools, registry } set
callTool(fnName, args) call a tool by function name
connectedCount() / toolCount() how many connected / tools
connectedServers() list with status and tools

Where to click — line numbers

  • MCP modal: index.html:621
  • Open button: app.js:376
  • Add/connect: app.js:429
  • Agentic call loop: app.js:1647
  • Tool assembly: app.js:1649
  • System-prompt note: app.js:1554
  • MCP client logic: mcp.js (whole file)

FAQ

  • Won't connect. Check: (a) the server returns Mcp-Session-Id; (b) CORS is present; (c) the endpoint accepts POST JSON-RPC. Errors show in the console (app.js:183).
  • Tools not visible in chat. The agentic mode only runs with one selected model (app.js:1762). In multi-model compare mode MCP isn't used.
  • The model doesn't call the tools. That's on the model; the hub itself appends an [Available MCP tools] block to the prompt (app.js:1554), but the decision is still up to the model.
  • Local server unreachable. Add CORS, and if needed run it through mcp-remote (SSE → Streamable HTTP).