@whisper-security/whisper-mcp 0.219.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/whisper-mcp.js +206 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 viaGraph B.V. (Whisper Security)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Whisper MCP server
|
|
2
|
+
|
|
3
|
+
Give an AI agent a real, routable IPv6 identity, egress anyone can verify from outside,
|
|
4
|
+
and DNS policy you control. Plus the infrastructure graph, to ask "is this host safe?"
|
|
5
|
+
before connecting.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"mcpServers": {
|
|
10
|
+
"whisper": {
|
|
11
|
+
"command": "npx",
|
|
12
|
+
"args": ["-y", "@whisper-security/whisper-mcp"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That is the whole setup. Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Cline,
|
|
19
|
+
Zed: same three lines.
|
|
20
|
+
|
|
21
|
+
## It does something useful with no account
|
|
22
|
+
|
|
23
|
+
No key, no signup, 45 tools, real answers:
|
|
24
|
+
|
|
25
|
+
- `whisper_verify` - is this address or hostname a real Whisper agent, and whose?
|
|
26
|
+
- `whisper_rdap` - the IP-anchored registration record for a `/128`
|
|
27
|
+
- `explain_indicator` - one-call threat assessment for a domain, IP, ASN or hash
|
|
28
|
+
- `query` - Cypher against WhisperGraph, the internet's infrastructure graph
|
|
29
|
+
- `read_docs`, `list_workflows`, `run_workflow` - documentation and ready-made investigations
|
|
30
|
+
|
|
31
|
+
Add an API key and the same server also registers agents, sets resolver policy, hands out
|
|
32
|
+
egress configuration and reads your agents' activity. Nothing is hidden behind the key
|
|
33
|
+
that could have been answered without it.
|
|
34
|
+
|
|
35
|
+
## Getting a key
|
|
36
|
+
|
|
37
|
+
`https://whisper.online` - an email address, no human in the loop. Then either set
|
|
38
|
+
`WHISPER_API_KEY` in the server's `env` block, or run `whisper login`.
|
|
39
|
+
|
|
40
|
+
## What this package does
|
|
41
|
+
|
|
42
|
+
It resolves the `whisper` binary for your platform from the GitHub release tagged for
|
|
43
|
+
this exact package version, checks it against the `.sha256` published beside it, caches it,
|
|
44
|
+
and runs `whisper mcp`. The verified digest is recorded beside the binary and re-checked on
|
|
45
|
+
every run, so a given version of this package always resolves to the same bytes and a cached
|
|
46
|
+
copy that no longer matches the digest we recorded is never executed.
|
|
47
|
+
|
|
48
|
+
Already have the CLI (`brew install whisper-sec/tap/whisper`, `apt install whisper`,
|
|
49
|
+
`scoop install whisper`)? Then `whisper mcp` is the same server and you do not need this
|
|
50
|
+
package at all.
|
|
51
|
+
|
|
52
|
+
## Supported platforms
|
|
53
|
+
|
|
54
|
+
linux (amd64, arm64, arm, 386, riscv64, mips, mipsle), darwin (amd64, arm64),
|
|
55
|
+
windows (amd64, arm64).
|
|
56
|
+
|
|
57
|
+
## Links
|
|
58
|
+
|
|
59
|
+
- [whisper.online](https://whisper.online)
|
|
60
|
+
- [Source](https://github.com/whisper-sec/whisper-cli)
|
|
61
|
+
- [Issues](https://github.com/whisper-sec/whisper-cli/issues)
|
|
62
|
+
|
|
63
|
+
MIT.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* `npx -y @whisper-security/whisper-mcp` - the Whisper MCP server, one command, no install.
|
|
6
|
+
*
|
|
7
|
+
* The server is a Go binary (`whisper mcp`). This wrapper exists so it can be run the way
|
|
8
|
+
* MCP clients expect, with npx, by anyone who has not installed the CLI.
|
|
9
|
+
*
|
|
10
|
+
* THE ONE RULE THIS FILE MUST NEVER BREAK: stdout belongs to the MCP JSON-RPC stream.
|
|
11
|
+
* A single stray byte on stdout corrupts the protocol and the client drops the connection,
|
|
12
|
+
* which presents as "the server does not work" with nothing in any log. Every diagnostic
|
|
13
|
+
* here goes to stderr, deliberately and without exception.
|
|
14
|
+
*
|
|
15
|
+
* TRUST. The binary is fetched from the GitHub release tagged for THIS package version and
|
|
16
|
+
* checked against the `.sha256` published beside it, so a given version of this package
|
|
17
|
+
* always resolves to the same bytes. The verified digest is recorded next to the binary and
|
|
18
|
+
* re-checked on EVERY run, so a cached copy that no longer matches that digest is never run.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const { spawn } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const VERSION = require('../package.json').version;
|
|
28
|
+
const REPO = 'whisper-sec/whisper-cli';
|
|
29
|
+
const BASE = `https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
30
|
+
|
|
31
|
+
/** stderr only. See the header: stdout is the protocol. */
|
|
32
|
+
function note(msg) {
|
|
33
|
+
process.stderr.write(`whisper-mcp: ${msg}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function die(msg, hint) {
|
|
37
|
+
process.stderr.write(`\nwhisper-mcp: ${msg}\n`);
|
|
38
|
+
if (hint) process.stderr.write(`${hint}\n`);
|
|
39
|
+
process.stderr.write('\n');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Our release assets are named by GOOS-GOARCH. Node spells several of those differently,
|
|
45
|
+
* so the mapping is explicit rather than a string concat: an unknown pair must produce a
|
|
46
|
+
* useful message naming what we DO ship, not a 404 the user has to decode.
|
|
47
|
+
*/
|
|
48
|
+
const TARGETS = {
|
|
49
|
+
'linux-x64': 'whisper-linux-amd64',
|
|
50
|
+
'linux-arm64': 'whisper-linux-arm64',
|
|
51
|
+
'linux-arm': 'whisper-linux-arm',
|
|
52
|
+
'linux-ia32': 'whisper-linux-386',
|
|
53
|
+
'linux-riscv64': 'whisper-linux-riscv64',
|
|
54
|
+
'linux-mips': 'whisper-linux-mips',
|
|
55
|
+
'linux-mipsel': 'whisper-linux-mipsle',
|
|
56
|
+
'darwin-x64': 'whisper-darwin-amd64',
|
|
57
|
+
'darwin-arm64': 'whisper-darwin-arm64',
|
|
58
|
+
'win32-x64': 'whisper-windows-amd64.exe',
|
|
59
|
+
'win32-arm64': 'whisper-windows-arm64.exe',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function assetName() {
|
|
63
|
+
const key = `${process.platform}-${process.arch}`;
|
|
64
|
+
const asset = TARGETS[key];
|
|
65
|
+
if (!asset) {
|
|
66
|
+
die(
|
|
67
|
+
`no Whisper build for ${key}.`,
|
|
68
|
+
` We ship: ${Object.keys(TARGETS).join(', ')}\n` +
|
|
69
|
+
` If you need ${key}, please open an issue at https://github.com/${REPO}/issues`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return asset;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cacheDir() {
|
|
76
|
+
// Honour XDG, fall back to the platform convention. Keyed by version so an upgrade
|
|
77
|
+
// never reuses an older binary and a downgrade never has to re-download.
|
|
78
|
+
const base =
|
|
79
|
+
process.env.WHISPER_MCP_CACHE ||
|
|
80
|
+
process.env.XDG_CACHE_HOME ||
|
|
81
|
+
(process.platform === 'win32'
|
|
82
|
+
? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
83
|
+
: process.platform === 'darwin'
|
|
84
|
+
? path.join(os.homedir(), 'Library', 'Caches')
|
|
85
|
+
: path.join(os.homedir(), '.cache'));
|
|
86
|
+
return path.join(base, 'whisper-mcp', VERSION);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256(buf) {
|
|
90
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchOrDie(url, what) {
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(60_000) });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
die(
|
|
99
|
+
`could not reach the download host while fetching ${what}.`,
|
|
100
|
+
` ${url}\n ${e && e.message ? e.message : e}\n` +
|
|
101
|
+
` If you are behind a proxy, set HTTPS_PROXY and try again.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
die(`${what} returned HTTP ${res.status}.`, ` ${url}`);
|
|
106
|
+
}
|
|
107
|
+
return Buffer.from(await res.arrayBuffer());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the binary, downloading and verifying it once per version. Concurrent `npx` runs
|
|
112
|
+
* are safe: each writes a unique temp file and renames it into place, and rename is atomic
|
|
113
|
+
* on every platform we ship to.
|
|
114
|
+
*/
|
|
115
|
+
async function ensureBinary() {
|
|
116
|
+
const asset = assetName();
|
|
117
|
+
const dir = cacheDir();
|
|
118
|
+
const exe = path.join(dir, process.platform === 'win32' ? 'whisper.exe' : 'whisper');
|
|
119
|
+
const stamp = `${exe}.sha256`;
|
|
120
|
+
|
|
121
|
+
// A cached binary is re-hashed against the digest we recorded when we verified it. Trusting
|
|
122
|
+
// the cache because it exists would mean anyone who can write the cache path gets code
|
|
123
|
+
// execution inside a process holding WHISPER_API_KEY in its environment. On a laptop that
|
|
124
|
+
// path is private; under a shared XDG_CACHE_HOME or on CI it is not, and the check costs
|
|
125
|
+
// about a tenth of a second.
|
|
126
|
+
if (fs.existsSync(exe) && fs.existsSync(stamp)) {
|
|
127
|
+
const recorded = fs.readFileSync(stamp, 'utf8').trim().slice(0, 64);
|
|
128
|
+
if (/^[0-9a-f]{64}$/.test(recorded) && sha256(fs.readFileSync(exe)) === recorded) {
|
|
129
|
+
return exe;
|
|
130
|
+
}
|
|
131
|
+
note('the cached binary no longer matches its recorded digest; fetching a clean copy');
|
|
132
|
+
try {
|
|
133
|
+
fs.rmSync(exe, { force: true });
|
|
134
|
+
fs.rmSync(stamp, { force: true });
|
|
135
|
+
} catch (e) {
|
|
136
|
+
die(
|
|
137
|
+
`the cached copy at ${exe} does not match its digest and could not be removed.`,
|
|
138
|
+
` ${e.message}\n Delete that directory and run again.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
note(`fetching the Whisper CLI ${VERSION} for ${process.platform}-${process.arch} (first run only)`);
|
|
144
|
+
const [bin, sumTxt] = await Promise.all([
|
|
145
|
+
fetchOrDie(`${BASE}/${asset}`, 'the binary'),
|
|
146
|
+
fetchOrDie(`${BASE}/${asset}.sha256`, 'its checksum'),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
// "<hex> <name>" - take the first field and nothing else.
|
|
150
|
+
const want = String(sumTxt).trim().split(/\s+/)[0].toLowerCase();
|
|
151
|
+
const got = sha256(bin);
|
|
152
|
+
if (!/^[0-9a-f]{64}$/.test(want)) {
|
|
153
|
+
die('the published checksum is not a sha256 digest, so the download cannot be verified.');
|
|
154
|
+
}
|
|
155
|
+
if (got !== want) {
|
|
156
|
+
die(
|
|
157
|
+
'CHECKSUM MISMATCH - refusing to run the downloaded binary.',
|
|
158
|
+
` expected ${want}\n got ${got}\n` +
|
|
159
|
+
` Nothing was executed. Please report this at https://github.com/${REPO}/issues`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 0o700 on the directory too, not just the file: a group-writable cache directory hands the
|
|
164
|
+
// same code execution to anyone who can create a file in it, whatever the binary's own mode.
|
|
165
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
166
|
+
// A random name with an exclusive-create flag. A predictable one is a symlink target someone
|
|
167
|
+
// can pre-place, and writeFileSync would follow it and write wherever it points.
|
|
168
|
+
const tmp = path.join(dir, `.whisper.${crypto.randomBytes(8).toString('hex')}.part`);
|
|
169
|
+
fs.writeFileSync(tmp, bin, { flag: 'wx', mode: 0o700 });
|
|
170
|
+
fs.renameSync(tmp, exe);
|
|
171
|
+
fs.writeFileSync(stamp, `${got}\n`, { mode: 0o600 });
|
|
172
|
+
note(`verified sha256 ${got.slice(0, 16)}... and cached at ${exe}`);
|
|
173
|
+
return exe;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const exe = await ensureBinary();
|
|
178
|
+
// Default to the MCP server; anything the caller passes is appended, so
|
|
179
|
+
// `npx @whisper-security/whisper-mcp --help` and the bare form both do the right thing.
|
|
180
|
+
const args = ['mcp', ...process.argv.slice(2)];
|
|
181
|
+
|
|
182
|
+
const child = spawn(exe, args, { stdio: 'inherit', env: process.env });
|
|
183
|
+
|
|
184
|
+
// Forward the signals a client uses to stop us, so the server shuts down cleanly
|
|
185
|
+
// instead of being orphaned when the MCP client goes away.
|
|
186
|
+
const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
187
|
+
for (const sig of SIGNALS) {
|
|
188
|
+
process.on(sig, () => {
|
|
189
|
+
if (!child.killed) child.kill(sig);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
child.on('error', (e) => die(`could not start the Whisper CLI: ${e.message}`));
|
|
193
|
+
child.on('exit', (code, signal) => {
|
|
194
|
+
if (signal) {
|
|
195
|
+
// Report the child's death the way a shell would, by dying the same way. Our own
|
|
196
|
+
// handlers above have to come off first: otherwise this re-raise is caught by them,
|
|
197
|
+
// turned into a kill of an already-dead child, and swallowed into exit 0.
|
|
198
|
+
for (const s of SIGNALS) process.removeAllListeners(s);
|
|
199
|
+
process.kill(process.pid, signal);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
process.exit(code === null ? 1 : code);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main().catch((e) => die(e && e.stack ? e.stack : String(e)));
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@whisper-security/whisper-mcp",
|
|
3
|
+
"version": "0.219.2",
|
|
4
|
+
"description": "Give an AI agent a real, routable IPv6 identity, verifiable egress and DNS policy. Keyless: verify any agent's identity and query the infrastructure graph. With a key: register, govern and route agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"agent-identity",
|
|
9
|
+
"ipv6",
|
|
10
|
+
"rdap",
|
|
11
|
+
"dnssec",
|
|
12
|
+
"dane",
|
|
13
|
+
"egress",
|
|
14
|
+
"threat-intelligence",
|
|
15
|
+
"claude",
|
|
16
|
+
"cursor",
|
|
17
|
+
"ai-agents"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://whisper.online",
|
|
20
|
+
"bugs": "https://github.com/whisper-sec/whisper-cli/issues",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/whisper-sec/whisper-cli.git",
|
|
24
|
+
"directory": "npm-mcp"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Whisper Security (viaGraph B.V.)",
|
|
28
|
+
"type": "commonjs",
|
|
29
|
+
"bin": {
|
|
30
|
+
"whisper-mcp": "bin/whisper-mcp.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/whisper-mcp.js",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|