Skip to content

Commit 52adf08

Browse files
authored
add homepage, repository, and documentation links to the pub result (#155)
Closes #154 This won't automatically include examples, as that would be a fair bit more involved, but it will include the documentation link if provided by the package.
1 parent fa1c2be commit 52adf08

File tree

13 files changed

+191
-144
lines changed

13 files changed

+191
-144
lines changed

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Dart SDK 3.8.0 - WP
22

3+
* Add documentation/homepage/repository links to pub results.
34
* Handle relative paths under roots without trailing slashes.
45
* Fix executable paths for dart/flutter on windows.
56
* Pass the provided root instead of the resolved root for project type detection.

pkgs/dart_mcp_server/bin/main.dart

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,31 @@ void main(List<String> args) async {
6969
);
7070
}
7171

72-
final argParser =
73-
ArgParser(allowTrailingOptions: false)
74-
..addOption(
75-
dartSdkOption,
76-
help:
77-
'The path to the root of the desired Dart SDK. Defaults to the '
78-
'DART_SDK environment variable.',
79-
)
80-
..addOption(
81-
flutterSdkOption,
82-
help:
83-
'The path to the root of the desired Flutter SDK. Defaults to '
84-
'the FLUTTER_SDK environment variable, then searching up from the '
85-
'Dart SDK.',
86-
)
87-
..addFlag(
88-
forceRootsFallback,
89-
negatable: true,
90-
defaultsTo: false,
91-
help:
92-
'Forces a behavior for project roots which uses MCP tools instead '
93-
'of the native MCP roots. This can be helpful for clients like '
94-
'cursor which claim to have roots support but do not actually '
95-
'support it.',
96-
)
97-
..addFlag(help, abbr: 'h', help: 'Show usage text');
72+
final argParser = ArgParser(allowTrailingOptions: false)
73+
..addOption(
74+
dartSdkOption,
75+
help:
76+
'The path to the root of the desired Dart SDK. Defaults to the '
77+
'DART_SDK environment variable.',
78+
)
79+
..addOption(
80+
flutterSdkOption,
81+
help:
82+
'The path to the root of the desired Flutter SDK. Defaults to '
83+
'the FLUTTER_SDK environment variable, then searching up from the '
84+
'Dart SDK.',
85+
)
86+
..addFlag(
87+
forceRootsFallback,
88+
negatable: true,
89+
defaultsTo: false,
90+
help:
91+
'Forces a behavior for project roots which uses MCP tools instead '
92+
'of the native MCP roots. This can be helpful for clients like '
93+
'cursor which claim to have roots support but do not actually '
94+
'support it.',
95+
)
96+
..addFlag(help, abbr: 'h', help: 'Show usage text');
9897

9998
const dartSdkOption = 'dart-sdk';
10099
const flutterSdkOption = 'flutter-sdk';

pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,18 @@ base mixin DartAnalyzerSupport
109109
log(LoggingLevel.warning, line, logger: 'DartLanguageServer');
110110
});
111111

112-
_lspConnection =
113-
Peer(lspChannel(_lspServer.stdout, _lspServer.stdin))
114-
..registerMethod(
115-
lsp.Method.textDocument_publishDiagnostics.toString(),
116-
_handleDiagnostics,
117-
)
118-
..registerMethod(r'$/analyzerStatus', _handleAnalyzerStatus)
119-
..registerFallback((Parameters params) {
120-
log(
121-
LoggingLevel.debug,
122-
() => 'Unhandled LSP message: ${params.method} - ${params.asMap}',
123-
);
124-
});
112+
_lspConnection = Peer(lspChannel(_lspServer.stdout, _lspServer.stdin))
113+
..registerMethod(
114+
lsp.Method.textDocument_publishDiagnostics.toString(),
115+
_handleDiagnostics,
116+
)
117+
..registerMethod(r'$/analyzerStatus', _handleAnalyzerStatus)
118+
..registerFallback((Parameters params) {
119+
log(
120+
LoggingLevel.debug,
121+
() => 'Unhandled LSP message: ${params.method} - ${params.asMap}',
122+
);
123+
});
125124

126125
unawaited(_lspConnection.listen());
127126

@@ -355,8 +354,9 @@ base mixin DartAnalyzerSupport
355354
diagnostics[diagnosticParams.uri] = diagnosticParams.diagnostics;
356355
log(LoggingLevel.debug, {
357356
ParameterNames.uri: diagnosticParams.uri,
358-
'diagnostics':
359-
diagnosticParams.diagnostics.map((d) => d.toJson()).toList(),
357+
'diagnostics': diagnosticParams.diagnostics
358+
.map((d) => d.toJson())
359+
.toList(),
360360
});
361361
}
362362

@@ -368,16 +368,18 @@ base mixin DartAnalyzerSupport
368368
final newRoots = await roots;
369369

370370
final oldWorkspaceFolders = _currentWorkspaceFolders;
371-
final newWorkspaceFolders =
372-
_currentWorkspaceFolders = HashSet<lsp.WorkspaceFolder>(
371+
final newWorkspaceFolders = _currentWorkspaceFolders =
372+
HashSet<lsp.WorkspaceFolder>(
373373
equals: (a, b) => a.uri == b.uri,
374374
hashCode: (a) => a.uri.hashCode,
375375
)..addAll(newRoots.map((r) => r.asWorkspaceFolder));
376376

377-
final added =
378-
newWorkspaceFolders.difference(oldWorkspaceFolders).toList();
379-
final removed =
380-
oldWorkspaceFolders.difference(newWorkspaceFolders).toList();
377+
final added = newWorkspaceFolders
378+
.difference(oldWorkspaceFolders)
379+
.toList();
380+
final removed = oldWorkspaceFolders
381+
.difference(newWorkspaceFolders)
382+
.toList();
381383

382384
// This can happen in the case of multiple notifications in quick
383385
// succession, the `roots` future will complete only after the state has

pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
109109
// Platforms are ignored for Dart, so no need to validate them.
110110
final invalidPlatforms = platforms.difference(_allowedFlutterPlatforms);
111111
if (invalidPlatforms.isNotEmpty) {
112-
final plural =
113-
invalidPlatforms.length > 1
114-
? 'are not valid platforms'
115-
: 'is not a valid platform';
112+
final plural = invalidPlatforms.length > 1
113+
? 'are not valid platforms'
114+
: 'is not a valid platform';
116115
errors.add(
117116
ValidationError(
118117
ValidationErrorType.itemInvalid,
@@ -153,14 +152,13 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
153152
return runCommandInRoot(
154153
request,
155154
arguments: commandArgs,
156-
commandForRoot:
157-
(_, _, sdk) =>
158-
switch (projectType) {
159-
'dart' => sdk.dartExecutablePath,
160-
'flutter' => sdk.flutterExecutablePath,
161-
_ => StateError('Unknown project type: $projectType'),
162-
}
163-
as String,
155+
commandForRoot: (_, _, sdk) =>
156+
switch (projectType) {
157+
'dart' => sdk.dartExecutablePath,
158+
'flutter' => sdk.flutterExecutablePath,
159+
_ => StateError('Unknown project type: $projectType'),
160+
}
161+
as String,
164162
commandDescription: '$projectType create',
165163
fileSystem: fileSystem,
166164
processManager: processManager,

pkgs/dart_mcp_server/lib/src/mixins/dtd.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,8 @@ base mixin DartToolingDaemonSupport
9696
continue;
9797
}
9898
if (debugSession.vmServiceUri case final vmServiceUri?) {
99-
final vmServiceFuture =
100-
activeVmServices[debugSession.id] = vmServiceConnectUri(
101-
vmServiceUri,
102-
);
99+
final vmServiceFuture = activeVmServices[debugSession.id] =
100+
vmServiceConnectUri(vmServiceUri);
103101
final vmService = await vmServiceFuture;
104102
// Start listening for and collecting errors immediately.
105103
final errorService = await _AppErrorsListener.forVmService(

pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,9 @@ base mixin PubDevSupport on ToolsSupport {
4343
try {
4444
result = jsonDecode(await _client.read(searchUrl));
4545

46-
final packageNames =
47-
dig<List>(result, ['packages'])
48-
.take(_resultsLimit)
49-
.map((p) => dig<String>(p, ['package']))
50-
.toList();
46+
final packageNames = dig<List>(result, [
47+
'packages',
48+
]).take(_resultsLimit).map((p) => dig<String>(p, ['package'])).toList();
5149

5250
if (packageNames.isEmpty) {
5351
return CallToolResult(
@@ -71,15 +69,17 @@ base mixin PubDevSupport on ToolsSupport {
7169
}
7270

7371
// Retrieve information about all the packages in parallel.
74-
final subQueryFutures =
75-
packageNames
76-
.map(
77-
(packageName) => (
78-
versionListing: retrieve('api/packages/$packageName'),
79-
score: retrieve('api/packages/$packageName/score'),
80-
),
81-
)
82-
.toList();
72+
final subQueryFutures = packageNames
73+
.map(
74+
(packageName) => (
75+
versionListing: retrieve('api/packages/$packageName'),
76+
score: retrieve('api/packages/$packageName/score'),
77+
docIndex: retrieve(
78+
'documentation/$packageName/latest/index.json',
79+
),
80+
),
81+
)
82+
.toList();
8383

8484
// Aggregate the retrieved information about each package into a
8585
// TextContent.
@@ -88,6 +88,17 @@ base mixin PubDevSupport on ToolsSupport {
8888
final packageName = packageNames[i];
8989
final versionListing = await subQueryFutures[i].versionListing;
9090
final scoreResult = await subQueryFutures[i].score;
91+
final libraryDocs = {
92+
for (var object
93+
in ((await subQueryFutures[i].docIndex) as List?)
94+
?.cast<Map<String, Object?>>() ??
95+
<Map<String, Object?>>[])
96+
if (!object.containsKey('enclosedBy'))
97+
object['name'] as String: Uri.https(
98+
'pub.dev',
99+
'documentation/$packageName/latest/${object['href']}',
100+
).toString(),
101+
};
91102
results.add(
92103
TextContent(
93104
text: jsonEncode({
@@ -97,12 +108,28 @@ base mixin PubDevSupport on ToolsSupport {
97108
'latest',
98109
'version',
99110
]),
100-
'description': dig<String>(versionListing, [
111+
'description': ?dig<String?>(versionListing, [
101112
'latest',
102113
'pubspec',
103114
'description',
104115
]),
116+
'homepage': ?dig<String?>(versionListing, [
117+
'latest',
118+
'pubspec',
119+
'homepage',
120+
]),
121+
'repository': ?dig<String?>(versionListing, [
122+
'latest',
123+
'pubspec',
124+
'repository',
125+
]),
126+
'documentation': ?dig<String?>(versionListing, [
127+
'latest',
128+
'pubspec',
129+
'documentation',
130+
]),
105131
},
132+
if (libraryDocs.isNotEmpty) ...{'libraries': libraryDocs},
106133
if (scoreResult != null) ...{
107134
'scores': {
108135
'pubPoints': dig<int>(scoreResult, ['grantedPoints']),
@@ -112,19 +139,15 @@ base mixin PubDevSupport on ToolsSupport {
112139
'downloadCount30Days',
113140
]),
114141
},
115-
'topics':
116-
dig<List>(
117-
scoreResult,
118-
['tags'],
119-
).where((t) => (t as String).startsWith('topic:')).toList(),
120-
'licenses':
121-
dig<List>(scoreResult, ['tags'])
122-
.where((t) => (t as String).startsWith('license'))
123-
.toList(),
124-
'publisher':
125-
dig<List>(scoreResult, ['tags'])
126-
.where((t) => (t as String).startsWith('publisher:'))
127-
.firstOrNull,
142+
'topics': dig<List>(scoreResult, [
143+
'tags',
144+
]).where((t) => (t as String).startsWith('topic:')).toList(),
145+
'licenses': dig<List>(scoreResult, [
146+
'tags',
147+
]).where((t) => (t as String).startsWith('license')).toList(),
148+
'publisher': dig<List>(scoreResult, ['tags'])
149+
.where((t) => (t as String).startsWith('publisher:'))
150+
.firstOrNull,
128151
},
129152
}),
130153
),

pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ base mixin RootsFallbackSupport on ToolsSupport, RootsTrackingSupport {
5050
// If the client supports roots, just use their stream (or lack thereof).
5151
// If they don't, use our own stream.
5252
_fallbackEnabled
53-
? _rootsListChangedFallbackController?.stream
54-
: super.rootsListChanged;
53+
? _rootsListChangedFallbackController?.stream
54+
: super.rootsListChanged;
5555

5656
StreamController<RootsListChangedNotification>?
5757
_rootsListChangedFallbackController;
@@ -76,8 +76,8 @@ base mixin RootsFallbackSupport on ToolsSupport, RootsTrackingSupport {
7676
@override
7777
Future<ListRootsResult> listRoots(ListRootsRequest request) async =>
7878
_fallbackEnabled
79-
? ListRootsResult(roots: _customRoots.toList())
80-
: super.listRoots(request);
79+
? ListRootsResult(roots: _customRoots.toList())
80+
: super.listRoots(request);
8181

8282
/// Adds the roots in [request] the custom roots and calls [updateRoots].
8383
///

pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,8 @@ Future<CallToolResult> runCommandInRoots(
8585
List<String> defaultPaths = const <String>[],
8686
required Sdk sdk,
8787
}) async {
88-
var rootConfigs =
89-
(request.arguments?[ParameterNames.roots] as List?)
90-
?.cast<Map<String, Object?>>();
88+
var rootConfigs = (request.arguments?[ParameterNames.roots] as List?)
89+
?.cast<Map<String, Object?>>();
9190

9291
// Default to use the known roots if none were specified.
9392
if (rootConfigs == null || rootConfigs.isEmpty) {
@@ -257,13 +256,12 @@ Future<String> defaultCommandForRoot(
257256
) async => switch (await inferProjectKind(rootUri, fileSystem)) {
258257
ProjectKind.dart => sdk.dartExecutablePath,
259258
ProjectKind.flutter => sdk.flutterExecutablePath,
260-
ProjectKind.unknown =>
261-
throw ArgumentError.value(
262-
rootUri,
263-
'rootUri',
264-
'Unknown project kind at root $rootUri. All projects must have a '
265-
'pubspec.',
266-
),
259+
ProjectKind.unknown => throw ArgumentError.value(
260+
rootUri,
261+
'rootUri',
262+
'Unknown project kind at root $rootUri. All projects must have a '
263+
'pubspec.',
264+
),
267265
};
268266

269267
/// Returns whether [uri] is under or exactly equal to [root].

pkgs/dart_mcp_server/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: >-
66
publish_to: none
77

88
environment:
9-
sdk: ^3.7.0
9+
sdk: ^3.8.0
1010

1111
executables:
1212
dart_mcp_server: main

pkgs/dart_mcp_server/test/test_harness.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,9 @@ final class AppDebugSession {
226226
final stdout = StreamQueue(process.stdoutStream());
227227
while (vmServiceUri == null && await stdout.hasNext) {
228228
final line = await stdout.next;
229-
final serviceString =
230-
isFlutter ? 'A Dart VM Service' : 'The Dart VM service';
229+
final serviceString = isFlutter
230+
? 'A Dart VM Service'
231+
: 'The Dart VM service';
231232
if (line.contains(serviceString)) {
232233
vmServiceUri = line
233234
.substring(line.indexOf('http:'))
@@ -376,8 +377,10 @@ Future<String> _getDTDUri(TestProcess dtdProcess) async {
376377
return dtdUri;
377378
}
378379

379-
typedef ServerConnectionPair =
380-
({ServerConnection serverConnection, DartMCPServer? server});
380+
typedef ServerConnectionPair = ({
381+
ServerConnection serverConnection,
382+
DartMCPServer? server,
383+
});
381384

382385
/// Starts up the [DartMCPServer] and connects [client] to it.
383386
///

0 commit comments

Comments
 (0)