vent-mcp
Allow your agent to complain before the same paper cut becomes tomorrow's bug.
vent-mcp is a small STDIO MCP server that gives agents a non-destructive
place to send process feedback, complaints, and friction reports while they are
working. The crate is named vent-mcp; the installed binary is named vent.
The Rust library surface is internal support for that shipped binary, not a
stable embedding API.
The idea pairs well with Benjamin Verbeek's Lovable talk, The agent that files its own bug reports: give the agent a low-friction route to report confusion, missing affordances, and repeated workflow failures while the context is still fresh.
Installation
Cargo (crates.io)
GitHub Releases
Download a prebuilt archive from the GitHub Releases page, extract it, and
place vent on your PATH.
From source
For a JSONL-only build without webhook/HTTP dependencies:
For a CLI-enabled JSONL-only build:
Then configure your MCP client to run:
CLI
With no arguments, vent runs as the STDIO MCP server.
The same binary can also be used directly from the shell when built with the
cli feature, which is enabled by default:
Tools
list_channels: lists configured channel names and descriptions.vent: sends a message to a channel and returns an ACK-only delivery result.
vent accepts:
If channel is omitted, the configured default_channel is used.
Configuration
Path precedence:
VENT_MCP_CONFIG$XDG_CONFIG_HOME/vent-mcp/config.toml~/.config/vent-mcp/config.toml
When the default path is missing, vent-mcp creates a usable default config.
If VENT_MCP_CONFIG points to a missing file, startup fails.
See configs/config.sample.toml.
Sinks
The default log sink writes JSONL events to vents.jsonl beside the config
file. If [logging].jsonl_dir is set, JSONL events are written there instead.
If a config has no sinks, vent uses the built-in log JSONL sink.
Webhook sinks POST the vent event as JSON. With no provider, the raw event is sent unchanged:
Each event includes an id, UTC timestamp, channel, message, and the project directory name. It does not include the full current working directory path.
Header values are read from environment variables. Webhook requests default to a
10 second timeout; set timeout_ms to override that per sink.
[[]]
= "webhook"
= "relay"
= "https://example.com/vent"
= 10000
= [
{ = "Authorization", = "VENT_MCP_WEBHOOK_AUTHORIZATION" },
]
Provider maps live in the same TOML config file. The left side is a vent event
field and the value is the dotted output JSON path. Numeric path segments create
arrays. If field_label_key is set, paths ending in .value also get a label
generated from the source key, such as channel to Channel.
[]
= "name"
= "content"
= "embeds.0.fields.0.value"
= "embeds.0.fields.1.value"
[[]]
= "webhook"
= "discord"
= "discord"
= "https://discord.com/api/webhooks/..."
Built-In Provider Defaults
vent-mcp ships a sane preconfigured provider list for common webhook receivers
in North America and Europe. These defaults are ready to use from the generated
config and configs/config.sample.toml: pick a
provider, add the webhook URL, and keep the mapping as-is unless the receiving
workflow needs a custom shape.
Automation hubs receive the full raw event. Chat providers receive the smallest useful message shape their incoming webhook supports.
| Provider | Target | Default payload |
|---|---|---|
zapier |
Webhooks by Zapier | Raw event fields |
make |
Make webhooks | Raw event fields |
n8n |
n8n Webhook node | Raw event fields |
pipedream |
Pipedream HTTP triggers | Raw event fields |
workato |
Workato webhooks | Raw event fields |
ifttt |
IFTTT Webhooks | value1, value2, value3 |
slack |
Slack incoming webhooks | text plus attachment fields |
mattermost |
Mattermost incoming webhooks | text plus attachment fields |
discord |
Discord Execute Webhook | content plus embed fields |
microsoft_teams |
Microsoft Teams incoming webhooks | text |
google_chat |
Google Chat incoming webhooks | text |
webex |
Webex incoming webhooks | markdown |
Telegram, WhatsApp Business, PagerDuty, and Opsgenie are not built-in defaults yet. They are useful targets, but they need static required fields or secrets outside the current dynamic field mapper: chat or recipient ids, message type constants, routing keys, event actions, bearer tokens, or API keys.
Zapier
Use a Zapier Catch Hook when you want Zapier to parse the JSON fields, or Catch Raw Hook when later Zap steps should receive the whole JSON body.
[]
= "id"
= "timestamp"
= "channel"
= "message"
= "project"
[[]]
= "webhook"
= "zapier"
= "zapier"
= "https://hooks.zapier.com/hooks/catch/123456/abcdef/"
= 10000
Zapier accepts valid JSON webhook payloads, so the default vent-mcp event body
can be sent directly.
The same raw mapping is used for make, n8n, pipedream, and workato.
Slack
The default Slack provider maps message to the top-level text fallback and
maps channel/project into attachment fields. Mattermost uses the same
Slack-compatible mapping.
[]
= "title"
= "text"
= "attachments.0.fields.0.value"
= "attachments.0.fields.1.value"
[[]]
= "webhook"
= "slack"
= "slack"
= "https://hooks.slack.com/services/..."
= 10000
= [
{ = "Authorization", = "VENT_MCP_SLACK_WEBHOOK_AUTHORIZATION" },
]
Discord
Discord expects at least one message field such as content, embeds,
components, a file, or a poll. The default Discord provider maps message to
content and maps channel/project into generated embed fields.
[]
= "name"
= "content"
= "embeds.0.fields.0.value"
= "embeds.0.fields.1.value"
[[]]
= "webhook"
= "discord"
= "discord"
= "https://discord.com/api/webhooks/..."
= 10000
= [
{ = "Authorization", = "VENT_MCP_DISCORD_WEBHOOK_AUTHORIZATION" },
]
Text Webhooks
Microsoft Teams, Google Chat, and Webex use message-only provider maps because their simple incoming webhook paths accept a single text or markdown body. The same sink shape works for all three:
[]
= "text"
[[]]
= "webhook"
= "microsoft_teams"
= "microsoft_teams"
= "https://..."
= 10000