Skip to content

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

  1. System Coding assistant
  2. Step MCP tool
  3. Step Resolve caller identity
  4. Step Scoped action
  5. System Salesforce
Also Audit log

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. 1

    The tool is defined as one job

    Create an opportunity, not access Salesforce. Scope is the primary control.

  2. 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. 3

    Arguments are validated against a typed schema

    Before anything is attempted, so a malformed call fails cleanly instead of half-executing.

  4. 4

    Writes carry an idempotency key

    Derived from the arguments, so a model retrying does not create a second record.

  5. 5

    Errors come back describing the fix

    Structured and specific, because a model can act on a reason and cannot act on failure.

  6. 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. 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-workflow

    Use 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. 2

    Resolve identity on every call

    An agent acting as a service account is an audit trail that names nobody.

    Headless skills build-workflow

    Use 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. 3

    Type the arguments, and make writes idempotent

    Because models retry, and a retried create is a duplicate.

    Headless skills tray-patterns

    Define 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. 4

    Return errors a model can act on

    "Failed" produces a retry loop. A reason produces a fix.

    Headless skills tray-gotchas tray-patterns

    Use 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. 5

    Log everything, and set a budget

    The log is the reason this is allowed at all.

    Headless skills tray-patterns

    Log 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. 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.

Last reviewed September 2026.