Skip to content

Commit a5089bf

Browse files
authored
implement a tool to resolve workspace symbols based on a query (#79)
This allows you to ask things like "Tell me about in my project" and it will tell you where it is located. Follow on features will allow for looking up more detailed information about the symbol such as its doc comments and API.
1 parent f127e5e commit a5089bf

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

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

+84-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
5757
unsupportedReason ??= await _initializeAnalyzerLspServer();
5858
if (unsupportedReason == null) {
5959
registerTool(analyzeFilesTool, _analyzeFiles);
60+
registerTool(resolveWorkspaceSymbolTool, _resolveWorkspaceSymbol);
6061
}
6162

6263
// Don't call any methods on the client until we are fully initialized
@@ -129,6 +130,39 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
129130
diagnostics: lsp.DiagnosticWorkspaceClientCapabilities(
130131
refreshSupport: true,
131132
),
133+
symbol: lsp.WorkspaceSymbolClientCapabilities(
134+
symbolKind:
135+
lsp.WorkspaceSymbolClientCapabilitiesSymbolKind(
136+
valueSet: [
137+
lsp.SymbolKind.Array,
138+
lsp.SymbolKind.Boolean,
139+
lsp.SymbolKind.Class,
140+
lsp.SymbolKind.Constant,
141+
lsp.SymbolKind.Constructor,
142+
lsp.SymbolKind.Enum,
143+
lsp.SymbolKind.EnumMember,
144+
lsp.SymbolKind.Event,
145+
lsp.SymbolKind.Field,
146+
lsp.SymbolKind.File,
147+
lsp.SymbolKind.Function,
148+
lsp.SymbolKind.Interface,
149+
lsp.SymbolKind.Key,
150+
lsp.SymbolKind.Method,
151+
lsp.SymbolKind.Module,
152+
lsp.SymbolKind.Namespace,
153+
lsp.SymbolKind.Null,
154+
lsp.SymbolKind.Number,
155+
lsp.SymbolKind.Obj,
156+
lsp.SymbolKind.Operator,
157+
lsp.SymbolKind.Package,
158+
lsp.SymbolKind.Property,
159+
lsp.SymbolKind.Str,
160+
lsp.SymbolKind.Struct,
161+
lsp.SymbolKind.TypeParameter,
162+
lsp.SymbolKind.Variable,
163+
],
164+
),
165+
),
132166
),
133167
textDocument: lsp.TextDocumentClientCapabilities(
134168
publishDiagnostics:
@@ -148,16 +182,31 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
148182
}
149183

150184
if (initializeResult != null) {
185+
// Checks that we can set workspaces on the LSP server.
151186
final workspaceSupport =
152187
initializeResult.capabilities.workspace?.workspaceFolders;
153188
if (workspaceSupport?.supported != true) {
154189
error ??= 'Workspaces are not supported by the LSP server';
155-
}
156-
if (workspaceSupport?.changeNotifications?.valueEquals(true) != true) {
190+
} else if (workspaceSupport?.changeNotifications?.valueEquals(true) !=
191+
true) {
157192
error ??=
158193
'Workspace change notifications are not supported by the LSP '
159194
'server';
160195
}
196+
197+
// Checks that we resolve workspace symbols.
198+
final workspaceSymbolProvider =
199+
initializeResult.capabilities.workspaceSymbolProvider;
200+
final symbolProvidersSupported =
201+
workspaceSymbolProvider != null &&
202+
workspaceSymbolProvider.map(
203+
(b) => b,
204+
(options) => options.resolveProvider == true,
205+
);
206+
if (!symbolProvidersSupported) {
207+
error ??=
208+
'Workspace symbol resolution is not supported by the LSP server';
209+
}
161210
}
162211

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

251+
/// Implementation of the [resolveWorkspaceSymbolTool], resolves a given
252+
/// symbol or symbols in a workspace.
253+
Future<CallToolResult> _resolveWorkspaceSymbol(
254+
CallToolRequest request,
255+
) async {
256+
await _doneAnalyzing?.future;
257+
final query = request.arguments!['query'] as String;
258+
final result = await _lspConnection.sendRequest(
259+
lsp.Method.workspace_symbol.toString(),
260+
lsp.WorkspaceSymbolParams(query: query).toJson(),
261+
);
262+
return CallToolResult(content: [TextContent(text: jsonEncode(result))]);
263+
}
264+
202265
/// Handles `$/analyzerStatus` events, which tell us when analysis starts and
203266
/// stops.
204267
void _handleAnalyzerStatus(Parameters params) {
@@ -268,7 +331,25 @@ base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
268331
static final analyzeFilesTool = Tool(
269332
name: 'analyze_files',
270333
description: 'Analyzes the entire project for errors.',
271-
inputSchema: ObjectSchema(),
334+
inputSchema: Schema.object(),
335+
);
336+
337+
@visibleForTesting
338+
static final resolveWorkspaceSymbolTool = Tool(
339+
name: 'resolve_workspace_symbol',
340+
description: 'Look up a symbol or symbols in all workspaces by name.',
341+
inputSchema: Schema.object(
342+
properties: {
343+
'query': Schema.string(
344+
description:
345+
'Queries are matched based on a case-insensitive partial name '
346+
'match, and do not support complex pattern matching, regexes, '
347+
'or scoped lookups.',
348+
),
349+
},
350+
required: ['query'],
351+
),
352+
annotations: ToolAnnotations(title: 'Project search', readOnlyHint: true),
272353
);
273354
}
274355

pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart

+25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:io';
6+
57
import 'package:dart_mcp/server.dart';
68
import 'package:dart_tooling_mcp_server/src/mixins/analyzer.dart';
79
import 'package:test/test.dart';
@@ -85,5 +87,28 @@ void main() {
8587
isA<TextContent>().having((t) => t.text, 'text', 'No errors'),
8688
);
8789
});
90+
91+
test('can look up symbols in a workspace', () async {
92+
final currentRoot = rootForPath(Directory.current.path);
93+
testHarness.mcpClient.addRoot(currentRoot);
94+
await pumpEventQueue();
95+
96+
final result = await testHarness.callToolWithRetry(
97+
CallToolRequest(
98+
name: DartAnalyzerSupport.resolveWorkspaceSymbolTool.name,
99+
arguments: {'query': 'DartAnalyzerSupport'},
100+
),
101+
);
102+
expect(result.isError, isNot(true));
103+
104+
expect(
105+
result.content.single,
106+
isA<TextContent>().having(
107+
(t) => t.text,
108+
'text',
109+
contains('analyzer.dart'),
110+
),
111+
);
112+
});
88113
});
89114
}

0 commit comments

Comments
 (0)