Understanding model context protocol transports
At Stainless, we generate client SDKs in various languages for our customers' APIs. Recently, those same customers started asking us for something new: a Model Context Protocol (MCP) server that wraps their API and exposes it as a set of tools for LLMs.
A transport in MCP is the layer that moves messages between clients and servers. Think of it as the delivery system for all communication. The transport handles reading and writing messages, formatting them correctly, and managing the connection.
MCP uses JSON-RPC 2.0 for its messages. This is a lightweight protocol that defines how to structure requests, responses, and notifications in JSON format. Every transport must support this format.
The standard transports built into MCP are:
stdio: Uses standard input and output streams, typically when the server runs as a subprocess of the client
Streamable HTTP: Uses HTTP POST requests for client-to-server communication and Server-Sent Events (SSE) for streaming from server to client
Here's where transport fits in the MCP architecture:
Why create a custom MCP transport
While the built-in transports work for many cases, sometimes they don't fit your specific needs. You might need a custom transport when:
Performance matters: When stdio or HTTP adds too much delay for your application
Framework integration: When you need to embed MCP in a system with its own messaging system
Security requirements: When you need special encryption or access controls
Custom protocols: When you already use WebSockets, ZeroMQ, or other protocols in your system
For example, at Stainless, we had a customer who needed to integrate MCP with their existing message queue system. The standard transports wouldn't work because their security model required all communication to go through this system. We helped them create a custom transport that maintained MCP compatibility while using their existing infrastructure.
Custom transports follow the same message structure as standard ones but let you control exactly how those messages travel between client and server.
Key requirements for a custom transport layer
To build a working custom transport, you need to follow several requirements that ensure it works properly with MCP clients and servers.
JSON-RPC compliance
MCP uses JSON-RPC 2.0 for all messages. Every transport must format messages according to this specification:
Requests include
jsonrpc
,id
,method
, and optionalparams
Responses include
jsonrpc
,id
, and eitherresult
orerror
Notifications look like requests but have no
id
and expect no response
Here's a simple example of a JSON-RPC request:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientVersion": "1.0.0" } }
Schema handling
MCP tools use JSON Schema to define their inputs and outputs. Your transport needs to pass these schemas correctly and handle parameter validation.
This can be tricky because:
Schemas can be nested with complex structures
Some fields are optional while others are required
Different programming languages handle JSON types differently
When building a custom transport, you'll need to flatten parameters from different sources (query, path, body, headers) and handle any naming conflicts.
Security considerations
If your transport sends data over a network, you need to think about security:
Encryption: Use TLS to protect data in transit
Authentication: Verify who's connecting with tokens or API keys
Access control: Limit who can use which tools
Origin validation: For HTTP-based transports, check the Origin header to prevent attacks
Even for local transports, consider binding only to localhost (127.0.0.1) instead of all interfaces (0.0.0.0) to limit exposure.
Concurrency support
Your transport might need to handle multiple connections at once. This requires:
Non-blocking I/O to prevent one slow client from blocking others
Thread-safe data structures if you share state between connections
Session tracking to keep each client's context separate
Step-by-step custom transport implementation
Let's walk through how to build a custom transport. We'll focus on the key parts you need to implement.
Define the transport interface
Every transport must implement a standard interface. In simplified form, it looks like this:
class Transport: async def start(self): """Start processing messages""" pass async def send(self, message): """Send a JSON-RPC message""" pass async def close(self): """Close the connection""" pass # Callbacks onmessage = None # Called when a message is received onerror = None # Called when an error occurs onclose = None # Called when the connection closes
This interface defines how the MCP client or server will interact with your transport. You'll implement these methods based on your chosen communication method.
Connect to MCP components
Once you've defined your transport, you need to connect it to either an MCP client or server:
from mcp import Server from my_transport import CustomTransport # Create the server and transport server = Server(name="example-server", version="1.0.0") transport = CustomTransport(config={"host": "localhost", "port": 9000}) # Connect them await server.connect(transport)
The server will call your transport's methods to send messages and will receive messages through the callbacks you set.
Handle message flow
The core of your transport is handling the flow of messages:
When a message comes in, parse it as JSON and call
onmessage
When the server calls
send
, format the message and send it outHandle any errors by calling
onerror
When the connection ends, call
onclose
Here's a simplified example of a message handling loop:
async def _receive_loop(self): try: while True: data = await self._read_message() if not data: break message = json.loads(data) if self.onmessage: self.onmessage(message) except Exception as e: if self.onerror: self.onerror(e) finally: if self.onclose: self.onclose()
Error handling
Good error handling is crucial for a reliable transport. Common errors include:
Network disconnections
Invalid JSON messages
Timeouts
Protocol violations
Your transport should catch these errors, log them appropriately, and either recover or gracefully shut down the connection.
Testing your custom transport
Before using your transport in production, you need to test it thoroughly. Here are key testing approaches:
Basic functionality testing
Start with simple tests to verify that your transport:
Correctly sends and receives messages
Handles initialization properly
Processes tool calls and returns results
Closes connections cleanly
You can create a test harness that connects a client and server using your transport and verifies the message flow.
Integration testing
Next, test your transport with real MCP clients and servers:
Use a tool like Claude Desktop as a client
Test with various tool definitions
Verify that schema validation works correctly
Check that error messages are helpful
Performance testing
Finally, test how your transport performs under load:
Run multiple concurrent connections
Send large messages and schemas
Measure latency and throughput
Check memory usage over time
Look for bottlenecks like message queues that grow without draining or memory leaks from unclosed connections.
Scaling and performance considerations
As your MCP usage grows, you might need to optimize your transport for better performance:
Connection pooling: Reuse connections instead of creating new ones for each session
Message batching: Group multiple requests or responses into a single message
Compression: Reduce the size of large messages or schemas
Dynamic loading: Load tool schemas only when needed instead of all at once
For large APIs with hundreds of endpoints, consider implementing a way to dynamically select which tools to load based on the user's needs.
Building reliable MCP integrations
Creating a custom MCP transport gives you control over how your LLM applications communicate with tools and services. By following the requirements and best practices outlined here, you can build a transport that's reliable, secure, and performant.
At Stainless, we've helped organizations deliver high-quality MCP experiences by focusing on robust transport implementations that handle edge cases gracefully. Whether you're integrating with existing systems or optimizing for specific performance needs, a well-designed custom transport can make your MCP integration more reliable.
FAQs about custom MCP transport implementation
How does a custom MCP transport differ from standard options?
A custom transport uses the same JSON-RPC message format as standard transports (stdio and HTTP) but lets you control exactly how those messages are sent between client and server, allowing integration with different protocols or messaging systems.
What programming languages can I use to implement a custom MCP transport?
You can use any language that supports JSON parsing and network communication, including Python, JavaScript, Go, Java, and Ruby. The choice depends on what language your existing systems use.
Are custom MCP transports compatible with standard MCP clients?
Yes, as long as your transport correctly implements the JSON-RPC 2.0 format and follows the MCP message lifecycle. The client doesn't need to know that you're using a custom transport.
What performance benefits can a custom transport provide?
A custom transport can reduce latency by using more efficient protocols, optimize for your specific network conditions, implement better connection pooling, or integrate directly with your existing message bus.
How do I handle authentication in a custom MCP transport?
You can implement authentication at the transport level using standard methods like OAuth tokens, API keys, or custom headers. The authentication method should match your security requirements and existing systems.
Understanding model context protocol transports
At Stainless, we generate client SDKs in various languages for our customers' APIs. Recently, those same customers started asking us for something new: a Model Context Protocol (MCP) server that wraps their API and exposes it as a set of tools for LLMs.
A transport in MCP is the layer that moves messages between clients and servers. Think of it as the delivery system for all communication. The transport handles reading and writing messages, formatting them correctly, and managing the connection.
MCP uses JSON-RPC 2.0 for its messages. This is a lightweight protocol that defines how to structure requests, responses, and notifications in JSON format. Every transport must support this format.
The standard transports built into MCP are:
stdio: Uses standard input and output streams, typically when the server runs as a subprocess of the client
Streamable HTTP: Uses HTTP POST requests for client-to-server communication and Server-Sent Events (SSE) for streaming from server to client
Here's where transport fits in the MCP architecture:
Why create a custom MCP transport
While the built-in transports work for many cases, sometimes they don't fit your specific needs. You might need a custom transport when:
Performance matters: When stdio or HTTP adds too much delay for your application
Framework integration: When you need to embed MCP in a system with its own messaging system
Security requirements: When you need special encryption or access controls
Custom protocols: When you already use WebSockets, ZeroMQ, or other protocols in your system
For example, at Stainless, we had a customer who needed to integrate MCP with their existing message queue system. The standard transports wouldn't work because their security model required all communication to go through this system. We helped them create a custom transport that maintained MCP compatibility while using their existing infrastructure.
Custom transports follow the same message structure as standard ones but let you control exactly how those messages travel between client and server.
Key requirements for a custom transport layer
To build a working custom transport, you need to follow several requirements that ensure it works properly with MCP clients and servers.
JSON-RPC compliance
MCP uses JSON-RPC 2.0 for all messages. Every transport must format messages according to this specification:
Requests include
jsonrpc
,id
,method
, and optionalparams
Responses include
jsonrpc
,id
, and eitherresult
orerror
Notifications look like requests but have no
id
and expect no response
Here's a simple example of a JSON-RPC request:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientVersion": "1.0.0" } }
Schema handling
MCP tools use JSON Schema to define their inputs and outputs. Your transport needs to pass these schemas correctly and handle parameter validation.
This can be tricky because:
Schemas can be nested with complex structures
Some fields are optional while others are required
Different programming languages handle JSON types differently
When building a custom transport, you'll need to flatten parameters from different sources (query, path, body, headers) and handle any naming conflicts.
Security considerations
If your transport sends data over a network, you need to think about security:
Encryption: Use TLS to protect data in transit
Authentication: Verify who's connecting with tokens or API keys
Access control: Limit who can use which tools
Origin validation: For HTTP-based transports, check the Origin header to prevent attacks
Even for local transports, consider binding only to localhost (127.0.0.1) instead of all interfaces (0.0.0.0) to limit exposure.
Concurrency support
Your transport might need to handle multiple connections at once. This requires:
Non-blocking I/O to prevent one slow client from blocking others
Thread-safe data structures if you share state between connections
Session tracking to keep each client's context separate
Step-by-step custom transport implementation
Let's walk through how to build a custom transport. We'll focus on the key parts you need to implement.
Define the transport interface
Every transport must implement a standard interface. In simplified form, it looks like this:
class Transport: async def start(self): """Start processing messages""" pass async def send(self, message): """Send a JSON-RPC message""" pass async def close(self): """Close the connection""" pass # Callbacks onmessage = None # Called when a message is received onerror = None # Called when an error occurs onclose = None # Called when the connection closes
This interface defines how the MCP client or server will interact with your transport. You'll implement these methods based on your chosen communication method.
Connect to MCP components
Once you've defined your transport, you need to connect it to either an MCP client or server:
from mcp import Server from my_transport import CustomTransport # Create the server and transport server = Server(name="example-server", version="1.0.0") transport = CustomTransport(config={"host": "localhost", "port": 9000}) # Connect them await server.connect(transport)
The server will call your transport's methods to send messages and will receive messages through the callbacks you set.
Handle message flow
The core of your transport is handling the flow of messages:
When a message comes in, parse it as JSON and call
onmessage
When the server calls
send
, format the message and send it outHandle any errors by calling
onerror
When the connection ends, call
onclose
Here's a simplified example of a message handling loop:
async def _receive_loop(self): try: while True: data = await self._read_message() if not data: break message = json.loads(data) if self.onmessage: self.onmessage(message) except Exception as e: if self.onerror: self.onerror(e) finally: if self.onclose: self.onclose()
Error handling
Good error handling is crucial for a reliable transport. Common errors include:
Network disconnections
Invalid JSON messages
Timeouts
Protocol violations
Your transport should catch these errors, log them appropriately, and either recover or gracefully shut down the connection.
Testing your custom transport
Before using your transport in production, you need to test it thoroughly. Here are key testing approaches:
Basic functionality testing
Start with simple tests to verify that your transport:
Correctly sends and receives messages
Handles initialization properly
Processes tool calls and returns results
Closes connections cleanly
You can create a test harness that connects a client and server using your transport and verifies the message flow.
Integration testing
Next, test your transport with real MCP clients and servers:
Use a tool like Claude Desktop as a client
Test with various tool definitions
Verify that schema validation works correctly
Check that error messages are helpful
Performance testing
Finally, test how your transport performs under load:
Run multiple concurrent connections
Send large messages and schemas
Measure latency and throughput
Check memory usage over time
Look for bottlenecks like message queues that grow without draining or memory leaks from unclosed connections.
Scaling and performance considerations
As your MCP usage grows, you might need to optimize your transport for better performance:
Connection pooling: Reuse connections instead of creating new ones for each session
Message batching: Group multiple requests or responses into a single message
Compression: Reduce the size of large messages or schemas
Dynamic loading: Load tool schemas only when needed instead of all at once
For large APIs with hundreds of endpoints, consider implementing a way to dynamically select which tools to load based on the user's needs.
Building reliable MCP integrations
Creating a custom MCP transport gives you control over how your LLM applications communicate with tools and services. By following the requirements and best practices outlined here, you can build a transport that's reliable, secure, and performant.
At Stainless, we've helped organizations deliver high-quality MCP experiences by focusing on robust transport implementations that handle edge cases gracefully. Whether you're integrating with existing systems or optimizing for specific performance needs, a well-designed custom transport can make your MCP integration more reliable.
FAQs about custom MCP transport implementation
How does a custom MCP transport differ from standard options?
A custom transport uses the same JSON-RPC message format as standard transports (stdio and HTTP) but lets you control exactly how those messages are sent between client and server, allowing integration with different protocols or messaging systems.
What programming languages can I use to implement a custom MCP transport?
You can use any language that supports JSON parsing and network communication, including Python, JavaScript, Go, Java, and Ruby. The choice depends on what language your existing systems use.
Are custom MCP transports compatible with standard MCP clients?
Yes, as long as your transport correctly implements the JSON-RPC 2.0 format and follows the MCP message lifecycle. The client doesn't need to know that you're using a custom transport.
What performance benefits can a custom transport provide?
A custom transport can reduce latency by using more efficient protocols, optimize for your specific network conditions, implement better connection pooling, or integrate directly with your existing message bus.
How do I handle authentication in a custom MCP transport?
You can implement authentication at the transport level using standard methods like OAuth tokens, API keys, or custom headers. The authentication method should match your security requirements and existing systems.
Understanding model context protocol transports
At Stainless, we generate client SDKs in various languages for our customers' APIs. Recently, those same customers started asking us for something new: a Model Context Protocol (MCP) server that wraps their API and exposes it as a set of tools for LLMs.
A transport in MCP is the layer that moves messages between clients and servers. Think of it as the delivery system for all communication. The transport handles reading and writing messages, formatting them correctly, and managing the connection.
MCP uses JSON-RPC 2.0 for its messages. This is a lightweight protocol that defines how to structure requests, responses, and notifications in JSON format. Every transport must support this format.
The standard transports built into MCP are:
stdio: Uses standard input and output streams, typically when the server runs as a subprocess of the client
Streamable HTTP: Uses HTTP POST requests for client-to-server communication and Server-Sent Events (SSE) for streaming from server to client
Here's where transport fits in the MCP architecture:
Why create a custom MCP transport
While the built-in transports work for many cases, sometimes they don't fit your specific needs. You might need a custom transport when:
Performance matters: When stdio or HTTP adds too much delay for your application
Framework integration: When you need to embed MCP in a system with its own messaging system
Security requirements: When you need special encryption or access controls
Custom protocols: When you already use WebSockets, ZeroMQ, or other protocols in your system
For example, at Stainless, we had a customer who needed to integrate MCP with their existing message queue system. The standard transports wouldn't work because their security model required all communication to go through this system. We helped them create a custom transport that maintained MCP compatibility while using their existing infrastructure.
Custom transports follow the same message structure as standard ones but let you control exactly how those messages travel between client and server.
Key requirements for a custom transport layer
To build a working custom transport, you need to follow several requirements that ensure it works properly with MCP clients and servers.
JSON-RPC compliance
MCP uses JSON-RPC 2.0 for all messages. Every transport must format messages according to this specification:
Requests include
jsonrpc
,id
,method
, and optionalparams
Responses include
jsonrpc
,id
, and eitherresult
orerror
Notifications look like requests but have no
id
and expect no response
Here's a simple example of a JSON-RPC request:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientVersion": "1.0.0" } }
Schema handling
MCP tools use JSON Schema to define their inputs and outputs. Your transport needs to pass these schemas correctly and handle parameter validation.
This can be tricky because:
Schemas can be nested with complex structures
Some fields are optional while others are required
Different programming languages handle JSON types differently
When building a custom transport, you'll need to flatten parameters from different sources (query, path, body, headers) and handle any naming conflicts.
Security considerations
If your transport sends data over a network, you need to think about security:
Encryption: Use TLS to protect data in transit
Authentication: Verify who's connecting with tokens or API keys
Access control: Limit who can use which tools
Origin validation: For HTTP-based transports, check the Origin header to prevent attacks
Even for local transports, consider binding only to localhost (127.0.0.1) instead of all interfaces (0.0.0.0) to limit exposure.
Concurrency support
Your transport might need to handle multiple connections at once. This requires:
Non-blocking I/O to prevent one slow client from blocking others
Thread-safe data structures if you share state between connections
Session tracking to keep each client's context separate
Step-by-step custom transport implementation
Let's walk through how to build a custom transport. We'll focus on the key parts you need to implement.
Define the transport interface
Every transport must implement a standard interface. In simplified form, it looks like this:
class Transport: async def start(self): """Start processing messages""" pass async def send(self, message): """Send a JSON-RPC message""" pass async def close(self): """Close the connection""" pass # Callbacks onmessage = None # Called when a message is received onerror = None # Called when an error occurs onclose = None # Called when the connection closes
This interface defines how the MCP client or server will interact with your transport. You'll implement these methods based on your chosen communication method.
Connect to MCP components
Once you've defined your transport, you need to connect it to either an MCP client or server:
from mcp import Server from my_transport import CustomTransport # Create the server and transport server = Server(name="example-server", version="1.0.0") transport = CustomTransport(config={"host": "localhost", "port": 9000}) # Connect them await server.connect(transport)
The server will call your transport's methods to send messages and will receive messages through the callbacks you set.
Handle message flow
The core of your transport is handling the flow of messages:
When a message comes in, parse it as JSON and call
onmessage
When the server calls
send
, format the message and send it outHandle any errors by calling
onerror
When the connection ends, call
onclose
Here's a simplified example of a message handling loop:
async def _receive_loop(self): try: while True: data = await self._read_message() if not data: break message = json.loads(data) if self.onmessage: self.onmessage(message) except Exception as e: if self.onerror: self.onerror(e) finally: if self.onclose: self.onclose()
Error handling
Good error handling is crucial for a reliable transport. Common errors include:
Network disconnections
Invalid JSON messages
Timeouts
Protocol violations
Your transport should catch these errors, log them appropriately, and either recover or gracefully shut down the connection.
Testing your custom transport
Before using your transport in production, you need to test it thoroughly. Here are key testing approaches:
Basic functionality testing
Start with simple tests to verify that your transport:
Correctly sends and receives messages
Handles initialization properly
Processes tool calls and returns results
Closes connections cleanly
You can create a test harness that connects a client and server using your transport and verifies the message flow.
Integration testing
Next, test your transport with real MCP clients and servers:
Use a tool like Claude Desktop as a client
Test with various tool definitions
Verify that schema validation works correctly
Check that error messages are helpful
Performance testing
Finally, test how your transport performs under load:
Run multiple concurrent connections
Send large messages and schemas
Measure latency and throughput
Check memory usage over time
Look for bottlenecks like message queues that grow without draining or memory leaks from unclosed connections.
Scaling and performance considerations
As your MCP usage grows, you might need to optimize your transport for better performance:
Connection pooling: Reuse connections instead of creating new ones for each session
Message batching: Group multiple requests or responses into a single message
Compression: Reduce the size of large messages or schemas
Dynamic loading: Load tool schemas only when needed instead of all at once
For large APIs with hundreds of endpoints, consider implementing a way to dynamically select which tools to load based on the user's needs.
Building reliable MCP integrations
Creating a custom MCP transport gives you control over how your LLM applications communicate with tools and services. By following the requirements and best practices outlined here, you can build a transport that's reliable, secure, and performant.
At Stainless, we've helped organizations deliver high-quality MCP experiences by focusing on robust transport implementations that handle edge cases gracefully. Whether you're integrating with existing systems or optimizing for specific performance needs, a well-designed custom transport can make your MCP integration more reliable.
FAQs about custom MCP transport implementation
How does a custom MCP transport differ from standard options?
A custom transport uses the same JSON-RPC message format as standard transports (stdio and HTTP) but lets you control exactly how those messages are sent between client and server, allowing integration with different protocols or messaging systems.
What programming languages can I use to implement a custom MCP transport?
You can use any language that supports JSON parsing and network communication, including Python, JavaScript, Go, Java, and Ruby. The choice depends on what language your existing systems use.
Are custom MCP transports compatible with standard MCP clients?
Yes, as long as your transport correctly implements the JSON-RPC 2.0 format and follows the MCP message lifecycle. The client doesn't need to know that you're using a custom transport.
What performance benefits can a custom transport provide?
A custom transport can reduce latency by using more efficient protocols, optimize for your specific network conditions, implement better connection pooling, or integrate directly with your existing message bus.
How do I handle authentication in a custom MCP transport?
You can implement authentication at the transport level using standard methods like OAuth tokens, API keys, or custom headers. The authentication method should match your security requirements and existing systems.