We launched the Rippling MCP server today, a new capability that allows any AI tool to take action in Rippling. Beyond just fetching records, it also enables agents to perform complex tasks and execute code to get those tasks done.
Building a poorly designed MCP server is easy. Just ask a coding agent to read your API docs, and you’ll have one running in minutes. And many companies did exactly that: they wrapped their existing APIs as tools and announced their new MCP server. But when we tested this approach, we quickly realized that the architecture was all wrong for the kind of work that people actually want their agents to perform on their behalf.
APIs were designed and built for human developers. A good API exposes hundreds of endpoints and thousands of fields and parameters that are broad on purpose so a developer can assess what is available to then write code that only uses what they need. The cost of all that surface area is paid once at build time.
Agents are different. They figure out how to solve a problem each time they go to solve it, like groundhog day. They do that by reading instructions and tool definitions. So when APIs are wrapped as MCP tools, the agent is drowned in context every time it uses the MCP. The agent can’t just request what it needs. For example, if asked the simple question “is anyone on my team out of the office today,” it has to muddle through a lot of extraneous data in search of the specific endpoints it needs to answer the question: managers, employees, leave types, leave requests, and more. All this to ultimately just say, “No, not today.”
That is an enormous amount of work for a one-word answer, and the agent pays for it on every request. You don’t realize how expensive poor MCP design is until your AI bill arrives at the end of the month.
We built our MCP to avoid this problem. Rather than force agents to speak in APIs built for human software developers, we let them communicate in their native language: code. The same task that once drowned an agent in context now costs 98% fewer tokens.
Our first use case
For our own Rippling employees, one of the most popular apps internally is Product Launch Tracker ( the “PLT”): a Rippling Custom App built to let the R&D department track and communicate product roadmap and timeline updates. Keeping PLT up-to-date is crucial to our company’s ability to execute across teams. The request to make the PLT available in our employees’ AI tools became one of the top feature requests across all departments, so we used it as the first use case to design the Rippling MCP.
Consider this prompt:
“I’m preparing for a launch review meeting today. Which product launches owned by my team need attention in the next 30 days? Consider launch date, confidence, development stage, and whether the owner is out today. Rank them by urgency.”
To answer this question, an agent must:
Find out who I am.
Find my direct reports.
Find the product launches each report owns that are due within 30 days.
Retrieve each launch’s target date, confidence, development stage, and last update.
Check whether each owner is out today.
Join the results, flag urgency, and rank the launches.
Designs we considered
We first had to decide what the agent should see. We considered three designs:
One MCP tool per API. APIs already work for human developers, so exposing each as a tool is the simplest design. But every tool definition consumes session context. Dependent calls require separate model turns, and each wrong call wastes one. Progressive disclosure can defer tool definitions, but it does not shrink response payloads. One large JSON response can consume more context than the entire tool catalog. Rippling has 238 APIs spanning the platform including HR, IT, Time, and Payroll. 58 new endpoints were added so far in 2026 alone. Given our scale of APIs, we ruled this option out.
Workflows exposed as MCP tools. A workflow bundles several APIs into a single tool, reducing round trips for a given task. For example, one workflow could fetch a team’s vacation balances given its manager. But when no workflow matches the request, the agent must combine workflows or fail. Supporting more use cases means adding more tools, eventually recreating the tool-bloat problem. At Rippling’s breadth, this makes it nearly impossible to predict every workflow in advance. Because of this, we ruled out workflows as the primary interface.
Code Mode. Cloudflare popularized Code Mode in September 2025 and introduced a server-side version in February 2026. In Cloudflare’s implementation, the agent uses a search tool to find available API functions, and an execute tool to run a program that calls them. This was the obvious fit for Rippling. Our products share a data and permissions model, so useful workflows often cross product boundaries. Code Mode lets the agent compose those capabilities in ways we could not predict in advance while keeping intermediate API responses out of the model’s context.
How we implemented Code Mode
Today, the Rippling MCP exposes one code tool. Its description lists the typed codemode.* functions the connected user is authorized to call. The model writes a JavaScript program against those functions, and the entire program runs inside a fresh Cloudflare Dynamic Worker isolate, where it chains calls and processes intermediate data. This keeps the number of MCP tool calls fixed. More importantly, the intermediate responses remain inside the isolate during execution which means only the program’s result returns to the model.
While Cloudflare’s Code Mode implementation supports a search and execute tool, we found that agents performed better using just one code tool with a MCP server supporting 31 functions. A search tool becomes valuable as the function catalog grows and we will add one when the catalog is large enough to warrant it.
Using the same example prompt from above, the following code is generated by GPT-5.6 Sol on Medium:
“I’m preparing for a launch review meeting today. Which product launches owned by my team need attention in the next 30 days? Consider launch date, confidence, development stage, and whether the owner is out today. Rank them by urgency.”
It hands back only the answer:
Code Mode lets the agent customize what the Rippling MCP returns. The first benefit we see from the response is the ability to create derived and calculated fields. Fields such as “team_size” and “upcoming_launches” and “owners_out_today” are all fields the agent invented through writing code.
The second benefit is efficiency. The MCP server returned a payload that equates to 204 tokens (based on OpenAI’s tiktoken library using the o200k_base encoding). To get the same answer using the one tool per API design, it costs 11,071 tokens. Code mode performs the same work using 98% fewer tokens.
|
Model turns (call + result) | 22 | 1 |
Function executions | 22 | 22 |
Records returned to the model | 57 | 3 |
Field value pairs returned to the model | 776 | 13 |
Payload returned to the model (bytes) | 32,207 | 727 |
Payload returned to the model (tokens) | 11,071 | 204 |
A product for agents built by agents
API endpoints and fields should have descriptions, but many don’t. While developers can work around it, agents cannot with MCPs. The MCP server instructions and tool definitions are not optional docs, they are the product. And they are what make a great MCP server.
We started out writing the instructions and tool definitions by tying it back to value proposition statements that could be understood by a human developer. But having a human guess the best way to teach agents when to use the lookup_direct_reports function quickly did not work. The MCP server is a product built for agents to consume, so we trusted agents to write the server instructions and tool definitions.
We treated our external text surfaces like code and built a mocked end-to-end evaluation harness. We initially created a golden data set of 50+ test cases. Each case pairs a user prompt with a reference answer, stubbed API responses, and the expected code to execute.
For example:
Prompt: What is the average tenure of Michael Mitchell's team?
Expected answer: Michael Mitchell has one direct report; average team tenure is 7.25 years (Tammy Smith, start date 2019-04-01).
Expected code:
Based on the AI clients our customers most commonly use, we created a text matrix by client, model, and reasoning setting.
|
Claude | Haiku 4.5 | medium |
Claude | Haiku 4.5 | high |
Claude | Sonnet 4.6 | medium |
Claude | Sonnet 4.6 | high |
Claude | Opus 4.8 | medium |
Claude | Opus 4.8 | high |
Codex | GPT 5.4 | medium |
Codex | GPT 5.4 | high |
Codex | GPT 5.4 Mini | medium |
Codex | GPT 5.4 Mini | high |
Codex | GPT 5.5 | medium |
Codex | GPT 5.5 | high |
Cursor | Composer 2.5 | n/a |
We ran every case in a clean context. The agent received nothing else except its baseline system prompt, our MCP server instructions, and our tool description. Because the API responses were stubbed, every agent faced the same scenario with the same data. We measured how well the agent used our single code tool with mock servers.
We built a judge to score each run. For every case, the judge used the reference answer, the stubbed responses the agent should have used, and the agent’s full transcript to assign a score. Each agent’s score was out of 100 across four categories:
Function selection: Did the agent choose the right codemode.* functions?
Function path: Did the agent choose the most optimal path to sequence function calls?
Invocation correctness: Did the generated JavaScript program run and pass the right values to the right parameters?
Final answer: Was the final result actually correct?
The performance results were passed to an optimizer LLM. It triaged the failures, proposed edits to the text, reran evals, and measured what changes improved performance. We ran this in a loop. Changes that lifted scores across all harness, model, and reasoning configurations we kept. Changes that only helped one model, we discarded.
As our performance scores began to improve, our eval harness exposed operational failures. The agent would write a semantically correct program, but the program would exceed the sandbox’s 60-second execution budget and timeout. For example, one eval prompted the agent to save five meeting action items into a new custom object. The program it wrote was correct (create the object, read its schema, write five custom records), but it chained every call in sequence:
While every call was valid, the plan ran past the 60 second timeout. Nothing told the agent how much time each function costs, so it guessed wrong. So we put p95 latency in every function description and taught the agent the math: sequential adds, parallel costs only the slowest call, 60s is the ceiling. Now it runs the five writes in parallel: 47.5 seconds collapses to 9.5 seconds, and splits across calls when a job won't fit. After we exposed p95 latency per function, production sandbox timeouts dropped by 70%.
We also kept seeing poor performance using Claude. Our eval harness ultimately surfaced something no wording could fix: clients don't read the text the same way. Notably, Claude Code only looked at the first 2,048 characters of our tool description when deciding whether to use the tool at all. Our full description ran past the 2KB limit. The routing rules that made programs correct were present and accurate, but completely invisible at the moment Claude decided whether Rippling was relevant. Its evals kept failing.
We did not replace the full description with shorthand. Instead, using our eval harness we were able to generate a compressed 2,000 character action header before the detailed guidance in our code tool. It expresses the most important capabilities, constraints, and state transitions with function names, arrows, and operators rather than prose. A few examples include:
|
Use lookup_me or lookup_person as the starting profile, then follow the returned manager ID with lookup_person until the manager is missing; a missing manager means the chain ends there. | reporting = me → lookup_person til mgr missing |
To request time off, resolve the requester with lookup_me, fetch their leave types and current balance, then call request_time_off with worker_id set to the requester's own id — check the balance first, never pass a literal worker_id. | lookup_me → search_leave_types → lookup_time_off_balance({worker_id:me.id}) → request_time_off({worker_id:me.id}); balance first |
Before creating a draft hire, resolve the department, manager, location, and employment type in the same call; search_work_locations returns locations, not employees, so pass the returned location_id, never a raw location string, and never hardcode IDs. | create_draft_hire: same call resolve dept/manager/location/type; search_work_locations → location_id, no raw location; no hardcoded IDs |
Shipping the action header improved Claude Code performance by 41% while not impacting Codex or Cursor.
Letting agents write code safely
Letting an agent write and run its own code against your company's system of record should give you pause. It is arbitrary code, generated on the fly, touching the same HR, IT, Finance, and people data everything else in Rippling depends on. That is why we designed the model to have room to work, but no authority of its own.
Each program runs inside an isolated Cloudflare Worker with no credentials and no network access. It cannot call out to the internet, read a secret, or touch a Rippling system directly. The only thing it can do is call the specific codemode.* functions we hand it for that request.
Which functions it gets is decided before a single line runs. Rippling reads the connected user's own policy and grants the isolate only the functions that person's role is allowed to call. If that permission set is missing or invalid, nothing runs at all. Being handed a function is not the same as bypassing a check. When the program calls one, the request leaves the isolate to a host that holds the token, and Rippling enforces the same scopes and field-level permissions that govern every other surface.
We also control how hard an agent can push. Because agent traffic is machine-scale and bursty, the server runs at the edge, where rate limiting and abuse control are applied, and every program runs under a hard sixty-second budget.
The result is a simple division of labor: the model proposes the work, and Rippling decides what is allowed. It is the same Object Graph permission model you already trust, now applied to the agent acting on your behalf.
The work ahead
An efficient and effective framework for the Rippling MCP Server lays the foundation for a lot of capability expansion in the future. As we add more functions to our MCP catalog, the same design lets agents take on more of the work that actually runs a company.
You can try the Rippling MCP with your AI tool of choice here: https://www.rippling.com/platform/ai/rippling-mcp
Disclaimer
Rippling and its affiliates do not provide tax, accounting, or legal advice. This material has been prepared for informational purposes only, and is not intended to provide or be relied on for tax, accounting, or legal advice. You should consult your own tax, accounting, and legal advisors before engaging in any related activities or transactions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88async () => { const today = new Date(); const end = new Date(today); end.setUTCDate(end.getUTCDate() + 30); const startDate = today.toISOString().slice(0, 10); const endDate = end.toISOString().slice(0, 10); const me = await codemode.lookup_me(); const team = await codemode.lookup_direct_reports({ manager_id: me.id }); const reports = Array.isArray(team?.direct_reports) ? team.direct_reports : []; const owners = await Promise.all( reports.map(async (report) => { const [launches, absence] = await Promise.all([ codemode.list_custom_records({ object: "product_launch__c", fields: "name,owner_role,target_ga__c," + "level_of_confidence__c,current_development_stage__c", filter: "owner_role == '" + report.id + "' AND target_ga__c >= '" + startDate + "' AND target_ga__c <= '" + endDate + "'" }), codemode.lookup_absence({ worker_id: report.id }) ]); return { owner: report.display_name, out_today: Boolean( absence?.is_absent ?? absence?.is_on_time_off ?? absence?.absent_today ?? absence?.on_leave ?? false ), launches: Array.isArray(launches?.records) ? launches.records : [] }; }) ); const upcoming = owners.flatMap(owner => owner.launches.map(launch => ({ launch: launch.name, owner: owner.owner, target_ga: launch.target_ga__c, confidence: launch.level_of_confidence__c ?? "Unknown", stage: launch.current_development_stage__c ?? "Unknown", owner_out_today: owner.out_today })) ); const attentionNeeded = upcoming .filter(launch => launch.confidence === "Low" || launch.owner_out_today) .map(launch => ({ ...launch, reason: [ launch.confidence === "Low" ? "Low confidence" : null, launch.owner_out_today ? "Owner is out today" : null ].filter(Boolean) })) .sort((a, b) => a.target_ga.localeCompare(b.target_ga) || a.launch.localeCompare(b.launch) ); return { window: `${startDate} to ${endDate}`, team_size: reports.length, upcoming_launches: upcoming.length, owners_out_today: owners.filter(owner => owner.out_today).length, launches_needing_attention: attentionNeeded.length, attention_needed: attentionNeeded }; }