You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Provide support for the latest transport of the MCP project, Streamable HTTP so that dart/flutter clients can utilize HTTP-based MCP Servers and tools.
Reacted by Thibault Zanini, Guanghui Liu, GiYeong Um, luisredondo, Ryan Ramchandar, Nikechukwu, 唯鹿, jpeiffer, Jannis Rosenbaum, Amir Fallahian and 6 more
Hi @jakemac53, I'm interested in contributing to this issue. Currently, StreamChannel is used for communication between the client and the server. Do you have a specific direction in mind for the Transport API?
Would you be open to a PR addressing this issue?
My original thought was that we don't need a Transport API at all - I don't know what it provides over just a raw StreamChannel. Instead, I was planning to just provide helper functions for each transport (see lib/stdio.dart for an example), which just give you the stream channel.
But, I am definitely open to any arguments for adding an explicit Transport class if you think we need one.
I've created a simple MVP to test communication with server via HTTP. It is stateless, so in order to call some tool we have to send all the requests in batch (initialize request, initialized notification, tools list request, concat tool call request).
Example source code
import'dart:async';
import'dart:io';
import'dart:convert';
import'package:dart_mcp/server.dart';
import'package:stream_channel/stream_channel.dart';
voidmain() async {
final httpServer =awaitHttpServer.bind(InternetAddress.loopbackIPv4, 3333);
print('Serving at http://${httpServer.address.host}:${httpServer.port}');
awaitfor (HttpRequest request in httpServer) {
// Only process POST requests to the /mcp endpoint.if (request.method !='POST'|| request.uri.path !='/mcp') {
request.response
..statusCode =HttpStatus.methodNotAllowed
..headers.contentType =ContentType.text
..write('Only POST requests to /mcp are allowed.');
await request.response.close();
continue;
}
// Ensure the Content-Type is application/json.final contentType = request.headers.contentType;
if (contentType ==null|| contentType.mimeType !='application/json') {
request.response
..statusCode =HttpStatus.unsupportedMediaType
..headers.contentType =ContentType.text
..write('Invalid Request: Content-Type must be application/json.');
await request.response.close();
continue;
}
final responseStreamController =StreamController<String>();
final channel =StreamChannel<String>(
Utf8Decoder().bind(request),
responseStreamController.sink,
);
final mcpServer =MCPServerWithTools(channel);
responseStreamController.stream.listen(
(response) async {
request.response
..headers.contentType =ContentType.json
..statusCode =HttpStatus.ok
..write(response);
await request.response.close();
print('Responded');
},
onDone: () async {
print('Done');
await mcpServer.shutdown();
await responseStreamController.close();
},
onError: (error, stackTrace) async {
print('Error writing RPC response to HTTP: $error\n$stackTrace');
request.response
..statusCode =HttpStatus.internalServerError
..headers.contentType =ContentType.json
..write(
jsonEncode({
'jsonrpc':'2.0',
'error': {
'code':-32000,
'message':'Internal server error while sending response: $error',
},
}),
);
await request.response.close();
},
);
}
}
/// This server uses the [ToolsSupport] mixin to provide tools to the client.baseclassMCPServerWithToolsextendsMCPServerwithToolsSupport {
MCPServerWithTools(super.channel, {super.protocolLogSink})
:super.fromStreamChannel(
implementation:Implementation(
name:'An example dart server with tools support',
version:'0.1.0',
),
instructions:'Just list and call the tools :D',
) {
registerTool(concatTool, _concat);
}
final concatTool =Tool(
name:'concat',
description:'concatenates many string parts into one string',
inputSchema:Schema.object(
properties: {
'parts':Schema.list(
description:'The parts to concatenate together',
items:Schema.string(),
),
},
required: ['parts'],
),
);
FutureOr<CallToolResult> _concat(CallToolRequest request) =>CallToolResult(
content: [
TextContent(
text: (request.arguments!['parts'] asList<dynamic>)
.cast<String>()
.join(''),
),
],
);
}
From what I see now helper functions for HTTP transport would be not enough.
I am a bit confused, it seems to me that your example demonstrates it would be enough? Ultimately, you were able to create a StreamChannel and give that to the server. So, basically you would just be extracting out the logic to create that stream given an /mcp post request into a helper, something like this:
import'dart:async';
import'dart:io';
import'dart:convert';
import'package:dart_mcp/server.dart';
import'package:stream_channel/stream_channel.dart';
StreamChannel<String>?streamableHttpChannel(HttpRequest request) {
// Only process POST requests to the /mcp endpoint.if (request.method !='POST'|| request.uri.path !='/mcp') {
request.response
..statusCode =HttpStatus.methodNotAllowed
..headers.contentType =ContentType.text
..write('Only POST requests to /mcp are allowed.');
await request.response.close();
returnnull;
}
// Ensure the Content-Type is application/json.final contentType = request.headers.contentType;
if (contentType ==null|| contentType.mimeType !='application/json') {
request.response
..statusCode =HttpStatus.unsupportedMediaType
..headers.contentType =ContentType.text
..write('Invalid Request: Content-Type must be application/json.');
await request.response.close();
returnnull;
}
final responseStreamController =StreamController<String>();
final channel =StreamChannel<String>(
Utf8Decoder().bind(request),
responseStreamController.sink,
);
responseStreamController.stream.listen(
(response) async {
request.response
..headers.contentType =ContentType.json
..statusCode =HttpStatus.ok
..write(response);
await request.response.close();
print('Responded');
},
onDone: () async {
print('Done');
await mcpServer.shutdown();
await responseStreamController.close();
},
onError: (error, stackTrace) async {
print('Error writing RPC response to HTTP: $error\n$stackTrace');
request.response
..statusCode =HttpStatus.internalServerError
..headers.contentType =ContentType.json
..write(
jsonEncode({
'jsonrpc':'2.0',
'error': {
'code':-32000,
'message':'Internal server error while sending response: $error',
},
}),
);
await request.response.close();
},
);
return channel;
}
voidmain() async {
final httpServer =awaitHttpServer.bind(InternetAddress.loopbackIPv4, 3333);
print('Serving at http://${httpServer.address.host}:${httpServer.port}');
awaitfor (HttpRequest request in httpServer) {
if (streamableHttpChannel(request) casefinal channel?) {
MCPServerWithTools(channel);
}
}
}
/// This server uses the [ToolsSupport] mixin to provide tools to the client.baseclassMCPServerWithToolsextendsMCPServerwithToolsSupport {
MCPServerWithTools(super.channel, {super.protocolLogSink})
:super.fromStreamChannel(
implementation:Implementation(
name:'An example dart server with tools support',
version:'0.1.0',
),
instructions:'Just list and call the tools :D',
) {
registerTool(concatTool, _concat);
}
final concatTool =Tool(
name:'concat',
description:'concatenates many string parts into one string',
inputSchema:Schema.object(
properties: {
'parts':Schema.list(
description:'The parts to concatenate together',
items:Schema.string(),
),
},
required: ['parts'],
),
);
FutureOr<CallToolResult> _concat(CallToolRequest request) =>CallToolResult(
content: [
TextContent(
text: (request.arguments!['parts'] asList<dynamic>)
.cast<String>()
.join(''),
),
],
);
}
I am a bit confused, it seems to me that your example demonstrates it would be enough? Ultimately, you were able to create a StreamChannel and give that to the server. So, basically you would just be extracting out the logic to create that stream given an /mcp post request into a helper, something like this:
Unfortunately, my example works only with batched requests. Below, I share the non-batched requests and the responses that will be received
Add caching for the server instances for each initialized client according to their Mcp-Session-Id, so we can later reuse the server for all the requests from this client
Add caching for the server instances for each initialized client according to their Mcp-Session-Id, so we can later reuse the server for all the requests from this client
Yes, I agree those are the things that we need generally, especially given batch requests are not even a part of the MCP spec any more afaik.
We could try and handle this though by passing a factory for creating the MCP server instances for each session or something like that, but I think that should probably be an optional thing and we would want the lower level API also exposed.
I think part of the challenge is to know when it's safe to close and send the response. Because the MCPServer takes a Stream for both the input and the output, and that's it, there isn't really a guaranteed way to say "I sent this request, and this is the correct response to post back to the client".
A developer could assume that the next response after the given request is the correct one to reply with. There's a few problems that I see with that approach though:
That's an assumption, and we all know that only good things happen when you code to assumptions...
What about race conditions where the AI agent fires two requests rapid-fire? How would the transport know which one to post back to each request. Again, you can assume the AI agent is strict request, wait, respond but that again is an assumption.
How do you know that the server will only reply with a single response to a single request. Technically, since both sides are stream, there's nothing preventing a 1-N vs N-1 scenario.
POST, SSE answers with stream-close cancel, subscriptions/listen, the Origin allow-list, the legacy input-required shim and a dart:io client channel that speaks 2026-07-28 are on main. The revision dropped sessions and no session route was built. Auth stays with the embedding server. Batching, a Transport class, full parity (#351), cancellation (#37) and pagination (#28) stay out of scope.
Activity
[-]Add support for Streamable HTTP to dart_mcp client[/-][+]Add support for Streamable HTTP to dart_mcp[/+]michael-mcroskey commentedon Jul 3, 2025
+1 from FlutterFlow — we're interested in Streamable HTTP support for dart_mcp as we're soon rolling out MCP support
jakemac53 commentedon Jul 7, 2025
Do you need support for authentication as well?
ksokolovskyi commentedon Jul 17, 2025
Hi @jakemac53, I'm interested in contributing to this issue. Currently,
StreamChannelis used for communication between the client and the server. Do you have a specific direction in mind for theTransportAPI?Would you be open to a PR addressing this issue?
jakemac53 commentedon Jul 21, 2025
Hi @ksokolovskyi thanks for offering to help here!
My original thought was that we don't need a
TransportAPI at all - I don't know what it provides over just a raw StreamChannel. Instead, I was planning to just provide helper functions for each transport (seelib/stdio.dartfor an example), which just give you the stream channel.But, I am definitely open to any arguments for adding an explicit
Transportclass if you think we need one.jakemac53 commentedon Jul 28, 2025
@ksokolovskyi checking if you have any thoughts or have you started looking into this at all?
ksokolovskyi commentedon Jul 31, 2025
Hi @jakemac53, sorry for the late reply.
I've created a simple MVP to test communication with server via HTTP. It is stateless, so in order to call some tool we have to send all the requests in batch (initialize request, initialized notification, tools list request, concat tool call request).
Example source code
Request
Response
[ { "jsonrpc": "2.0", "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "An example dart server with tools support", "version": "0.1.0" }, "instructions": "Just list and call the tools :D" }, "id": 1 }, { "jsonrpc": "2.0", "result": null, "id": 2 }, { "jsonrpc": "2.0", "result": { "tools": [ { "name": "concat", "description": "concatenates many string parts into one string", "inputSchema": { "type": "object", "properties": { "parts": { "type": "array", "description": "The parts to concatenate together", "items": { "type": "string" } } }, "required": [ "parts" ] } } ] }, "id": 3 }, { "jsonrpc": "2.0", "result": { "content": [ { "text": "abc", "type": "text" } ] }, "id": 4 } ]From what I see now helper functions for HTTP transport would be not enough.
jakemac53 commentedon Jul 31, 2025
I am a bit confused, it seems to me that your example demonstrates it would be enough? Ultimately, you were able to create a StreamChannel and give that to the server. So, basically you would just be extracting out the logic to create that stream given an
/mcppost request into a helper, something like this:nikeokoronkwo commentedon Aug 1, 2025
Would love to see this feature.
Would authentication be supported as well for the client and server?
ksokolovskyi commentedon Aug 1, 2025
Unfortunately, my example works only with batched requests. Below, I share the non-batched requests and the responses that will be received
Non-batched requests
Non-batched responses
{ "jsonrpc": "2.0", "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "An example dart server with tools support", "version": "0.1.0" }, "instructions": "Just list and call the tools :D" }, "id": 1 } { "jsonrpc": "2.0", "result": null, "id": 2 } { "jsonrpc": "2.0", "error": { "code": -32601, "message": "Unknown method \"tools/list\".", "data": { "request": { "jsonrpc": "2.0", "id": 3, "method": "tools/list" } } }, "id": 3 } { "jsonrpc": "2.0", "error": { "code": -32601, "message": "Unknown method \"tools/call\".", "data": { "request": { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "concat", "arguments": { "parts": [ "a", "b", "c" ] } } } } }, "id": 4 }This happens because we are losing the state of the
MCPServerWithToolsas we recreate it for each request.From what I see, to make it work with the current architecture of the package, we need to:
Mcp-Session-Idsupport (https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management)Mcp-Session-Id, so we can later reuse the server for all the requests from this client@jakemac53, what do you think?
jakemac53 commentedon Aug 4, 2025
Yes, I agree those are the things that we need generally, especially given batch requests are not even a part of the MCP spec any more afaik.
However, if you look at the typescript SDK examples, they don't handle these for you, they just have an example of doing it yourself https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#streamable-http.
We could try and handle this though by passing a factory for creating the MCP server instances for each session or something like that, but I think that should probably be an optional thing and we would want the lower level API also exposed.
jpeiffer commentedon Aug 11, 2025
I think part of the challenge is to know when it's safe to close and send the response. Because the MCPServer takes a Stream for both the input and the output, and that's it, there isn't really a guaranteed way to say "I sent this request, and this is the correct response to post back to the client".
A developer could assume that the next response after the given request is the correct one to reply with. There's a few problems that I see with that approach though:
84 remaining items
validate capability extension identifiers
pin the header walk past a matching property
route server-to-client requests on older revisions (#654)
let a caller add headers to the client channel (#667)
cap request bodies in the Streamable HTTP handler (#657)
add a padded request state to five write paths (#664)
pin the header walk past a matching property (#662)
validate capability extension identifiers (#661)
add optional origin validation for Streamable HTTP (#669)
Yusufihsangorgel commentedon Sep 15, 2026
POST, SSE answers with stream-close cancel, subscriptions/listen, the Origin allow-list, the legacy input-required shim and a dart:io client channel that speaks 2026-07-28 are on main. The revision dropped sessions and no session route was built. Auth stays with the embedding server. Batching, a Transport class, full parity (#351), cancellation (#37) and pagination (#28) stay out of scope.
jakemac53 commentedon Oct 26, 2026
@Yusufihsangorgel can we call this one closed now? Anything else you are aware of?
Yusufihsangorgel commentedon Oct 27, 2026
Yes, this can be closed. Streamable HTTP is on main, with remaining protocol and API follow-ups tracked under #668.