Skip to content
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.7.0

- Add support for Completions capability per MCP 2025-06-18 spec
- Add ServerCapabilitiesCompletions class for explicit completions capability declaration
- Update ServerCapabilities to include completions field
- Update client capability check to use explicit completions capability instead of inferring from prompts/resources
- Add integration tests and example for completions capability usage

## 0.6.4

- Fix issue with StreamableHTTP server not setting correct content-type for SSE
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Ensure you have the correct Dart SDK version installed. See <https://dart.dev/ge
- Prompts
- Sampling
- Roots
- Completions

## Model Context Protocol Version

Expand Down
162 changes: 162 additions & 0 deletions example/completions_capability_demo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Demonstrates proper completions capability usage per MCP 2025-06-18 spec
//
// This example shows:
// 1. Server declaring completions capability explicitly
// 2. Resource template with argument completion
// 3. Prompt with argument completion
// 4. How completion callbacks work
library;

import 'package:mcp_dart/mcp_dart.dart';

void main() async {
// Server declares completions support explicitly per 2025-06-18 spec
final server = McpServer(
Implementation(name: "completions-demo", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
completions: ServerCapabilitiesCompletions(),
resources: ServerCapabilitiesResources(),
prompts: ServerCapabilitiesPrompts(),
),
),
);

// Add a resource template with argument completion
// Clients can request completion for the {path} argument
server.resourceTemplate(
"file-reader",
ResourceTemplateRegistration(
"file:///{path}",
listCallback: null,
completeCallbacks: {
'path': (value) async {
// Simulate file path completion
// In a real scenario, this would scan the file system
final suggestions = [
'README.md',
'CHANGELOG.md',
'LICENSE',
'pubspec.yaml',
'lib/mcp_dart.dart',
'lib/src/types.dart',
];

// Filter suggestions based on current value
final filtered = suggestions
.where((s) => s.toLowerCase().contains(value.toLowerCase()))
.toList();

return filtered.isEmpty ? suggestions : filtered;
},
},
),
(uri, variables, extra) async {
final path = variables['path'] ?? 'unknown';
return ReadResourceResult(
contents: [
TextResourceContents(
uri: uri.toString(),
mimeType: 'text/plain',
text:
'Content of file: $path\n\n(This is a demo - actual file reading not implemented)',
),
],
);
},
metadata: (
description: "Read files with auto-completion support",
mimeType: "text/plain"
),
);

// Add a prompt with argument completion
server.prompt(
'code-review',
description: 'Generate code review for a specific file type',
argsSchema: {
'language': PromptArgumentDefinition(
description: 'Programming language to review',
required: true,
type: String,
completable: CompletableField(
def: CompletableDef(
complete: (value) async {
// Provide language completions
final languages = [
'dart',
'javascript',
'typescript',
'python',
'java',
'go',
'rust',
'c',
'cpp',
];

// Filter based on current input
final filtered = languages
.where((lang) => lang.startsWith(value.toLowerCase()))
.toList();

return filtered.isEmpty ? languages : filtered;
},
),
),
),
'style': PromptArgumentDefinition(
description: 'Review style',
required: false,
type: String,
completable: CompletableField(
def: CompletableDef(
complete: (value) async {
// Provide style completions
return ['concise', 'detailed', 'security-focused', 'performance-focused'];
},
),
),
),
},
callback: (args, extra) async {
final language = args?['language'] ?? 'unknown';
final style = args?['style'] ?? 'detailed';

return GetPromptResult(
messages: [
PromptMessage(
role: PromptMessageRole.user,
content: TextContent(
text: 'Please provide a $style code review for $language code:\n\n'
'(This is where the code would be inserted)',
),
),
],
);
},
);

// Add a simple tool (no completion - just for demonstration)
server.tool(
'echo',
description: 'Echo back the input message',
toolInputSchema: ToolInputSchema(
properties: {
'message': {'type': 'string', 'description': 'Message to echo'},
},
required: ['message'],
),
callback: ({args, extra}) async {
final message = args?['message'] ?? '';
return CallToolResult.fromContent(
content: [
TextContent(text: 'Echo: $message'),
],
);
},
);

// Connect to stdio transport
await server.connect(StdioServerTransport());
}
4 changes: 2 additions & 2 deletions lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ class Client extends Protocol {
requiredCapability = 'tools';
break;
case "completion/complete":
supported = serverCaps.prompts != null || serverCaps.resources != null;
requiredCapability = 'prompts or resources';
supported = serverCaps.completions != null;
requiredCapability = 'completions';
break;
default:
_logger.warn(
Expand Down
28 changes: 28 additions & 0 deletions lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,26 @@ class ServerCapabilitiesTools {
};
}

/// Describes capabilities related to completions.
class ServerCapabilitiesCompletions {
/// Whether the server supports `notifications/completions/list_changed`.
final bool? listChanged;

const ServerCapabilitiesCompletions({
this.listChanged,
});

factory ServerCapabilitiesCompletions.fromJson(Map<String, dynamic> json) {
return ServerCapabilitiesCompletions(
listChanged: json['listChanged'] as bool?,
);
}

Map<String, dynamic> toJson() => {
if (listChanged != null) 'listChanged': listChanged,
};
}

/// Capabilities a server may support.
class ServerCapabilities {
/// Experimental, non-standard capabilities.
Expand All @@ -548,25 +568,32 @@ class ServerCapabilities {
/// Present if the server offers tools (`tools/list`, `tools/call`).
final ServerCapabilitiesTools? tools;

/// Present if the server offers completions (`completion/complete`).
final ServerCapabilitiesCompletions? completions;

const ServerCapabilities({
this.experimental,
this.logging,
this.prompts,
this.resources,
this.tools,
this.completions,
});

factory ServerCapabilities.fromJson(Map<String, dynamic> json) {
final pMap = json['prompts'] as Map<String, dynamic>?;
final rMap = json['resources'] as Map<String, dynamic>?;
final tMap = json['tools'] as Map<String, dynamic>?;
final cMap = json['completions'] as Map<String, dynamic>?;
return ServerCapabilities(
experimental: json['experimental'] as Map<String, dynamic>?,
logging: json['logging'] as Map<String, dynamic>?,
prompts: pMap == null ? null : ServerCapabilitiesPrompts.fromJson(pMap),
resources:
rMap == null ? null : ServerCapabilitiesResources.fromJson(rMap),
tools: tMap == null ? null : ServerCapabilitiesTools.fromJson(tMap),
completions:
cMap == null ? null : ServerCapabilitiesCompletions.fromJson(cMap),
);
}

Expand All @@ -576,6 +603,7 @@ class ServerCapabilities {
if (prompts != null) 'prompts': prompts!.toJson(),
if (resources != null) 'resources': resources!.toJson(),
if (tools != null) 'tools': tools!.toJson(),
if (completions != null) 'completions': completions!.toJson(),
};
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: mcp_dart
description: Dart Implementation of Model Context Protocol (MCP) SDK.
version: 0.6.4
version: 0.7.0
repository: https://github.com/leehack/mcp_dart

environment:
Expand Down
105 changes: 105 additions & 0 deletions test/integration/completions_capability_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'package:mcp_dart/mcp_dart.dart';
import 'package:test/test.dart';

void main() {
group('Completions Capability Integration Tests', () {
test('Server declares completions capability', () {
final mcpServer = McpServer(
Implementation(name: "test-server", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
completions: ServerCapabilitiesCompletions(),
),
),
);

final caps = mcpServer.server.getCapabilities();
expect(caps.completions, isNotNull);
expect(caps.completions, isA<ServerCapabilitiesCompletions>());
});

test('Server declares completions capability with listChanged', () {
final mcpServer = McpServer(
Implementation(name: "test-server", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
completions: ServerCapabilitiesCompletions(listChanged: true),
),
),
);

final caps = mcpServer.server.getCapabilities();
expect(caps.completions, isNotNull);
expect(caps.completions?.listChanged, equals(true));
});

test('Server without completions capability returns null', () {
final mcpServer = McpServer(
Implementation(name: "test-server", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
tools: ServerCapabilitiesTools(),
),
),
);

final caps = mcpServer.server.getCapabilities();
expect(caps.completions, isNull);
});

test('Server with completions capability can be verified', () {
// Create a server with completions capability
final mcpServer = McpServer(
Implementation(name: "test-server", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
completions: ServerCapabilitiesCompletions(),
),
),
);

// Verify server capabilities include completions
// This would normally happen during the connect/initialize handshake
final caps = mcpServer.server.getCapabilities();
expect(caps.completions, isNotNull);
});

test('Server with multiple capabilities including completions', () {
final mcpServer = McpServer(
Implementation(name: "test-server", version: "1.0.0"),
options: ServerOptions(
capabilities: ServerCapabilities(
completions: ServerCapabilitiesCompletions(listChanged: true),
tools: ServerCapabilitiesTools(listChanged: true),
resources: ServerCapabilitiesResources(
subscribe: true,
listChanged: true,
),
prompts: ServerCapabilitiesPrompts(listChanged: true),
),
),
);

final caps = mcpServer.server.getCapabilities();
expect(caps.completions, isNotNull);
expect(caps.completions?.listChanged, equals(true));
expect(caps.tools, isNotNull);
expect(caps.resources, isNotNull);
expect(caps.prompts, isNotNull);
});

test('Completions capability survives serialization round-trip', () {
final originalCaps = ServerCapabilities(
completions: ServerCapabilitiesCompletions(listChanged: true),
experimental: {'test': true},
);

final json = originalCaps.toJson();
final deserializedCaps = ServerCapabilities.fromJson(json);

expect(deserializedCaps.completions, isNotNull);
expect(deserializedCaps.completions?.listChanged, equals(true));
expect(deserializedCaps.experimental?['test'], equals(true));
});
});
}
Loading