Most MCP explainers stop at the protocol. This one shows an actual session: three JSON-RPC calls against a Tixl hub, with the real responses. You'll see what an AI agent gets, and just as important, what it never sees.
New to MCP? Start with the overview. This post assumes you know that an agent calls tools/list to discover tools and tools/call to run one.
We use Hacker News search as the example because anyone can reproduce it: no login, public data. One honest caveat: HN happens to have a public search API behind it. For a legacy portal with no API, the only thing that changes is the capture step, where someone records the workflow once. Nothing the agent sees is different.
The transcript below was captured on September 20, 2026 from a local Tixl hub running the same code as the hosted one. The tool, hn-search, is a single-step integration. Real integrations chain several requests; the agent-facing contract looks the same.
Every MCP client starts with initialize. Clients send it to POST /mcp/messages; the SSE endpoint tells them where to post.
// request
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"demo","version":"0"}}}
// response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": { "listChanged": true } },
"serverInfo": { "name": "tixl", "version": "0.1.0" }
}
}The server names itself and advertises one capability: tools. That's all the agent needs to know before it asks what's available.
Next the client calls tools/list. This is the whole interface the model gets:
{
"name": "hn-search",
"description": "Search Hacker News stories, Ask HN, and Show HN posts by keyword",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (e.g. 'AI agents', 'FastAPI', 'YC W26')"
},
"type": {
"type": "string",
"description": "Post type filter: story, ask_hn, show_hn, job (default: story)",
"enum": ["story", "ask_hn", "show_hn", "job"]
},
"limit": {
"type": "integer",
"description": "Number of results to return (max 20, default 10)"
}
},
"required": ["query"]
}
}Nothing else is exposed. The model decides whether to call the tool, and how, from this schema alone. That makes the description and the constraints do real work: the enum stops the model from inventing a post type, and required tells it which argument it can't skip.
Now the agent runs the tool with business parameters, and only those:
// request
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"hn-search",
"arguments":{"query":"MCP server","type":"show_hn","limit":3}}}The response wraps the result as MCP text content. Here is what result.content[0].text held (two of the three results shown):
{
"stories": [
{
"title": "Show HN: Ghidra MCP Server – 110 tools for AI-assisted reverse engineering",
"url": "https://github.com/bethington/ghidra-mcp",
"points": 298,
"author": "xerzes",
"comments": 68,
"posted_at": 1770187911,
"story_id": "46882389",
"hn_link": "https://news.ycombinator.com/item?id=46882389"
},
{
"title": "Show HN: MCP server for searching and downloading documents from Anna's Archive",
"url": "https://github.com/iosifache/annas-mcp",
"points": 256,
"author": "iosifache",
"comments": 79,
"posted_at": 1752095198,
"story_id": "44514753",
"hn_link": "https://news.ycombinator.com/item?id=44514753"
}
]
}The whole round trip took about 0.4 seconds in our run, including the upstream request. That's one run on a laptop, not a benchmark. The point is the shape: structured fields the model can reason over, not a screenshot to interpret and not HTML to parse. Note that posted_at is a raw Unix timestamp; shaping output like that is part of building an integration.
Compare what crossed the wire with what didn't. The agent saw a name, a description, a schema and a result. It never saw the URLs, headers, extraction rules or the order of requests behind the tool. Those live on the hub, and the agent that executes a step receives one plain HTTP request at a time.
Logins are the other half. When a capture records a session, cookies and CSRF tokens show up as inputs of the request. Those must never become parameters a model fills in. The hub strips them from the schema before the agent sees it. Here is an invented example run through the real cleaning code, a policy lookup where the recording captured a session:
// as recorded // as the agent sees it
"properties": { "properties": {
"policy_number": {...}, "policy_number": {...},
"include_history": {...}, "include_history": {...}
"cookies": {...}, },
"csrf_token": {...} "required": ["policy_number"]
},
"required": ["policy_number",
"cookies", "csrf_token"]At run time the user's own saved session is injected on their machine, at the moment the request is sent. The model never handles it, and the hub doesn't store it. This is also why a tool like this can reach a system that only accepts connections from inside your network: the request is sent from inside it.
Someone records the workflow once, with the Tixl Capture app or the command line recorder. We generate the integration from the recording, test it against the real system, and publish it as a tool. You don't write the definition and you don't maintain it.
Maintenance matters more than the first build. When a step starts failing, the hub tries to repair the definition and keeps the fix only if the repaired step works. A failed login is different: expired credentials can't be fixed by rewriting a request, so those surface as a re-authentication, not a silent patch.
The connection details, including the hosted endpoint and the local option for session-based tools, are in the docs.
Tell us the workflow. We'll capture it, build the tool, and have it available to your agents.
Book a scoping call