Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions Sources/MCP/Server/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,17 @@ public actor Server {
case Initialize.name, Ping.name:
break
default:
try checkInitialized()
guard isInitialized else {
let response = AnyMethod.response(
id: request.id,
error: MCPError.invalidRequest("Server is not initialized")
)
if sendResponse {
try await send(response)
return nil
}
return response
}
}
}

Expand Down Expand Up @@ -853,8 +863,12 @@ public actor Server {

if configuration.strict {
// Check initialization state unless this is an initialized notification
if message.method != InitializedNotification.name {
try checkInitialized()
// Notifications never get a response, so drop them instead of throwing.
if message.method != InitializedNotification.name, !isInitialized {
await logger?.warning(
"Ignoring notification before initialization",
metadata: ["method": "\(message.method)"])
return
}
}

Expand Down Expand Up @@ -892,12 +906,6 @@ public actor Server {
}
}

private func checkInitialized() throws {
guard isInitialized else {
throw MCPError.invalidRequest("Server is not initialized")
}
}

/// Validate the client capabilities.
/// Throws an error if the server is configured to be strict and the capability is not supported.
private func validateClientCapability<T>(
Expand Down
44 changes: 44 additions & 0 deletions Tests/MCPTests/ServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,48 @@ struct ServerTests {
await clientTransport.disconnect()
await serverTransport.disconnect()
}

@Test("Strict mode rejects requests before initialization with an error response")
func testStrictModeRejectsRequestBeforeInitialize() async throws {
let transport = MockTransport()
let server = Server(
name: "TestServer",
version: "1.0",
capabilities: .init(tools: .init()),
configuration: .strict
)
await server.withMethodHandler(ListTools.self) { _ in
ListTools.Result(tools: [])
}
try await server.start(transport: transport)

try await transport.queue(request: ListTools.request(id: "early", .init()))
try await Task.sleep(for: .milliseconds(200))

#expect(await transport.sentMessages.count == 1)
let response = try #require(
await transport.decodeLastSentMessage() as Response<ListTools>?)
#expect(response.id == "early")
guard case .failure(let error) = response.result else {
Issue.record("Expected an error response, got \(response.result)")
return
}
#expect(error.code == -32600)

await server.stop()
}

@Test("Strict mode does not respond to notifications before initialization")
func testStrictModeIgnoresNotificationBeforeInitialize() async throws {
let transport = MockTransport()
let server = Server(name: "TestServer", version: "1.0", configuration: .strict)
try await server.start(transport: transport)

try await transport.queue(notification: RootsListChangedNotification.message())
try await Task.sleep(for: .milliseconds(200))

#expect(await transport.sentMessages.isEmpty)

await server.stop()
}
}