I've built two Model Context Protocol servers that let an AI assistant act on my behalf. One sends email, edits my calendar and writes to spreadsheets in Google Workspace. The other reads, posts, votes and sends private messages on Reddit. I also built mcp-surface, a command-line tool that connects to an MCP server the way a client does and reports what it actually exposes.
This page covers how the three fit together, and what a security and correctness review of both servers found in October 2026.
One check at the end of CI
A broken MCP server usually doesn't look broken. It starts, finishes the handshake and shows as connected, while the model gets a worse tool or none at all. Unit tests miss this, because they test the code behind each tool, not the list of tools a client is shown. So the last step in each server's CI runs mcp-surface, in the "In CI" row at the top of this page.
Any change to a tool's name, description, inputs or annotations fails the build until the snapshot is updated in the same pull request, which turns every such change into a diff someone has to review.
$ npm run surface
google-workspace 2.0.0 (279ms)
23 tools · 0 resources · 0 prompts · capabilities: tools
⋮
Snapshot mcp-surface.json
~ tool calendar_create_event: description, inputSchema changed
~ tool calendar_delete_event: description, inputSchema changed
~ tool calendar_list_events: inputSchema changed
~ tool calendar_update_event: description, inputSchema changed
~ tool sheets_write_range: inputSchema changed
⋮
Run with --update-snapshot if these changes are intended.
It caught something on the first day. Upgrading the schema library from Zod 3 to Zod 4 looked routine, and all 30 unit tests still passed. mcp-surface reported that all 23 Google tools had changed: Zod 4 stopped marking objects as closed to extra properties, and added a 120-byte pattern to every email field, which pushed the server's tool definitions past their 16 KB size budget. I kept Zod 3.
The repositories
- google-workspace-mcp 23 tools for Gmail, Calendar, Drive, Sheets and YouTube, through my own Google OAuth client. It asks for no restricted scopes, so it can send mail but not read the inbox, and only sees Drive files it created. That keeps it out of the paid security assessment Google requires for restricted scopes.
- reddit-mcp 12 tools, five of which post, comment, vote, message or mark mail read under the account. OAuth with a long-lived refresh token, and access tokens renewed by a single shared request when parallel calls find one expired.
- mcp-surface The checker. TypeScript, 19 checks, published on npm with build provenance, and also usable as a GitHub Action.
What the review found
The review started from one assumption: every tool argument is written by a model, and a model that has just read a web page or an email can be steered by what it read. A server that sends email and writes to spreadsheets has to treat its inputs the way a web server treats a form submission. Each fix below shipped with a test that builds the bad input.
Header injection through an email subject
The Gmail tools wrote the subject, recipients and attachment names straight into the message
headers, so a subject of Hi\r\nBcc: someone@example.com added a hidden recipient. Line
breaks in those fields are now rejected, and non-ASCII text is encoded the way the email standards
require (RFC 2047 and 2231).
Formula injection into a spreadsheet
Sheet writes used Google's "as if typed" mode, so a value like
=IMPORTDATA("https://…"&A1) became a live formula that could send the sheet's
contents to any address. Values are now written as plain text unless the caller asks for formulas.
Requests that escaped their API path
Subreddit names, usernames and post ids went into Reddit request URLs unchecked. URLs resolve
.., so a subreddit named ../api/v1/me reached a different Reddit endpoint
with the account's access token attached, and percent-encoding doesn't help because it leaves
.. alone. Each value is now checked against what Reddit actually allows.
Reddit failures that looked like success
When Reddit rejects a post, for a misspelt subreddit or a rate limit, it still answers with HTTP 200 and puts the reasons in the body. The tool passed that through, so the assistant could report a post that was never made. Those responses are now errors that include Reddit's reasons. Ids had the same problem: Reddit's write endpoints silently ignore a comment id where a post id belongs, so every id is now type-checked before the request is sent.
Calendar invitations that were never sent
Google Calendar only emails attendees when the request asks it to, so "invite Alice" created an event Alice never heard about, even though the tool's description said she would. Invitations, updates and cancellations are now emailed by default, with an option to turn that off.
A consent flow anyone on the network could answer
Both servers get their tokens through a one-time browser sign-in that redirects to a small local web
server. That server listened on every network interface, and the Google flow didn't check that the
redirect came from the sign-in it started. It now listens on localhost only, the Google flow checks
an OAuth state value and uses PKCE, and token files are kept readable only by their owner.
282 MB of dependencies, cut to 45 MB
The googleapis package bundles every Google API there is. Switching the Google server to
the five per-API packages it actually uses shrank its installed dependencies by more than 80%.
The checker had a bug of its own
mcp-surface introduced itself to every server as version 0.1.0, a number written into the code at the first release and never updated. It now reads its version from its own package (merged after 0.3.0, so the next npm release carries it), and an end-to-end test checks what a server actually receives.
What both servers now have
- Tests that run without network access or credentials, on Node 22 and 24.
- A committed tool snapshot that CI checks the running server against.
- Read-only, destructive and idempotent annotations on every tool, so a client can approve reads automatically and ask before writes.
- A 30-second timeout on every request, trimmed error messages, and rate-limit errors that say when to retry.
- No open
npm auditadvisories at release, plus ESLint, Dependabot, a changelog and a security policy.
Limits
- The test suites run without network access or credentials, so they never call the real Google or Reddit APIs.
- By default, mcp-surface checks the tool list a server shows, not what its tools do when called. Three of its 19 checks run only with
--call.