diff --git a/mcp_toolkit/lib/src/mcp_toolkit_extensions.dart b/mcp_toolkit/lib/src/mcp_toolkit_extensions.dart index 76f57ef0..127aaf07 100644 --- a/mcp_toolkit/lib/src/mcp_toolkit_extensions.dart +++ b/mcp_toolkit/lib/src/mcp_toolkit_extensions.dart @@ -77,7 +77,7 @@ mixin MCPToolkitExtensions on MCPToolkitBindingBase { registerServiceExtension( name: extensionName, callback: (final parameters) async { - final wireArgs = parameters.map(MapEntry.new); + final wireArgs = _extractToolArguments(parameters); final registration = entry.toRegistration(); final args = coerceArgumentsForSchema( registration.descriptor.inputSchema, @@ -191,3 +191,11 @@ mixin MCPToolkitExtensions on MCPToolkitBindingBase { }; } } + +// Flutter's VM service extension transport injects isolateId into callback +// parameters. It routes the VM service call to the target isolate; it is not +// an MCP tool argument. Strip it before schema coercion/validation because +// tool schemas intentionally reject unknown properties. +Map _extractToolArguments( + final Map parameters, +) => Map.from(parameters)..remove('isolateId'); diff --git a/mcp_toolkit/test/mcp_toolkit_bootstrap_test.dart b/mcp_toolkit/test/mcp_toolkit_bootstrap_test.dart index 8d35c046..3df91f19 100644 --- a/mcp_toolkit/test/mcp_toolkit_bootstrap_test.dart +++ b/mcp_toolkit/test/mcp_toolkit_bootstrap_test.dart @@ -1,9 +1,52 @@ -// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: invalid_use_of_protected_member, lines_longer_than_80_chars +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mcp_toolkit/mcp_toolkit.dart'; void main() { + test( + 'service extension callbacks do not treat isolateId as a tool argument', + () async { + final binding = _CapturingToolkitBinding()..initialize(); + Map? capturedRequest; + + final tool = mcpToolkitTool( + namespace: 'app', + definition: MCPToolDefinition( + name: 'inspect_number', + description: 'Inspect a number', + inputSchema: ObjectSchema( + properties: {'x': IntegerSchema()}, + required: ['x'], + ), + ), + handler: (final request) { + capturedRequest = request; + return MCPCallResult(message: 'inspected', parameters: {'ok': true}); + }, + ); + + binding.initializeServiceExtensions( + errorMonitor: _TestErrorMonitor(), + entries: {tool}, + ); + + final callback = binding.callbacks['inspect_number']; + expect(callback, isNotNull); + + final result = await callback!({ + 'isolateId': 'isolates/4805254787721395', + 'x': '120', + }); + + expect(result['ok'], isTrue); + expect(capturedRequest, isNotNull); + expect(capturedRequest, isNot(contains('isolateId'))); + expect(capturedRequest?['x'], '120'); + }, + ); + testWidgets( 'bootstrapFlutter initializes toolkit, adds entries, and forwards zone errors', (final tester) async { @@ -81,3 +124,18 @@ void main() { }, ); } + +final class _CapturingToolkitBinding extends MCPToolkitBindingBase + with MCPToolkitExtensions { + final callbacks = {}; + + @override + void registerServiceExtension({ + required final String name, + required final ServiceExtensionCallback callback, + }) { + callbacks[name] = callback; + } +} + +final class _TestErrorMonitor with ErrorMonitor {}