Connect Your MCP Server

If your product exposes an MCP (Model Context Protocol) server, you can plug it into Aimdoc. Your server's tools become available to your agent in chat and to services running in the background — a signed-in user can ask "show me my open invoices" and the agent answers with live data from your product, with no extra login and no per-user OAuth setup.

This is also a distribution channel for your MCP server: every user of your product gets its capabilities through the in-app assistant they already use, whether or not they've ever connected an AI client themselves.

How it works

  1. Your app identifies the signed-in user to Aimdoc via the SDK's identify call, signed with your identity-verification secret — the setup described in Identity Verification.
  2. You connect your MCP server once for the organization under Integrations → MCP Servers, with the Aimdoc identity auth mode.
  3. You bind that server to an agent (agent builder → MCP Servers) and/or a service, and pick which tools each may use.
  4. When the agent or service calls one of your tools, Aimdoc mints a short-lived signed token — an identity assertion — naming the current user, and sends it as the Authorization: Bearer header on the MCP request.
  5. Your MCP server verifies the token with the same shared secret and decides what that user may access.

The trust chain is identity verification run in reverse: your server signs identity to Aimdoc at identify time; Aimdoc signs identity back to you at tool-call time. No token is ever exposed to the browser, and you store no per-user auth state. Credentials live on the organization server, not on each agent or service.

Server requirements

  • Transport: MCP Streamable HTTP (the current MCP spec transport).
  • Reachability: a public HTTPS URL. Private hosts and raw IP addresses are rejected.
  • Discovery: initialize and tools/list must succeed with any valid Aimdoc-signed token, including anonymous ones — the agent builder's Test connection button and unidentified visitors both present tokens with no user in them. Gate user-scoped tools, not the handshake.

Connect a server

Connect the server once. Every agent and service can then bind to it.

  1. Open Integrations → MCP ServersAdd MCP Server. You can also connect a server in place from an agent's MCP Servers tab or a service's Tools section.
  2. Enter a name and your server URL (e.g. https://mcp.yourcompany.com/mcp).
  3. Choose an authentication mode:
    • None — no credential. Only for public servers.
    • API key — a static key sent as a Bearer token on every call. Simple, but every request acts as the same principal regardless of who is chatting or which service is running.
    • Aimdoc identity — per-user signed tokens, described below. Recommended when your tools return user- or account-specific data.
  4. Click Test connection. Aimdoc connects, lists your tools, and saves the catalog on the organization server.

Renaming the server, replacing the key, re-discovering tools, disabling, or deleting happens here and reaches every agent and service bound to it. Disable or delete warns with the list of bound consumers first.

The tool names and descriptions you expose are shown to the model, so write them the way you'd write them for any MCP client: say what the tool does and when to call it.

Use it on an agent or service

Agent: open the agent → MCP ServersAdd MCP Server. Pick a connected organization server (or connect a new one), choose a slug, and tick the tools this agent may use. Publish the agent.

Service: in the service editor under Tools, bind the same way. The binding is part of the draft; publish the service for it to reach runs. Start from agent… copies that agent's bindings into the draft without replacing servers you already added.

Verifying identity assertions

With the Aimdoc identity mode, every request to your MCP server carries a JWT as a Bearer token, signed with HS256 using your organization's identity-verification secret — the same secret your server already uses to sign identify calls (Integrations → Identity Verification). Verification is a few lines with any JWT library.

Claims

{
  "iss": "aimdoc",
  "aud": "https://mcp.yourcompany.com/mcp",
  "sub": "user_123",
  "email": "jane@acme.com",
  "account": { "external_id": "account_456", "domain": "acme.com", "name": "Acme" },
  "identity_tier": "verified",
  "org": "b1f0…",
  "agent_id": "9c2e…",
  "conversation_id": "77aa…",
  "iat": 1754400000,
  "exp": 1754400300,
  "jti": "f3a1c9d2e8b04477"
}
  • iss — always "aimdoc".
  • aud — your MCP server URL exactly as configured under Integrations → MCP Servers. Reject tokens whose audience isn't your URL.
  • subyour own user ID: the external_id your app passed to identify, echoed back. Present when identity_tier is "verified" or "email_reply".
  • email — the user's email, trimmed and lowercased. Only present alongside sub.
  • account — the account from identify (external_id, domain, name; absent fields omitted). Only present when identity claims are present and the user is linked to an account.
  • identity_tier — how Aimdoc authenticated the subject:
    • "verified" — this session proved its identity with a valid user_hash
    • "email_reply" — the user replied from a verified contact's mailbox to a conversation Aimdoc initiated, authenticated by the thread's secret reply token. It carries sub / email / account like "verified". If your server only trusts fully signed sessions, keep checking identity_tier == "verified" and treat unknown tiers as anonymous.
    • "anonymous" — the request genuinely comes from Aimdoc, but the visitor is not identified
  • org, agent_id, conversation_id — Aimdoc-side context IDs, useful for logging and support. conversation_id is present on live chats.
  • iat / exp — tokens live for about 5 minutes and are minted per call. Nothing to store or refresh on your side.

The JWT header carries "typ": "aimdoc-identity-assertion+jwt" if you want to route on it.

What to check

  1. The signature is valid HS256 under your Aimdoc signing secret. Pin the algorithm to HS256 — never let the token header choose it.
  2. exp has not passed (standard libraries check this by default).
  3. aud equals your configured server URL.
  4. iss equals "aimdoc".

Then authorize: map sub to a user in your system (it's your ID — no lookup table needed) and scope tool results to what that user may see. Treat identity_tier: "anonymous" like an unauthenticated caller: allow the handshake and any public tools, and have user-scoped tools return an error such as "sign in to your account to use this".

If your server only trusts fully signed sessions, keep checking identity_tier == "verified" and treat unknown tiers — including email_reply — as anonymous. Accept email_reply only when mailbox possession is enough trust for the tools you expose.

// Node.js (Express-style middleware). The secret must stay server-side —
// same rule as identity verification.
import jwt from 'jsonwebtoken'

const AIMDOC_SECRET = process.env.AIMDOC_IDENTITY_SECRET
const MCP_URL = 'https://mcp.yourcompany.com/mcp'

function verifyAimdocToken(req, res, next) {
  const token = (req.headers.authorization || '').replace(/^Bearer /, '')
  try {
    const claims = jwt.verify(token, AIMDOC_SECRET, {
      algorithms: ['HS256'],
      audience: MCP_URL,
      issuer: 'aimdoc',
    })
    // Fail closed: only trust fully signed sessions. Accept "email_reply"
    // here too if mailbox-possession trust is enough for your tools.
    const trusted = claims.identity_tier === 'verified'
    req.aimdoc = {
      userId: trusted ? claims.sub : null,
      accountId: claims.account?.external_id ?? null,
      verified: trusted,
    }
    next()
  } catch {
    res.status(401).json({ error: 'invalid_token' })
  }
}
# Python (FastAPI-style dependency)
import os
import jwt  # PyJWT
from fastapi import HTTPException, Request

AIMDOC_SECRET = os.environ["AIMDOC_IDENTITY_SECRET"]
MCP_URL = "https://mcp.yourcompany.com/mcp"

def verify_aimdoc_token(request: Request) -> dict:
    token = (request.headers.get("authorization") or "").removeprefix("Bearer ")
    try:
        claims = jwt.decode(
            token,
            AIMDOC_SECRET,
            algorithms=["HS256"],  # pin the algorithm
            audience=MCP_URL,
            issuer="aimdoc",
            options={"require": ["exp", "iat", "aud", "iss"]},
        )
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="invalid_token")
    # Fail closed: only trust fully signed sessions. Accept "email_reply"
    # here too if mailbox-possession trust is enough for your tools.
    trusted = claims.get("identity_tier") == "verified"
    return {
        "user_id": claims.get("sub") if trusted else None,
        "account_id": (claims.get("account") or {}).get("external_id"),
        "verified": trusted,
    }

Assertions from background service runs

In addition to live conversations, Aimdoc can call your MCP server from a background service run — an automated task an agent performs on a user's behalf (for example, a weekly usage report the user asked for). These requests carry the same signed assertion (HS256, your signing secret, about 5 minutes, aud = your MCP server URL), with three additional claims:

{
  "context": "service_run",
  "service": {
    "key": "weekly_usage_report",
    "service_id": "…",
    "version_id": "…",
    "automation_id": "…",
    "run_id": "…"
  },
  "grant": {
    "captured_at": "2026-08-15T20:09:00Z",
    "created_via": "agent"
  }
}
  • context: "service_run" marks the request as delegated: no user is present in a live session; Aimdoc is acting under an authorization the user (or a workspace admin) granted earlier. Requests without this claim are live-conversation calls, unchanged from before.
  • service identifies what is running: key is the stable service identifier, run_id the individual execution. version_id and automation_id are present when they apply. If you want per-service authorization, allowlist on service.key.
  • grant says when and how the delegation was captured (created_via is "agent", "admin", "sdk", or "api"). grant is only present on "verified" identity.
  • Identity claims (sub, email, account) are present when the run is for a verified user, and sub is the external_id your own identify call supplied. Anonymous-tier service runs carry no identity or grant claims — treat them like anonymous visitors.

You do not need to change anything for these requests to work: signature, aud, exp, and tier checks are identical. Validate the new claims only if you want to distinguish delegated from live traffic, authorize per service, or log run ids. If your validator rejects unknown claims, allow these three.

Operational notes

  • Rotating your secret (Integrations → Identity Verification → rotate) immediately invalidates both directions: your identify signatures and Aimdoc's assertions to your server. Update your server's copy of the secret at the same time. In-flight assertions expire within minutes.
  • Per-call tokens: Aimdoc mints a fresh assertion for every tool call. Your server needs no session state, token cache, or refresh logic.
  • Limits: the agent makes at most 5 MCP tool calls per assistant turn; connection and call timeouts are 10 and 30 seconds; tool results are truncated at 20,000 characters. Return compact, structured results — the model reads them directly.
  • Changing your server's tools: the catalog is captured when you last tested or re-discovered the organization server. After adding or changing tools, open Integrations → MCP Servers, re-discover, enable the new tools on each agent or service that should use them, and publish.

FAQ

Why don't my users have to log in to use these tools? They already did — in your app. Your app vouches for their identity via the signed identify call, and Aimdoc relays that identity to your MCP server in a token you can verify. There is no third login because there is no third party: both ends of the chain already trust your signing secret.

What do anonymous visitors get? Chats where the visitor hasn't been identified (or the session's user_hash didn't verify) send assertions with identity_tier: "anonymous" and no sub. Your server decides: expose public tools, or return an error the agent will relay gracefully. Authenticated replies on agent-initiated email threads may instead arrive as email_reply with identity claims — see Verifying identity assertions.

Can I use this token format with other clients? The claims are shaped to align with the OAuth Identity Assertion Authorization Grant (the standard behind MCP's Enterprise-Managed Authorization), so a future migration to standards-based cross-app access is a claims-mapping exercise, not a redesign.

Is the shared secret safe enough? The secret already protects identity verification, and it only grants access to your own server. Keep it server-side on both ends. If you need asymmetric keys, contact us — it's on the roadmap.

Was this page helpful?