diff --git a/pkgs/dart_tooling_mcp_server/README.md b/pkgs/dart_tooling_mcp_server/README.md index 83e1854..ad969f7 100644 --- a/pkgs/dart_tooling_mcp_server/README.md +++ b/pkgs/dart_tooling_mcp_server/README.md @@ -9,6 +9,7 @@ WIP. This package is still experimental and is likely to evolve quickly. | Tool Name | Feature Group | Description | | --- | --- | --- | | `analyze_files` | `static analysis` | Analyzes the entire project for errors. | +| `signature_help` | `static_analysis` | Gets signature information for usage at a given cursor position. | | `resolve_workspace_symbol` | `static analysis` | Look up a symbol or symbols in all workspaces by name. | | `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. | diff --git a/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart b/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart index 72bc444..384ef3e 100644 --- a/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart +++ b/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart @@ -67,6 +67,7 @@ base mixin DartAnalyzerSupport if (unsupportedReasons.isEmpty) { registerTool(analyzeFilesTool, _analyzeFiles); registerTool(resolveWorkspaceSymbolTool, _resolveWorkspaceSymbol); + registerTool(signatureHelpTool, _signatureHelp); } // Don't call any methods on the client until we are fully initialized @@ -173,6 +174,7 @@ base mixin DartAnalyzerSupport textDocument: lsp.TextDocumentClientCapabilities( publishDiagnostics: lsp.PublishDiagnosticsClientCapabilities(), + signatureHelp: lsp.SignatureHelpClientCapabilities(), ), ), ).toJson(), @@ -272,6 +274,27 @@ base mixin DartAnalyzerSupport return CallToolResult(content: [TextContent(text: jsonEncode(result))]); } + /// Implementation of the [signatureHelpTool], get signature help for a given + /// position in a file. + Future _signatureHelp(CallToolRequest request) async { + final errorResult = await _ensurePrerequisites(request); + if (errorResult != null) return errorResult; + + final uri = Uri.parse(request.arguments![ParameterNames.uri] as String); + final position = lsp.Position( + line: request.arguments![ParameterNames.line] as int, + character: request.arguments![ParameterNames.column] as int, + ); + final result = await _lspConnection.sendRequest( + lsp.Method.textDocument_signatureHelp.toString(), + lsp.SignatureHelpParams( + textDocument: lsp.TextDocumentIdentifier(uri: uri), + position: position, + ).toJson(), + ); + return CallToolResult(content: [TextContent(text: jsonEncode(result))]); + } + /// Ensures that all prerequisites for any analysis task are met. /// /// Returns an error response if any prerequisite is not met, otherwise @@ -382,6 +405,33 @@ base mixin DartAnalyzerSupport annotations: ToolAnnotations(title: 'Project search', readOnlyHint: true), ); + @visibleForTesting + static final signatureHelpTool = Tool( + name: 'signature_help', + description: + 'Get signature help for an API being used at a given cursor ' + 'position in a file.', + inputSchema: Schema.object( + properties: { + ParameterNames.uri: Schema.string( + description: 'The URI of the file to get signature help for.', + ), + ParameterNames.line: Schema.int( + description: 'The line number of the cursor position.', + ), + ParameterNames.column: Schema.int( + description: 'The column number of the cursor position.', + ), + }, + required: [ + ParameterNames.uri, + ParameterNames.line, + ParameterNames.column, + ], + ), + annotations: ToolAnnotations(title: 'Signature help', readOnlyHint: true), + ); + @visibleForTesting static final noRootsSetResponse = CallToolResult( isError: true, diff --git a/pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart b/pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart index 9d426a6..d19491e 100644 --- a/pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart +++ b/pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart @@ -171,6 +171,7 @@ ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list( title: 'Paths to run this tool on. Must resolve to a path that is ' 'within the "root".', + items: Schema.string(), ), }, required: [ParameterNames.root], diff --git a/pkgs/dart_tooling_mcp_server/lib/src/utils/constants.dart b/pkgs/dart_tooling_mcp_server/lib/src/utils/constants.dart index 0b2062f..38a4fbb 100644 --- a/pkgs/dart_tooling_mcp_server/lib/src/utils/constants.dart +++ b/pkgs/dart_tooling_mcp_server/lib/src/utils/constants.dart @@ -4,9 +4,12 @@ /// A namespace for all the parameter names. extension ParameterNames on Never { + static const column = 'column'; static const command = 'command'; + static const line = 'line'; static const packageName = 'packageName'; static const paths = 'paths'; + static const position = 'position'; static const query = 'query'; static const root = 'root'; static const roots = 'roots'; diff --git a/pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart b/pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart index 08fef38..cf7a46e 100644 --- a/pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart +++ b/pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:dart_mcp/server.dart'; import 'package:dart_tooling_mcp_server/src/mixins/analyzer.dart'; import 'package:dart_tooling_mcp_server/src/utils/constants.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -112,6 +113,37 @@ void main() { ); }); + test('can get signature help', () async { + final counterAppRoot = rootForPath(counterAppPath); + testHarness.mcpClient.addRoot(counterAppRoot); + await pumpEventQueue(); + + final result = await testHarness.callToolWithRetry( + CallToolRequest( + name: DartAnalyzerSupport.signatureHelpTool.name, + arguments: { + ParameterNames.uri: p.join(counterAppRoot.uri, 'lib', 'main.dart'), + ParameterNames.line: 16, + ParameterNames.column: 15, + }, + ), + ); + expect(result.isError, isNot(true)); + + expect( + result.content.single, + isA().having( + (t) => t.text, + 'text', + allOf( + contains('Creates a MaterialApp'), // From the doc comment + contains('MaterialApp({Key? key,'), // The actual signature + contains('"label":"Key? key'), // Specific label for the key param + ), + ), + ); + }); + test('cannot analyze without roots set', () async { final result = await testHarness.callToolWithRetry( CallToolRequest(name: DartAnalyzerSupport.analyzeFilesTool.name),