Skip to content
Open
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
10 changes: 9 additions & 1 deletion mcp_toolkit/lib/src/mcp_toolkit_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ mixin MCPToolkitExtensions on MCPToolkitBindingBase {
registerServiceExtension(
name: extensionName,
callback: (final parameters) async {
final wireArgs = parameters.map(MapEntry<String, Object?>.new);
final wireArgs = _extractToolArguments(parameters);
final registration = entry.toRegistration();
final args = coerceArgumentsForSchema(
registration.descriptor.inputSchema,
Expand Down Expand Up @@ -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<String, Object?> _extractToolArguments(
final Map<String, String> parameters,
) => Map<String, Object?>.from(parameters)..remove('isolateId');
60 changes: 59 additions & 1 deletion mcp_toolkit/test/mcp_toolkit_bootstrap_test.dart
Original file line number Diff line number Diff line change
@@ -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<String, String>? 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 {
Expand Down Expand Up @@ -81,3 +124,18 @@ void main() {
},
);
}

final class _CapturingToolkitBinding extends MCPToolkitBindingBase
with MCPToolkitExtensions {
final callbacks = <String, ServiceExtensionCallback>{};

@override
void registerServiceExtension({
required final String name,
required final ServiceExtensionCallback callback,
}) {
callbacks[name] = callback;
}
}

final class _TestErrorMonitor with ErrorMonitor {}
Loading