MCP Stress Tester avatar
MCP Stress Tester

Pricing

Pay per usage

Go to Store
MCP Stress Tester

MCP Stress Tester

Developed by

Jakub Kopecký

Jakub Kopecký

Maintained by Community

A simple MCP Stress Tester client Actor for stress-testing your Model Context Protocol server. 💻⚡

0.0 (0)

Pricing

Pay per usage

0

Total users

1

Monthly users

1

Runs succeeded

0%

Last modified

17 days ago

.dockerignore

# configurations
.idea
.vscode
# crawlee and apify storage folders
apify_storage
crawlee_storage
storage
# installed files
node_modules
# git folder
.git
# dist folder
dist

.gitignore

# This file tells Git which files shouldn't be added to source control
.idea
.vscode
.zed
storage
apify_storage
crawlee_storage
node_modules
dist
tsconfig.tsbuildinfo
storage/*
!storage/key_value_stores
storage/key_value_stores/*
!storage/key_value_stores/default
storage/key_value_stores/default/*
!storage/key_value_stores/default/INPUT.json
# Added by Apify CLI
.venv

biome.json

{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all"
}
}
}

package.json

{
"name": "mcp-stress-tester",
"version": "0.0.1",
"type": "module",
"description": "This is an example of an Apify actor.",
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.3",
"apify": "^3.4.1"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.1",
"@biomejs/biome": "1.9.4",
"biome": "^0.3.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"scripts": {
"start": "npm run start:dev",
"start:prod": "node dist/main.js",
"start:dev": "tsx src/main.ts",
"build": "tsc",
"format": "npx biome format",
"lint": "npx biome lint",
"test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1"
},
"author": "It's not you it's me",
"license": "ISC"
}

tsconfig.json

{
"extends": "@apify/tsconfig",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "dist",
"noUnusedLocals": false,
"skipLibCheck": true,
"lib": ["DOM"]
},
"include": [
"./src/**/*"
]
}

.actor/Dockerfile

# Specify the base Docker image. You can read more about
# the available images at https://docs.apify.com/sdk/js/docs/guides/docker-images
# You can also use any other image from Docker Hub.
FROM apify/actor-node:20 AS builder
# Check preinstalled packages
RUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json
# to speed up the build using Docker layer cache.
COPY package*.json ./
# Install all dependencies. Don't audit to speed up the installation.
RUN npm install --include=dev --audit=false
# Next, copy the source files using the user set
# in the base image.
COPY . ./
# Install all dependencies and build the project.
# Don't audit to speed up the installation.
RUN npm run build
# Create final image
FROM apify/actor-node:20
# Check preinstalled packages
RUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json
# to speed up the build using Docker layer cache.
COPY package*.json ./
# Install NPM packages, skip optional and development dependencies to
# keep the image small. Avoid logging too much and print the dependency
# tree for debugging
RUN npm --quiet set progress=false \
&& npm install --omit=dev --omit=optional \
&& echo "Installed NPM packages:" \
&& (npm list --omit=dev --all || true) \
&& echo "Node.js version:" \
&& node --version \
&& echo "NPM version:" \
&& npm --version \
&& rm -r ~/.npm
# Copy built JS files from builder image
COPY --from=builder /usr/src/app/dist ./dist
# Next, copy the remaining files and directories with the source code.
# Since we do this after NPM install, quick build will be really fast
# for most source file changes.
COPY . ./
# Create and run as a non-root user.
RUN adduser -h /home/apify -D apify && \
chown -R apify:apify ./
USER apify
# Run the image.
CMD npm run start:prod --silent

.actor/actor.json

{
"actorSpecification": 1,
"name": "mcp-stress-tester",
"title": "Scrape single page in TypeScript",
"description": "Scrape data from single page with provided URL.",
"version": "0.0",
"meta": {
"templateId": "ts-start"
},
"input": "./input_schema.json",
"dockerfile": "./Dockerfile"
}

.actor/input_schema.json

{
"title": "MCP Stress Tester",
"type": "object",
"schemaVersion": 1,
"properties": {
"target": {
"title": "Target URL",
"type": "string",
"description": "Specify the target URL",
"editor": "textfield"
},
"sse": {
"title": "Enable SSE",
"type": "boolean",
"description": "Enable SSE switch",
"default": false,
"editor": "checkbox"
},
"clients": {
"title": "Number of clients",
"type": "integer",
"description": "Number of clients to create",
"default": 10,
"minimum": 1,
"editor": "number"
},
"clientsCreationBatchSize": {
"title": "Clients creation batch size",
"type": "integer",
"description": "Number of clients to create in each batch",
"default": 5,
"minimum": 1,
"editor": "number"
},
"opsRate": {
"title": "Operations rate",
"type": "integer",
"description": "Number of operations per minute",
"default": 30,
"minimum": 1,
"editor": "number"
},
"maxRetries": {
"title": "Maximum retries",
"type": "integer",
"description": "Maximum number of retries for client creation",
"default": 5,
"minimum": 0,
"editor": "number"
},
"initialBackoffMs": {
"title": "Initial backoff (ms)",
"type": "integer",
"description": "Initial backoff time in milliseconds",
"default": 100,
"minimum": 1,
"editor": "number"
},
"maxBackoffMs": {
"title": "Maximum backoff (ms)",
"type": "integer",
"description": "Maximum backoff time in milliseconds",
"default": 10000,
"minimum": 1,
"editor": "number"
},
"backoffFactor": {
"title": "Backoff factor",
"type": "integer",
"description": "Factor by which to increase backoff time after each retry",
"default": 2,
"minimum": 1,
"editor": "number"
}
},
"required": ["target"]
}

src/main.ts

1import { Actor, log } from "apify";
2import {
3 type Client,
4 ClientOptions,
5} from "@modelcontextprotocol/sdk/client/index.js";
6import { createClient } from "./utils.js";
7
8await Actor.init();
9
10interface Input {
11 target: string;
12 sse: boolean;
13 clients: number;
14 clientsCreationBatchSize: number;
15 opsRate: number;
16 maxRetries: number;
17 initialBackoffMs: number;
18 maxBackoffMs: number;
19 backoffFactor: number;
20}
21// Structure of input is defined in input_schema.json
22const input = await Actor.getInput<Input>();
23if (!input) throw new Error("Input is missing!");
24const {
25 target: targetUrl,
26 sse: sseEnabled,
27 clients: numClients,
28 clientsCreationBatchSize,
29 opsRate,
30 maxRetries,
31 initialBackoffMs,
32 maxBackoffMs,
33 backoffFactor,
34} = input;
35
36const a = 1;
37
38log.info(`Starting benchmark with the following configuration:
39- Target URL: ${targetUrl}
40- SSE Enabled: ${sseEnabled}
41- Number of Clients: ${numClients}
42- Clients Creation Batch Size: ${clientsCreationBatchSize}
43- Operations Per Minute: ${opsRate}
44- Max Retries: ${maxRetries}
45- Initial Backoff: ${initialBackoffMs}ms
46- Max Backoff: ${maxBackoffMs}ms
47- Backoff Factor: ${backoffFactor}
48`);
49
50const numBatches = Math.floor(numClients / clientsCreationBatchSize);
51
52const clients: Client[] = [];
53for (let i = 0; i < numBatches; i++) {
54 const batch = await Promise.all(
55 Array.from({ length: clientsCreationBatchSize }, () =>
56 createClient({
57 targetUrl,
58 sseEnabled,
59 token: process.env.APIFY_TOKEN as string,
60 maxRetries,
61 initialBackoffMs,
62 maxBackoffMs,
63 backoffFactor,
64 }),
65 ),
66 );
67 log.info(
68 `Created client batch ${i + 1} of ${Math.ceil(numClients / clientsCreationBatchSize)}`,
69 );
70 clients.push(...batch);
71}
72if (numClients % clientsCreationBatchSize !== 0) {
73 const remainingClients = numClients % clientsCreationBatchSize;
74 const batch = await Promise.all(
75 Array.from({ length: remainingClients }, () =>
76 createClient({
77 targetUrl,
78 sseEnabled,
79 token: process.env.APIFY_TOKEN as string,
80 maxRetries,
81 initialBackoffMs,
82 maxBackoffMs,
83 backoffFactor,
84 }),
85 ),
86 );
87 log.info(`Created final client batch of ${remainingClients}`);
88 clients.push(...batch);
89}
90
91log.info(
92 `Successfully created ${clients.length} clients. Starting benchmark operations...`,
93);
94
95const startTime = Date.now();
96let operationsCompleted = 0;
97let operationsFailed = 0;
98
99const intervalIDs = clients.map((_, index) => {
100 return setInterval(async () => {
101 try {
102 await clients[index].listTools(undefined, {
103 timeout: 60000,
104 });
105 operationsCompleted++;
106 if (operationsCompleted % 100 === 0) {
107 const elapsedSeconds = (Date.now() - startTime) / 1000;
108 const opsPerSec = operationsCompleted / elapsedSeconds;
109 log.info(
110 `Completed ${operationsCompleted} operations (${(opsPerSec).toFixed(2)} ops/sec), failed: ${operationsFailed}`,
111 );
112 }
113 } catch (error) {
114 operationsFailed++;
115 log.error(`Error listing tools for client ${index}: ${error}`);
116 }
117 }, 60000 / opsRate);
118});
119Actor.on("aborting", async () => {
120 log.info("Received abort. Closing clients...");
121 for (const id of intervalIDs) {
122 clearInterval(id);
123 }
124
125 const elapsedSeconds = (Date.now() - startTime) / 1000;
126 log.info(`
127Benchmark Summary:
128- Total operations completed: ${operationsCompleted}
129- Total operations failed: ${operationsFailed}
130- Average throughput: ${(operationsCompleted / elapsedSeconds).toFixed(2)} ops/sec
131- Total runtime: ${elapsedSeconds.toFixed(2)} seconds
132`);
133
134 await Promise.all(clients.map((client) => client.close()))
135 .then(() => {
136 log.info("All clients closed.");
137 process.exit(0);
138 })
139 .catch((error) => {
140 log.error("Error closing clients:", error);
141 process.exit(1);
142 });
143});
144
145while (true) {
146 await new Promise((resolve) => setTimeout(resolve, 1000));
147}
148
149// Gracefully exit the Actor process. It's recommended to quit all Actors with an exit().
150await Actor.exit();

src/utils.ts

1import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
5export type CreateClientOptions = {
6 targetUrl: string;
7 sseEnabled: boolean;
8 token: string;
9 maxRetries?: number;
10 initialBackoffMs?: number;
11 maxBackoffMs?: number;
12 backoffFactor?: number;
13};
14
15/**
16 * Sleep for the specified number of milliseconds
17 */
18const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
19
20/**
21 * Create a client with exponential backoff retry logic
22 */
23export async function createClient(options: CreateClientOptions): Promise<Client> {
24 const {
25 targetUrl,
26 sseEnabled,
27 token,
28 maxRetries = 5,
29 initialBackoffMs = 100,
30 maxBackoffMs = 10000,
31 backoffFactor = 2,
32 } = options;
33
34 let retries = 0;
35 let backoffMs = initialBackoffMs;
36
37 while (true) {
38 try {
39 let transport: StreamableHTTPClientTransport | SSEClientTransport;
40 if (sseEnabled) {
41 transport = new SSEClientTransport(
42 new URL(targetUrl),
43 {
44 requestInit: {
45 headers: {
46 Authorization: `Bearer ${token}`,
47 }
48 },
49 eventSourceInit: {
50 // The EventSource package augments EventSourceInit with a "fetch" parameter.
51 // You can use this to set additional headers on the outgoing request.
52 // Based on this example: https://github.com/modelcontextprotocol/typescript-sdk/issues/118
53 async fetch(input: Request | URL | string, init?: RequestInit) {
54 const headers = new Headers(init?.headers || {});
55 headers.set('authorization', `Bearer ${token}`);
56 return fetch(input, { ...init, headers });
57 },
58 // We have to cast to "any" to use it, since it's non-standard
59 } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
60 }
61 );
62 } else {
63 transport = new StreamableHTTPClientTransport(
64 new URL(targetUrl),
65 {
66 requestInit: {
67 headers: {
68 Authorization: `Bearer ${token}`,
69 }
70 },
71 }
72 );
73 }
74
75 const client = new Client({
76 name: 'benchmark-client',
77 version: '1.0.0',
78 });
79
80 await client.connect(transport);
81 return client;
82 } catch (error) {
83 retries++;
84 if (retries > maxRetries) {
85 console.error(`Failed to create client after ${maxRetries} retries:`, error);
86 throw error;
87 }
88
89 // Add jitter to avoid thundering herd problem
90 const jitter = Math.random() * 0.3 + 0.85; // Random value between 0.85 and 1.15
91 const actualBackoff = Math.min(backoffMs * jitter, maxBackoffMs);
92
93 console.warn(`Client creation failed, retrying in ${Math.round(actualBackoff)}ms (attempt ${retries}/${maxRetries})`);
94 await sleep(actualBackoff);
95
96 // Increase backoff for next attempt
97 backoffMs = Math.min(backoffMs * backoffFactor, maxBackoffMs);
98 }
99 }
100}