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.
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
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
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
Step 7 — Run Your Web Chat
Start the app:
python app.py
Open http://127.0.0.1:5000 in your browser.
Expected Output
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 tonightWhat notes do I have?
Expected Output
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! 🚀
Continue Learning with Rohi
You've used your 3 free Rohi questions. Create a free account to continue learning.