Building a Custom Agent
This guide walks through the entire process of building a custom specialist agent — from writing the MCP server, to registering it in oHallo, to testing it with a real conversation. By the end, you will have an “Inventory Check Agent” that customers can ask about product stock levels.
The goal
Section titled “The goal”A customer writes: “Do you have the Model X-200 in stock? I need 50 units.”
The orchestrator recognises this as an inventory question, routes to your Inventory Check Agent, which calls your MCP server to check real-time stock levels, and responds with accurate availability information.
Step 1: Build the MCP server
Section titled “Step 1: Build the MCP server”Create a new project with two tools: check_stock (check availability for a specific product) and search_products (find products by name or category).
mkdir inventory-mcp && cd inventory-mcpnpm init -ynpm install @standfast/mcp-server @hono/node-servernpm install -D typescript @types/nodeCreate src/server.ts:
import { createMcpServer, McpError } from '@standfast/mcp-server'import type { McpToolDef } from '@standfast/mcp-server'import { serve } from '@hono/node-server'
// Simulated product inventory -- replace with your real databaseconst inventory: Record<string, { sku: string name: string category: string stockQuantity: number warehouse: string unitPrice: number currency: string reorderLeadDays: number}> = { 'X-200': { sku: 'X-200', name: 'Model X-200 Industrial Sensor', category: 'sensors', stockQuantity: 120, warehouse: 'Rotterdam-NL', unitPrice: 84.50, currency: 'EUR', reorderLeadDays: 14, }, 'X-300': { sku: 'X-300', name: 'Model X-300 Precision Sensor', category: 'sensors', stockQuantity: 0, warehouse: 'Rotterdam-NL', unitPrice: 129.00, currency: 'EUR', reorderLeadDays: 21, }, 'C-100': { sku: 'C-100', name: 'Controller C-100 Base Unit', category: 'controllers', stockQuantity: 45, warehouse: 'Rotterdam-NL', unitPrice: 320.00, currency: 'EUR', reorderLeadDays: 28, }, 'A-50': { sku: 'A-50', name: 'Mounting Adapter A-50', category: 'accessories', stockQuantity: 500, warehouse: 'Rotterdam-NL', unitPrice: 12.00, currency: 'EUR', reorderLeadDays: 7, },}
const tools: McpToolDef[] = [ { name: 'check_stock', description: 'Check the current stock level for a specific product by SKU. Returns the available quantity, warehouse location, unit price, and reorder lead time. Use this when a customer asks about availability of a specific product.', inputSchema: { type: 'object', properties: { tenantId: { type: 'string' }, workspaceId: { type: 'string' }, sku: { type: 'string', description: 'The product SKU code, e.g. X-200, C-100', }, requiredQuantity: { type: 'integer', description: 'The quantity the customer needs. Used to determine if stock is sufficient.', }, }, required: ['tenantId', 'workspaceId', 'sku'], }, handler: async (args) => { const sku = (args.sku as string).toUpperCase() const requiredQuantity = (args.requiredQuantity as number) ?? 1
const product = inventory[sku] if (!product) { throw new McpError('not_found', `Product with SKU "${sku}" not found in catalog`) }
const sufficient = product.stockQuantity >= requiredQuantity
return { sku: product.sku, name: product.name, category: product.category, stockQuantity: product.stockQuantity, warehouse: product.warehouse, unitPrice: { amount: product.unitPrice, currency: product.currency }, requestedQuantity: requiredQuantity, available: sufficient, shortfall: sufficient ? 0 : requiredQuantity - product.stockQuantity, reorderLeadDays: product.reorderLeadDays, } }, }, { name: 'search_products', description: 'Search the product catalog by name or category. Returns matching products with stock levels and pricing. Use this when a customer asks about available products or browses a category.', inputSchema: { type: 'object', properties: { tenantId: { type: 'string' }, workspaceId: { type: 'string' }, query: { type: 'string', description: 'Search term to match against product name or SKU', }, category: { type: 'string', enum: ['sensors', 'controllers', 'accessories'], description: 'Filter by product category', }, inStockOnly: { type: 'boolean', description: 'If true, only return products with stock > 0', }, }, required: ['tenantId', 'workspaceId'], }, handler: async (args) => { const query = ((args.query as string) ?? '').toLowerCase() const category = args.category as string | undefined const inStockOnly = (args.inStockOnly as boolean) ?? false
let results = Object.values(inventory)
if (query) { results = results.filter( (p) => p.name.toLowerCase().includes(query) || p.sku.toLowerCase().includes(query) ) }
if (category) { results = results.filter((p) => p.category === category) }
if (inStockOnly) { results = results.filter((p) => p.stockQuantity > 0) }
return { products: results.map((p) => ({ sku: p.sku, name: p.name, category: p.category, stockQuantity: p.stockQuantity, inStock: p.stockQuantity > 0, unitPrice: { amount: p.unitPrice, currency: p.currency }, })), totalResults: results.length, } }, },]
async function main() { const app = await createMcpServer({ name: 'inventory-checker', version: '1.0.0', authType: 'api_key', tools, })
serve({ fetch: app.fetch, port: 4300 }, (info) => { console.log(`Inventory MCP server listening on port ${info.port}`) })}
main()Test it locally:
MCP_API_KEY=dev-key npx tsx src/server.tsVerify tools are registered:
curl -s http://localhost:4300/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer dev-key" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq '.result.tools[].name'Test the check_stock tool:
curl -s http://localhost:4300/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer dev-key" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "check_stock", "arguments": { "tenantId": "test", "workspaceId": "test", "sku": "X-200", "requiredQuantity": 50 } } }' | jq .Step 2: Deploy and register in MCP Hub
Section titled “Step 2: Deploy and register in MCP Hub”Deploy your MCP server to a hosting provider that supports Node.js. It needs to be reachable over HTTPS from oHallo’s infrastructure. Set a strong MCP_API_KEY in your production environment.
Register in oHallo:
- Go to Settings then MCP Hub.
- Click Add MCP Server.
- Enter:
- Name: Inventory Checker
- URL:
https://inventory.yourcompany.com/mcp - API Key: the key you set in
MCP_API_KEY
- Click Connect. oHallo will call
tools/listand display your two tools.
Step 3: Create the custom agent
Section titled “Step 3: Create the custom agent”Create a specialist agent via the API. This agent will be assigned the inventory tools and will have a system prompt that guides its behaviour:
curl -s -X POST https://api.ohallo.eu/api/agent-definitions/custom \ -H "Authorization: Bearer sf_live_v1_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "name": "Inventory Check Agent", "description": "Checks product availability and stock levels. Called when customers ask about product availability, stock, or delivery timelines for specific products.", "systemPrompt": "You are an inventory specialist. When asked about product availability, use the check_stock tool with the product SKU. If the customer does not provide a SKU, use search_products to find matching items first. Always include the unit price and stock quantity in your response. If a product is out of stock, mention the reorder lead time.", "workspaceId": "ws_abc123" }' | jq .Response:
{ "id": "agent_inv_001", "name": "Inventory Check Agent", "description": "Checks product availability and stock levels...", "type": "custom", "workspaceId": "ws_abc123", "createdAt": "2026-03-25T12:00:00Z"}Now connect the MCP server to the agent. In the oHallo dashboard:
- Go to Settings then Agents.
- Click on Inventory Check Agent.
- Under MCP Connections, select Inventory Checker.
- Save.
The agent now has access to check_stock and search_products.
Step 4: Test with a real conversation
Section titled “Step 4: Test with a real conversation”Send a test message to one of your oHallo channels (email, chat, or WhatsApp). For example:
“Hi, I need to order 50 units of the Model X-200. Can you check if they are in stock?”
Here is what happens behind the scenes:
- The Orchestrator reads the message and identifies the intent: inventory check for a specific product.
- It routes to the Inventory Check Agent because its description matches (“Checks product availability and stock levels”).
- The Inventory Check Agent calls
check_stockwithsku: "X-200"andrequiredQuantity: 50. - Your MCP server returns: 120 in stock, available, unit price EUR 84.50.
- The Message Agent composes the response:
“The Model X-200 Industrial Sensor is in stock. We currently have 120 units available in our Rotterdam warehouse, which covers your request for 50 units. The unit price is EUR 84.50. Would you like me to proceed with a quote?”
Step 5: Monitor agent executions and tool calls
Section titled “Step 5: Monitor agent executions and tool calls”After testing, verify that everything worked correctly.
View in the dashboard:
- Go to Settings then Agents.
- Click on Inventory Check Agent.
- Select View Executions.
- You should see the recent execution with the
check_stocktool call, including the arguments passed and the response received.
View via the API:
curl -s "https://api.ohallo.eu/api/agent-executions?agentId=agent_inv_001&limit=5" \ -H "Authorization: Bearer sf_live_v1_your_key_here" | jq .Response:
{ "executions": [ { "id": "exec_001", "agentId": "agent_inv_001", "conversationId": "conv_test123", "status": "completed", "startedAt": "2026-03-25T15:00:00Z", "completedAt": "2026-03-25T15:00:03Z", "toolCalls": [ { "id": "tc_001", "toolName": "check_stock", "mcpServerName": "inventory-checker", "arguments": { "sku": "X-200", "requiredQuantity": 50 }, "result": { "sku": "X-200", "name": "Model X-200 Industrial Sensor", "stockQuantity": 120, "available": true }, "durationMs": 180, "status": "success" } ] } ]}Check for:
- Status:
completedmeans the agent ran successfully.failedmeans something went wrong. - Tool call status:
successmeans the MCP server responded correctly.errormeans it returned an error or timed out. - Duration: Tool calls should complete well under 30 seconds. If they are slow, optimise your MCP server’s data access.
- Arguments: Verify the agent extracted the correct values from the customer’s message.
Summary
Section titled “Summary”The steps to build a custom agent are:
- Build an MCP server with the tools the agent needs.
- Deploy the server and register it in oHallo’s MCP Hub.
- Create the agent with a clear description and system prompt.
- Connect the MCP server to the agent.
- Test with a real message and monitor the execution logs.
You can repeat this pattern for any domain — order management, warranty claims, appointment scheduling, quoting, or anything else your business handles.
Next steps
Section titled “Next steps”- Testing and Debugging — debug tool calls and common issues
- Tool Schema — advanced input schema patterns
- Managing the Knowledge Base — add static knowledge alongside your live tools