Skip to content

add signature_help tool #110

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 3 commits into from
May 5, 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
1 change: 1 addition & 0 deletions pkgs/dart_tooling_mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
50 changes: 50 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -173,6 +174,7 @@ base mixin DartAnalyzerSupport
textDocument: lsp.TextDocumentClientCapabilities(
publishDiagnostics:
lsp.PublishDiagnosticsClientCapabilities(),
signatureHelp: lsp.SignatureHelpClientCapabilities(),
),
),
).toJson(),
Expand Down Expand Up @@ -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<CallToolResult> _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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
3 changes: 3 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/utils/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
32 changes: 32 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 @@ -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;

Expand Down Expand Up @@ -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<TextContent>().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),
Expand Down