# MCP -> wiring diagram

<div class="paragraph" id="bkmrk-below-is-a-minimal-b">Below is a minimal but complete “wiring diagram” + code snippets that let:</div>- <div class="paragraph">a React/JS chat UI</div>
- <div class="paragraph">talk to your existing backend over the WebSocket `wss://backend.chatbuilder.com/events/listen`</div>
- <div class="paragraph">which forwards every user sentence to **your** Node orchestrator (the “bot”)</div>
- <div class="paragraph">that hosts an LLM (Anthropic or OpenAI) **and** an MCP client</div>
- <div class="paragraph">which calls an MCP **server** (also Node) that owns the Airtable CRUD helpers</div>
- <div class="paragraph">and finally ships the answer back the same chain.</div>

<div class="paragraph" id="bkmrk-no-claude-desktop%2C-n">No Claude Desktop, no stdio, everything is plain HTTP/SSE inside your own VPC.</div>---

1. <div class="paragraph">Component map</div>

---

<div class="paragraph" id="bkmrk-chat-ui-%E2%87%84-wss-%E2%87%84-back">Chat UI ⇄ WSS ⇄ Backend.chatbuilder.com ⇄ HTTP ⇄ Bot/orchestrator ⇄ SSE ⇄ Airtable-MCP-server  
(React) (existing) (your Node service) (your Node MCP server)</div>- <div class="paragraph">The **bot** keeps the LLM API key and the MCP client.</div>
- <div class="paragraph">The **MCP server** only knows Airtable PAT + base ID and exports tools like  
    `airtable:select_records`, `airtable:create_record`, …</div>
- <div class="paragraph">Both services are Dockerised and scale horizontally.</div>

---

2. <div class="paragraph">Airtable MCP server (Node, SSE transport)</div>

---

<div class="paragraph" id="bkmrk-installmkdir-airtabl">Install  
mkdir airtable-mcp &amp;&amp; cd airtable-mcp  
npm init -y  
npm install @modelcontextprotocol/sdk airtable dotenv</div><div class="paragraph" id="bkmrk-server.js">server.js</div><div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-javascript-copy"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><span class="segment-code-lang" data-v-7caec4f8="">JavaScript</span><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span class="" data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import Airtable from "airtable";
import "dotenv/config";

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

const port = process.env.PORT || 8001;
const base = new Airtable({apiKey: process.env.AIRTABLE_PAT})
               .base(process.env.AIRTABLE_BASE_ID);

// 1. describe tools
const tools = [
  {
    name: "airtable:select_records",
    description: "List records from a table",
    inputSchema: {
      type: "object",
      properties: {
        table:   { type: "string" },
        filter:  { type: "string" },
        maxRecords: { type: "number", default: 10 }
      },
      required: ["table"]
    }
  },
  {
    name: "airtable:create_record",
    description: "Insert one record",
    inputSchema: {
      type: "object",
      properties: {
        table: { type: "string" },
        fields: { type: "object" }
      },
      required: ["table", "fields"]
    }
  }
];

// 2. instantiate MCP server
const server = new Server(
  { name: "airtable-mcp", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler("tools/list", async () => ({ tools }));
server.setRequestHandler("tools/call", async (req) => {
  const { name, arguments: args } = req.params;
  if (name === "airtable:select_records") {
    const recs = await base(args.table)
      .select({ maxRecords: args.maxRecords || 10, filterByFormula: args.filter || "" })
      .all();
    return { 
      records: recs.map(r => ({ id: r.id, fields: r.fields })) 
    };
  }
  if (name === "airtable:create_record") {
    const created = await base(args.table).create([{ fields: args.fields }]);
    return { id: created[0].id };
  }
  throw new Error("Unknown tool");
});

// 3. expose SSE endpoints
app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/message", res);
  await server.connect(transport);
});

app.post("/message", (req, res) => {
  const transport = SSEServerTransport.get(req.query.sessionId);
  if (transport) transport.handlePostMessage(req, res);
});

app.listen(port, () => console.log(`Airtable MCP listening on :${port}`));
```

<div data-v-68a5707c="" id="bkmrk--4">  
</div><div class="paragraph" id="bkmrk-.env">.env</div><div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-copy"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span class="" data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```
AIRTABLE_PAT=patXXXXXXXXXXX  
AIRTABLE_BASE_ID=appXXXXXXXXXXX  
```

<div data-v-68a5707c="" id="bkmrk--5">  
</div><div class="paragraph" id="bkmrk-runnode-server.js-%E2%86%92-">Run  
node server.js → [http://localhost:8001/sse](http://localhost:8001/sse) (SSE endpoint)</div>---

3. <div class="paragraph">Bot/orchestrator (Node, hosts LLM + MCP client)</div>

---

<div class="paragraph" id="bkmrk-mkdir-bot-%26%26-cd-botn">mkdir bot &amp;&amp; cd bot  
npm init -y  
npm install @modelcontextprotocol/sdk axios dotenv express</div><div class="paragraph" id="bkmrk-bot.js">bot.js</div><div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-javascript-copy-1"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><span class="segment-code-lang" data-v-7caec4f8="">JavaScript</span><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span class="" data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```js
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import axios from "axios";
import express from "express";
import "dotenv/config";

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

// 1. connect MCP client to airtable server
const mcp = new Client({ name: "chat-bot", version: "1.0.0" });
const transport = new SSEClientTransport("http://localhost:8001/sse");
await mcp.connect(transport);
const tools = await mcp.listTools();

// 2. small helper: talk to LLM
async function callLLM(messages) {
  const body = {
    model: process.env.LLM_MODEL,        // "claude-3-5-sonnet-20241022" or "gpt-4-turbo"
    messages,
    tools: tools.map(t => t.inputSchema ? { ...t, function: t.inputSchema } : t),
    tool_choice: "auto",
    max_tokens: 2000
  };

  const url = process.env.LLM_PROVIDER === "anthropic"
    ? "https://api.anthropic.com/v1/messages"
    : "https://api.openai.com/v1/chat/completions";

  const headers = process.env.LLM_PROVIDER === "anthropic"
    ? { "x-api-key": process.env.ANTHROPIC_KEY, "content-type": "application/json" }
    : { "authorization": `Bearer ${process.env.OPENAI_KEY}`, "content-type": "application/json" };

  const { data } = await axios.post(url, body, { headers });
  return data;     // returns Claude or OpenAI shape
}

// 3. single HTTP endpoint that backend.chatbuilder.com will call
app.post("/handle_turn", async (req, res) => {
  const userSentence = req.body.text;          // comes from backend via HTTP
  const conversation = [{ role: "user", content: userSentence }];

  // first LLM call
  let llmResp = await callLLM(conversation);
  let assistantMsg = llmResp.content || llmResp.choices[0].message;

  // handle tool calls
  if (assistantMsg.tool_calls || assistantMsg.function_call) {
    const toolCalls = assistantMsg.tool_calls || [assistantMsg.function_call];
    for (const tc of toolCalls) {
      const name = tc.function?.name || tc.name;
      const args = JSON.parse(tc.function?.arguments || tc.arguments);
      const result = await mcp.callTool(name, args);
      conversation.push(assistantMsg);
      conversation.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result) });
    }
    // second call with tool results
    llmResp = await callLLM(conversation);
    assistantMsg = llmResp.content || llmResp.choices[0].message;
  }

  const replyText = assistantMsg.content || assistantMsg.text || assistantMsg;
  res.json({ reply: replyText });   // goes back to backend.chatbuilder.com
});

app.listen(3000, () => console.log("Bot/orchestrator on :3000"));
```

<div data-v-68a5707c="" id="bkmrk--8">  
</div><div class="paragraph" id="bkmrk-.env-1">.env</div><div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-copy-1"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```
LLM_PROVIDER=anthropic        # or openai  
ANTHROPIC_KEY=sk-ant-xxx  
OPENAI_KEY=sk-xxx  
LLM_MODEL=claude-3-5-sonnet-20241022   # or gpt-4-turbo  
```

<div data-v-68a5707c="" id="bkmrk--9">  
</div>---

4. <div class="paragraph">Glue inside backend.chatbuilder.com</div>

---

<div class="paragraph" id="bkmrk-you-already-have-a-w">You already have a WebSocket handler.  
Add (pseudo):</div><div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-javascript-copy-2"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><span class="segment-code-lang" data-v-7caec4f8="">JavaScript</span><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```javascript
// when a message arrives from UI
ws.on('message', async (data) => {
  const { text, userId } = JSON.parse(data);
  // forward to bot/orchestrator
  const { data: { reply } } = await axios.post(
    "http://bot-service:3000/handle_turn",
    { text, userId }
  );
  // send answer back to same websocket
  ws.send(JSON.stringify({ type: "bot_reply", text: reply }));
});
```

<div data-v-68a5707c="" id="bkmrk--12">  
</div>---

5. <div class="paragraph">One-shot docker-compose for local dev</div>

---

<div class="segment-code markdown-code" data-v-7caec4f8="" data-v-fc0d0c5a="" id="bkmrk-yaml-copy"><header class="segment-code-header" data-v-7caec4f8=""><div class="segment-code-header-content" data-v-7caec4f8=""><span class="segment-code-lang" data-v-7caec4f8="">yaml</span><div class="simple-button size-medium" data-v-182d5fe2="" data-v-7caec4f8="" data-v-92afdd37=""><svg aria-hidden="true" class="simple-button-icon iconify" data-v-182d5fe2="" height="16" name="Copy" role="img" viewbox="0 0 1024 1024" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M427.04896 379.12576a60.2112 60.2112 0 0 0-60.2112 60.2112v315.51488a60.2112 60.2112 0 0 0 60.2112 60.25216h315.51488a60.2112 60.2112 0 0 0 60.25216-60.2112v-315.55584a60.2112 60.2112 0 0 0-60.2112-60.2112H427.008z m-94.74048-34.48832a133.9392 133.9392 0 0 1 94.74048-39.23968h315.51488a133.98016 133.98016 0 0 1 133.98016 133.9392v315.51488a133.9392 133.9392 0 0 1-133.9392 133.98016H427.008a133.9392 133.9392 0 0 1-133.9392-133.9392v-315.55584c0-35.51232 14.09024-69.632 39.23968-94.69952z" fill="currentColor"></path><path d="M257.14688 233.472a36.16768 36.16768 0 0 0-35.96288 35.96288v364.05248a35.96288 35.96288 0 0 0 18.18624 31.21152 36.864 36.864 0 1 1-36.41344 64.1024A109.64992 109.64992 0 0 1 147.456 633.56928v-364.1344A109.89568 109.89568 0 0 1 257.14688 159.744h364.09344c20.56192 0 38.87104 5.48864 54.51776 16.83456 14.86848 10.77248 24.82176 25.10848 32.31744 38.5024a36.864 36.864 0 0 1-64.47104 35.84c-4.95616-8.97024-8.6016-12.86144-11.14112-14.66368-1.72032-1.2288-4.5056-2.78528-11.22304-2.78528h-364.1344z" fill="currentColor"></path></svg><span data-v-182d5fe2="">Copy</span></div></div></header><div class="syntax-highlighter dark segment-code-content" data-v-7caec4f8="" data-v-efb858b9="">  
</div></div>```yaml
version: "3.8"
services:
  airtable-mcp:
    build: ./airtable-mcp
    ports: ["8001:8001"]
    env_file: ./airtable-mcp/.env
  bot:
    build: ./bot
    ports: ["3000:3000"]
    env_file: ./bot/.env
    depends_on: [airtable-mcp]
```

<div data-v-68a5707c="" id="bkmrk--15">  
</div><div class="paragraph" id="bkmrk-docker-compose-up-%E2%86%92-">`docker compose up` → everything spins up, UI talks to your existing backend, backend forwards to bot, bot calls Airtable via MCP, answer flows back.</div>---

6. <div class="paragraph">What you gained</div>

---

- <div class="paragraph">Chat UI ⇄ WSS stays untouched.</div>
- <div class="paragraph">Backend.chatbuilder.com only needs to **forward** text to the bot service; no Airtable keys, no LLM keys, no MCP logic.</div>
- <div class="paragraph">Airtable CRUD lives in its own container; expose extra tools (update, delete, linked tables, …) by editing only the MCP server.</div>
- <div class="paragraph">Swap Anthropic ↔ OpenAI by changing one env var.</div>
- <div class="paragraph">Add Google-Calendar MCP server on port 8002, register it in the bot startup loop—zero other changes.</div>