Borrowing it
Nothing to install: this file belongs to windborne/zulipmcp. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.
curl -O https://raw.githubusercontent.com/windborne/zulipmcp/main/AGENTS.mdgit clone --depth 1 https://github.com/windborne/zulipmcpWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/instructions/windborne/zulipmcp/agents-md)<a href="https://agentmods.dev/instructions/windborne/zulipmcp/agents-md"><img src="https://agentmods.dev/badge/instructions/windborne/zulipmcp/agents-md/github.svg" alt="Measured on agentmods" height="20"></a>Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.
<a href="https://agentmods.dev/instructions/windborne/zulipmcp/agents-md"><img src="https://agentmods.dev/badge/instructions/windborne/zulipmcp/agents-md.svg" alt="Reviewed on agentmods" width="80" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.01506 | $0.01506 |
| Opus 5.5 | $0.00602 | $0.00602 |
| Sonnet 5.5 | $0.00301 | $0.00301 |
| Haiku 4.5 | $0.00151 | $0.00151 |
Grade B, and why
zulipmcp AGENTS.md scanned grade B with 1 finding against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 23d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Reads MCP configurationmediumAgent snooping
mcp.json carries server URLs and auth tokens; reading it lets a mod discover and abuse other integrations.
- **Codex MCP config is not `.mcp.json` native.** The listener translates `.mcp.json` into Codex `-c mcp_servers...` overrides. Keep secrets in env/header fields; env refs in command/args/cwd/url must fail closed to avoi How it starts
The opening of the file, as written. The whole thing — 40 lines — stays where its author put it; the contents beside it link to each section on GitHub.
zulipmcp
Public open-source repo. All code, commits, and PRs are visible to the world.
Rules
- Nothing WindBorne-specific. No internal URLs, emails, Zulip instances, or infra references.
- No internal context in PRs, commits, or comments.
- Elegant, general code. Type hints everywhere. Minimal dependencies.
Architecture
core.py— Zulip API wrappers. No MCP dependency. Returns Python objects.mcp.py— MCP tool layer +SessionState. Thin wrappers overcore.py. Hooks system lives here.agent_backends.py— Claude/Codex/OpenCode command builders for the listener. No Zulip API dependency.hermes_plugin/zulip/— Native Hermes gateway transport. It does not use MCP session state.
Separation is load-bearing: core.py must stay MCP-agnostic so it works as a standalone library import.
Gotchas
listen()is async, everything else is sync. It runsget_events()in a thread executor to interleave MCP keepalive pings. Don't make other tools async — FastMCP handles sync tools fine.- Event queue narrow does NOT filter reactions.
is_dismiss_reaction()receives reactions from all streams — it fetches the reacted-on message to verify stream/topic. Don't remove that check. _sessionis module-level singleton. One session per process. Don't add multi-session — MCP is one-client-per-server.reply()checks for missed messages before sending. Thelast_seen_message_idbookkeeping is subtle — trace it carefully before changing.- Send tools enforce
MAX_MESSAGE_LENGTH. Zulip silently truncates over-limit messages and still reports success — the guard returns an error to the caller instead. It measures the normalized form (postnormalize_zulip_markdown()), since normalization can grow content. Override withZULIP_MAX_MESSAGE_LENGTHfor non-default realm caps. - Two dismiss-check paths exist intentionally.
listen()catches via events,reply()via REST poll. Both needed — user might react during tool execution (not listening). - Interrupt-file reads must go through
_consume_interrupt_file(). It claims via atomic rename before reading, so a writer replacing the file mid-consume can't have its content deleted unread — and it decodes witherrors="replace"so a bad-bytes file can't wedge every future poll. Don't inline a read+delete elsewhere. ZULIPMCP_READ_ONLYis enforced only bycore._ReadOnlyAdapteronget_client()'s session (refuses every non-GET).WRITE_TAGhiding is UX; tag every new write tool. A barerequestscall or a secondzulip.Clientbypasses the guard, and a read needing a non-GET would weaken it, so add neither. Unknown flag values must keep raising.- Private stream security is asymmetric on purpose. Unset
BOT_ALLOWED_PRIVATE_STREAMS= no access (default-deny); only__ALL__unlocks DM reads. UnsetBOT_ALLOWED_WRITE_STREAMS= all writes allowed (backwards-compat). Don't "fix" the asymmetry. Tools don't re-check: new narrow reads must go throughfetch_narrow, whose_check_read_scoperefuses any narrow that doesn't name an allowed scope before fetching, then drops strays via_is_readable. Single-message reads that return content must go throughcore.get_message_by_id, whose_is_readablecall is their only gate. Never subtract hidden streams with negated terms instead: narrows ride the GET query string and break past ~50 terms. - Unread/mention markers read
flagsoff the message dict; missingflagsrenders no marker. A fetch path that drops them fails silently (message events carry them beside the message;get_eventsmerges them in).unreadis human-only (bots never mark read) and skips the account's own messages (API-sent messages start unread for their sender). <msg>blocks are a trust boundary.format_messagesbuilds them: bodies viaescape_msg_text(every<→<, Unicode tag characters dropped), attributes viaescape_msg_attr(adds",>); headers printed above blocks (set_context,get_messages) escape names too. Don't exempt code blocks or escape only tag-shaped<msg(anything reading surrounding text is bypassable), and don't escape names in output without blocks (nothing to protect; breaks round-tripping into tool args). Look-alikes are out of scope: this guards who said what, not injection in general.- Logging is configured in
run_server()only — not at import, not in__main__. Library importers own their root logger and must get no files; wrappers callrun_server()directly. configure()must be called beforerun_server().run_server()may auto-init a session that reads hook state.auto_typing_start=Falsegates starts only;listen()/end_session()still send "stop", andreply()sends "stop" in place of its start. Zulip keys the indicator per user, not per process, so gating stops can't isolate an external owner — it would only leave a stale bar on idle/exit, and a reply with no stop leaves the pre-reply status stranded under the new message until the server expiry. Don't gate the stops.set_context()stays a no-op there: a stop that early would wipe whatever the owner posted at boot.- Codex MCP config is not
.mcp.jsonnative. The listener translates.mcp.jsoninto Codex-c mcp_servers...overrides. Keep secrets in env/header fields; env refs in command/args/cwd/url must fail closed to avoid argv leaks. - OpenCode MCP config is not
.mcp.jsonnative. The listener translates.mcp.jsoninto inline JSON viaOPENCODE_CONFIG_CONTENT. Header values are embedded directly (no env-var indirection like Codex). Env refs in command/args/url are rejected. core.pysend paths (send_message/send_direct_message/edit_message) rewrite markdown vianormalize_zulip_markdown(). Blank lines are injected before tables and bold/link combos Zulip breaks on are rewritten. Fenced/indented code is exempt from both fixes; inline code spans are exempt only for the bold/link rewrite. The Hermes gateway adapter sends via the raw client and is NOT normalized.ZULIPMCP_MARKDOWN_AUTOFIX=0disables all of it.remove_reaction()needs explicitreaction_typefor custom emoji. Zulip's DELETE reactions endpoint defaultsreaction_typetounicode_emoji— custom emoji removals fail silently without it. The listen indicator caches_listen_reaction_typealongside_listen_emoji; always pass both when removing.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 23d ago First seen · 40 lines · 1,506 tokens per session scan B dbbf37211225
zulipmcp AGENTS.md is an instructions file published in the GitHub repository windborne/zulipmcp (1 stars, last pushed 24d ago), licensed Apache-2.0. It adds 1,506 tokens to every session, about $0.0060 per session on Opus 5.5. A static security scan graded it B with 1 finding (reads mcp configuration). It comes from a forked repository.
zulipmcp AGENTS.md compared with instructions from other repositories
| Mod | Type | Stars | Last push | Licence | Scan | Tokens | Works with |
|---|---|---|---|---|---|---|---|
| zulipmcp AGENTS.mdwindborne/zulipmcp | Instructions file | 1 | 24d ago | Apache-2.0 | B | 1,506 | Codex, OpenCode |
| vscode buildNext.instructions.mdmicrosoft/vscode | Instructions file | 195k | today | MIT | A | 9,719 | GitHub Copilot |
| vscode oss-third-party-notices.instructions.mdmicrosoft/vscode | Instructions file | 195k | today | MIT | A | 5,516 | GitHub Copilot |
| next.js AGENTS.mdvercel/next.js | Instructions file | 143k | yesterday | MIT | A | 7,461 | Codex, OpenCode |
| langchain AGENTS.mdlangchain-ai/langchain | Instructions file | 149k | today | MIT | A | 4,680 | Codex, OpenCode |
| vscode copilot-instructions.mdmicrosoft/vscode | Instructions file | 195k | today | MIT | A | 3,093 | GitHub Copilot |
| vscode model-prompts.instructions.mdmicrosoft/vscode | Instructions file | 195k | today | MIT | A | 2,022 | GitHub Copilot |