Testing and Debugging
This guide covers how to verify your MCP server works correctly before connecting it to oHallo, and how to debug issues once it is live.
Testing locally with curl
Section titled “Testing locally with curl”MCP servers use JSON-RPC 2.0 over HTTP. All requests are POST to the /mcp endpoint with a JSON body. You need four things in every request:
Content-Type: application/jsonheaderAccept: application/json, text/event-streamheaderAuthorization: Bearer <your-key>header, when your server checks a key as the one in Build an MCP Server does- A JSON-RPC body with
jsonrpc,id,method, andparams
The examples call the server from Build an MCP Server, which listens on port 4100 and checks the key in MCP_API_KEY. Its SDK sends each answer as a server-sent event with the JSON-RPC response on the data: line, so the commands keep that line with sed and format it with jq. Start your server locally first:
MCP_API_KEY=test-key npx tsx src/server.tsVerify tool registration with tools/list
Section titled “Verify tool registration with tools/list”The first thing to test is whether your tools are registered correctly. This is the call that oHallo makes when you add an MCP server in the dashboard, and again every five minutes, except while the connection is inactive:
curl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test-key" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }' | sed -n 's/^data: //p' | jq .Check the response:
- Every tool you defined should appear in
result.tools - Each tool should have
name,description, andinputSchema, and anoutputSchemawhen it declares one - All required fields should be listed in
inputSchema.required tenantIdandworkspaceIdappear only in a tool that reads them: oHallo adds both to the arguments of every call
If a tool is missing, verify that your server registers it with registerTool in the function that builds the server.
Test individual tools with tools/call
Section titled “Test individual tools with tools/call”Call each tool with realistic test data. A tool that declares tenantId and workspaceId receives them from oHallo; when you call such a tool directly, pass them yourself. get_product declares neither:
curl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test-key" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_product", "arguments": { "sku": "EX-100" } } }' | sed -n 's/^data: //p' | jq .A successful response looks like:
{ "result": { "content": [ { "type": "text", "text": "{\"sku\":\"EX-100\",\"name\":\"Example Desk Lamp\",\"price\":{\"amount\":49.5,\"currency\":\"EUR\"},\"inStock\":true}" } ], "structuredContent": { "sku": "EX-100", "name": "Example Desk Lamp", "price": { "amount": 49.5, "currency": "EUR" }, "inStock": true } }, "jsonrpc": "2.0", "id": 2}The tool’s data is in result.structuredContent, and the same JSON string is in result.content[0].text.
Test error cases
Section titled “Test error cases”Test every error path your tool can produce:
# Test not_found errorcurl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test-key" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_product", "arguments": { "sku": "EX-999" } } }' | sed -n 's/^data: //p' | jq .An error comes back in a successful JSON-RPC response whose result is marked isError: true, with the JSON of { error_type, reason } in result.content[0].text. Verify that the error type and reason match what you expect.
Arguments that do not match the input schema come back the same way, with a text item that begins Input validation error:
# Test an argument of the wrong typecurl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test-key" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "get_product", "arguments": { "sku": 100 } } }' | sed -n 's/^data: //p' | jq .{ "result": { "content": [ { "type": "text", "text": "Input validation error: Invalid arguments for tool get_product: sku: Invalid input: expected string, received number" } ], "isError": true }, "jsonrpc": "2.0", "id": 4}Test authentication
Section titled “Test authentication”Verify that requests without a valid token are rejected:
# No auth header: should be rejectedcurl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/list", "params": {} }'
# Wrong token: should be rejectedcurl -s http://localhost:4100/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer wrong-key" \ -d '{ "jsonrpc": "2.0", "id": 6, "method": "tools/list", "params": {} }'Both are answered 401 with an error and no tool data:
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Unauthorized"},"id":null}Checking tool call logs in oHallo
Section titled “Checking tool call logs in oHallo”Once your MCP server is connected to oHallo and handling live conversations, you can inspect its tool calls from the dashboard:
- Open the conversation under Conversations.
- Select Inspect on one of the assistant’s replies.
- The panel lists each agent that worked on that reply, with the number of tools it called and how long it took. Select an agent to see its tool calls, and a tool call to see its input and its output or error.
This is the fastest way to diagnose issues like “the agent called the wrong tool” or “the agent passed unexpected arguments”.
Agent observability API
Section titled “Agent observability API”You can also query tool call data programmatically using the oHallo API, with an API key that holds the conversations:read scope. This is useful for building monitoring dashboards or alerting on failures.
List the agent executions for a conversation:
Executions are queried per conversation. The conversationId query parameter is required; without it the endpoint returns 400. The response is a bare JSON array of executions.
curl -s "https://api.ohallo.eu/api/agent-executions?conversationId=c9f8e7d6-b5a4-3210-fedc-ba9876543210" \ -H "Authorization: Bearer sf_live_v1_your_key_here" | jq .Response (trimmed to the main fields):
[ { "id": "8a1b2c3d-4e5f-4061-8273-90abcdef1234", "agentId": "7c3f9a1e-2b4d-4c8a-9f6e-1a2b3c4d5e6f", "agentName": "Order Lookup Agent", "agentRole": "specialist", "status": "completed", "terminationReason": "final_answer", "toolCallCount": 1, "durationMs": 2040, "createdAt": "2026-03-25T14:30:00.000Z" }]Get tool calls for a specific execution:
curl -s "https://api.ohallo.eu/api/tool-calls?executionId=8a1b2c3d-4e5f-4061-8273-90abcdef1234" \ -H "Authorization: Bearer sf_live_v1_your_key_here" | jq .Response (trimmed to the main fields):
[ { "id": "5f6e7d8c-9b0a-4132-8546-a1b2c3d4e5f6", "executionId": "8a1b2c3d-4e5f-4061-8273-90abcdef1234", "toolName": "get_order", "input": { "orderNumber": "ORD-48291" }, "output": { "orderNumber": "ORD-48291", "status": "shipped" }, "error": null, "policyDecision": "allow", "durationMs": 245, "createdAt": "2026-03-25T14:30:01.000Z" }]This returns every tool call in the execution in the order it was made, with its input, output, any error, and timing. A populated error means the MCP server returned an error or timed out. Pass conversationId instead of executionId to get the tool calls from every execution in the conversation. A key restricted to a brand or a workspace lists tool calls by conversationId, for a conversation its restriction reaches, and is answered 403 on the executionId form.
Common issues
Section titled “Common issues”Tool not found
Section titled “Tool not found”Symptom: The agent reports it cannot find the tool, or the tool does not appear when you add the MCP server.
Causes:
- The tool name your server registers does not match what the agent is trying to call. Tool names are case-sensitive.
- The tool was added to your server after oHallo last read
tools/list. oHallo reads it again every five minutes. - The server is not running or is not reachable from oHallo’s network.
- The
/mcpendpoint is not responding totools/list.
Fix: Run tools/list manually and verify the tool name appears exactly as expected. Check that the server URL in oHallo includes the /mcp path.
The endpoint is refused when you save the connection
Section titled “The endpoint is refused when you save the connection”Symptom: Saving the connection fails with “The MCP endpoint has to be a public https address.”
Cause: oHallo calls public host names over https only, on port 443 or 80. An http endpoint, an endpoint with another port, such as :4100, an IP address, or a private host name such as localhost is refused. A public host name that resolves to a private address is refused when oHallo reads its tools/list, and saving answers that the endpoint is unreachable.
Fix: Serve the endpoint on a public host name at the standard HTTPS port, behind a reverse proxy or load balancer if your server listens on another port.
406 Not Acceptable
Section titled “406 Not Acceptable”Symptom: A request returns HTTP 406 with Not Acceptable: Client must accept both application/json and text/event-stream, before any tool runs.
Cause: The Streamable HTTP transport requires the request to accept both media types. MCP client libraries, oHallo’s included, send that header for you, so this surfaces when a request is hand-built. curl sends Accept: */* by default, which the transport rejects.
Fix: Send -H "Accept: application/json, text/event-stream" on every curl request to an MCP endpoint, as the examples above do.
Authentication failure
Section titled “Authentication failure”Symptom: Adding the MCP server in oHallo fails because your server rejects the key, or tool calls return authentication errors.
Causes:
- The
MCP_API_KEYenvironment variable is not set on your server. - The API key registered in oHallo does not match the key your server expects.
Fix: Verify the MCP_API_KEY environment variable is set and matches the key configured in oHallo. Test with curl using the same key. When your server answers a check with 401 or 403, the MCP hub shows the connection as Error at once, and oHallo calls it again after a later check succeeds.
Tool call timeout
Section titled “Tool call timeout”Symptom: Tool calls fail with a timeout error. The agent reports that the tool did not respond.
oHallo gives each call to your tool up to 10 seconds to answer, and the same to open the connection to your server when it has none open. A call still open at its limit is cancelled: oHallo sends notifications/cancelled with the call’s request ID and closes the request, so your server can stop working on it. On a phone call, a tool the assistant calls itself has 3 seconds to answer, a limit oHallo sets, and a read the conversation stops waiting for is cancelled the same way; a tool an agent calls keeps the 10 seconds.
A call an agent makes to a tool that only reads, classified read or read_pii, is sent again when it times out, cannot connect, or gets an HTTP 429, 500, 502, 503 or 504 answer: up to three sends in all, about 2 and then 4 seconds apart, inside the time the agent waits. A call to a tool that changes state is sent once; when it comes again, it comes with the same idempotency key, as Repeated calls and the idempotency key describes.
Causes:
- Your tool handler takes too long to execute.
- Your server is making a slow downstream API call without a timeout.
- Network latency between oHallo and your server is too high.
Fix: Add timeouts to all downstream calls in your tool handlers. If a query is inherently slow, consider caching or pre-computing results. Profile your handler to find the bottleneck.
Calls stop reaching your server
Section titled “Calls stop reaching your server”Symptom: For a short time, calls to your tools are answered as unavailable, or the MCP hub shows the connection as Degraded or Error.
oHallo protects a connection that is failing:
- After three failed calls within a minute (timeouts, dropped connections and HTTP errors), oHallo answers calls to the connection as unavailable for up to 30 seconds, then tries your server again. A result marked
isErroris an answer, and does not count as a failure. - The check every five minutes sets the connection’s status. One or two failed checks in a row show it as Degraded, and oHallo keeps calling it. Three show it as Error, and oHallo calls none of its tools until a check succeeds again.
Fix: Make the server answer tools/list reliably, and keep each call within the timeout.
The assistant says a result cannot be shown
Section titled “The assistant says a result cannot be shown”Symptom: The tool runs, and the assistant reads “This result cannot be shown in this conversation.”
Cause: The tool is declared A customer or A customer’s company, and its answer does not name the verified customer, or their company, as the owner of the record: it names someone else, names no owner (an error without the owner field is one), or is not JSON.
Fix: Return the owner at the declared field in every answer, errors about the customer’s own records included, as Tool Schema describes.
Unexpected tool arguments
Section titled “Unexpected tool arguments”Symptom: Your tool receives arguments it does not expect, or required arguments are missing.
Causes:
- The agent is extracting information from the customer’s message and may interpret values differently than expected. For example, a customer saying “order twelve thirty-four” might be passed as
"1234"or"twelve thirty-four". - Your input schema description is ambiguous, causing the agent to pass the wrong type of data.
Fix: Improve your parameter descriptions with explicit format examples (e.g. "The order number, format ORD-XXXXX, e.g. ORD-48291"). Validate inputs in your handler and return a validation_error whose reason names the expected format.
Empty or unhelpful responses
Section titled “Empty or unhelpful responses”Symptom: The agent’s response to the customer is vague or missing details, even though your tool returns the correct data.
Causes:
- Your tool returns data with unclear field names (e.g.
dt_estinstead ofestimatedDelivery). - Your tool returns too much data, and the agent cannot identify the relevant parts. An answer over 64 KB reaches the reply only through the agent’s account of it (How the answer reaches the reply).
- Your tool returns a flat string instead of structured data.
Fix: Return structured objects with descriptive field names. Include units and context (e.g. { amount: 149.99, currency: 'EUR' } instead of { total: 149.99 }). Keep responses focused on what the tool’s description promises.
Debug checklist
Section titled “Debug checklist”Use this checklist when your MCP server is not working as expected:
- Can you reach the server? Curl the
/mcpendpoint from a machine that can reach it, at the public address you registered. Check firewall rules and DNS. - Does tools/list return your tools? If not, the server is not started or the tools are not registered.
- Does authentication work? Test with the correct key, a wrong key, and no key.
- Does tools/call return the expected data? Test with known good inputs.
- Does error handling work? Test with invalid inputs and verify you get structured errors, not stack traces.
- Is the server fast enough? Tool calls must answer within 10 seconds, and within 3 seconds when the assistant calls them itself on a phone call. Add
console.timearound slow operations. - Are descriptions specific enough? Vague descriptions leave your tools unused or called with the wrong arguments.
- Is the response structured? Flat strings are harder for agents to use than structured objects.
- Is each tool classified and declared? A tool without a risk classification is never called, and an agent calls a tool only once its Customer records declaration is saved.
Next steps
Section titled “Next steps”- Build an MCP Server: if you have not built your first server yet
- Tool Schema: improve your input schemas and return values
- Building a Custom Agent: create an agent that uses your tools