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.
@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).
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"))Full code is on the dev branch here: https://gitlab.com/michaelwhitford/mcpulse/-/tree/dev?ref_type=heads
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
Where to store the list of tools in the implementation? Should I instantiate it with an atom to store the tools?
tools would be provided by the library user, right?
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?
let me sketch this out while I have a minute :-)
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
I'll look at Component, thank you very much for the pointers, I will keep playing with this.
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?
Yeah naming is hard I am already thinking I need to expand the names to make it more clear.
I'm really interested in MCP and glad that somebody's tackling this :-)
it just seemed so weird to be calling tool/tool-call and tool/tool-list
how about list-tools and invoke-tool ?
always good to watch out for clojure.core name clashes when defining protocols - e.g. names like name list
always cause problems