| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| |
| import 'package:json_rpc_2/error_code.dart' as error_code; |
| import 'package:json_rpc_2/json_rpc_2.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:stream_channel/stream_channel.dart'; |
| import 'package:stream_transform/stream_transform.dart'; |
| |
| import '../api/api.dart'; |
| import '../shared.dart'; |
| import '../utils/constants.dart'; |
| import '../utils/json_rpc_2_object.dart'; |
| |
| part 'completions_support.dart'; |
| part 'elicitation_request_support.dart'; |
| part 'legacy_input_required_shim.dart'; |
| part 'logging_support.dart'; |
| part 'prompts_support.dart'; |
| part 'request_scoped.dart'; |
| part 'resources_support.dart'; |
| part 'roots_tracking_support.dart'; |
| part 'subscriptions_support.dart'; |
| part 'tools_support.dart'; |
| |
| /// The client context used to initialize an [MCPServer]. |
| /// |
| /// Legacy transports provide this once per connection after negotiating a |
| /// protocol version. Request-scoped transports provide it once per request. |
| final class MCPServerInitialization { |
| const MCPServerInitialization({ |
| required this.protocolVersion, |
| required this.clientCapabilities, |
| this.clientInfo, |
| this.logLevel, |
| }); |
| |
| /// The protocol version used for this connection or request. |
| final ProtocolVersion protocolVersion; |
| |
| /// The capabilities declared by the client. |
| final ClientCapabilities clientCapabilities; |
| |
| /// The implementation information declared by the client, if any. |
| /// |
| /// The legacy handshake always provides this. Request-scoped transports may |
| /// omit it, since clients are not required to send it on every request. |
| final Implementation? clientInfo; |
| |
| /// The log level the client asked for on this request, if any. |
| /// |
| /// Request-scoped transports read this from the reserved |
| /// `io.modelcontextprotocol/logLevel` request metadata key, see |
| /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging. |
| /// On 2026-07-28 [LoggingSupport.initialize] copies this onto |
| /// [LoggingSupport.loggingLevel], `null` included. That revision took |
| /// `logging/setLevel` out, and [LoggingSupport] does not register it. The |
| /// legacy handshake has no per-request level and leaves this null, so the |
| /// level starts at [LoggingLevel.warning] unless the server picked one for |
| /// itself, and `logging/setLevel` moves it from there. |
| final LoggingLevel? logLevel; |
| } |
| |
| /// Base class to extend when implementing an MCP server. |
| /// |
| /// Actual functionality beyond server initialization is done by mixing in |
| /// additional support mixins such as [ToolsSupport], [ResourcesSupport] etc. |
| abstract base class MCPServer extends MCPBase { |
| /// Completes when this server has finished initialization. |
| /// |
| /// Legacy transports complete this after the final acknowledgement from the |
| /// client. Request-scoped transports should call [handleInitialized] after |
| /// [initialize] and any transport-specific setup have completed. |
| Future<InitializedNotification?> get initialized => _initialized.future; |
| final Completer<InitializedNotification?> _initialized = Completer(); |
| |
| /// Whether this server is still active and has completed initialization. |
| bool get ready => isActive && _initialized.isCompleted; |
| |
| /// The name, current version, and other info to give to the client. |
| final Implementation implementation; |
| |
| /// Instructions for how to use this server, which are given to the client. |
| /// |
| /// These may be used in system prompts. |
| final String? instructions; |
| |
| /// How many times a handler may be rerun for an `input_required` result. |
| /// |
| /// Used only on revisions before 2026-07-28. Exceeding it is an `-32603` |
| /// error. Values below 1 throw a [RangeError]. |
| final int maxInputRequiredRounds; |
| |
| /// The negotiated protocol version. |
| /// |
| /// Only assigned after [initialize] has been called. |
| late ProtocolVersion protocolVersion; |
| |
| /// [protocolVersion] once [initialize] has assigned it, else `null`. |
| /// |
| /// The legacy input-required wrap can run for a handler registered before |
| /// [initialize], so the shim reads this instead of the late field. |
| ProtocolVersion? _inputRequiredProtocolVersion; |
| |
| /// The capabilities of the client. |
| /// |
| /// Only assigned after [initialize] has been called. |
| late ClientCapabilities clientCapabilities; |
| |
| /// Whether this connection can answer requests sent by the server. |
| /// |
| /// Legacy connections always can. Request-scoped transports set this from |
| /// their optional server-request callback before dispatching a message. |
| bool _serverRequestsSupported = true; |
| |
| late final _LegacyInputRequiredShim _legacyInputRequiredShim = |
| _LegacyInputRequiredShim(this); |
| |
| /// The client implementation information provided during initialization. |
| /// |
| /// `null` until [initialize] has been called, and remains `null` when the |
| /// client did not declare any implementation information. |
| Implementation? clientInfo; |
| |
| /// The capabilities of this server, which [discover] advertises. |
| /// |
| /// This can be modified by overriding the [initialize] method. |
| final ServerCapabilities capabilities = ServerCapabilities(); |
| |
| @override |
| String get name => implementation.name; |
| |
| /// Emits an event any time the client notifies us of a change to the list of |
| /// roots it supports. |
| /// |
| /// If `null` then the client doesn't support these notifications. |
| /// |
| /// This is a broadcast stream, events are not buffered and only future events |
| /// are given. |
| Stream<RootsListChangedNotification?>? get rootsListChanged => |
| _rootsListChangedController?.stream; |
| StreamController<RootsListChangedNotification?>? _rootsListChangedController; |
| |
| MCPServer.fromStreamChannel( |
| super.channel, { |
| required this.implementation, |
| this.instructions, |
| super.protocolLogSink, |
| this.maxInputRequiredRounds = 8, |
| }) { |
| if (maxInputRequiredRounds < 1) { |
| throw RangeError.range( |
| maxInputRequiredRounds, |
| 1, |
| null, |
| 'maxInputRequiredRounds', |
| ); |
| } |
| registerRequestHandler(InitializeRequest.methodName, initializeLegacy); |
| |
| registerNotificationHandler( |
| InitializedNotification.methodName, |
| handleInitialized, |
| ); |
| } |
| |
| /// Registers [impl] for [name], and on revisions before 2026-07-28 wraps |
| /// the handlers that may answer `input_required` in the legacy shim. |
| @override |
| void registerRequestHandler<T extends Request?, R extends Result?>( |
| String name, |
| FutureOr<R> Function(T) impl, |
| ) { |
| if (!_inputRequiredMethods.contains(name)) { |
| return super.registerRequestHandler<T, R>(name, impl); |
| } |
| super.registerRequestHandler<T, R>( |
| name, |
| (request) async => |
| await _legacyInputRequiredShim.fulfill( |
| name, |
| request as WithInputResponses, |
| (retry) async => (await impl(retry as T)) as Result, |
| ) |
| as R, |
| ); |
| } |
| |
| @override |
| Future<void> shutdown() async { |
| await super.shutdown(); |
| await _rootsListChangedController?.close(); |
| } |
| |
| @mustCallSuper |
| /// Registers the features available to a client with [initialization]. |
| /// |
| /// Mixins and subclasses should register request handlers and other features |
| /// in this method, as well as editing [capabilities]. |
| /// |
| /// Transport-specific initialization, including the legacy MCP initialize |
| /// request, is handled separately. |
| FutureOr<void> initialize(MCPServerInitialization initialization) { |
| protocolVersion = initialization.protocolVersion; |
| _inputRequiredProtocolVersion = protocolVersion; |
| clientCapabilities = initialization.clientCapabilities; |
| clientInfo = initialization.clientInfo; |
| if (clientCapabilities.roots?.listChanged == true) { |
| _rootsListChangedController = |
| StreamController<RootsListChangedNotification?>.broadcast(); |
| registerNotificationHandler( |
| RootsListChangedNotification.methodName, |
| _rootsListChangedController!.sink.add, |
| ); |
| } |
| // Registering this handler is itself a statement about the lifecycle, so |
| // only a server on a request-scoped revision does it. A client probing |
| // under the stdio backward compatibility rules would read an answer here |
| // as "this connection is modern". |
| if (protocolVersion.methodIsValid(DiscoverRequest.methodName)) { |
| registerRequestHandler(DiscoverRequest.methodName, discover); |
| } |
| } |
| |
| /// Answers the `server/discover` request with the protocol versions this |
| /// server serves, the capabilities [initialize] registered, and the |
| /// instructions it was given. |
| /// |
| /// Only the revisions from [ProtocolVersion.v2026_07_28] on are advertised: |
| /// earlier ones are negotiated with the legacy `initialize` handshake, which |
| /// this request replaced. A transport that serves a narrower set than this |
| /// package implements rejects the versions it does not serve itself, the way |
| /// `handleStreamableHttpRequest` does with its own version header check. |
| /// |
| /// The request-scoped dispatcher fills in the rest of what the schema |
| /// requires, `resultType` and the caching hints, and stamps the server's |
| /// identity into `_meta`. This only answers the fields that are specific to |
| /// discovery. Override it to advertise something else. |
| /// |
| /// The request has no parameters of its own beyond the `_meta` envelope, and |
| /// the per-request context that envelope carries reaches a server through |
| /// [initialize] rather than through here, so it is optional the way the other |
| /// requests without parameters are. |
| /// |
| /// https://modelcontextprotocol.io/specification/2026-07-28/server/discover |
| FutureOr<DiscoverResult> discover([DiscoverRequest? request]) => |
| DiscoverResult( |
| supportedVersions: [ |
| for (final version in ProtocolVersion.values) |
| if (version.methodIsValid(DiscoverRequest.methodName)) |
| version.versionString, |
| ], |
| capabilities: advertisedCapabilities, |
| instructions: instructions, |
| ); |
| |
| /// The capabilities [discover] advertises. |
| /// |
| /// On these revisions a client only hears a list change or a resource update |
| /// over a `subscriptions/listen` stream it opened with the matching |
| /// `promptsListChanged`, `toolsListChanged`, `resourcesListChanged` or |
| /// `resourceSubscriptions` filter. [SubscriptionsSupport] acknowledges the |
| /// request, and the Streamable HTTP transport routes matching notifications |
| /// from that request's server. A server with [SubscriptionsSupport] therefore |
| /// advertises the capabilities it registered, and overrides this to do so. |
| /// Without that mixin the four bits standing for those notifications come off |
| /// here: `listChanged` on each of the three, and `subscribe` on |
| /// [ServerCapabilities.resources]. |
| /// Every other key the server registered is passed through, and |
| /// `initializeLegacy` keeps all of them, since the revisions that handshake |
| /// negotiates still serve `resources/subscribe` and send the list changes |
| /// without a filter. |
| @protected |
| ServerCapabilities get advertisedCapabilities => ServerCapabilities.fromMap({ |
| ...capabilities as Map<String, Object?>, |
| if (capabilities.prompts case final prompts?) |
| Keys.prompts: Prompts.fromMap( |
| Map<String, Object?>.from(prompts as Map<String, Object?>) |
| ..remove(Keys.listChanged), |
| ), |
| if (capabilities.resources case final resources?) |
| Keys.resources: Resources.fromMap( |
| Map<String, Object?>.from(resources as Map<String, Object?>) |
| ..remove(Keys.listChanged) |
| ..remove(Keys.subscribe), |
| ), |
| if (capabilities.tools case final tools?) |
| Keys.tools: Tools.fromMap( |
| Map<String, Object?>.from(tools as Map<String, Object?>) |
| ..remove(Keys.listChanged), |
| ), |
| }); |
| |
| @mustCallSuper |
| /// Handles the initialize request used by legacy MCP protocols. |
| /// |
| /// Most servers should override [initialize] to register features. Override |
| /// this method only to customize legacy protocol negotiation or its wire |
| /// response. |
| FutureOr<InitializeResult> initializeLegacy(InitializeRequest request) async { |
| // If we don't support or understand the version, set it to the latest one |
| // that we do support. If the client doesn't support that version they will |
| // terminate the connection. |
| final clientProtocolVersion = request.protocolVersion; |
| final negotiatedProtocolVersion = |
| clientProtocolVersion == null || !clientProtocolVersion.isSupported |
| ? ProtocolVersion.latestSupported |
| : clientProtocolVersion; |
| |
| late final ClientCapabilities clientCapabilities; |
| try { |
| clientCapabilities = request.capabilities; |
| // The getter validates what the client sent, so this error describes |
| // the request and not a bug on this side. |
| // ignore: avoid_catching_errors |
| } on ArgumentError { |
| throw RpcException.invalidParams( |
| 'The initialize request has invalid capabilities', |
| ); |
| } |
| |
| assert(!_initialized.isCompleted); |
| await initialize( |
| MCPServerInitialization( |
| protocolVersion: negotiatedProtocolVersion, |
| clientCapabilities: clientCapabilities, |
| clientInfo: request.clientInfo, |
| ), |
| ); |
| return InitializeResult( |
| protocolVersion: negotiatedProtocolVersion, |
| serverCapabilities: capabilities, |
| serverInfo: implementation, |
| instructions: instructions, |
| ); |
| } |
| |
| /// Completes [initialized]. |
| /// |
| /// Legacy clients call this handler after accepting our [InitializeResult]. |
| /// Request-scoped transports may call it without a notification after |
| /// [initialize] and transport-specific setup have completed. |
| /// |
| /// The server should not send a response. |
| @mustCallSuper |
| void handleInitialized([InitializedNotification? notification]) { |
| _initialized.complete(notification); |
| } |
| |
| /// Whether or not the connected client supports roots requests. |
| /// |
| /// Only safe to call after calling [initialize] on `super` since this |
| /// is based on the client capabilities. |
| bool get supportsRoots => clientCapabilities.supportsRoots; |
| |
| /// Whether or not the connected client supports sampling requests. |
| /// |
| /// Only safe to call after calling [initialize] on `super` since this |
| /// is based on the client capabilities. |
| bool get supportsSampling => clientCapabilities.supportsSampling; |
| } |
| |
| /// The error [_rejectRemovedMethod] throws when [protocolVersion] does not |
| /// have [method]. |
| /// |
| /// Only a revision with an `InputRequiredResult` can be pointed at it, and |
| /// `elicit` also lands here on the revisions before 2025-06-18 added |
| /// `elicitation/create`. |
| RpcException _removedMethod(String method, ProtocolVersion protocolVersion) { |
| final replacement = |
| protocolVersion >= ProtocolVersion.v2026_07_28 |
| ? ' Ask the client for input with an InputRequiredResult on ' |
| '${CallToolRequest.methodName}, ${GetPromptRequest.methodName}, ' |
| 'or ${ReadResourceRequest.methodName} instead.' |
| : ''; |
| return RpcException( |
| error_code.INTERNAL_ERROR, |
| 'Protocol version ${protocolVersion.versionString} does not have ' |
| '$method.$replacement', |
| ); |
| } |
| |
| /// Refuses to send [method] when [ProtocolVersion.methodIsValid] says |
| /// [protocolVersion] does not have it. |
| /// |
| /// Each sender calls this first, so a method the revision does not have fails |
| /// for that reason instead of for a missing client capability. |
| void _rejectRemovedMethod(String method, ProtocolVersion protocolVersion) { |
| if (protocolVersion.methodIsValid(method)) return; |
| throw _removedMethod(method, protocolVersion); |
| } |
| |
| /// The error a server must return when handling a request needs [capability], |
| /// which the client did not declare, carrying [required] under |
| /// `data.requiredCapabilities`. |
| RpcException _missingClientCapability( |
| String capability, |
| ClientCapabilities required, |
| ) => RpcException( |
| McpErrorCodes.missingRequiredClientCapability, |
| 'The client did not declare the $capability capability', |
| data: {Keys.requiredCapabilities: required}, |
| ); |
| |
| /// The refusal for a client that did not declare `roots`. |
| RpcException get _missingRoots => _missingClientCapability( |
| 'roots', |
| ClientCapabilities(roots: RootsCapabilities()), |
| ); |
| |
| /// The refusal for a client that did not declare `sampling`. |
| RpcException get _missingSampling => |
| _missingClientCapability('sampling', ClientCapabilities(sampling: {})); |
| |
| /// The refusal for a client whose `sampling` left out `tools`. |
| RpcException get _missingSamplingTools => _missingClientCapability( |
| 'sampling.tools', |
| ClientCapabilities(sampling: {Keys.tools: <String, Object?>{}}), |
| ); |
| |
| /// The refusal for a client that did not declare `elicitation.form`. |
| RpcException get _missingFormElicitation => _missingClientCapability( |
| 'elicitation.form', |
| ClientCapabilities(elicitation: ElicitationCapability(form: {})), |
| ); |
| |
| /// The refusal for a client that did not declare `elicitation.url`. |
| RpcException get _missingUrlElicitation => _missingClientCapability( |
| 'elicitation.url', |
| ClientCapabilities(elicitation: ElicitationCapability(url: {})), |
| ); |
| |
| /// The client capability reads a server makes, on the capabilities themselves |
| /// so that [handleRequestScopedMessage] can make them without a server. |
| /// |
| /// [MCPServer] and [ElicitationRequestSupport] expose and document all of |
| /// these but [supportsSamplingTools], which only the input request guard |
| /// needs. |
| extension on ClientCapabilities { |
| bool get supportsRoots => roots != null; |
| |
| bool get supportsSampling => sampling != null; |
| |
| /// Whether or not the client can answer a sampling request carrying `tools` |
| /// or `toolChoice`. |
| bool get supportsSamplingTools => sampling?[Keys.tools] != null; |
| |
| bool get supportsFormElicitation { |
| final elicitation = this.elicitation; |
| if (elicitation == null) return false; |
| return elicitation.form != null || |
| (elicitation as Map<String, Object?>).isEmpty; |
| } |
| |
| bool get supportsUrlElicitation => elicitation?.url != null; |
| } |