Skip to main content
Tracent TechnologiesTracent Technologies
Get started
All guides

Build an MCP server

How to wrap your API as a Model Context Protocol server so the Tracent gateway can route AI agent calls to it. The walkthrough uses the official TypeScript SDK, which is also the stack Tracent uses for its own internal MCP servers.

~12 min read

What you are building

An MCP server is a small process that exposes a typed set of tools to any MCP client. The Tracent gateway is an MCP client: it discovers your tools, validates calls against your schemas, applies the org's Tool Group policy (rate limits, allowed roles, PII redaction, the §7.8 HITL gate), records the request in the audit log, and returns the result to whichever AI agent made the call (Claude, ChatGPT, Gemini, your in-house agent).

The contract has three parts. listTools returns the catalogue. callTool executes a tool by name with validated arguments. transport is how the client and server talk to each other; for production we recommend the SSE (Server-Sent Events) transport over HTTPS, with the stdio transport reserved for local development.

Prerequisites

  • Node 20+ and either pnpm, npm, or Bun. Examples below use pnpm.
  • An API you want to expose: a payment rail, a banking endpoint, a commerce platform, a logistics tracker. Tracent works categorically, not partner-specifically.
  • A read of the Tracent concepts page so the terminology (Tool Group, audit log, HITL gate) is already context.

Step 1: Scaffold the project

mkdir my-mcp-server && cd my-mcp-server
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript tsx @types/node

The official SDK handles transport, message framing, and the listTools/callTool plumbing. Zod gives us runtime-validated input schemas; the gateway also validates client-side, but a server that validates its own input is a server that fails loudly when contracts drift.

Step 2: Define your tools

A tool is a single API operation with a name, a description, a typed input, and a typed output. Tracent recommends tight scopes: a tool called do_payment_things is a sign the integration was rushed.

// src/tools.ts
import { z } from "zod";

export const tools = {
  create_charge: {
    description:
      "Initiate a payment charge against a customer's account.",
    input: z.object({
      amount: z.number().int().positive(),
      currency: z.enum(["NGN", "USD", "KES"]),
      customer_email: z.string().email(),
      reference: z.string().min(1).max(120),
    }),
    output: z.object({
      reference: z.string(),
      status: z.enum(["pending", "successful", "failed"]),
      authorization_url: z.string().url().optional(),
    }),
  },

  verify_transaction: {
    description: "Confirm the status of a previously initiated charge.",
    input: z.object({
      reference: z.string().min(1),
    }),
    output: z.object({
      reference: z.string(),
      status: z.enum(["pending", "successful", "failed"]),
      amount_settled: z.number().int().nonnegative().optional(),
    }),
  },
};

Three to five tools is the sweet spot at launch. Add more as the partner relationship deepens and the gateway sees the callable-tool patterns customers actually need.

Step 3: Implement the handlers

// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { tools } from "./tools.js";

const server = new Server(
  { name: "my-payment-mcp", version: "0.1.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: Object.entries(tools).map(([name, t]) => ({
    name,
    description: t.description,
    inputSchema: zodToJsonSchema(t.input),
  })),
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const tool = tools[request.params.name as keyof typeof tools];
  if (!tool) throw new Error(`Unknown tool: ${request.params.name}`);

  const args = tool.input.parse(request.params.arguments);
  const result = await callPartnerApi(request.params.name, args);
  return {
    content: [{ type: "text", text: JSON.stringify(result) }],
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);

callPartnerApi is your own function: it makes the actual HTTP request to the partner API, handles auth (the API key arrives as an env var; do not hard-code it), retries on 5xx, and returns a parsed response. Keep it boring; the surprises live at the API boundary, not in your wrapper.

Step 4: PII discipline at the boundary

The Tracent gateway runs the canonical lib/ndpa/redactor.ts on every payload before writing to the audit log. Your server inherits this for free: the gateway calls you with the request as the agent constructed it, you call your partner API with what the partner needs, and the audit log shows the redacted version on the way out.

The trade-off you do choose is whether to alsoredact at your server's boundary. We recommend yes for two reasons. First, defence in depth: if the gateway redactor ever fails open, your server is the second line. Second, your own observability (Sentry, Datadog) catches server errors before they reach the gateway audit log, so you want those errors to carry no PII.

// Apply your own redactor before any local log or error report.
import { redactPii } from "@/your-own/redactor";

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const tool = tools[request.params.name as keyof typeof tools];
  if (!tool) throw new Error(`Unknown tool: ${request.params.name}`);

  const args = tool.input.parse(request.params.arguments);
  const { redacted } = redactPii(args);

  try {
    const result = await callPartnerApi(request.params.name, args);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    // Logs only the redacted args, never the originals.
    console.error("Partner call failed", {
      tool: request.params.name,
      redactedArgs: redacted,
      message: error instanceof Error ? error.message : String(error),
    });
    throw error;
  }
});

The canonical PII keys list (bvn, nin, account_number, balance, phone, address, date_of_birth) is the floor. If your partner API has its own sensitive field name (e.g., a card-PAN field), add it to your local redactor; surface the addition back to Tracent so the gateway redactor picks it up too.

Step 5: Declare HITL-gated tools

A tool that moves money requires the §7.8 human-in-the-loop gate before execution. The declaration lives on the Tool Group, not in your server: in the Tracent console, group the fund-movement tools into a Tool Group with hitl_required = true. The gateway enforces it before your server ever sees the request.

For your server's perspective this is invisible: by the time callTool fires, the user has already entered their PIN. You handle the call the same way you would any other.

The customer of your server (the merchant deploying through Tracent) configures HITL per Tool Group in /console/tool-groups. The default for new groups ships with hitl_required = true; opting out is deliberate.

Step 6: Test locally

# Run the server over stdio (good for the MCP Inspector or a CLI client)
pnpm tsx src/server.ts

# In another terminal, validate the contract using the MCP Inspector
npx @modelcontextprotocol/inspector pnpm tsx src/server.ts

The Inspector enumerates your tools, lets you call each with synthetic arguments, and shows the responses. If your input-schema validation rejects a payload, the Inspector surfaces the Zod error; if your output does not match the declared schema, the gateway will reject it later, so make sure both sides are clean.

Step 7: Register with the Tracent gateway

Open /console/servers/new in the Tracent customer console. Choose the category that matches your API (Payments, Banking, Mobile money, Commerce, Payouts, Logistics, Health). Provide the SSE endpoint URL when you have one in production; for the sandbox phase you can run stdio and connect through a local proxy.

Once registered, your server appears in /console/servers with status active. Add its tools to one or more Tool Groups in /console/tool-groups, set the policy (allowed roles, rate limits, PII mask keys, HITL requirement, retention window), and run a sandbox call from /console/servers/[id]/test. The first successful call appears on the dashboard counters and in the live audit log within seconds.

Where to go from here

  • The API reference documents the Gateway Sync endpoint that your server can optionally subscribe to for policy updates.
  • If your integration handles cross-border data flow, the sub-processor disclosure page shows how Tracent maps third-party processors to NDPA obligations; the same model applies to your server when it calls a partner outside Nigeria.
  • Open a conversation with hello@tracenttechnologies.com if you want your integration listed in the public directory at /integrations. Phase 1 priority is African apps; the listing is editorial and gated on a signed partnership term sheet.