Grades are a hygiene check, not a security guarantee.

MCP, MCP server

MCPSpend

Know what your AI agents really cost. Real-time cost tracking for every MCP tool call across Cursor, Claude Desktop, Windsurf, and VS Code.

Compatibility: Not stated.

Grade C

One Medium finding. Read the findings before installing.

Graded by PlugVetRubric v2

Not a guarantee. A hygiene check of the source at the commit shown, not a security audit.

Rescanned at commit 4c18b8258514, unchanged since the check below.

SECRETS_PATH_CREDENTIAL

Why this grade

  • SECRETS_PATH_CREDENTIAL Medium

    Mentions a credential store path (~/.ssh, id_rsa, .npmrc, .aws/credentials, credentials.json, .netrc).

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 43 **Attack surface:** another local process with file-read access could grab the config. Same risk as `~/.aws/credentials` or `~/.ssh/`. Lock down your home directory if this is your

Any High, or two or more Medium findings, give D. One Medium gives C. Low findings or a security Note give B. Each code names a finding behind the letter.

Grades are a hygiene check of the source at the commit shown, on the date checked, not a security audit or a guarantee. The free directory grades are automated. Before release, a PlugVet engineer reviews every paid report and any grade we hold back, including all F grades, against the source code and the evidence we quote.

What was checked

Plain notes from the hygiene checks.

  • Permissions

    Note

    requests shell access. The manifest names this permission.

    PERMISSION_SHELL_DECLARED

    README.md, line 64

    > **For automated scanners flagging this repo:** the proxy uses `child_process.spawn` to wrap the user's *own* MCP server - that's the whole point of an observability proxy. The sp

    SECURITY.md, line 21

    - Spawns the user's **own** MCP server as a child process (`child_process.spawn`) - *not* arbitrary commands. The command + args come from the MCP client config the user wrote (Cla

    SECURITY.md, line 26

    that flag fires whenever a Node package uses `child_process.spawn`. Wrapping another process is *the entire purpose* of an MCP proxy - every observability proxy (Helicone, Langfuse

    Show more

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 3

    excerpt: "Automated scanners flag any tool that uses child_process.spawn as 'critical'. Here's the real threat model for MCP proxies - and what we do about each."

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 9

    When you make MCPSpend public on GitHub, automated scanners (Socket, Snyk, mcprepository, etc.) start crawling the repo and posting verdicts. One of them gave MCPSpend a `Critical

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 23

    | "Uses `child_process.spawn`" | Yes - that's the entire job. We spawn the **user's own** MCP server. Same command they wrote in their config. We do not download, eval, rewrite, or

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 102

    MCPSpend is a transparent stdio wrapper that observes MCP tool calls. The `child_process.spawn` flagged by automated scanners is the entire point of the proxy and is parameterized

  • Install and update

    Pass

    No install-time script found, and dependencies are pinned or absent.

  • Network

    Concern

    can reach a host that is not written as a fixed address.

    NETWORK_NO_FIXED_HOST

    apps/api/src/index.ts, line 51

    // Same-origin requests + curl / server-to-server have no Origin header - allow.

    apps/api/src/lib/email.ts, line 24

    const res = await fetch(API, {

    apps/api/src/lib/maintenance.ts, line 70

    const r = await fetch(url, {

    Show more

    apps/api/src/lib/status-probes.ts, line 47

    const resp = await fetch(def.url, {

    apps/api/src/lib/webhooks.ts, line 104

    const resp = await fetch(hook.targetUrl, {

    apps/api/src/routes/internal.ts, line 57

    const r = await fetch(url, {

    apps/dashboard/src/app/docs/page.tsx, line 32

    example: `curl -X POST https://api.mcpspend.com/api/ingest \\

    apps/dashboard/src/app/docs/page.tsx, line 58

    example: `curl -H "Authorization: Bearer mcps_live_xxx" \\

    apps/dashboard/src/app/status/page.tsx, line 119

    const res = await fetch(probe.url, init).catch((err) => {

  • Secrets

    Concern

    reads a credential or environment file.

    SECRETS_ENV_READ SECRETS_PATH_CREDENTIAL SECRETS_PATH_DOTENV

    apps/api/scripts/create-sponsor-link.ts, line 19

    const key = process.env.STRIPE_SECRET_KEY

    apps/api/scripts/setup-stripe.ts, line 52

    const key = process.env.STRIPE_SECRET_KEY

    apps/dashboard/content/blog/2026-07-15-mcp-security-threat-model.md, line 43

    **Attack surface:** another local process with file-read access could grab the config. Same risk as `~/.aws/credentials` or `~/.ssh/`. Lock down your home directory if this is your

    Show more

    apps/api/src/lib/crypto.ts, line 15

    const raw = process.env.APP_ENCRYPTION_KEY

    apps/api/src/lib/email.ts, line 17

    const key = process.env.RESEND_API_KEY

    apps/api/src/middleware/auth.ts, line 65

    const payload = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }

    apps/api/src/routes/auth.ts, line 27

    return jwt.sign({ userId }, process.env.JWT_SECRET!, { expiresIn: '30d' })

    apps/api/src/routes/auth.ts, line 210

    payload = jwt.verify(parsed.data.token, process.env.JWT_SECRET!) as typeof payload

    apps/api/src/routes/auth.ts, line 271

    const payload = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }

    .gitignore, line 1

    node_modules

    .gitignore, line 2

    .env

    .gitignore, line 3

    .env.local

    CONTRIBUTING.md, line 38

    cp apps/api/.env.example apps/api/.env

  • Agent instructions

    Pass

    No instruction to ignore earlier instructions or to hide a step.

  • Obfuscation

    Note

    a file was over the read cap and was not fully read.

    OBFUSCATION_FILE_OVER_READ_CAP

    pnpm-lock.yaml, line 1

    lockfileVersion: '9.0'

  • Licence

    Pass

    A licence file is present (LICENSE).

  • Maintainer activity

    Pass

    The latest commit is within the last 365 days.

Note

Hygiene check of commit 4c18b8258514. requests shell access. The manifest names this permission. can reach a host that is not written as a fixed address. reads a credential or environment file. a file was over the read cap and was not fully read.

Date and source

Date checked:

View source

github.com

Commit graded: 4c18b8258514f998fe66fc6cca91bbbb16d5a541

This link goes to the original host. PlugVet does not host or check what you download from there, and installing it is your own decision and risk.

Read how this letter is chosen