Back to Blog
Technical Tutorials

Front-End Meets AI: Building a Chatbot with MCPify and LLMs

Build a production-grade web chatbot that answers with real-time data. Complete guide with Node.js backend, SSE streaming, and MCPify tool integration.

Herman Sjøberg
Herman Sjøberg
AI Integration Expert
August 27, 202514 min read
AI chatbotMCPweb developmentGPT-5SSEJavaScriptNode.js

Key Takeaways

  • Complete architecture for real-time AI chatbots with live data
  • Working Node.js backend with OpenAI and MCPify integration
  • Frontend HTML/JS example with SSE streaming
  • Production tips for UX, security, and performance
  • Alternative approach using typed tools from MCPify

Front‑End Meets AI: Building a Chatbot with MCPify and LLMs

TL;DR: This guide shows how to build a production‑grade chatbot that answers complex questions with live data. You will wire a web chat UI to an LLM (GPT‑5 or Claude) and let the model call MCPify tools that wrap your APIs. We cover architecture, UX, and code for streaming replies, secure tool calls, and real‑time results.


Why real‑time chatbots beat static Q&A

Users expect answers that are current, personalized, and actionable. A plain LLM can be brilliant at language, but it cannot magically know your inventory right now, your user's latest order, or tomorrow's weather in Rome. The fix is to let the model use tools that fetch and act on live data.

MCPify makes this easy by turning any REST, GraphQL, or proprietary API into a Model Context Protocol (MCP) service. Your LLM stays the brain. MCPify is the plumbing that exposes transparent, richly described tools the model can call when it needs fresh data.


Architecture at a glance

Goal: A chat experience where the user asks questions and gets high‑quality answers backed by your APIs.

  • Front‑end chat UI A web or mobile interface that captures messages and renders streaming responses.
  • LLM backend Calls GPT‑5 or Claude via API. The model decides when to invoke tools to fetch live data.
  • MCPify gateway A multi‑tenant gateway where each wrapped API becomes an MCP tool with exact schemas, examples, rate limits, and more.

Typical flow:

  1. User asks a question in the UI.
  2. Backend calls the LLM and advertises available MCPify tools.
  3. If needed, the LLM issues a tool call.
  4. Backend relays that call to the MCPify gateway, which securely invokes your API and returns structured results.
  5. LLM composes a final, user‑friendly answer and streams it back to the UI.

What MCP brings to the table

  • Open standard: MCP standardizes how AI apps connect to tools and data.
  • Radical transparency: No hidden interpretation. LLMs see where data lives and how it is structured.
  • Perfect tool descriptions: Exhaustive endpoint metadata, input schemas, examples, rate limits, and response shapes.
  • Big‑response control: Pagination, response chunking, JSON filtering, and cache transparency so models extract only what they need.
  • Multi‑tenant gateway: One gateway, many services. OAuth, rate limits, logging, and analytics in one place.

See: MCPify Docs, Quickstart


Step 1 ‑ MCPify an API

You can MCPify any external or internal API. Example: a simple weather API.

  1. Describe your API Prepare an OpenAPI spec or a minimal JSON config and send it to our team. MCPify generates typed MCP tools from it.

    {
      "service": "weather",
      "base_url": "https://api.open-meteo.com/v1",
      "auth": { "type": "none" },
      "endpoints": [
        {
          "name": "get_forecast",
          "description": "Get hourly temperature forecast",
          "method": "GET",
          "path": "/forecast",
          "query": {
            "latitude": "number",
            "longitude": "number",
            "hourly": "string",
            "timezone": "string"
          },
          "response": {
            "type": "object",
            "properties": { "hourly": { "type": "object" } }
          }
        }
      ]
    }
    
  2. Deploy MCPify serves your API as a hosted MCP endpoint (example): https://weather.mcp.mcpify.org/mcp The service now exposes one or more MCP tools with precise JSON schemas.

  3. Connect your client Both OpenAI and Anthropic support MCP natively, so your LLM sees rich, typed tools out of the box.


Step 2 ‑ Backend that bridges LLM and MCPify

We will build a minimal Node.js server that:

  • Accepts a chat message.
  • Calls GPT‑5 with tools enabled.
  • If the model asks to call a tool, the server invokes MCPify.
  • Streams the final answer to the front‑end via SSE.

Note: This example uses OpenAI's Chat Completions API with a generic mcp_request function that forwards requests to MCPify. In production, prefer typed tool definitions auto‑generated by MCPify for higher accuracy.

Install dependencies

npm i express openai dotenv

server.js

import 'dotenv/config';
import express from 'express';
import OpenAI from 'openai';

const app = express();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Each MCPify service is its own MCP server:
// https://{service}.mcp.mcpify.org/mcp
const MCPIFY_DOMAIN = process.env.MCPIFY_DOMAIN || 'mcp.mcpify.org';

// A generic tool the model can call to reach MCPify.
// For production, fetch real, typed tool schemas from MCPify and register them instead.
const mcpRequestTool = {
  type: 'function',
  function: {
    name: 'mcp_request',
    description: 'Call a tool on an MCPify MCP service.',
    parameters: {
      type: 'object',
      properties: {
        service: { type: 'string', description: 'MCPify service id, e.g., weather' },
        tool: { type: 'string', description: 'Tool name, e.g., get_forecast' },
        arguments: { type: 'object', additionalProperties: true }
      },
      required: ['service', 'tool']
    }
  }
};

// Helper to call an MCPify service via MCP tools/call
// (in production, use the official MCP SDK client instead of raw JSON-RPC)
async function callMcpify({ service, tool, arguments: toolArgs }) {
  const res = await fetch(`https://${service}.${MCPIFY_DOMAIN}/mcp`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream'
      // Auth for the upstream API is configured in MCPify at onboarding,
      // not passed through from this server.
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/call',
      params: { name: tool, arguments: toolArgs || {} }
    })
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`MCPify error ${res.status}: ${text}`);
  }
  const rpc = await res.json();
  if (rpc.error) {
    throw new Error(`MCP error ${rpc.error.code}: ${rpc.error.message}`);
  }
  return rpc.result;
}

// SSE chat endpoint ‑ GET for simplicity
app.get('/api/chat', async (req, res) => {
  const userMessage = req.query.message || '';
  if (!userMessage) {
    res.status(400).json({ error: 'Missing message query param' });
    return;
  }

  // SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');

  // Assemble base messages
  const messages = [
    {
      role: 'system',
      content:
        'You are a helpful assistant. Use tools for real‑time data. Summarize tool outputs in clean prose.'
    },
    { role: 'user', content: String(userMessage) }
  ];

  try {
    // First pass ‑ decide whether to call a tool
    const first = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL || 'gpt-5',
      messages,
      tools: [mcpRequestTool],
      tool_choice: 'auto'
    });

    const msg = first.choices?.[0]?.message;
    const toolCalls = msg?.tool_calls || [];

    if (toolCalls.length > 0) {
      // Execute each tool call sequentially (simple demo)
      for (const call of toolCalls) {
        if (call.type === 'function' && call.function?.name === 'mcp_request') {
          const args = JSON.parse(call.function.arguments || '{}');
          const result = await callMcpify(args);
          messages.push({
            role: 'tool',
            tool_call_id: call.id,
            content: JSON.stringify(result)
          });
        }
      }
      // Second pass ‑ stream the final answer with tool results included
      const stream = await openai.chat.completions.create({
        model: process.env.OPENAI_MODEL || 'gpt-5',
        messages,
        stream: true
      });

      for await (const chunk of stream) {
        const delta = chunk.choices?.[0]?.delta?.content;
        if (delta) {
          res.write(`data: ${JSON.stringify({ token: delta })}\n\n`);
        }
      }
      res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
      res.end();
      return;
    }

    // No tool needed ‑ stream the answer directly
    const stream = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL || 'gpt-5',
      messages,
      stream: true
    });

    for await (const chunk of stream) {
      const delta = chunk.choices?.[0]?.delta?.content;
      if (delta) {
        res.write(`data: ${JSON.stringify({ token: delta })}\n\n`);
      }
    }
    res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
    res.end();
  } catch (err) {
    res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
    res.end();
  }
});

app.listen(process.env.PORT || 3000, () => {
  console.log('Server running on http://localhost:3000');
});

Notes:

  • For production, replace the generic mcp_request with typed tools auto‑generated by MCPify so the model gets strict schemas per endpoint.
  • The example shows a simple two‑pass approach. Tool calls are decided first, then we stream the final answer.
  • Use environment variables for OPENAI_API_KEY, OPENAI_MODEL, and MCPIFY_DOMAIN.

Step 3 ‑ A minimal front‑end that streams replies

You can use any framework. Below is a small vanilla HTML example that opens an SSE connection and appends tokens as they arrive.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>MCPify Chat Demo</title>
    <style>
      body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 2rem; }
      #messages { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; height: 400px; overflow-y: auto; }
      .msg { margin: 0.5rem 0; }
      .user { text-align: right; }
      .assistant { text-align: left; }
      #form { margin-top: 1rem; display: flex; gap: 0.5rem; }
      input[type="text"] { flex: 1; padding: 0.6rem; }
      button { padding: 0.6rem 1rem; }
    </style>
  </head>
  <body>
    <h1>MCPify Chat Demo</h1>

    <div id="messages"></div>

    <form id="form">
      <input id="input" type="text" placeholder="Ask me something..." />
      <button type="submit">Send</button>
    </form>

    <script>
      const messagesEl = document.getElementById('messages');
      const form = document.getElementById('form');
      const input = document.getElementById('input');

      function append(role, text) {
        const div = document.createElement('div');
        div.className = `msg ${role}`;
        div.textContent = text;
        messagesEl.appendChild(div);
        messagesEl.scrollTop = messagesEl.scrollHeight;
      }

      form.addEventListener('submit', (e) => {
        e.preventDefault();
        const q = input.value.trim();
        if (!q) return;
        append('user', q);
        input.value = '';

        // Open SSE stream
        const es = new EventSource('/api/chat?message=' + encodeURIComponent(q));
        let buffer = '';

        es.onmessage = (evt) => {
          try {
            const data = JSON.parse(evt.data);
            if (data.token) {
              buffer += data.token;
            }
            if (data.done) {
              append('assistant', buffer);
              buffer = '';
              es.close();
            }
            if (data.error) {
              append('assistant', 'Error: ' + data.error);
              es.close();
            }
          } catch {
            // Ignore malformed chunks
          }
        };

        es.onerror = () => {
          es.close();
        };
      });
    </script>
  </body>
</html>

Production tips:

  • Use POST plus Fetch streaming or WebSockets for more complex payloads.
  • Keep a conversation store so the LLM sees recent context.
  • Render Markdown in answers for code blocks and tables.

Example prompts the bot can answer with live data

With your weather service MCPified:

  • "What will the weather be in Rome tomorrow morning?"
  • "Is there a rain risk today in Oslo after 5pm?" The model calls weather.get_forecast via MCPify, extracts only what it needs, and responds in natural language.

If you MCPify your internal systems:

  • "Where is my order 12345?"
  • "Show me last week's conversion rate by campaign." The model calls your ERP or analytics MCPify tools and composes an answer with inline summaries or tables.

UX and reliability best practices

  • Stream everything: Stream tokens to feel responsive, even while tools are running. The typing effect reduces user drop‑off.
  • Be explicit about loading: Show "checking that for you..." while a tool call is pending.
  • Summarize, don't dump: Use the LLM to present clean prose, not raw JSON.
  • Guardrails: Set rate limits and auth in MCPify. Keep secrets out of prompts.
  • Make big data small: Encourage the LLM to use MCPify's filtering, pagination, and chunking so responses stay within token limits.
  • Observability: Use MCPify analytics and your server logs to track tool usage, errors, and latency.
  • Typed tools over generic: In production, register typed tools discovered from your MCPify service — exhaustive tool metadata gives the model what it needs to construct a valid call without trial-and-error.

See: MCPify Caching and Cost Controls and MCPify JSON Tools


Alternative: typed tools from MCPify (recommended)

Instead of the generic mcp_request, discover typed tool definitions from your MCPify service via the standard MCP tools/list method and pass them directly to your LLM client. This gives the model exact parameter schemas per endpoint.

Pseudo‑code:

// 1) Discover tool definitions via MCP tools/list
const rpc = await fetch('https://weather.mcp.mcpify.org/mcp', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream'
  },
  body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} })
}).then(r => r.json());

// 2) Map MCP tool schemas to your LLM client's tool format
const tools = rpc.result.tools.map(t => ({
  type: 'function',
  function: { name: t.name, description: t.description, parameters: t.inputSchema }
}));

// 3) Call the LLM with typed tools
const first = await openai.chat.completions.create({
  model: 'gpt-5',
  messages,
  tools,
  tool_choice: 'auto'
});

// 4) Execute tool calls as before and stream the final answer

Typed tools reduce guesswork and let the model choose the exact endpoint, with clear examples and response shapes.


Deployment checklist

  • MCPify your APIs and confirm tool metadata looks correct
  • Add OAuth or API keys in MCPify's secure vault
  • Register MCPify tools in your LLM client
  • Implement streaming and typing indicators
  • Add error handling and retries for tool calls
  • Log tool usage and add alerting on error spikes
  • Pen test auth boundaries and PII flows
  • Measure cost and latency, enable caching in MCPify

Call to action

Ready to ship a chatbot that answers with real‑time data and executes actions?

If you want our team to MCPify your APIs for you, say hello: Contact MCPify


Sources

Who This Article Is For

Web developers and chatbot creators building AI-powered chat interfaces with real-time data

About the Author

Herman Sjøberg

Herman Sjøberg

AI Integration Expert

Herman excels at assisting businesses in generating value through AI adoption. With expertise in cloud architecture (Azure Solutions Architect Expert), DevOps, and machine learning, he's passionate about making AI integration accessible to everyone through MCPify.

Connect on LinkedIn