MCP server
- Endpoint
https://malwagon.com/mcp
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "lookup_hash",
"arguments": {
"sha256": "9c1f0b6d2ae74318bd50c7a9e6f34182"
"bb0d5e7c94a2f61038de7b5c0a91f2d4"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
_meta keys are required on every call, and the same two values are mirrored into the MCP-Protocol-Version and Mcp-Method headers. The digest is the target. No bytes cross this boundary, in either direction.{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "..." }],
"structuredContent": {
"sha256": "9c1f0b6d...0a91f2d4",
"scans": {
"items": [
{ "scan_id": 4127,
"verdict": "malicious",
"score": 88 }
],
"returned": 1,
"total": 1,
"truncated": false
}
}
}
}
What it is for
An analyst working with an AI client should not have to be the wire between the two. The MCP server exposes this platform's analysis as tools the client can call itself: it looks a hash up, searches an indicator across the corpus, queues a scan and reads the finished report, and it does that with the same identity, the same plan limits and the same privacy rules a person would have.
It is a second surface over the work the REST API already does, not a second platform. A scan submitted through a tool call is the same scan, in the same queue, on the same machines, and it produces the same report.
It is included with every paid plan, and it authenticates with a bearer token like the REST API does. See pricing.
The tools
Five, each carrying the scope it costs. A token issued read can call the four that read; only a submit token can queue an analysis, and a client is told which it holds before it tries.
| Tool | Scope | What it answers |
|---|---|---|
| lookup_hash | read | Find analyses of a known SHA-256 digest. |
| get_report | read | The derived analysis report for one scan: verdict, capabilities, behaviour summary, observed operations and defanged indicators. |
| search_indicator | read | Find scans where an indicator was observed: an IP, a domain, a URL, a mutex, a registry key, a hash or a JA3/JA4 fingerprint. |
| poll_scan | read | The current status of one scan, for polling after submit_scan. |
| submit_scan | submit | Queue a new analysis of a target that can be named as text: a SHA-256 to look up, a URL to visit, a command to run, or a package to install. |
Connect a client
Two values do all the work: the endpoint and a bearer token. Where a client keeps them differs; what it sends does not.
2026-07-28, which has no handshake and carries the protocol version and the client's capabilities in every request. The initialize handshake older clients send is still answered, on 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, so a stock client connects rather than failing at hello. What an older client cannot do is call a tool: without both _meta keys the call is refused with -32602, which names the missing key. If your client has not adopted the newer revision, the small stdio bridge below speaks your client's revision on one side and this one on the other, and it holds no policy of its own. The token, the plan and the scope are still decided here.{
"mcpServers": {
"malwagon": {
"url": "https://malwagon.com/mcp",
"headers": {
"Authorization": "Bearer mwg_a1b2c3d4_kZq7Xn2R9tLpWv0sYcB4hJdF6gMaE3uNbT1xQfV5rDo"
}
}
}
}
2026-07-28: Claude Desktop keeps a block like it in its own configuration file, and Claude Code registers the same server from the command line. Where the setting lives is the client's business and its documentation is the authority on that; the URL and the header are ours. Until the client adopts the revision, point it at the bridge instead and keep the same two values there.Server URL https://malwagon.com/mcp Authorization Bearer mwg_a1b2c3d4_kZq7Xn2R9tLpWv0sYcB4hJdF6gMaE3uNbT1xQfV5rDo
2026-07-28, and a local bridge cannot stand in for it the way it can for a desktop client.{
"mcpServers": {
"malwagon": {
"command": "python3",
"args": ["/path/to/mcp_bridge.py"],
"env": {
"MWG_MCP_URL": "https://malwagon.com/mcp",
"MWG_MCP_TOKEN": "mwg_a1b2c3d4_kZq7Xn2R9tLpWv0sYcB4hJdF6gMaE3uNbT1xQfV5rDo"
}
}
}
}
/mcp-bridge.py, put it wherever the client can reach it and give that path here; the path above is a placeholder, not a location on this host. A single file with no dependencies beyond the standard library. It answers the handshake locally, because there is nothing here to forward one to, and adds the revision block and the mirrored headers to every call it passes on. The token is read from the environment rather than from the command line, so it does not appear in a process list.curl -sS -X POST "https://malwagon.com/mcp" \
-H "Authorization: Bearer $MWG_TOKEN" \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/list" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list",
"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}'
What a client sends
Revision 2026-07-28 has no handshake. Every request states its own protocol version and client capabilities, and the server answers each one on its own terms, so a dropped connection costs nothing to resume.
In params._meta
io.modelcontextprotocol/protocolVersionio.modelcontextprotocol/clientInfoio.modelcontextprotocol/clientCapabilities
The prefix is reserved by the specification, so these exact strings are the contract rather than a convention.
Mirrored into headers
MCP-Protocol-VersionMcp-MethodMcp-Name
They must agree with the body. A header that contradicts what it mirrors is refused rather than reconciled, because a proxy that rewrote one of them is a thing the server cannot see.
What a call passes through
Four gates, in this order, and each one is answered before the next is asked. A call that fails any of them never reaches the analysis pipeline.
- 01 Envelope One JSON-RPC request, the declared revision, and every mirrored header agreeing with the body it mirrors.
- 02 Token Resolved to an account on every call, never cached between them, so a revoked token stops at the next one.
- 03 Plan and scope The plan has to include this surface and the token's scope has to cover this tool. Both are read live.
- 04 Budget Reads and submissions carry separate allowances; a submission also spends a credit, and one that cannot be charged is refused rather than queued.
Error codes
Standard JSON-RPC codes, plus the two ranges the specification reserves. Nothing outside these is emitted, and each carries one meaning.
| Code | Meaning |
|---|---|
| -32700 | The body is not JSON. |
| -32600 | Valid JSON, but not a JSON-RPC request. |
| -32601 | No such method on this server. |
| -32602 | The params are wrong for the method named. |
| -32603 | The server failed while handling a valid call. |
| -32020 | A mirrored header disagrees with the body it mirrors. |
| -32022 | The declared revision is not one this server speaks. |
| -31001 | No usable token, or a plan that does not include this surface. |
| -31002 | The token is valid and its scope does not cover this tool. |
| -31003 | Too many calls. Back off; the limits are in the reference. |
What it will not do
It never carries a sample. No tool accepts file bytes and none returns them. A submission names its target as text, a digest, a URL, a command line or a package, and a report comes back as derived findings. The bytes stay on the hardware that ran them.
A read token cannot be talked into writing. Scope is checked per tool call against the token, not against the session, so a client that has been persuaded to try submit_scan with a read token is refused rather than asked to confirm.
It has no privileges of its own. The token belongs to an account, and every call is answered with that account's plan, its credits and its visibility. A tool call cannot read a scan the person holding the token could not open in the console.
It is rate limited like everything else. Reads and submissions carry their own budgets, and a spent budget comes back as a tool error inside a successful response rather than as a transport failure, because a client that cannot read the difference will retry the wrong one.