Marcelo Paiva
Saturation
Theme
A bearded man in a turban and jacket holds a hand to his face amid swirling curves.Meant to show: A recruiter hiring on site for a restaurant group, moving candidates along by asking rather than clicking, with exactly the access his job gives him.

Letting an agent do the recruiter's job, with the recruiter's permissions

A proof of concept that made an HR platform operable by an AI agent: an MCP server of over 160 typed tools on the existing REST API, and a streaming chat client that answers in real product components rather than paragraphs. Working on day one; the whole recruiting lifecycle in nine working days.

Role
Head of UX and accessibility — led the team on server, client and security model
Year
2026
Built with
TypeScript, MCP, Vercel AI SDK, Next.js, Zod, Generative UI, WCAG 2.2 AA

A recruiter's week is mostly small, precise actions in a large product. Move these four people to interview. Clone that requisition for the Austin office. Text the candidate to say we got their application. Each is a few clicks, and the clicks are spread across a dozen screens.

The question was whether an agent could take those actions for a person, in plain language, without becoming a new way around everything the platform already enforces.

Nothing new between the agent and the data

The server does not talk to the database. It calls the platform's existing REST API, the same one the product uses, with the signed-in person's own session. So validation, business rules, permission checks and tenant isolation all come along without being rebuilt. The agent can do exactly what that person can do, and nothing more.

That one decision made everything after it cheap. A rule the product already enforces does not have to be enforced twice, and cannot drift when it changes.

How fast it went

The first day ended with a working server, the core recruiting tools, text messaging, interview scheduling and a chat client talking to all of it. By the ninth working day it covered recruiting end to end, through to sending an offer letter for signature, with 107 tests passing. The other HR domains came after.

The offer letters show how. Their API had no documentation, so we found it by watching the product's own traffic, probed about eighty guesses at the right routes, and read the offer states out of the legacy front end's source. Eight tools and eleven tests shipped the same day.

That pace is the point, more than any one tool. Nobody wrote a specification and waited for a quarter. We built the thing, used it, and let what broke decide the next day's work. A diary in the repository records each of those days, and the release notes are generated from it.

The tools

It speaks the Model Context Protocol, so the tools do not know which model is calling them. The same server works from Claude Code, Cursor or the chat client below, and composes with other servers: "post the pipeline to the hiring channel" is this server and a Slack one, taking turns.

There are over 160 tools. Recruiting is the deepest, with 65, running from requisitions and candidate search through interview scheduling, scorecards, background checks and offer letters. Performance, goals, onboarding, core HR and one-on-ones follow. Every input is a Zod schema. The description on each tool is written for the model, because that is who reads it. It says when to use the tool, what to call first and what the person must confirm.

The limits live in the schemas, where a model cannot argue with them. Bulk moves and grades take at most 25 candidates. Write and destructive tools say so in their descriptions, and cancelling an offer says it cannot be undone.

Two things are designed and not yet built, and nothing ships to customers without them: a confirmation gate the server enforces rather than one the model is asked to honor, and an audit log of every tool call. A description asking the model to confirm first is a convention, not a control.

Answers made of product, not prose

The chat client is Next.js on the Vercel AI SDK, and it streams. What matters is what it streams. A question about a requisition does not come back as a paragraph describing one. It comes back as the requisition card, the pipeline funnel or the data grid the product would show, rendered from a spec the model writes. Every answer ends with the next likely actions as buttons.

Two engineering choices kept it usable:

  • Only the tools a message needs. Sending all 160 tool definitions with every message would spend most of the context before the conversation started. A router matches the message to tool groups, so "hello" sends two tools and "move these candidates" sends the handful that can.
  • Checks the model never has to remember. When an answer is about to show a requisition, the client looks up in parallel whether its hiring manager or recruiter has left the company. The warning goes into the stream, so it shows up whether or not the model thought to ask.

The client was held to WCAG 2.2 AA from the second day: skip links, target sizes, contrast in both themes, and focus management for content that streams in. A full audit closed the last fifteen issues.

Tested like a product

About 140 tests cover the HTTP client, the helpers, the tools, and the client's tool selection and component catalog. A proof of concept that nobody can trust is just a demo.

What it showed

The platform did not need a new AI layer. It needed its existing API described well enough for a model to use, and a permission model that was already correct. The work that decides whether this is safe is the unglamorous part: what a tool refuses, what it caps, and what a person has to say yes to.

About the drawing

The drawing for this piece was generated, not chosen. Hashing the piece’s name fixes one point on the color wheel; the other two follow from it. The same name always gives the same three colors.

#5fae6a#2d338f#c14754

How this drawing was made

The line under the drawing marked “Meant to show” is written by a person, not the model. A generated drawing keeps what is large and loses what is small, so the detail that decides who someone is can go missing, and a picture only reaches people who can see it. Where the drawing and that line disagree, the line is the intent.