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.
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 └─────────────┘
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
fetchsupport - 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
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"]
}
Add build and start scripts to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Create the source directory:
mkdir src
touch src/index.ts
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);
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)
}`
}
]
};
}
}
);
Each MCP tool has three important parts:
- Name — the identifier the agent uses to select the tool
- Schema — Zod definitions for validating arguments
- 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)
}`
}
]
};
}
}
);
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)
}`
}
]
};
}
}
);
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
The compiled server should now be available at dist/index.js.
You can launch it directly:
node dist/index.js
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");
Run it with:
node test-client.js
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"
}
}
}
}
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
Validate this OpenAPI schema against Apidog rules:
[paste schema]
List all environments for project proj_12345
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"
}
}
}
}
In Cursor, invoke the server like this:
@apidog run_test projectId="proj_12345" environmentId="staging"
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);
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
Expected output:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "run_test",
"description": "...",
"inputSchema": {}
},
{
"name": "validate_schema",
"description": "...",
"inputSchema": {}
},
{
"name": "list_environments",
"description": "...",
"inputSchema": {}
}
]
}
}
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
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
Also verify that:
- The path in
~/.claude/settings.jsonis absolute - Node is available in the environment used by Claude Code
-
dist/index.jsexists - 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_KEYis 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
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
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_testfor execution,validate_schemafor OpenAPI validation, andlist_environmentsfor 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_testfor executing test collections -
validate_schemafor OpenAPI validation -
list_environmentsfor 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)