-
Notifications
You must be signed in to change notification settings - Fork 24
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
Merged
Merged
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
cf1b4b4
A pub search functionality
sigurdm 65de883
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm dbcabff
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm ff7cc43
Detailed search term description
sigurdm a383fb5
Rename argument `search-query` -> `query`
sigurdm 0fe6f8c
Remove unused import
sigurdm 5426861
Underscores in tool name
sigurdm 245d790
Readonlyhint: true
sigurdm 8f5e9cb
Remove copy-paste waste
sigurdm ce713a1
Extract `dig` json utility
sigurdm cd3ba90
Remove unused query parameters
sigurdm f9219b4
Make client mockable
sigurdm f1f9f2c
Add test
sigurdm 2b28b8d
Better golden file error message
sigurdm 084cfe7
Privatify name
sigurdm c803a7b
Test failure
sigurdm e0afe2d
Run subqueries in parallel
sigurdm 29888c8
Test and fix json dig
sigurdm 7442c9c
Make pool top-level static
sigurdm 9070d8a
Update pkgs/dart_tooling_mcp_server/test_fixtures/pub_dev_responses/R…
sigurdm b46121c
Update pkgs/dart_tooling_mcp_server/test_fixtures/pub_dev_responses/R…
sigurdm 9e1283a
Update pkgs/dart_tooling_mcp_server/lib/src/utils/json.dart
sigurdm ab08279
json util, handle specified types
sigurdm 77ccf09
Update pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
sigurdm 95da176
Handle generic types
sigurdm 7a4c89c
Remove nonsense
sigurdm 525ea44
Bump subosito/flutter-action from 1 to 2 in the github-actions group …
dependabot[bot] 2093225
Making `instructions` optional. (#98)
domesticmouse b10e03c
Require roots for all CLI tools (#101)
jakemac53 29cd5b2
Add runtime errors resource and tool to clear errors. (#94)
jakemac53 adf7ee9
add option to log protocol messages to a Sink<String> (#102)
jakemac53 fed9134
Modify DTD connection verbiage in example client (#104)
kenzieschmoll 4871a32
add test for server closing early to validate behavior (#105)
jakemac53 06c71c7
Continue making `instructions` optional. (#107)
domesticmouse 23aa993
Release dart_mcp version 0.2.0 (#106)
jakemac53 009219c
Add supported tools to the MCP server README (#109)
kenzieschmoll 346d7c4
add signature_help tool (#110)
jakemac53 f5c83bd
show thinking text and input/output token usage (#108)
jakemac53 cc41bfb
Tolerate missing sub-responses
sigurdm e483a8c
Report error when no packages found
sigurdm 472d00b
merge
sigurdm b30e7f9
dartfmt
sigurdm 10c723d
Extract result count constant
sigurdm 1f06c85
End with period
sigurdm 0745135
Use named records
sigurdm 152457d
End with period
sigurdm 524fac2
Break up lines
sigurdm b9d78aa
Extract test helper
sigurdm c3b41d8
Explain no 'or' operator in tool description
sigurdm d36f0f5
Merge
sigurdm 5636292
Fix merge
sigurdm d0cdb29
Sort dependencies
sigurdm 1e4524e
Address some of review
sigurdm e7f3935
Fix rename
sigurdm e3e187e
Refactor to use `runWithClient`
sigurdm 9e6e34b
dynamic -> Object
sigurdm b808801
Use list of strings instead of list of objects
sigurdm 6c98900
Restrict max number of listed identifiers
sigurdm a9c13b9
Report the publisher of the package
sigurdm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
211 changes: 211 additions & 0 deletions
211
pkgs/dart_tooling_mcp_server/lib/src/mixins/pub_dev_search.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// 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'; | ||
|
||
/// Limit the number of concurrent requests. | ||
final _pool = Pool(10); | ||
|
||
/// The number of reults to return for a query. | ||
// If this should be set higher than 10 we need to implement paging of the | ||
// http://pub.dev/api/search endpoint. | ||
final _resultsLimit = 10; | ||
|
||
/// The number of identifiers we list per packages. | ||
final _maxIdentifiersListed = 200; | ||
|
||
/// Mix this in to any MCPServer to add support for doing searches on pub.dev. | ||
base mixin PubDevSupport on ToolsSupport { | ||
final _client = Client(); | ||
|
||
@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 `query`.')], | ||
isError: true, | ||
); | ||
} | ||
final searchUrl = Uri.https('pub.dev', 'api/search', {'q': query}); | ||
final Object? result; | ||
try { | ||
result = jsonDecode(await _client.read(searchUrl)); | ||
|
||
final packageNames = | ||
dig<List>(result, ['packages']) | ||
.take(_resultsLimit) | ||
.map((p) => dig<String>(p, ['package'])) | ||
.toList(); | ||
sigurdm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (packageNames.isEmpty) { | ||
return CallToolResult( | ||
content: [ | ||
TextContent( | ||
text: 'No packages mached the query, consider simplifying it.', | ||
), | ||
], | ||
isError: true, | ||
); | ||
} | ||
|
||
Future<Object?> retrieve(String path) { | ||
return _pool.withResource(() async { | ||
try { | ||
return jsonDecode(await _client.read(Uri.https('pub.dev', path))); | ||
} on ClientException { | ||
return null; | ||
} | ||
}); | ||
} | ||
|
||
// Retrieve information about all the packages in parallel. | ||
final subQueryFutures = | ||
packageNames | ||
.map( | ||
(packageName) => ( | ||
versionListing: retrieve('api/packages/$packageName'), | ||
score: retrieve('api/packages/$packageName/score'), | ||
docIndex: retrieve( | ||
'documentation/$packageName/latest/index.json', | ||
), | ||
), | ||
) | ||
.toList(); | ||
|
||
// 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 = await subQueryFutures[i].versionListing; | ||
final scoreResult = await subQueryFutures[i].score; | ||
final docIndex = await subQueryFutures[i].docIndex; | ||
|
||
Map<String, Object?> identifiers(Object index) { | ||
final items = dig<List>(index, []); | ||
return { | ||
'qualifiedNames': [ | ||
for (final item in items.take(_maxIdentifiersListed)) | ||
dig<String>(item, ['qualifiedName']), | ||
], | ||
}; | ||
} | ||
|
||
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(), | ||
'publisher': | ||
dig<List>(scoreResult, ['tags']) | ||
.where((t) => (t as String).startsWith('publisher:')) | ||
.firstOrNull, | ||
}, | ||
if (docIndex != null) ...{'api': identifiers(docIndex)}, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
return CallToolResult(content: results); | ||
} on Exception catch (e) { | ||
return CallToolResult( | ||
content: [TextContent(text: 'Failed searching pub.dev: $e')], | ||
isError: true, | ||
); | ||
} | ||
} | ||
|
||
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, publisher, and a list of ' | ||
'identifiers in the public api.', | ||
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. | ||
|
||
To search for alternatives do multiple searches. There is no "or" operator. | ||
''', | ||
), | ||
}, | ||
required: ['query'], | ||
), | ||
); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
sigurdm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// | ||
/// 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<Object> 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', | ||
sigurdm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.