# MCP Integration Guide

> **Audience:** AI coding agents and developers building a [Model Context Protocol](https://modelcontextprotocol.io)
> (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](#5-minimal-mcp-server-copy-paste) 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:

1. Serve MCP over a public HTTPS endpoint.
2. Name and describe each tool well.
3. Provide a strict input schema for each tool.
4. Mark which tools read vs. which tools mutate.
5. 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/list` and `tools/call`.** `tools/list` must return every tool with a
  `name`, a `description`, and an `inputSchema`. `tools/call` executes 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 `inputSchema` with `type: "object"`, `properties`, and an explicit `required` array.
- Give **every** property a `description`. Put units, formats, and defaults in that description
  (*"ISO 8601 date"*, *"defaults to 20"*, *"3-letter currency code"*).
- Use `enum` for closed sets so the model can't invent values.
- Prefer a few well-described parameters over one free-form `query` string.
- 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: true` to every tool that only reads. These can be enabled freely.
- Add `annotations.destructiveHint: true` to 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:

```json
{
  "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:

```json
{
  "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`, or `videoUrl`
  (camelCase or snake_case) with `type: "string"` and `format: "uri"`.
- A descriptor object field such as `artifact`, `file`, `media`, or `attachment`.
- An array field such as `artifacts`, `files`, `media`, or `attachments` when your tool can
  receive multiple descriptors.

Integration rules:

- Do not ask the agent to paste base64 into tool arguments. Fetch the `url` before `expiresAt`.
- Treat the signed URL as a secret. Do not log it, store it permanently, or return it to the user.
- Validate `kind`, `mimeType`, `sizeBytes`, and `sha256` before 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 `_context` server-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.

```ts
// 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:

- `registerTool` publishes `name`, `description`, and `inputSchema` via `tools/list` automatically.
- The Zod `.describe(...)` calls become the per-property `description`s 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/list` returns successfully.
- [ ] Every tool has a clear, action-oriented `description`.
- [ ] Every tool has an `inputSchema` with an explicit `required` array and a `description` on 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:

1. The tenant adds your server (URL + credentials) and the platform calls `tools/list`.
2. Tools are stored as an **allowlist**: enabled tools can be used; **destructive tools start
   disabled** until a human enables them.
3. 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** *(c)* the
   connection is active.
4. During a conversation the agent calls `tools/call` with arguments matching your `inputSchema`.
   If it ever names a tool that isn't available, the call fails fast — it cannot invent tools.
5. 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.