MCP Integration Guide

    v1.1.1 · updated 2026-07-04

    View raw markdown

    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:

    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:

    {
      "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, 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.

    // 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 descriptions 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 © 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.