fix: tolerate numeric args from MCP JSON in all kanban commands

Replace fragile 'as String'/'as String?' casts in callAsTool() with
string interpolation so that numeric values sent by MCP JSON clients
(double instead of String) no longer throw a type cast error.

Fixes: type 'double' is not a subtype of type 'String' in type cast

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Chris Hendrickson 2026-04-25 16:01:36 -04:00
parent 0ad1fae213
commit 1e263f08c0
13 changed files with 41 additions and 35 deletions

View file

@ -35,8 +35,8 @@ class AddCommentCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final comment = args['comment'] as String; final comment = '${args['comment']}';
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final store = TicketStore( final store = TicketStore(

View file

@ -30,7 +30,7 @@ class ArchiveCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban; final config = context.config.kanban;

View file

@ -27,9 +27,11 @@ class BoardCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final typeFilter = args['type'] as String?; final typeFilter = args['type'] != null ? '${args['type']}' : null;
final labelFilter = args['label'] as String?; final labelFilter = args['label'] != null ? '${args['label']}' : null;
final milestoneFilter = args['milestone'] as String?; final milestoneFilter = args['milestone'] != null
? '${args['milestone']}'
: null;
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban; final config = context.config.kanban;

View file

@ -43,10 +43,10 @@ class CreateCommand extends DewCommand with DewToolCommand {
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban; final config = context.config.kanban;
final title = args['title'] as String; final title = '${args['title']}';
final typeId = args['type'] as String; final typeId = '${args['type']}';
final columnArg = args['column'] as String?; final columnArg = args['column'] != null ? '${args['column']}' : null;
final body = args['body'] as String? ?? ''; final body = args['body'] != null ? '${args['body']}' : '';
final milestones = _toStringList(args['milestone']); final milestones = _toStringList(args['milestone']);
final labels = _toStringList(args['label']); final labels = _toStringList(args['label']);

View file

@ -28,7 +28,7 @@ class DeleteCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final store = TicketStore( final store = TicketStore(
kanbanDir: context.dirs.kanban, kanbanDir: context.dirs.kanban,

View file

@ -29,7 +29,7 @@ class GetCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final store = TicketStore( final store = TicketStore(
kanbanDir: context.dirs.kanban, kanbanDir: context.dirs.kanban,

View file

@ -40,9 +40,9 @@ class LinkCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final targetId = (args['target'] as String).toUpperCase(); final targetId = '${args['target']}'.toUpperCase();
final type = args['type'] as String; final type = '${args['type']}';
if (id == targetId) if (id == targetId)
throw ArgumentError('A ticket cannot be linked to itself.'); throw ArgumentError('A ticket cannot be linked to itself.');

View file

@ -38,10 +38,12 @@ class ListCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final columnFilter = args['column'] as String?; final columnFilter = args['column'] != null ? '${args['column']}' : null;
final typeFilter = args['type'] as String?; final typeFilter = args['type'] != null ? '${args['type']}' : null;
final labelFilter = args['label'] as String?; final labelFilter = args['label'] != null ? '${args['label']}' : null;
final milestoneFilter = args['milestone'] as String?; final milestoneFilter = args['milestone'] != null
? '${args['milestone']}'
: null;
final includeArchived = args['include-archived'] as bool? ?? false; final includeArchived = args['include-archived'] as bool? ?? false;
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);

View file

@ -31,8 +31,8 @@ class MoveCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final column = args['column'] as String; final column = '${args['column']}';
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban; final config = context.config.kanban;

View file

@ -46,11 +46,13 @@ class SearchCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final query = (args['query'] as String).toLowerCase(); final query = '${args['query']}'.toLowerCase();
final columnFilter = args['column'] as String?; final columnFilter = args['column'] != null ? '${args['column']}' : null;
final typeFilter = args['type'] as String?; final typeFilter = args['type'] != null ? '${args['type']}' : null;
final labelFilter = args['label'] as String?; final labelFilter = args['label'] != null ? '${args['label']}' : null;
final milestoneFilter = args['milestone'] as String?; final milestoneFilter = args['milestone'] != null
? '${args['milestone']}'
: null;
final includeArchived = args['include-archived'] as bool? ?? false; final includeArchived = args['include-archived'] as bool? ?? false;
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);

View file

@ -35,7 +35,7 @@ class UnarchiveCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban; final config = context.config.kanban;
@ -50,7 +50,7 @@ class UnarchiveCommand extends DewCommand with DewToolCommand {
if (ticket == null) throw ArgumentError('Ticket $id not found.'); if (ticket == null) throw ArgumentError('Ticket $id not found.');
if (ticket.column != 'archive') return '$id is not archived.'; if (ticket.column != 'archive') return '$id is not archived.';
final columnArg = args['column'] as String?; final columnArg = args['column'] != null ? '${args['column']}' : null;
final targetColumn = columnArg ?? config.columns.first.id; final targetColumn = columnArg ?? config.columns.first.id;
if (!config.columns.any((c) => c.id == targetColumn)) { if (!config.columns.any((c) => c.id == targetColumn)) {
throw ArgumentError( throw ArgumentError(

View file

@ -30,8 +30,8 @@ class UnlinkCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final targetId = (args['target'] as String).toUpperCase(); final targetId = '${args['target']}'.toUpperCase();
final context = await ProjectContext.find(fs: _fs); final context = await ProjectContext.find(fs: _fs);
final store = TicketStore( final store = TicketStore(

View file

@ -37,11 +37,11 @@ class UpdateCommand extends DewCommand with DewToolCommand {
@override @override
Future<String> callAsTool(Map<String, dynamic> args) async { Future<String> callAsTool(Map<String, dynamic> args) async {
final id = (args['id'] as String).toUpperCase(); final id = '${args['id']}'.toUpperCase();
final title = args['title'] as String?; final title = args['title'] != null ? '${args['title']}' : null;
final typeId = args['type'] as String?; final typeId = args['type'] != null ? '${args['type']}' : null;
final column = args['column'] as String?; final column = args['column'] != null ? '${args['column']}' : null;
final body = args['body'] as String?; final body = args['body'] != null ? '${args['body']}' : null;
final rawMilestones = args['milestone'] as List?; final rawMilestones = args['milestone'] as List?;
final milestones = rawMilestones != null && rawMilestones.isNotEmpty final milestones = rawMilestones != null && rawMilestones.isNotEmpty
? rawMilestones.cast<String>().where((s) => s.isNotEmpty).toList() ? rawMilestones.cast<String>().where((s) => s.isNotEmpty).toList()