Overview
Connecting an LLM to an API is easy.
Deciding what it must never be allowed to do is the actual engineering work.
The goal was to let coding agents work with three common enterprise surfaces:
- an issue tracker
- a documentation system
- a team messaging platform
The naive approach would expose a generic HTTP client and rely on the prompt to keep the model within bounds. That makes the model responsible for policy, path safety, retry semantics, and API correctness.
The safer approach is the opposite: give the model small tools, then enforce every important boundary in ordinary code.
Capability Design
Each server exposes operations that match user intent rather than raw endpoints:
get_issue update_issue transition_issue
search_pages update_page add_comment
send_message reply_to_message wait_for_reply
Tool inputs are typed and outputs are normalized. The agent should not need to know vendor payload shapes, pagination conventions, or document storage formats.
This reduces both prompt size and failure surface. A model choosing transition_issue(key, "In Review") is safer than a model constructing an arbitrary request against a workflow API.
Writes Use Safelists
Read tools can return curated data. Write tools need a stricter contract.
Update operations accept a flat field map, but only a known set of keys survives validation. Unknown keys fail before the API call. The same rule applies to page updates: title, body, labels, and parent are allowed; arbitrary storage properties are not.
Safelists solve two problems at once:
- A hallucinated field cannot leak into a vendor API.
- A malicious instruction embedded in retrieved content cannot expand the tool’s authority.
The model can choose among permitted actions. It cannot redefine what permission means.
File Tools Are Different
An upload tool can read a local file and send it somewhere else. That is an exfiltration primitive, even when the destination is legitimate.
For that reason, upload tools are not merely disabled by convention. They are absent from the MCP tool registry unless an administrator configures an existing upload root.
When enabled, an upload must pass all of these checks:
- resolve to an absolute path under the configured root
- refer to a regular file
- not be a symbolic link
- remain below a fixed size limit
- use a caller-independent destination API
Download paths receive similar containment and filename sanitization. Vendor-provided filenames are treated as untrusted input; path separators and traversal segments never reach the filesystem.
The critical design choice is registration-time gating. If no safe root exists, the dangerous capability does not exist from the model’s perspective.
Converting Rich Documents Safely
Documentation platforms often store pages as an XHTML-like format with proprietary macros. Returning that directly to an LLM wastes context and makes editing fragile.
The documentation server converts standard content into Markdown:
- headings, lists, tables, links, and images
- inline formatting
- code blocks with language metadata
Unknown macros are preserved as attributed fenced blocks. The agent can edit around them without pretending to understand or regenerate their internal structure.
On write, links and image sources are scheme-checked, code fences become native code macros, and macro content is escaped safely. The conversion is intentionally conservative: preserve what cannot be understood; never silently reinterpret it.
Retry Semantics Depend on the Operation
Retries are not universally safe.
A failed search can usually be repeated. A message send is different: if the server accepted the message but the response was lost, retrying may post it twice.
The messaging server therefore distinguishes:
- explicit rate limiting, where the server confirms the request should be retried after a delay
- connection failures before a request is accepted
- ambiguous service errors, where automatic replay could duplicate a side effect
Only safe categories retry automatically. Rate limits honor the server’s delay. Ambiguous send failures return a structured error and let the caller decide.
Idempotency is not a property of the retry loop. It is a property of the operation being retried.
Normalized Errors
Every server maps vendor failures into a small shared taxonomy:
bad_input
not_found
permission_denied
rate_limited
upstream_error
configuration_error
The agent receives an actionable category and a bounded message, not a full upstream response or stack trace. This makes recovery prompts simpler and prevents accidental exposure of internal request details.
What Worked
- Designing tools around user actions instead of REST endpoints.
- Making upload capability conditional on a hard filesystem boundary.
- Validating paths after resolution and rejecting symlinks.
- Using field safelists for every generic update operation.
- Treating retry policy as part of each tool’s side-effect contract.
- Preserving unknown document macros instead of destructively translating them.
What Was Hard
- Distinguishing a safe retry from a duplicate side effect.
- Round-tripping rich documents without losing vendor-specific structures.
- Providing useful errors without exposing raw upstream responses.
- Keeping tools convenient enough that users did not fall back to broader, unsafe integrations.
Takeaway
An MCP server should not make an agent powerful in the abstract.
It should make a small set of useful actions safe, explicit, and difficult to misuse.