architecture

michaelwhitford 2025-03-16T16:39:02.849179Z

I have a question about how to architect my system. I wrote some protocols and they seem to be working ok, but I want my MCPServer to be able to take an arbitrary number of MCPTool instances, and am not sure how to incorporate that. Some code in the thread.

Piotr Roterski 2025-03-17T11:16:18.160329Z

@michael819 I think it's a good moment to zoom out and consider whether tool in your system can be stateful or not, and if it can depend on other tools - answers to those questions determine what architecture is better here. Or rather, instead of asking if it can, you should be asking yourself whether it should - the design is both about allowing and disallowing flexibility. If the answer is yes and tools are stateful and can depend on each other, then you need all that lifecycle handling (start/stop) and dependency management (what starts and stops in what order). However, if the answer is no, then you don't need all of that (!) and a tool can even be a map (instead of a protocol) and be perfectly fine being managed by an atom in a (stateful) server. https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/ does not give a clear answer here, but my intuition is that tools =~ llm functions (and we know those are easier when pure/stateless) and servers are the abstraction for keeping state (including through resources but not only).

michaelwhitford 2025-03-16T16:39:21.065089Z

In server.clj:

(defprotocol MCPServer
  (version [this] "get supported specification version")
  (capabilities [this] "get server capabilities")
  (serverinfo [this] "get server info")
  (initialize [this message] "initialize a new session")
  (tools [this] "get vector of available tool methods")
  (tool [this message] "invoke specific tool method")
  (resources [this] "get vector of available resource methods")
  (resource [this message] "invoke specific resource method")
  (prompts [this] "get vector of available prompt methods")
  (prompt [this message] "invoke specific prompt method")
  (logging [this] "get vector of logging methods")
  (log [this message] "invoke specific log method")
  (experimental [this] "get vector of exerimental methods")
  (experiment [this message] "invoke an experimental method"))
In tool.clj:
(defprotocol MCPTool
  (version [this] "get tool version")
  (name [this] "get tool name")
  (description [this] "get tool description")
  (input [this] "get tool input schema")
  (output [this] "get tool output schema")
  (call [this message] "Call Tool"))

michaelwhitford 2025-03-16T16:59:59.434249Z

Full code is on the dev branch here: https://gitlab.com/michaelwhitford/mcpulse/-/tree/dev?ref_type=heads

lukasz 2025-03-16T17:35:35.671849Z

Just thinking out loud - looks like you need to add register-tool to the MPCServer protocol to inject tool instances. You could even look into not writing any code for this and piggy back on Component, which is built around protocols and allows for dependency injection

michaelwhitford 2025-03-16T17:42:59.101589Z

Where to store the list of tools in the implementation? Should I instantiate it with an atom to store the tools?

lukasz 2025-03-16T17:43:25.368579Z

tools would be provided by the library user, right?

michaelwhitford 2025-03-16T17:46:01.060619Z

Yes, but the implementation of the server would need to access the instance of the tool for other protocol methods. For instance register a tool, then the (tool ...) call would need to call the (call tool ...) method on the tool instance. Where to store the registered tools on the server implementation?

lukasz 2025-03-16T17:48:19.784779Z

let me sketch this out while I have a minute :-)

lukasz 2025-03-16T18:04:36.751939Z

something like this, no error checking etc https://gist.github.com/lukaszkorecki/4969508b1bce14128312486e68f9872f - the atom is also a hack, I'd probably use Component to manage this internally and not reinvent dependency injection and all that

michaelwhitford 2025-03-16T18:07:36.391529Z

I'll look at Component, thank you very much for the pointers, I will keep playing with this.

lukasz 2025-03-16T18:08:21.735009Z

one feedback I have is that names of protocol methods don't quite convey the intent - is tool meant to find and return a tool, or is it supposed to invoke it?

michaelwhitford 2025-03-16T18:08:49.582179Z

Yeah naming is hard I am already thinking I need to expand the names to make it more clear.

lukasz 2025-03-16T18:08:56.389669Z

I'm really interested in MCP and glad that somebody's tackling this :-)

michaelwhitford 2025-03-16T18:09:15.859779Z

it just seemed so weird to be calling tool/tool-call and tool/tool-list

lukasz 2025-03-16T18:09:43.752039Z

how about list-tools and invoke-tool ?

lukasz 2025-03-16T18:11:03.905859Z

always good to watch out for clojure.core name clashes when defining protocols - e.g. names like name list always cause problems