Skip to content

Tool Schema

Every tool on an MCP server has an input schema that defines what parameters it accepts, and a handler that returns structured data. The schema uses JSON Schema format, and oHallo’s agents rely on it to determine what data to pass when calling your tools.

An input schema is a JSON Schema object with type: 'object'. Each property defines a parameter that the tool accepts:

{
type: 'object',
properties: {
orderNumber: {
type: 'string',
description: 'The order number to look up, e.g. ORD-48291',
},
},
required: ['orderNumber'],
}

MCP Hub automatically injects tenantId and workspaceId into every tool call’s arguments at call time. You do not need to declare them in your input schema — they arrive as extra fields in the args object your handler receives.

Your server can use them for:

  • Data isolation — if you serve multiple oHallo accounts, filter queries by tenantId
  • Configuration lookup — load account-specific settings based on the tenantId or workspaceId
  • Audit logging — record which account triggered each operation
handler: async (args) => {
const tenantId = args.tenantId as string // injected by MCP Hub
const workspaceId = args.workspaceId as string // injected by MCP Hub
const orderNumber = args.orderNumber as string
// Use tenantId to scope the query
const order = await db.query(
'SELECT * FROM orders WHERE tenant_id = $1 AND order_number = $2',
[tenantId, orderNumber]
)
return order
}

If your server does not need account-level data isolation (e.g. it already serves a single customer), you can simply ignore these fields.

Define the parameters your tool needs. Use clear types and include a description for each:

properties: {
customerId: {
type: 'string',
description: 'The unique customer identifier, e.g. cust_12345',
},
includeHistory: {
type: 'boolean',
description: 'If true, include the last 10 orders in the response',
},
maxResults: {
type: 'integer',
description: 'Maximum number of results to return (1-100, default 20)',
},
}

Use enum when a parameter should be one of a fixed set of values:

status: {
type: 'string',
enum: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'],
description: 'Filter orders by status',
}

Dates are represented as strings. Use the description to specify the expected format:

fromDate: {
type: 'string',
description: 'Start date for the search range in ISO 8601 format, e.g. 2026-01-15',
},
toDate: {
type: 'string',
description: 'End date for the search range in ISO 8601 format, e.g. 2026-03-20',
}
productIds: {
type: 'array',
items: { type: 'string' },
description: 'List of product IDs to check inventory for',
}
shippingAddress: {
type: 'object',
properties: {
street: { type: 'string', description: 'Street address' },
city: { type: 'string', description: 'City name' },
postalCode: { type: 'string', description: 'Postal or ZIP code' },
country: { type: 'string', description: 'ISO 3166-1 alpha-2 country code, e.g. DE, NL, US' },
},
required: ['street', 'city', 'postalCode', 'country'],
description: 'The delivery address for the shipment',
}

List all mandatory parameters in the required array. Optional parameters are omitted from required — the agent will only include them when it has the information:

{
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search term to match against product name or description',
},
category: {
type: 'string',
description: 'Filter by product category, e.g. electronics, clothing',
},
maxResults: {
type: 'integer',
description: 'Maximum number of results to return (default 20)',
},
},
required: ['query'],
}

In this example, category and maxResults are optional. The agent will include them when the customer mentions a category or asks for a specific number of results. Note that tenantId and workspaceId are not listed here — they are injected automatically by MCP Hub.

The planning agent reads your tool and parameter descriptions to decide when and how to call each tool. Good descriptions directly affect how well the agent serves your customers.

Tool descriptions should explain what the tool does, what it returns, and when to use it:

WeakStrong
"Get order""Look up an order by order number. Returns the order status, line items with quantities and prices, shipping address, and estimated delivery date."
"Search""Search the product catalog by name, SKU, or description. Returns matching products with prices and stock availability. Use this when a customer asks about a product."

Parameter descriptions should include the expected format, an example value, and any constraints:

WeakStrong
"The ID""The customer's order number, e.g. ORD-48291"
"Date""Order date in ISO 8601 format, e.g. 2026-03-20"
"Status""Filter by order status. One of: pending, confirmed, shipped, delivered, cancelled"

Tool handlers return plain JavaScript objects. The MCP framework serialises them to JSON automatically. Return structured data with clear field names:

handler: async (args) => {
const order = await db.getOrder(args.orderNumber as string)
return {
orderNumber: order.number,
status: order.status,
placedAt: order.createdAt.toISOString(),
items: order.lines.map((line) => ({
name: line.productName,
quantity: line.quantity,
unitPrice: line.unitPrice,
currency: line.currency,
})),
total: {
amount: order.totalAmount,
currency: order.currency,
},
shipping: {
carrier: order.shippingCarrier,
trackingNumber: order.trackingNumber,
estimatedDelivery: order.estimatedDelivery?.toISOString() ?? null,
},
}
}

Guidelines for return values:

  • Use clear field names. The agent reads the returned data to compose its response. estimatedDelivery is better than eta or del_dt.
  • Include units and currency. Do not return bare numbers. { amount: 149.99, currency: 'EUR' } is better than { total: 149.99 }.
  • Return null for missing optional fields. The agent can tell the customer “no tracking information available yet” rather than seeing an undefined field.
  • Keep responses focused. Return the data relevant to the tool’s purpose. A get_order tool does not need to return the entire customer profile.

Use McpError to signal errors. The agent reads the error type and message to explain the situation to the customer:

import { McpError } from '@standfast/mcp-server'
handler: async (args) => {
const orderId = args.orderNumber as string
if (!orderId.match(/^ORD-\d+$/)) {
throw new McpError('validation_error', 'Order number must match format ORD-XXXXX')
}
const order = await db.getOrder(orderId)
if (!order) {
throw new McpError('not_found', `Order ${orderId} does not exist`)
}
if (order.tenantId !== args.tenantId) {
throw new McpError('permission_denied', 'You do not have access to this order')
}
return { orderNumber: order.number, status: order.status }
}

Choose the error type that best matches the situation:

Error typeUse whenAgent behaviour
not_foundThe requested resource does not existTells the customer the item was not found
validation_errorInput parameters are malformed or out of rangeAsks the customer to clarify or correct their input
authentication_errorThe request could not be authenticatedReports a system issue (not shown as customer’s fault)
permission_deniedThe caller lacks access to this resourceTells the customer they do not have access
rate_limitToo many requests in a short periodMay retry or inform the customer of a temporary delay
server_errorAn unexpected failure in your systemReports a temporary issue and suggests trying again

Complete example: search tool with filters

Section titled “Complete example: search tool with filters”

Here is a complete tool definition for a product search with multiple filter options:

{
name: 'search_products',
description: 'Search the product catalog by name or description. Supports filtering by category, price range, and availability. Returns matching products with current prices and stock status.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search term to match against product name and description',
},
category: {
type: 'string',
enum: ['electronics', 'clothing', 'home', 'sports', 'books'],
description: 'Filter results to a specific product category',
},
minPrice: {
type: 'number',
description: 'Minimum price in EUR, e.g. 10.00',
},
maxPrice: {
type: 'number',
description: 'Maximum price in EUR, e.g. 500.00',
},
inStockOnly: {
type: 'boolean',
description: 'If true, only return products that are currently in stock',
},
page: {
type: 'integer',
description: 'Page number for pagination (starts at 1, default 1)',
},
pageSize: {
type: 'integer',
description: 'Number of results per page (1-50, default 20)',
},
},
required: ['query'],
},
handler: async (args) => {
const query = args.query as string
const category = args.category as string | undefined
const minPrice = args.minPrice as number | undefined
const maxPrice = args.maxPrice as number | undefined
const inStockOnly = (args.inStockOnly as boolean) ?? false
const page = (args.page as number) ?? 1
const pageSize = Math.min((args.pageSize as number) ?? 20, 50)
const results = await productSearch({
query,
category,
minPrice,
maxPrice,
inStockOnly,
offset: (page - 1) * pageSize,
limit: pageSize,
})
return {
products: results.items.map((p) => ({
id: p.id,
name: p.name,
description: p.shortDescription,
category: p.category,
price: { amount: p.price, currency: 'EUR' },
inStock: p.stockQuantity > 0,
stockQuantity: p.stockQuantity,
})),
pagination: {
page,
pageSize,
totalResults: results.total,
totalPages: Math.ceil(results.total / pageSize),
},
}
},
}