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
9 changes: 9 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.4.1-wip

- Fix the `resource` parameter type on `EmbeddedResource` to be
`ResourceContents` instead of `Contents`.
- **Note**: This is technically breaking but the previous API would not have
been possible to use in a functional manner, so it is assumed that it had
no usage previously.
- Fix the `type` getter on `EmbeddedResource` to read the actual type field.

## 0.4.0

- Update the tool calling example to include progress notifications.
Expand Down
5 changes: 3 additions & 2 deletions pkgs/dart_mcp/lib/src/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ extension type EmbeddedResource.fromMap(Map<String, Object?> _value)
static const expectedType = 'resource';

factory EmbeddedResource({
required Content resource,
required ResourceContents resource,
Annotations? annotations,
Meta? meta,
}) => EmbeddedResource.fromMap({
Expand All @@ -459,14 +459,15 @@ extension type EmbeddedResource.fromMap(Map<String, Object?> _value)
});

String get type {
final type = _value['resource'] as String;
final type = _value['type'] as String;
assert(type == expectedType);
return type;
}

/// Either [TextResourceContents] or [BlobResourceContents].
ResourceContents get resource => _value['resource'] as ResourceContents;

@Deprecated('Use `.resource.mimeType`.')
String? get mimeType => _value['mimeType'] as String?;
}

Expand Down
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: dart_mcp
version: 0.4.0
version: 0.4.1-wip
description: A package for making MCP servers and clients.
repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp
issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp
Expand Down
69 changes: 69 additions & 0 deletions pkgs/dart_mcp/test/api/tools_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ignore_for_file: lines_longer_than_80_chars

import 'dart:async';
import 'dart:convert';

import 'package:dart_mcp/server.dart';
import 'package:test/test.dart';
Expand Down Expand Up @@ -1966,6 +1967,74 @@ void main() {
final result = await serverConnection.callTool(request);
expect(result.structuredContent, {'bar': 'baz'});
});

test('can return embedded resources', () async {
final environment = TestEnvironment(
TestMCPClient(),
(channel) => TestMCPServerWithTools(
channel,
tools: [Tool(name: 'foo', inputSchema: ObjectSchema())],
toolHandlers: {
'foo': (request) {
return CallToolResult(
content: [
EmbeddedResource(
resource: TextResourceContents(
uri: 'file:///my_resource',
text: 'Really awesome text',
),
),
EmbeddedResource(
resource: BlobResourceContents(
uri: 'file:///my_resource',
blob: base64Encode([1, 2, 3, 4, 5, 6, 7, 8]),
),
),
],
);
},
},
),
);
final serverConnection = environment.serverConnection;
await serverConnection.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: environment.client.capabilities,
clientInfo: environment.client.implementation,
),
);
final request = CallToolRequest(name: 'foo', arguments: {});
final result = await serverConnection.callTool(request);
expect(result.content, hasLength(2));
expect(
result.content,
containsAll([
isA<EmbeddedResource>()
.having((r) => r.type, 'type', EmbeddedResource.expectedType)
.having(
(r) => r.resource,
'resource',
isA<TextResourceContents>().having(
(r) => r.text,
'text',
'Really awesome text',
),
),
isA<EmbeddedResource>()
.having((r) => r.type, 'type', 'resource')
.having(
(r) => r.resource,
'resource',
isA<BlobResourceContents>().having(
(r) => r.blob,
'blob',
base64Encode([1, 2, 3, 4, 5, 6, 7, 8]),
),
),
]),
);
});
});
}

Expand Down
2 changes: 2 additions & 0 deletions pkgs/dart_mcp_server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Fix analyze tool handling of invalid roots.
- Fix erroneous SDK version error messages when connecting to a VM Service
instead of DTD URI.
- Add a tool for reading package: URIs. Results are returned as embedded
resources or resource links (when reading a directory).

# 0.1.1 (Dart SDK 3.10.0)

Expand Down
1 change: 1 addition & 0 deletions pkgs/dart_mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ For more information, see the official VS Code documentation for
| `list_running_apps` | | Returns the list of running app process IDs and associated DTD URIs for apps started by the launch_app tool. |
| `pub` | pub | Runs a pub command for the given project roots, like `dart pub get` or `flutter pub add`. |
| `pub_dev_search` | pub.dev search | Searches pub.dev for packages relevant to a given search query. The response will describe each result with its download count, package description, topics, license, and publisher. |
| `read_package_uris` | | Reads "package" scheme URIs which represent paths under the lib directory of Dart package dependencies. Package uris are always relative, and the first segment is the package name. For example, the URI "package:test/test.dart" represents the path "lib/test.dart" under the "test" package. This API supports both reading files and listing directories. |
| `remove_roots` | Remove roots | Removes one or more project roots previously added via the add_roots tool. |
| `resolve_workspace_symbol` | Project search | Look up a symbol or symbols in all workspaces by name. Can be used to validate that a symbol exists or discover small spelling mistakes, since the search is fuzzy. |
| `run_tests` | Run tests | Run Dart or Flutter tests with an agent centric UX. ALWAYS use instead of `dart test` or `flutter test` shell commands. |
Expand Down
204 changes: 204 additions & 0 deletions pkgs/dart_mcp_server/lib/src/mixins/package_uri_reader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// 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.

import 'dart:async';
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:dart_mcp/server.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:mime/mime.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;

import '../utils/analytics.dart';
import '../utils/cli_utils.dart';
import '../utils/constants.dart';
import '../utils/file_system.dart';

/// Adds a tool for reading package URIs to an MCP server.
base mixin PackageUriSupport on ToolsSupport, RootsTrackingSupport
implements FileSystemSupport {
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
registerTool(readPackageUris, _readPackageUris);
return super.initialize(request);
}

Future<CallToolResult> _readPackageUris(CallToolRequest request) async {
final args = request.arguments!;
final validated = validateRootConfig(
args,
fileSystem: fileSystem,
knownRoots: await roots,
);
if (validated.errorResult case final error?) {
return error;
}
// The root is always non-null if there is no error present.
final root = validated.root!;

// Note that we intentionally do not cache this, because the work to deal
// with invalidating it would likely be more expensive than just
// re-discovering it.
final packageConfig = await findPackageConfig(
fileSystem.directory(Uri.parse(root.uri)),
);
if (packageConfig == null) {
return _noPackageConfigFound(root);
}

final resultContent = <Content>[];
for (final uri in (args[ParameterNames.uris] as List).cast<String>()) {
await for (final content in _readPackageUri(
Uri.parse(uri),
packageConfig,
)) {
resultContent.add(content);
}
}

return CallToolResult(content: resultContent);
}

Stream<Content> _readPackageUri(Uri uri, PackageConfig packageConfig) async* {
if (uri.scheme != 'package') {
yield TextContent(text: 'The URI "$uri" was not a "package:" URI.');
return;
}
final packageName = uri.pathSegments.first;
final path = p.url.joinAll(uri.pathSegments.skip(1));
final package = packageConfig.packages.firstWhereOrNull(
(package) => package.name == packageName,
);
if (package == null) {
yield TextContent(
text:
'The package "$packageName" was not found in your package config, '
'make sure it is listed in your dependencies, or use `pub add` to '
'add it.',
);
return;
}

if (package.root.scheme != 'file') {
// We expect all package roots to be file URIs.
yield Content.text(
text:
'Unexpected root URI for package $packageName '
'"${package.root.scheme}", only "file" schemes are supported',
);
return;
}

final packageRoot = Root(uri: package.root.toString());
final resolvedUri = package.packageUriRoot.resolve(path);
if (!isUnderRoot(packageRoot, resolvedUri.toString(), fileSystem)) {
yield TextContent(
text: 'The uri "$uri" attempted to escape it\'s package root.',
);
return;
}

final osFriendlyPath = p.fromUri(resolvedUri);
final entityType = await fileSystem.type(
osFriendlyPath,
followLinks: false,
);
switch (entityType) {
case FileSystemEntityType.directory:
final dir = fileSystem.directory(osFriendlyPath);
yield Content.text(text: '## Directory "$uri":');
await for (final entry in dir.list(followLinks: false)) {
yield ResourceLink(
name: p.basename(osFriendlyPath),
description: entry is Directory ? 'A directory' : 'A file',
uri: packageConfig.toPackageUri(entry.uri)!.toString(),
mimeType: lookupMimeType(entry.path) ?? '',
);
}
case FileSystemEntityType.link:
// We are only returning a reference to the target, so it is ok to not
// check the path. The agent may have the permissions to read the linked
// path on its own, even if it is outside of the package root.
var targetUri = resolvedUri.resolve(
await fileSystem.link(osFriendlyPath).target(),
);
// If we can represent it as a package URI, do so.
final asPackageUri = packageConfig.toPackageUri(targetUri);
if (asPackageUri != null) {
targetUri = asPackageUri;
}
yield ResourceLink(
name: p.basename(targetUri.path),
description: 'Target of symlink at $uri',
uri: targetUri.toString(),
mimeType: lookupMimeType(targetUri.path) ?? '',
);
case FileSystemEntityType.file:
final file = fileSystem.file(osFriendlyPath);
final mimeType = lookupMimeType(resolvedUri.path) ?? '';
final resourceUri = packageConfig.toPackageUri(resolvedUri)!.toString();
// Attempt to treat it as a utf8 String first, if that fails then just
// return it as bytes.
try {
yield Content.embeddedResource(
resource: TextResourceContents(
uri: resourceUri,
text: await file.readAsString(),
mimeType: mimeType,
),
);
} catch (_) {
yield Content.embeddedResource(
resource: BlobResourceContents(
uri: resourceUri,
mimeType: mimeType,
blob: base64Encode(await file.readAsBytes()),
),
);
}
case FileSystemEntityType.notFound:
yield TextContent(text: 'File not found: $uri');
default:
yield TextContent(
text: 'Unsupported file system entity type $entityType',
);
}
}

CallToolResult _noPackageConfigFound(Root root) => CallToolResult(
isError: true,
content: [
TextContent(
text:
'No package config found for root ${root.uri}. Have you ran `pub '
'get` in this project?',
),
],
)..failureReason = CallToolFailureReason.noPackageConfigFound;

@visibleForTesting
static final readPackageUris = Tool(
name: 'read_package_uris',
description:
'Reads "package" scheme URIs which represent paths under the lib '
'directory of Dart package dependencies. Package uris are always '
'relative, and the first segment is the package name. For example, the '
'URI "package:test/test.dart" represents the path "lib/test.dart" under '
'the "test" package. This API supports both reading files and listing '
'directories.',
inputSchema: Schema.object(
properties: {
ParameterNames.uris: Schema.list(
description: 'All the package URIs to read.',
items: Schema.string(),
),
ParameterNames.root: rootSchema,
},
required: [ParameterNames.uris, ParameterNames.root],
),
);
}
4 changes: 3 additions & 1 deletion pkgs/dart_mcp_server/lib/src/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'mixins/analyzer.dart';
import 'mixins/dash_cli.dart';
import 'mixins/dtd.dart';
import 'mixins/flutter_launcher.dart';
import 'mixins/package_uri_reader.dart';
import 'mixins/prompts.dart';
import 'mixins/pub.dart';
import 'mixins/pub_dev_search.dart';
Expand All @@ -45,7 +46,8 @@ final class DartMCPServer extends MCPServer
DartToolingDaemonSupport,
FlutterLauncherSupport,
PromptsSupport,
DashPrompts
DashPrompts,
PackageUriSupport
implements
AnalyticsSupport,
ProcessManagerSupport,
Expand Down
1 change: 1 addition & 0 deletions pkgs/dart_mcp_server/lib/src/utils/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ enum CallToolFailureReason {
invalidRootPath,
invalidRootScheme,
noActiveDebugSession,
noPackageConfigFound,
noRootGiven,
noRootsSet,
noSuchCommand,
Expand Down
Loading