feat(mcp): support modern requests and legacy compatibility

This commit is contained in:
Kaede Akino committed 2026-09-28 09:37:13 +08:00
1 parent 3ff8bbe114
commit 8c785aefcd
9 files changed
+407 -26

No files matched your search

Submodule AgentKit updated: f78416b2b8...fc40c02791.
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Exercise a built termind-mcp executable against a deterministic loopback server."""
import http.server
import json
import subprocess
import sys
import threading
def check(executable, modern):
observed = []
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_args):
pass
def do_POST(self):
message = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
observed.append((message["method"], dict(self.headers)))
reply = {"jsonrpc": "2.0", "id": message.get("id", 1)}
if message["method"] == "initialize":
reply["result"] = {"protocolVersion": "2024-11-05"}
status = 200
else:
reply["error"] = {
"code": -32022 if modern else -32602,
"message": "Test protocol error",
"data": {"supported": ["2026-07-28"]},
}
status = 400
data = json.dumps(reply).encode()
self.send_response(status)
if message["method"] == "initialize":
self.send_header("Mcp-Session-Id", "test-session")
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_DELETE(self):
observed.append(("DELETE", dict(self.headers)))
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()
server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
call = {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "中文", "arguments": {}}}
messages = []
if modern:
call["params"]["_meta"] = {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
}
else:
messages.append({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05"}})
messages.append(call)
try:
result = subprocess.run(
[executable, "--url", f"http://127.0.0.1:{server.server_port}/mcp", "--token", "test-only"],
input="".join(json.dumps(message) + "\n" for message in messages),
capture_output=True, text=True, timeout=10, check=True,
)
replies = [json.loads(line) for line in result.stdout.splitlines()]
assert replies[-1]["error"]["code"] == (-32022 if modern else -32602), replies
call_headers = {key.lower(): value for method, headers in observed if method == "tools/call" for key, value in headers.items()}
if modern:
assert call_headers["mcp-method"] == "tools/call"
assert call_headers["mcp-name"] == "=?base64?5Lit5paH?="
assert call_headers["mcp-protocol-version"] == "2026-07-28"
assert "mcp-session-id" not in call_headers
assert len(observed) == 1, observed
else:
assert call_headers["mcp-session-id"] == "test-session"
assert call_headers["mcp-protocol-version"] == "2024-11-05"
assert observed[-1][0] == "DELETE"
finally:
server.shutdown()
server.server_close()
thread.join()
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Usage: python3 Scripts/check-mcp-bridge.py /path/to/termind-mcp")
check(sys.argv[1], modern=True)
check(sys.argv[1], modern=False)
print("MCP bridge modern and legacy headers, lifecycle, and JSON-RPC error passthrough passed.")
+3
View File
@@ -125,6 +125,9 @@ final class MCPServerController {
disabledTools, hosts: hosts, metrics: metrics, connections: connections,
authentication: authentication, background: background
)
if !enabled {
Task { [protocolServer] in await protocolServer.revokeModernOperations() }
}
}
func rotateToken() {
+162 -8
View File
@@ -6,7 +6,8 @@ actor MCPServerProtocol {
static let versionHeader = "mcp-protocol-version"
static let maximumSessions = 64
static let idleTimeout = Duration.seconds(30 * 60)
static let supportedStreamableVersions = [MCPClient.preferredVersion]
static let supportedInitializationVersions = ["2025-11-25", "2025-06-18", "2024-11-05"]
static let modernVersion = "2026-07-28"
typealias ClientCountHandler = @Sendable (Int) -> Void
@@ -64,6 +65,11 @@ actor MCPServerProtocol {
clientCountChanged(0)
}
func revokeModernOperations() async {
let ids = sessions.filter { $0.value.isModern }.map(\.key)
for id in ids { await removeSession(id) }
}
private func streamablePOST(_ request: HTTPRequest) async -> HTTPResponse {
guard request.header("content-type")?.lowercased().contains("application/json") == true else {
return .text(415, "MCP requests must use application/json.")
@@ -73,6 +79,13 @@ actor MCPServerProtocol {
return Self.jsonResponse(.error(id: .null, code: -32700, message: "Parse error"), status: 400)
}
if message.method == "server/discover"
|| message.params?["_meta"]?.objectValue?["io.modelcontextprotocol/protocolVersion"] != nil
|| request.header(Self.versionHeader) == Self.modernVersion
{
return await modernPOST(request, message: message)
}
if message.method == "initialize" {
guard sessions.count < Self.maximumSessions else {
return .text(503, "Too many MCP sessions are active.")
@@ -91,7 +104,7 @@ actor MCPServerProtocol {
guard let sessionID = request.header(Self.sessionHeader) else {
return .text(400, "Mcp-Session-Id is required after initialize.")
}
guard var session = sessions[sessionID], !session.isLegacy else {
guard var session = sessions[sessionID], !session.isLegacy, !session.isModern else {
return .text(404, "The MCP session is unknown or expired.")
}
guard let version = request.header(Self.versionHeader), version == session.protocolVersion else {
@@ -104,11 +117,152 @@ actor MCPServerProtocol {
return Self.jsonResponse(response)
}
private func modernPOST(_ request: HTTPRequest, message: RPCMessage) async -> HTTPResponse {
let meta = message.params?["_meta"]?.objectValue
let version = meta?["io.modelcontextprotocol/protocolVersion"]?.stringValue
guard request.header(Self.versionHeader) == version, version != nil,
request.header("mcp-method") == message.method,
message.method != "tools/call"
|| Self.decodeHeader(request.header("mcp-name")) == message.params?["name"]?.stringValue
else {
return Self.jsonResponse(
.error(id: message.id ?? .null, code: -32020, message: "Header mismatch"), status: 400)
}
guard version == Self.modernVersion else {
return Self.jsonResponse(
.error(
id: message.id ?? .null, code: -32022,
message: "Unsupported protocol version",
data: .object([
"supported": .array(
([Self.modernVersion] + Self.supportedInitializationVersions).map(AgentJSONValue.string)),
"requested": .string(version ?? ""),
])), status: 400)
}
let info = meta?["io.modelcontextprotocol/clientInfo"]?.objectValue ?? [:]
guard meta?["io.modelcontextprotocol/clientCapabilities"]?.objectValue != nil,
meta?["io.modelcontextprotocol/clientInfo"] == nil
|| (info["name"]?.stringValue != nil && info["version"]?.stringValue != nil),
let id = message.id
else {
return Self.jsonResponse(
.error(id: message.id ?? .null, code: -32602, message: "Missing request metadata"), status: 400)
}
let serverMeta: AgentJSONValue = .object([
"io.modelcontextprotocol/serverInfo": .object([
"name": .string("termind"), "version": .string(Bundle.termindDisplayVersion),
])
])
if message.method == "server/discover" {
return Self.jsonResponse(
.success(
id: id,
result: .object([
"resultType": .string("complete"),
"supportedVersions": .array(
([Self.modernVersion] + Self.supportedInitializationVersions).map(AgentJSONValue.string)),
"capabilities": .object(["tools": .object([:])]), "_meta": serverMeta,
"instructions": .string(
"For related tool calls, pass the returned operation_handle. Treat this opaque handle as a capability; it expires with the operation or server lifetime."
),
])))
}
if message.method == "tools/list" {
let descriptors = await tools.descriptors
return Self.jsonResponse(
.success(
id: id,
result: .object([
"resultType": .string("complete"), "_meta": serverMeta,
"tools": .array(
descriptors.map { descriptor in
var tool = Self.toolJSON(descriptor).objectValue ?? [:]
var schema = descriptor.inputSchema.objectValue ?? [:]
var properties = schema["properties"]?.objectValue ?? [:]
properties["operation_handle"] = .object([
"type": .string("string"),
"description": .string(
"Opaque capability returned by an earlier related tool call. Required when using its secret handle, command, or transfer ticket."
),
])
schema["properties"] = .object(properties)
tool["inputSchema"] = .object(schema)
tool["description"] = .string(
descriptor.summary
.replacingOccurrences(
of: "same live MCP session", with: "same live operation_handle"
)
.replacingOccurrences(
of: "ticket/session/process", with: "ticket/operation/process"))
return .object(tool)
}),
])))
}
guard message.method == "tools/call" else {
return Self.jsonResponse(.error(id: id, code: -32601, message: "Method not found"), status: 404)
}
guard message.params?["arguments"] == nil || message.params?["arguments"]?.objectValue != nil else {
return Self.jsonResponse(.error(id: id, code: -32602, message: "Arguments must be an object"))
}
var arguments = message.params?["arguments"]?.objectValue ?? [:]
let suppliedHandle = arguments.removeValue(forKey: "operation_handle")
let operationID: String
if let suppliedHandle {
guard let handle = suppliedHandle.stringValue, sessions[handle]?.isModern == true else {
return Self.jsonResponse(
.error(id: id, code: -32602, message: "Operation handle is unknown or expired"))
}
operationID = handle
} else {
guard sessions.count < Self.maximumSessions else { return .text(503, "Too many active operations") }
operationID = UUID().uuidString + UUID().uuidString
sessions[operationID] = MCPServerSession(
id: operationID, runID: UUID(), secretBroker: SecretBroker(), protocolVersion: Self.modernVersion,
clientName: info["name"]?.stringValue ?? "MCP client", isModern: true, initialized: true,
isLegacy: false)
}
var call = message
call.params?["arguments"] = .object(arguments)
guard let response = await dispatch(call, sessionID: operationID) else { return HTTPResponse(status: 202) }
let successful = response.value.objectValue?["result"]?.objectValue?["isError"] == .bool(false)
let retainsState =
suppliedHandle != nil
|| (successful
&& ["request_user_secret", "sftp_upload", "sftp_download"].contains(
message.params?["name"]?.stringValue ?? ""))
if !retainsState { await removeSession(operationID) }
var envelope = response.value.objectValue ?? [:]
if var result = envelope["result"]?.objectValue {
result["resultType"] = .string("complete")
if retainsState {
var structured = result["structuredContent"]?.objectValue ?? [:]
structured["operation_handle"] = .string(operationID)
result["structuredContent"] = .object(structured)
var content = result["content"]?.arrayValue ?? []
content.append(.object(["type": .string("text"), "text": .string("operation_handle: " + operationID)]))
result["content"] = .array(content)
}
envelope["result"] = .object(result)
}
return Self.jsonResponse(RPCResponse(value: .object(envelope)))
}
private static func decodeHeader(_ value: String?) -> String? {
guard let value, value.utf8.allSatisfy({ $0 == 9 || (32...126).contains($0) }),
value == value.trimmingCharacters(in: .whitespacesAndNewlines)
else { return nil }
if value.hasPrefix("=?base64?"), value.hasSuffix("?=") {
guard let data = Data(base64Encoded: String(value.dropFirst(9).dropLast(2))) else { return nil }
return String(data: data, encoding: .utf8)
}
return value
}
private func deleteSession(_ request: HTTPRequest) async -> HTTPResponse {
guard let id = request.header(Self.sessionHeader) else {
return .text(400, "Mcp-Session-Id is required.")
}
guard let removed = sessions.removeValue(forKey: id) else {
guard let existing = sessions[id], !existing.isModern, let removed = sessions.removeValue(forKey: id) else {
return .text(404, "The MCP session is unknown or expired.")
}
removed.legacyContinuation?.finish()
@@ -328,7 +482,7 @@ actor MCPServerProtocol {
return MCPServerSession(
id: id, runID: UUID(), secretBroker: SecretBroker(),
protocolVersion: legacy
? MCPClient.legacyVersion : MCPClient.preferredVersion,
? MCPClient.legacyVersion : "2025-11-25",
clientName: message.clientName ?? "MCP client", isLegacy: legacy
)
}
@@ -370,8 +524,8 @@ actor MCPServerProtocol {
private func sessionCountDidChange() { clientCountChanged(sessions.count) }
static func negotiatedStreamableVersion(_ requested: String?) -> String {
guard let requested, supportedStreamableVersions.contains(requested) else {
return MCPClient.preferredVersion
guard let requested, supportedInitializationVersions.contains(requested) else {
return "2025-11-25"
}
return requested
}
@@ -471,12 +625,12 @@ private nonisolated struct RPCResponse: Sendable {
Self(value: .object(["jsonrpc": .string("2.0"), "id": id, "result": result]))
}
static func error(id: AgentJSONValue, code: Int, message: String) -> Self {
static func error(id: AgentJSONValue, code: Int, message: String, data: AgentJSONValue = .null) -> Self {
Self(
value: .object([
"jsonrpc": .string("2.0"), "id": id,
"error": .object([
"code": .number(Double(code)), "message": .string(message),
"code": .number(Double(code)), "message": .string(message), "data": data,
]),
]))
}
@@ -7,6 +7,7 @@ nonisolated struct MCPServerSession: Sendable {
var secretBroker: SecretBroker
var protocolVersion: String
var clientName: String
var isModern = false
var initialized = false
var isLegacy: Bool
var legacyContinuation: AsyncStream<Data>.Continuation?
+117 -11
View File
@@ -15,6 +15,112 @@ import struct TermindDomain.Host
@Suite(.serialized)
@MainActor
struct MCPServerSessionTests {
@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 == 4)
let listed = await server.handle(request("tools/list"))
#expect(try responseObject(listed)["result"]?.objectValue?["tools"]?.arrayValue?.count == 18)
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-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
func streamableHandshakeMintsASessionAndPreservesStringIDs() async throws {
let protocolServer = MCPServerProtocol(tools: makeMCPServerTools())
@@ -32,7 +138,7 @@ struct MCPServerSessionTests {
#expect(initialized["id"]?.stringValue == "request-001")
#expect(
initialized["result"]?.objectValue?["protocolVersion"]?.stringValue
== MCPClient.preferredVersion
== "2025-11-25"
)
let instructions = try #require(
initialized["result"]?.objectValue?["instructions"]?.stringValue
@@ -46,14 +152,14 @@ struct MCPServerSessionTests {
let notification = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: MCPClient.preferredVersion
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: MCPClient.preferredVersion
sessionID: sessionID, version: "2025-11-25"
))
let tools = try #require(
try responseObject(listed)["result"]?.objectValue?["tools"]?.arrayValue
@@ -101,13 +207,13 @@ struct MCPServerSessionTests {
let initialized = await server.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: MCPClient.preferredVersion
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: MCPClient.preferredVersion))
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))
@@ -115,7 +221,7 @@ struct MCPServerSessionTests {
jsonRequest(
"""
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_metrics","arguments":{"host_id":"\(host.id.uuidString)","include":["gpu"]}}}
""", sessionID: sessionID, version: MCPClient.preferredVersion))
""", 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)
@@ -167,13 +273,13 @@ struct MCPServerSessionTests {
_ = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: MCPClient.preferredVersion
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: MCPClient.preferredVersion
sessionID: sessionID, version: "2025-06-18"
))
let code = try responseObject(response)["error"]?.objectValue?["code"]?.integerValue
#expect(code == -32602)
@@ -227,7 +333,7 @@ struct MCPServerSessionTests {
_ = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
sessionID: sessionID, version: MCPClient.preferredVersion
sessionID: sessionID, version: "2025-06-18"
))
let hostID = UUID()
let call = Task {
@@ -240,7 +346,7 @@ struct MCPServerSessionTests {
"consumer_tool":"ssh_exec","purpose":"sudo password"
}
}}
""", sessionID: sessionID, version: MCPClient.preferredVersion))
""", sessionID: sessionID, version: "2025-06-18"))
}
try await waitForProtocolCondition {
if case .secret? = authentication.current?.request { return true }
@@ -264,7 +370,7 @@ struct MCPServerSessionTests {
let afterCancellation = await protocolServer.handle(
jsonRequest(
#"{"jsonrpc":"2.0","id":3,"method":"ping"}"#,
sessionID: sessionID, version: MCPClient.preferredVersion
sessionID: sessionID, version: "2025-06-18"
))
#expect(afterCancellation.status == 404)
}
+1 -1
View File
@@ -2866,7 +2866,7 @@
repositoryURL = "https://github.com/AkinoKaede/AgentKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.16.0;
minimumVersion = 0.17.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/AkinoKaede/AgentKit.git",
"state" : {
"revision" : "f78416b2b8523d09950872b96f0de841e2d704e8",
"version" : "0.16.0"
"revision" : "fc40c027917daef9ddcfe6c1ea3b0a03c9f7a2c5",
"version" : "0.17.0"
}
},
{
+30 -3
View File
@@ -29,8 +29,19 @@ struct TermindMCPBridge {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json, text/event-stream", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(configuration.token)", forHTTPHeaderField: "Authorization")
if let sessionID { request.setValue(sessionID, forHTTPHeaderField: "Mcp-Session-Id") }
if let protocolVersion {
let params = object["params"] as? [String: Any]
let meta = params?["_meta"] as? [String: Any]
let modernVersion = meta?["io.modelcontextprotocol/protocolVersion"] as? String
if let modernVersion {
request.setValue(modernVersion, forHTTPHeaderField: "MCP-Protocol-Version")
request.setValue(method, forHTTPHeaderField: "Mcp-Method")
if let name = (params?["name"] ?? params?["uri"]) as? String {
request.setValue(headerValue(name), forHTTPHeaderField: "Mcp-Name")
}
} else if let sessionID {
request.setValue(sessionID, forHTTPHeaderField: "Mcp-Session-Id")
}
if modernVersion == nil, let protocolVersion {
request.setValue(protocolVersion, forHTTPHeaderField: "MCP-Protocol-Version")
}
@@ -39,12 +50,18 @@ struct TermindMCPBridge {
guard let response = rawResponse as? HTTPURLResponse else {
throw BridgeError.notHTTP
}
if let assigned = response.value(forHTTPHeaderField: "Mcp-Session-Id"),
if modernVersion == nil, let assigned = response.value(forHTTPHeaderField: "Mcp-Session-Id"),
!assigned.isEmpty
{
sessionID = assigned
}
guard (200..<300).contains(response.statusCode) else {
if let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
envelope["jsonrpc"] as? String == "2.0", envelope["error"] is [String: Any]
{
write(data)
continue
}
if id != nil {
write(errorResponse(id: id, message: "Termind HTTP server returned \(response.statusCode)."))
} else {
@@ -88,6 +105,16 @@ struct TermindMCPBridge {
}
}
private static func headerValue(_ value: String) -> String {
if value.utf8.allSatisfy({ $0 == 9 || (32...126).contains($0) }),
value == value.trimmingCharacters(in: .whitespacesAndNewlines),
!(value.hasPrefix("=?base64?") && value.hasSuffix("?="))
{
return value
}
return "=?base64?" + Data(value.utf8).base64EncodedString() + "?="
}
private static func errorResponse(id: Any?, message: String) -> Data {
let object: [String: Any] = [
"jsonrpc": "2.0", "id": id ?? NSNull(),