MCP Integration Guide
Audience: AI coding agents and developers building a Model Context Protocol
(MCP) server that will be connected to this platform’s AI agents. If you were handed this URL and
asked to “build an MCP integration,” this is your authoritative, platform-specific spec.
This platform lets each tenant connect remote MCP servers so their AI agents gain extra tools
(look up an order, create a ticket, check inventory, …). The quality of that integration depends
entirely on how your server names tools, describes them, shapes their inputs, and returns
results. Follow this guide and your server will plug in cleanly and be used deterministically —
the agent will pick the right tool, with the right arguments, at the right time.
If you are an AI assistant generating the server: read the whole document first, then scaffold from
Section 5 and adapt.
1. What this is
A checklist-driven contract for a remote MCP server that this platform can consume. You expose
tools over MCP; the platform discovers them with tools/list, lets the tenant enable the ones they
want, and calls them with tools/call while an agent is conversing with an end user.
You do not need to know anything about this platform’s internals. You only need to:
- Serve MCP over a public HTTPS endpoint.
- Name and describe each tool well.
- Provide a strict input schema for each tool.
- Mark which tools read vs. which tools mutate.
- Return small, clean, actionable results.
2. Platform requirements
Hard requirements — a server that violates these cannot be connected:
- HTTPS only. The endpoint URL must be
https://…. Plain HTTP is rejected. - Transport: Streamable HTTP (recommended) or SSE. Use the official MCP server SDK so you get a
compliant transport for free. - Public host. For SSRF safety the platform refuses private, loopback, and link-local addresses
(e.g.localhost,127.0.0.1,10.x,192.168.x,169.254.x,::1). Deploy somewhere with a
public hostname. - Implement
tools/listandtools/call.tools/listmust return every tool with a
name, adescription, and aninputSchema.tools/callexecutes one tool. - Authentication (pick one): none ·
Authorization: Bearer <token>· API key in a custom header ·
arbitrary custom header(s) · OAuth 2.0. Credentials are entered by the tenant at connect time and
stored encrypted; your server just has to require them. - Keep payloads compact. The platform records invocations for auditing and truncates request and
response bodies at ~32 KB. Large blobs are wasteful and get cut off — return references, not dumps.
Tool name length & namespacing
The platform namespaces your tool as mcp_<server>_<tool> and truncates the whole thing to 64
characters (provider limit). Keep tool names short and make them distinct in their first ~20
characters so they don’t collide after truncation.
3. Designing tools for deterministic use
This is the part that determines whether the agent uses your tools correctly. The agent decides
which tool to call and how almost entirely from the name, the description, and the
inputSchema you publish. Treat them as the API contract with the model.
3.1 Naming
- Lowercase
snake_case, verb-first, specific:get_order_status,create_support_ticket,
search_inventory. - Avoid vague names (
run,do,handle,query) and avoid two tools whose names only differ
after character ~20 (see truncation note above).
3.2 Descriptions
- One or two sentences of action-oriented prose: “Looks up the current status and ETA of an
order by its ID.” - Describe what it does and when it’s useful, in plain language. The model infers applicability
from this text. - Do not write procedural conditions like “only call this when the user is angry” or “call
this after calling X” — the model does not reliably follow embedded control flow, and it makes the
tool brittle. Describe capability, not orchestration. - Don’t restate the schema in prose; the schema already carries parameter detail.
3.3 Input schema (JSON Schema)
- Always provide
inputSchemawithtype: "object",properties, and an explicitrequiredarray. - Give every property a
description. Put units, formats, and defaults in that description
(“ISO 8601 date”, “defaults to 20”, “3-letter currency code”). - Use
enumfor closed sets so the model can’t invent values. - Prefer a few well-described parameters over one free-form
querystring. - Make truly-required things
required; make everything else optional with a sane server-side
default. Missing-but-required arguments cause failed calls and frustrated agents.
3.4 Read vs. write — set the hints
The platform default-denies destructive tools: on sync it auto-disables any tool whose name
matches a danger heuristic (delete | remove | drop | destroy | purge | truncate | wipe | revoke | cancel | terminate | uninstall | reset | deactivate) so a human must opt in. You control this
explicitly with annotations:
- Add
annotations.readOnlyHint: trueto every tool that only reads. These can be enabled freely. - Add
annotations.destructiveHint: trueto every tool that mutates or deletes. These arrive
disabled; the tenant turns them on deliberately.
Setting these correctly means safe tools are available immediately and dangerous ones are gated —
exactly the behavior you want.
3.5 Result shape
- Return only what the agent needs to answer the user. Strip internal metadata — similarity
scores, raw pagination cursors, opaque database IDs, debug fields. - Prefer short, human-readable strings the model can quote back. Round numbers; format dates.
- Keep results small (remember the ~32 KB audit truncation). For big datasets, return the top few
items plus a count, not everything.
3.6 Errors
- Return clear, actionable error messages: “order_id not found”, “date must be in the future”.
The model relays these to the user or self-corrects. - Never put secrets, tokens, or credentials in error text. The platform redacts known secret
values from audit logs as a backstop, but do not rely on it — don’t emit them in the first place.
3.7 Latency & idempotency
- Respond quickly. Tool calls happen inside a live conversation; slow tools degrade the experience
and can trigger upstream retries. - Reads must be side-effect-free. Make writes idempotent where you can (safe to retry).
4. Auth & security checklist
- Serve over HTTPS with a valid certificate.
- Require authentication for anything non-public; follow least privilege for the credential the
tenant will provision. - Support credential rotation — the tenant can re-enter credentials; don’t hard-pin one secret.
- Validate and sanitize all tool inputs on your side. Treat tool arguments as untrusted.
- Never log or return secrets. Scope each token to only the tools/data it needs.
- The tenant’s credential is stored encrypted at rest on this platform and sent to your server
only on each call — your job is to verify it and fail closed if it’s missing or invalid.
4.1 Private artifacts from chat media
When an end user sends an image, audio, document, or video and the tenant enables the agent skill
private_storage, the platform stores the file in a private bucket and calls your MCP tool with
short-lived signed references, not inline blobs.
Declare the media parameter your tool needs in inputSchema. The platform fills that declared
argument when a matching private artifact is available, and also includes _context.artifacts as
trusted metadata for correlation/backward compatibility. Do not rely on _context.artifacts as
the primary payload contract.
For example, if your tool declares an image URL:
{
"type": "object",
"properties": {
"imageUrl": {
"type": "string",
"format": "uri",
"description": "Short-lived signed URL for the image to inspect."
}
},
"required": ["imageUrl"]
}
The platform calls tools/call with arguments like:
{
"imageUrl": "https://storage.example/signed-url",
"_context": {
"source": "WHATSAPP",
"externalId": "wamid.example",
"artifacts": [
{
"id": "artifact_abc123",
"kind": "document",
"mimeType": "application/pdf",
"filename": "quote.pdf",
"sizeBytes": 18422,
"sha256": "64-hex-character-checksum",
"url": "https://storage.example/signed-url",
"expiresAt": "2026-07-03T18:10:00.000Z",
"delivery": "signed_url"
}
]
}
}
Supported private artifact argument shapes:
- Kind-specific URL fields such as
imageUrl,audioUrl,documentUrl, orvideoUrl
(camelCase or snake_case) withtype: "string"andformat: "uri". - A descriptor object field such as
artifact,file,media, orattachment. - An array field such as
artifacts,files,media, orattachmentswhen your tool can
receive multiple descriptors.
Integration rules:
- Do not ask the agent to paste base64 into tool arguments. Fetch the
urlbeforeexpiresAt. - Treat the signed URL as a secret. Do not log it, store it permanently, or return it to the user.
- Validate
kind,mimeType,sizeBytes, andsha256before persisting the file in your system. - If your tool schema is strict, allow the media parameter you declare plus an optional
_context
object. The platform injects_contextserver-side, so client-provided spoofed context is
overwritten. - Return a concise result after processing the artifact, for example: “invoice saved as INV-123”.
5. Minimal MCP server (copy-paste)
A minimal TypeScript server using @modelcontextprotocol/sdk with one read-only tool and one
destructive tool, each with a strict schema and the correct annotation. Clone, adapt the handlers
to your backend, deploy behind HTTPS.
// server.ts — npm i @modelcontextprotocol/sdk zod
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'
const server = new McpServer({ name: 'acme-orders', version: '1.0.0' })
// READ — safe, enabled immediately.
server.registerTool(
'get_order_status',
{
title: 'Get order status',
description: 'Looks up the current status and ETA of an order by its ID.',
inputSchema: { order_id: z.string().describe('The order ID, e.g. "ORD-10293".') },
annotations: { readOnlyHint: true },
},
async ({ order_id }) => {
const order = await lookupOrder(order_id) // your backend
if (!order) return { content: [{ type: 'text', text: 'order_id not found' }], isError: true }
return {
content: [{ type: 'text', text: `Order ${order_id}: ${order.status}, ETA ${order.eta}.` }],
}
},
)
// WRITE — destructive, arrives DISABLED; tenant opts in.
server.registerTool(
'cancel_order',
{
title: 'Cancel order',
description: 'Cancels an order that has not yet shipped.',
inputSchema: {
order_id: z.string().describe('The order ID to cancel.'),
reason: z.string().optional().describe('Optional cancellation reason.'),
},
annotations: { destructiveHint: true },
},
async ({ order_id, reason }) => {
const res = await cancelOrder(order_id, reason) // your backend
return { content: [{ type: 'text', text: res.ok ? `Order ${order_id} cancelled.` : res.error }] }
},
)
// Streamable HTTP transport over HTTPS (terminate TLS at your host / proxy).
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
await server.connect(transport)
// Wire `transport` to your HTTP framework's POST handler per the SDK docs.
Notes:
registerToolpublishesname,description, andinputSchemaviatools/listautomatically.- The Zod
.describe(...)calls become the per-propertydescriptions the agent reads. - Return errors as text with
isError: true— clear and actionable, never leaking internals.
6. Self-check before you share your URL
Run through this before handing the endpoint to a tenant:
- [ ] Endpoint is
https://on a public host;tools/listreturns successfully. - [ ] Every tool has a clear, action-oriented
description. - [ ] Every tool has an
inputSchemawith an explicitrequiredarray and adescriptionon each
property. - [ ] Read tools set
annotations.readOnlyHint: true; write/delete tools set
annotations.destructiveHint: true. - [ ] Tool names are short, distinct in their first ~20 chars, and
snake_case. - [ ] Results are small and free of internal metadata; errors are actionable and secret-free.
- [ ] Authentication is required where appropriate and credentials are validated server-side.
7. How this platform consumes your tools
So you know what happens after you connect:
- The tenant adds your server (URL + credentials) and the platform calls
tools/list. - Tools are stored as an allowlist: enabled tools can be used; destructive tools start
disabled until a human enables them. - Each AI agent then selects which of the enabled tools it may use. A tool is callable only if
it is (a) enabled on the tenant allowlist and (b) selected on the agent and © the
connection is active. - During a conversation the agent calls
tools/callwith arguments matching yourinputSchema.
If it ever names a tool that isn’t available, the call fails fast — it cannot invent tools. - Every invocation is audited (arguments, result, duration, success) with secrets redacted and
bodies truncated at ~32 KB.
That’s the whole contract. Build to this guide and your integration will be picked up, gated
safely, and used deterministically.