Skip to content

implement a tool to resolve workspace symbols based on a query #79

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 2 commits into from
Apr 24, 2025
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
87 changes: 84 additions & 3 deletions pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
unsupportedReason ??= await _initializeAnalyzerLspServer();
if (unsupportedReason == null) {
registerTool(analyzeFilesTool, _analyzeFiles);
registerTool(resolveWorkspaceSymbolTool, _resolveWorkspaceSymbol);
}

// Don't call any methods on the client until we are fully initialized
Expand Down Expand Up @@ -129,6 +130,39 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
diagnostics: lsp.DiagnosticWorkspaceClientCapabilities(
refreshSupport: true,
),
symbol: lsp.WorkspaceSymbolClientCapabilities(
symbolKind:
lsp.WorkspaceSymbolClientCapabilitiesSymbolKind(
valueSet: [
lsp.SymbolKind.Array,
lsp.SymbolKind.Boolean,
lsp.SymbolKind.Class,
lsp.SymbolKind.Constant,
lsp.SymbolKind.Constructor,
lsp.SymbolKind.Enum,
lsp.SymbolKind.EnumMember,
lsp.SymbolKind.Event,
lsp.SymbolKind.Field,
lsp.SymbolKind.File,
lsp.SymbolKind.Function,
lsp.SymbolKind.Interface,
lsp.SymbolKind.Key,
lsp.SymbolKind.Method,
lsp.SymbolKind.Module,
lsp.SymbolKind.Namespace,
lsp.SymbolKind.Null,
lsp.SymbolKind.Number,
lsp.SymbolKind.Obj,
lsp.SymbolKind.Operator,
lsp.SymbolKind.Package,
lsp.SymbolKind.Property,
lsp.SymbolKind.Str,
lsp.SymbolKind.Struct,
lsp.SymbolKind.TypeParameter,
lsp.SymbolKind.Variable,
],
),
),
),
textDocument: lsp.TextDocumentClientCapabilities(
publishDiagnostics:
Expand All @@ -148,16 +182,31 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
}

if (initializeResult != null) {
// Checks that we can set workspaces on the LSP server.
final workspaceSupport =
initializeResult.capabilities.workspace?.workspaceFolders;
if (workspaceSupport?.supported != true) {
error ??= 'Workspaces are not supported by the LSP server';
}
if (workspaceSupport?.changeNotifications?.valueEquals(true) != true) {
} else if (workspaceSupport?.changeNotifications?.valueEquals(true) !=
true) {
error ??=
'Workspace change notifications are not supported by the LSP '
'server';
}

// Checks that we resolve workspace symbols.
final workspaceSymbolProvider =
initializeResult.capabilities.workspaceSymbolProvider;
final symbolProvidersSupported =
workspaceSymbolProvider != null &&
workspaceSymbolProvider.map(
(b) => b,
(options) => options.resolveProvider == true,
);
if (!symbolProvidersSupported) {
error ??=
'Workspace symbol resolution is not supported by the LSP server';
}
}

if (error != null) {
Expand Down Expand Up @@ -199,6 +248,20 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
return CallToolResult(content: messages);
}

/// Implementation of the [resolveWorkspaceSymbolTool], resolves a given
/// symbol or symbols in a workspace.
Future<CallToolResult> _resolveWorkspaceSymbol(
CallToolRequest request,
) async {
await _doneAnalyzing?.future;
final query = request.arguments!['query'] as String;
final result = await _lspConnection.sendRequest(
lsp.Method.workspace_symbol.toString(),
lsp.WorkspaceSymbolParams(query: query).toJson(),
);
return CallToolResult(content: [TextContent(text: jsonEncode(result))]);
}

/// Handles `$/analyzerStatus` events, which tell us when analysis starts and
/// stops.
void _handleAnalyzerStatus(Parameters params) {
Expand Down Expand Up @@ -268,7 +331,25 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
static final analyzeFilesTool = Tool(
name: 'analyze_files',
description: 'Analyzes the entire project for errors.',
inputSchema: ObjectSchema(),
inputSchema: Schema.object(),
);

@visibleForTesting
static final resolveWorkspaceSymbolTool = Tool(
name: 'resolve_workspace_symbol',
Copy link
Contributor

Choose a reason for hiding this comment

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

same question WRT the tool name. Should this be understandable by a human trying to decide whether to approve or deny the use of this tool?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the newest API version there is an additional field called "title" so I could add a user friendly title under there?

We should probably do this to all the tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tool annotations to give it a better name, PTAL

description: 'Look up a symbol or symbols in all workspaces by name.',
inputSchema: Schema.object(
properties: {
'query': Schema.string(
description:
'Queries are matched based on a case-insensitive partial name '
'match, and do not support complex pattern matching, regexes, '
'or scoped lookups.',
),
},
required: ['query'],
),
annotations: ToolAnnotations(title: 'Project search', readOnlyHint: true),
);
}

Expand Down
25 changes: 25 additions & 0 deletions pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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:io';

import 'package:dart_mcp/server.dart';
import 'package:dart_tooling_mcp_server/src/mixins/analyzer.dart';
import 'package:test/test.dart';
Expand Down Expand Up @@ -85,5 +87,28 @@ void main() {
isA<TextContent>().having((t) => t.text, 'text', 'No errors'),
);
});

test('can look up symbols in a workspace', () async {
final currentRoot = rootForPath(Directory.current.path);
testHarness.mcpClient.addRoot(currentRoot);
await pumpEventQueue();

final result = await testHarness.callToolWithRetry(
CallToolRequest(
name: DartAnalyzerSupport.resolveWorkspaceSymbolTool.name,
arguments: {'query': 'DartAnalyzerSupport'},
),
);
expect(result.isError, isNot(true));

expect(
result.content.single,
isA<TextContent>().having(
(t) => t.text,
'text',
contains('analyzer.dart'),
),
);
});
});
}
Loading