Implement MCP tool registry and kanban tool provider
- Add McpTool + McpToolProvider interface to core - Add McpToolRegistry to mcp package (aggregates providers) - Add DewMcpServer (MCPServer + ToolsSupport via dart_mcp 0.5.0) - Add 'mcp serve' subcommand — starts a real stdio MCP server - Implement KanbanToolProvider with 5 tools: kanban_create_ticket, kanban_list_tickets, kanban_get_ticket, kanban_update_ticket, kanban_delete_ticket - Wire McpToolRegistry + KanbanToolProvider in CLI - 26 tests passing, dart analyze clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
a1c36f7136
commit
a74bd94547
14 changed files with 490 additions and 18 deletions
|
|
@ -4,16 +4,17 @@ import 'package:dew_kanban/dew_kanban.dart' as kanban;
|
||||||
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
final registry = CommandRegistry();
|
final commandRegistry = CommandRegistry();
|
||||||
kanban.registerCommands(registry);
|
final toolRegistry = mcp.McpToolRegistry();
|
||||||
mcp.registerCommands(registry);
|
|
||||||
|
|
||||||
final runner = CommandRunner<void>(
|
toolRegistry.register(kanban.KanbanToolProvider());
|
||||||
'dew',
|
|
||||||
'A project management tool.',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final command in registry.commands) {
|
kanban.registerCommands(commandRegistry);
|
||||||
|
mcp.registerCommands(commandRegistry, toolRegistry);
|
||||||
|
|
||||||
|
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
||||||
|
|
||||||
|
for (final command in commandRegistry.commands) {
|
||||||
runner.addCommand(command);
|
runner.addCommand(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ library;
|
||||||
|
|
||||||
export 'src/config.dart';
|
export 'src/config.dart';
|
||||||
export 'src/dew_core_base.dart';
|
export 'src/dew_core_base.dart';
|
||||||
|
export 'src/mcp_tool_provider.dart';
|
||||||
|
|
|
||||||
27
packages/core/lib/src/mcp_tool_provider.dart
Normal file
27
packages/core/lib/src/mcp_tool_provider.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
typedef McpToolHandler = Future<String> Function(Map<String, dynamic> args);
|
||||||
|
|
||||||
|
/// A single tool exposed to an MCP client.
|
||||||
|
class McpTool {
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
/// Raw JSON Schema object (type: object) describing the tool's parameters.
|
||||||
|
final Map<String, dynamic> inputSchema;
|
||||||
|
|
||||||
|
final McpToolHandler handler;
|
||||||
|
|
||||||
|
const McpTool({
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.inputSchema,
|
||||||
|
required this.handler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement this interface to expose tools to the MCP server.
|
||||||
|
///
|
||||||
|
/// Feature packages implement this in their own library without needing to
|
||||||
|
/// depend on [packages/mcp] — they only depend on [packages/core].
|
||||||
|
abstract interface class McpToolProvider {
|
||||||
|
List<McpTool> get tools;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/dew_kanban_base.dart';
|
export 'src/dew_kanban_base.dart';
|
||||||
|
export 'src/kanban_tool_provider.dart';
|
||||||
export 'src/ticket.dart';
|
export 'src/ticket.dart';
|
||||||
export 'src/ticket_store.dart';
|
export 'src/ticket_store.dart';
|
||||||
|
|
||||||
|
|
|
||||||
204
packages/kanban/lib/src/kanban_tool_provider.dart
Normal file
204
packages/kanban/lib/src/kanban_tool_provider.dart
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'ticket.dart';
|
||||||
|
import 'ticket_store.dart';
|
||||||
|
|
||||||
|
/// Exposes Kanban board operations as MCP tools.
|
||||||
|
class KanbanToolProvider implements McpToolProvider {
|
||||||
|
@override
|
||||||
|
List<McpTool> get tools => [
|
||||||
|
McpTool(
|
||||||
|
name: 'kanban_create_ticket',
|
||||||
|
description: 'Create a new kanban ticket.',
|
||||||
|
inputSchema: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'title': {'type': 'string', 'description': 'Ticket title.'},
|
||||||
|
'type': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Ticket type (e.g. task, bug).',
|
||||||
|
},
|
||||||
|
'column': {
|
||||||
|
'type': 'string',
|
||||||
|
'description':
|
||||||
|
'Initial column. Defaults to the first configured column.',
|
||||||
|
},
|
||||||
|
'body': {'type': 'string', 'description': 'Ticket description.'},
|
||||||
|
},
|
||||||
|
'required': ['title', 'type'],
|
||||||
|
},
|
||||||
|
handler: _createTicket,
|
||||||
|
),
|
||||||
|
McpTool(
|
||||||
|
name: 'kanban_list_tickets',
|
||||||
|
description: 'List kanban tickets, optionally filtered by column or type.',
|
||||||
|
inputSchema: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'column': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Filter to tickets in this column.',
|
||||||
|
},
|
||||||
|
'type': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Filter to tickets of this type.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: _listTickets,
|
||||||
|
),
|
||||||
|
McpTool(
|
||||||
|
name: 'kanban_get_ticket',
|
||||||
|
description: 'Get a kanban ticket by ID.',
|
||||||
|
inputSchema: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Ticket ID (e.g. DEW-0001).',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['id'],
|
||||||
|
},
|
||||||
|
handler: _getTicket,
|
||||||
|
),
|
||||||
|
McpTool(
|
||||||
|
name: 'kanban_update_ticket',
|
||||||
|
description: 'Update one or more fields on an existing kanban ticket.',
|
||||||
|
inputSchema: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'id': {'type': 'string', 'description': 'Ticket ID.'},
|
||||||
|
'title': {'type': 'string', 'description': 'New title.'},
|
||||||
|
'type': {'type': 'string', 'description': 'New ticket type.'},
|
||||||
|
'column': {'type': 'string', 'description': 'New column.'},
|
||||||
|
'body': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'New body (replaces existing body).',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['id'],
|
||||||
|
},
|
||||||
|
handler: _updateTicket,
|
||||||
|
),
|
||||||
|
McpTool(
|
||||||
|
name: 'kanban_delete_ticket',
|
||||||
|
description: 'Delete a kanban ticket by ID.',
|
||||||
|
inputSchema: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'id': {'type': 'string', 'description': 'Ticket ID.'},
|
||||||
|
},
|
||||||
|
'required': ['id'],
|
||||||
|
},
|
||||||
|
handler: _deleteTicket,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<TicketStore> _store() async {
|
||||||
|
final context = await ProjectContext.find();
|
||||||
|
return TicketStore(
|
||||||
|
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||||
|
prefix: context.config.kanban.prefix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _createTicket(Map<String, dynamic> args) async {
|
||||||
|
final context = await ProjectContext.find();
|
||||||
|
final config = context.config.kanban;
|
||||||
|
final title = args['title'] as String;
|
||||||
|
final typeId = args['type'] as String;
|
||||||
|
final column = args['column'] as String? ?? config.columns.first.id;
|
||||||
|
final body = args['body'] as String? ?? '';
|
||||||
|
|
||||||
|
if (!config.ticketTypes.any((t) => t.id == typeId)) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Unknown type "$typeId". Valid: ${config.ticketTypes.map((t) => t.id).join(', ')}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!config.columns.any((c) => c.id == column)) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Unknown column "$column". Valid: ${config.columns.map((c) => c.id).join(', ')}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final store = TicketStore(
|
||||||
|
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||||
|
prefix: config.prefix,
|
||||||
|
);
|
||||||
|
final ticket = await store.create(
|
||||||
|
title: title,
|
||||||
|
type: typeId,
|
||||||
|
column: column,
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
return 'Created ${ticket.id}: ${ticket.title}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _listTickets(Map<String, dynamic> args) async {
|
||||||
|
final columnFilter = args['column'] as String?;
|
||||||
|
final typeFilter = args['type'] as String?;
|
||||||
|
final store = await _store();
|
||||||
|
var tickets = await store.list();
|
||||||
|
|
||||||
|
if (columnFilter != null) {
|
||||||
|
tickets = tickets.where((t) => t.column == columnFilter).toList();
|
||||||
|
}
|
||||||
|
if (typeFilter != null) {
|
||||||
|
tickets = tickets.where((t) => t.type == typeFilter).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tickets.isEmpty) return 'No tickets found.';
|
||||||
|
|
||||||
|
return tickets
|
||||||
|
.map((t) => '[${t.id}] (${t.type}) [${t.column}] ${t.title}')
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getTicket(Map<String, dynamic> args) async {
|
||||||
|
final id = (args['id'] as String).toUpperCase();
|
||||||
|
final store = await _store();
|
||||||
|
final ticket = await store.findById(id);
|
||||||
|
if (ticket == null) throw ArgumentError('Ticket $id not found.');
|
||||||
|
return _formatTicket(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _updateTicket(Map<String, dynamic> args) async {
|
||||||
|
final id = (args['id'] as String).toUpperCase();
|
||||||
|
final store = await _store();
|
||||||
|
final updated = await store.update(
|
||||||
|
id,
|
||||||
|
title: args['title'] as String?,
|
||||||
|
type: args['type'] as String?,
|
||||||
|
column: args['column'] as String?,
|
||||||
|
body: args['body'] as String?,
|
||||||
|
);
|
||||||
|
return 'Updated ${updated.id}.\n${_formatTicket(updated)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _deleteTicket(Map<String, dynamic> args) async {
|
||||||
|
final id = (args['id'] as String).toUpperCase();
|
||||||
|
final store = await _store();
|
||||||
|
await store.delete(id);
|
||||||
|
return 'Deleted $id.';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTicket(Ticket t) {
|
||||||
|
final buf = StringBuffer();
|
||||||
|
buf.writeln('[${t.id}] (${t.type}) [${t.column}] ${t.title}');
|
||||||
|
buf.writeln(
|
||||||
|
'Created: ${t.created.toLocal().toString().split('.').first}',
|
||||||
|
);
|
||||||
|
if (t.body.isNotEmpty) {
|
||||||
|
buf.writeln();
|
||||||
|
buf.writeln(t.body);
|
||||||
|
}
|
||||||
|
for (final (i, comment) in t.comments.indexed) {
|
||||||
|
buf.writeln();
|
||||||
|
buf.writeln('── Comment ${i + 1} ${'─' * 20}');
|
||||||
|
buf.write(comment);
|
||||||
|
}
|
||||||
|
return buf.toString().trimRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -139,5 +139,73 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('KanbanToolProvider', () {
|
||||||
|
test('exposes five tools with unique names', () {
|
||||||
|
final provider = KanbanToolProvider();
|
||||||
|
expect(provider.tools, hasLength(5));
|
||||||
|
final names = provider.tools.map((t) => t.name).toSet();
|
||||||
|
expect(names, {
|
||||||
|
'kanban_create_ticket',
|
||||||
|
'kanban_list_tickets',
|
||||||
|
'kanban_get_ticket',
|
||||||
|
'kanban_update_ticket',
|
||||||
|
'kanban_delete_ticket',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all tools have non-empty descriptions and object inputSchema', () {
|
||||||
|
for (final tool in KanbanToolProvider().tools) {
|
||||||
|
expect(tool.description, isNotEmpty, reason: '${tool.name} description');
|
||||||
|
expect(tool.inputSchema['type'], 'object',
|
||||||
|
reason: '${tool.name} schema type');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create and list tools have handlers that work', () async {
|
||||||
|
// Use a real temp dir with a fake dew.yaml so ProjectContext.find()
|
||||||
|
// resolves — handlers call ProjectContext.find() internally.
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp('kanban_tool_test_');
|
||||||
|
final origDir = Directory.current;
|
||||||
|
try {
|
||||||
|
await Directory(p.join(tempDir.path, '.project', 'kanban')).create(recursive: true);
|
||||||
|
await File(p.join(tempDir.path, '.project', 'dew.yaml')).writeAsString('''
|
||||||
|
dew:
|
||||||
|
mcp:
|
||||||
|
host: localhost
|
||||||
|
port: 9090
|
||||||
|
kanban:
|
||||||
|
prefix: T
|
||||||
|
ticket_types:
|
||||||
|
- id: task
|
||||||
|
name: Task
|
||||||
|
columns:
|
||||||
|
- id: todo
|
||||||
|
name: To Do
|
||||||
|
color: blue
|
||||||
|
''');
|
||||||
|
Directory.current = tempDir;
|
||||||
|
final provider = KanbanToolProvider();
|
||||||
|
|
||||||
|
final createTool = provider.tools.firstWhere(
|
||||||
|
(t) => t.name == 'kanban_create_ticket',
|
||||||
|
);
|
||||||
|
final result = await createTool.handler({
|
||||||
|
'title': 'Hello',
|
||||||
|
'type': 'task',
|
||||||
|
});
|
||||||
|
expect(result, contains('T-0001'));
|
||||||
|
|
||||||
|
final listTool = provider.tools.firstWhere(
|
||||||
|
(t) => t.name == 'kanban_list_tickets',
|
||||||
|
);
|
||||||
|
final listResult = await listTool.handler({});
|
||||||
|
expect(listResult, contains('T-0001'));
|
||||||
|
} finally {
|
||||||
|
Directory.current = origDir;
|
||||||
|
await tempDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/dew_mcp_base.dart';
|
export 'src/dew_mcp_base.dart';
|
||||||
|
export 'src/mcp_tool_registry.dart';
|
||||||
|
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:dew_mcp/src/dew_mcp_base.dart';
|
import 'package:dew_mcp/src/dew_mcp_base.dart';
|
||||||
|
import 'package:dew_mcp/src/mcp_tool_registry.dart';
|
||||||
|
|
||||||
/// Registers all MCP commands into [registry].
|
/// Registers the MCP command into [commandRegistry].
|
||||||
void registerCommands(CommandRegistry registry) {
|
///
|
||||||
registry.register(McpCommand());
|
/// [toolRegistry] is passed to [McpCommand] so the `serve` subcommand can
|
||||||
|
/// start the server with all registered tool providers.
|
||||||
|
void registerCommands(
|
||||||
|
CommandRegistry commandRegistry,
|
||||||
|
McpToolRegistry toolRegistry,
|
||||||
|
) {
|
||||||
|
commandRegistry.register(McpCommand(toolRegistry));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Export any libraries intended for clients of this package.
|
|
||||||
|
|
|
||||||
41
packages/mcp/lib/src/commands/serve_command.dart
Normal file
41
packages/mcp/lib/src/commands/serve_command.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
import 'package:dart_mcp/stdio.dart';
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../dew_mcp_server.dart';
|
||||||
|
import '../mcp_tool_registry.dart';
|
||||||
|
|
||||||
|
class ServeCommand extends DewCommand {
|
||||||
|
final McpToolRegistry _toolRegistry;
|
||||||
|
|
||||||
|
ServeCommand(this._toolRegistry);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'serve';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description =
|
||||||
|
'Start the Dew MCP server on stdio. '
|
||||||
|
'Connect your MCP client to this process.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final context = await ProjectContext.find();
|
||||||
|
final tools = _toolRegistry.allTools;
|
||||||
|
|
||||||
|
io.stderr.writeln(
|
||||||
|
'Dew MCP server starting — '
|
||||||
|
'${tools.length} tool(s) registered, '
|
||||||
|
'project root: ${context.root}',
|
||||||
|
);
|
||||||
|
|
||||||
|
DewMcpServer(
|
||||||
|
stdioChannel(input: io.stdin, output: io.stdout),
|
||||||
|
tools,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the process alive until the MCP client closes the connection.
|
||||||
|
await io.stdin.drain<void>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import 'commands/serve_command.dart';
|
||||||
|
import 'mcp_tool_registry.dart';
|
||||||
|
|
||||||
/// Top-level CLI command for MCP server operations.
|
/// Top-level CLI command for MCP server operations.
|
||||||
class McpCommand extends DewCommand {
|
class McpCommand extends DewCommand {
|
||||||
|
McpCommand(McpToolRegistry toolRegistry) {
|
||||||
|
addSubcommand(ServeCommand(toolRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final String name = 'mcp';
|
final String name = 'mcp';
|
||||||
|
|
||||||
|
|
|
||||||
40
packages/mcp/lib/src/dew_mcp_server.dart
Normal file
40
packages/mcp/lib/src/dew_mcp_server.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:dart_mcp/server.dart';
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
/// An MCP server that serves all tools registered in a [McpToolRegistry].
|
||||||
|
base class DewMcpServer extends MCPServer with ToolsSupport {
|
||||||
|
DewMcpServer(super.channel, List<McpTool> tools)
|
||||||
|
: super.fromStreamChannel(
|
||||||
|
implementation: Implementation(name: 'dew', version: '1.0.0'),
|
||||||
|
instructions:
|
||||||
|
'Tools for managing a Dew project (kanban tickets, etc.).',
|
||||||
|
) {
|
||||||
|
for (final tool in tools) {
|
||||||
|
registerTool(
|
||||||
|
Tool(
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
// ObjectSchema is an extension type over Map<String,Object?>;
|
||||||
|
// pass validateArguments:false to avoid runtime cast issues with
|
||||||
|
// nested Map<String,dynamic> literals.
|
||||||
|
inputSchema: ObjectSchema.fromMap(
|
||||||
|
tool.inputSchema.cast<String, Object?>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(request) async {
|
||||||
|
final args = (request.arguments ?? const {}).cast<String, dynamic>();
|
||||||
|
try {
|
||||||
|
final text = await tool.handler(args);
|
||||||
|
return CallToolResult(content: [Content.text(text: text)]);
|
||||||
|
} catch (e) {
|
||||||
|
return CallToolResult(
|
||||||
|
content: [Content.text(text: 'Error: $e')],
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validateArguments: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/mcp/lib/src/mcp_tool_registry.dart
Normal file
13
packages/mcp/lib/src/mcp_tool_registry.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
/// Collects [McpToolProvider] instances and exposes their combined tool list.
|
||||||
|
class McpToolRegistry {
|
||||||
|
final List<McpToolProvider> _providers = [];
|
||||||
|
|
||||||
|
/// Adds [provider] to the registry.
|
||||||
|
void register(McpToolProvider provider) => _providers.add(provider);
|
||||||
|
|
||||||
|
/// All tools from every registered provider, in registration order.
|
||||||
|
List<McpTool> get allTools =>
|
||||||
|
List.unmodifiable(_providers.expand((p) => p.tools));
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
dew_core:
|
dew_core:
|
||||||
path: ../core
|
path: ../core
|
||||||
|
dart_mcp: ^0.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^6.0.0
|
lints: ^6.0.0
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,57 @@
|
||||||
import 'package:dew_mcp/dew_mcp.dart';
|
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:dew_mcp/dew_mcp.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class _PingProvider implements McpToolProvider {
|
||||||
|
@override
|
||||||
|
List<McpTool> get tools => [
|
||||||
|
McpTool(
|
||||||
|
name: 'ping',
|
||||||
|
description: 'Returns pong.',
|
||||||
|
inputSchema: {'type': 'object', 'properties': {}},
|
||||||
|
handler: (_) async => 'pong',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('McpCommand', () {
|
group('McpCommand', () {
|
||||||
test('has correct name and description', () {
|
test('has correct name and description', () {
|
||||||
final cmd = McpCommand();
|
final cmd = McpCommand(McpToolRegistry());
|
||||||
expect(cmd.name, 'mcp');
|
expect(cmd.name, 'mcp');
|
||||||
expect(cmd.description, isNotEmpty);
|
expect(cmd.description, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('has serve subcommand', () {
|
||||||
|
final cmd = McpCommand(McpToolRegistry());
|
||||||
|
expect(cmd.subcommands.keys, contains('serve'));
|
||||||
|
});
|
||||||
|
|
||||||
test('registerCommands adds mcp command to registry', () {
|
test('registerCommands adds mcp command to registry', () {
|
||||||
final registry = CommandRegistry();
|
final commandRegistry = CommandRegistry();
|
||||||
registerCommands(registry);
|
registerCommands(commandRegistry, McpToolRegistry());
|
||||||
expect(registry.commands.map((c) => c.name), contains('mcp'));
|
expect(commandRegistry.commands.map((c) => c.name), contains('mcp'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('McpToolRegistry', () {
|
||||||
|
test('starts empty', () {
|
||||||
|
expect(McpToolRegistry().allTools, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('register adds provider tools', () {
|
||||||
|
final registry = McpToolRegistry();
|
||||||
|
registry.register(_PingProvider());
|
||||||
|
expect(registry.allTools, hasLength(1));
|
||||||
|
expect(registry.allTools.first.name, 'ping');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('aggregates tools from multiple providers', () {
|
||||||
|
final registry = McpToolRegistry();
|
||||||
|
registry.register(_PingProvider());
|
||||||
|
registry.register(_PingProvider());
|
||||||
|
expect(registry.allTools, hasLength(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
24
pubspec.lock
24
pubspec.lock
|
|
@ -129,6 +129,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
dart_mcp:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_mcp
|
||||||
|
sha256: "92a2ee1cca577ed54fa4f3d5ae0e80ce2dd61e1892e793bb4f951b30eab9c4b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -201,6 +209,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.11.0"
|
version: "4.11.0"
|
||||||
|
json_rpc_2:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_rpc_2
|
||||||
|
sha256: "82dfd37d3b2e5030ae4729e1d7f5538cbc45eb1c73d618b9272931facac3bec1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|
@ -417,6 +433,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue