npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jmeter-mcp-server

v0.3.1

Published

Stdio MCP server to build, run and read reports for JMeter test plans

Readme

jmeter-mcp-server

npm version CI Node.js TypeScript License: MIT

Give an LLM real, deterministic control over Apache JMeter — build test plans, run real load tests, and read back real results, without ever hand-writing .jmx XML or opening the GUI.

"Load test my API: 20 users hitting POST /orders for 2 minutes,
 5% think time, fail anything over 800ms"

...turns into a running JMeter test and a real report, through typed tool calls an MCP client (Claude Code, Claude Desktop, etc.) makes directly.

Why not just ask an LLM to write the .jmx itself?

It can — a .jmx is just XML, and any capable model has seen plenty of JMeter test plans. The problem is how it fails: JMeter's format is a hashTree with dozens of fragile, easy-to-misremember details — exact guiclass/testclass pairs, property names that don't match their GUI label (ThreadGroup.num_threads is a stringProp, not an intProp), integer bitmasks for assertion match types, strict parent/child pairing with sibling <hashTree> tags. None of it is self-checking. A wrong value still produces valid, loadable XML that just quietly does the wrong thing.

That's not hypothetical — it happened building this project. An early version of the If Controller generated a property called useExpression set to true, which reads like "yes, evaluate my condition." The real JMeter source does the opposite: useExpression=true means don't evaluate it as an expression — just check if the string is literally "true". Every non-trivial condition silently, permanently failed. No error, no warning — the child sampler just never ran. It only surfaced by actually executing the generated plan against real JMeter and noticing a sample count of zero.

That's the whole case for this server in one story: an LLM regenerating XML from memory re-risks that exact mistake on every single request. This server encodes the correct shape once, in a serializer (and a matching parser for the reverse direction) checked against real JMeter source and bundled examples, exercised by 166 automated tests including real JMeter runs — and exposes it as typed tools instead. Concretely:

  • Correctness through one tested code path, not regenerated-from-memory XML every time.
  • Cheap incremental edits. Plans are a small JSON tree with stable node ids — adding, removing, renaming, moving, or disabling an element is one tool call by id, not rewriting a whole .jmx file. An existing .jmx (hand-written or exported from the GUI) can be imported and edited the same way.
  • Aggregated results, not raw samples. get_execution_report returns computed stats (error %, avg/median/p90/p95/p99, throughput) — not thousands of sample rows to average by hand.
  • Real async execution. execute_test_plan returns immediately with an executionId; long-running load tests never block anything.

The generated .jmx is standard JMeter output — open it in the real GUI any time.

Example

You:    Build a load test: 10 users for 30s hitting GET https://api.example.com/health,
        fail anything that takes over 500ms, then run it and tell me the p95.

Claude: [create_test_plan, add_thread_group, add_http_sampler, add_duration_assertion,
         add_aggregate_report_listener, execute_test_plan, get_execution_status, get_execution_report]

        Ran 300 requests over 30s, 0 failures. p95 latency: 214ms, avg: 187ms, throughput: 10.1 req/s.

Every step above is a real typed MCP tool call — see Tools for the full set (34 element types across samplers, controllers, timers, extractors, assertions, and listeners, plus editing, inspection, and .jmx import/export tools) and Example workflow for the raw call sequence.

Quick start

claude mcp add jmeter \
  -e JMETER_HOME=/opt/homebrew/opt/jmeter/libexec \
  -- npx -y jmeter-mcp-server

That's it — no cloning, no build. Adjust JMETER_HOME to your JMeter install (see Prerequisites). Full setup details, Claude Desktop config, and local-dev instructions are in Adding this server to Claude Code.

How a test plan is represented

Each plan is a JSON tree ({id, type, props, children[]}), not XML text. Authoring tools append a child under a given parentId, editing tools (remove_element, update_element, move_element, etc.) mutate that same tree in place, and the tree is only serialized to a real .jmx file on demand (get_test_plan_xml) or at execution time. import_test_plan runs the reverse direction, parsing an existing .jmx back into this same tree shape. This is what makes incremental edits cheap and keeps all the fiddly XML schema knowledge in two places (src/jmx/serializer.ts for tree → XML, src/jmx/parser.ts for XML → tree, sharing prop shapes from src/jmx/propTypes.ts) instead of spread across every tool.

Tools

Authoring (each returns the new node's id, used as parentId for whatever you attach under it next) — grouped the same way JMeter's own right-click Add menu groups them, so if you already know the GUI, you already know where to look:

| Tool | Adds | |---|---| | create_test_plan | Root TestPlan node — returns planId and the root node id |

Threads (Users):

| Tool | Adds | |---|---| | add_thread_group | Thread Group (virtual users) | | add_setup_thread_group | setUp Thread Group (runs once before all Thread Groups) | | add_teardown_thread_group | tearDown Thread Group (runs once after all Thread Groups) |

Sampler:

| Tool | Adds | |---|---| | add_http_sampler | HTTP Request sampler | | add_jdbc_request | JDBC Request sampler | | add_jsr223_sampler | JSR223 Sampler (Groovy/BeanShell/JS/JEXL script as the sample) | | add_ftp_request | FTP Request sampler | | add_tcp_sampler | TCP Sampler |

Logic Controller:

| Tool | Adds | |---|---| | add_transaction_controller | Transaction Controller (groups child samplers into one named transaction) | | add_loop_controller | Loop Controller (repeats child samplers) | | add_if_controller | If Controller (conditionally runs child samplers) | | add_while_controller | While Controller (repeats children while a condition holds) | | add_random_controller | Random Controller (runs one random child per pass) | | add_interleave_controller | Interleave Controller (alternates through children) |

Config Element:

| Tool | Adds | |---|---| | add_csv_data_set | CSV Data Set Config (parameterization from a file) | | add_user_defined_variables | User Defined Variables | | add_jdbc_connection_configuration | JDBC Connection Configuration (pooled datasource) | | add_http_request_defaults | HTTP Request Defaults | | add_cookie_manager | HTTP Cookie Manager | | add_header_manager | HTTP Header Manager |

Timer:

| Tool | Adds | |---|---| | add_constant_timer | Constant Timer (pacing/think-time) | | add_uniform_random_timer | Uniform Random Timer (randomized pacing) | | add_constant_throughput_timer | Constant Throughput Timer (target rate pacing) |

Pre Processors:

| Tool | Adds | |---|---| | add_jsr223_preprocessor | JSR223 PreProcessor | | add_user_parameters | User Parameters (per-thread variable value sets) |

Post Processors:

| Tool | Adds | |---|---| | add_json_extractor | JSON Extractor post-processor | | add_regex_extractor | Regular Expression Extractor post-processor | | add_xpath_extractor | XPath Extractor post-processor | | add_jsr223_postprocessor | JSR223 PostProcessor |

Assertions:

| Tool | Adds | |---|---| | add_response_assertion | Response Assertion | | add_json_assertion | JSON Assertion (JSONPath validation) | | add_duration_assertion | Duration Assertion (response-time SLA) | | add_size_assertion | Size Assertion (response byte-size check) |

Listener:

| Tool | Adds | |---|---| | add_aggregate_report_listener | Aggregate Report listener | | add_summary_report_listener | Summary Report listener | | add_view_results_tree_listener | View Results Tree listener (full request/response capture for debugging) | | add_backend_listener | Backend Listener (streams live metrics to InfluxDB/Graphite/etc.) |

Editing (mutate an already-built plan):

| Tool | Purpose | |---|---| | remove_element | Remove an element (and its subtree); rejects removing the root TestPlan node | | update_element | Shallow-merge (or replace) a node's props; a prop value of null deletes that key. Validated against the node's type when known | | rename_element | Rename an element's testname | | move_element | Move an element (and its subtree) to a new parent, optionally at a specific index; rejects moving a node into its own subtree | | reorder_children | Reorder a node's direct children (must pass an exact permutation of the current children) | | set_element_enabled | Enable/disable an element without removing it |

Inspection:

| Tool | Purpose | |---|---| | list_test_plans | List every plan in the workspace | | get_test_plan | Full element tree of a plan, including every node's id | | get_test_plan_xml | Serialize a plan to its JMeter .jmx XML, without running JMeter | | import_test_plan | Import an externally authored .jmx (e.g. exported from the JMeter GUI) as a new plan. Element types this server doesn't model are kept as opaque UnknownElement nodes instead of being dropped |

Execution & reporting (async — a run happens in the background):

| Tool | Purpose | |---|---| | execute_test_plan | Serialize to .jmx and run JMeter in non-GUI mode; returns { executionId } immediately | | get_execution_status | running / completed / failed, plus a tail of the JMeter log | | stop_execution | Send SIGTERM to a running JMeter process | | get_execution_report | Aggregated stats (per label + overall) parsed from the run's JTL output |

Example workflow

create_test_plan            → { planId, rootNodeId }
add_thread_group             (parentId: rootNodeId)  → { nodeId: threadGroupId }
add_http_sampler              (parentId: threadGroupId) → { nodeId: samplerId }
add_response_assertion        (parentId: samplerId)
add_aggregate_report_listener (parentId: threadGroupId)
execute_test_plan             (planId) → { executionId }
get_execution_status           (executionId)   ← poll until "completed"
get_execution_report            (executionId) → aggregated latency/error stats

Testing

166 automated tests, no framework beyond Node's built-in test runner:

npm test               # 155 tests: tree-mutation and XML-shape unit tests, XML -> tree parsing,
                        # serialize -> parse round-trips, and every tool called over the real MCP
                        # protocol (stdio, the same way Claude Code/Desktop talk to it) - no
                        # JMeter install needed, fully hermetic
npm run test:integration  # 11 tests: real JMeter runs - the If Controller story above, While
                        # Controller loop counts, timer pacing, extractors, assertions, etc.
                        # (needs JMETER_HOME)
npm run test:all

npm test spawns the actual built server (dist/index.js) via StdioClientTransport and drives it exactly as a real client would — not just calling internal functions — so a broken tool schema or a malformed response shows up as a real protocol error, not a passing unit test.

Both suites run on every push and pull request via GitHub Actions — the integration job installs a real JMeter binary on the runner, so it's exercising the same code path as a local run, not a mock.

Prerequisites

  • Node.js 18+
  • JMeter installed locally, with the JMETER_HOME environment variable pointing at the installation directory (the one containing bin/jmeter). On macOS via Homebrew, brew install jmeter puts it at /opt/homebrew/opt/jmeter/libexec.

Adding this server to Claude Code

Via npx (recommended — published on npm)

No cloning or building required; npx fetches and runs the published version on the fly:

claude mcp add jmeter \
  -e JMETER_HOME=/opt/homebrew/opt/jmeter/libexec \
  -- npx -y jmeter-mcp-server

Adjust the JMETER_HOME path to wherever JMeter is installed on your machine. Optionally set JMETER_MCP_WORKSPACE too (see below) if you want plans and executions stored somewhere other than the default.

The default scope is local (this project directory only). To make it available across every project, add -s user:

claude mcp add jmeter -s user \
  -e JMETER_HOME=/opt/homebrew/opt/jmeter/libexec \
  -- npx -y jmeter-mcp-server

Confirm it registered and is responding:

claude mcp list

From a local clone (development)

If you're working on this repository's code instead of using the published package, point at the built dist/index.js directly:

npm install
npm run build
claude mcp add jmeter \
  -e JMETER_HOME=/opt/homebrew/opt/jmeter/libexec \
  -- node /absolute/path/to/jmeter-mcp-server/dist/index.js

Claude Desktop

Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "jmeter": {
      "command": "npx",
      "args": ["-y", "jmeter-mcp-server"],
      "env": {
        "JMETER_HOME": "/opt/homebrew/opt/jmeter/libexec"
      }
    }
  }
}

Note: unlike a terminal-launched app, Claude Desktop does not inherit environment variables exported in your shell profile (.zshrc, etc.) — only true system-wide ones. Always set JMETER_HOME explicitly in the env block above rather than relying on it already being "set on your machine".

Environment variables

| Variable | Required | Purpose | |---|---|---| | JMETER_HOME | Yes | JMeter installation directory (must contain bin/jmeter) | | JMETER_MCP_WORKSPACE | No | Where plans and executions are stored. Defaults to ./jmeter-workspace relative to wherever the server process starts |

Workspace layout

<workspace>/
  plans/<planId>/plan.json           # JSON tree — source of truth for a plan
  executions/<executionId>/
    generated.jmx                    # serialized at execute_test_plan time
    aggregate-report.jtl             # output of the Aggregate Report listener, if present
    summary-report.jtl               # output of the Summary Report listener, if present
    jmeter.log
    meta.json                        # execution status, pid, timestamps, exit code

Editing and importing plans

Beyond the add_* authoring tools, a plan can be mutated after the fact (remove_element, update_element, rename_element, move_element, reorder_children, set_element_enabled) and an externally authored .jmx (e.g. exported from the JMeter GUI) can be brought in with import_test_plan. import_test_plan understands the most common element types (thread groups, HTTP samplers, assertions, extractors, controllers, config elements, the three report listeners, etc.); anything it doesn't recognize is kept as an opaque UnknownElement node whose original XML is preserved and re-emitted as-is by get_test_plan_xml/execute_test_plan, instead of being dropped - import_test_plan's response reports unknownElementCount/ unknownElementTypes so you know what wasn't fully understood. Coverage can be extended incrementally in src/jmx/parser.ts.

v1 scope

Not yet supported (candidates for a future release): generating the HTML dashboard report (-e -o), parent-type validation on add_*/move_element/ import_test_plan (nothing stops attaching an element under a semantically wrong parent), distributed execution.

Note on add_csv_data_set: the filename must be an absolute path. execute_test_plan runs JMeter from a fresh per-execution directory, so a relative path (which JMeter's GUI would resolve against the .jmx file's own location) won't resolve there. An absolute path baked into a plan is also machine-specific — it won't travel if you share plan.json with someone on a different machine. This is only checked at creation time: add_csv_data_set rejects a relative or nonexistent path up front, but later changing a CSVDataSet's filename via update_element, or importing a .jmx that already has a relative one via import_test_plan, is not checked - it will only surface as a failure at execute_test_plan time.

Note on add_jdbc_request/add_jdbc_connection_configuration, add_ftp_request, and add_backend_listener: these generate correct, JMeter-loadable XML, but exercising them for real needs infrastructure this project doesn't provide (a database, an FTP server, an InfluxDB/Graphite instance) — they were verified structurally, not against a real backend.

Note on add_view_results_tree_listener's captureFullData option: it has no effect right now. execute_test_plan always runs JMeter with -Jjmeter.save.saveservice.output_format=csv, and JMeter's CSV writer never emits response body/header columns no matter what the SampleSaveConfiguration flags say — only its XML output format can carry full response bodies. The option is wired up correctly in the generated .jmx (verified: the flags really do flip in the XML) for the day this server supports XML-format runs, but until then it's a no-op — confirmed by running a real capture and checking the resulting JTL has no responseData/samplerData/ requestHeaders/responseHeaders columns regardless of the setting.

Note on add_tcp_sampler: server/port/request are live-verified. The numeric fields (connectTimeoutMs, timeoutMs) are rendered as stringProp following this project's general convention for sampler numeric fields, but that specific choice for TCPSampler wasn't confirmed against a real JMeter-GUI-saved example (none was available to check against) — flagging in case a real save turns out to expect intProp.

License

MIT