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

mcp-server-inspector

v0.1.1

Published

Health-check any MCP server before you trust it: does it actually run, what does it really expose, how alive is it, and what did we NOT test.

Readme

mcp-server-inspector

English | 中文

在把一个 MCP 服务器接进你的 agent 之前,先搞清楚你到底知道它什么。

MCP 生态现在有十几万个服务器(见下方「生态数据与出处」),而决定装哪一个的依据,通常是一段 README。README 描述的是意图。当你把一个服务器接进持有你凭证的 agent 时,真正要紧的是它跑起来会做什么。这两件事不一致的频率足够高,所以唯一靠谱的办法是实测。

mcp-server-inspector 做一件事:真连上去,然后如实报告。

npx mcp-server-inspector @modelcontextprotocol/server-filesystem

它是体检报告,不是目录

这一条要说在最前面,因为很容易被混为一谈。

| | 做什么 | 不做什么 | |---|---|---| | Registry(注册表) | 收录、检索、分发服务器包 | 不告诉你这个包跑起来会怎样 | | mcp-server-inspector | 启动它、跟它握手、看它真实暴露什么、与它自己宣称的对照 | 不收录、不检索、不替你决定装哪个 |

Registry 回答「有哪些」,mcp-server-inspector 回答「这一个到底是什么情况」。两者是互补的,不是竞争关系。

它也不是安全审计。理由在下面「我们没测什么」里说清楚了——那一节是这份文档里最重要的一节。


它做三件事

1. 真连上去(不是读文档猜)

按 stdio 传输规范走完整握手:

spawn 子进程
  → initialize                                        (协议版本 2025-11-25)
  → notifications/initialized
  → tools/list                                        (跟着 nextCursor 翻页翻到底)
  → 终止子进程

翻页这件事本身就是必须做对的:只读第一页会少报工具数,而「少报」恰恰是本工具存在的理由,所以它自己绝不能犯这个错。

2. 读它的静态声明

读 package.json 和 README:依赖数量、安装期脚本、许可证、仓库地址、发版时间、维护者数量。

安装期脚本只报告,不执行。这一点没有例外。

而且这里必须区分两类脚本,因为它们的威胁模型完全不同:

| 类别 | 脚本 | 在谁的机器上跑 | 权重 | |---|---|---|---| | 消费端 | preinstall / install / postinstall | 使用者 npm install 时自动执行 | −20(risk) | | 维护端 | prepare / prepublishOnly | 维护者自己的构建/发布流程 | −1(info,仅披露) |

一开始把两者混为一谈,结果是几乎每个 TypeScript 包都被判高危。实测在官方的 @modelcontextprotocol/server-everything 上,光这一个混淆就吃掉 20 分,把 MCP 项目自己的参考实现从 usable 压到 caution(50/100,exit 1)。prepare 是 TypeScript 项目的标准构建步骤,它不在使用者安装时执行,所以不是消费端执行面。拆开之后同一服务器回到 74/100、exit 0。

3. 把差距写成报告

报告固定分三段:

一、事实      可复现的观测,谁跑都一样
二、判断      基于上面事实的结论,可以不同意
三、没测什么  无条件打印

第三段永远打印,哪怕分数满分。


为什么失败模式必须可区分

这是本工具最重要的工程决定。

探测一个服务器会怎么失败?实践中大多数失败不是协议错误,而是挂起、崩溃、输出根本不是 JSON。一个统一的「failed」标签会把最有用的信息抹掉——因为「它挂住了」和「它立刻就退了」指向完全不同的修法。

所以探测结果有 7 种状态,每种都是独立标签:

| 状态 | 含义 | 通常意味着 | |---|---|---| | ok | 握手完成 | 可用 | | no-handshake | 启动了,但 initialize 没有合法响应 | 协议实现不对;客户端接不上 | | no-tools | 握手正常,但 0 个工具 | 宣称的能力不存在 | | crash | 响应完成前自行退出 | 缺环境变量、缺依赖、启动即崩 | | timeout | 没退出,也没回答 | 死锁、等输入、阻塞 | | not-json | stdout 不是合法协议帧 | 往协议通道里打了日志/banner | | spawn-error | 进程根本没能拉起来 | 二进制不存在、路径错、权限不对 |

crash 和 spawn-error 必须分开,而且这里踩过一个真实的坑:在 Windows 上,当二进制不存在时,Node 仍然会往 child.exitCode 里塞一个错误码(实测 -4058,即 ENOENT)。如果照直读那个值,一个「命令写错了」会被报成「服务器崩溃了」——修法完全不同。所以本工具把启动失败锁存下来,任何下游观测都不能覆盖它,并且把 exitCode 归零为 null(一个从未存在过的进程没有退出码)。


计时要量的是服务器,不是 npm

这一节记录一次被实测推翻的假设,因为推翻的过程本身说明了「先测再改」为什么重要。

问题是:谁在为启动买单

本工具主要通过 npx 启动目标。曾观察到同一个服务器经 npm 启动要 3.3 秒,直接 node 启动只要 0.24 秒。如果拿墙钟(elapsedMs)去判断服务器快慢,那 3 秒其实是 npm 的启动开销,却被记在了服务器账上——而评分里有一条「握手很快 +4」,npm 启动的服务器永远拿不到,healthy 档事实上对最常被安装的那批服务器不可达。

第一个假设,以及它为什么错了

假设:npm 的开销发生在我们写入 initialize 之前,所以只要在写入的那一刻开始计时,就能把 npm 排除掉。

于是给探针加了 requestWrittenAt,从写入时刻算到响应。实测(npx -y @modelcontextprotocol/server-sequential-thinking):

     21ms  spawn 返回
    139ms  initialize 已写入
   5963ms  响应到达

写入确实发生在 139ms,响应在 5963ms。filesystem 服务器更清楚——它自己的启动横幅出现在 4864ms,响应在 4881ms。

结论:假设错了。 npm 的引导不是「写入之前的前奏」,我们的写入被缓冲进了它的启动过程,整段开销都落在这一轮往返里面。所以「从写入开始计时」和「量墙钟」得到的是同一个数字(实测 12098ms vs 12144ms),什么都没修好。

正确的做法:按启动路径做算术

唯一可靠的分界线是启动方式。同一个构建产物,两种启动方式各跑三次:

经 npx -y <pkg>     3445, 3399, 3502ms   平均 3449ms
经 node <同一个文件>  777,  717,  741ms   平均  745ms
                                     差        2704ms

这 2704ms 是 npm,不是服务器,而且每次启动都要付一遍(冷启动 3544ms vs 热启动 3552ms,说明是引导开销而非下载)。

所以探针现在报两个数:

| 字段 | 含义 | 是否参与评分 | |---|---|---| | elapsedMs | 墙钟,含我们这边的启动开销 | 否 | | launcherMs | 归给 npm/npx 的部分(常量 2500ms,向下取整以免过度扣减) | — | | serverMs | elapsedMs − launcherMs(仅在确实用了启动器时扣) | 是 |

常量 2500 无法在单次运行中测出来(两段开销交叠在一起,没有带内信号能分开),所以它来自上面那组对照实验。校验方法:同一个服务器分别经 npm 和经 node 启动,serverMs 相差仅 122ms(659 vs 781)——说明这个常量是公允的。

单次采样不够,所以有了 --repeat

三次采样的离散度很大:同一个服务器从 585ms 到 1630ms 都有。拿单次结果下判断,下一次运行就可能翻盘。

所以加了 --repeat <次数>(默认 1,最多 10)。多次运行时取最小值而非平均——噪声是单边的:机器繁忙只会增加延迟,不会减少,所以最快的那次最接近服务器的真实成本,取平均反而会把我们自己的调度抖动算进服务器头上。

阈值也随之分档:只有 1 次采样时,必须明显低于界线才给「握手很快」(否则下次运行就翻);有 3 次以上采样时,最小值就是可信估计,按实给分。报告的措辞也跟着改——单次采样就写「单次采样」,多次就写「3 次取最快」,不假装单次采样能撑起精确结论。

结果

修好之后,四个官方服务器(--repeat 3):

| 服务器 | 3 次采样(serverMs) | 取最快 | 结论 | |---|---|---|---| | server-memory | 644 / 617 / 602 | 602ms | 健康 81 | | server-sequential-thinking | 692 / 642 / 609 | 609ms | 健康 81 | | server-filesystem | 1254 / 988 / 687 | 687ms | 可用 78 | | server-everything | 1539 / 966 / 808 | 808ms | 可用 78 |

修之前四个全部卡在 69–77 的「可用」档。这是第一次有真实服务器拿到「健康」——而能拿到,只是因为采样揭示了它们真实成本约 600ms,之前那些 1500ms+ 的单次数字多半是我们自己的噪声。


信任评分怎么算

分数是一个可手算核对的量,不是黑箱:

总分 = 60(中性起点:"两个方向都没观察到")
     + 所有命中的信号权重
     然后夹到 [0, 100]

权重表集中在 lib/trust.js 的 WEIGHTS 里,一共 33 项,放在一处就是为了让人能自己加一遍。几项主要的:

| 信号 | 权重 | |---|---| | probe.spawn-error(起不来) | −50 | | probe.no-handshake(接不上) | −45 | | probe.crash(崩溃) | −40 | | instructions.injection-shaped(全局指令里有注入形状) | −35 | | probe.timeout(卡死) | −30 | | tool-count.claimed-mismatch-large(宣称与实测工具数大幅不符) | −28 | | claims.install-hooks(有消费端安装期脚本) | −20 | | tool.dangerous-description(工具描述含可疑措辞) | −18 | | claims.maintainer-hooks(有维护端脚本,仅披露) | −1 | | good.recent-release(近期有发版) | +8 | | good.tools-match(宣称与实测一致) | +6 |

对应五个档位:

| 档位 | 条件 | |---|---| | 健康 | 实测通过,分数 ≥ 80 | | 可用(有需留意项) | 实测通过,分数 ≥ 60 | | 需谨慎 | 分数 ≥ 35,或握手正常但 0 工具 | | 不建议使用 | 分数 < 35 | | 无法使用 | 探测未能成立 |

几条刻意的设计取舍:

  • 没跑过探测就不给自信的分。 --offline 模式固定落在「可用」档,因为纯靠元数据得不出运行时结论。报告会明说「只做了静态检查(未连接服务器)」。
  • 元数据没读到就不扣分。 早期版本对「没有仓库地址」「没有 MCP 标记」扣分,但当目标是一个裸脚本路径时,它本来就没有 package.json——那是我们输入的问题,不是服务器的问题。为自己的检查没跑成而惩罚对方,正是本工具要防的那种无根据判断,所以它自己不能犯。这个 bug 被实测抓出来了(干净服务器分数从 59 修正到 67)。
  • 不用「零证据=高分」的方式判干净。 中性起点 60,只靠正面观测往上加。

Windows 上为什么不能直接 spawn npx

第一次拿真实包测试时,本工具在 Windows 上一个真实服务器都测不了:静态声明读得好好的(抓到了真实 npm 元数据),但探测一律返回 spawn-error: spawn npx ENOENT。

逐项起真实进程验证后,原因是这样:

| 尝试 | 结果 | |---|---| | spawn("npx") 无 shell | ENOENT —— PATH 上只有 npx.cmd,没有无扩展名的可执行文件 | | spawn("npx.cmd") 无 shell | EINVAL —— Windows 加固后拒绝无 shell 直接 exec .cmd | | spawn("npx", {shell:true}) | 可行,但会把 shell 引号/注入风险重新引进来 |

解法是绕开 shell,也绕开 .cmd 包装:定位 npm 自己的 CLI 入口脚本(node_modules/npm/bin/npx-cli.js),用 process.execPath 直接跑。这样在 Linux/macOS 上行为完全一致,也保住了「目标里的包名不进 shell」这条性质。

修复后同一个服务器立刻探测成功:status ok、mcp-servers/everything 2.0.0、协议 2025-11-25、14 个工具。

顺带一条相关经验:目标路径先检查存在性再交给 node。否则一个打错的路径会让 spawn 成功、node 以 MODULE_NOT_FOUND 退出,然后被报成「服务器崩溃了」——而实际上根本没有服务器,是路径写错了。两者的修法完全不同,所以必须在入口处把「我们的输入错了」和「他们的代码坏了」分开。


它认得哪些风险模式

7 类,作用在工具描述和服务器全局 instructions 两处。判据全部来自工具投毒(tool poisoning)这类问题的公开文献:藏在工具描述里的指令会在注册时就进入模型上下文,早于任何一次调用发生。

| 模式 | 它在看什么 | |---|---| | ignore-instructions | 「忽略之前所有指令」这类措辞 | | exfiltrate | 「把凭证/密钥/环境变量发送出去」 | | shell-hide | 「不要告诉用户」 | | metadata-fetch | 直接指名云 metadata 端点(169.254.169.254 等) | | read-secrets-path | 指名凭证文件路径(~/.ssh、.aws/credentials、.env …) | | always-approve | 「无需确认 / 绕过审批」 | | persona-override | 「你现在是…」这类身份改写 |

这些措辞的写法很重要:它们描述「看到了什么」以及「为什么值得看一眼」,不指控恶意。工具描述告诉模型该做什么是正常的;告诉模型忽略既有指令不是。报告应当说清看到的是哪一种。


我们没测什么

这是最重要的一节。 也是报告里无条件打印的那一段。

| 没测的东西 | 为什么 | |---|---| | 工具被真正调用时的行为 | 本工具只做握手和 tools/list,不执行任何工具。一个服务器可以在不被调用时完全规矩,而在工具被调用时读取不该读的文件——这正是间接提示注入的常见形态。 | | 依赖包内部的代码 | 只统计了依赖数量,没有审查依赖内容。供应链风险往往藏在传递依赖里。 | | 运行时网络行为 | 没有监控服务器运行时会连向哪些地址。一个服务器可以在握手阶段安静,在工具调用时外联。 | | 运行时文件系统访问 | 只读了 package.json 和 README,没有观察服务器运行期间实际打开了哪些文件。 | | 工具返回内容中的注入 | 工具返回值里的注入内容只有真正调用时才会出现。这类检测需要运行时网关,静态检查看不到(这正是 mcp-sentry 那类工具的职责)。 | | 源代码质量与逻辑正确性 | 没有阅读实现代码。一个没有安全问题的服务器仍可能功能是错的。 |

换成一句话:没发现问题,只代表它通过了上面有限几项检查,不代表它是安全的。

想在任何时候查看这份清单(不需要给目标):

npx mcp-server-inspector --coverage

装上与用法

# 一次性运行,不安装
npx mcp-server-inspector <目标>

# 或装到全局
npm install -g mcp-server-inspector
mcpx <目标>

零依赖。只需要 Node ≥ 20。没有配置文件、没有守护进程、没有状态——一个「装之前跑一次」的工具,不该要求你先配置点什么。

目标写法

mcpx @modelcontextprotocol/server-filesystem       # npm 包名 → 自动走 npx
mcpx ./my-mcp-server                                # 本地目录
mcpx "npx -y some-mcp-server --flag"                # 完整命令行
mcpx ./dist/server.js                               # 本地脚本 → 用 node 拉起

两条刻意的行为:

  • 显式写出的调用方式原样保留。 早期版本会把 npx -y pkg 重新包一次,生成 npx -y npx -y pkg——已经修掉了。
  • 本地路径不存在会当场报错,而不是交给 node 去跑。否则你会看到一个「服务器崩溃」的诊断,附带 MODULE_NOT_FOUND——那个诊断是错的:根本没有服务器,是路径写错了。报告会明确写成「探测目标不存在(输入问题,不是服务器问题)」,不让自己的输入错误算到对方头上。

选项

| 选项 | 作用 | |---|---| | --offline | 不连接服务器,只做静态检查(快,但看不到运行时真相) | | --timeout <毫秒> | 探测超时,默认 15000 | | --repeat <次数> | 重复探测几次、取最快的一次做耗时判断(默认 1,最多 10)。单次计时有噪声,在意耗时结论时用 3 | | --json | 输出 JSON(含 coverage 覆盖范围说明),适合接 CI | | --quiet | 不打印进度提示 | | --coverage | 单独打印「会检查 / 不会检查」两份清单 | | -h, --help / -v, --version | 帮助 / 版本 |

退出码

| 码 | 含义 | |---|---| | 0 | 健康 / 可用 | | 1 | 需谨慎 / 不建议 / 无法使用 | | 2 | 用法错误 |


在代码里用

import { inspect } from "mcp-server-inspector";

const r = await inspect("@modelcontextprotocol/server-filesystem", {
  timeoutMs: 10000,
});

console.log(r.trust.band, r.trust.score);   // "healthy" 82
console.log(r.probe.status);                // "ok"
console.log(r.probe.toolCount);             // 14(翻页翻完的真实数量)
console.log(r.claims.installScripts);       // [](只报告,不执行)
console.log(r.trust.untested.length);       // 6(永远存在的诚实清单)

也可以只取需要的部分:

import { probe } from "mcp-server-inspector/probe";     // 只做真连探测
import { claimsFor } from "mcp-server-inspector/claims"; // 只读静态元数据
import { assess } from "mcp-server-inspector/trust";     // 只做评分
import { render, toJson } from "mcp-server-inspector/report";

JSON 输出结构

{
  "tool": "mcp-server-inspector",
  "schemaVersion": 1,
  "target": "@scope/name",
  "probed": true,
  "package":  { "source": "npm", "name": "…", "installScripts": [], "…": "…" },
  "runtime":  { "status": "ok", "toolCount": 14, "tools": [], "exitCode": null },
  "verdict":  { "band": "healthy", "score": 82, "summary": "…" },
  "findings": [ { "id": "…", "severity": "…", "detail": "…", "evidence": "…", "weight": 0 } ],
  "coverage": {
    "tested":    [ "…" ],
    "notTested": [ { "id": "…", "title": "…", "why": "…" } ],
    "disclaimer": "未发现问题 ≠ 安全。…"
  }
}

--offline 模式下 runtime 是 null,不是空对象——因为确实没有运行时观测可报。消费这份 JSON 时请显式处理这种情形:

{ "probed": false, "runtime": null, "verdict": { "band": "usable" } }

coverage.notTested 和 coverage.disclaimer 始终存在,包括一切正常的时候、也包括 runtime 为 null 的时候。任何消费这份 JSON 的地方都应该把它传下去。


开发

npm test        # → node test/inspect.js

测试有 290 项断言,分 10 节。几处刻意的做法:

  • 测试起真实子进程,不 mock child_process。 test/fixtures/ 里有两个真实服务器:good-server.js 是符合规范的实现(可用 argv 切换:--tools N、--paginate N、--instructions、--poisoned、--no-description、--no-schema、--no-annotations);broken-server.js 有 8 种故意坏法(silent / crash / garbage / banner / empty / noresult / stagger / partial)。
  • 不变量式断言。 比如「每条 finding 都必须带解释」「分数必须等于 60 加权重和」「没跑探测时不得出现运行时 finding」「报告必须无条件打印第三节」「直接启动的服务器不得被授予它没付过的启动器折扣」「崩溃的服务器既不该得到计时加分、也不该被额外判处计时减分」。这类断言抓到的 bug 比逐条对输出的多。
  • partial 模式把一帧拆成两次写,用来证明组帧逻辑不假设「一个 data 事件 = 一条消息」——本地这么假设能跑,有负载时会挂。- 5b 节专门锁死计时隔离。 它会回归失败如果:npm 启动的服务器因为别人的开销而丢分、直接启动与经 npm 启动的同一服务器分数不一致、被扣减后的 serverMs 为负或超过墙钟、单次采样的临界值贸然给分、或者采样次数没有被如实标注。
  • 断言一个现象,就必须把那个现象放进数据里。 这里犯过一次:测「维护端脚本只扣 1 分」时用替换而非追加构造 flags,顺手把两个正面信号(good.recent-release、good.multi-maintainer)也丢了,于是差值是 13 而不是 1——看起来像代码错了,实际是测试写错了。逐项打印权重才定位到。先怀疑代码、再怀疑测试期望,但两者都要真查。

零依赖 ESM + JSDoc 类型标注(// @ts-check)。


诚实的边界(再说一次)

mcp-server-inspector 是准入前的最低限度体检,不是安全审计,不是质量认证,不是代码审查。

  • 通过 ≠ 安全。 只说明没有在握手和工具声明阶段观察到明显问题。
  • 工具数量 ≠ 能力。 14 个工具不代表能做 14 件事,也不代表做得好。
  • 活着 ≠ 好。 一个服务器能顺利握手,仍可能功能全错。
  • 评分是启发式,不是测量。 权重是我们选的,公开可查、可手算、可以不同意。它排序「值得看一眼的程度」,不量化「安全性」。

这个工具的价值不在于给出一个分数,而在于把「我知道什么」和「我不知道什么」分开写清楚。如果这一点做到了,它就有用。


生态数据与出处

上面引用的生态规模,出处如下,可自行核对:

  • MCP 服务器数量:138,000+,来自 MCP 官方 Registry(registry.modelcontextprotocol.io)。官方 modelcontextprotocol/servers 仓库的第三方列表已被该 Registry 取代。
  • 该数量靠人工筛选分类——2026 年的生态参考资料把「最近 90 天内有提交」这类判据写进收录标准。十几万个包靠人力筛,说明工具缺口是真实存在的,这也正是本项目立项的依据。

关于竞品形态的判断(例如 Apple/macOS 生态的饱和状况)属于调研期的观察,随时间会变,不作为本工具正确性的依据。


许可证

MIT