Skip to content

Commit 8fca578

Browse files
Merge pull request #20 from decibri/development
fix: strict input schemas, security policy, SDK floor raise for 0.2.1
2 parents 83f5718 + eee568b commit 8fca578

9 files changed

Lines changed: 217 additions & 17 deletions

File tree

.github/dependabot.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
open-pull-requests-limit: 5
8+
groups:
9+
npm-deps:
10+
patterns: ["*"]
11+
12+
- package-ecosystem: github-actions
13+
directory: "/"
14+
schedule:
15+
interval: weekly
16+
open-pull-requests-limit: 5
17+
groups:
18+
github-actions:
19+
patterns: ["*"]

.github/workflows/ci.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ jobs:
4747

4848
- name: Install dependencies
4949
# npm ci, not npm install: a green run proves the locked tree works,
50-
# not whatever npm resolved today. No --ignore-scripts, unlike
51-
# publish.yml: publishing has no use for the native postinstall, but
52-
# the smoke tests exercise the decibri binary, so it must run here.
50+
# not whatever npm resolved today. Scripts stay enabled, unlike
51+
# publish.yml: nothing in the tree declares an install script today
52+
# (decibri ships prebuilt binaries as optional dependencies), so
53+
# --ignore-scripts would change nothing, and the default keeps this
54+
# job faithful to what a consumer's npm install actually does.
5355
run: npm ci
5456

5557
- name: Diagnostic - raw device enumeration

CHANGELOG.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.1] - 2026-07-14
11+
12+
### Changed
13+
14+
- Every tool input schema now declares `additionalProperties: false` and an
15+
explicit `required` array. The MCP SDK does not enforce the input schema
16+
at the transport layer, so this does not change what the handlers accept;
17+
it tells validating clients and schema-aware layers to reject arguments
18+
the tool does not declare. No parameter is added, removed, or renamed,
19+
and every existing valid call is unaffected.
20+
- Raised the declared floor for `@modelcontextprotocol/sdk` from `^1.0.0`
21+
to `^1.25.2`, past the fixes for GHSA-w48q-cv73-mx4w (patched in 1.24.0)
22+
and GHSA-8r9q-7v3j-jr4g (patched in 1.25.2). Neither advisory is
23+
reachable in this server, which speaks stdio only and registers no
24+
resource templates, but the declared range is the only version
25+
constraint consumers get: npm does not ship a lockfile inside the
26+
package, so the floor must not admit a vulnerable version. This
27+
project's own resolution (1.29.0) is unchanged.
28+
29+
### Added
30+
31+
- `SECURITY.md`: a security policy covering responsible disclosure, what
32+
the server accesses on the machine it runs on, why it runs natively
33+
rather than in a container, and the supply chain posture of the
34+
published package.
35+
- Dependabot configuration covering npm dependencies and GitHub Actions
36+
workflow versions, on a weekly schedule.
37+
1038
## [0.2.0] - 2026-07-13
1139

1240
### Changed
@@ -112,7 +140,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
112140
listing, WAV output validation, and error responses.
113141
- Tag-triggered npm publish workflow.
114142

115-
[Unreleased]: https://github.com/decibri/mcp-listen/compare/v0.2.0...HEAD
143+
[Unreleased]: https://github.com/decibri/mcp-listen/compare/v0.2.1...HEAD
144+
[0.2.1]: https://github.com/decibri/mcp-listen/compare/v0.2.0...v0.2.1
116145
[0.2.0]: https://github.com/decibri/mcp-listen/compare/v0.1.3...v0.2.0
117146
[0.1.3]: https://github.com/decibri/mcp-listen/compare/v0.1.2...v0.1.3
118147
[0.1.2]: https://github.com/decibri/mcp-listen/compare/v0.1.1...v0.1.2

SECURITY.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Security
2+
3+
mcp-listen takes security seriously. It is an MCP server that captures audio from the user's microphone, can transcribe it locally, and can pass the transcribed text to a language model, so we are especially attentive to input validation, file handling, and what leaves the machine. If you believe you have found a security vulnerability in this repository, please report it as described below.
4+
5+
## Responsible Disclosure
6+
7+
We are strongly committed to the responsible disclosure of security vulnerabilities. Please follow these guidelines when reporting security issues:
8+
9+
- Email **security@decibri.com** with "SECURITY - mcp-listen" in the subject line.
10+
- Alternatively, use GitHub's [private vulnerability reporting](https://github.com/decibri/mcp-listen/security/advisories/new) to report directly through GitHub.
11+
- Please do not report security vulnerabilities through public GitHub issues.
12+
13+
When reporting, please include the following details where applicable:
14+
15+
- A description of the vulnerability and how it can be exploited
16+
- The affected version of the package
17+
- The platform and architecture (e.g. Windows x64, macOS arm64, Linux x64, Linux arm64)
18+
- The MCP client in use, if relevant
19+
- Steps to reproduce the issue
20+
- Any other relevant information that could help us fix the vulnerability
21+
22+
We review reports as quickly as possible and work with reporters to coordinate remediation and disclosure.
23+
24+
## What mcp-listen Accesses
25+
26+
Understanding what the server touches is useful context when assessing the impact of a report.
27+
28+
mcp-listen:
29+
30+
- Reads from audio input devices when a capture tool is invoked
31+
- Writes WAV files to the system temporary directory. `capture_audio` returns the recording's path to the caller; `voice_query` deletes its recording when the query completes.
32+
- Transcribes captured audio locally, in process, using whisper.cpp. Audio never leaves the machine.
33+
- Sends transcribed text to the local Ollama daemon at `127.0.0.1:11434` when `voice_query` is used. This is the only network call in the codebase. An Ollama daemon configured with cloud models will relay that text off the machine; that relay is a property of the user's Ollama configuration, not of mcp-listen, but it is worth knowing when assessing where text can travel.
34+
- Reads the `WHISPER_MODEL_PATH` environment variable to locate the local Whisper model. It reads no API credentials: none are used. The bundled Ollama client library can read `OLLAMA_API_KEY`, but it attaches that credential only to requests to `ollama.com`, and mcp-listen only ever calls the local daemon, so that code path is unreachable.
35+
36+
It does not collect telemetry or phone home. It runs with the privileges of the user who starts it.
37+
38+
## Why mcp-listen Runs Natively
39+
40+
mcp-listen does not ship a container image. This is a considered position, not an omission.
41+
42+
The server's purpose is to capture audio from the host machine's microphone, and containerisation is not a meaningful boundary for that job:
43+
44+
- On macOS and Windows, a container cannot reach the host microphone in any standard configuration: Linux containers run inside a virtual machine with no route to the host's audio hardware, and Windows-native containers have no audio capture support.
45+
- On Linux, a container can reach the microphone only by passing the audio device or the sound-server socket through the container boundary, which reopens exactly the boundary the container was supposed to provide.
46+
- Containerisation adds no consent layer either. Where the operating system has microphone consent controls (the per-app prompt on macOS, the desktop-app microphone privacy setting on Windows), they govern native processes; a containerised capture path would sit outside those controls, not strengthen them.
47+
48+
The runtime posture is the mitigation instead: the server speaks only stdio to the client that spawned it, opens no listening sockets, makes no network calls other than to the local Ollama daemon, executes no code at install time, and runs as the user who started it.
49+
50+
## Supply Chain and Package Integrity
51+
52+
mcp-listen is published to npm as a single JavaScript package. It ships no binaries of its own; the native audio layer comes from the `decibri` dependency, which has its own security policy.
53+
54+
### Build integrity
55+
56+
- The package is published exclusively from GitHub Actions on GitHub-hosted runners. Nothing is published manually.
57+
- Publishing is triggered only by tagged releases and is gated behind a protected GitHub environment requiring manual approval, restricted to `v*` tags.
58+
- A pre-publish verification gate asserts that every file the package ships is present in the tarball and fails the release if any credential, publisher binary, or test fixture is present.
59+
- The full build and release configuration is open source and auditable in `.github/workflows/publish.yml`.
60+
61+
### Publishing and authentication
62+
63+
- npm publishing uses Trusted Publishing via OIDC. No long-lived npm tokens are stored in the repository or CI system. Each publish uses a short-lived, workflow-specific credential issued by npm.
64+
- The package declares no install or post-install scripts, and neither does any package in its dependency tree. Installing mcp-listen executes no code on the consumer's machine.
65+
66+
### Provenance and attestation
67+
68+
- Every release publishes with npm provenance attestation, cryptographically linking the published version to the exact source commit and build workflow.
69+
- Provenance attestations are recorded in the public Sigstore transparency log and can be verified with `npm audit signatures`.
70+
71+
### Dependency monitoring
72+
73+
- Dependencies are continuously monitored by Dependabot for security advisories and version updates.
74+
- Every pull request and every push to `main` runs the test suite on Linux, macOS, and Windows across Node.js 18, 20, and 22.
75+
76+
## Supported Versions
77+
78+
This security policy applies to the following versions:
79+
80+
| Version | Supported |
81+
| --- | --- |
82+
| 0.2.x | Yes |
83+
| < 0.2 | No |
84+
85+
Security fixes are applied to the latest release only. mcp-listen is pre-1.0 and older versions are not backported. Upgrade to the latest release.
86+
87+
## Scope
88+
89+
**In scope:**
90+
91+
- The mcp-listen server itself: tool input handling, file handling, and the audio capture path
92+
- The published npm package and its release process
93+
- Anything that allows an untrusted MCP client to cause mcp-listen to act outside its intended behaviour
94+
95+
**Out of scope:**
96+
97+
- Vulnerabilities in dependencies. Report those to the dependency's maintainers. If a dependency vulnerability is exploitable *through* mcp-listen in a way specific to how mcp-listen uses it, that is in scope and we want to hear about it.
98+
- The behaviour of the language model or MCP client that invokes mcp-listen
99+
- Issues requiring an attacker to already have local access to the machine, since mcp-listen runs with the privileges of the user who starts it
100+
101+
## CVE Policy
102+
103+
For confirmed vulnerabilities, we will request a CVE identifier where appropriate and publish a GitHub Security Advisory with details of the issue, affected versions, and remediation steps. Security advisories are visible at the [mcp-listen security advisories page](https://github.com/decibri/mcp-listen/security/advisories).
104+
105+
## Security Best Practices for Users
106+
107+
- Keep your dependencies up to date regularly
108+
- Only install mcp-listen from the official npm registry
109+
- Verify provenance attestations on installed packages with `npm audit signatures`
110+
- Run `npm audit` regularly to check for known vulnerabilities in your dependency tree
111+
- Be aware that `voice_query` sends transcribed text to the local Ollama daemon, and that a daemon configured with cloud models will relay that text off the machine
112+
- Grant microphone access following the principle of least privilege
113+
114+
## Reporting Concerns About This Policy
115+
116+
If you have questions about this security policy itself, or suggestions for improvement, please open a regular issue on the repository. These are not security vulnerability reports and do not require private disclosure.
117+
118+
## Acknowledgments
119+
120+
Thank you to the researchers and community members who help keep mcp-listen users secure. If you report a valid vulnerability and would like public acknowledgment, we will credit you in the security advisory and release notes.
121+
122+
## Contact
123+
124+
For security questions, email **security@decibri.com** with "SECURITY - mcp-listen" in the subject line.

index.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const { chat } = require('./lib/llm');
1212

1313
// ── Server ─────────��───────────────────────��────────────────
1414

15+
// Tools only: no resources or resource templates are registered, and that
16+
// is load-bearing. The SDK's resources/read path has carried a ReDoS
17+
// advisory (GHSA-8r9q-7v3j-jr4g, patched in 1.25.2) that this server does
18+
// not reach only because it registers no resources. Anyone adding a
19+
// resource here must first confirm the SDK floor is current.
1520
const server = new Server(
1621
{ name: 'mcp-listen', version },
1722
{ capabilities: { tools: {} } }
@@ -24,7 +29,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2429
{
2530
name: 'list_audio_devices',
2631
description: 'List available audio input devices (microphones) on this machine. Each device has a numeric index, a human-readable name, and a stable id. Prefer the id when selecting a device: indexes can shift when devices are added or removed, and names are not unique.',
27-
inputSchema: { type: 'object', properties: {} }
32+
inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false }
2833
},
2934
{
3035
name: 'capture_audio',
@@ -40,7 +45,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4045
type: ['number', 'string'],
4146
description: 'Device to record from: the numeric index or the stable string id, both reported by list_audio_devices. Prefer the id; indexes can shift when devices are added or removed. Omit for system default microphone.'
4247
}
43-
}
48+
},
49+
required: [],
50+
additionalProperties: false
4451
}
4552
},
4653
{
@@ -73,7 +80,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
7380
type: 'string',
7481
description: 'System prompt for the LLM (default: You are a helpful assistant.)'
7582
}
76-
}
83+
},
84+
required: [],
85+
additionalProperties: false
7786
}
7887
}
7988
]

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcp-listen",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Give your AI agents the ability to listen. Microphone capture and speech-to-text tools for MCP-compatible agents.",
55
"mcpName": "io.github.decibri/mcp-listen",
66
"main": "index.js",
@@ -42,7 +42,7 @@
4242
"url": "https://github.com/decibri/mcp-listen/issues"
4343
},
4444
"dependencies": {
45-
"@modelcontextprotocol/sdk": "^1.0.0",
45+
"@modelcontextprotocol/sdk": "^1.25.2",
4646
"decibri": "^5.0.0"
4747
},
4848
"optionalDependencies": {
@@ -61,4 +61,4 @@
6161
"x64",
6262
"arm64"
6363
]
64-
}
64+
}

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"url": "https://github.com/decibri/mcp-listen",
77
"source": "github"
88
},
9-
"version": "0.2.0",
9+
"version": "0.2.1",
1010
"packages": [
1111
{
1212
"registryType": "npm",
1313
"identifier": "mcp-listen",
14-
"version": "0.2.0",
14+
"version": "0.2.1",
1515
"transport": {
1616
"type": "stdio"
1717
}

test/smoke.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,32 @@ async function run() {
123123
return;
124124
}
125125

126-
// Test 2: All 3 tools advertised
126+
// Test 2: All 3 tools advertised, each with a strict input schema. The
127+
// SDK does not enforce the input schema at the transport layer, so the
128+
// schema is the only place undeclared arguments can be rejected, and only
129+
// by a validating client or layer. That makes additionalProperties: false
130+
// and an explicit required array part of the tool contract: every schema
131+
// must declare both, even when required is empty.
127132
try {
128133
const res = await server.send('tools/list', {});
129134
const names = res.result.tools.map(t => t.name).sort();
130135
const expected = ['capture_audio', 'list_audio_devices', 'voice_query'];
131136
if (JSON.stringify(names) !== JSON.stringify(expected)) {
132137
throw new Error(`Expected tools ${expected}, got ${names}`);
133138
}
134-
log('pass', 'All 3 tools advertised');
139+
for (const tool of res.result.tools) {
140+
const schema = tool.inputSchema;
141+
if (!schema || schema.type !== 'object') {
142+
throw new Error(`${tool.name}: inputSchema must be an object schema`);
143+
}
144+
if (schema.additionalProperties !== false) {
145+
throw new Error(`${tool.name}: inputSchema must declare additionalProperties: false`);
146+
}
147+
if (!Array.isArray(schema.required)) {
148+
throw new Error(`${tool.name}: inputSchema must declare an explicit required array`);
149+
}
150+
}
151+
log('pass', 'All 3 tools advertised with strict input schemas');
135152
passed++;
136153
} catch (err) {
137154
log('fail', `All 3 tools advertised: ${err.message}`);

0 commit comments

Comments
 (0)