Skip to content

fix(filesystem): reject non-init requests before MCP handshake completes#4203

Open
nakshatra-nahar wants to merge 1 commit into
modelcontextprotocol:mainfrom
nakshatra-nahar:fix/filesystem-handshake-gating
Open

fix(filesystem): reject non-init requests before MCP handshake completes#4203
nakshatra-nahar wants to merge 1 commit into
modelcontextprotocol:mainfrom
nakshatra-nahar:fix/filesystem-handshake-gating

Conversation

@nakshatra-nahar
Copy link
Copy Markdown

@nakshatra-nahar nakshatra-nahar commented May 19, 2026

Description

Currently secure-filesystem-server accepts and successfully handles tools/list (and other requests) before the MCP lifecycle handshake completes. Per the MCP spec, the only methods a server should accept pre-handshake are initialize and ping. This PR adds a pre-dispatch gate that rejects all other methods with JSON-RPC InvalidRequest (-32600) until the handshake finishes.

Server Details

  • Server: filesystem (@modelcontextprotocol/server-filesystem)
  • Changes to: lifecycle handling in src/filesystem/index.ts + new integration test file

Motivation and Context

Closes #4195.

Returning a normal JSON-RPC result for tools/list (or anything else) before handshake:

  • Masks misconfigured clients that skip initialize.
  • Weakens assumptions used by security tooling reasoning about session state.
  • Makes session-scoped state (capabilities, auth context, subscriptions) harder to reason about.

Implementation note (please flag if a different shape is preferred): the MCP TypeScript SDK does not currently expose a middleware or pre-dispatch hook for request gating, so installHandshakeGate() wraps the protocol's internal _requestHandlers map. It is invoked once, after all server.registerTool(...) calls have run, and replaces each entry except initialize and ping with a thin wrapper that throws InvalidRequest until oninitialized has fired. A longer-term fix likely belongs at the SDK layer (modelcontextprotocol/typescript-sdk) so every reference server benefits; happy to file a follow-up issue there if you'd like.

How Has This Been Tested?

  • New file src/filesystem/__tests__/handshake-gating.test.ts adds two integration tests, both spawning dist/index.js and exchanging line-delimited JSON-RPC over stdio:
    1. tools/list sent without initialize → asserts response has error.code === -32600 and message matches /handshake|initialize/i, with no result key.
    2. Full handshake (initializenotifications/initializedtools/list) → asserts a populated result.tools array.
  • Verified Red-Green-Revert: stashed the index.ts change, rebuilt, re-ran the gating tests — Test 1 fails as expected, confirming the regression test catches the bug.
  • Full npm test --workspace=src/filesystem passes: 8 test files, 148/148 tests (146 pre-existing + 2 new).
  • npm run build --workspace=src/filesystem is clean (tsc exit 0, no warnings).

Breaking Changes

No breaking changes for spec-compliant clients (all official MCP clients perform the initialize handshake before sending other requests). Clients that incorrectly skip the handshake and previously got a successful response from tools/list will now receive a JSON-RPC InvalidRequest error — which is the correct, spec-mandated behaviour.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follow MCP security best practices
  • I have updated the server's README accordingly (no user-facing config or behaviour change; happy to add a note if maintainers want one)
  • I have tested this with an MCP client (the SDK's own Client exercises this path via the existing structured-content.test.ts suite)
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options (none added)

Additional context

The fix lives entirely inside src/filesystem/index.ts (single installHandshakeGate() function + one flag + one flag-set in the existing oninitialized callback). The other reference servers in this repo (everything, fetch, git, memory, sequentialthinking, time) have the same underlying SDK behaviour and would benefit from the same gating; I intentionally kept this PR scoped to filesystem to match the issue and to keep the diff reviewable. Happy to send follow-up PRs per server, or to wait for an SDK-level fix — whichever maintainers prefer.

Currently, the secure-filesystem-server accepts and successfully handles
tools/list (and other requests) before the MCP lifecycle handshake
completes. Per the MCP spec, the only methods a server should accept
pre-handshake are initialize and ping.

The MCP TypeScript SDK does not currently expose a middleware or
pre-dispatch hook for request gating, so we wrap the protocol's internal
request handler map. After all tool registrations have installed
SDK-managed tools/list and tools/call handlers, installHandshakeGate()
replaces each entry except initialize and ping with a wrapper that
throws InvalidRequest until oninitialized has fired.

Add an integration test that exercises both directions:
- tools/list before initialize -> rejected with code -32600
- full initialize -> notifications/initialized -> tools/list -> accepted

Closes modelcontextprotocol#4195
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filesystem: tools/list succeeds without initialize handshake (MCP lifecycle bypass)

1 participant