Building a Custom Agent
Build a custom AI agent that connects to the FrontDeskOS MCP server using the MCP TypeScript SDK. Full control over the agent logic, UI, and tool usage.
Overview
While Claude Desktop and Retell AI provide ready-made agent experiences, building a custom agent gives you full control over the interaction flow, UI, and business logic. This guide walks through creating a custom MCP client that connects to FrontDeskOS.
Project Setup
Terminalbash
mkdir my-frontdesk-agent && cd my-frontdesk-agent
npm init -y
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk @frontdeskos/mcp-serverCreating the MCP Client
First, create a client that connects to the FrontDeskOS MCP server and discovers available tools:
mcp-client.tstypescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export async function createMCPClient() {
const transport = new StdioClientTransport({
command: "npx",
args: ["@frontdeskos/mcp-server"],
env: {
FRONTDESK_API_KEY: process.env.FRONTDESK_API_KEY!,
FRONTDESK_WORKSPACE_ID: process.env.FRONTDESK_WORKSPACE_ID!,
},
});
const client = new Client(
{ name: "my-frontdesk-agent", version: "1.0.0" },
{ capabilities: {} }
);
await client.connect(transport);
// Discover available tools
const { tools } = await client.listTools();
console.log(`Connected to FrontDeskOS with ${tools.length} tools`);
// Read resources for context
const { resources } = await client.listResources();
console.log(`Available resources: ${resources.map(r => r.uri).join(", ")}`);
return client;
}Building the Agent Loop
Now, build the agent loop that takes user input, sends it to an LLM with tool definitions, and executes any tool calls:
agent.tstypescript
import Anthropic from "@anthropic-ai/sdk";
import { createMCPClient } from "./mcp-client.js";
const anthropic = new Anthropic();
async function main() {
const mcp = await createMCPClient();
// Get tools and convert to Anthropic's format
const { tools: mcpTools } = await mcp.listTools();
const tools = mcpTools.map((tool) => ({
name: tool.name,
description: tool.description || "",
input_schema: tool.inputSchema as Anthropic.Tool["input_schema"],
}));
// Load business context from resources
const businessInfo = await mcp.readResource({
uri: "frontdesk://business-info",
});
const staffDir = await mcp.readResource({
uri: "frontdesk://staff-directory",
});
const systemPrompt = `You are an AI assistant for front desk management.
You have access to FrontDeskOS tools to manage calls, appointments, leads, and analytics.
Business context:
${businessInfo.contents[0].text}
Staff directory:
${staffDir.contents[0].text}
Use the available tools to help the user manage their front desk operations.
Always confirm important actions before executing them.`;
// Conversation loop
const messages: Anthropic.MessageParam[] = [];
async function chat(userMessage: string): Promise<string> {
messages.push({ role: "user", content: userMessage });
let response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
system: systemPrompt,
tools,
messages,
});
// Handle tool use loop
while (response.stop_reason === "tool_use") {
const assistantMessage = response.content;
messages.push({ role: "assistant", content: assistantMessage });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of assistantMessage) {
if (block.type === "tool_use") {
console.log(`Calling tool: ${block.name}`);
const result = await mcp.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>,
});
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result.content),
});
}
}
messages.push({ role: "user", content: toolResults });
response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
system: systemPrompt,
tools,
messages,
});
}
// Extract text response
const textBlocks = response.content.filter(
(b) => b.type === "text"
);
const reply = textBlocks.map((b) => b.text).join("\n");
messages.push({ role: "assistant", content: reply });
return reply;
}
return { chat, close: () => mcp.close() };
}
// Run the agent
const agent = await main();
const reply = await agent.chat("What calls did we get today?");
console.log(reply);Adding a Web Interface
Wrap the agent in an HTTP server to create a chat API:
server.tstypescript
import express from "express";
import { main as createAgent } from "./agent.js";
const app = express();
app.use(express.json());
// Store agent sessions
const sessions = new Map();
app.post("/api/chat", async (req, res) => {
const { session_id, message } = req.body;
// Get or create agent session
if (!sessions.has(session_id)) {
sessions.set(session_id, await createAgent());
}
const agent = sessions.get(session_id);
const reply = await agent.chat(message);
res.json({ reply });
});
app.listen(3000, () => {
console.log("Custom FrontDesk agent running on :3000");
});SSE Transport
For production deployments, consider using SSE transport instead of stdio. This allows your agent to connect to a remote FrontDeskOS server over HTTP, which is more suitable for cloud deployments. See the Authentication page for SSE transport configuration.
Best Practices
- Cache resource reads at the start of a session rather than on every request.
- Implement confirmation prompts for write operations (booking, cancelling, etc.).
- Add logging for all tool calls for debugging and audit purposes.
- Handle MCP connection drops gracefully with automatic reconnection.
- Use restricted API keys with only the scopes your agent needs.
- Implement rate limit handling with exponential backoff.