-
Notifications
You must be signed in to change notification settings - Fork 0
MCP
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.
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:
- On app startup,
MCP_MANAGER.connectAll()fires and the hub tries to connect to every saved server (app.js:2461). - With each server it does a JSON-RPC "handshake":
-
initialize→ the server replies with the protocol version and its capabilities; -
notifications/initializednotification (no reply needed); -
tools/list→ the server returns the list of tools.
-
- 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). - 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).
-
Transport: Streamable HTTP. Every request is a
POSTto the server URL. -
Content types: the client sends
application/jsonand accepts eitherapplication/jsonortext/event-stream(SSE). SSE is parsed line by line (mcp.js:39). -
Headers:
-
Authorization— optional, taken from the "Auth header" field (likeBearer xxx). -
Mcp-Session-Id— important: the server MUST return this header afterinitialize, 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.
- In chat, hit the 🧩 MCP Servers button (
app.js:376). - 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.
-
Server name — any name, becomes the tool prefix (e.g.
- Hit ➕ Add & Connect — the client tries to connect immediately
(
app.js:429). - 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.
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 modelRemove: MCP_MANAGER.remove(entry.id);
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.
The server must:
- Catch
POSTwith JSON-RPC 2.0. - Handle
initialize,notifications/initialized,tools/list,tools/call. - Return the
Mcp-Session-Idheader. - Emit CORS.
- (optionally) be able to reply with SSE.
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.
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"}}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).
- 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.
| 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 |
- 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)
-
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).