Published Mar 26, 2026
By Orbiq Team

How to Build an MCP Server for Compliance Automation

Step-by-step tutorial for building a Model Context Protocol (MCP) server that wraps the documented Orbiq Admin API v1 surface for certifications, documents, knowledge base entries, access requests, and NDA workflows.

Developers
Agentic AI
MCP Server
Model Context Protocol
Compliance Automation
API
AI Agents

How to Build an MCP Server for Compliance Automation

A Model Context Protocol (MCP) server is a standard wrapper that lets AI agents call tools and read resources through a consistent interface. For compliance automation, that matters because your data already lives in a system of record: certifications, documents, access requests, NDA workflows, and approved knowledge base answers.

The right pattern is not to invent an agent-only API. It is to wrap the published Orbiq Admin API v1 so MCP-compatible clients can use the same documented endpoints your own integrations would use. That keeps the server maintainable, auditable, and aligned with the official API reference at docs.orbiqhq.com.


What You Will Build

In this tutorial, you will build a TypeScript MCP server that exposes five read-focused compliance tools backed by the documented Orbiq Admin API v1:

  1. get_certifications — Lists certifications and expiry dates
  2. get_documents — Lists trust center documents, optionally filtered by access level
  3. search_knowledge_base — Retrieves approved knowledge base entries and filters them by keyword
  4. get_access_requests — Lists pending or filtered access requests
  5. get_nda_acceptances — Lists NDA acceptances, optionally filtered by email

This is enough for useful agent workflows:

  • "What certifications do we currently have?"
  • "Show me public documents I can send to a prospect."
  • "Find our approved answer for data residency."
  • "List pending access requests waiting for review."
  • "Has this contact already accepted the NDA?"

Why MCP Fits Compliance Work

Compliance automation already depends on deterministic systems and explicit approvals. MCP is a good fit because it makes those systems available to AI clients without hiding the underlying API model.

Integration approachDevelopment effortClient compatibilityMaintenance
Custom integration per AI clientHighOne client onlyMaintain N adapters
Raw model function callingMediumModel-specificRework per model/tool schema
MCP server over the Admin APIMediumAny MCP clientOne server, one API surface

For compliance teams, the practical benefit is simple: Claude Desktop, Cursor, Windsurf, or an in-house agent can all call the same set of tools, and those tools map cleanly to the documented API.


Prerequisites

Before you begin, you need:

  • Node.js 20+
  • An Orbiq account
  • A server-to-server API key for the Admin API
  • Basic familiarity with TypeScript, REST APIs, and JSON
  • An MCP client for testing, such as Claude Desktop or MCP Inspector

The Admin API reference documents API key management under /integrations/api-keys, and the rest of this tutorial assumes your MCP server authenticates with a bearer API key rather than an interactive JWT.

Set up the project:

mkdir orbiq-mcp-server && cd orbiq-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
mkdir src

Export the credentials:

export ORBIQ_API_KEY="your_api_key_here"
export ORBIQ_BASE_URL="https://app.orbiqhq.com/api/v1"

Step 1: Create the MCP Server Scaffold

Create src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const ORBIQ_API_KEY = process.env.ORBIQ_API_KEY;
const ORBIQ_BASE_URL =
  process.env.ORBIQ_BASE_URL || "https://app.orbiqhq.com/api/v1";

if (!ORBIQ_API_KEY) {
  throw new Error("ORBIQ_API_KEY environment variable is required");
}

const server = new McpServer({
  name: "orbiq-compliance",
  version: "1.0.0",
});

async function orbiqFetch<T = Record<string, unknown>>(
  endpoint: string,
  init: RequestInit = {}
): Promise<T> {
  const response = await fetch(`${ORBIQ_BASE_URL}${endpoint}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${ORBIQ_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  if (!response.ok) {
    throw new Error(
      `Orbiq API error: ${response.status} ${response.statusText}`
    );
  }

  return response.json() as Promise<T>;
}

function pickCollection<T>(
  body: Record<string, unknown>,
  ...keys: string[]
): T[] {
  for (const key of keys) {
    const value = body[key];
    if (Array.isArray(value)) {
      return value as T[];
    }
  }

  return [];
}

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Orbiq MCP server listening on stdio");
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Two details matter here:

  • The API reference documents a conventional bearer-auth REST surface, so your MCP server should behave like a thin wrapper, not a bespoke integration layer.
  • Collection responses use resource-specific top-level keys. Normalize that once in pickCollection() instead of hard-coding data.items assumptions everywhere.

Step 2: Add a Certifications Tool

Expose the certifications collection as an MCP tool:

type Certification = {
  id?: string;
  title?: string;
  state?: string;
  issue_date?: string;
  expiry_date?: string;
  featured?: boolean;
};

server.tool(
  "get_certifications",
  "List compliance certifications with status and expiry dates",
  {},
  async () => {
    const body = await orbiqFetch<Record<string, unknown>>("/certifications");
    const certifications = pickCollection<Certification>(body, "certifications");

    const summary = certifications.map((cert) => ({
      title: cert.title,
      state: cert.state,
      issue_date: cert.issue_date,
      expiry_date: cert.expiry_date,
      featured: cert.featured,
    }));

    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(summary, null, 2),
        },
      ],
    };
  }
);

This gives agents a reliable way to answer status questions such as:

  • "Do we currently have SOC 2 Type II?"
  • "Which certifications expire in the next 90 days?"

Step 3: Add a Documents Tool

The Admin API documents GET /documents, POST /documents, and PUT /documents/{id}/file. For MCP, start with a read-only listing tool:

type DocumentItem = {
  id?: string;
  title?: string;
  description?: string;
  access_level?: string;
  expiry_date?: string;
  featured?: boolean;
  created_at?: string;
};

server.tool(
  "get_documents",
  "List trust center documents, optionally filtered by access level",
  {
    access_level: z
      .enum(["public", "signed_in", "nda_only"])
      .optional()
      .describe("Optional access level filter"),
  },
  async ({ access_level }) => {
    const body = await orbiqFetch<Record<string, unknown>>("/documents");
    let documents = pickCollection<DocumentItem>(body, "documents");

    if (access_level) {
      documents = documents.filter(
        (doc) => doc.access_level === access_level
      );
    }

    const summary = documents.map((doc) => ({
      title: doc.title,
      description: doc.description,
      access_level: doc.access_level,
      expiry_date: doc.expiry_date,
      featured: doc.featured,
      created_at: doc.created_at,
    }));

    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(summary, null, 2),
        },
      ],
    };
  }
);

This is useful for prospect workflows:

  • "List the public security documents we can share."
  • "Show me which NDA-only documents exist for due diligence."

Step 4: Add a Knowledge Base Search Tool

The published API reference includes a GET /knowledge-base collection. That is a better foundation for an MCP server than inventing a private question-answering endpoint.

type KnowledgeBaseEntry = {
  id?: string;
  question?: string;
  answer?: string;
  category?: string;
  tags?: string[];
};

server.tool(
  "search_knowledge_base",
  "Search approved trust center knowledge base entries by keyword",
  {
    query: z.string().describe("Keyword or phrase to search for"),
    limit: z.number().int().min(1).max(10).default(5),
  },
  async ({ query, limit }) => {
    const body = await orbiqFetch<Record<string, unknown>>("/knowledge-base");
    const entries = pickCollection<KnowledgeBaseEntry>(
      body,
      "knowledge_base",
      "entries"
    );

    const needle = query.toLowerCase();
    const matches = entries
      .filter((entry) =>
        [
          entry.question ?? "",
          entry.answer ?? "",
          entry.category ?? "",
          ...(entry.tags ?? []),
        ]
          .join(" ")
          .toLowerCase()
          .includes(needle)
      )
      .slice(0, limit)
      .map((entry) => ({
        question: entry.question,
        answer: entry.answer,
        category: entry.category,
        tags: entry.tags ?? [],
      }));

    return {
      content: [
        {
          type: "text" as const,
          text:
            matches.length > 0
              ? JSON.stringify(matches, null, 2)
              : `No knowledge base matches found for: ${query}`,
        },
      ],
    };
  }
);

This gives you a documented, auditable pattern:

  • Store approved answers in the trust center knowledge base.
  • Let the MCP tool retrieve and filter them.
  • Let the AI client synthesize its final answer from those approved entries.

That is safer than introducing an undocumented "answer anything" endpoint into your automation flow.


Step 5: Add Access Request and NDA Tools

The API reference also documents access request and NDA workflows. The access request examples in the official docs use a contacts collection, so normalize that explicitly in your wrapper.

type AccessRequest = {
  id?: string;
  name?: string;
  email?: string;
  company_name?: string;
  review_status?: string;
  created_at?: string;
};

type NdaAcceptance = {
  id?: string;
  email?: string;
  status?: string;
  signed_at?: string;
  nda_template_title?: string;
};

server.tool(
  "get_access_requests",
  "List access requests, optionally filtered by review status",
  {
    review_status: z
      .enum(["to_review", "approved", "rejected"])
      .optional()
      .describe("Optional review status filter"),
  },
  async ({ review_status }) => {
    const query = review_status
      ? `/access-requests?review_status=${encodeURIComponent(review_status)}`
      : "/access-requests";

    const body = await orbiqFetch<Record<string, unknown>>(query);
    const requests = pickCollection<AccessRequest>(
      body,
      "contacts",
      "access_requests"
    );

    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(
            requests.map((request) => ({
              id: request.id,
              name: request.name,
              email: request.email,
              company_name: request.company_name,
              review_status: request.review_status,
              created_at: request.created_at,
            })),
            null,
            2
          ),
        },
      ],
    };
  }
);

server.tool(
  "get_nda_acceptances",
  "List NDA acceptances, optionally filtered by email address",
  {
    email: z.string().email().optional(),
  },
  async ({ email }) => {
    const body = await orbiqFetch<Record<string, unknown>>("/nda-acceptances");
    let acceptances = pickCollection<NdaAcceptance>(
      body,
      "nda_acceptances",
      "acceptances"
    );

    if (email) {
      acceptances = acceptances.filter(
        (item) => item.email?.toLowerCase() === email.toLowerCase()
      );
    }

    return {
      content: [
        {
          type: "text" as const,
          text:
            acceptances.length > 0
              ? JSON.stringify(acceptances, null, 2)
              : email
                ? `No NDA acceptances found for ${email}`
                : "No NDA acceptances found.",
        },
      ],
    };
  }
);

For production use, keep this MCP server read-oriented. The API reference documents write actions such as PATCH /access-requests/{id}, but approval and rejection are better left behind human review rather than autonomous tool calls.


Step 6: Test with an MCP Client

Option A: MCP Inspector

npx @modelcontextprotocol/inspector npx tsx src/index.ts

This is the fastest way to validate tool schemas and inspect raw outputs.

Option B: Claude Desktop

Add the server to claude_desktop_config.json:

{
  "mcpServers": {
    "orbiq-compliance": {
      "command": "npx",
      "args": ["tsx", "/path/to/orbiq-mcp-server/src/index.ts"],
      "env": {
        "ORBIQ_API_KEY": "your_api_key_here",
        "ORBIQ_BASE_URL": "https://app.orbiqhq.com/api/v1"
      }
    }
  }
}

Then test prompts such as:

  • "List our current certifications and highlight anything expiring soon."
  • "Show me public trust center documents."
  • "Search our knowledge base for data residency."
  • "List access requests that are still waiting for review."
  • "Has security@prospect.com accepted the NDA?"

Option C: Cursor or Windsurf

Both editors can use the same MCP configuration. That makes compliance status available in the IDE without inventing editor-specific integrations.


Example Production Patterns

Sales Engineer Assistant

A sales engineer asks:

"Do we have ISO 27001, and can I share the policy pack?"

The MCP client can call:

  1. get_certifications
  2. get_documents

That gives a grounded answer from live trust center data instead of a stale internal note.

Knowledge Base Grounded Security Answers

A vendor sends a question about encryption, retention, or data residency. The agent calls search_knowledge_base with the topic, returns the approved answer candidates, and drafts a response for human review.

Access Review Workflow

A compliance operator asks:

"Show me all access requests still waiting for review."

The client calls get_access_requests with review_status: "to_review" and gets a queue directly from the Admin API.


Deployment Notes

For team-wide use, package the server as an npm module or container:

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
CMD ["node", "dist/index.js"]

Production recommendations:

  • Use read-only API keys for the MCP server where possible.
  • Log tool invocations for auditability.
  • Keep write actions human-in-the-loop, especially access approval and document publication.
  • Normalize collection responses once in your wrapper because the Admin API uses resource-specific top-level keys.
  • Fail closed on missing credentials, unknown response shapes, and non-2xx responses.

How Orbiq Helps

Orbiq's Admin API v1 gives you a clean MCP integration surface because the core compliance objects are already explicit: documents, certifications, knowledge base entries, access requests, NDA workflows, branding, and API keys. That is what you want for agent tooling. The MCP layer should be thin and predictable, not magical.

If you want to automate publishing and evidence collection in CI/CD as well, pair this tutorial with Compliance as Code: A Developer's Guide to Automated Compliance. If you want the earlier API-first trust center setup, see How to Build a Trust Center with the Orbiq API.


FAQ

What is an MCP server?

An MCP server exposes tools and resources to AI clients through a standard protocol. It is a transport and schema layer around your actual systems of record.

Why use MCP instead of calling the API directly from one AI app?

Because you only implement the tool layer once. Claude Desktop, Cursor, Windsurf, and custom MCP clients can all use the same server instead of each needing a custom integration.

Should my MCP server expose write actions?

Usually not at first. Start with read-oriented tools over documented collections such as documents, certifications, knowledge base entries, access requests, and NDA acceptances. Add write actions only when you have clear approval and audit requirements around them.

Is knowledge-base retrieval enough without a dedicated answer endpoint?

For many compliance workflows, yes. It is often better. Your MCP server can retrieve approved knowledge base entries and let the client summarize them, which keeps the automation grounded in content your team already controls.

What is the main implementation detail people get wrong?

They assume every collection comes back as data.items. The Orbiq Admin API uses resource-specific top-level keys, so build a small normalization helper and keep the rest of your MCP tools simple.


Sources & References

  1. Orbiq Trust Center Admin API v1
  2. Orbiq Developer Hub
  3. Model Context Protocol documentation
How to Build an MCP Server for Compliance Automation