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

@mcpeak/perf

v0.1.0

Published

Split MCP tool call latency into external HTTP wait and in-server time

Readme

English · 한국어

@mcpeak/perf

Splits the time a single MCP tool call takes into external HTTP wait and in-server time, and tells you in plain sentences where changing the server code would make it shorter. From the CLI, use it as mcpeak perf.

Installation

npm install -g @mcpeak/cli

@mcpeak/cli installs this package as a dependency. Requires Node 22.18 or later.

Usage

mcpeak perf server.suite.json -- node ./server.mjs
mcpeak perf server.suite.json --tool get_forecast -- node ./server.mjs        # one tool only
mcpeak perf server.suite.json --repeat 10 --interval-ms 500 -- node ./server.mjs
mcpeak perf server.suite.json --baseline perf.baseline.json --update-baseline -- node ./server.mjs   # write a baseline
mcpeak perf server.suite.json --baseline perf.baseline.json -- node ./server.mjs                     # compare with it
mcpeak perf server.suite.json --json -- node ./server.mjs                     # result JSON

The first argument is the same suite file mcpeak test takes. Calling with real inputs a person approved is what makes the latency of the real code path show up. Only callTool cases that do not expect a rejection ({ "type": "isError", "expected": true }) are measured. Each case is called --repeat times in a row (A×5, B×5, …), and the report has one row per case with:

  • response time p50 and p95
  • external wait p50: the union of the intervals from the start of each HTTP request observed inside the server process to the end of its response body
  • in-server p50: the response time minus the external wait
  • external requests per call
  • cold start (launch plus handshake, first tools/list)

There are two diagnostic rules.

| Rule | What it looks for | |---|---| | perf/sequential-independent | Requests inside one call that do not depend on an earlier response are sent one after another. It also reports an upper bound on the time sending them in parallel could save | | perf/repeated-request | One call sends the same request more than once, or several cases with different inputs get the same response to the same request every time |

Below is an example after adding a compare_cities tool, which looks up three cities one after another, to examples/live-weather-server. The numbers are illustrative. The report is printed in Korean.

mcpeak perf 결과
서버 node examples/live-weather-server/server.mjs · 도구 11개 · 관측 Node 프리로드(fetch, node:http, http2, undici)
설정 반복 5회 · 호출 간격 0ms · 케이스 4개 (callTool 중 거절 기대 제외)
콜드 스타트 기동+핸드셰이크 312ms · tools/list 4ms

케이스                  n  응답 p50   p95  외부 대기 p50  서버 안 p50  외부 요청/호출
get-forecast-success    5       287   668            285            2               2
compare-cities-success  5      1310  1702           1301            9               6
lookup-country-success  5         1     3              0            1               0
convert-units-success   0                                                              측정 안 됨: isError 5회

진단 1건 (종료 코드에 영향 없음)
→ 'compare_cities' 호출 안에서 독립 요청 2건이 순차로 나갑니다 (5회 중 5회): GET geocoding-api.open-meteo.com/v1/search?name=…&count=…&language=… ×2
→ 병렬로 보내면 최대 약 867ms 줄일 수 있습니다 (p50 기준 상한).
→ Promise.all 로 한 번에 보내고, 상대 API 의 동시 요청 한도와 429 백오프를 함께 두세요.

기준선 비교 (perf.baseline.json)
→ 회귀 없음. 서버 안 p50 은 기준의 1.5배+20ms 안이고 외부 요청 수는 기준 이하입니다.

외부 대기는 서버 프로세스 안에서 관측한 HTTP 요청(fetch·node:http·http2·undici)의 시작부터 응답 본문 끝까지의 합집합입니다.
서버 안 시간은 응답 시간에서 그것을 뺀 나머지이고, 처리뿐 아니라 레이트리미터·타이머 대기, 첫 연결의 TLS, stdio 전송이 들어 있습니다.
수치는 이 컴퓨터와 이 네트워크에서 잰 값입니다. 전후 비교는 같은 조건에서 --baseline 으로 하세요.

The diagnostic says: inside a compare_cities call, 2 independent requests go out one after another (in 5 of 5 runs). Sending them in parallel could save at most about 867ms (upper bound at p50). Send them at once with Promise.all, together with the other API's concurrency limit and a 429 backoff.

get_forecast is a dependency chain that puts the coordinates from the geocoding response into the forecast request, so it gets no diagnostic. compare_cities gets one because the geocoding requests for the second and third cities do not depend on any earlier response yet are sent in sequence. 867ms is the external wait of 1301ms minus one city's chain (geocoding plus forecast, about 434ms).

Options

mcpeak perf <suite.json> (-- <executable> [args...] | --command <executable> [--arg <value> ...] [--env <NAME> ...])
            [--repeat <n>] [--interval-ms <ms>] [--tool <name> ...]
            [--baseline <path> [--update-baseline]] [--threshold-pct <n>] [--threshold-ms <n>] [--json]

| Option | Default | Meaning | |---|---|---| | <suite.json> | required | The same suite test takes. Uses the callTool cases that do not expect a rejection | | target options | required | --, or --command/--arg/--env. Same as mcpeak test. --url and --header-env are rejected | | --repeat <n> | 5 | Calls per case. An integer from 1 to 100 | | --interval-ms <ms> | 0 | Wait between calls. An integer from 0 to 60000. Shown in the report's settings line | | --tool <name> | none | Repeatable. Only the cases for that tool | | --baseline <path> | none | The baseline file. Compared against when given; written when combined with --update-baseline | | --update-baseline | off | Writes the baseline file from this measurement. Does not compare. Cannot be combined with --tool (the baseline is always rewritten from the whole suite) | | --threshold-pct <n> | 50 | The ratio part of the in-server regression threshold. An integer from 0 to 1000 | | --threshold-ms <n> | 20 | The floor of the in-server regression threshold. An integer from 0 to 60000 | | --json | off | Prints only the report JSON to stdout |

Options that take a value accept both --x v and --x=v. Every option except --tool is rejected when given twice. Options only test has (--junit, --repair-bundle, --determinism, --reset-cmd, --stderr-lines, --session, --record-session) are not accepted.

Because --interval-ms defaults to 0, a server with a rate limiter may show that wait as in-server time. Any positive default would pretend to know that server's limit, so the default is 0 and the settings line states it so the reader can judge.

A baseline comparison checks two things for every case present in both the baseline and this run.

  • In-server p50 above baseline × (1 + threshold-pct / 100) + threshold-ms is a regression. By default that is 1.5 times the baseline plus 20ms.
  • External requests per call (p50) going up by even one is a regression.

Response time and external wait are compared and shown, but not judged. External wait swings with the network regardless of the server code.

Exit codes

| Code | Meaning | |---|---| | 0 | Measurement finished. Still 0 when there are diagnostics | | 2 | Baseline regression (in-server time, external request count, or a case in the baseline that could not be measured this time) | | 1 | Usage error, connection failure, suite error, missing or malformed baseline file. Also 1, after the report is printed, when --baseline is given but no external calls could be observed |

A run where the call throws, times out, returns isError: true, or returns an empty response is left out of the latency statistics and counted separately. After a timeout the server may still be working on that call and would mix its requests into later measurements, so the remaining calls are skipped, the measurement ends, and the report says so.

Limitations

  • Remote (Streamable HTTP) servers are not measured. The observation code can only be loaded into a server process this tool starts, so --url is rejected.
  • Only Node servers are split. Observation is a preload loaded into the server process through NODE_OPTIONS=--import, which other runtimes such as Python do not pick up. For those, only response time and cold start are reported, and the report says nothing was observed.
  • Paths whose bodies are not visible cannot be judged for dependency. Direct node:http (axios, node-fetch, and so on), http2, and undici requests show their timing but not their bodies, so the sequential independence check counts them as "cannot judge". It errs toward no false warning.
  • In-server time is not "server processing". It also includes rate limiter and timer waits, DNS lookups and first connection TLS outside the observed channels, and stdio transport.
  • The numbers belong to that computer and that network. There is no mode yet that replays recorded external call durations to remove external variance. Compare before and after under the same conditions with --baseline.
  • It is not a load tool. It calls one at a time, so it does not measure throughput or concurrency limits. It does not fix diagnostics automatically either.

Reports and baseline files never carry request or response headers or bodies, and URLs only appear with query values masked as …. The same trace and the same measurements always produce byte-identical reports. The latency numbers themselves are measurements and differ from run to run.

The design and its evidence are in docs/2026-10-18-perf-지연-분해-설계.md and ADR-0118 (written in Korean).