blob: 7ebb461326b9d3bb6f718816c534987c04e05f4f [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// A server reachable over the Streamable HTTP transport from the 2026-07-28
/// revision of the protocol.
///
/// Run it with `dart run example/streamable_http_server.dart`. It prints a
/// `curl` command for the tool it registers, which is how this example is
/// driven. The command asks for progress
/// and the tool reports it, so the answer comes back as an SSE response
/// stream: the report first, then a result containing `Hello, world!`.
library;
import 'dart:io' as io;
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp/streamable_http.dart';
void main() async {
// The one path this server answers on.
const path = '/mcp';
final httpServer = await io.HttpServer.bind(
io.InternetAddress.loopbackIPv4,
0,
);
final endpoint = 'http://${httpServer.address.host}:${httpServer.port}$path';
httpServer.listen((request) async {
// The handler reads no path, so the one this server answers on is the
// host's to choose and to enforce.
if (request.uri.path != path) {
request.response
..statusCode = io.HttpStatus.notFound
..contentLength = 0;
await request.response.close();
return;
}
// Loopback is not protection on its own, since DNS rebinding reaches it
// from a remote page. The specification requires a server to answer 403
// when `Origin` is present and invalid; nothing here is meant to be driven
// from a page, so no origin is valid. Read the header as a list: `value`
// throws when a request repeats it, and a caller chooses that.
if (request.headers['origin'] != null) {
request.response
..statusCode = io.HttpStatus.forbidden
..contentLength = 0;
await request.response.close();
return;
}
// The handler also leaves authentication to the host. This example does
// not authenticate the caller. A server reachable from anywhere but this
// machine needs it.
// Every POST gets its own server: this protocol revision carries the client
// context on the request instead of an `initialize` handshake, so there is
// no connection to keep around between requests.
try {
await handleStreamableHttpRequest(
request,
MCPServerWithGreeting.new,
// `greet` sends a notification when the request carries a progress
// token, which the command below does. It goes out on the response
// stream, and this callback is the host's own copy of it.
onNotification:
(notification) => io.stderr.writeln('notification: $notification'),
);
} catch (error) {
// The request already carries the failure. This is the host's copy.
io.stderr.writeln('request failed: $error');
}
});
print('Listening on $endpoint\n');
// `Mcp-Method` and `Mcp-Name` mirror the body so that intermediaries can
// route and inspect the request without parsing it; `Mcp-Name` is only
// required for the methods that name a target, such as `tools/call`.
print('''
curl -sS $endpoint \\
-H 'Content-Type: application/json' \\
-H 'Accept: application/json, text/event-stream' \\
-H 'MCP-Protocol-Version: 2026-07-28' \\
-H 'Mcp-Method: tools/call' \\
-H 'Mcp-Name: greet' \\
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "greet",
"arguments": {"name": "world"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "0"},
"io.modelcontextprotocol/clientCapabilities": {},
"progressToken": 1
}
}
}'
''');
}
/// A server with one tool, built fresh for each request.
base class MCPServerWithGreeting extends MCPServer with ToolsSupport {
MCPServerWithGreeting(super.channel)
: super.fromStreamChannel(
implementation: Implementation(
name: 'An example dart server on Streamable HTTP',
version: '0.1.0',
),
) {
registerTool(greetTool, _greet);
}
/// A tool that says hello to the name it is given.
final greetTool = Tool(
name: 'greet',
description: 'greets whoever it is given',
inputSchema: Schema.object(
properties: {'name': Schema.string(description: 'Who to greet')},
required: ['name'],
),
);
/// The implementation of the `greet` tool.
CallToolResult _greet(CallToolRequest request) {
// Sending this commits the response to an SSE response stream, and the
// `curl` command above gets one back.
if (request.meta?.progressToken case final progressToken?) {
notifyProgress(
ProgressNotification(
progressToken: progressToken,
progress: 1,
total: 1,
message: 'Greeting ${request.arguments!['name']}',
),
);
}
return CallToolResult(
content: [TextContent(text: 'Hello, ${request.arguments!['name']}!')],
);
}
}