# Custom and internal server checks

For a server an agency or the company built, often over warehouse marts. Builders follow warehouse-and-internal-mcp when it is installed; this file is the reviewer's checklist of what to ask for, see tested and record. A code-level review of the source belongs to a security engineer or the client's code security review command; this skill checks behaviour and configuration a reviewer can test.

## Authentication

- Tokens are validated for this server's audience. Expired tokens, tokens for another audience and tokens from an unexpected issuer are rejected.
- No token passthrough: the server never forwards the client's token to an upstream API. It holds its own upstream credentials behind its service boundary.
- PKCE with S256, and HTTPS for every endpoint except loopback redirect URIs.
- Short-lived access tokens, and refresh token rotation for public clients.
- Issuer rules from the 2026-07-28 revision: if the server runs its own authorization server, it includes `iss` in authorization responses (RFC 9207). If it signs in to an upstream service as an OAuth client, it validates `iss` and keys stored client credentials by issuer.
- An OAuth proxy that uses one static client ID upstream must get consent per dynamically registered client, so a user always sees which client they are approving (the confused deputy problem). The 2026-07-28 revision deprecates Dynamic Client Registration in favour of Client ID Metadata Documents; ask which one the server supports.
- Missing audience validation or any token passthrough is blocker B6.

## Authorisation inside the data path

- The caller's allowed accounts come from the verified identity, never from an account parameter the caller supplies. Trusting a caller-supplied account is B8.
- Filtering happens before retrieval, not on the way out.
- A denial does not reveal whether the record exists.
- Caches are keyed by scope, parameters and data version, so two teams asking for the same report name never share a result.
- State handles (the 2026-07-28 revision removed protocol sessions, so a server that keeps state across calls mints a handle such as a workflow ID) are random, bound server-side to the verified user and never treated as authentication. A guessable or unbound handle lets one user act on another's state.

## Tool contracts

- Bounded inputs: explicit date ranges, row limits and scan limits (a bytes-billed cap on BigQuery, for example). A row limit and a scan limit solve different problems.
- A partial result says so, with a continuation, instead of looking complete.
- Unknown is kept distinct from zero: a missing usage feed is not zero usage.
- Each result states the freshness of each source.
- No general "run anything" tool. If SQL is exposed, it runs under a read-only role.
- Hard-to-reverse tools can carry `_meta["anthropic/requiresUserInteraction"]: true`, which makes Claude Code prompt on every call, even in auto and bypass modes. Other clients ignore it, so it is defence in depth, not an enforcing layer.

## Transport

- Local servers: stdio preferred. A local HTTP server binds to loopback and still needs real authentication plus Origin and Host validation; binding to 127.0.0.1 alone is not an authentication boundary. A non-loopback bind without authentication is B6.
- Remote servers: HTTPS only.

## Dependencies

- The MCP SDK and its exact version are named and checked against the GitHub Advisory Database on the review date. The TypeScript SDK v2 ships as separate packages (`@modelcontextprotocol/server`, `client`, `express` and `server-legacy`), so check every `@modelcontextprotocol/*` package in the lockfile, not only `@modelcontextprotocol/sdk`.
- Floor on 2026-11-06, the lowest versions no published advisory affects: `@modelcontextprotocol/sdk` 1.32.0, `server` and `client` 2.3.0, `server-legacy` 2.3.1, `express` 2.0.2, `node` 2.1.0, `hono` 2.0.1; Python `mcp` 1.30.0 or 2.2.0. Recheck before quoting: a version floor is not the audience check below.
- Examples from 2026, advisories read on 2026-11-06:
  - One client's responses reach another when a transport or server instance is shared between clients, the common stateless pattern: the TypeScript SDK 1.10.0 to 1.25.3 (CVE-2026-25536, fixed in 1.26.0) and `@modelcontextprotocol/server` 2.0.0 to before 2.3.0 (CVE-2026-111770, GHSA-jm63-cq47-j5jp, fixed in 2.3.0, which rejects shared use, so the server also needs a new instance per request or session). A server that shares one on an affected version is B6.
  - Bearer authentication accepted tokens issued for another service: the TypeScript SDK from 1.6.0 to before 1.32.0 (and `@modelcontextprotocol/server` below 2.3.0, `@modelcontextprotocol/express` below 2.0.2, `@modelcontextprotocol/server-legacy` below 2.3.1; CVE-2026-111771) and the Python SDK from 1.10.0 to before 1.30.0, and 2.0.0 to before 2.2.0 (CVE-2026-116684).
    - Both advisories say upgrading alone changes nothing: the fixed versions ship the audience check switched off. A server that relies on the SDK's bearer authentication stays B6 until one of two things is true, and test 3 below passes. Either it runs a fixed version with the check on (TypeScript: `expectedResource` passed to `requireBearerAuth()` and the token's audience returned as `AuthInfo.resource` from `verifyAccessToken()`; Python: `validate_token_resource=True` and `resource_server_url` in `AuthSettings`, with the audience returned as `AccessToken.resource` from `verify_token()`), or its verifier rejects any token whose `aud` does not name the server.
  - For custom clients and bridges: the OAuth client let the MCP server choose which authorization server received stored credentials, in the TypeScript SDK 1.12.0 to before 1.31.0 and `@modelcontextprotocol/client` 2.0.0 to before 2.2.0 (CVE-2026-104850, GHSA-6qxp-vccf-f47h), and in the Python SDK 1.9.1 to before 1.30.0 and 2.0.0a1 to before 2.2.0 (CVE-2026-115563, GHSA-qx49-fqc8-xw99). A match on the exact version is B7.

## Server-side request forgery

- Tools that fetch URLs use an allowlist and block cloud metadata endpoints and private address ranges.

## Logging

- Actor, tool, target, time, result and approval reference. Never secrets; personal data minimised (data-exposure.md).
- OpenTelemetry rather than the protocol's logging feature, which the 2026-07-28 revision deprecates.

## Tests to see run

Ask for dated results, or watch them run in a test environment:

1. Two users with different access ask the same question concurrently, with at least 20 overlapping requests; each gets only their own records.
2. An expired token is rejected.
3. A token issued for another service is rejected.
4. A removed user loses access on the next call.
5. An oversized request is refused, or truncated with a partial flag.
6. A stale source produces a stale label, not a confident answer.
7. An outsider-text canary from assets/injection-canaries.yaml on every tool that returns free text.
8. A fixture or mock response never appears as a production answer; results and logs carry the environment.

## Evidence the builder hands over

- The test results with dates.
- The deployed version or image digest.
- A tools/list capture and its fingerprint from `node scripts/review-mcp-tools.mjs`, saved as the baseline.
- Threat model notes: who can call which tool, which upstream credentials exist and where they are stored.
- The named owner for updates, incidents and revocation. No owner is B10.
