Skip to content

Commit 61ba1ea

Browse files
authored
Add more and better examples (#228)
Towards #220. I also added a utility function for created stdio stream channels to simplify the examples (and real user code), and fixed a bug in the PromptMessage constructor. Still missing a few examples, but this covers the most common use cases. cc @gaaclarke
1 parent b77cda5 commit 61ba1ea

20 files changed

+644
-123
lines changed

mcp_examples/bin/file_system_server.dart

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,13 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:io' as io;
88

9-
import 'package:async/async.dart';
109
import 'package:dart_mcp/server.dart';
10+
import 'package:dart_mcp/stdio.dart';
1111
import 'package:path/path.dart' as p;
12-
import 'package:stream_channel/stream_channel.dart';
1312

1413
void main() {
1514
SimpleFileSystemServer.fromStreamChannel(
16-
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
17-
.transform(StreamChannelTransformer.fromCodec(utf8))
18-
.transformStream(const LineSplitter())
19-
.transformSink(
20-
StreamSinkTransformer.fromHandlers(
21-
handleData: (data, sink) {
22-
sink.add('$data\n');
23-
},
24-
),
25-
),
15+
stdioChannel(input: io.stdin, output: io.stdout),
2616
);
2717
}
2818

mcp_examples/bin/workflow_client.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:args/args.dart';
1010
import 'package:async/async.dart';
1111
import 'package:cli_util/cli_logging.dart';
1212
import 'package:dart_mcp/client.dart';
13+
import 'package:dart_mcp/stdio.dart';
1314
import 'package:google_generative_ai/google_generative_ai.dart' as gemini;
1415

1516
/// The list of Gemini models that are accepted as a "--model" argument.
@@ -414,12 +415,10 @@ final class WorkflowClient extends MCPClient with RootsSupport {
414415
parts.skip(1).toList(),
415416
);
416417
serverConnections.add(
417-
connectStdioServer(
418-
process.stdin,
419-
process.stdout,
418+
connectServer(
419+
stdioChannel(input: process.stdout, output: process.stdin),
420420
protocolLogSink: logSink,
421-
onDone: process.kill,
422-
),
421+
)..done.then((_) => process.kill()),
423422
);
424423
} catch (e) {
425424
logger.stderr('Failed to connect to server $server: $e');

pkgs/dart_mcp/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
- Fixes communication problem when a `MCPServer` is instantiated without
44
instructions.
5+
- Fix the `content` argument to `PromptMessage` to be a single `Content` object.
6+
- Add new `package:dart_mcp/stdio.dart` library with a `stdioChannel` utility
7+
for creating a stream channel that separates messages by newlines.
8+
- Added more examples.
59

610
## 0.3.0
711

pkgs/dart_mcp/example/README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
# Simple Client and Server
1+
# Client and Server examples
22

3-
See `bin/simple_client.dart` and `bin/simple_server.dart` for a basic example of
4-
how to use the `MCPClient` and `MCPServer` classes. These don't use any LLM to
5-
invoke tools.
3+
For each client or server feature, there is a corresponding example here with
4+
the {feature}_client.dart and {feature}_server.dart file names. Sometimes
5+
multiple features are demonstrated together where appropriate, in which case the
6+
file name will indicate this.
67

7-
# Full Features Examples
8+
To run the examples, run the client file directly, so for instance
9+
`dart run example/tools_client.dart` with run the example client which invokes
10+
tools, connected to the example server that provides tools
11+
(at `example/tools_server.dart`).
12+
13+
# Full Featured Examples
814

915
See https://github.com/dart-lang/ai/tree/main/mcp_examples for some more full
1016
featured examples using gemini to automatically invoke tools.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A client that interacts with a server that provides prompts.
6+
library;
7+
8+
import 'dart:async';
9+
import 'dart:io';
10+
11+
import 'package:dart_mcp/client.dart';
12+
import 'package:dart_mcp/stdio.dart';
13+
14+
void main() async {
15+
// Create the client, which is the top level object that manages all
16+
// server connections.
17+
final client = MCPClient(
18+
Implementation(name: 'example dart client', version: '0.1.0'),
19+
);
20+
print('connecting to server');
21+
22+
// Start the server as a separate process.
23+
final process = await Process.start('dart', [
24+
'run',
25+
'example/prompts_server.dart',
26+
]);
27+
// Connect the client to the server.
28+
final server = client.connectServer(
29+
stdioChannel(input: process.stdout, output: process.stdin),
30+
);
31+
// When the server connection is closed, kill the process.
32+
unawaited(server.done.then((_) => process.kill()));
33+
print('server started');
34+
35+
// Initialize the server and let it know our capabilities.
36+
print('initializing server');
37+
final initializeResult = await server.initialize(
38+
InitializeRequest(
39+
protocolVersion: ProtocolVersion.latestSupported,
40+
capabilities: client.capabilities,
41+
clientInfo: client.implementation,
42+
),
43+
);
44+
print('initialized: $initializeResult');
45+
46+
// Ensure the server supports the prompts capability.
47+
if (initializeResult.capabilities.prompts == null) {
48+
await server.shutdown();
49+
throw StateError('Server doesn\'t support prompts!');
50+
}
51+
52+
// Notify the server that we are initialized.
53+
server.notifyInitialized();
54+
print('sent initialized notification');
55+
56+
// List all the available prompts from the server.
57+
print('Listing prompts from server');
58+
final promptsResult = await server.listPrompts(ListPromptsRequest());
59+
for (final prompt in promptsResult.prompts) {
60+
// For each prompt, get the full prompt text, filling in any arguments.
61+
final promptResult = await server.getPrompt(
62+
GetPromptRequest(
63+
name: prompt.name,
64+
arguments: {
65+
for (var arg in prompt.arguments ?? <PromptArgument>[])
66+
arg.name: switch (arg.name) {
67+
'tags' => 'myTag myOtherTag',
68+
'platforms' => 'vm,chrome',
69+
_ => throw ArgumentError('Unrecognized argument ${arg.name}'),
70+
},
71+
},
72+
),
73+
);
74+
final promptText = promptResult.messages
75+
.map((m) => (m.content as TextContent).text)
76+
.join('');
77+
print('Found prompt `${prompt.name}`: "$promptText"');
78+
}
79+
80+
// Shutdown the client, which will also shutdown the server connection.
81+
await client.shutdown();
82+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A server that implements the prompts API using the [PromptsSupport] mixin.
6+
library;
7+
8+
import 'dart:io' as io;
9+
10+
import 'package:dart_mcp/server.dart';
11+
import 'package:dart_mcp/stdio.dart';
12+
13+
void main() {
14+
// Create the server and connect it to stdio.
15+
MCPServerWithPrompts(stdioChannel(input: io.stdin, output: io.stdout));
16+
}
17+
18+
/// Our actual MCP server.
19+
///
20+
/// This server uses the [PromptsSupport] mixin to provide prompts to the
21+
/// client.
22+
base class MCPServerWithPrompts extends MCPServer with PromptsSupport {
23+
MCPServerWithPrompts(super.channel)
24+
: super.fromStreamChannel(
25+
implementation: Implementation(
26+
name: 'An example dart server with prompts support',
27+
version: '0.1.0',
28+
),
29+
instructions: 'Just list the prompts :D',
30+
) {
31+
// Actually add the prompt.
32+
addPrompt(runTestsPrompt, _runTestsPrompt);
33+
}
34+
35+
/// The prompt implementation, takes in a [request] and builds the prompt
36+
/// by substituting in arguments.
37+
GetPromptResult _runTestsPrompt(GetPromptRequest request) {
38+
// The actual arguments should be comma separated, but we allow for space
39+
// separated and then convert it here.
40+
final tags = (request.arguments?['tags'] as String?)?.split(' ').join(',');
41+
final platforms = (request.arguments?['platforms'] as String?)
42+
?.split(' ')
43+
.join(',');
44+
return GetPromptResult(
45+
messages: [
46+
// This is a prompt that should execute as if it came from the user,
47+
// instructing the LLM to run a specific CLI command based on the
48+
// arguments given.
49+
PromptMessage(
50+
role: Role.user,
51+
content: Content.text(
52+
text:
53+
'Execute the shell command `dart test --failures-only'
54+
'${tags != null ? ' -t $tags' : ''}'
55+
'${platforms != null ? ' -p $platforms' : ''}'
56+
'`',
57+
),
58+
),
59+
],
60+
);
61+
}
62+
63+
/// A prompt that can be used to run tests.
64+
///
65+
/// This prompt has two arguments, `tags` and `platforms`.
66+
final runTestsPrompt = Prompt(
67+
name: 'run_tests',
68+
description: 'Run your dart tests',
69+
arguments: [
70+
PromptArgument(
71+
name: 'tags',
72+
description: 'The test tags to include, space or comma separated',
73+
),
74+
PromptArgument(
75+
name: 'platforms',
76+
description: 'The platforms to run on, space or comma separated',
77+
),
78+
],
79+
);
80+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// A client that connects to a server and exercises the resources API.
6+
import 'dart:async';
7+
import 'dart:io';
8+
9+
import 'package:dart_mcp/client.dart';
10+
import 'package:dart_mcp/stdio.dart';
11+
12+
void main() async {
13+
// Create a client, which is the top level object that manages all
14+
// server connections.
15+
final client = MCPClient(
16+
Implementation(name: 'example dart client', version: '0.1.0'),
17+
);
18+
print('connecting to server');
19+
20+
// Start the server as a separate process.
21+
final process = await Process.start('dart', [
22+
'run',
23+
'example/resources_server.dart',
24+
]);
25+
// Connect the client to the server.
26+
final server = client.connectServer(
27+
stdioChannel(input: process.stdout, output: process.stdin),
28+
);
29+
// When the server connection is closed, kill the process.
30+
unawaited(server.done.then((_) => process.kill()));
31+
print('server started');
32+
33+
// Initialize the server and let it know our capabilities.
34+
print('initializing server');
35+
final initializeResult = await server.initialize(
36+
InitializeRequest(
37+
protocolVersion: ProtocolVersion.latestSupported,
38+
capabilities: client.capabilities,
39+
clientInfo: client.implementation,
40+
),
41+
);
42+
print('initialized: $initializeResult');
43+
44+
// Ensure the server supports the resources capability.
45+
if (initializeResult.capabilities.resources == null) {
46+
await server.shutdown();
47+
throw StateError('Server doesn\'t support resources!');
48+
}
49+
50+
// Notify the server that we are initialized.
51+
server.notifyInitialized();
52+
print('sent initialized notification');
53+
54+
// List all the available resources from the server.
55+
print('Listing resources from server');
56+
final resourcesResult = await server.listResources(ListResourcesRequest());
57+
for (final resource in resourcesResult.resources) {
58+
// For each resource, read its content.
59+
final content = (await server.readResource(
60+
ReadResourceRequest(uri: resource.uri),
61+
)).contents.map((part) => (part as TextResourceContents).text).join('');
62+
print(
63+
'Found resource: ${resource.name} with uri ${resource.uri} and contents: '
64+
'"$content"',
65+
);
66+
}
67+
68+
// List all the available resource templates from the server.
69+
print('Listing resource templates from server');
70+
final templatesResult = await server.listResourceTemplates(
71+
ListResourceTemplatesRequest(),
72+
);
73+
for (final template in templatesResult.resourceTemplates) {
74+
print('Found resource template `${template.uriTemplate}`');
75+
// For each template, fill in the path variable and read the resource.
76+
for (var path in ['zip', 'zap']) {
77+
final uri = template.uriTemplate.replaceFirst(RegExp('{.*}'), path);
78+
final contents = (await server.readResource(
79+
ReadResourceRequest(uri: uri),
80+
)).contents.map((part) => (part as TextResourceContents).text).join('');
81+
print('Read resource `$uri`: "$contents"');
82+
}
83+
}
84+
85+
// Shutdown the client, which will also shutdown the server connection.
86+
await client.shutdown();
87+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A server that implements the resources API using the [ResourcesSupport]
6+
/// mixin.
7+
library;
8+
9+
import 'dart:io' as io;
10+
11+
import 'package:dart_mcp/server.dart';
12+
import 'package:dart_mcp/stdio.dart';
13+
14+
void main() {
15+
// Create the server and connect it to stdio.
16+
MCPServerWithResources(stdioChannel(input: io.stdin, output: io.stdout));
17+
}
18+
19+
/// An MCP server with resource and resource template support.
20+
///
21+
/// This server uses the [ResourcesSupport] mixin to provide resources to the
22+
/// client.
23+
base class MCPServerWithResources extends MCPServer with ResourcesSupport {
24+
MCPServerWithResources(super.channel)
25+
: super.fromStreamChannel(
26+
implementation: Implementation(
27+
name: 'An example dart server with resources support',
28+
version: '0.1.0',
29+
),
30+
instructions: 'Just list and read the resources :D',
31+
) {
32+
// Add a standard resource with a fixed URI.
33+
addResource(
34+
Resource(uri: 'example://resource.txt', name: 'An example resource'),
35+
(request) => ReadResourceResult(
36+
contents: [TextResourceContents(text: 'Example!', uri: request.uri)],
37+
),
38+
);
39+
40+
// A resource template which always just returns the path portion of the
41+
// requested URI as the content of the resource.
42+
addResourceTemplate(
43+
ResourceTemplate(
44+
uriTemplate: 'example_template://{path}',
45+
name: 'Example resource template',
46+
),
47+
(request) {
48+
// This template only handles resource URIs with this exact prefix,
49+
// returning null defers to the next resource template handler.
50+
if (!request.uri.startsWith('example_template://')) {
51+
return null;
52+
}
53+
return ReadResourceResult(
54+
contents: [
55+
TextResourceContents(
56+
text: request.uri.substring('example_template://'.length),
57+
uri: request.uri,
58+
),
59+
],
60+
);
61+
},
62+
);
63+
}
64+
}

0 commit comments

Comments
 (0)