mcp-server-cat

For my upcoming conference talk, I wanted to build a simple demo application that can draw things on a canvas and expose its functionality as an MCP server, a protocol that enables AI agents to get data or execute various tasks. I've created many demos like that in the past, so I had quite a clear idea of what I needed. I was using Visual Studio 17.14 with the new GitHub Copilot Agent mode. Here is how it went.

Beginner's luck

I created an empty console app and asked Copilot to add an API (using the Minimal API approach) that would enable users to add lines and circles on my canvas. I explicitly requested not to add any database logic and to keep everything in memory, as it's just a demo. This worked surprisingly well. Copilot added the API endpoints and stored the shapes in an in-memory collection. It even added an endpoint to list all existing shapes, which I didn't request, but I would have needed it anyway.

️ First hick-ups

I noticed that the type of collection for storing the shapes was List<object>, which is not a great practice in a strongly typed language. The objects would not even serialize correctly if they were sent to the client. Additionally, the project I created was a console app, and it didn't have references to some ASP.‎NET Core types for Minimal APIs. It was my mistake - I should've created an empty ASP.‎NET Core app instead.

Copilot overlooked that. Normally, it compiles the code after each step, so in my case, it eventually discovered the problem and started working on it. It added a reference to a NuGet package Microsoft.AspNetCore.App (it asked before), but it was not the correct solution. The package is outdated and should no longer be used. The code would compile, but the application would crash on start. I had to explicitly tell Copilot to change the project SDK to Microsoft.NET.Sdk.Web explicitly.

Trial and error

Next, I instructed Copilot to define strong types for the shapes. Copilot understood what I meant and wanted to create a class to represent the Line and Circle objects. However, because all my code was in one file and I was using top-level statements, Copilot received a compilation error when it added the new classes at the beginning of the file.

The classes themselves were fine, but the code couldn't compile, so Copilot rewrote the code several times without success. It tried adding and removing namespaces and renaming things but didn't figure out that the order was wrong. Only after the 4th attempt to change something and compile the app did it try to move the new classes into a separate file. This finally removed the error. Thus, Copilot ultimately succeeded, but it took approximately 1 minute to converge on a working solution.

Serialization issues

ASP.NET Core uses System.Text.Json as a serializer, but it does not automatically serialize properties of polymorphic types. I knew that, so I had to request the addition of the JsonDerivedType attributes. I intentionally skipped this instruction to see if Copilot would figure it out automatically, but it didn't.

✔️ Generating front-end

At this point, I had the APIs for adding and listing the shapes. Now, for the front-end part - I asked to create an HTML page that would draw the shapes on canvas. This worked well. Copilot knew that it needed to add UseStaticFiles to Program.cs, and it generated an index.html page with a simple JavaScript that called the API endpoint and drew the shapes.

I clarified my previous instruction that the page should reload the shapes and refresh the drawing every 5 seconds. Copilot made a minimal change to make the page behave in this way. That was nice.

It just doesn't work

However, when I ran the project and sent some shapes to the API from Postman, nothing was displayed on the page. The problem was again in JSON serialization - the server was using camel case property names, but the client code expected Pascal case.

When I asked Copilot to fix it (without specifying exactly how), it didn't find the correct place in Program.cs to configure the contract resolver. Instead, it added a configuration that would work for controller-based APIs, but not for the minimal ones. I had to explicitly mention which function in the Program.cs I want to update. However, we finally got both the front-end and API to work.

No web search means hallucination

The biggest issue came when I asked to expose the API as an MCP server. The Agent mode in Visual Studio apparently cannot use web search yet (it only uses it in Ask mode), so it was unaware of MCP at all. It was hallucinating all the time, creating entirely new API endpoints that were precisely the same as the current ones, except for the "/mcp" prefix in the URL. That's not good.

I instructed it that there must be some NuGet package to do that, but it didn't know about it because of a missing web search capability. It thought the package is called MCP, but the actual name is ModelContextProtocol.AspNetCore. I had to manually find a GitHub repo with an example of how to use the package. I gave Copilot the URL of the repository, but it didn't help - Copilot offered to clone the repository, which I didn't want, as it was unclear where it would clone it.

Instead, I found the two files that had what I needed in the MCP samples repo, and pasted them to Copilot as an example. Copilot understood that it should take the API endpoints and build the MCP server tool around them, but it also deleted the original API endpoints. Additionally, it somehow disrupted access to the collection of shapes - it attempted to move it to a separate class, but this broke the code that was already using it.

It's a trap

From then on, it only got worse. Copilot didn't determine exactly where the MCP server should be registered. It registered the services in Program.cs after the builder.Build() was called, which compiles, but doesn't work. Then, it moved half of Program.cs to a separate class while keeping the incorrect order of registrations. I gave up and fixed that manually because there was such a mess that it would be too hard to explain how it should be fixed.

I finished the MCP server manually and wanted to try it from VS Code. VS Core also features Copilot Agent mode, along with additional capabilities. For example, you can register a custom MCP server in the configuration so the Copilot can use it.

I struggled with that for some time due to CORS and certificate-related issues (for some reason, the custom MCP server only worked with HTTP, not HTTPS). But after a few minutes of fighting, I was able to draw a cat using GitHub Copilot Agent in VS Code using my MCP server. Also, I had to use the SSE protocol explicitly. Otherwise, I wasn't able to establish a connection.

Overall, it was a very interesting experience, and I will continue to experiment with it. The fact that Copilot can manipulate more files in the project, run the compilation, and iteratively improve its solution is quite useful. It creates checkpoints, allowing you to revert previous edits if they are not satisfactory.

❓ Will it ever be useful?

It is still early, and sometimes it is like betting on red in a roulette. You can be sure it will come eventually, but you never know how many attempts will be necessary.

However, I believe that we'll find many scenarios where we can confidently apply this agentic approach. Simple changes, such as adding a new column to a database entity and showing it in the UI, can be a good candidate. Real-world projects are full of such easy tasks.

The .NET community has just seen another open-source drama. FluentAssertions, a popular library that provides a natural and easy-to-read syntax for unit tests, has suddenly changed its license. Starting with version 8, you must pay $130 per developer if you use it in commercial projects.

Group of people deciding how to monetize open-source project without making another drama

Reading the related discussions on GitHub and Reddit is painful. Most people only complain about paying for something that has always been free, completely ignoring the effort the authors had to put into the library, and any constructive critique is very rare.

Every open-source project needs significant investment to ensure it will work long-term. Keeping the project healthy and reliable requires thousands of hours of its maintainers. It is perfectly OK to want to get paid for the work. The key question is how to do it without losing trust.

We saw what happened when the author of Moq published an update that collected the e-mail addresses of developers using the library. I don't think the original intent was to do any harm. Still, the idea of a library that hooks in the build pipeline, collects information from developer machines, and sends them somewhere was far behind the red line for most users.

With FluentAssertions, we can see a similar story. The NuGet package name is the same, and you may not even notice that version 8 uses a different license when upgrading. Some people reference package versions with wildcards, which upgrades the dependencies even without making a single change in the code. Yes, people can stay on version 7 forever, but the risk of accidentally upgrading the package is quite high. Don't tell me you never clicked on the "upgrade all NuGet packages in the solution" option.

I think that making such a fundamental change deserves publishing NuGet package with a different name. Also, I believe that the change was not announced early enough, and it came out as a surprise to most of the community. Whether we like it or not, paying for using open-source projects is a sensitive topic, and communicating things clearly and in advance would significantly reduce the frustration.

I still think that the only viable business model for OSS is to have an open-source core component surrounded by commercial add-ons. The core component must be free, and it must be clearly promised it will be free forever. No catches, no tricks. This is the only way to avoid losing the trust of the users. We took this strategy with DotVVM 10 years ago and I never regretted it. We managed to acquire many loyal customers over the years, and the commercial products helped the project become sustainable.

If you maintain an open-source project, I understand you need to get paid for that. I need it, too. But please be careful when transitioning to a commercial model. Every drama like this with FluentAssertions damages the overall trust in all open-source libraries.

Today, I ran into an unusual behavior of ASP.NET Core application deployed to Azure App Service. It could not find the connection string, even though it was present on the Connection Strings pane of the App Service in the Azure portal.

This is how the screen looked like:

Screenshot of the connection string in Azure portal

The application was accessing the connection string using the standard ASP.NET Core configuration API, as shown in the following code snippet:

sevices.AddNpgsqlDataSource(configuration.GetConnectionString("aisistentka-db"));

Naturally, everything works as expected locally, but when I deployed the app to Azure, it did not start, with the exception “Host can’t be null.”

When diagnosing this kind of issues, it is a good idea to start with the Kudu console (located at https://your-site-name.scm.azurewebsites.net). A quick check of the environment variables usually shows what is wrong.

Every connection string should be passed to the application as an environment variable. Normally, the ASP.NET Core’s GetConnectionString method should look for the ConnectionStrings:connectionStringName configuration key (which is usually in the appsettings.json file or in User Secrets). Since environment variables cannot contain colons, they can be replaced with double underscores – the .NET configuration system treats these separators as equal.

However, the type field in the Azure portal (you can see it in the picture at the beginning of the article) provides a special behavior and somehow controls how the environment variable names are composed. In the case of PostgreSQL, the resulting variable name is POSTGRESQLCONNSTR_aisistentkadb. As you can see, instead of ConnectionStrings__ prefix, the prefix is POSTGRESQLCONNSTR_, and the dash from the connection string name is removed.

This was a bit unexpected for me. The GetConnectionString method cannot see the variable, but when I use the type “SQL Server”, the same approach works (though, the dashes in connection string names do not). How is this possible?

I looked in the source code of .NET and found out that there is a special treatment of these Azure prefixes, but not all of them are included. It only supports SQL Server, SQL Azure, MySQL, and Custom types. All other options will produce an incorrect name of environment variable that the application will not find.

    /// <summary>
    /// Provides configuration key-value pairs that are obtained from environment variables.
    /// </summary>
    public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
    {
        private const string MySqlServerPrefix = "MYSQLCONNSTR_";
        private const string SqlAzureServerPrefix = "SQLAZURECONNSTR_";
        private const string SqlServerPrefix = "SQLCONNSTR_";
        private const string CustomConnectionStringPrefix = "CUSTOMCONNSTR_";
...

The solution was to use the Custom type and remove the dash from the connection string name.

I once heard, “If you fear something, learn about it, disassemble it to the tiniest pieces, and the fear will just go away.

Well, it didn’t work. I read a book about building LLM from scratch, which helped me understand the model's architecture and how it works inside. However, I am still concerned about the power of AI models and the risks our world may face in the future. Although we still don't understand many fundamental concepts of how the human brain works, and some scientists say we are not even close to getting to human-level intelligence, I am still a bit worried about the scale and speed the new technologies emerge. Many inventions of science were not achieved by logical thinking or inference but by mistake or trial and error. Spawning millions of model instances and automating them to make “random” experiments to discover something new doesn’t seem that impossible to me.

The book shows how to build the smallest version of GPT-2 in Python and preload model weights published by OpenAI. By the way, GPT-3 has the same architecture, but the model is scaled to a larger number of parameters.

I was curious if this could be done in C#, and I found the TorchSharp library. It is a wrapper for native libraries used by PyTorch. The API was intentionally kept to be as close to Python as possible, so the code does not look like .NET at all. But it makes the library easy to learn and use, since a vast majority of examples are in Python. What surprised me is that the actual LLM implementation in C# has only about 200 lines of code. All the magic is in the model weights. PyTorch/TorchSharp provides a very nice abstraction over the primitives from which deep neural networks are composed.

I was wondering if it makes sense to do a session about it, for example, at our MeetUpdate. The problem is that I am not an AI scientist, and the topic is hard. I think I understand all the crucial aspects and will be able to explain what is going on. But still, there are many things I have practically no experience with. Second, understanding the session requires at least some knowledge of how neural networks work and the basics of linear algebra. I am not sure what the experience of the audience would be. And finally, I would be speaking about something that is not my creation at all - it would be merely a description of things others have invented, and my added value would only be in trying to explain it in a short meetup session.

On the other hand, playing with it was really fun, and maybe it can motivate someone to start learning more about ML and neural networks.

Shall I do it?

Crazy developer explains LLM internals to the students

My new book, “Modernizing .NET Web Applications,” is finally out - available for purchase in both printed and digital versions. If you are interested in getting a copy, see the new book’s website.

A pile of copies of my new book Modernizing .NET Web Applications

It was a challenging journey for me, but I liked every moment of it, and it is definitely not my last book. I just need to bump into another topic and get enthusiastic about it (which seems not to be that hard).

I finished the manuscript on the last day of June, but some things had already changed before the book was published. For example, the System.Data.SqlClient package became deprecated. Additionally, all samples in the book used .NET 8, but .NET 9 is just around the corner, and there will be many new and interesting features and performance improvements. Despite the fact that they do not primarily target the area of modernization, they are highly relevant - they constitute one of the strongest arguments for moving away from legacy frameworks. Chapter 2 is dedicated to the benefits of the new versions of .NET, and it is one of the longest chapters of the book. Why else would you modernize if not to use the new platform features?

To ensure you can stay updated, I’ve set up a newsletter where I’ll regularly post all the news concerning the modernization of .NET applications.

The book had an official celebration and announcement at Update Conference Prague, and I’d like to thank all the people around me who helped the book to happen.