`swift-format lint --strict --recursive Termind TermindTests
TermindMCPBridge` is in the contribution instructions but had never been
run to completion: 4133 violations across 141 files, most of them
indentation and line breaking the pretty-printer would have settled on
its own.
`swift-format format -i` accounts for all but five. The rest are
lint-only rules it will not rewrite for you:
- `SSHKeyMaterial` declared its libssh2 entry point in the C symbol's
own spelling. `@_silgen_name` already carries the linkage name, so the
Swift side is free to be `libssh2PublicKeyFromPrivateMemory`.
- Four `.forEach { ... }` calls become for-in loops, two in
`PortForwardingController` and two in tests.
Formatting only, and checked as such rather than assumed: all 5409
string literals under `Termind` are byte-identical across the change, so
no localization key moved. Comparing whitespace-stripped token streams
leaves 43 files with any real change, and each is one of the rules
above, a dropped semicolon, a `case a, b` split onto separate lines with
its raw values kept explicit, or a file-scope `private` respelled
`fileprivate` — which is the same access level in that position.
Builds, and the suite stays at 975 of 975.
137 lines
5.5 KiB
Swift
137 lines
5.5 KiB
Swift
import Darwin
|
|
import Foundation
|
|
|
|
@main
|
|
struct TermindMCPBridge {
|
|
static func main() async {
|
|
let configuration = Configuration(arguments: Array(CommandLine.arguments.dropFirst()))
|
|
guard let url = URL(string: configuration.url), !configuration.token.isEmpty else {
|
|
fail("Usage: termind-mcp [--url URL] [--token TOKEN]")
|
|
exit(2)
|
|
}
|
|
|
|
let session = URLSession(configuration: .ephemeral)
|
|
var sessionID: String?
|
|
var protocolVersion: String?
|
|
|
|
while let line = readLine(strippingNewline: true) {
|
|
guard let input = line.data(using: .utf8),
|
|
let object = try? JSONSerialization.jsonObject(with: input) as? [String: Any]
|
|
else {
|
|
fail("Ignoring a stdin line that is not a JSON-RPC object.")
|
|
continue
|
|
}
|
|
let id = object["id"]
|
|
let method = object["method"] as? String
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.httpBody = input
|
|
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 {
|
|
request.setValue(protocolVersion, forHTTPHeaderField: "MCP-Protocol-Version")
|
|
}
|
|
|
|
do {
|
|
let (data, rawResponse) = try await session.data(for: request)
|
|
guard let response = rawResponse as? HTTPURLResponse else {
|
|
throw BridgeError.notHTTP
|
|
}
|
|
if let assigned = response.value(forHTTPHeaderField: "Mcp-Session-Id"),
|
|
!assigned.isEmpty
|
|
{
|
|
sessionID = assigned
|
|
}
|
|
guard (200..<300).contains(response.statusCode) else {
|
|
if id != nil {
|
|
write(errorResponse(id: id, message: "Termind HTTP server returned \(response.statusCode)."))
|
|
} else {
|
|
fail("Termind HTTP server returned \(response.statusCode) for \(method ?? "notification").")
|
|
}
|
|
continue
|
|
}
|
|
guard response.statusCode != 202, !data.isEmpty else { continue }
|
|
guard let responseObject = try? JSONSerialization.jsonObject(with: data),
|
|
JSONSerialization.isValidJSONObject(responseObject)
|
|
else {
|
|
if id != nil { write(errorResponse(id: id, message: "Termind returned invalid JSON.")) }
|
|
continue
|
|
}
|
|
if method == "initialize",
|
|
let envelope = responseObject as? [String: Any],
|
|
let result = envelope["result"] as? [String: Any],
|
|
let negotiated = result["protocolVersion"] as? String
|
|
{
|
|
protocolVersion = negotiated
|
|
}
|
|
write(try JSONSerialization.data(withJSONObject: responseObject))
|
|
} catch {
|
|
if id != nil {
|
|
write(errorResponse(id: id, message: "Cannot reach Termind: \(error.localizedDescription)"))
|
|
} else {
|
|
fail("Cannot deliver \(method ?? "notification"): \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
if let sessionID {
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "DELETE"
|
|
request.setValue("Bearer \(configuration.token)", forHTTPHeaderField: "Authorization")
|
|
request.setValue(sessionID, forHTTPHeaderField: "Mcp-Session-Id")
|
|
if let protocolVersion {
|
|
request.setValue(protocolVersion, forHTTPHeaderField: "MCP-Protocol-Version")
|
|
}
|
|
_ = try? await session.data(for: request)
|
|
}
|
|
}
|
|
|
|
private static func errorResponse(id: Any?, message: String) -> Data {
|
|
let object: [String: Any] = [
|
|
"jsonrpc": "2.0", "id": id ?? NSNull(),
|
|
"error": ["code": -32000, "message": message],
|
|
]
|
|
return (try? JSONSerialization.data(withJSONObject: object)) ?? Data()
|
|
}
|
|
|
|
private static func write(_ data: Data) {
|
|
FileHandle.standardOutput.write(data)
|
|
FileHandle.standardOutput.write(Data([0x0A]))
|
|
}
|
|
|
|
private static func fail(_ value: String) {
|
|
FileHandle.standardError.write(Data((value + "\n").utf8))
|
|
}
|
|
}
|
|
|
|
private struct Configuration {
|
|
var url: String
|
|
var token: String
|
|
|
|
init(arguments: [String], environment: [String: String] = ProcessInfo.processInfo.environment) {
|
|
url = environment["TERMIND_MCP_URL"] ?? "http://127.0.0.1:7391/mcp"
|
|
token = environment["TERMIND_MCP_TOKEN"] ?? ""
|
|
var index = 0
|
|
while index < arguments.count {
|
|
switch arguments[index] {
|
|
case "--url" where index + 1 < arguments.count:
|
|
index += 1
|
|
url = arguments[index]
|
|
case "--token" where index + 1 < arguments.count:
|
|
index += 1
|
|
token = arguments[index]
|
|
default:
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum BridgeError: LocalizedError {
|
|
case notHTTP
|
|
var errorDescription: String? { "Termind returned a non-HTTP response." }
|
|
}
|