Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions mcp_examples/bin/file_system_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,13 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;

import 'package:async/async.dart';
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp/stdio.dart';
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';

void main() {
SimpleFileSystemServer.fromStreamChannel(
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
.transform(StreamChannelTransformer.fromCodec(utf8))
.transformStream(const LineSplitter())
.transformSink(
StreamSinkTransformer.fromHandlers(
handleData: (data, sink) {
sink.add('$data\n');
},
),
),
stdioChannel(input: io.stdin, output: io.stdout),
);
}

Expand Down
9 changes: 4 additions & 5 deletions mcp_examples/bin/workflow_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:args/args.dart';
import 'package:async/async.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:dart_mcp/client.dart';
import 'package:dart_mcp/stdio.dart';
import 'package:google_generative_ai/google_generative_ai.dart' as gemini;

/// The list of Gemini models that are accepted as a "--model" argument.
Expand Down Expand Up @@ -414,12 +415,10 @@ final class WorkflowClient extends MCPClient with RootsSupport {
parts.skip(1).toList(),
);
serverConnections.add(
connectStdioServer(
process.stdin,
process.stdout,
connectServer(
stdioChannel(input: process.stdout, output: process.stdin),
protocolLogSink: logSink,
onDone: process.kill,
),
)..done.then((_) => process.kill()),
);
} catch (e) {
logger.stderr('Failed to connect to server $server: $e');
Expand Down
4 changes: 4 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

## 0.3.0

Expand Down
16 changes: 11 additions & 5 deletions pkgs/dart_mcp/example/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Simple Client and Server
# Client and Server examples

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

# Full Features Examples
To run the examples, run the client file directly, so for instance
`dart run example/tools_client.dart` with run the example client which invokes
tools, connected to the example server that provides tools
(at `example/tools_server.dart`).

# Full Featured Examples

See https://github.com/dart-lang/ai/tree/main/mcp_examples for some more full
featured examples using gemini to automatically invoke tools.
Expand Down
82 changes: 82 additions & 0 deletions pkgs/dart_mcp/example/prompts_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2025, 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 client that interacts with a server that provides prompts.
library;

import 'dart:async';
import 'dart:io';

import 'package:dart_mcp/client.dart';
import 'package:dart_mcp/stdio.dart';

void main() async {
// Create the client, which is the top level object that manages all
// server connections.
final client = MCPClient(
Implementation(name: 'example dart client', version: '0.1.0'),
);
print('connecting to server');

// Start the server as a separate process.
final process = await Process.start('dart', [
'run',
'example/prompts_server.dart',
]);
// Connect the client to the server.
final server = client.connectServer(
stdioChannel(input: process.stdout, output: process.stdin),
);
// When the server connection is closed, kill the process.
unawaited(server.done.then((_) => process.kill()));
print('server started');

// Initialize the server and let it know our capabilities.
print('initializing server');
final initializeResult = await server.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: client.capabilities,
clientInfo: client.implementation,
),
);
print('initialized: $initializeResult');

// Ensure the server supports the prompts capability.
if (initializeResult.capabilities.prompts == null) {
await server.shutdown();
throw StateError('Server doesn\'t support prompts!');
}

// Notify the server that we are initialized.
server.notifyInitialized();
print('sent initialized notification');

// List all the available prompts from the server.
print('Listing prompts from server');
final promptsResult = await server.listPrompts(ListPromptsRequest());
for (final prompt in promptsResult.prompts) {
// For each prompt, get the full prompt text, filling in any arguments.
final promptResult = await server.getPrompt(
GetPromptRequest(
name: prompt.name,
arguments: {
for (var arg in prompt.arguments ?? <PromptArgument>[])
arg.name: switch (arg.name) {
'tags' => 'myTag myOtherTag',
'platforms' => 'vm,chrome',
_ => throw ArgumentError('Unrecognized argument ${arg.name}'),
},
},
),
);
final promptText = promptResult.messages
.map((m) => (m.content as TextContent).text)
.join('');
print('Found prompt `${prompt.name}`: "$promptText"');
}

// Shutdown the client, which will also shutdown the server connection.
await client.shutdown();
}
80 changes: 80 additions & 0 deletions pkgs/dart_mcp/example/prompts_server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2025, 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe for these examples, it would be good to add a dartdoc comment at the top that describes what each example is meant to show and a pointer to the README.md. In general, for examples, these don't have enough comments.

It's also helpful when perusing the code in an IDE to have dartdoc links back to the classes that are relevant (e.g. to MCPServer and PromptsSupport for this one).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had Gemini add some comments and then cleaned them up a bit, also moved some things around to simplify it.

The version checking for instance doesn't need to happen manually (it is handled for you) and also things can be registered in the constructor instead of initialize which is more concise (no need to override and call super).

/// A server that implements the prompts API using the [PromptsSupport] mixin.
library;

import 'dart:io' as io;

import 'package:dart_mcp/server.dart';
import 'package:dart_mcp/stdio.dart';

void main() {
// Create the server and connect it to stdio.
MCPServerWithPrompts(stdioChannel(input: io.stdin, output: io.stdout));
}

/// Our actual MCP server.
///
/// This server uses the [PromptsSupport] mixin to provide prompts to the
/// client.
base class MCPServerWithPrompts extends MCPServer with PromptsSupport {
MCPServerWithPrompts(super.channel)
: super.fromStreamChannel(
implementation: Implementation(
name: 'An example dart server with prompts support',
version: '0.1.0',
),
instructions: 'Just list the prompts :D',
) {
// Actually add the prompt.
addPrompt(runTestsPrompt, _runTestsPrompt);
}

/// The prompt implementation, takes in a [request] and builds the prompt
/// by substituting in arguments.
GetPromptResult _runTestsPrompt(GetPromptRequest request) {
// The actual arguments should be comma separated, but we allow for space
// separated and then convert it here.
final tags = (request.arguments?['tags'] as String?)?.split(' ').join(',');
final platforms = (request.arguments?['platforms'] as String?)
?.split(' ')
.join(',');
return GetPromptResult(
messages: [
// This is a prompt that should execute as if it came from the user,
// instructing the LLM to run a specific CLI command based on the
// arguments given.
PromptMessage(
role: Role.user,
content: Content.text(
text:
'Execute the shell command `dart test --failures-only'
'${tags != null ? ' -t $tags' : ''}'
'${platforms != null ? ' -p $platforms' : ''}'
'`',
),
),
],
);
}

/// A prompt that can be used to run tests.
///
/// This prompt has two arguments, `tags` and `platforms`.
final runTestsPrompt = Prompt(
name: 'run_tests',
description: 'Run your dart tests',
arguments: [
PromptArgument(
name: 'tags',
description: 'The test tags to include, space or comma separated',
),
PromptArgument(
name: 'platforms',
description: 'The platforms to run on, space or comma separated',
),
],
);
}
87 changes: 87 additions & 0 deletions pkgs/dart_mcp/example/resources_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) 2025, 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 client that connects to a server and exercises the resources API.
import 'dart:async';
import 'dart:io';

import 'package:dart_mcp/client.dart';
import 'package:dart_mcp/stdio.dart';

void main() async {
// Create a client, which is the top level object that manages all
// server connections.
final client = MCPClient(
Implementation(name: 'example dart client', version: '0.1.0'),
);
print('connecting to server');

// Start the server as a separate process.
final process = await Process.start('dart', [
'run',
'example/resources_server.dart',
]);
// Connect the client to the server.
final server = client.connectServer(
stdioChannel(input: process.stdout, output: process.stdin),
);
// When the server connection is closed, kill the process.
unawaited(server.done.then((_) => process.kill()));
print('server started');

// Initialize the server and let it know our capabilities.
print('initializing server');
final initializeResult = await server.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: client.capabilities,
clientInfo: client.implementation,
),
);
print('initialized: $initializeResult');

// Ensure the server supports the resources capability.
if (initializeResult.capabilities.resources == null) {
await server.shutdown();
throw StateError('Server doesn\'t support resources!');
}

// Notify the server that we are initialized.
server.notifyInitialized();
print('sent initialized notification');

// List all the available resources from the server.
print('Listing resources from server');
final resourcesResult = await server.listResources(ListResourcesRequest());
for (final resource in resourcesResult.resources) {
// For each resource, read its content.
final content = (await server.readResource(
ReadResourceRequest(uri: resource.uri),
)).contents.map((part) => (part as TextResourceContents).text).join('');
print(
'Found resource: ${resource.name} with uri ${resource.uri} and contents: '
'"$content"',
);
}

// List all the available resource templates from the server.
print('Listing resource templates from server');
final templatesResult = await server.listResourceTemplates(
ListResourceTemplatesRequest(),
);
for (final template in templatesResult.resourceTemplates) {
print('Found resource template `${template.uriTemplate}`');
// For each template, fill in the path variable and read the resource.
for (var path in ['zip', 'zap']) {
final uri = template.uriTemplate.replaceFirst(RegExp('{.*}'), path);
final contents = (await server.readResource(
ReadResourceRequest(uri: uri),
)).contents.map((part) => (part as TextResourceContents).text).join('');
print('Read resource `$uri`: "$contents"');
}
}

// Shutdown the client, which will also shutdown the server connection.
await client.shutdown();
}
64 changes: 64 additions & 0 deletions pkgs/dart_mcp/example/resources_server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2025, 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 that implements the resources API using the [ResourcesSupport]
/// mixin.
library;

import 'dart:io' as io;

import 'package:dart_mcp/server.dart';
import 'package:dart_mcp/stdio.dart';

void main() {
// Create the server and connect it to stdio.
MCPServerWithResources(stdioChannel(input: io.stdin, output: io.stdout));
}

/// An MCP server with resource and resource template support.
///
/// This server uses the [ResourcesSupport] mixin to provide resources to the
/// client.
base class MCPServerWithResources extends MCPServer with ResourcesSupport {
MCPServerWithResources(super.channel)
: super.fromStreamChannel(
implementation: Implementation(
name: 'An example dart server with resources support',
version: '0.1.0',
),
instructions: 'Just list and read the resources :D',
) {
// Add a standard resource with a fixed URI.
addResource(
Resource(uri: 'example://resource.txt', name: 'An example resource'),
(request) => ReadResourceResult(
contents: [TextResourceContents(text: 'Example!', uri: request.uri)],
),
);

// A resource template which always just returns the path portion of the
// requested URI as the content of the resource.
addResourceTemplate(
ResourceTemplate(
uriTemplate: 'example_template://{path}',
name: 'Example resource template',
),
(request) {
// This template only handles resource URIs with this exact prefix,
// returning null defers to the next resource template handler.
if (!request.uri.startsWith('example_template://')) {
return null;
}
return ReadResourceResult(
contents: [
TextResourceContents(
text: request.uri.substring('example_template://'.length),
uri: request.uri,
),
],
);
},
);
}
}
Loading