Skip to content

A 'pub-dev-search' mcp tool #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cf1b4b4
A pub search functionality
sigurdm May 2, 2025
65de883
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm May 5, 2025
dbcabff
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm May 5, 2025
ff7cc43
Detailed search term description
sigurdm May 5, 2025
a383fb5
Rename argument `search-query` -> `query`
sigurdm May 5, 2025
0fe6f8c
Remove unused import
sigurdm May 5, 2025
5426861
Underscores in tool name
sigurdm May 5, 2025
245d790
Readonlyhint: true
sigurdm May 5, 2025
8f5e9cb
Remove copy-paste waste
sigurdm May 5, 2025
ce713a1
Extract `dig` json utility
sigurdm May 5, 2025
cd3ba90
Remove unused query parameters
sigurdm May 5, 2025
f9219b4
Make client mockable
sigurdm May 5, 2025
f1f9f2c
Add test
sigurdm May 5, 2025
2b28b8d
Better golden file error message
sigurdm May 5, 2025
084cfe7
Privatify name
sigurdm May 5, 2025
c803a7b
Test failure
sigurdm May 5, 2025
e0afe2d
Run subqueries in parallel
sigurdm May 5, 2025
29888c8
Test and fix json dig
sigurdm May 5, 2025
7442c9c
Make pool top-level static
sigurdm May 5, 2025
9070d8a
Update pkgs/dart_tooling_mcp_server/test_fixtures/pub_dev_responses/R…
sigurdm May 5, 2025
b46121c
Update pkgs/dart_tooling_mcp_server/test_fixtures/pub_dev_responses/R…
sigurdm May 5, 2025
9e1283a
Update pkgs/dart_tooling_mcp_server/lib/src/utils/json.dart
sigurdm May 6, 2025
ab08279
json util, handle specified types
sigurdm May 6, 2025
77ccf09
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm May 6, 2025
95da176
Handle generic types
sigurdm May 6, 2025
7a4c89c
Remove nonsense
sigurdm May 6, 2025
525ea44
Bump subosito/flutter-action from 1 to 2 in the github-actions group …
dependabot[bot] May 1, 2025
2093225
Making `instructions` optional. (#98)
domesticmouse May 1, 2025
b10e03c
Require roots for all CLI tools (#101)
jakemac53 May 2, 2025
29cd5b2
Add runtime errors resource and tool to clear errors. (#94)
jakemac53 May 2, 2025
adf7ee9
add option to log protocol messages to a Sink<String> (#102)
jakemac53 May 2, 2025
fed9134
Modify DTD connection verbiage in example client (#104)
kenzieschmoll May 2, 2025
4871a32
add test for server closing early to validate behavior (#105)
jakemac53 May 2, 2025
06c71c7
Continue making `instructions` optional. (#107)
domesticmouse May 4, 2025
23aa993
Release dart_mcp version 0.2.0 (#106)
jakemac53 May 5, 2025
009219c
Add supported tools to the MCP server README (#109)
kenzieschmoll May 5, 2025
346d7c4
add signature_help tool (#110)
jakemac53 May 5, 2025
f5c83bd
show thinking text and input/output token usage (#108)
jakemac53 May 5, 2025
cc41bfb
Tolerate missing sub-responses
sigurdm May 6, 2025
e483a8c
Report error when no packages found
sigurdm May 6, 2025
472d00b
merge
sigurdm May 6, 2025
b30e7f9
dartfmt
sigurdm May 6, 2025
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
1 change: 1 addition & 0 deletions pkgs/dart_tooling_mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ WIP. This package is still experimental and is likely to evolve quickly.
| `dart_fix` | `static tool` | Runs `dart fix --apply` for the given project roots. |
| `dart_format` | `static tool` | Runs `dart format .` for the given project roots. |
| `pub` | `static tool` | Runs a `dart pub` command for the given project roots. |
| `pub_dev_search` | `package search` | Searches pub.dev for packages relevant to a given search query. |
| `get_runtime_errors` | `runtime analysis` | Retrieves the list of runtime errors that have occurred in the active Dart or Flutter application. |
| `take_screenshot` | `runtime analysis` | Takes a screenshot of the active Flutter application in its current state. |
| `get_widget_tree` | `runtime analysis` | Retrieves the widget tree from the active Flutter application. |
Expand Down
212 changes: 212 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// 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:dart_mcp/server.dart';
import 'package:http/http.dart';
import 'package:pool/pool.dart';

import '../utils/json.dart';

// Override this to stub responses for testing.
Client Function() createClient = Client.new;

/// Limit the number of concurrent requests.
final _pool = Pool(10);

/// Mix this in to any MCPServer to add support for doing searches on pub.dev.
base mixin PubDevSupport on ToolsSupport {
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
registerTool(pubDevTool, _runPubDevSearch);
return super.initialize(request);
}

/// Implementation of the [pubDevTool].
Future<CallToolResult> _runPubDevSearch(CallToolRequest request) async {
final query = request.arguments?['query'] as String?;
if (query == null) {
return CallToolResult(
content: [
TextContent(text: 'Missing required argument `search-query`.'),
],
isError: true,
);
}
final client = createClient();
final searchUrl = Uri(
scheme: 'https',
host: 'pub.dev',
path: 'api/search',
queryParameters: {'q': query},
);
final Object? result;
try {
result = jsonDecode(await client.read(searchUrl));

final packageNames =
dig<List>(result, [
'packages',
]).take(10).map((p) => dig<String>(p, ['package'])).toList();

if (packageNames.isEmpty) {
return CallToolResult(
content: [
TextContent(
text: 'No packages mached the query, consider simplifying it',
),
],
isError: true,
);
}

Future<Object?> retrieve(String path) async {
return _pool.withResource(() async {
try {
return jsonDecode(
await client.read(
Uri(scheme: 'https', host: 'pub.dev', path: path),
),
);
} on ClientException {
return null;
}
});
}

// Retrieve information about all the packages in parallel.
final subQueryFutures = packageNames.map(
(packageName) =>
(
retrieve('api/packages/$packageName'),
retrieve('api/packages/$packageName/score'),
retrieve('documentation/$packageName/latest/index.json'),
).wait,
);

final subqueryResults = await Future.wait(subQueryFutures);

// Aggregate the retrieved information about each package into a
// TextContent.
final results = <TextContent>[];
for (var i = 0; i < packageNames.length; i++) {
final packageName = packageNames[i];
final versionListing = subqueryResults[i].$1;
final scoreResult = subqueryResults[i].$2;
final index = subqueryResults[i].$3;

List<Object?> identifiers(Object index) {
final items = dig<List>(index, []);
final identifiers = <Map<String, Object?>>[];
for (final item in items) {
identifiers.add({
'qualifiedName': dig(item, ['qualifiedName']),
'desc': 'Object holding options for retrying a function.',
});
}
return identifiers;
}

results.add(
TextContent(
text: jsonEncode({
'packageName': packageName,
if (versionListing != null) ...{
'latestVersion': dig<String>(versionListing, [
'latest',
'version',
]),
'description': dig<String>(versionListing, [
'latest',
'pubspec',
'description',
]),
},
if (scoreResult != null) ...{
'scores': {
'pubPoints': dig<int>(scoreResult, ['grantedPoints']),
'maxPubPoints': dig<int>(scoreResult, ['maxPoints']),
'likes': dig<int>(scoreResult, ['likeCount']),
'downloadCount': dig<int>(scoreResult, [
'downloadCount30Days',
]),
},
'topics':
dig<List>(
scoreResult,
['tags'],
).where((t) => (t as String).startsWith('topic:')).toList(),
'licenses':
dig<List>(scoreResult, ['tags'])
.where((t) => (t as String).startsWith('license'))
.toList(),
},
if (index != null) ...{'api': identifiers(index)},
}),
),
);
}

return CallToolResult(content: results);
} on Exception catch (e) {
return CallToolResult(
content: [TextContent(text: 'Failed searching pub.dev: $e')],
isError: true,
);
} finally {
client.close();
}
}

static final pubDevTool = Tool(
name: 'pub_dev_search',
description:
'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 a list of identifiers '
'in the public api',

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

annotations: ToolAnnotations(title: 'pub.dev search', readOnlyHint: true),
inputSchema: Schema.object(
properties: {
'query': Schema.string(
title: 'Search query',
description: '''
The query to run against pub.dev package search.

Besides freeform keyword search `pub.dev` supports the following search query
expressions:

- `"exact phrase"`: By default, when you perform a search, the results include
packages with similar phrases. When a phrase is inside quotes, you'll see
only those packages that contain exactly the specified phrase.

- `dependency:<package_name>`: Searches for packages that reference
`package_name` in their `pubspec.yaml`.

- `dependency*:<package_name>`: Searches for packages that depend on
`package_name` (as direct, dev, or transitive dependencies).

- `topic:<topic-name>`: Searches for packages that have specified the
`topic-name` [topic](/topics).

- `publisher:<publisher-name.com>`: Searches for packages published by `publisher-name.com`

- `sdk:<sdk>`: Searches for packages that support the given SDK. `sdk` can be either `flutter` or `dart`

- `runtime:<runtime>`: Searches for packages that support the given runtime. `runtime` can be one of `web`, `native-jit` and `native-aot`.

- `updated:<duration>`: Searches for packages updated in the given past days,
with the following recognized formats: `3d` (3 days), `2w` (two weeks), `6m` (6 months), `2y` 2 years.

- `has:executable`: Search for packages with Dart files in their `bin/` directory.
''',
),
},
required: ['query'],
),
);
}
2 changes: 2 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'mixins/analyzer.dart';
import 'mixins/dart_cli.dart';
import 'mixins/dtd.dart';
import 'mixins/pub.dart';
import 'mixins/pub_dev_search.dart';
import 'utils/process_manager.dart';

/// An MCP server for Dart and Flutter tooling.
Expand All @@ -25,6 +26,7 @@ final class DartToolingMCPServer extends MCPServer
DartAnalyzerSupport,
DartCliSupport,
PubSupport,
PubDevSupport,
DartToolingDaemonSupport
implements ProcessManagerSupport {
DartToolingMCPServer(
Expand Down
111 changes: 111 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/utils/json.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.

/// Utility for indexing json data structures.
///
/// Each element of [path] should be a `String`, `int` or `(String, String)`.
///
/// For each element `key` of [path], recurse into [json].
///
/// If the `key` is a String, the next json structure should be a Map, and have
/// `key` as a property. Recurse into that property.
///
/// If `key` is an `int`, the next json structure must be a List, with that
/// index. Recurse into that index.
///
/// If `key` is a `(String k, String v)` the next json structure must be a List
/// of maps, one of them having the property `k` with value `v`, recurse into
/// that map.
///
/// If at some point the types don't match throw a [FormatException].
///
/// Returns the result as a [T].
T dig<T>(dynamic json, List<dynamic> path) {
var i = 0;
String currentElementType() => switch (json) {
Null _ => 'null',
num _ => 'a number',
String _ => 'a string',
List _ => 'a list',
Map _ => 'a map',
_ => throw ArgumentError('Bad json'),
};
String currentPath() =>
i == 0 ? 'root' : path.sublist(0, i).map((i) => '[$i]').join('');
for (; i < path.length; i++) {
outer:
switch (path[i]) {
case final String key:
if (json is! Map) {
throw FormatException(
'Expected a map at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
json = json[key];
case final int key:
if (json is! List) {
throw FormatException(
'Expected a list at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
if (key >= json.length) {
throw FormatException(
'Expected at least ${key + 1} element(s) at ${currentPath()}. '
'Found only ${json.length} element(s)',
);
}
json = json[key];
case (final String key, final String value):
if (json is! List) {
throw FormatException(
'Expected a list at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
final t = json;
for (var j = 0; j < t.length; j++) {
final element = t[j];
if (element is! Map) {
json = element;
throw FormatException(
'Expected a map at ${currentPath()}[$j]. '
'Found ${currentElementType()}.',
);
}
if (element[key] == value) {
json = element;
break outer;
}
}
throw FormatException(
'No element with $key=$value at ${currentPath()}',
);
case final key:
throw ArgumentError('Bad key $key in', 'path');
}
}

if (json is! T) {
final targetTypeName = switch (T) {
const (int) => 'an int',
const (double) => 'a number',
const (num) => 'a number',
const (String) => 'a string',
const (List) => 'a list',
const (Map) => 'a map',
const (List<Object?>) => 'a list',
const (Map<String, Object?>) => 'a map',
const (Map<String, dynamic>) => 'a map',
const (Null) => 'null',
_ => throw ArgumentError('$T is not a json type'),
};
throw FormatException(
'Expected $targetTypeName at ${currentPath()}. '
'Found ${currentElementType()}.',
);
}
return json;
}
2 changes: 2 additions & 0 deletions pkgs/dart_tooling_mcp_server/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
dds_service_extensions: ^2.0.1
devtools_shared: ^11.2.0
dtd: ^2.4.0
http: ^1.3.0
json_rpc_2: ^3.0.3
# TODO: Get this another way.
language_server_protocol:
Expand All @@ -27,6 +28,7 @@ dependencies:
third_party/pkg/language_server_protocol
meta: ^1.16.0
path: ^1.9.1
pool: ^1.5.1
process: ^5.0.3
stream_channel: ^2.1.4
test_descriptor: ^2.0.2
Expand Down
Loading