
MCP Stress Tester
Pricing
Pay per usage
Go to Store

MCP Stress Tester
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 foldersapify_storagecrawlee_storagestorage
# installed filesnode_modules
# git folder.git
# dist folderdist
.gitignore
# This file tells Git which files shouldn't be added to source control
.idea.vscode.zedstorageapify_storagecrawlee_storagenode_modulesdisttsconfig.tsbuildinfostorage/*!storage/key_value_storesstorage/key_value_stores/*!storage/key_value_stores/defaultstorage/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 packagesRUN 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 imageFROM apify/actor-node:20
# Check preinstalled packagesRUN 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 debuggingRUN 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 imageCOPY /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.json22const 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}ms46- Max Backoff: ${maxBackoffMs}ms47- 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/sec131- Total runtime: ${elapsedSeconds.toFixed(2)} seconds132`);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 milliseconds17 */18const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));19
20/**21 * Create a client with exponential backoff retry logic22 */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/11853 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-standard59 } as any, // eslint-disable-line @typescript-eslint/no-explicit-any60 }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 problem90 const jitter = Math.random() * 0.3 + 0.85; // Random value between 0.85 and 1.1591 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 attempt97 backoffMs = Math.min(backoffMs * backoffFactor, maxBackoffMs);98 }99 }100}