DEV Community

Preecha
Preecha

Posted on

How to Build an MCP Server That Gives AI Agents API Testing Powers

TL;DR

Build an MCP server with TypeScript that exposes three tools: run_test, validate_schema, and list_environments. Configure it in ~/.claude/settings.json for Claude Code or .cursor/mcp.json for Cursor. Your AI agents can then run Apidog tests, validate OpenAPI schemas, and fetch environments without leaving the chat interface. The complete server is about 150 lines and uses @modelcontextprotocol/sdk.

Try Apidog today

Build an MCP server that lets Claude Code, Cursor, and other AI agents run Apidog API tests, validate schemas, and compare responses without leaving the chat interface.

Imagine your AI agent has just finished building an API endpoint. Instead of copying code, opening Apidog, creating a test collection, and running validation manually, you can call one tool and receive the results in your coding session.

That’s what the Model Context Protocol (MCP) enables. MCP gives AI agents a standardized way to access external tools and data sources. In this tutorial, you’ll build an Apidog MCP server with three tools:

  • run_test — execute API tests
  • validate_schema — validate an OpenAPI schema
  • list_environments — fetch project environments
┌─────────────────┐         ┌──────────────────┐         ┌─────────────┐
│  AI Agent       │         │  MCP Server      │         │  Apidog     │
│  (Claude Code)  │◄───────►│  (Your Code)     │◄───────►│  API        │
└─────────────────┘   JSON  └──────────────────┘  HTTP   └─────────────┘
Enter fullscreen mode Exit fullscreen mode

What Is MCP?

MCP, or Model Context Protocol, is a protocol that lets AI agents access external tools and data sources. It works like a plugin system across Claude Code, Cursor, and other MCP-compatible clients.

An MCP server exposes:

  • Tools — functions an agent can call
  • Resources — data an agent can read

Our server will expose tools that call Apidog’s API.

Prerequisites

You’ll need:

  • Node.js with built-in fetch support
  • TypeScript
  • An Apidog API key
  • An Apidog project ID

The server reads the API key from the APIDOG_API_KEY environment variable.

Step 1: Create the TypeScript Project

Create a project and install the dependencies:

mkdir apidog-mcp-server
cd apidog-mcp-server
npm init -y

npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Enter fullscreen mode Exit fullscreen mode

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

Add build and start scripts to package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create the source directory:

mkdir src
touch src/index.ts
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the MCP Server

Start src/index.ts with the server and stdio transport:

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

const server = new McpServer({
  name: "apidog",
  version: "1.0.0",
  description: "Apidog API testing tools for AI agents"
});

// Tools will be defined here.

const transport = new StdioServerTransport();
await server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

StdioServerTransport communicates with the MCP client through standard input and output. This is what allows Claude Code, Cursor, or another client to launch the server as a local process.

Step 3: Add the run_test Tool

Add a tool that executes tests for a project. It accepts an optional environment and test suite:

server.tool(
  "run_test",
  {
    projectId: z
      .string()
      .describe("Apidog project ID (found in the project URL)"),
    environmentId: z
      .string()
      .optional()
      .describe("Optional environment ID for test execution"),
    testSuiteId: z
      .string()
      .optional()
      .describe("Optional test suite ID to run a specific suite")
  },
  async ({ projectId, environmentId, testSuiteId }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY environment variable not set"
          }
        ]
      };
    }

    let url = `https://api.apidog.com/v1/projects/${projectId}/tests/run`;

    const params = new URLSearchParams();

    if (environmentId) {
      params.append("environmentId", environmentId);
    }

    if (testSuiteId) {
      params.append("testSuiteId", testSuiteId);
    }

    if (params.toString()) {
      url += `?${params.toString()}`;
    }

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Content-Type": "application/json"
        }
      });

      if (!response.ok) {
        const error = await response.text();

        return {
          content: [
            {
              type: "text",
              text: `API Error: ${response.status} ${error}`
            }
          ]
        };
      }

      const results = await response.json();

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results, null, 2)
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Request failed: ${
              error instanceof Error ? error.message : String(error)
            }`
          }
        ]
      };
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

Each MCP tool has three important parts:

  1. Name — the identifier the agent uses to select the tool
  2. Schema — Zod definitions for validating arguments
  3. Handler — the asynchronous function that calls the Apidog API

The descriptions in the Zod schema help the AI agent understand how to call the tool.

Step 4: Add the validate_schema Tool

Use a second tool to validate an OpenAPI 3.x schema before deployment:

server.tool(
  "validate_schema",
  {
    schema: z
      .object({})
      .describe("OpenAPI 3.x schema object to validate"),
    strict: z
      .boolean()
      .optional()
      .default(false)
      .describe("Enable strict mode for additional checks")
  },
  async ({ schema, strict }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY environment variable not set"
          }
        ]
      };
    }

    try {
      const response = await fetch(
        "https://api.apidog.com/v1/schemas/validate",
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${apiKey}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({ schema, strict })
        }
      );

      const result = await response.json();

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Validation failed: ${JSON.stringify(
                result.errors,
                null,
                2
              )}`
            }
          ]
        };
      }

      return {
        content: [
          {
            type: "text",
            text: result.valid
              ? "Schema is valid OpenAPI 3.x"
              : `Warnings: ${JSON.stringify(result.warnings, null, 2)}`
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Validation failed: ${
              error instanceof Error ? error.message : String(error)
            }`
          }
        ]
      };
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

The strict argument is optional and defaults to false. When enabled, the request includes strict validation checks.

Step 5: Add the list_environments Tool

Add a tool that lists the environments available in an Apidog project:

server.tool(
  "list_environments",
  {
    projectId: z.string().describe("Apidog project ID")
  },
  async ({ projectId }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY environment variable not set"
          }
        ]
      };
    }

    try {
      const response = await fetch(
        `https://api.apidog.com/v1/projects/${projectId}/environments`,
        {
          headers: {
            Authorization: `Bearer ${apiKey}`
          }
        }
      );

      if (!response.ok) {
        const error = await response.text();

        return {
          content: [
            {
              type: "text",
              text: `API Error: ${response.status} ${error}`
            }
          ]
        };
      }

      const environments = await response.json();

      return {
        content: [
          {
            type: "text",
            text:
              environments.length === 0
                ? "No environments found for this project"
                : environments
                    .map(
                      (environment: any) =>
                        `- ${environment.name} (ID: ${environment.id})${
                          environment.isDefault ? " [default]" : ""
                        }`
                    )
                    .join("\n")
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Request failed: ${
              error instanceof Error ? error.message : String(error)
            }`
          }
        ]
      };
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

The tool formats each environment as a readable list and marks the default environment when isDefault is true.

Step 6: Build and Test the Server

Compile the TypeScript source:

npm run build
Enter fullscreen mode Exit fullscreen mode

The compiled server should now be available at dist/index.js.

You can launch it directly:

node dist/index.js
Enter fullscreen mode Exit fullscreen mode

Test with a Small MCP Client

Create test-client.js:

import { spawn } from "child_process";

const server = spawn("node", ["dist/index.js"], {
  env: {
    ...process.env,
    APIDOG_API_KEY: "your-api-key"
  }
});

server.stdout.on("data", (data) => {
  console.log(`Server output: ${data}`);
});

server.stderr.on("data", (data) => {
  console.error(`Server error: ${data}`);
});

const message = {
  jsonrpc: "2.0",
  id: 1,
  method: "initialize",
  params: {
    protocolVersion: "2024-11-05",
    capabilities: {},
    clientInfo: {
      name: "test-client",
      version: "1.0.0"
    }
  }
};

server.stdin.write(JSON.stringify(message) + "\n");
Enter fullscreen mode Exit fullscreen mode

Run it with:

node test-client.js
Enter fullscreen mode Exit fullscreen mode

Step 7: Configure Claude Code

Create or edit ~/.claude/settings.json:

{
  "mcpServers": {
    "apidog": {
      "command": "node",
      "args": ["/absolute/path/to/apidog-mcp-server/dist/index.js"],
      "env": {
        "APIDOG_API_KEY": "your-api-key-here"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use an absolute path for the compiled server. Restart Claude Code after saving the configuration.

You can then invoke the tools with prompts such as:

Use the run_test tool to run tests on my Apidog project.

Project ID: proj_12345
Environment: staging
Enter fullscreen mode Exit fullscreen mode
Validate this OpenAPI schema against Apidog rules:
[paste schema]
Enter fullscreen mode Exit fullscreen mode
List all environments for project proj_12345
Enter fullscreen mode Exit fullscreen mode

Step 8: Configure Cursor

Create .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "apidog": {
      "command": "node",
      "args": ["/absolute/path/to/apidog-mcp-server/dist/index.js"],
      "env": {
        "APIDOG_API_KEY": "your-api-key-here"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In Cursor, invoke the server like this:

@apidog run_test projectId="proj_12345" environmentId="staging"
Enter fullscreen mode Exit fullscreen mode

The same server can be configured for Claude Code, Cursor, and other MCP-compatible clients.

Complete Source Code

Here is the complete src/index.ts:

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

const server = new McpServer({
  name: "apidog",
  version: "1.0.0",
  description: "Apidog API testing tools for AI agents"
});

// Tool: run_test
server.tool(
  "run_test",
  {
    projectId: z.string().describe("Apidog project ID"),
    environmentId: z.string().optional().describe("Environment ID"),
    testSuiteId: z.string().optional().describe("Test suite ID")
  },
  async ({ projectId, environmentId, testSuiteId }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY not set"
          }
        ]
      };
    }

    let url = `https://api.apidog.com/v1/projects/${projectId}/tests/run`;

    const params = new URLSearchParams();

    if (environmentId) {
      params.append("environmentId", environmentId);
    }

    if (testSuiteId) {
      params.append("testSuiteId", testSuiteId);
    }

    if (params.toString()) {
      url += `?${params.toString()}`;
    }

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Content-Type": "application/json"
        }
      });

      const results = await response.json();

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results, null, 2)
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Request failed: ${
              error instanceof Error ? error.message : String(error)
            }`
          }
        ]
      };
    }
  }
);

// Tool: validate_schema
server.tool(
  "validate_schema",
  {
    schema: z.object({}).describe("OpenAPI schema"),
    strict: z.boolean().optional().default(false)
  },
  async ({ schema, strict }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY not set"
          }
        ]
      };
    }

    const response = await fetch(
      "https://api.apidog.com/v1/schemas/validate",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ schema, strict })
      }
    );

    const result = await response.json();

    return {
      content: [
        {
          type: "text",
          text: result.valid
            ? "Schema is valid"
            : `Issues: ${JSON.stringify(
                result.errors || result.warnings,
                null,
                2
              )}`
        }
      ]
    };
  }
);

// Tool: list_environments
server.tool(
  "list_environments",
  {
    projectId: z.string().describe("Apidog project ID")
  },
  async ({ projectId }) => {
    const apiKey = process.env.APIDOG_API_KEY;

    if (!apiKey) {
      return {
        content: [
          {
            type: "text",
            text: "Error: APIDOG_API_KEY not set"
          }
        ]
      };
    }

    const response = await fetch(
      `https://api.apidog.com/v1/projects/${projectId}/environments`,
      {
        headers: {
          Authorization: `Bearer ${apiKey}`
        }
      }
    );

    const environments = await response.json();

    return {
      content: [
        {
          type: "text",
          text: environments
            .map(
              (environment: any) =>
                `- ${environment.name} (${environment.id})${
                  environment.isDefault ? " [default]" : ""
                }`
            )
            .join("\n")
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

What You Built

Component Purpose
MCP server Bridges AI agents to the Apidog API
run_test Executes test collections programmatically
validate_schema Catches OpenAPI errors before deployment
list_environments Discovers available test environments
Zod validation Provides type-safe parameter handling
Stdio transport Works with Claude Code, Cursor, and other MCP clients

Test the MCP Server Locally

Before configuring a client, verify that the server responds to MCP requests.

List the available tools:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | node dist/index.js
Enter fullscreen mode Exit fullscreen mode

Expected output:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "run_test",
        "description": "...",
        "inputSchema": {}
      },
      {
        "name": "validate_schema",
        "description": "...",
        "inputSchema": {}
      },
      {
        "name": "list_environments",
        "description": "...",
        "inputSchema": {}
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Test a tool call:

echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_environments","arguments":{"projectId":"your-project-id"}}}' \
  | node dist/index.js
Enter fullscreen mode Exit fullscreen mode

If the server returns the environment list, the MCP process and tool routing are working.

Next Steps

You can extend the server with additional tools:

  • compare_responses — diff test results across environments
  • get_test_history — fetch historical test runs
  • trigger_mock_server — start or stop mock endpoints

Before using the server in production, consider:

  • Adding retry logic for transient network failures
  • Implementing rate limiting to avoid API throttling
  • Adding structured logging for failed tool calls
  • Storing API keys in a secure vault instead of environment variables
  • Adding response type definitions instead of relying on any

To share the server with your team:

  • Publish it as a private npm package such as @your-org/apidog-mcp-server
  • Document the required environment variables
  • Include MCP configuration examples for common clients

Troubleshooting

MCP server does not load in Claude Code

Check the following:

which node
ls -la dist/
node dist/index.js
Enter fullscreen mode Exit fullscreen mode

Also verify that:

  • The path in ~/.claude/settings.json is absolute
  • Node is available in the environment used by Claude Code
  • dist/index.js exists
  • The server starts without errors
  • The tool definitions appear before server.connect()

Check Claude Code’s MCP logs for additional startup errors.

Tools do not appear after configuration

  • Restart Claude Code completely
  • Run npm run build
  • Confirm all three tools are registered
  • Start the server manually with node dist/index.js

API requests return 401

  • Confirm APIDOG_API_KEY is set in the client configuration
  • Remove extra spaces or quotes around the key
  • Verify that your Apidog account has API access enabled
  • Test the key manually:
curl \
  -H "Authorization: Bearer $APIDOG_API_KEY" \
  https://api.apidog.com/v1/user
Enter fullscreen mode Exit fullscreen mode

Zod validation errors

  • Check that parameter names match the schema exactly
  • Provide all required fields
  • Confirm optional fields use .optional()
  • Check the full Zod error message to identify the invalid field

TypeScript compilation errors

Run the following checks:

npm install
npx tsc --version
rm -rf dist
npm run build
Enter fullscreen mode Exit fullscreen mode

Pay particular attention to type mismatches in fetch responses. Add explicit response types or type assertions where needed.

Key Takeaways

  • MCP connects AI agents to external APIs. Build the server once and use it with Claude Code, Cursor, or another MCP-compatible client.
  • Three tools cover common API testing tasks. Use run_test for execution, validate_schema for OpenAPI validation, and list_environments for discovery.
  • Zod prevents invalid tool parameters. Schema definitions validate arguments before making API calls.
  • Client configuration is tool-specific. Claude Code uses ~/.claude/settings.json; Cursor uses .cursor/mcp.json.
  • Production deployments need additional safeguards. Add retries, rate limiting, structured error handling, and secure API key storage.

FAQ

What is MCP in AI?

MCP, or Model Context Protocol, is a standardized protocol that lets AI agents access external tools and data sources. It works like a plugin system for AI agents.

How do I create an MCP server for Apidog?

Install @modelcontextprotocol/sdk, define tools with Zod validation, implement handlers that call the Apidog API, and connect the server with StdioServerTransport.

Can I use this with Cursor?

Yes. Add the server configuration to .cursor/mcp.json in your project root. The same server works with Claude Code, Cursor, and other MCP clients.

What tools should I expose?

Start with:

  • run_test for executing test collections
  • validate_schema for OpenAPI validation
  • list_environments for fetching available environments

You can add tools for response comparison, test history, or mock server management as your workflow grows.

Is the Apidog MCP server production-ready?

The tutorial code is a starting point. Add retry logic, rate limiting, robust error handling, and secure API key storage before using it in production.

Do I need an Apidog API key?

Yes. Set APIDOG_API_KEY as an environment variable. The server reads it at runtime to authenticate API requests.

Can I share this MCP server with my team?

Yes. Publish it as a private npm package, document the required environment variables, and include example MCP configurations.

Top comments (0)