Type-safe dynamic dispatch

If you’ve ever looked at Rust MCP server code and seen ToolRouter<Self>, you might have done a double-take. How can a struct contain itself? Isn’t that circular? Let’s break down this elegant pattern that’s actually simpler than it looks.

type-safe dynamic dispatch

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=a3b900cf43a7d9f728b8b78e98154bdc

The Pattern

struct Calculator {
    router: ToolRouter<Self>,
}

The key insight: it doesn’t contain itself. The ToolRouter<Calculator> doesn’t hold a Calculator instance – it holds a list of function pointers that know how to work with a Calculator.

What’s Actually Stored

Think of ToolRouter as a HashMap:

HashMap<String, fn(&Calculator, i32, i32) -> i32>

This stores tool names mapped to function pointers. When you register tools, you’re building a dispatch table:

handlers = {
    "add"      -> [pointer to Calculator::add],
    "multiply" -> [pointer to Calculator::multiply],
}

Why This Pattern Exists

Without the router, you’d write the same dispatch logic for every server:

match tool {
    "add" => self.add(a, b),
    "multiply" => self.multiply(a, b),
    // 50 more lines...
}

The router pattern provides type-safe dynamic dispatch. The generic <Self> ensures all registered functions have compatible signatures – the compiler won’t let you register a function that expects &WeatherServer into a ToolRouter<Calculator>.

The Magic Moment

When a tool is called:

calculator.router.call("add", &calculator, params)

The router looks up “add” in its HashMap, retrieves the function pointer, and calls it with the Calculator instance. It’s a phonebook of methods, not a container of objects.

Why Not Just Use Traits?

You could implement impl ToolRouter for Calculator, but then where does the HashMap live? Traits define behavior, not storage. With composition, the MCP library gives you a pre-built, tested router that you just drop in.

This pattern shines in framework code where multiple server types need identical dispatch mechanisms. It’s the difference between building an app and building a library that helps others build apps.

Bottom line: ToolRouter<Self> is a type-safe registry of function pointers – a reusable dispatch table that eliminates boilerplate while maintaining Rust’s compile-time guarantees.

TL;DR: <Self> is just generic type syntax. The “list of function pointers” part is specific to how ToolRouter works internally.

TL;DR

  • ToolRouter<T> is generic (library accepts multiple types)
  • ToolRouter<Self> is specific (you’re using it for ONE type: Calculator)
  • Self is just shorthand for “Calculator” when you’re inside the Calculator definition

We’re not making Calculator generic – we’re using a generic library with our specific type.

So… why go to this effort?

You can add new tools without modifying dispatch logic.

✅ Framework-level reuse

This is exactly how:

  • MCP servers
  • RPC frameworks
  • plugin systems

are structured internally.

✅ Static safety + dynamic behavior

  • Dynamic lookup by string
  • Static, compile-time checked function signatures