# Shelf Watch #3: What good looks like on a hosted enterprise MCP server

> Source: <https://dev.to/bot2botlabs/shelf-watch-3-what-good-looks-like-on-a-hosted-enterprise-mcp-server-3jj7>
> Published: 2026-11-13 16:43:00+00:00

*Originally published in the [`mcp-ship-check`](https://github.com/bot2botlabs/mcp-ship-check) repo, home of the free, offline scanner this post describes.*

**Update (2026-11-11).** Since this post first ran, we've finished a static code audit of **300 MCP servers**: **10** had a verified code-level vulnerability. One is fixed and published ([`atlas-mcp-server`, GHSA-w3gh-28jp-2hmv](https://github.com/IamNishant51/atlas-mcp-server/security/advisories/GHSA-w3gh-28jp-2hmv)); the rest are being disclosed privately to their maintainers, and we name a project only after its fix ships. Separately, a `bridge` sweep of 116 registry-listed servers that document `mcp-remote` found **284 invocations across 87 repos, and 94% of them don't pin a version**. And the device-code HTTPS fix for CVE-2026-108716 has shipped in [`mcp-remote` v0.16.6](https://github.com/punkpeye/mcp-remote/commit/e4dc3713b0d6a727cbeb21f38324d20a6692dc5f) (`e4dc371`): if you use `--device-code`, pin `0.16.6` or later.

In Shelf Watch #2 we read the code of 194 MCP servers and found the usual failure modes: a tool argument reaching a shell, a database "read-only" guard that only checked the first keyword, an HTTP transport listening on every network interface. That post was about what goes wrong.

This one is about the other end of the shelf: what a well-guarded hosted MCP server should look like, especially one that runs for many tenants at once, like the servers MSPs and security vendors are starting to ship. Below is our **Enterprise MCP Reference Blueprint**: four controls we'd want on any multi-tenant MCP server. None of them needs an enterprise budget. A two-person team can build every one.

The blueprint draws on what we've seen in production hosted MCP architectures and on the MCP specification's `2025-11-25` [Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) and [Elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) sections. The specific limits and exclusions below are **our design targets**, not a description of any one product.

Then we'll look at the part of the connection a server can't protect on its own: the client-side bridge that many setups use to reach a remote MCP server.

**The blueprint.** Clients sign in with OAuth 2.1 and PKCE, as the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) describes, not with a static API key. The token's scopes map onto the product's existing role-based access, with separate read and write grants per area of the product. An agent gets exactly the access its operator's role already has, and reading never implies writing.

**Why it matters.** The MCP layer inherits the permission model your product has already battle-tested, instead of inventing a second, weaker one beside it. A leaked or over-shared token can only do what that user could already do.

**How a small team can copy it:**

`yourapp:invoices:read`, `yourapp:invoices:write`), and grant them from the roles you already have.
**The blueprint.** Cap requests per token. We use 100 per minute as a starting point for an interactive agent. Let each call touch one resource, and scope every write tool to a single subject: one device, process, file, user or incident. There's no "apply to all" write path.

**Why it matters.** Even with valid credentials, a confused or prompt-injected agent can't turn one bad call into a fleet-wide action. The worst case is bounded by design rather than by hoping the model behaves.

`all: true` flag.`maxItems`), and paginate reads.
**The blueprint.** Every write tool waits for synchronous, in-chat human approval before it runs. If the connecting client can't show that approval step, the call **fails closed** instead of running unapproved.

**Why it matters.** This is the property the vulnerable servers in Shelf Watch #2 lacked. They ran dangerous operations with no human in the loop at all. "Refuse when we can't ask" is the safe default; "run anyway" is how a missing UI feature becomes an incident.

`readOnlyHint` for reads, `destructiveHint` for anything that changes or deletes. In Shelf Watch #2, a third of the tools we could inspect declared neither.`elicitation` capability when they connect, so your server knows up front whether it can ask.
**The blueprint.** Tools that move money or entitlements (license allocation, trial changes, billing) aren't exposed to external MCP clients at all. If a vendor's own first-party assistant needs them, it gets them on a separate internal surface. The external surface is deliberately a subset of the internal one.

**Why it matters.** Not every tool belongs on a connector that any compatible client can reach. Anything that moves money, entitlements or billing state is a much bigger prize than a read-only query.

`msp-seat-reconcile`
All four controls above live on the server. But many clients reach a remote MCP server through a local bridge: most often the open-source [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) package, launched from the client's config with something like `npx -y mcp-remote@<version> https://…`. That bridge runs on the user's own machine, handles their login, and stores their tokens. A well-guarded server can't protect a user whose bridge is out of date.

Two public advisories show why this matters:

`mcp-remote` before `0.1.16`. A malicious server could put a crafted URL in its OAuth metadata and get it run as a command on the user's machine `0.8.0` through `0.14.3`. In the device-code login flow, the client secret and the issued tokens can be sent over plain HTTP if the authorization server's endpoints aren't HTTPS `--device-code`, so a normal browser login doesn't hit it.
`e4dc371`) now requires HTTPS for the device-authorization and token endpoints (HTTP only on loopback), and `--allow-http` doesn't relax that. Releases before it, `0.14.4`–` 0.16.5` included, lack the check, so if you use `0.16.6` or later.
Pinning a version is a tradeoff. An exact pin is reproducible and immune to a bad new release, but it stays frozen as advisories are published against it. An unpinned `npx mcp-remote` picks up fixes, but it also picks up whatever ships next. Our suggestion: **pin an exact version, then actually re-check it on a schedule.** That goes double if your product's docs tell customers which bridge to run: your setup snippet is part of your security surface.

To make the re-check cheap, we added a `bridge` check to our free, offline [`mcp-ship-check`](https://github.com/bot2botlabs/mcp-ship-check). It reads MCP client configs (`claude_desktop_config.json`, `.mcp.json`, `mcp.json`, including `.cursor/` and `.vscode/`) and the code blocks in your docs, and checks every `mcp-remote` invocation against known advisories:

```
npx github:bot2botlabs/mcp-ship-check bridge --strict
```

Here's what it reports on a sample config with three servers (re-run with v0.3.3 after the 0.16.6 fix):

```
✖ bridge: 1 config/Markdown file(s) scanned; 3 mcp-remote invocation(s); 3 error(s), 0 warning(s) — --strict: warnings are errors
✖ bridge-cve-2025-6514: claude_desktop_config.json:3 (acme-legacy): mcp-remote@0.1.15 is in CVE-2025-6514's affected range (< 0.1.16): OS command injection via a malicious server's OAuth authorization_endpoint — upgrade to 0.1.16 or later
✖ bridge-cve-2026-108716: claude_desktop_config.json:4 (acme): mcp-remote@0.12.0 is in CVE-2026-108716's affected range (>= 0.8.0, < 0.16.6; the advisory record lists 0.8.0–0.14.3; fixed upstream in 0.16.6 (e4dc371)): client secret and tokens can be sent over cleartext HTTP in the device-code grant; this invocation doesn't pass --device-code, so the vulnerable path isn't used as written — upgrade to 0.16.6 or later, which enforces HTTPS on the device-code endpoints
✖ bridge-cve-2026-108716: claude_desktop_config.json:5 (acme-ci): mcp-remote@0.16.5 is in CVE-2026-108716's affected range (>= 0.8.0, < 0.16.6; the advisory record lists 0.8.0–0.14.3; fixed upstream in 0.16.6 (e4dc371)): client secret and tokens can be sent over cleartext HTTP in the device-code grant; this invocation passes --device-code, so the vulnerable path is in use — upgrade to 0.16.6 or later, which enforces HTTPS on the device-code endpoints
FAIL (4 checks)
```

Two details matter here. First, the check never calls a version "patched" just because it sits above an advisory's listed range: it records a fixed release only once the fix is confirmed in the upstream source, and otherwise says the fix is unconfirmed. Second, the affected range follows the fix, not just the advisory record: `0.16.5` with `--device-code` is still flagged, because the HTTPS check first shipped in `0.16.6`, and an unpinned bridge run with `--device-code` gets a reminder to pin `0.16.6` or later. It also flags `--allow-http` pointed at a non-loopback `http://` URL, and an unpinned `mcp-remote` as information (since it can't be checked offline). Without `--strict` it only warns, so you can add it to CI without breaking anything on day one.

Server side:

`npx github:bot2botlabs/mcp-ship-check sinks --strict` on your source: no shell strings, unguarded paths, interpolated SQL or exposed HTTP transport.
Client side:

`npx github:bot2botlabs/mcp-ship-check bridge --strict` in CI, over your configs and docs.
*Shipping an MCP server and want a second set of eyes on its trust boundaries before launch? We do quick, hands-on reviews for small teams and solo builders — open an issue on [`bot2botlabs/mcp-ship-check`](https://github.com/bot2botlabs/mcp-ship-check) or reach out, and we'll take a look.*

**Sources**

*Bot2Bot Labs keeps a daily, public-data archive of the MCP shelf. This is the third in a series on what's actually out there.*
