Skip to content

Commit 63fd276

Browse files
authored
Implement in-memory / passthrough transport (#122)
* Implement in-memory / passthrough transport * Add InMemoryTransport to README
1 parent 9ad382d commit 63fd276

File tree

3 files changed

+607
-0
lines changed

3 files changed

+607
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ The Swift SDK provides multiple built-in transports:
777777
|-----------|-------------|-----------|----------|
778778
| [`StdioTransport`](/Sources/MCP/Base/Transports/StdioTransport.swift) | Implements [stdio transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdio) using standard input/output streams | Apple platforms, Linux with glibc | Local subprocesses, CLI tools |
779779
| [`HTTPClientTransport`](/Sources/MCP/Base/Transports/HTTPClientTransport.swift) | Implements [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) using Foundation's URL Loading System | All platforms with Foundation | Remote servers, web applications |
780+
| [`InMemoryTransport`](/Sources/MCP/Base/Transports/InMemoryTransport.swift) | Custom in-memory transport for direct communication within the same process | All platforms | Testing, debugging, same-process client-server communication |
780781
| [`NetworkTransport`](/Sources/MCP/Base/Transports/NetworkTransport.swift) | Custom transport using Apple's Network framework for TCP/UDP connections | Apple platforms only | Low-level networking, custom protocols |
781782

782783
### Custom Transport Implementation
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import Foundation
2+
import Logging
3+
4+
/// An in-memory transport implementation for direct communication within the same process.
5+
///
6+
/// - Example:
7+
/// ```swift
8+
/// // Create a connected pair of transports
9+
/// let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
10+
///
11+
/// // Use with client and server
12+
/// let client = Client(name: "MyApp", version: "1.0.0")
13+
/// let server = Server(name: "MyServer", version: "1.0.0")
14+
///
15+
/// try await client.connect(transport: clientTransport)
16+
/// try await server.connect(transport: serverTransport)
17+
/// ```
18+
public actor InMemoryTransport: Transport {
19+
/// Logger instance for transport-related events
20+
public nonisolated let logger: Logger
21+
22+
private var isConnected = false
23+
private var pairedTransport: InMemoryTransport?
24+
25+
// Message queues
26+
private var incomingMessages: [Data] = []
27+
private var messageContinuation: AsyncThrowingStream<Data, Swift.Error>.Continuation?
28+
29+
/// Creates a new in-memory transport
30+
///
31+
/// - Parameter logger: Optional logger instance for transport events
32+
public init(logger: Logger? = nil) {
33+
self.logger =
34+
logger
35+
?? Logger(
36+
label: "mcp.transport.in-memory",
37+
factory: { _ in SwiftLogNoOpLogHandler() }
38+
)
39+
}
40+
41+
/// Creates a connected pair of in-memory transports
42+
///
43+
/// This is the recommended way to create transports for client-server communication
44+
/// within the same process. The returned transports are already paired and ready
45+
/// to be connected.
46+
///
47+
/// - Parameter logger: Optional logger instance shared by both transports
48+
/// - Returns: A tuple of (clientTransport, serverTransport) ready for use
49+
public static func createConnectedPair(
50+
logger: Logger? = nil
51+
) async -> (client: InMemoryTransport, server: InMemoryTransport) {
52+
let clientLogger: Logger
53+
let serverLogger: Logger
54+
55+
if let providedLogger = logger {
56+
// If a logger is provided, use it directly for both transports
57+
clientLogger = providedLogger
58+
serverLogger = providedLogger
59+
} else {
60+
// Create default loggers with appropriate labels
61+
clientLogger = Logger(
62+
label: "mcp.transport.in-memory.client",
63+
factory: { _ in SwiftLogNoOpLogHandler() }
64+
)
65+
serverLogger = Logger(
66+
label: "mcp.transport.in-memory.server",
67+
factory: { _ in SwiftLogNoOpLogHandler() }
68+
)
69+
}
70+
71+
let clientTransport = InMemoryTransport(logger: clientLogger)
72+
let serverTransport = InMemoryTransport(logger: serverLogger)
73+
74+
// Perform pairing
75+
await clientTransport.pair(with: serverTransport)
76+
await serverTransport.pair(with: clientTransport)
77+
78+
return (clientTransport, serverTransport)
79+
}
80+
81+
/// Pairs this transport with another for bidirectional communication
82+
///
83+
/// - Parameter other: The transport to pair with
84+
/// - Important: This method should typically not be called directly.
85+
/// Use `createConnectedPair()` instead.
86+
private func pair(with other: InMemoryTransport) {
87+
self.pairedTransport = other
88+
}
89+
90+
/// Establishes connection with the transport
91+
///
92+
/// For in-memory transports, this validates that the transport is properly
93+
/// paired and sets up the message stream.
94+
///
95+
/// - Throws: MCPError.internalError if the transport is not paired
96+
public func connect() async throws {
97+
guard !isConnected else {
98+
logger.debug("Transport already connected")
99+
return
100+
}
101+
102+
guard pairedTransport != nil else {
103+
throw MCPError.internalError(
104+
"Transport not paired. Use createConnectedPair() to create paired transports.")
105+
}
106+
107+
isConnected = true
108+
logger.info("Transport connected successfully")
109+
}
110+
111+
/// Disconnects from the transport
112+
///
113+
/// This closes the message stream and marks the transport as disconnected.
114+
public func disconnect() async {
115+
guard isConnected else { return }
116+
117+
isConnected = false
118+
messageContinuation?.finish()
119+
messageContinuation = nil
120+
121+
// Notify paired transport of disconnection
122+
if let paired = pairedTransport {
123+
await paired.handlePeerDisconnection()
124+
}
125+
126+
logger.info("Transport disconnected")
127+
}
128+
129+
/// Handles disconnection from the paired transport
130+
private func handlePeerDisconnection() {
131+
if isConnected {
132+
messageContinuation?.finish(throwing: MCPError.connectionClosed)
133+
messageContinuation = nil
134+
isConnected = false
135+
logger.info("Peer transport disconnected")
136+
}
137+
}
138+
139+
/// Sends a message to the paired transport
140+
///
141+
/// Messages are delivered directly to the paired transport's receive queue
142+
/// without any additional encoding or framing.
143+
///
144+
/// - Parameter data: The message data to send
145+
/// - Throws: MCPError.internalError if not connected or no paired transport
146+
public func send(_ data: Data) async throws {
147+
guard isConnected else {
148+
throw MCPError.internalError("Transport not connected")
149+
}
150+
151+
guard let paired = pairedTransport else {
152+
throw MCPError.internalError("No paired transport")
153+
}
154+
155+
logger.debug("Sending message", metadata: ["size": "\(data.count)"])
156+
157+
// Deliver message to paired transport
158+
await paired.deliverMessage(data)
159+
}
160+
161+
/// Delivers a message from the paired transport
162+
private func deliverMessage(_ data: Data) {
163+
guard isConnected else {
164+
logger.warning("Received message while disconnected")
165+
return
166+
}
167+
168+
logger.debug("Message received", metadata: ["size": "\(data.count)"])
169+
170+
if let continuation = messageContinuation {
171+
continuation.yield(data)
172+
} else {
173+
// Queue message if stream not yet created
174+
incomingMessages.append(data)
175+
}
176+
}
177+
178+
/// Receives messages from the paired transport
179+
///
180+
/// - Returns: An AsyncThrowingStream of Data objects representing messages
181+
public func receive() -> AsyncThrowingStream<Data, Swift.Error> {
182+
return AsyncThrowingStream<Data, Swift.Error> { continuation in
183+
self.messageContinuation = continuation
184+
185+
// Deliver any queued messages
186+
for message in self.incomingMessages {
187+
continuation.yield(message)
188+
}
189+
self.incomingMessages.removeAll()
190+
191+
// Check if already disconnected
192+
if !self.isConnected {
193+
continuation.finish()
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)