From 548dba7a44fe3697b00b04a5227de13c52dbb402 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Fri, 26 Jun 2026 13:19:46 -0400 Subject: [PATCH] fix: restore wasm pub score compatibility Restore web/WASM-safe default exports for pub.dev/pana scoring and prepare mcp_dart 2.2.2 release. --- CHANGELOG.md | 8 + README.md | 2 +- doc/getting-started.md | 2 +- doc/quick-reference.md | 2 +- lib/mcp_dart.dart | 10 +- lib/src/client/module_web.dart | 1 + lib/src/client/stdio_stub.dart | 75 +++++ lib/src/server/io_stubs.dart | 565 +++++++++++++++++++++++++++++++++ lib/src/server/module_web.dart | 7 +- lib/src/shared/logging.dart | 2 +- lib/src/shared/module_web.dart | 2 + pubspec.yaml | 2 +- 12 files changed, 668 insertions(+), 10 deletions(-) create mode 100644 lib/src/client/stdio_stub.dart create mode 100644 lib/src/server/io_stubs.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 482cb4bc..6d9d06a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.2.2 + +### Platform support + +- Made the package barrel's default export path web/WASM-safe while preserving + Dart IO native exports, working around pub.dev/pana 0.23.13 WASM platform + scoring for conditional exports. + ## 2.2.1 ### Spec Alignment diff --git a/README.md b/README.md index cbf64d12..992114dd 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Add to your `pubspec.yaml`: ```yaml dependencies: - mcp_dart: ^2.2.0 + mcp_dart: ^2.2.2 ``` Then install dependencies: diff --git a/doc/getting-started.md b/doc/getting-started.md index b562e467..3df316d6 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -8,7 +8,7 @@ Add the MCP Dart SDK to your `pubspec.yaml`: ```yaml dependencies: - mcp_dart: ^2.2.0 + mcp_dart: ^2.2.2 ``` Then run: diff --git a/doc/quick-reference.md b/doc/quick-reference.md index b68aeedc..105492a9 100644 --- a/doc/quick-reference.md +++ b/doc/quick-reference.md @@ -7,7 +7,7 @@ Fast lookup guide for common MCP Dart SDK operations. ```yaml # pubspec.yaml dependencies: - mcp_dart: ^2.2.0 + mcp_dart: ^2.2.2 ``` ```bash diff --git a/lib/mcp_dart.dart b/lib/mcp_dart.dart index 62d0672a..e2b5f668 100644 --- a/lib/mcp_dart.dart +++ b/lib/mcp_dart.dart @@ -13,6 +13,10 @@ export 'src/types.dart'; // Exports shared types used across the MCP protocol. export 'src/shared/uuid.dart'; // Exports UUID generation utilities. export 'src/shared/logging.dart'; // Exports logging for customization -// Platform-specific exports -export 'src/exports.dart' // Stub export for other platforms - if (dart.library.js_interop) 'src/exports_web.dart'; // Web-specific exports +// Platform-specific exports. +// +// Keep the default branch web/WASM-safe. Pub.dev currently runs pana 0.23.13, +// which does not select `dart.library.js_interop` for WASM platform scoring and +// would otherwise follow the native `dart:io` exports. Native platforms still +// get the full implementation through `dart.library.io`. +export 'src/exports_web.dart' if (dart.library.io) 'src/exports.dart'; diff --git a/lib/src/client/module_web.dart b/lib/src/client/module_web.dart index 884fac96..1be6dfe1 100644 --- a/lib/src/client/module_web.dart +++ b/lib/src/client/module_web.dart @@ -6,5 +6,6 @@ library; export './client.dart'; // Client-side implementation for MCP protocol. +export './stdio_stub.dart'; // API-compatible stdio stubs. export './streamable_https.dart'; // Streamable HTTPS implementation. export './task_client.dart'; // Task client helper. diff --git a/lib/src/client/stdio_stub.dart b/lib/src/client/stdio_stub.dart new file mode 100644 index 00000000..766467a9 --- /dev/null +++ b/lib/src/client/stdio_stub.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:mcp_dart/src/shared/transport.dart'; +import 'package:mcp_dart/src/types.dart'; + +Never _unsupported() => throw UnsupportedError( + 'StdioClientTransport is only available on Dart IO platforms.', + ); + +/// Configuration parameters for launching a stdio server process. +/// +/// This web/default-platform stub preserves the public API shape without +/// importing `dart:io`. The real implementation is selected on Dart IO +/// platforms through the package barrel's conditional export. +class StdioServerParameters { + /// The executable command to run to start the server process. + final String command; + + /// Command line arguments to pass to the executable. + final List args; + + /// Environment variables to use when spawning the process. + final Map? environment; + + /// How to handle the stderr stream of the child process on IO platforms. + final Object? stderrMode; + + /// The working directory to use when spawning the process. + final String? workingDirectory; + + /// Creates parameters for launching the stdio server. + const StdioServerParameters({ + required this.command, + this.args = const [], + this.environment, + this.stderrMode, + this.workingDirectory, + }); +} + +/// Stub for the stdio client transport on platforms without `dart:io`. +class StdioClientTransport implements Transport { + /// Creates a stdio client transport stub. + StdioClientTransport(this.serverParams); + + /// Configuration for launching the server process. + final StdioServerParameters serverParams; + + @override + void Function()? onclose; + + @override + void Function(Error error)? onerror; + + @override + void Function(JsonRpcMessage message)? onmessage; + + @override + String? get sessionId => null; + + /// Stderr is unavailable without a spawned process. + Stream>? get stderr => null; + + @override + Future start() async => _unsupported(); + + @override + Future close() async { + onclose?.call(); + } + + @override + Future send(JsonRpcMessage message, {int? relatedRequestId}) async => + _unsupported(); +} diff --git a/lib/src/server/io_stubs.dart b/lib/src/server/io_stubs.dart new file mode 100644 index 00000000..20f6e612 --- /dev/null +++ b/lib/src/server/io_stubs.dart @@ -0,0 +1,565 @@ +import 'dart:async'; + +import 'package:mcp_dart/src/server/mcp_server.dart'; +import 'package:mcp_dart/src/shared/transport.dart'; +import 'package:mcp_dart/src/types.dart'; + +Never _unsupported(String apiName) => throw UnsupportedError( + '$apiName is only available on Dart IO platforms.', + ); + +/// ID for SSE streams. +typedef StreamId = String; + +/// ID for events in SSE streams. +typedef EventId = String; + +/// Interface for resumability support via event storage. +abstract class EventStore { + /// Stores an event for later retrieval. + Future storeEvent(StreamId streamId, JsonRpcMessage message); + + /// Replays events after a specified event ID. + Future replayEventsAfter( + EventId lastEventId, { + required Future Function(EventId eventId, JsonRpcMessage message) + send, + }); +} + +/// Simple in-memory event store for resumability. +class InMemoryEventStore implements EventStore { + final Map> _events = {}; + int _eventCounter = 0; + + @override + Future storeEvent(StreamId streamId, JsonRpcMessage message) async { + final eventId = (++_eventCounter).toString(); + _events.putIfAbsent(streamId, () => []); + _events[streamId]!.add((id: eventId, message: message)); + return eventId; + } + + @override + Future replayEventsAfter( + EventId lastEventId, { + required Future Function(EventId eventId, JsonRpcMessage message) + send, + }) async { + String? streamId; + var fromIndex = -1; + + for (final entry in _events.entries) { + final index = entry.value.indexWhere((event) => event.id == lastEventId); + if (index >= 0) { + streamId = entry.key; + fromIndex = index; + break; + } + } + + if (streamId == null) { + throw StateError('Event ID not found: $lastEventId'); + } + + for (var i = fromIndex + 1; i < _events[streamId]!.length; i++) { + final event = _events[streamId]![i]; + await send(event.id, event.message); + } + + return streamId; + } +} + +/// Configuration options for StreamableHTTPServerTransport. +class StreamableHTTPServerTransportOptions { + /// Function that generates a session ID for the transport. + final String? Function()? sessionIdGenerator; + + /// A callback for session initialization events. + final void Function(String sessionId)? onsessioninitialized; + + /// If true, the server will return JSON responses instead of SSE streams. + final bool enableJsonResponse; + + /// Event store for resumability support. + final EventStore? eventStore; + + /// Enables host/origin validation to mitigate DNS rebinding attacks. + final bool enableDnsRebindingProtection; + + /// Explicit host allowlist used when DNS rebinding protection is enabled. + final Set? allowedHosts; + + /// Explicit origin allowlist used when DNS rebinding protection is enabled. + final Set? allowedOrigins; + + /// If true, reject unsupported `MCP-Protocol-Version` headers. + final bool strictProtocolVersionHeaderValidation; + + /// If true, reject JSON-RPC batch payloads. + final bool rejectBatchJsonRpcPayloads; + + /// The maximum number of events allowed during SSE resumption. + final int maxReplayedEvents; + + /// Creates configuration options for StreamableHTTPServerTransport. + StreamableHTTPServerTransportOptions({ + this.sessionIdGenerator, + this.onsessioninitialized, + this.enableJsonResponse = false, + this.eventStore, + this.enableDnsRebindingProtection = true, + this.allowedHosts, + this.allowedOrigins, + this.strictProtocolVersionHeaderValidation = true, + this.rejectBatchJsonRpcPayloads = true, + this.maxReplayedEvents = 1000, + }); +} + +/// Stub for Streamable HTTP server transport on platforms without `dart:io`. +class StreamableHTTPServerTransport + implements Transport, RequestIdAwareTransport { + /// Creates a new StreamableHTTPServerTransport stub. + StreamableHTTPServerTransport({required this.options}); + + /// Transport options retained for API compatibility. + final StreamableHTTPServerTransportOptions options; + + @override + String? sessionId; + + @override + void Function()? onclose; + + @override + void Function(Error error)? onerror; + + @override + void Function(JsonRpcMessage message)? onmessage; + + @override + Future start() async => + _unsupported('StreamableHTTPServerTransport.start'); + + /// Handles an incoming HTTP request on IO platforms. + Future handleRequest(Object request, [dynamic parsedBody]) async => + _unsupported('StreamableHTTPServerTransport.handleRequest'); + + @override + Future close() async { + onclose?.call(); + } + + @override + Future send(JsonRpcMessage message, {int? relatedRequestId}) => + sendWithRequestId(message, relatedRequestId: relatedRequestId); + + @override + Future sendWithRequestId( + JsonRpcMessage message, { + RequestId? relatedRequestId, + }) async => + _unsupported('StreamableHTTPServerTransport.send'); +} + +/// Server transport for SSE on IO platforms. +class SseServerTransport implements Transport { + /// Creates a new SSE server transport stub. + SseServerTransport({ + required Object response, + required String messageEndpointPath, + }) : _response = response, + _messageEndpointPath = messageEndpointPath; + + final Object _response; + final String _messageEndpointPath; + + @override + void Function()? onclose; + + @override + void Function(Error error)? onerror; + + @override + void Function(JsonRpcMessage message)? onmessage; + + @override + String get sessionId => _unsupported('SseServerTransport.sessionId'); + + @override + Future start() async { + // Touch constructor fields so analyzer does not flag them as unused. + Object.hash(_response, _messageEndpointPath); + _unsupported('SseServerTransport.start'); + } + + /// Handles incoming HTTP POST requests on IO platforms. + Future handlePostMessage(Object request, {dynamic parsedBody}) async => + _unsupported('SseServerTransport.handlePostMessage'); + + @override + Future close() async { + onclose?.call(); + } + + @override + Future send(JsonRpcMessage message, {int? relatedRequestId}) async => + _unsupported('SseServerTransport.send'); +} + +/// Manages Server-Sent Events (SSE) connections and routes HTTP requests. +class SseServerManager { + /// Creates an SSE server manager stub. + SseServerManager( + this.mcpServer, { + this.ssePath = '/sse', + this.messagePath = '/messages', + this.enableDnsRebindingProtection = false, + this.allowedHosts, + this.allowedOrigins, + }); + + /// Map to store active SSE transports, keyed by session ID. + final Map activeSseTransports = {}; + + /// The main MCP Server instance. + final McpServer mcpServer; + + /// Path for establishing SSE connections. + final String ssePath; + + /// Path for sending messages to the server. + final String messagePath; + + /// Enables host/origin validation to mitigate DNS rebinding attacks. + final bool enableDnsRebindingProtection; + + /// Explicit host allowlist used when DNS rebinding protection is enabled. + final Set? allowedHosts; + + /// Explicit origin allowlist used when DNS rebinding protection is enabled. + final Set? allowedOrigins; + + /// Routes incoming HTTP requests on IO platforms. + Future handleRequest(Object request) async => + _unsupported('SseServerManager.handleRequest'); + + /// Handles the initial GET request to establish an SSE connection. + Future handleSseConnection(Object request) async => + _unsupported('SseServerManager.handleSseConnection'); +} + +/// Stub for stdio server transport on platforms without `dart:io`. +class StdioServerTransport implements Transport { + /// Creates a new stdio server transport stub. + StdioServerTransport({Object? stdin, Object? stdout}) + : _stdin = stdin, + _stdout = stdout; + + final Object? _stdin; + final Object? _stdout; + + @override + void Function()? onclose; + + @override + void Function(Error error)? onerror; + + @override + void Function(JsonRpcMessage message)? onmessage; + + @override + String? get sessionId => null; + + @override + Future start() async { + Object.hash(_stdin, _stdout); + _unsupported('StdioServerTransport.start'); + } + + @override + Future close() async { + onclose?.call(); + } + + @override + Future send(JsonRpcMessage message, {int? relatedRequestId}) async => + _unsupported('StdioServerTransport.send'); +} + +/// OAuth 2.0 Protected Resource Metadata advertised by a Streamable HTTP server. +class OAuthProtectedResourceMetadata { + /// Canonical MCP resource URI that access tokens are issued for. + final Uri resource; + + /// Authorization server issuer URLs that can issue tokens for [resource]. + final List authorizationServers; + + /// Supported bearer token presentation methods. + final List bearerMethodsSupported; + + /// Scopes the resource server can advertise to clients. + final List? scopesSupported; + + /// Additional metadata fields to include in the protected-resource document. + final Map additionalFields; + + /// Creates OAuth protected-resource metadata. + const OAuthProtectedResourceMetadata({ + required this.resource, + required this.authorizationServers, + this.bearerMethodsSupported = const ['header'], + this.scopesSupported, + this.additionalFields = const {}, + }); + + /// Converts this metadata to its wire JSON shape. + Map toJson() => { + ...additionalFields, + 'resource': resource.toString(), + 'authorization_servers': + authorizationServers.map((uri) => uri.toString()).toList(), + 'bearer_methods_supported': bearerMethodsSupported, + if (scopesSupported != null) 'scopes_supported': scopesSupported, + }; +} + +/// Bearer `WWW-Authenticate` challenge parameters for OAuth resource servers. +class OAuthBearerChallenge { + /// Protected-resource metadata URL advertised through `resource_metadata`. + final Uri? resourceMetadata; + + /// Scope required for the failed request. + final String? scope; + + /// OAuth bearer error code, such as `insufficient_scope`. + final String? error; + + /// Optional human-readable bearer error description. + final String? errorDescription; + + /// Additional bearer challenge parameters. + final Map additionalParameters; + + /// Creates a bearer challenge. + const OAuthBearerChallenge({ + this.resourceMetadata, + this.scope, + this.error, + this.errorDescription, + this.additionalParameters = const {}, + }); + + /// Creates the challenge recommended for insufficient-scope responses. + const OAuthBearerChallenge.insufficientScope({ + required Uri resourceMetadata, + required String scope, + String? errorDescription, + Map additionalParameters = const {}, + }) : this( + resourceMetadata: resourceMetadata, + scope: scope, + error: 'insufficient_scope', + errorDescription: errorDescription, + additionalParameters: additionalParameters, + ); + + /// Converts this challenge to a `WWW-Authenticate` header value. + String toHeaderValue() { + final parameters = { + ...additionalParameters, + if (resourceMetadata != null) + 'resource_metadata': resourceMetadata.toString(), + if (scope != null) 'scope': scope!, + if (error != null) 'error': error!, + if (errorDescription != null) 'error_description': errorDescription!, + }; + final serializedParameters = parameters.entries + .map((entry) => '${entry.key}="${_quoteHeaderValue(entry.value)}"') + .join(', '); + return serializedParameters.isEmpty + ? 'Bearer' + : 'Bearer $serializedParameters'; + } +} + +String _quoteHeaderValue(String value) { + const backslash = '\\'; + const escapedBackslash = '\\\\'; + const quote = '"'; + const escapedQuote = r'\"'; + return value + .replaceAll(backslash, escapedBackslash) + .replaceAll(quote, escapedQuote); +} + +/// OAuth protected-resource behavior for [StreamableMcpServer]. +class OAuthProtectedResourceOptions { + /// Metadata returned from protected-resource well-known endpoints. + final OAuthProtectedResourceMetadata metadata; + + /// Public protected-resource metadata URL advertised in bearer challenges. + final Uri? metadataUri; + + /// Optional scope challenge returned on unauthorized requests. + final String? scope; + + /// Optional endpoint-specific metadata path. + final String? metadataPath; + + /// Also serve metadata at the root protected-resource well-known endpoint. + final bool serveRootMetadata; + + /// Creates OAuth protected-resource server options. + const OAuthProtectedResourceOptions({ + required this.metadata, + this.metadataUri, + this.scope, + this.metadataPath, + this.serveRootMetadata = true, + }); +} + +/// Result returned by [StreamableMcpServer.authenticationHandler]. +class StreamableMcpAuthenticationResult { + // Mirrors the IO implementation's private state for const constructor shape. + // ignore: unused_field + final _StreamableMcpAuthenticationStatus _status; + + /// Scope required for an insufficient-scope response. + final String? scope; + + /// Optional human-readable bearer error description. + final String? errorDescription; + + /// Additional bearer challenge parameters. + final Map additionalChallengeParameters; + + const StreamableMcpAuthenticationResult._( + this._status, { + this.scope, + this.errorDescription, + this.additionalChallengeParameters = const {}, + }); + + /// Allows the request to proceed. + const StreamableMcpAuthenticationResult.allow() + : this._(_StreamableMcpAuthenticationStatus.allow); + + /// Rejects the request as unauthenticated or invalidly authenticated. + const StreamableMcpAuthenticationResult.unauthorized({ + String? errorDescription, + Map additionalChallengeParameters = const {}, + }) : this._( + _StreamableMcpAuthenticationStatus.unauthorized, + errorDescription: errorDescription, + additionalChallengeParameters: additionalChallengeParameters, + ); + + /// Rejects the request because the presented token lacks required scope. + const StreamableMcpAuthenticationResult.insufficientScope({ + required String scope, + String? errorDescription, + Map additionalChallengeParameters = const {}, + }) : this._( + _StreamableMcpAuthenticationStatus.insufficientScope, + scope: scope, + errorDescription: errorDescription, + additionalChallengeParameters: additionalChallengeParameters, + ); +} + +enum _StreamableMcpAuthenticationStatus { + allow, + unauthorized, + insufficientScope, +} + +/// A high-level server implementation that manages sessions over Streamable HTTP. +class StreamableMcpServer { + /// Default port used by the IO implementation. + static const int defaultPort = 3000; + + /// Creates a high-level Streamable HTTP server stub. + StreamableMcpServer({ + required McpServer Function(String sessionId) serverFactory, + this.host = 'localhost', + this.port = defaultPort, + this.path = '/mcp', + this.eventStore, + this.authenticator, + this.authenticationHandler, + this.oauthProtectedResource, + this.enableDnsRebindingProtection = true, + this.allowedHosts, + this.allowedOrigins, + this.strictProtocolVersionHeaderValidation = true, + this.rejectBatchJsonRpcPayloads = true, + }) : _serverFactory = serverFactory; + + final McpServer Function(String sessionId) _serverFactory; + + /// Host to bind the HTTP server to on IO platforms. + final String host; + + /// Port to bind the HTTP server to on IO platforms. + final int port; + + /// Path to listen for MCP requests on. + final String path; + + /// Event store for resumability support. + final EventStore? eventStore; + + /// Optional callback to authenticate requests. + final FutureOr Function(dynamic request)? authenticator; + + /// Optional callback that can return detailed authentication failures. + final FutureOr Function(dynamic request)? + authenticationHandler; + + /// Optional OAuth protected-resource metadata and challenge behavior. + final OAuthProtectedResourceOptions? oauthProtectedResource; + + /// Enables host/origin validation to mitigate DNS rebinding attacks. + final bool enableDnsRebindingProtection; + + /// Explicit host allowlist used when DNS rebinding protection is enabled. + final Set? allowedHosts; + + /// Explicit origin allowlist used when DNS rebinding protection is enabled. + final Set? allowedOrigins; + + /// If true, reject unsupported `MCP-Protocol-Version` headers. + final bool strictProtocolVersionHeaderValidation; + + /// If true, reject JSON-RPC batch payloads. + final bool rejectBatchJsonRpcPayloads; + + /// Starts the HTTP server on IO platforms. + Future start() async { + // Touch constructor fields so analyzer does not flag them as unused without + // invoking user-provided callbacks on unsupported platforms. + Object.hash( + _serverFactory, + host, + port, + path, + eventStore, + authenticator, + authenticationHandler, + oauthProtectedResource, + enableDnsRebindingProtection, + allowedHosts, + allowedOrigins, + strictProtocolVersionHeaderValidation, + rejectBatchJsonRpcPayloads, + ); + _unsupported('StreamableMcpServer.start'); + } + + /// Stops the HTTP server and closes active sessions. + Future stop() async {} +} diff --git a/lib/src/server/module_web.dart b/lib/src/server/module_web.dart index 1ce6725e..55a624ac 100644 --- a/lib/src/server/module_web.dart +++ b/lib/src/server/module_web.dart @@ -5,5 +5,8 @@ /// These exports provide stubs or limited functionality for web compatibility. library; -// No server exports for web platform -// Server functionality is primarily designed for non-web environments +export 'io_stubs.dart'; // API-compatible stubs for IO-only transports. +export 'mcp_server.dart'; // Web-safe MCP server facade and helpers. +export 'mcp_ui.dart'; // MCP Apps helper registrations. +export 'server.dart'; // Core server implementation. +export 'tasks.dart'; // Task management utilities. diff --git a/lib/src/shared/logging.dart b/lib/src/shared/logging.dart index 2aa3766e..fead8707 100644 --- a/lib/src/shared/logging.dart +++ b/lib/src/shared/logging.dart @@ -1,4 +1,4 @@ -import 'logging_io.dart' if (dart.library.js_interop) 'logging_web.dart'; +import 'logging_web.dart' if (dart.library.io) 'logging_io.dart'; enum LogLevel { debug, info, warn, error } diff --git a/lib/src/shared/module_web.dart b/lib/src/shared/module_web.dart index c2fa5463..e6ebd131 100644 --- a/lib/src/shared/module_web.dart +++ b/lib/src/shared/module_web.dart @@ -5,6 +5,8 @@ library; // export 'json_schema_validator.dart'; // JSON Schema validator. (Removed) +export 'iostream.dart'; // Stream/sink transport without dart:io dependencies. export 'protocol.dart'; // MCP protocol utilities for message serialization/deserialization. +export 'task_interfaces.dart'; // Task interfaces. export 'transport.dart'; // Transport layer for server-client communication. export 'uri_template.dart'; // URI template utilities. diff --git a/pubspec.yaml b/pubspec.yaml index 5b255890..28287d8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mcp_dart description: Dart and Flutter SDK for building Model Context Protocol (MCP) servers, clients, hosts, and AI tools. -version: 2.2.1 +version: 2.2.2 repository: https://github.com/leehack/mcp_dart homepage: https://github.com/leehack/mcp_dart issue_tracker: https://github.com/leehack/mcp_dart/issues