Files
Termind/TFMTests/MCPServerSessionTests.swift

595 lines
29 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import AgentKit
import Foundation
import TermindApplication
import TermindDomain
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
import struct TermindDomain.Host
@testable import Termind
@Suite(.serialized)
@MainActor
struct MCPServerSessionTests {
@Test(arguments: ["2025-11-25", "2025-06-18", "2025-03-26", "2026-07-28"], [false, true])
func realHTTPTransportSupportsInspectorRequests(_ version: String, closeConnection: Bool) async throws {
let http = LoopbackHTTPServer()
let server = MCPServerProtocol(tools: makeMCPServerTools())
var port: UInt16 = 0
for attempt in 0..<10 {
let (states, continuation) = AsyncStream<LoopbackHTTPServer.State>.makeStream()
port = UInt16.random(in: 49_152...60_000)
try await http.start(
port: port, token: "integration-test-token", stateHandler: { continuation.yield($0) },
handler: { await server.handle($0) }
)
var ready = false
for await state in states {
if case .ready = state {
ready = true
break
}
if case .failed(let message) = state {
await http.stop()
if attempt == 9 { throw NSError(domain: message, code: 1) }
break
}
}
if ready { break }
}
defer {
Task {
await http.stop()
await server.stop()
}
}
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 3
configuration.httpMaximumConnectionsPerHost = 1
let client = URLSession(configuration: configuration)
defer { client.invalidateAndCancel() }
let url = URL(string: "http://127.0.0.1:\(port)/mcp")!
func post(_ method: String, id: Int?, sessionID: String? = nil) async throws -> (Data, HTTPURLResponse) {
var params: [String: AgentJSONValue] = [:]
if method == "initialize" {
params = [
"protocolVersion": .string(version),
"clientInfo": .object(["name": .string("inspector-test"), "version": .string("1")]),
"capabilities": .object([:]),
]
} else {
params["_meta"] = .object(["progressToken": .string("inspector-progress")])
}
if version == "2026-07-28" {
params["_meta"] = .object([
"io.modelcontextprotocol/protocolVersion": .string(version),
"io.modelcontextprotocol/clientCapabilities": .object([:]),
])
}
var message: [String: AgentJSONValue] = [
"jsonrpc": .string("2.0"), "method": .string(method), "params": .object(params),
]
if let id { message["id"] = .number(Double(id)) }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = AgentJSONValue.object(message).encodedData
if closeConnection { request.setValue("close", forHTTPHeaderField: "Connection") }
request.setValue("Bearer integration-test-token", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json, text/event-stream", forHTTPHeaderField: "Accept")
request.setValue("http://localhost:6274", forHTTPHeaderField: "Origin")
request.setValue(version, forHTTPHeaderField: "MCP-Protocol-Version")
request.setValue(method, forHTTPHeaderField: "Mcp-Method")
if let sessionID { request.setValue(sessionID, forHTTPHeaderField: "Mcp-Session-Id") }
let (data, response) = try await client.data(for: request)
return (data, try #require(response as? HTTPURLResponse))
}
var sessionID: String?
if version != "2026-07-28" {
let (_, initialized) = try await post("initialize", id: 1)
#expect(initialized.statusCode == 200)
#expect(
initialized.value(forHTTPHeaderField: "Access-Control-Expose-Headers")?.contains("Mcp-Session-Id")
== true)
let assigned: String = try #require(initialized.value(forHTTPHeaderField: "Mcp-Session-Id"))
sessionID = assigned
let (data, notified) = try await post("notifications/initialized", id: nil, sessionID: sessionID)
#expect(notified.statusCode == 202)
#expect(data.isEmpty)
}
let (listed, response) = try await post("tools/list", id: 2, sessionID: sessionID)
#expect(response.statusCode == 200)
let envelope = try AgentJSONValue.decode(listed).objectValue
#expect(envelope?["result"]?.objectValue?["tools"]?.arrayValue?.count == 21)
if version == "2026-07-28" {
#expect(envelope?["result"]?.objectValue?["ttlMs"] == .number(0))
#expect(envelope?["result"]?.objectValue?["cacheScope"] == .string("private"))
let (discovered, _) = try await post("server/discover", id: 3)
let discovery = try AgentJSONValue.decode(discovered).objectValue?["result"]?.objectValue
#expect(discovery?["ttlMs"] == .number(0))
#expect(discovery?["cacheScope"] == .string("private"))
if !closeConnection {
Attachment.record(discovered, named: "mcp-modern-discover.json")
Attachment.record(listed, named: "mcp-modern-tools-list.json")
}
}
#expect(response.value(forHTTPHeaderField: "Access-Control-Allow-Origin") == "http://localhost:6274")
var preflight = URLRequest(url: url)
preflight.httpMethod = "OPTIONS"
preflight.setValue("http://localhost:6274", forHTTPHeaderField: "Origin")
preflight.setValue("POST", forHTTPHeaderField: "Access-Control-Request-Method")
preflight.setValue(
"authorization,content-type,mcp-session-id,mcp-protocol-version,mcp-method",
forHTTPHeaderField: "Access-Control-Request-Headers")
let (_, raw) = try await client.data(for: preflight)
#expect((raw as? HTTPURLResponse)?.statusCode == 204)
#expect(
(raw as? HTTPURLResponse)?.value(forHTTPHeaderField: "Access-Control-Allow-Origin")
== "http://localhost:6274")
preflight.setValue("http://127.0.0.1.attacker.com", forHTTPHeaderField: "Origin")
let (_, rejectedOrigin) = try await client.data(for: preflight)
#expect((rejectedOrigin as? HTTPURLResponse)?.statusCode == 403)
#expect((rejectedOrigin as? HTTPURLResponse)?.value(forHTTPHeaderField: "Access-Control-Allow-Origin") == nil)
preflight.setValue("http://localhost:6274", forHTTPHeaderField: "Origin")
preflight.setValue("x-unrecognized-policy", forHTTPHeaderField: "Access-Control-Request-Headers")
let (_, rejectedHeader) = try await client.data(for: preflight)
#expect((rejectedHeader as? HTTPURLResponse)?.statusCode == 400)
preflight.httpMethod = "POST"
let (_, unauthorized) = try await client.data(for: preflight)
#expect((unauthorized as? HTTPURLResponse)?.statusCode == 401)
#expect(
(unauthorized as? HTTPURLResponse)?.value(forHTTPHeaderField: "Access-Control-Allow-Origin")
== "http://localhost:6274")
preflight.httpMethod = "OPTIONS"
preflight.url = URL(string: "http://127.0.0.1:\(port)/uploads/not-a-ticket")!
let (_, uploadPreflight) = try await client.data(for: preflight)
#expect((uploadPreflight as? HTTPURLResponse)?.statusCode != 204)
#expect((uploadPreflight as? HTTPURLResponse)?.value(forHTTPHeaderField: "Access-Control-Allow-Origin") == nil)
await http.stop()
await server.stop()
}
@Test
func modernUploadHandleSupportsCrossRequestStatusAndIsRevokedOnStop() async throws {
let host = Host(name: "Upload", host: "upload.example")
let tools = makeMCPServerTools(hosts: [host])
tools.uploads.configure(port: 7391)
let server = MCPServerProtocol(tools: tools)
func call(_ name: String, _ arguments: [String: AgentJSONValue]) async -> HTTPResponse {
let body: AgentJSONValue = .object([
"jsonrpc": .string("2.0"), "id": .number(1), "method": .string("tools/call"),
"params": .object([
"name": .string(name), "arguments": .object(arguments),
"_meta": .object([
"io.modelcontextprotocol/protocolVersion": .string("2026-07-28"),
"io.modelcontextprotocol/clientInfo": .object([
"name": .string("same-name"), "version": .string("1"),
]),
"io.modelcontextprotocol/clientCapabilities": .object([:]),
]),
]),
])
return await server.handle(
HTTPRequest(
method: "POST", target: "/mcp", version: "HTTP/1.1",
headers: [
"content-type": "application/json", "mcp-protocol-version": "2026-07-28",
"mcp-method": "tools/call", "mcp-name": name,
], body: body.encodedData))
}
// Invalid stateful calls must not exhaust the 64-operation capacity.
for _ in 0..<70 {
let failed = await call("sftp_upload", [:])
#expect(failed.status == 200)
#expect(try responseObject(failed)["result"]?.objectValue?["isError"] == .bool(true))
}
let ordinary = await call("list_hosts", [:])
#expect(try responseObject(ordinary)["result"]?.objectValue?["isError"] == .bool(false))
let created = await call(
"sftp_upload",
[
"host_id": .string(host.id.uuidString), "remote_path": .string("/tmp/test"),
"byte_count": .number(0), "sha256": .string(String(repeating: "0", count: 64)),
])
let result = try #require(try responseObject(created)["result"]?.objectValue)
#expect(result["isError"] == .bool(false))
let payload = try #require(result["structuredContent"]?.objectValue)
let handle = try #require(payload["operation_handle"])
let uploadID = try #require(payload["upload_id"])
let isolated = await call("sftp_upload_status", ["upload_id": uploadID])
#expect(try responseObject(isolated)["result"]?.objectValue?["isError"] == .bool(true))
let failedRelated = await call("sftp_upload", ["operation_handle": handle])
#expect(try responseObject(failedRelated)["result"]?.objectValue?["isError"] == .bool(true))
let status = await call("sftp_upload_status", ["upload_id": uploadID, "operation_handle": handle])
#expect(try responseObject(status)["result"]?.objectValue?["isError"] == .bool(false))
await server.stop()
let revoked = await call("sftp_upload_status", ["upload_id": uploadID, "operation_handle": handle])
#expect(try responseObject(revoked)["error"]?.objectValue?["code"] == .number(-32602))
}
@Test
func modernRequestsAreIndependentAndValidateMirroredHeaders() async throws {
let server = MCPServerProtocol(tools: makeMCPServerTools())
func request(_ method: String, mirrored: String? = nil) -> HTTPRequest {
let body: AgentJSONValue = .object([
"jsonrpc": .string("2.0"), "id": .number(1), "method": .string(method),
"params": .object([
"_meta": .object([
"io.modelcontextprotocol/protocolVersion": .string("2026-07-28"),
"io.modelcontextprotocol/clientCapabilities": .object([:]),
])
]),
])
return HTTPRequest(
method: "POST", target: "/mcp", version: "HTTP/1.1",
headers: [
"content-type": "application/json", "mcp-protocol-version": "2026-07-28",
"mcp-method": mirrored ?? method,
], body: body.encodedData)
}
let discover = await server.handle(request("server/discover"))
#expect(discover.status == 200)
#expect(discover.headers["Mcp-Session-Id"] == nil)
let result = try #require(try responseObject(discover)["result"]?.objectValue)
#expect(result["resultType"] == .string("complete"))
#expect(result["supportedVersions"]?.arrayValue?.count == 5)
let listed = await server.handle(request("tools/list"))
#expect(try responseObject(listed)["result"]?.objectValue?["tools"]?.arrayValue?.count == 21)
let mismatch = await server.handle(request("tools/list", mirrored: "tools/call"))
#expect(mismatch.status == 400)
#expect(try responseObject(mismatch)["error"]?.objectValue?["code"] == .number(-32020))
await server.stop()
}
@Test(arguments: ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"])
func initializePreservesEachSupportedLegacyVersion(_ version: String) async throws {
let server = MCPServerProtocol(tools: makeMCPServerTools())
let response = await server.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"\(version)","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}
"""))
#expect(response.status == 200)
#expect(try responseObject(response)["result"]?.objectValue?["protocolVersion"] == .string(version))
#expect(response.headers["Mcp-Session-Id"] != nil)
await server.stop()
}
@Test(arguments: ["2025-03-26", "2025-06-18"])
func marchBatchesAndOptionalVersionHeaderAreVersionScoped(_ version: String) async throws {
let server = MCPServerProtocol(tools: makeMCPServerTools())
let initialized = await server.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"\(version)","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}
"""))
let id = try #require(initialized.headers["Mcp-Session-Id"])
let notification = #"{"jsonrpc":"2.0","method":"notifications/initialized"}"#
let notified = await server.handle(jsonRequest(notification, sessionID: id))
#expect(notified.status == (version == "2025-03-26" ? 202 : 400))
if version != "2025-03-26" {
_ = await server.handle(jsonRequest(notification, sessionID: id, version: version))
}
let batch =
#"[{"jsonrpc":"2.0","id":"list","method":"tools/list"},{"jsonrpc":"2.0","method":"notifications/initialized"},{"jsonrpc":"2.0","id":7,"method":"ping"},false]"#
let result = await server.handle(jsonRequest(batch, sessionID: id, version: version))
if version == "2025-03-26" {
#expect(result.status == 200)
guard case .data(let data) = result.body else { throw MCPProtocolTestFailure.streamingResponse }
let responses = try #require(try AgentJSONValue.decode(data).arrayValue)
#expect(responses.count == 3)
#expect(responses[0].objectValue?["id"] == .string("list"))
#expect(responses[0].objectValue?["result"]?.objectValue?["tools"]?.arrayValue?.count == 21)
#expect(responses[1].objectValue?["id"] == .number(7))
#expect(responses[2].objectValue?["error"]?.objectValue?["code"] == .number(-32600))
let notifications = await server.handle(jsonRequest("[" + notification + "]", sessionID: id))
#expect(notifications.status == 202)
let invalid = await server.handle(jsonRequest("[]", sessionID: id))
#expect(invalid.status == 400)
let wrongVersion = await server.handle(jsonRequest(batch, sessionID: id, version: "2025-06-18"))
#expect(wrongVersion.status == 400)
} else {
#expect(result.status == 400)
}
let deleted = await server.handle(
HTTPRequest(
method: "DELETE", target: "/mcp", version: "HTTP/1.1",
headers: ["mcp-session-id": id], body: Data()))
#expect(deleted.status == 200)
let expired = await server.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":9,"method":"ping"}"#,
sessionID: id, version: version))
#expect(expired.status == 404)
await server.stop()
}
@Test
func streamableHandshakeMintsASessionAndPreservesStringIDs() async throws {
let protocolServer = MCPServerProtocol(tools: makeMCPServerTools())
let initialize = await protocolServer.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":"request-001","method":"initialize","params":{
"protocolVersion":"1900-01-01","clientInfo":{"name":"Tests","version":"1"},
"capabilities":{}
}}
"""))
let sessionID = try #require(initialize.headers["Mcp-Session-Id"])
let initialized = try responseObject(initialize)
#expect(initialized["id"]?.stringValue == "request-001")
#expect(
initialized["result"]?.objectValue?["protocolVersion"]?.stringValue
== "2025-11-25"
)
let instructions = try #require(
initialized["result"]?.objectValue?["instructions"]?.stringValue
)
#expect(instructions.contains("request_user_secret"))
#expect(instructions.contains("ssh_exec_stdin (type=text/secret/eof)"))
#expect(instructions.contains("one-shot handle"))
#expect(instructions.contains("Termind handles SSH/SFTP authentication"))
#expect(instructions.contains("non-PTY commands"))
let notification = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: "2025-11-25"
))
#expect(notification.status == 202)
let listed = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#,
sessionID: sessionID, version: "2025-11-25"
))
let tools = try #require(
try responseObject(listed)["result"]?.objectValue?["tools"]?.arrayValue
)
#expect(tools.count == 21)
let readOnly = Set([
"list_hosts", "get_metrics", "request_user_secret", "ssh_exec_output", "ssh_exec_list",
"sftp_list", "sftp_stat", "sftp_read", "sftp_upload_status", "sftp_download", "sftp_download_status",
])
for tool in tools {
let name = try #require(tool.objectValue?["name"]?.stringValue)
let hint = tool.objectValue?["annotations"]?.objectValue?["readOnlyHint"]
#expect(hint == .bool(readOnly.contains(name)), "\(name)")
if name == "sftp_upload_status" || name == "sftp_download_status" {
#expect(tool.objectValue?["annotations"]?.objectValue?["openWorldHint"] == .bool(false))
}
if name == "sftp_download" {
let annotations = try #require(tool.objectValue?["annotations"]?.objectValue)
#expect(annotations["readOnlyHint"] == .bool(true))
#expect(annotations["destructiveHint"] == .bool(false))
#expect(annotations["idempotentHint"] == .bool(false))
#expect(annotations["openWorldHint"] == .bool(true))
}
if name == "sftp_copy" {
let annotations = try #require(tool.objectValue?["annotations"]?.objectValue)
#expect(annotations["destructiveHint"] == .bool(true))
#expect(annotations["idempotentHint"] == .bool(false))
#expect(annotations["openWorldHint"] == .bool(true))
}
}
}
@Test
func getMetricsIsListedAndReturnsStructuredCachedData() async throws {
let host = Host(name: "Monitor", host: "monitor.example")
let now = Date(timeIntervalSince1970: 1_800_000_000)
let reader = TestMetricsReader(
MetricsSnapshot(capturedAt: now, state: .live, point: MetricsPoint(date: now, cpu: 33)))
let server = MCPServerProtocol(tools: makeMCPServerTools(hosts: [host], metrics: reader))
let initialize = await server.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"Tests","version":"1"},"capabilities":{}}}"#
))
let sessionID = try #require(initialize.headers["Mcp-Session-Id"])
let initialized = await server.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: "2025-06-18"
))
#expect(initialized.status == 202)
let listed = await server.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#,
sessionID: sessionID, version: "2025-06-18"))
let tools = try #require(try responseObject(listed)["result"]?.objectValue?["tools"]?.arrayValue)
let descriptor = try #require(tools.first { $0.objectValue?["name"]?.stringValue == "get_metrics" })
#expect(descriptor.objectValue?["annotations"]?.objectValue?["readOnlyHint"] == .bool(true))
let response = await server.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_metrics","arguments":{"host_id":"\(host.id.uuidString)","include":["gpu"]}}}
""", sessionID: sessionID, version: "2025-06-18"))
let result = try #require(try responseObject(response)["result"]?.objectValue)
#expect(result["isError"] == .bool(false))
let data = try #require(result["structuredContent"]?.objectValue)
#expect(data["overview"]?.objectValue?["cpu_percent"] == .number(33))
#expect(data["gpu"]?.objectValue?["status"] == .string("probing"))
#expect(data["processes"] == nil)
#expect(reader.reads == [host.id])
}
@Test
func establishedSessionsRequireTheirVersionHeader() async throws {
let protocolServer = MCPServerProtocol(tools: makeMCPServerTools())
let initialize = await protocolServer.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","clientInfo":{"name":"Tests","version":"1"},
"capabilities":{}
}}
"""))
let sessionID = try #require(initialize.headers["Mcp-Session-Id"])
let missing = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#, sessionID: sessionID
))
#expect(missing.status == 400)
let wrong = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#,
sessionID: sessionID, version: MCPClient.legacyVersion
))
#expect(wrong.status == 400)
}
@Test
func malformedToolCallsUseInvalidParams() async throws {
let protocolServer = MCPServerProtocol(tools: makeMCPServerTools())
let initialize = await protocolServer.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","clientInfo":{"name":"Tests","version":"1"},
"capabilities":{}
}}
"""))
let sessionID = try #require(initialize.headers["Mcp-Session-Id"])
_ = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: "2025-06-18"
))
let response = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"missing","arguments":{}}}"#,
sessionID: sessionID, version: "2025-06-18"
))
let code = try responseObject(response)["error"]?.objectValue?["code"]?.integerValue
#expect(code == -32602)
}
@Test
func legacySSEEmitsEndpointBeforeMessages() async throws {
let protocolServer = MCPServerProtocol(tools: makeMCPServerTools())
let opened = await protocolServer.handle(
HTTPRequest(
method: "GET", target: "/sse", version: "HTTP/1.1", headers: [:], body: Data()
))
guard case .stream(let stream) = opened.body else {
Issue.record("Expected an SSE stream")
return
}
var iterator = stream.makeAsyncIterator()
let endpoint = String(decoding: try #require(await iterator.next()), as: UTF8.self)
#expect(endpoint.hasPrefix("event: endpoint\n"))
let marker = "sessionId="
let sessionID = try #require(endpoint.components(separatedBy: marker).last)
.trimmingCharacters(in: .whitespacesAndNewlines)
let posted = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#,
target: "/messages?sessionId=\(sessionID)"
))
#expect(posted.status == 202)
let message = String(decoding: try #require(await iterator.next()), as: UTF8.self)
#expect(message.hasPrefix("event: message\n"))
#expect(message.contains("\"id\":1"))
}
@Test(.requiresVisibleWindow)
func deletingASessionCancelsItsSecretPopupWithoutResurrectingTheSession() async throws {
let authentication = ExternalAuthCoordinator()
let protocolServer = MCPServerProtocol(
tools: makeMCPServerTools(
externalAuthentication: authentication
))
let initialize = await protocolServer.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","clientInfo":{"name":"Tests","version":"1"},
"capabilities":{}
}}
"""))
let sessionID = try #require(initialize.headers["Mcp-Session-Id"])
_ = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: "2025-06-18"
))
let call = Task {
await protocolServer.handle(
jsonRequest(
"""
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
"name":"request_user_secret","arguments":{
"prompt":"Password for sudo","purpose":"sudo password"
}
}}
""", sessionID: sessionID, version: "2025-06-18"))
}
try await waitForProtocolCondition {
if case .secret? = authentication.current?.request { return true }
return false
}
let deleted = await protocolServer.handle(
HTTPRequest(
method: "DELETE", target: "/mcp", version: "HTTP/1.1",
headers: [MCPServerProtocol.sessionHeader: sessionID], body: Data()
))
#expect(deleted.status == 200)
#expect(authentication.current == nil)
let cancelled = await call.value
let cancelledResult = try #require(
responseObject(cancelled)["result"]?.objectValue
)
#expect(cancelledResult["isError"]?.boolValue == true)
#expect(cancelledResult["structuredContent"] == nil)
let afterCancellation = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":3,"method":"ping"}"#,
sessionID: sessionID, version: "2025-06-18"
))
#expect(afterCancellation.status == 404)
}
}
private func jsonRequest(
_ json: String, target: String = "/mcp", sessionID: String? = nil,
version: String? = nil
) -> HTTPRequest {
var headers = ["content-type": "application/json"]
if let sessionID { headers[MCPServerProtocol.sessionHeader] = sessionID }
if let version { headers[MCPServerProtocol.versionHeader] = version }
return HTTPRequest(
method: "POST", target: target, version: "HTTP/1.1",
headers: headers, body: Data(json.utf8)
)
}
private func responseObject(_ response: HTTPResponse) throws -> [String: AgentJSONValue] {
guard case .data(let data) = response.body else {
throw MCPProtocolTestFailure.streamingResponse
}
return try #require(try AgentJSONValue.decode(data).objectValue)
}
private enum MCPProtocolTestFailure: Error { case streamingResponse, conditionTimedOut }
@MainActor
private func waitForProtocolCondition(_ condition: @MainActor () -> Bool) async throws {
for _ in 0..<100 {
if condition() { return }
await Task.yield()
}
throw MCPProtocolTestFailure.conditionTimedOut
}