DAY 5

Web Chat UI 🌐💬🤖

Learn how to create a simple web chat UI for your AI agent, making it accessible and interactive for users. By the end of Day 5, you'll have a basic web interface to test your AI model.

⏱ 15 mins
⚡ +50 XP

Day 5 — Wrap Your Agent in a Web Chat UI

MCP & AI Agents — Agent Bootcamp Part 2 — RohithBuilds

Yesterday your agent worked — but only in a terminal, and only for you. Today you'll wrap it in a Flask app with a real chat interface in the browser, the same way Part 1, Day 6 turned your research agent into a web app.

Step 1 — Recap & Today's Plan

By the end of today, your project folder will look like this:

mcp-agent-bootcamp/
├── server.py        (Days 1-3 — your MCP server)
├── agent.py          (NEW — reusable agent logic)
├── app.py            (NEW — Flask app)
├── client.py         (Day 4 — terminal version, still works)
├── notes.db
├── requirements.txt
├── .gitignore
└── templates/
    └── index.html    (NEW — chat UI)

The plan: extract the reusable parts of Day 4's client.py into agent.py, then build a small Flask app and chat page around it.

💡 A Design Tradeoff, By Choice

Each chat message today will open a fresh connection to server.py (like Day 4 did once per run). That means each reply takes a couple of extra seconds — but the code stays simple and easy to follow. A production app would keep one connection open across requests; that's a great next optimization once this is working.

Step 2 — Extract Your Agent Logic into agent.py

Create a new file agent.py. This takes the reusable pieces from client.py — connection setup, the tool-format converter, and one full turn of conversation — and packages them as a function any app can call:

import json
import os
from dotenv import load_dotenv
from groq import Groq
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

load_dotenv()
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))

server_params = StdioServerParameters(
    command="python",
    args=["server.py"],
)

SYSTEM_PROMPT = (
    "You are Rohi, a helpful assistant for a CS student. "
    "Use the available tools whenever they help answer the question."
)


def mcp_tools_to_groq_format(mcp_tools):
    groq_tools = []
    for tool in mcp_tools:
        groq_tools.append({
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.inputSchema,
            }
        })
    return groq_tools


async def run_turn(messages):
    """Run one turn of conversation.
    `messages` must already include the new user message.
    Appends the assistant's reply to `messages` and returns its text."""
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools_result = await session.list_tools()
            groq_tools = mcp_tools_to_groq_format(tools_result.tools)

            response = groq_client.chat.completions.create(
                model="llama-3.3-70b-versatile",
                messages=messages,
                tools=groq_tools,
            )
            reply = response.choices[0].message

            if reply.tool_calls:
                messages.append({
                    "role": "assistant",
                    "content": reply.content,
                    "tool_calls": [
                        {
                            "id": call.id,
                            "type": "function",
                            "function": {
                                "name": call.function.name,
                                "arguments": call.function.arguments,
                            },
                        }
                        for call in reply.tool_calls
                    ],
                })

                for call in reply.tool_calls:
                    args = json.loads(call.function.arguments)
                    result = await session.call_tool(call.function.name, arguments=args)
                    messages.append({
                        "role": "tool",
                        "tool_call_id": call.id,
                        "content": result.content[0].text,
                    })

                final = groq_client.chat.completions.create(
                    model="llama-3.3-70b-versatile",
                    messages=messages,
                )
                final_text = final.choices[0].message.content
            else:
                final_text = reply.content

            messages.append({"role": "assistant", "content": final_text})
            return final_text

This is almost exactly Day 4's logic — just reorganized into one function, run_turn(), that any code can call with a message list.

Step 3 — Test agent.py Standalone

Before bringing Flask into the picture, confirm agent.py works on its own. Create test_agent.py:

import asyncio
from agent import run_turn, SYSTEM_PROMPT

async def test():
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": "How many days until GATE 2027 on 2027-02-08?"}
    ]
    reply = await run_turn(messages)
    print("Rohi:", reply)

asyncio.run(test())

Run it:

python test_agent.py

Expected Output

Terminal showing agent.py returning a natural-language exam countdown answer

Step 4 — Install Flask

This is a new venv for this course, so Flask isn't installed yet. Run:

pip install flask gunicorn

Expected Output

Terminal showing flask and gunicorn installing successfully

Step 5 — Build the Flask App

Create app.py:

from flask import Flask, request, jsonify, render_template
import asyncio
from agent import run_turn, SYSTEM_PROMPT

app = Flask(__name__)

conversation = [{"role": "system", "content": SYSTEM_PROMPT}]


@app.route("/")
def home():
    return render_template("index.html")


@app.route("/chat", methods=["POST"])
def chat():
    data = request.get_json()
    user_message = data.get("message", "").strip()

    if not user_message:
        return jsonify({"success": False, "error": "Please type a message."})

    conversation.append({"role": "user", "content": user_message})
    reply_text = asyncio.run(run_turn(conversation))

    return jsonify({"success": True, "reply": reply_text})


@app.route("/reset", methods=["POST"])
def reset():
    global conversation
    conversation = [{"role": "system", "content": SYSTEM_PROMPT}]
    return jsonify({"success": True})


if __name__ == "__main__":
    app.run(debug=True)

Three routes: / serves the chat page, /chat handles each message (calling asyncio.run() to bridge into agent.py's async world), and /reset clears the conversation — handy while testing.

Step 6 — Build the Chat UI

Just like Part 1, Day 6, we'll generate the frontend with a setup script. Create setup_frontend.py:

import os
os.makedirs("templates", exist_ok=True)

html_code = '''
<!DOCTYPE html>
<html>
<head>
<title>Rohi - MCP Agent</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
    font-family: Arial, sans-serif;
    background: #f5f7fb;
    display: flex;
    justify-content: center;
    padding: 40px;
}
.container {
    background: white;
    width: 100%;
    max-width: 600px;
    padding: 24px;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
}
.header h1 { font-size: 20px; }
#resetBtn {
    background: #eee;
    border: none;
    padding: 6px 12px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
}
#chatBox {
    height: 420px;
    overflow-y: auto;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 12px;
    margin-bottom: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.message {
    padding: 10px 14px;
    border-radius: 12px;
    max-width: 80%;
    line-height: 1.4;
    white-space: pre-wrap;
}
.message.user {
    align-self: flex-end;
    background: #0066cc;
    color: white;
}
.message.bot {
    align-self: flex-start;
    background: #f0f1f5;
    color: #222;
}
.message.thinking {
    font-style: italic;
    color: #888;
}
.input-row {
    display: flex;
    gap: 8px;
}
#messageInput {
    flex: 1;
    padding: 12px;
    border: 1px solid #ccc;
    border-radius: 8px;
    font-size: 15px;
}
#sendBtn {
    background: #0066cc;
    color: white;
    border: none;
    padding: 12px 20px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 15px;
}
</style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1>🤖 Rohi — MCP Agent</h1>
        <button id="resetBtn">Reset</button>
    </div>
    <div id="chatBox"></div>
    <div class="input-row">
        <input id="messageInput" type="text" placeholder="Ask about exams, notes, anything...">
        <button id="sendBtn">Send</button>
    </div>
</div>

<script>
async function sendMessage() {
    const input = document.getElementById("messageInput");
    const message = input.value.trim();
    if (!message) return;

    addMessage("user", message);
    input.value = "";
    addMessage("bot", "Thinking...", true);

    const response = await fetch("/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: message })
    });
    const data = await response.json();

    removeThinking();

    if (data.success) {
        addMessage("bot", data.reply);
    } else {
        addMessage("bot", "Error: " + data.error);
    }
}

function addMessage(role, text, isThinking) {
    const chatBox = document.getElementById("chatBox");
    const div = document.createElement("div");
    div.className = "message " + role + (isThinking ? " thinking" : "");
    div.textContent = text;
    chatBox.appendChild(div);
    chatBox.scrollTop = chatBox.scrollHeight;
}

function removeThinking() {
    const el = document.querySelector(".thinking");
    if (el) el.remove();
}

document.getElementById("sendBtn").addEventListener("click", sendMessage);
document.getElementById("messageInput").addEventListener("keypress", function (e) {
    if (e.key === "Enter") sendMessage();
});
document.getElementById("resetBtn").addEventListener("click", async function () {
    await fetch("/reset", { method: "POST" });
    document.getElementById("chatBox").innerHTML = "";
});
</script>
</body>
</html>
'''

with open("templates/index.html", "w", encoding="utf-8") as f:
    f.write(html_code)

print("templates/index.html created")

Run it:

python setup_frontend.py

Expected Output

Terminal showing templates/index.html created

Step 7 — Run Your Web Chat

Start the app:

python app.py

Open http://127.0.0.1:5000 in your browser.

Expected Output

Browser showing the empty Rohi MCP Agent chat interface

Try the same conversation from Day 4, but now in the browser:

  • How many days until GATE 2027 on 2027-02-08?
  • Add a note: revise OS notes tonight
  • What notes do I have?

Expected Output

Browser chat interface showing a multi-turn conversation where Rohi answers an exam question, confirms a saved note, and lists notes

Click Reset to clear the conversation and start fresh — useful while testing.

Step 8 — Save Your Progress

Update your requirements.txt now that Flask and Gunicorn are installed:

pip freeze > requirements.txt

Double-check your .gitignore still includes:

venv/
__pycache__/
*.pyc
.env
notes.db

✅ Day 5 Complete

Here is what you accomplished today:

Key Takeaways

  • Extracted reusable agent logic into agent.py ✅
  • Verified the agent works standalone ✅
  • Built a Flask app with /chat and /reset routes ✅
  • Bridged async MCP code into Flask's sync routes with asyncio.run() ✅
  • Built a browser chat UI from scratch ✅
  • Had a full multi-tool conversation with your agent in the browser ✅

What Is Coming Tomorrow

On Day 6 you will:

  • Add a brand-new MCP tool that calls a real, live public API
  • Get current weather for any city — no API key required
  • Watch your agent use this new tool automatically, right from the chat UI you just built

See you there! 🚀

← Previous Lesson