@vulsight/guard-mcp
v0.3.0
Published
MCP server that asks VulSight Guard before an agent pays.
Downloads
2,179
Maintainers
Readme
@vulsight/guard-mcp
An MCP server that gives an agent in Claude Code, Codex, or any other MCP client a check_payment tool: ask VulSight Guard before paying, stop on deny, and quote the reason to the user. This tool is cooperative. The agent has to call it, and an agent that skips the call can still pay. For a check the agent cannot skip, route its x402 traffic through the guard proxy: the seller URL becomes https://vulsight-guard.vercel.app/p/<proxy token>/merchant.example/dataset (the seller's own scheme is left out, https is implied) and every payment is checked before it is forwarded. The payment hook in the @vulsight/guard SDK is enforced the same way for agents that use it.
Install
Claude Code:
claude mcp add vulsight-guard -e VULSIGHT_API_KEY=vs_test_... -- npx -y @vulsight/guard-mcpCodex, in ~/.codex/config.toml:
[mcp_servers.vulsight-guard]
command = "npx"
args = ["-y", "@vulsight/guard-mcp"]
env = { "VULSIGHT_API_KEY" = "vs_test_..." }
tool_timeout_sec = 450The last line leaves room for context filing, the decision, and the default two-minute review wait. Guard API calls have a 105-second deadline each; review polls add the requested poll duration. Increase the client limit further if you raise wait_seconds above its default. Claude Code needs no such line.
Any other MCP client (Cursor, Windsurf, Cline, VS Code, Zed, or your own agent on an MCP SDK) takes the same server through its own config, most of them in this JSON shape (VS Code keys it servers, Zed context_servers):
{
"mcpServers": {
"vulsight-guard": {
"command": "npx",
"args": ["-y", "@vulsight/guard-mcp"],
"env": { "VULSIGHT_API_KEY": "vs_test_..." }
}
}
}If the client caps how long a tool call may run, raise that cap to at least 450 seconds or pass a shorter wait_seconds on check_payment. Paste the skill text (npx -y @vulsight/guard-mcp --skill) into the client's rules or instructions file so the agent knows to call the tool before it pays. Agents built on a framework rather than an MCP client (the Vercel AI SDK, LangChain, the OpenAI Agents SDK) use the @vulsight/guard SDK instead, so the check runs in code rather than waiting for the agent to call a tool.
Then give the agent the skill that tells it when to call the tool. The package prints it. Run the lines for your tool once:
# Claude Code
mkdir -p ~/.claude/skills/vulsight-guard
npx -y @vulsight/guard-mcp --skill > ~/.claude/skills/vulsight-guard/SKILL.md
# Codex, once, in the project the agent works in
npx -y @vulsight/guard-mcp --skill codex >> AGENTS.mdnpx -y @vulsight/guard-mcp downloads and runs the server. The package bundles the @vulsight/guard SDK, so there is nothing else to install. --version prints the version.
Claude Code can also file every tool result on its own, so the agent does not have to remember. The package prints the hook settings:
npx -y @vulsight/guard-mcp --hooknpx -y @vulsight/guard-mcp --hook prints one JSON object; add its hooks key to ~/.claude/settings.json (or the project's .claude/settings.json). It runs npx -y @vulsight/guard-mcp file-tool-result after every tool call, async so the tool call does not wait on it. The command reads Claude Code's hook payload on stdin, posts the tool's result under the page's origin when the tool was given an http(s) url (WebFetch, for example) and under the tool's name otherwise, skips the guard's own tools (the server must be installed under a name containing vulsight, as the install line does, for the skip to apply), AskUserQuestion, and empty results, and exits 0 on any runtime failure, so a guard that is down never stops the agent (a failure is one line on stderr, in Claude Code's debug log; only a missing key exits 1, a non-blocking notice, so an unconfigured hook is not silent forever). It reads VULSIGHT_API_KEY and VULSIGHT_BASE_URL from the environment Claude Code was started in, so export them in that shell. The hook and the server derive one session from the Claude Code project directory (both are given it), so a page the hook flags is considered by the next check_payment: only payments to that page's seller when the hook filed it under the page's origin, and every payment when it filed it under the tool's name. Because it runs in the background, a filing can still be in flight when the agent calls check_payment a moment later and land after the decision; the post has 105 seconds to finish within the hook's 120-second timeout. The server can still finish a filing after its client disconnects. Treat the hook as background defense: before paying, the agent files the content it is about to act on with file_context, as the skill says, and the declared payment and the rules carry the enforcement. Codex has no equivalent hook; there the agent calls file_context.
The hook files every tool result, so a secret a tool prints (a .env read, printenv) is sent to the guard. The complete submitted text and check results are kept privately until account deletion. An enabled shadow comparison sends the same text to its background service and keeps its result and highlighted passages with that history. The hook and the server must use the same API key: the content result is matched by key and session, so a filing under another key does not affect a check_payment. The hook therefore shares that key's per-minute rate limit, and a very chatty tool loop can hit the ceiling, after which the hook drops filings, one line on stderr each, until the minute passes. Your own messages are never filed: you are the principal, and the check is for untrusted content.
Environment
| Variable | Required | What it is |
| --- | --- | --- |
| VULSIGHT_API_KEY | yes | An API key from your VulSight Guard dashboard. The server never prints it. |
| VULSIGHT_BASE_URL | no | Where VulSight Guard runs. Defaults to https://vulsight-guard.vercel.app. |
| VULSIGHT_SESSION | no | Pins the guard session that every filing and check_payment carries. Unset, the session is one per Claude Code project directory, else one per server process. |
Tools
Every result is plain sentences the agent can quote to its user. A rejected input or a failure to reach a verdict is a short plain-text answer, flagged as an error, never a list and never a stack trace.
check_paymenttakes the payment as the seller's 402 states it (kind: "x402_payment"withpayTo,amountAtomic,asset,network,scheme,resourceUrl, andobserved402PayTo, the payee the seller's own 402 named, so a redirected payee is denied) or the EVM contract call about to be signed (kind: "evm_call"withto,value,data,network), an optionalcontext(the text of the page or tool result that led to the payment, clipped to 64 KiB, and the guard files a clipped text as unchecked since it never read the rest), an optionalseller_402(the seller's 402 answer as the agent received it, its body and the resource description from its payment terms, filed the same way; the decision citescontextwhen both are given),wait_seconds(default 120, at most 300) to wait for a human reviewer, and an optionalidempotency_key, your key for this payment attempt (1 to 200 characters): send the same key again when you retry a call that timed out, so the guard decides the payment once, unlessseller_402changed; without one, a repeated call is decided again. It answers with the verdict, the reasons, and the decision id. Amounts are decimal strings in the asset's smallest unit and networks are CAIP-2 ids:eip155:84532is Base Sepolia andeip155:8453is Base. It also knows Solana Devnet (solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) and Solana Mainnet (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp). Solana wallet and mint addresses are case-sensitive, and settlementtxHashvalues are base58 transaction signatures.report_settlementrecords a payment the guard allowed once it settles, so the ledger shows it:decisionIdfromcheck_payment,txHash(the header'stransaction),payer, andnetworkfrom thePAYMENT-RESPONSEheader of the paid response,payee,amountAtomic, andassetfrom the 402 requirement the agent accepted, anddirectionsent. An optionalresponse, the body of the answer to the paid request, is filed under the seller that decision paid, beside the settlement, so the nextcheck_paymentwaits for it, and the answer adds what the check found. A server with no record of the decision (one restarted since the check) files it without its seller and says so.get_policyreads the policy the guard applies to this agent: networks, assets, payee lists, limits, and the review timeout.list_recent_decisionslists the account's newest decisions,limitfrom 1 to 50.file_contextfiles text the agent read that did not go throughcheck_payment'scontext(a page, a file, a search result, another tool's output):text, and anorigin: for a web page its origin as a URL (https://seller.example), so a flagged page holds only payments to that seller, else a label such as the tool's name (defaulttool); any other label, a bare host or a tool's name, holds every payment in the session. The answer to a paid request goes here too, with the seller's origin, when there is no receipt to report, for example when the seller answered it with an error or another 402. It answers in a sentence or two: flagged, clear (no evidence found, not proof the text is safe), or unchecked when the content check did not finish or the text was over 64 KiB, in which case only its start was filed and the answer says so. A NUL byte is filed as the replacement character and counts three bytes toward the 64 KiB cap, and a leading byte order mark is kept, so a text opening with one has a new hash and is scanned once more.
Text filed through context, seller_402, response, or file_context belongs to one session: the value of VULSIGHT_SESSION when it is set, else one per Claude Code project directory (the session the hook uses too), else one per server process. A flagged text affects later payments in that session for an hour: only payments to its seller when it was filed under a site (a page's origin as a URL), and every payment when it was filed under any other label, a bare host or a tool's name. With content enforcement enabled, a completed content block denies the payment even when its amount and seller are allowed; a check requiring review holds it for a person. A session that files faster than its key's per-minute limit (or the account's, across every key) gets a 429, which file_context reports as an error and the hook drops with one line on stderr. A file_context filing or a hook filing never blocks the agent. A context or seller_402 filing that fails inside check_payment fails the check closed, and the answer says do not pay. A response filing that fails does not stop the settlement: the answer says the response was not checked, and the next check_payment files it again before it decides. While the guard is out (a 429, a 5xx, or no answer) that check fails closed, saying do not pay; a response the guard refuses fails one check with the refusal and is then dropped.
Contract support
MCP accepts only exact x402 proposals. Its verdict covers the supplied payment terms, not omitted execution extensions or supplementary permit and approval signatures. Those need their own supported checks; a payment approval is not general signing permission.
The evm_call proposal supports canonical USDC transfers and bounded allowance grants on the native USDC contracts on Base and Base Sepolia. Unknown functions, arbitrary token contracts, extra calldata, unsupported execution-envelope fields and dangerous approval forms are denied, including in observe mode. Old approvals are checked against the same support limits before reuse. An allowlisted address does not grant permission to call any function on it.
Zero grants still undergo asset and spender checks. Finite grants can be spent later by the approved spender; Guard does not track total on-chain allowance or control that later execution. NFT operations, swaps, batches and arbitrary Solana programs are not covered. MCP checks a proposal and returns a verdict. Your wallet integration must enforce that verdict on the exact action it signs; another signing tool can bypass MCP.
Transports
stdio by default. --http <port> serves streamable HTTP at http://127.0.0.1:<port>/mcp for clients that connect over HTTP. Each request is stateless and shares the process's one session, and a request whose Host header is not 127.0.0.1:<port> or localhost:<port> is refused with 403:
VULSIGHT_API_KEY=vs_test_... npx -y @vulsight/guard-mcp --http 3333Check it
bun test packages/mcp