About this talk
This talk covers TMCP, a new framework for building MCP servers in TypeScript. The speaker, Paolo Ricciuti, explains the importance of the Model Context Protocol (MCP), which allows clients to communicate with servers using JSON-RPC. He discusses the design and architecture of an MCP server, including how to integrate it with frameworks like SvelteKit. Ricciuti also highlights the challenges faced when using existing SDKs and how he resolved these by creating TMCP, which simplifies validation and server management while maintaining a small package size. He emphasizes the benefits of TMCP over existing solutions, showcasing its scalability and efficiency in handling requests.
Full transcript
[music] >> All right. Welcome everyone to my talk, which is called "TMCP, a new way to build MCP servers in TypeScript." Uh before we begin, I think a small presentation is due, and I brought my assistant with me to help me with that. So, let's see. I can say uh Can you help me present myself? I absolutely can help you present yourself. You are Paolo Ricciuti. That
is correct. Uh where do I come from? You come from Campobasso, Italy. That is Uh let's see. Uh where can do people find me online? People can find you on Blue Sky as paolo.ricciuti.me. That is correct. Uh what about GitHub? You're absolutely right. People can find you on GitHub on GitHub as Paolo Ricciuti. Uh how would you introduce me to this audience? And let's see. Uh I
would definitely mention your involvement with the Svelte project. You are a Svelte ambassador and a Svelte maintainer. That is correct. Uh where do I work? You lead the Svelte team at Main Matter. Uh Main Matter is an engineering consultancy that focuses on Svelte, Rust, and Ember. And you partner with clients to solve their toughest challenges or with adopting Svelte, depending on where they are. And that's absolutely
true. Like, that's one of the best part of my job. I get to uh work with Svelte, but also to teach Svelte in my day-to-day job, because we do what's called team augmentation, which means that we don't just execute on your feature, we join your team, we help you hire, we teach you best practice, and like that's what I like. And remember guys, always thank your friendly
overlord. So, thank you. And now, let's talk about Oh, blood to health, let me remove this conversation. I remember that. What? Ah, it broke itself. It's fine. It's fine. Okay, so before we begin, I think it's good to talk a bit about MCP. MCP stands for model context protocol, which is an acronym. And the M in MCP is the same M that we have in LLM, which
is model. And the reason why we need something like MCP is because once a model has been trained, it's kind of frozen in time, and you can't add more information to it. Well, you can, and you can by adding context to the model. So, for example, if you want a model to talk like to work in your code base, you can add the context of your code
base, and you can ask some question, and by prompting, you are adding context to the model. And that's the the C of the of the acronym. The P is something much much older, is the same P which we find in HTTP, and it's protocol. And the protocol is just something like it's a contract between two parties, a server and a client generally. And in the HTTP version,
we have a client which is generally a browser, and a server which is like our HTTP server, what we build, and they basically communicate with each other with a known language. And the fact that they have this shared language allowed them to communicate. And that's basically the same with MCP. So, we have MCP clients and MCP servers. So, MCP clients are stuff like Claude Code, Open Code,
Pi, Goose, but there are I don't know, hundreds of them. And basically those are those application that you generally use to communicate with an LLM. It's not the LLM itself, it's just the harness around the LLM. And the harness basically provide a system prompt to the LLM, but they also can connect to the MCP servers that you specify. And an MCP server on the other hand is
just a process that is listening and the the protocol specify that they communicate using JSON-RPC, which is JavaScript Notation Object Remote Procedure Call. And at the end of the day, the point is that the MCP clients can communicate with the MCP server sending those payloads. And the MCP server can either be a process that runs on your computer and they communicate uh through with standard in and
standard out, or they can even be an HTTP server and they communicate over HTTP. The only important bit is that this MCP server is re- is receiving these uh JSON-RPC payload and then it's doing something. Now, what it's doing? Well, uh there are I don't know. At this point, I think there are thousands of MCP servers. And each MCP server can do something different. For example, the
Figma MCP server can send back the information about your design and your design token to the LLM. Uh but there are also stuff like the Linear MCP server. And with the Linear MCP server, you can communicate with your LLM and then, for example, close an issue. So, like, the MCP server can act on the real world and actually close an issue for you, for example. There are
other MCP server like the Vercel MCP server that uh for example, listen for the code that is being generated from your LLM. They we we static analysis on the code and we send back suggestions so that the LLM can fix their code in the proper way. So, the point is that an MCP client send information to the MCP server and the MCP server send back the information
to the MCP client. And you can do some pretty funky stuff with MCP. For example, this slide deck is an MCP server. So, I can say can you go to the next slide? And if everything goes correctly, hopefully, ah, it didn't work. Ah, it's fine. This is what you would have seen. I forgot to refresh the page and I lost my mouse now. This is what if
it worked. I've lost my mouse once again. Okay. You would have seen this. It's fine. Uh I'm not going to use this because obviously it doesn't work, but uh the point is as a Svelte maintainer, uh whenever I approach a new technology, I try to use it with Svelte. So, in this case, uh using uh MC like approaching to MCP, I try to say, "Okay, how can
I uh build an MCP server with SvelteKit, for example?" And when I'm doing I did it doing some research, basically everyone and their mother was using at model context protocol {slash} SDK. For a good reason. That's the SDK that basically the same maintainers of the protocol itself are building for TypeScript. And so, I started exploring this this um this SDK. Now, before we continue talking about the
SDK and what else, uh I want to do a small detour and show you Uh this is how you can build an MCP and the pointer in Next.js. So, like you have this file app/mcp/root.ts and you have to export a function with an HTTP verb and that function receive a request as input and you have to return a response as output. This is how you do it
in a SolidStart. Very similar. You get You export a function with an HTTP verb. You get a request. You return a response. This is how you do it in Bun. So, you can create a a quick server just by doing bun.serve. You export You pass a fetch function that receive a request, return a response. Uh Cloudflare worker exports a fetch Deno, it's a function that receive a
request and return a response. Finally, this is how you do it in SvelteKit, but I think at this point it's pretty clear. All right, what you get is that you receive like in modern front-end framework, you get a request as input and you receive a response. Now, this was one of the first examples that I found out while I was researching the official SDK. And you will
see a request and response here, but those are not just there. Those are actually imported from Express. And unless you've been living under a rock, you know that Express is a way to build an HTTP server in TypeScript uh in in JavaScript in general in Node.js. And the point is that uh those request and response are slightly different from the one that I showed you before because
those are Node request and Node response. And you can see the difference because we are not creating a new response with a status of of 200, for example, but we are actually invoking like we are receiving a request and a response from the framework and then we are invoking methods on top of the response to set the status to set the JSON value and stuff like that.
And that's why in the official SDK when you create a transport you need to pass the request and the response in because the the transport needs to be able to invoke those methods on the response. For some reason they also accept the request.body despite the fact that they accept the request and they could just read the body from there but that's a different story. So the point
is that to to use the official SDK with a modern front-end ah, you can't really do it. So there are ways around this obviously. For example, the remix guys created this node fetch server which is a way that expose like it's a library that expose some low-level API like create request and send response that can basically convert between node request, node response from to web request and
web response. Or for example, Vercel created this MCP handler library which basically wrap your MCP server and create a fake response that can send down into the transport and then collect all the methods that are invoked and craft an actual web response ah, out of this. Okay, this was a small detour. Another I don't know if you know this guy. This guy is called Valibot and if
you don't know him, you might know his cousin, Zod. Zod is a validation library and it's basically a way when you receive some untrusted data, for example, data generated by an LLM you can create this schema with this kind of object like the Z object and once you parse that data with this schema you are sure that if you go to the next line, that data will
be the right shape. So, like it's a validation library, and this also helps TypeScript to know that there is an ID property, a name property, and stuff like that. Valibot is very similar to this. Like it's a bit more verbose, but it's way more composable, and most importantly, it's way smaller. Zod averages around like 10 kilobytes, so you can't really send it to the client. Um Valibot,
in the smallest form, because it's very tree shakeable, uh it's like 600 bytes. So, like it's very, very small. And that's why it's my preferred library. Now, the creator of Valibot and the creator of Zod and the creator of Arc Type, which is another validation library, they came together and created this thing which is called Standard Schema. And basically, they all agreed to um adhere to a
type so that uh if a library author want to use a validation library, they can use Standard Schema, and so they can say, "Whatever validation library that adhere to this Standard Schema, I can accept it as the first argument, and then I can use it to validate the data." So, as you can see like in the second part, uh you can the same function, you can pass
a Zod schema, a Valibot schema, or a an Arc Type schema. Now, I was fully expecting uh people like the like the official SDK to support this, and so I tried. And it failed. And so I said, "Okay, well, they are not using that. I need to use Zod." So, I installed Zod, and it still failed. And so I got really confused because like what what are
they even using? And so generally, when I when I'm confused and when the the project is an open source project, I start to dig into the code uh to see what's going on. And this is at the time at to be fair, things right now are getting way way better, but at the time this was the package.json of their library. Now, if you're not familiar with JavaScript
dependency, there is a hard dependency on Zod version 3, and that's why I was getting the error because I was using Zod version 4. And the reason why they have that dependency on Zod V3 is because they were also using Zod to JSON schema, which is a library to convert between your Zod schema to JSON schema, which is what the model context protocol prescribed. So, you have
to use JSON schema to communicate, and they were using this library that was not supporting Zod before. But, there's also something way way worse. If I'm using this library with Vest Kit, I'm not using Express, but I'm still installing Express because they have a dependency on Express. Why do they have a dependency on Express? The reason is that they package everything in the same package. So, once
you install the package, you get the the class to create an MCP server, but you also get the class to create a transport, and to create an HTTP transport, and an STDIO transport, and a client even, and all the rest of the libraries are all libraries that I don't need, that Express need, and I'm not using Express, but yet I'm installing each one of these packages in
my in my library. And that struck me in the wrong I started experimenting a bit. And also, this was not just the only thing because as I've said, uh they actually got way way better under this front. So, like now they are actually thinking of splitting the packages and stuff like that. But, there are other things that I really didn't like about the library. For example, this
is another example. And you see that if you want to have a session associated with your MCP server, you have to create you have to keep the sessions in memory, first thing first, and then you have to create a new MCP server for each session and connected to the to a new transport. Uh and I really didn't like this. But then also the API, for example, this
is how you register a resource uh and how you register a prompt with the official SDK, and you see that like this resource you can register it either as a string, so I you can like the second argument can be a string, or this class that you need to import, which is called the resource template, which is if you have like a dynamic resource, right? And this
resource template then has a list and a complete parameters, which is not type safe. So, if I mistype and instead of writing param, I write params, uh I will get no error back and it will just not work. And also, this is not very discoverable, right? So, like I have to know about the resource template to be able to create a resource template. Uh TypeScript cannot help
me Uh also, if I want to do the same thing like something similar using a complete in the prompt part, I have this arc schema, and then I have this param, and then I have this completable function, which is completely different from the rest, which accept a schema, and then I can complete that single parameter. So, the API was not the best. Also, if I wanted to
send an elicitation, this is where an MCP server can request more information from the MCP client, you actually have to write the JSON schema by hand. So, in all the rest of the API, you can use Zod, your validation library, to define your schemas. In this case, you have to write literally JSON schema. >> [snorts] >> again, all of these things kind of struck me in the
wrong way. And so, I started experimenting a bit and I said, "Okay, you know what? Like, at the end of the day, I like an MCP server is just a JSON-RPC exchanger, right? So, I installed this very simple library, which is very small, which is called JSON-RPC 2.0. And then, you can create this JSON-RPC server. You can add methods to it. For example, I can add the
initialize method. And this is just defined by the protocol itself. I can add the ping method. And then, what I can do is that I can use bun very quickly, and I can say, "Okay, if the request is a JSON-RPC request, I will invoke the JSON-RPC server. And if the JSON-RPC server respond, I can just respond back." And this is a functional MCP server without any dependency,
if not for this But, obviously, this doesn't do anything. So, can we make this do something? And yes, you can do stuff like this. You can add the tool list method, which is again defined by the protocol. And you can just return a list of tools. In this case, a test tool. And now, when I connect to this MCP server with from Cloud Cool, this will actually
say that there is a Obviously, if you try to invoke that tool, it doesn't do anything, because we don't have that method finalized. So, we can, for example, add the tool to an array, and then add this handler here and then we just return the tools like this and then we add the tools call which again is defined by the protocol. We search the tool by name
and then if we find it we invoke the we invoke the handler. The point is that experimenting experimenting experimenting I went from me just being curious about what this MCP thingy was to me writing a brand new library and this brand new library is called TMCP like it has the goal of like deep load a bit the official SDK. So let's actually see a bit of the
code. So this is how you do it like it's a very minimal MCP server. You import MCP server from TMCP. You pass the name description and the version you pass the capabilities and then this MCP server is just a JSON RPC server. So like this does not have any connection. You can technically just invoke server.receive and wire everything like you like I did with the bundle serve
command or you can use we will see later some transport but this will actually give you an error because you still need to convert between your validation library of choice to JSON schema and so to do that you need to pass an adapter. In this case you can use Zod V4 like this time is V4 but if you like Zod V3 you can actually use Zod V3.
And if you like for example Arch type you can use Arch type. All of these are all different packages that allow your MCP server to use your validation library of choice. You can use effect you can use ValiBot like I like to do that and if your validation library is not covered by these it's very simple to build your own adapter or you can actually just shoot
me a message and I will likely maintain it because it's very simple to maintain it to to build. So, now that I have this, I can import volley button, and then this is how you build a tool. So, uh the important bit here is that you can see that like I mean there's name, there's description, and then there's schema, and the first part is always a configuration
object. And this is something that when I'm designing a library, I think a lot about it. Like I think your API needs to be intuitive, needs to be expected. When you are uh uh invoke creating multiple uh methods, they kind of needs to look all the same. uh this is how you build a tool, but this is how you build a resource. It's very similar. Name and
description are the same, they are called the same, and then there's something specific about the resource, which is the URI, and you get the conf- that in the configuration object, and the second argument is always the handler. This is how you create a template. So, in this case, yeah, in this case, TypeScript can actually help you. When you do server dot, you can see that you have
the ability to create a template. And in this case, uh the URI is actually fully typed. So, if I write MCP test params, and then I try to invoke the complete with param, I will actually get an error there. And uh so, uh once again, we have list and complete, it's in the same configuration object. It's not an external class that you need to import. This is
how you build a prompt. It's very similar to the re- to the resource template. You have the name, the description, the schema, which has the same name as the schema that you were using for the tool. You have the complete, you have the param, which uh once again is fully typed. And this is how you do it uh uh an elicitation message. So, it's a server.elicitation and
then you just pass your validation library because why I already know how to convert between the two, why should I let you write code like the JSON schema by hand? Now, up until this moment, uh the MCP server was actually still just a JSON-RPC server. So, how can we make it easier to actually use it? Well, we can import the STDIO transport from a different package because
once again, there are some dependency that I need to install when I use the HTTP transport and I don't want you that are building an STDIO transport to get all those extra dependencies. And then once you have the STDIO transport, you can just do transport, you pass the server, and then you do transport.listen. What about the HTTP transport? Once again, different package, and then you can just
do something like this. You get the request, you can pass it directly to to respond. If the transport is returning a response, then that means that that's an MCP request, otherwise it will return null, and you can just return not found. Now, the official SDK also provide some middlewares for Express so that you can use authentication with your MCP server. When you are building authentication for your
MCP server, your MCP server needs to act as an authorization server, and that's not easy to build. Now, granted that I think that that's not place for an MCP server library, like an MCP server library should just provide a way to build an MCP server, and you should use something like better out, I couldn't left it just like this. And so I actually have an out package
that allows you to just specify how to create a code, how to create a token, how to create a refresh token. You pass these to the transport, and now your transport act as an authorization server, so that authorize your users inside your MCP server. Another thing that I said is that the MCP is handling the sessions for you. Now, the MCP protocol in general kind of assume
that you are running one single instance, and you can just store stuff in memory. But, if you have run something at scale, you know that's not always the case. You might have an API gateway that then balance to different servers. You might be using serverless, and so you cannot know that the original get request for the the transport for the notification channel will go to the same
server that the post request. And so, to do that, you can actually use the session manager. So, we have a bunch of session manager that basically use in this case Redis, for example, under the hood, and the Redis capabilities of doing pub/sub to communicate cross server, so that you can actually send a notification back in a very easy way. Like, you don't have to think about it.
You just define the the session manager, and that's it. And you can use Redis, or you can use Postgres, or you can use durable object with Cloudflare. Um another thing that I did, you've seen a bunch of examples and responding to an issue to an MCP request is actually pretty verbose. If you want to return a simple text within your within your tool, you have to open
an object and return a content, which is an array with then an object with type text and text the text that you want. And so, you can actually import a bunch of utils from TMCP from the main package itself. And so, now instead of writing all of that, you write tool.text okay, and I did the rest of the job. Now, the elephant in the room. If we
compare between the two, you can see that TMCP kind of wins a lot of stuff. We have like a package size of 325 KB instead of 4 MB, and install of 2.5 MB instead of 14 MB. This is important bit. We have five direct dependency, which means that if any of those five dependency gets compromised, you get compromised. But, they have 17 of those. And even worse,
we have the same five dependency as a total dependency, they have 93. Which means that if any of those 93 packages gets compromised, you get compromised. Now, in terms of installs, we are not even close. I have like say 608K a week, which is not bad. They have millions, but it's being used a bit. The AI to me initiative is using it. Obviously, Svelte is using it.
And the nice thing is that it's using it Storybook both for their MCP and for the MCP that you build. Like you can add an add-on, and they are using TMCP. Now, time is running out. So, if you're interested, you can check out the repository palerdotio/tmcp. And if you're interested more about what Main Matter is doing, you can visit mainmatter.com to learn more how we can help
you. And obviously, thanks for listening. >> [applause]