Integration · AI operations
How to expose an internal system as an MCP tool
An agent has your CRM credentials and a natural language instruction, and nobody can say afterwards what it actually did. Here is how a governed MCP tool actually works, the prompts that build it, and what running it demands.
Built with Tray Headless
- System Coding assistant
- Step MCP tool
- Step Resolve caller identity
- Step Scoped action
- System Salesforce
Identity is resolved per call instead of baked into the server, so an agent acts as the person who asked instead of as a service account.
The short answer
What is an MCP tool?
Exposing a system as an MCP tool is four parts: a tool scoped to a job instead of wrapping an API, identity resolved per call so the agent acts as the person who asked, writes made idempotent because a model will retry, and every invocation logged with its arguments and result. The part teams get wrong is scope. Wrapping a whole API as one tool gives an agent every capability the credential has, and the blast radius of a misunderstood instruction becomes the entire system.
What matters here
- Scope the tool to a job, not an API. One tool per intent limits what a misread instruction can do.
- Resolve identity per call. An agent acting as a shared service account is an audit trail that names nobody.
- Make every write idempotent. Models retry, and a retried create is a duplicate record somebody has to clean up.
- Return errors a model can act on. "Failed" produces a retry loop; "missing required field: close date" produces a fix.
- Log arguments and results, not just that the tool was called. Reconstructing what an agent did is the whole point.
Who this is for
You run platform engineering or AI operations. Teams want agents that can act on internal systems, and the current options are a broad credential or nothing.
How it works in practice
The path from an agent deciding to act to the action being safe to allow.
- 1
The tool is defined as one job
Create an opportunity, not access Salesforce. Scope is the primary control.
- 2
The caller identity is resolved on every call
The agent acts as the person who asked, with their permissions, not as a service account.
- 3
Arguments are validated against a typed schema
Before anything is attempted, so a malformed call fails cleanly instead of half-executing.
- 4
Writes carry an idempotency key
Derived from the arguments, so a model retrying does not create a second record.
- 5
Errors come back describing the fix
Structured and specific, because a model can act on a reason and cannot act on failure.
- 6
Every invocation is logged in full
Who, what tool, what arguments, what result. That log is the reason this is allowed at all.
What a governed MCP tool is made of
Four parts, though the first is the one that decides how bad a mistake can be.
Job-scoped design
One tool per intent, with the narrowest arguments that do the job. Wrapping an API hands an agent every capability the credential has.
Per-call identity
The caller resolved and their permissions applied on each invocation, so the audit trail names a person rather than a service account.
Idempotent writes
A key derived from the arguments. Models retry on timeouts and ambiguity, and a retried create is a duplicate.
Full invocation logging
Arguments, result, latency and identity. Without it nobody can answer what the agent did, which is the first question asked.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up and scope the tool to a job
Scope is the primary control, and it is decided before any code.
Headless skills
build-workflowUse build-workflow. The systems in play are Salesforce, or whatever we run in those seats. Do not build a tool that wraps the API. Build one tool per job, and help me name them: create_opportunity, with account, name, amount, close date and stage update_opportunity_stage, with an id and a stage find_account, with a name or domain, read only Each takes the narrowest arguments that do the job and nothing more. A single salesforce_query tool gives an agent everything the credential can reach, and the blast radius of one misread instruction becomes the whole CRM. Narrow tools are the control, and everything else here is secondary to that.
Naming the tools is the design. If a tool name needs the word "and" in it, it is two tools.
- 2
Resolve identity on every call
An agent acting as a service account is an audit trail that names nobody.
Headless skills
build-workflowUse build-workflow. Every invocation resolves the calling identity and acts with that person's permissions. Take the caller from the authenticated session rather than from an argument, because an argument can be supplied by the model and an authenticated session cannot. Apply their real permissions. If the person cannot edit that opportunity in the CRM, the agent acting on their behalf cannot either. An agent should never be able to do more than the person asking. Never use a shared service account with broad rights. It is easier and it produces an audit log that says a robot did it, which is exactly the answer nobody can accept.
- 3
Type the arguments, and make writes idempotent
Because models retry, and a retried create is a duplicate.
Headless skills
tray-patternsDefine a strict schema per tool: required fields, types, enums for anything the target system constrains, and sensible bounds. Validate before attempting anything, so a malformed call fails cleanly rather than half-executing and leaving a partial record. Then make every write idempotent. Derive a key from the arguments and the caller, check it before writing, and return the existing result if the same call arrives twice. Models retry on timeouts, on ambiguous responses, and sometimes because they simply decide to. Without idempotency that is a duplicate opportunity, and somebody finds it at the end of the quarter.
- 4
Return errors a model can act on
"Failed" produces a retry loop. A reason produces a fix.
Headless skills
tray-gotchastray-patternsUse tray-gotchas, then design the error responses deliberately. Return structured errors that name the problem and the fix: missing required field and which one, invalid enum value and the accepted list, permission denied and on what, record not found and what was searched for. Never return a bare failure. A model given "failed" retries the identical call, often several times, and then reports success to the user because the last attempt returned something. Distinguish retryable from terminal. A rate limit is retryable. A permission denial is not, and a model retrying it looks exactly like an attack in your logs.
- 5
Log everything, and set a budget
The log is the reason this is allowed at all.
Headless skills
tray-patternsLog every invocation: who called, which tool, the full arguments, the result, the latency, and whether it succeeded. That log is the entire reason exposing internal systems to an agent is acceptable. Without it the honest answer to what did the agent do is nobody knows. Then add limits: a per-caller rate limit, a daily budget for expensive tools, and an approval step for anything above a threshold, such as creating an opportunity above a value or writing to more than a handful of records at once. Report per tool: invocations, error rate by type, retries after an error, and calls that hit a limit. A tool with a high retry rate usually has a bad error message, not a bad caller.
- 6
Check it end to end, then hand the scopes to platform
Because which tools exist is a governance decision.
Run the per-step schema checks and the whole-workflow audit before this touches production. Test with a deliberately confused instruction and confirm the tool refuses, not improvises. Then open the same workflow in Tray Build so platform engineering can add tools, adjust scopes and change approval thresholds in the visual canvas. Which tools exist and what they may do is a governance decision, and it should be visible to somebody who does not read code.
What it connects to
The agent calls the tool, the tool acts on a system, and the identity comes from neither.
Salesforce
The system the tool acts on, reached through narrowly scoped operations instead of a general query interface.
Reads and writes
Okta
Resolve the calling identity and their group membership, which is what the downstream permission check is made against.
Reads
Snowflake
Land every invocation with its arguments and result, so agent behaviour is analysable rather than anecdotal.
Writes
Slack
Request approval for an action above a threshold, from the person who owns the outcome.
Writes
Datadog
Emit latency and error rate per tool, because a slow tool changes agent behaviour in ways nobody predicts.
Writes
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against Microsoft Dynamics 365, Google BigQuery, Microsoft Teams, Azure Active Directory, HubSpot or Databricks.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
This lets a model act on production systems. Every control here exists because of that.
It runs on the platform, not on your laptop
Tool invocations arrive whenever an agent decides, at any hour, and execute on the same engine with retries and a complete history.
Every call names a person
Identity resolved per invocation, never a shared service account. An audit log that says a robot did it is not an audit log.
Credentials live in the workspace, never in the repo
The tool holds no secret. It names a workspace authentication, which is what lets a credential be rotated without redeploying anything an agent depends on.
Platform owns the tool catalogue
Which tools exist, their scopes and the approval thresholds open in Tray Build, so governance is visible to people who do not read code.
Test with a confused instruction
Before production, give it an ambiguous or contradictory request and confirm it refuses rather than improvising. That is the failure mode that matters.
Questions people ask
Why scope tools to a job instead of wrapping the API?
Because one general query tool gives an agent every capability the credential has. Narrow tools mean a misunderstood instruction can only do the narrow thing, and scope is the primary control.
Why resolve identity per call?
Because an agent using a shared service account produces an audit log that names nobody. Acting as the person who asked also means the agent can never do more than they could themselves.
Why must writes be idempotent?
Because models retry, on timeouts, on ambiguous responses, and sometimes for no visible reason. Without an idempotency key that retry is a duplicate record found at the end of the quarter.
What makes a good error response?
One that names the problem and the fix. A bare failure sends a model into a retry loop and it often then reports success, because the final attempt returned something.
What has to be logged?
Caller, tool, full arguments, result and latency on every invocation. That log is the entire reason exposing internal systems to an agent is defensible.
Further reading
Background on the same subject, for the case rather than the build.
Related guides
AI operations
How to build human approval for agent actions
Decide what needs approving by blast radius, show the approver what will happen, expire cleanly, and keep the record. The Headless prompts that build it.
AI operations
How to build LLM cost and usage tracking
Attribute spend to a team and a feature, alert on the rate rather than the total, and catch the loop before the invoice does. The prompts that build it.
AI operations
How to build a knowledge base to vector sync
Chunk on structure, carry permissions into the index, delete on delete, and re-embed only what changed. The Headless prompts that build it.
Last reviewed September 2026.