Skip to content

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.

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.

Create a new project with two tools: check_stock (check availability for a specific product) and search_products (find products by name or category).

Terminal window
mkdir inventory-mcp && cd inventory-mcp
npm init -y
npm install @standfast/mcp-server @hono/node-server
npm install -D typescript @types/node

Create 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 database
const 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:

Terminal window
MCP_API_KEY=dev-key npx tsx src/server.ts

Verify tools are registered:

Terminal window
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:

Terminal window
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 .

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:

  1. Go to Settings then MCP Hub.
  2. Click Add MCP Server.
  3. Enter:
    • Name: Inventory Checker
    • URL: https://inventory.yourcompany.com/mcp
    • API Key: the key you set in MCP_API_KEY
  4. Click Connect. oHallo will call tools/list and display your two tools.

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:

Terminal window
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:

  1. Go to Settings then Agents.
  2. Click on Inventory Check Agent.
  3. Under MCP Connections, select Inventory Checker.
  4. Save.

The agent now has access to check_stock and search_products.

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:

  1. The Orchestrator reads the message and identifies the intent: inventory check for a specific product.
  2. It routes to the Inventory Check Agent because its description matches (“Checks product availability and stock levels”).
  3. The Inventory Check Agent calls check_stock with sku: "X-200" and requiredQuantity: 50.
  4. Your MCP server returns: 120 in stock, available, unit price EUR 84.50.
  5. 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:

  1. Go to Settings then Agents.
  2. Click on Inventory Check Agent.
  3. Select View Executions.
  4. You should see the recent execution with the check_stock tool call, including the arguments passed and the response received.

View via the API:

Terminal window
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: completed means the agent ran successfully. failed means something went wrong.
  • Tool call status: success means the MCP server responded correctly. error means 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.

The steps to build a custom agent are:

  1. Build an MCP server with the tools the agent needs.
  2. Deploy the server and register it in oHallo’s MCP Hub.
  3. Create the agent with a clear description and system prompt.
  4. Connect the MCP server to the agent.
  5. 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.