mcp_sse_server 0.2.1 copy "mcp_sse_server: ^0.2.1" to clipboard
mcp_sse_server: ^0.2.1 copied to clipboard

A Dart implementation of the Model Context Protocol server side: protocol types, JSON-RPC 2.0 plumbing, pluggable transports, and a high-level API for registering tools, resources, and prompts.

example/example.dart

// A small but complete MCP server: two tools, a resource, a resource
// template, and a prompt, served over Streamable HTTP with SSE.
//
// Run it with:
//
//     dart run example/example.dart
//
// then point an MCP client at the printed URL.
import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:mcp_sse_server/mcp_sse_server.dart';

/// The note store, one per session.
///
/// The handlers below read it live, so a note added through `add_note` is
/// visible to `resources/read` on the very next call — and one client's notes
/// are invisible to another's.
Map<String, String> _newNotes() => {
      'welcome': 'This note lives inside the example server.',
    };

/// The one place note URIs are built and parsed, so the two never drift.
final UriTemplate _noteUri = UriTemplate('note://{id}');

McpServer buildServer() {
  final notes = _newNotes();
  final server = McpServer(
    name: 'example-server',
    version: '0.1.0',
    title: 'Example MCP Server',
    instructions:
        'Use "roll_dice" for randomness and "add_note" to store text. '
        'Notes are readable as note://<id> resources.',
  );

  server.addTool(Tool(
    name: 'roll_dice',
    title: 'Roll dice',
    description: 'Rolls a number of dice with the given number of sides.',
    inputSchema: {
      'type': 'object',
      'properties': {
        'count': {'type': 'integer', 'minimum': 1, 'maximum': 100},
        'sides': {'type': 'integer', 'minimum': 2, 'default': 6},
      },
      'required': ['count'],
    },
    outputSchema: {
      'type': 'object',
      'properties': {
        'rolls': {
          'type': 'array',
          'items': {'type': 'integer'},
        },
        'total': {'type': 'integer'},
      },
      'required': ['rolls', 'total'],
    },
    annotations:
        const ToolAnnotations(readOnlyHint: true, openWorldHint: false),
    handler: (arguments, context) async {
      final count = (arguments['count'] as num).toInt();
      final sides = (arguments['sides'] as num?)?.toInt() ?? 6;
      final random = Random();
      final rolls = <int>[];
      // One notification per die would drown the client, so report in steps.
      final step = (count / 10).ceil();
      for (var i = 0; i < count; i++) {
        // Long loops should stay responsive to cancellation and report progress.
        context.throwIfCancelled();
        if ((i + 1) % step == 0 || i + 1 == count) {
          await context.reportProgress(i + 1, total: count, message: 'rolling');
        }
        rolls.add(random.nextInt(sides) + 1);
      }
      return CallToolResult.structured({
        'rolls': rolls,
        'total': rolls.fold<int>(0, (sum, roll) => sum + roll),
      });
    },
  ));

  server.addTool(Tool(
    name: 'add_note',
    title: 'Add a note',
    description: 'Stores a note and returns a link to it.',
    inputSchema: {
      'type': 'object',
      'properties': {
        'id': {'type': 'string'},
        'text': {'type': 'string'},
      },
      'required': ['id', 'text'],
    },
    annotations: const ToolAnnotations(idempotentHint: true),
    handler: (arguments, context) async {
      final id = arguments['id'] as String;
      final text = arguments['text'] as String;
      notes[id] = text;
      final uri = _noteUri.expand({'id': id});
      await context.log(LoggingLevel.info, 'stored note "$id"');
      // Tell the client its cached resource list is stale.
      await context.server.notifyResourceListChanged();
      await context.server.notifyResourceUpdated(uri);
      return CallToolResult(content: [
        TextContent('Saved note "$id".'),
        ResourceLink(
          uri: uri,
          name: id,
          mimeType: 'text/plain',
          size: utf8.encode(text).length,
        ),
      ]);
    },
  ));

  // A fixed URI the client can rely on. The reader looks the text up on every
  // call, so editing the note through `add_note` is reflected here.
  server.addResource(Resource(
    uri: _noteUri.expand({'id': 'welcome'}),
    name: 'welcome',
    title: 'Welcome note',
    mimeType: 'text/plain',
    reader: (uri, context) => ReadResourceResult([
      TextResourceContents(
          uri: uri, text: notes['welcome']!, mimeType: 'text/plain'),
    ]),
  ));

  server.addResourceTemplate(ResourceTemplate(
    uriTemplate: _noteUri.template,
    name: 'note',
    title: 'A stored note',
    mimeType: 'text/plain',
    // `welcome` is registered above as a fixed resource; listing it here too
    // would show the client the same URI twice.
    lister: () => [
      for (final entry in notes.entries)
        if (entry.key != 'welcome')
          ResourceInfo(
            uri: _noteUri.expand({'id': entry.key}),
            name: entry.key,
            mimeType: 'text/plain',
            size: utf8.encode(entry.value).length,
          ),
    ],
    completer: (request, context) => CompleteResult(notes.keys
        .where((id) => id.startsWith(request.argumentValue))
        .toList()),
    reader: (uri, variables, context) {
      final text = notes[variables['id']];
      if (text == null) throw McpError.resourceNotFound(uri);
      return ReadResourceResult([
        TextResourceContents(uri: uri, text: text, mimeType: 'text/plain'),
      ]);
    },
  ));

  server.addPrompt(Prompt(
    name: 'summarize_note',
    title: 'Summarize a note',
    description: 'Asks the model to summarize a stored note.',
    arguments: const [
      PromptArgument(
        name: 'id',
        description: 'The id of the note to summarize.',
        required: true,
      ),
    ],
    completer: (request, context) => CompleteResult(notes.keys
        .where((id) => id.startsWith(request.argumentValue))
        .toList()),
    handler: (arguments, context) {
      final id = arguments['id'] as String;
      final text = notes[id];
      if (text == null) {
        throw McpError.invalidParams('There is no note called "$id"');
      }
      return GetPromptResult(
        description: 'Summarize the note "$id"',
        messages: [
          PromptMessage.user('Summarize the following note in one sentence:'),
          PromptMessage(role: Role.user, content: TextContent(text)),
        ],
      );
    },
  ));

  return server;
}

Future<void> main(List<String> arguments) async {
  final port = arguments.isEmpty ? 8080 : int.parse(arguments.first);
  final http = await McpHttpServer.bind(
    onSession: buildServer,
    port: port,
    sessionIdleTimeout: const Duration(minutes: 30),
  );

  stderr.writeln('MCP server listening on ${http.endpoint}');
  stderr.writeln('Press Ctrl-C to stop.');

  await ProcessSignal.sigint.watch().first;
  await http.close(force: true);
}
0
likes
150
points
39
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dart implementation of the Model Context Protocol server side: protocol types, JSON-RPC 2.0 plumbing, pluggable transports, and a high-level API for registering tools, resources, and prompts.

Repository (GitHub)
View/report issues

Topics

#mcp #json-rpc #server #sse

License

MIT (license)

Dependencies

meta

More

Packages that depend on mcp_sse_server