MCPInstall
mcp_server logo

mcp_server

by app-appplayer 9 stars app-appplayer/mcp_server

MCP Server is a Dart plugin for creating Model Context Protocol servers for LLM applications.

READMEmain

MCP Server

A Dart plugin for implementing Model Context Protocol (MCP) servers. This plugin allows Flutter applications to expose data, functionality, and interaction patterns to Large Language Model (LLM) applications in a standardized way.

Features

  • Every MCP revision, 2024-11-05 through 2026-07-28, plus the 2026-07-28 tasks extension — see Protocol Versions below
  • Core primitives — Resources, Tools, Prompts, plus Completions (2025-06-18+)
  • Server-initiated requests — requestClientSampling, requestClientRoots, requestClientElicitation with response routing and timeout
  • Authorization — every HTTP request is decided before dispatch (401 / 403 with WWW-Authenticate), Protected Resource Metadata (RFC 9728), audience and scope checks
  • Structured tool output (2025-06-18) — outputSchema + structuredContent + resource_link content type
  • Icons + sampling tool calls (2025-11-25) — Tool.icons / Resource.icons / Prompt.icons, plus the new tools / toolChoice fields on sampling/createMessage
  • List-changed notifications — spec-canonical notifications/{tools,resources,prompts}/list_changed
  • Progress + cancellation — outbound notifyProgress; inbound notifications/cancelled and notifications/progress listeners
  • Session events — onConnect / onDisconnect streams, per-session protocol version + capabilities
  • Caching hints (2026-07-28) — ttlMs / cacheScope on discover, lists and reads (cacheTtl, publicLists)
  • Health + metrics — getHealth(), named timers, custom counters
  • Transports — stdio, SSE (legacy), Streamable HTTP (canonical 2025-03-26+)
  • Platforms: wherever dart:io is available (Android, iOS, Linux, Windows, macOS)

Protocol Versions

Implements every revision of the Model Context Protocol; each client gets the one it opens with.

VersionNotes
2024-11-05Original; HTTP+SSE transport
2025-03-26Streamable HTTP, authorization, JSON-RPC batches
2025-06-18Elicitation, structured tool output, resource links, MCP-Protocol-Version header; batches removed
2025-11-25Icons, sampling with tools, URL elicitation, tasks
2026-07-28Stateless: no handshake, server/discover, per-request _meta, subscriptions/listen, multi round-trip requests, caching hints; tasks extension

initialize negotiates among McpProtocol.supportedVersions (2025-11-25 and earlier). 2026-07-28 has no handshake: on stdio a request naming its version in _meta is served in that revision; over Streamable HTTP it is served on the same endpoint as the session revisions. A server that keeps per-session state sets enableStateless: false (TransportConfig.streamableHttp or StreamableHttpServerConfig), and 2026-07-28 clients fall back to initialize.

Coming from 2.x: MIGRATION-3.0.md.

Tasks

A tool's execution.taskSupport (optional, required, forbidden) decides whether a call runs as a task.

  • 2025-11-25 (requestor-driven): declare ServerCapabilities(tasks: {'list': {}, 'cancel': {}, 'requests': {'tools': {'call': {}}}}). A call carrying task is answered with CreateTaskResult and runs in the background; tasks/get, tasks/result (waits for the end), tasks/list and tasks/cancel follow it. The server can also ask a client to run sampling/createMessage / elicitation/create as a task (requestClientTask, clientTaskResult, …).
  • 2026-07-28 (tasks extension, server-directed): declare extensions: {tasksExtensionId: {}}. A call from a client that declared the extension runs as a task when the tool allows it; tasks/get, tasks/update and tasks/cancel follow it.
  • Inside a task's work, server.currentTask.requestInput({...}) asks the client for input (a tagged request on 2025-11-25, inputRequests on the extension).

Tasks belong to the authenticated user, or else (2025-11-25) to the session that created them; nobody else can read, list or cancel them. Where there is neither — the 2026-07-28 path without authentication — a task is protected only by its id (128 random bits); use authentication when task results are sensitive. Limits: McpTaskStore(maxTtl:, defaultTtl:, maxTasksPerOwner:).

JSON Schema

JsonSchemaValidator (exported by this package) evaluates the schemas MCP carries — tool inputSchema / outputSchema, elicitation requestedSchema.

  • Dialects: 2020-12 (used when a schema has no $schema), 2019-09, draft-07, draft-06, draft-04. A schema naming another dialect is refused with JsonSchemaException.
  • References resolve inside the schema, against documents passed in registry, and against the bundled meta-schemas. Nothing is fetched over the network; a reference that does not resolve rejects the schema.
  • Evaluation is bounded (JsonSchemaLimits: depth, subschema count, evaluations, time).
  • format is an annotation unless assertFormat: true, which checks email, uri, date and date-time.
  • Conformance: the official JSON-Schema-Test-Suite (required tests of all five dialects, plus the optional tests for the features above).

Getting Started

Basic Usage

import 'package:mcp_server/mcp_server.dart';

void main() {
  // Create a server with simple boolean capabilities
  final server = Server(
    name: 'Example Server',
    version: '1.0.0',
    capabilities: ServerCapabilities.simple(
      tools: true,
      resources: true,
      prompts: true,
    ),
  );

  // Add a simple calculator tool
  server.addTool(
    name: 'calculator',
    description: 'Perform basic calculations',
    inputSchema: {
      'type': 'object',
      'properties': {
        'operation': {
          'type': 'string',
          'enum': ['add', 'subtract', 'multiply', 'divide'],
          'description': 'Mathematical operation to perform'
        },
        'a': {
          'type': 'number',
          'description': 'First operand'
        },
        'b': {
          'type': 'number',
          'description': 'Second operand'
        }
      },
      'required': ['operation', 'a', 'b']
    },
    handler: (arguments) async {
      final operation = arguments['operation'] as String;
      final a = (arguments['a'] is int) ? (arguments['a'] as int).toDouble() : arguments['a'] as double;
      final b = (arguments['b'] is int) ? (arguments['b'] as int).toDouble() : arguments['b'] as double;
      
      double result;
      switch (operation) {
        case 'add':
          result = a + b;
          break;
        case 'subtract':
          result = a - b;
          break;
        case 'multiply':
          result = a * b;
          break;
        case 'divide':
          if (b == 0) {
            return CallToolResult(
              content: [TextContent(text: 'Division by zero error')],
              isError: true,
            );
          }
          result = a / b;
          break;
        default:
          return CallToolResult(
            content: [TextContent(text: 'Unknown operation: $operation')],
            isError: true,
          );
      }
      
      return CallToolResult(content: [TextContent(text: 'Result: $result')]);
    },
  );

  // Add a resource
  server.addResource(
    uri: 'time://current',
    name: 'Current Time',
    description: 'Get the current date and time',
    mimeType: 'text/plain',
    handler: (uri, params) async {
      final now = DateTime.now().toString();
      
      return ReadResourceResult(
        contents: [
          TextResourceContents(
            uri: uri,
            text: now,
            mimeType: 'text/plain',
          ),
        ],
      );
    },
  );

  // Add a template prompt
  server.addPrompt(
    name: 'greeting',
    description: 'Generate a customized greeting',
    arguments: [
      PromptArgument(
        name: 'name',
        description: 'Name to greet',
        required: true,
      ),
      PromptArgument(
        name: 'formal',
        description: 'Whether to use formal greeting style',
        required: false,
      ),
    ],
    handler: (arguments) async {
      final name = arguments['name'] as String;
      final formal = arguments['formal'] as bool? ?? false;

      final String systemPrompt = formal
          ? 'You are a formal assistant. Address the user with respect and formality.'
          : 'You are a friendly assistant. Be warm and casual in your tone.';

      // Prompt messages are `user` or `assistant`; instructions go in the
      // text (the specification has no system role here).
      final messages = [
        PromptMessage(
          role: Role.user,
          content: TextContent(text: '$systemPrompt\n\nPlease greet $name'),
        ),
      ];

      return GetPromptResult(
        description: 'A ${formal ? 'formal' : 'casual'} greeting for $name',
        messages: messages,
      );
    },
  );

  // Connect to transport - old way (still supported)
  final transportResult = McpServer.createStdioTransport();
  final transport = transportResult.get();
  server.connect(transport);
}

// NEW: Simplified unified API (recommended)
void simplifiedMain() async {
  final serverResult = await McpServer.createAndStart(
    config: McpServer.simpleConfig(
      name: 'Example Server',
      version: '1.0.0',
    ),
    transportConfig: TransportConfig.stdio(),
  );

  await serverResult.fold(
    (server) async {
      // Add tools, resources, prompts as shown above
      
      // Server is already running
      await Future.delayed(const Duration(hours: 24));
    },
    (error) => print('Server failed: $error'),
  );
}

Transport Configuration

The MCP Server now supports unified transport configuration:

// STDIO Transport
TransportConfig.stdio()

// SSE Transport  
TransportConfig.sse(
  host: 'localhost',
  port: 8080,
  endpoint: '/sse',
  authToken: 'optional-token',
)

// Streamable HTTP Transport - SSE Streaming Mode (default)
TransportConfig.streamableHttp(
  host: 'localhost', 
  port: 8081,
  endpoint: '/mcp',
  isJsonResponseEnabled: false, // SSE streaming mode (default)
)

// Streamable HTTP Transport - JSON Response Mode
TransportConfig.streamableHttp(
  host: 'localhost', 
  port: 8081,
  endpoint: '/mcp',
  isJsonResponseEnabled: true, // JSON response mode
)

Core Concepts

Server Capabilities

The MCP Server supports two ways to configure capabilities:

Simple Boolean Configuration

For basic usage and testing, use ServerCapabilities.simple():

final server = Server(
  name: 'Simple Server',
  version: '1.0.0',
  capabilities: ServerCapabilities.simple(
    tools: true,
    toolsListChanged: true,
    resources: true,
    resourcesSubscribe: true,
    resourcesListChanged: true,
    prompts: true,
    promptsListChanged: true,
    logging: true,
    completions: true,
  ),
);

Object-Based Configuration

The schema's capability objects, plus tasks and extensions:

final server = Server(
  name: 'Advanced Server',
  version: '1.0.0',
  capabilities: ServerCapabilities(
    tools: ToolsCapability(listChanged: true),
    resources: ResourcesCapability(listChanged: true, subscribe: true),
    prompts: PromptsCapability(listChanged: true),
    logging: LoggingCapability(),
    completions: CompletionsCapability(),
    // 2025-11-25 tasks; 2026-07-28 tasks extension
    tasks: {'list': {}, 'cancel': {}, 'requests': {'tools': {'call': {}}}},
    extensions: {tasksExtensionId: {}},
  ),
);

Sampling, roots and elicitation are client capabilities: the server asks for them (requestClientSampling, requestClientRoots, requestClientElicitation, or McpMrtr input requests on 2026-07-28) when the client declared them.

Server

The Server class is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

final server = Server(
  name: 'My App',
  version: '1.0.0',
  capabilities: ServerCapabilities.simple(
    tools: true,
    resources: true,
    prompts: true,
  ),
);

Resources

Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:

// Static resource
server.addResource(
  uri: 'config://app',
  name: 'App Configuration',
  description: 'Application configuration data',
  mimeType: 'text/plain',
  handler: (uri, params) async {
    final configData = "app_name=MyApp\nversion=1.0.0\ndebug=false";
    
    return ReadResourceResult(
      contents: [
        TextResourceContents(
          uri: uri,
          text: configData,
          mimeType: 'text/plain',
        ),
      ],
    );
  },
);

// Resource with URI template
server.addResource(
  uri: 'file://{path}',
  name: 'File Resource',
  description: 'Access files on the system',
  mimeType: 'application/octet-stream',
  uriTemplate: {
    'type': 'object',
    'properties': {
      'path': {
        'type': 'string',
        'description': 'Path to the file'
      }
    }
  },
  handler: (uri, params) async {
    // Extract path and read file content
    final path = params['path'] ?? uri.substring('file://'.length);
    final content = await File(path).readAsString();
    
    return ReadResourceResult(
      contents: [
        TextResourceContents(
          uri: uri,
          text: content,
          mimeType: 'text/plain',
        ),
      ],
    );
  },
);

Tools

Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:

server.addTool(
  name: 'currentDateTime',
  description: 'Get the current date and time',
  inputSchema: {
    'type': 'object',
    'properties': {
      'format': {
        'type': 'string',
        'description': 'Output format (full, date, time)',
        'default': 'full'
      }
    },
    'required': []
  },
  handler: (args) async {
    final format = args['format'] as String? ?? 'full';
    final now = DateTime.now();
    
    String result;
    switch (format) {
      case 'date':
        result = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
        break;
      case 'time':
        result = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
        break;
      case 'full':
      default:
        result = now.toIso8601String();
        break;
    }
    
    return CallToolResult(content: [TextContent(text: result)]);
  },
);

Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:

server.addPrompt(
  name: 'codeReview',
  description: 'Generate a code review for a code snippet',
  arguments: [
    PromptArgument(
      name: 'code',
      description: 'Code to review',
      required: true,
    ),
    PromptArgument(
      name: 'language',
      description: 'Programming language of the code',
      required: true,
    ),
  ],
  handler: (args) async {
    final code = args['code'] as String;
    final language = args['language'] as String;

    final systemPrompt = '''
You are an expert code reviewer. Review the provided code with these guidelines:
1. Identify potential bugs or issues
2. Suggest optimizations for performance or readability
3. Highlight good practices used in the code
4. Provide constructive feedback for improvements
Be specific in your feedback and provide code examples when suggesting changes.
''';

    final messages = [
      PromptMessage(
        role: Role.user,
        content: TextContent(
            text: '$systemPrompt\nPlease review this $language code:\n\n```$language\n$code\n```'),
      ),
    ];

    return GetPromptResult(
      description: 'Code review for $language code',
      messages: messages,
    );
  },
);

Session Management

The MCP Server provides a powerful stream-based system for monitoring client session events:

// Create a logger
final logger = Logger('mcp_server.sessions');

// Listen for client connections
server.onConnect.listen((session) {
  logger.info('Client connected: ${session.id}');
  // Initialize session resources
});

// Listen for client disconnections
server.onDisconnect.listen((session) {
  logger.info('Client disconnected: ${session.id}');
  // Clean up session resources
});

The ClientSession object contains useful information:

  • Session ID
  • Connection timestamp
  • Protocol version
  • Client capabilities
  • Client root directories

Best Practices for Session Management

  1. Always handle both events: Listen for both connection and disconnection events to maintain session integrity.

  2. Add error handling to your stream subscriptions:

    server.onConnect.listen(
      (session) {
        // Normal event handling
      },
      onError: (error) {
        logger.severe('Error in connection stream: $error');
      },
    );
    
  3. Cancel subscriptions when no longer needed:

    final subscription = server.onConnect.listen((session) { /* ... */ });
    
    // Later, when done:
    subscription.cancel();
    
  4. Use session events for state management: Maintain application state based on session events.

  5. Track metrics for monitoring: Count sessions, durations, and other metrics.

Transport Layers

Standard I/O

For command-line tools and direct integrations:

final transportResult = McpServer.createStdioTransport();
final transport = transportResult.get();
server.connect(transport);

Server-Sent Events (SSE)

For HTTP-based communication:

final sseConfig = SseServerConfig(
  endpoint: '/sse',
  messagesEndpoint: '/messages',
  port: 8080,
);
final transportResult = McpServer.createSseTransport(sseConfig);
final transport = transportResult.get();
server.connect(transport);

Streamable HTTP Transport

The Streamable HTTP transport supports two response modes:

SSE Streaming Mode (Default)

For real-time streaming of responses:

final serverResult = await McpServer.createAndStart(
  config: McpServer.simpleConfig(
    name: 'My Server',
    version: '1.0.0',
  ),
  transportConfig: TransportConfig.streamableHttp(
    host: 'localhost',
    port: 8081,
    endpoint: '/mcp',
    isJsonResponseEnabled: false, // SSE streaming mode
  ),
);

JSON Response Mode

For single JSON responses (simpler but no streaming):

final serverResult = await McpServer.createAndStart(
  config: McpServer.simpleConfig(
    name: 'My Server',
    version: '1.0.0',
  ),
  transportConfig: TransportConfig.streamableHttp(
    host: 'localhost',
    port: 8081,
    endpoint: '/mcp',
    isJsonResponseEnabled: true, // JSON response mode
  ),
);

Important Notes:

  • The response mode is fixed at server startup and cannot be changed dynamically
  • Clients must include both application/json and text/event-stream in their Accept headers regardless of the server's mode
  • SSE mode allows streaming multiple responses, while JSON mode returns a single response

Logging

The package includes a comprehensive logging utility with customizable levels and formatting:

// Create a named logger
final logger = Logger('mcp_server.example');

// Log at different levels
logger.severe('Connection failed: unable to connect to transport');
logger.warning('Resource cache is approaching capacity limit');
logger.info('Server started successfully on port 8080');
logger.fine('Session initialized with protocol version');

// Send structured logs to MCP client
server.sendLog(
  McpLogLevel.info, 
  'Operation registered',
  data: {
    'operationId': 'op-123',
    'sessionId': 'session-456',
    'type': 'tool:calculator',
    'timestamp': DateTime.now().toIso8601String(),
  }
);

MCP Primitives

The MCP protocol defines three core primitives that servers can implement:

PrimitiveControlDescriptionExample Use
PromptsUser-controlledInteractive templates invoked by user choiceSlash commands, menu options
ResourcesApplication-controlledContextual data managed by the client applicationFile contents, API responses
ToolsModel-controlledFunctions exposed to the LLM to take actionsAPI calls, data updates

Additional Features

Progress and Cancellation

A client that wants progress sends a progressToken; the server reports against it (rate-limited by progressMinInterval) until the call ends:

server.addToolWithProgress(
  'longRunningOperation',
  'Perform a long-running operation with progress reporting',
  {'type': 'object'},
  (args, {onProgress}) async {
    for (var i = 0; i < 10; i++) {
      // The client cancelled: stop (no response is sent for a cancelled call).
      if (server.currentRequestCancelled) break;
      onProgress?.call(i / 10, 'Step ${i + 1} of 10');
      await Future<void>.delayed(const Duration(seconds: 1));
    }
    return CallToolResult(content: [TextContent(text: 'Done')]);
  },
);

Resource Updates

// To the sessions subscribed to the URI (and, on 2026-07-28, the
// subscriptions/listen streams watching it)
server.notifyResourceUpdated('config://app');

Caching Hints (2026-07-28)

server/discover, the four list methods and resources/read carry ttlMs (Server(cacheTtl: ...)) and cacheScope (publicLists), so clients can reuse results. cacheResource / getCachedResource / invalidateCache are a separate server-side cache an application may use in its handlers.

Authorization (HTTP)

server.enableAuthentication(
  myTokenValidator, // TokenValidator: decides each token, reports its scopes and audience
  requiredScopes: (method, params) =>
      method == 'tools/call' && params?['name'] == 'delete' ? ['files:write'] : const [],
);
server.configureProtectedResource(
  resource: 'https://api.example.com/mcp',
  authorizationServers: ['https://auth.example.com'],
  scopesSupported: ['files:read', 'files:write'],
);

Every HTTP request is checked before it is dispatched: no token → 401, an invalid token or one issued for another resource → 401 invalid_token, missing scopes → 403 insufficient_scope; each answer carries WWW-Authenticate with resource_metadata. Handlers see the caller as McpCaller.current. The bundled OAuthTokenValidator (RFC 7662 introspection) refuses a token whose introspection names no audience.

Health Monitoring

final health = server.getHealth();
Logger('mcp_server.health').info('Running: ${health.isRunning}, '
    'sessions: ${health.connectedSessions}, uptime: ${health.uptime}, '
    'tools: ${health.registeredTools}, resources: ${health.registeredResources}');

server.incrementMetric('api_calls');
server.startTimer('operation_duration');
// ... perform operation
server.stopTimer('operation_duration');

Error Handling

Tool failures are results, not protocol errors: a handler that throws is answered with an isError result, and arguments that break inputSchema are too (2025-11-25 and later; -32602 before). An unknown tool, prompt or resource is a protocol error with the code the negotiated revision assigns. A handler can also return the error result itself:

try {
  // Attempt an operation
  final result = await performOperation();
  return CallToolResult(content: [TextContent(text: result)]);
} catch (e) {
  // Return an error result
  return CallToolResult(
    content: [TextContent(text: 'Operation failed: ${e.toString()}')],
    isError: true,
  );
}

Examples

Check out the example directory for a complete sample application.

Resources

Issues and Feedback

Please file any issues, bugs, or feature requests in our issue tracker.

License

This project is licensed under the MIT License - see the LICENSE file for details.

More Developer Tools MCP servers

Similar servers from the same category.

Sourcebot

sourcebot-dev

Enables code search across multiple repository hosts including GitHub, GitLab, Gitea, Gerrit, and Bitbucket with advanced filtering options for exploring large codebases through natural language queries.

searchgit

CodeAlive

codealive-ai

Provides a bridge to CodeAlive's platform for semantic code search, repository exploration, and context-aware chat completions that leverage deep understanding of entire codebases including documentation and dependencies.

search

Provides a secure, sandboxed environment for executing Python, Node.js, and shell commands in ephemeral containers with file management capabilities for code testing, visualization, and data analysis.

filesystemdatabase

NovaCV

lightbaby

Integrates with NovaCV API to generate, analyze, and convert resumes in various formats without dealing with complex document formatting

PDFSupport+5