<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anton Makarevich</title>
    <description>The latest articles on DEV Community by Anton Makarevich (@antonmakarevich).</description>
    <link>https://dev.to/antonmakarevich</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1120868%2Faccbbcc3-e396-44d4-87b9-ad813f091974.jpeg</url>
      <title>DEV Community: Anton Makarevich</title>
      <link>https://dev.to/antonmakarevich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonmakarevich"/>
    <language>en</language>
    <item>
      <title>How I Teach LLMs to Play BattleTech (Part 2): Building an MCP Server and Agents in C#</title>
      <dc:creator>Anton Makarevich</dc:creator>
      <pubDate>Wed, 28 Jan 2026 12:55:26 +0000</pubDate>
      <link>https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-2-building-an-mcp-server-and-agents-in-c-e86</link>
      <guid>https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-2-building-an-mcp-server-and-agents-in-c-e86</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is Part 2 of a series on building an LLM-powered BattleTech bot.&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-1-architecture-agents-and-tools-18om"&gt;Part 1: Architecture, Agents, and Tools&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to Expect
&lt;/h2&gt;

&lt;p&gt;Here’s how the series is structured:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-1-architecture-agents-and-tools-18om"&gt;Part 1.&lt;/a&gt; Theory &amp;amp; Architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Introduction
&lt;/li&gt;
&lt;li&gt;MakaMek Architecture Overview
&lt;/li&gt;
&lt;li&gt;How LLM Models Use Tools
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Part 2. Hands-On Implementation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Building an MCP Server to Query Game State
&lt;/li&gt;
&lt;li&gt;Creating Agents with the Microsoft Agent Framework
&lt;/li&gt;
&lt;li&gt;Empowering Agents with Tools
&lt;/li&gt;
&lt;li&gt;Conclusion
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  MCP Server for Querying Game State
&lt;/h2&gt;

&lt;p&gt;In Part 1, we covered the motivation, architecture, and theory behind using tools and agents in my BattleTech bot.&lt;/p&gt;

&lt;p&gt;Let's see how to implement all of this in practice. Most examples around this topic are available in Python, mainly for historical reasons. As we saw earlier, however, agents and tools are just pieces of “traditional” software, they can be implemented in any programming language. Since MakaMek is a .NET application, it makes perfect sense to create agents and tools in the same tech stack.&lt;/p&gt;

&lt;p&gt;All code is available on the &lt;a href="https://github.com/anton-makarevich/MakaMek" rel="noopener noreferrer"&gt;MakaMek GitHub page&lt;/a&gt; — check the &lt;code&gt;BotContainer&lt;/code&gt; (for MCP) and &lt;code&gt;BotAgent&lt;/code&gt; (for AI agents) projects.&lt;/p&gt;

&lt;p&gt;Let’s take a closer look at the MCP server. The idea is that it should have access to the &lt;code&gt;ClientGame&lt;/code&gt; class and the corresponding calculators, and be able to query them to retrieve the tactical situation. This means we need to add an MCP server to the &lt;code&gt;BotContainer&lt;/code&gt; project. It also needs to be a remote server so that AI agents can reach it over the network.&lt;/p&gt;

&lt;p&gt;There is an official &lt;a href="https://github.com/modelcontextprotocol/csharp-sdk" rel="noopener noreferrer"&gt;MCP SDK available for .NET and C#&lt;/a&gt;. It is currently in preview, which means the APIs are not fully stable yet, but I haven’t noticed any issues so far.&lt;/p&gt;

&lt;p&gt;So how do we implement an MCP server in a .NET app? The process is straightforward and consists of just a few steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the NuGet packages
&lt;/h3&gt;

&lt;p&gt;To create a remote MCP server, add the following packages to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package ModelContextProtocol &lt;span class="nt"&gt;--version&lt;/span&gt; 0.6.0-preview.1
dotnet add package ModelContextProtocol.AspNetCore &lt;span class="nt"&gt;--version&lt;/span&gt; 0.6.0-preview.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one (&lt;code&gt;ModelContextProtocol&lt;/code&gt;) is core MCP functionality, it's enough if you only need to create a local server, the &lt;code&gt;ModelContextProtocol.AspNetCore&lt;/code&gt; is required for a remote MCP to enable HTTP/SSE server and transport.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Register the MCP server in DI
&lt;/h3&gt;

&lt;p&gt;Once the packages are added, register the MCP server with the DI container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerInfo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Implementation&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MakaMek MCP Server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHttpTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// enables a remote server accessible via HTTP/SSE&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stateless&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// important if you want to host it in a serverless function&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTools&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeploymentTools&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="c1"&gt;// classes with tools exposed by this server&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTools&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MovementTools&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTools&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeaponsAttackTools&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Expose the MCP endpoints
&lt;/h3&gt;

&lt;p&gt;Because the MCP uses a standard endpoints, no custom controller code is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Implement your tools
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Tools&lt;/em&gt;, as explained in Part 1, are just regular C# methods decorated with certain attributes for discoverability.&lt;/p&gt;

&lt;p&gt;Here is an example of one of the simplest tools. More complex ones are available in the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Indicates that this class contains MCP tools (optional with manual registration)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeploymentTools&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IGameStateProvider&lt;/span&gt; &lt;span class="n"&gt;_gameStateProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DeploymentTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IGameStateProvider&lt;/span&gt; &lt;span class="n"&gt;gameStateProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_gameStateProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameStateProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Description is one of the most important parts: it is what the LLM "sees".&lt;/span&gt;
    &lt;span class="c1"&gt;// It should clearly explain when the tool should be used and what it does.&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get valid deployment zones (hexes). Should be used by the deployment agent."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HexCoordinateData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetDeploymentZones&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_gameStateProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientGame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEdgeHexCoordinates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToData&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Connect your agent to the MCP server
&lt;/h3&gt;

&lt;p&gt;That’s it. When you start the application, your MCP server becomes available to any agents on the same network. Just add the corresponding MCP configuration to your favorite coding agent (VS Code, Cursor, Claude, Kiro — you name it) to quickly test that it's actually working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"makamek-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Creating Agents with Microsoft Agent Framework
&lt;/h2&gt;

&lt;p&gt;Now that we have an MCP server, let’s create our own agents to use it. Again, we’ll do this in C#, and the obvious choice is the &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Microsoft Agent Framework&lt;/a&gt; (MAF), since it provides a .NET SDK alongside the Python one. MAF is a fairly new product, but it is based on Semantic Kernel after its merge with AutoGen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I should mention that this is one of the areas where Microsoft’s approach can be confusing. They move fast and change APIs often, which makes production use risky. For example, in the week since I finished this feature, some APIs already changed in a new preview release. In this post, I’ll reference the version I used during development. Be aware that it’s not the latest one, and you may need to adjust names and APIs if you upgrade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So let’s see how to create agents using MAF.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the NuGet packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.AspNetCore.OpenApi &lt;span class="nt"&gt;--version&lt;/span&gt; 10.0.2 &lt;span class="c"&gt;# Needed for models supporting the OpenAI API&lt;/span&gt;
dotnet add package Microsoft.Agents.AI &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.0-preview.260108.1 &lt;span class="c"&gt;# Microsoft Agent Framework&lt;/span&gt;
dotnet add package Microsoft.Extensions.AI &lt;span class="nt"&gt;--version&lt;/span&gt; 10.2.0
dotnet add package Microsoft.Extensions.AI.Abstractions &lt;span class="nt"&gt;--version&lt;/span&gt; 10.2.0
dotnet add package Microsoft.Extensions.AI.OpenAI &lt;span class="nt"&gt;--version&lt;/span&gt; 10.2.0-preview.1.26063.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Microsoft.Agents.AI&lt;/code&gt; is the Microsoft Agent Framework itself. The remaining packages provide abstractions and extensions to wire it into the broader Microsoft AI ecosystem. I’ll come back to that shortly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Choose and abstract your model provider
&lt;/h3&gt;

&lt;p&gt;Next, think about the model you want to use to power your agents. If your hardware allows it (for example, a decent GPU with enough VRAM), running a local model is often a good place to start. Local models are weaker than frontier models, but they’re free to run (apart from your electricity bill).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LM Studio&lt;/strong&gt; is my tool of choice to run local models, but the same approach works with &lt;strong&gt;Ollama&lt;/strong&gt; or &lt;strong&gt;Foundry Local&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A good practice is not to hardcode the model provider and instead make it swappable. To illustrate this, let’s create two providers that conform to a common interface.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.1 Common LLM provider interface
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ILlmProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IChatClient&lt;/code&gt; comes from &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt; and represents a client capable of communicating with any chat model.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.2 OpenAI implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;openAiClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openAiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_config&lt;/code&gt; is a section of a standard ASP.NET Core configuration for the model provider (see the full project in the repo for details).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AsIChatClient()&lt;/code&gt; comes from &lt;code&gt;Microsoft.Extensions.AI.OpenAI&lt;/code&gt; and knows how to wrap a concrete OpenAI client into the generic &lt;code&gt;IChatClient&lt;/code&gt; abstraction.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.3 Local / offline model implementation
&lt;/h4&gt;

&lt;p&gt;Most local models support the OpenAI API, which is why I call this provider “OpenAI-like”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;openAiClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApiKeyCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NO_API_KEY_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OpenAIClientOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_endpoint&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openAiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation is almost identical, except that the local model does not require a real API key. Instead, we provide the endpoint of the LM Studio (or Ollama) server running the model.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.4 Register providers with DI
&lt;/h4&gt;

&lt;p&gt;Next, register the providers as dependencies. It’s handy to expose the active provider via configuration so models can be swapped without code changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LocalOpenAiLikeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OpenAiProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILlmProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LlmProviderConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"LocalOpenAI"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LocalOpenAiLikeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;"OpenAI"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OpenAiProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Unsupported LLM provider type '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Create agents with MAF
&lt;/h3&gt;

&lt;p&gt;Once the wiring is done, we can move on to the actual implementation. In my case, I need four agents, one per game phase. They differ mainly by their prompts and the tools they use; the underlying model can be the same.&lt;/p&gt;

&lt;p&gt;With that in mind, I created an abstract &lt;code&gt;BaseAgent&lt;/code&gt; class and four specialized agents derived from it. Model setup is shared in the base class.&lt;/p&gt;

&lt;p&gt;To access the model, we need an instance of &lt;code&gt;AIAgent&lt;/code&gt; from MAF. There are several ways to create it, either directly via a constructor or via a builder. The builder approach is more flexible because it allows you to plug in middleware, so that’s what I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LlmProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// defined in the derived agent&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;allTools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// tools (we’ll look at them in detail later)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCallMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNewThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ToolCallMiddleware&lt;/code&gt; is a callback executed by the &lt;code&gt;AIAgent&lt;/code&gt; before it invokes a tool requested by the model. It’s very useful for observability and guardrails.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;thread&lt;/code&gt; holds the entire conversation history between the agent and the model. It’s important to create one so the model has proper context, for example knowing that a previous step triggered a tool call.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Run the agent
&lt;/h3&gt;

&lt;p&gt;Now that we have an &lt;code&gt;AIAgent&lt;/code&gt; instance, we can start making calls to the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Empowering Agents with Tools
&lt;/h2&gt;

&lt;p&gt;At this point, the agent can reason and return a text response. But what about our MCP server? How do we use the tools it exposes? And how do we convert a model’s response into a structured command that the game can actually execute?&lt;/p&gt;

&lt;p&gt;Remember the &lt;code&gt;tools&lt;/code&gt; array we passed to the agent factory method? Let’s take a closer look at it. This array contains definitions of all &lt;code&gt;AITool&lt;/code&gt;s that we want to make available to the model, including MCP tools, API tools, and local tools. In this post, we focus only on MCP and local tools, since those are the ones used by the MakaMek agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. MCP tools
&lt;/h3&gt;

&lt;p&gt;To access tools exposed by a remote MCP server, we first create an MCP client by providing the server endpoint and a transport mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;McpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClientTransportOptions&lt;/span&gt; 
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;TransportMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpTransportMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StreamableHttp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// HTTP/SSE transport for remote servers&lt;/span&gt;
            &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpEndpoint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note the &lt;code&gt;await using&lt;/code&gt; statement: &lt;code&gt;mcpClient&lt;/code&gt; should be disposed when no longer needed. Be careful with the scope, though — the client must remain alive for as long as the agent needs to call MCP tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the MCP client is created, we can list all tools available on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListToolsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If MCP tools are the only ones you use, this list can already be passed directly to the agent factory.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Local tools
&lt;/h3&gt;

&lt;p&gt;Local tools are functions defined in the same module (in .NET terms, the same assembly) as the agents. In MakaMek, each specialized agent exposes its own local tools, mostly to translate the model’s decisions into concrete MakaMek commands.&lt;/p&gt;

&lt;p&gt;Here’s an example of a local tool used by the &lt;code&gt;DeploymentAgent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Execute a deployment decision for a unit"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;MakeDeploymentDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unit GUID"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;unitId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Q coordinate"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"R coordinate"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Facing direction 0-5"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tactical reasoning"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeployUnitCommand&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;UnitId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unitId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HexCoordinateData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GameOriginId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Will be set by ClientGame&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;PendingDecision&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Deployment decision recorded"&lt;/span&gt; 
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that both the method and all its parameters are decorated with the &lt;code&gt;Description&lt;/code&gt; attribute. This metadata is important: the LLM uses it to understand when and how the tool should be called.&lt;/p&gt;

&lt;p&gt;Once the tools are defined, we expose them by overriding &lt;code&gt;GetLocalTools()&lt;/code&gt; in a specialized agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AITool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetLocalTools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;AIFunctionFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MakeDeploymentDecision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"make_deployment_decision"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;AIFunctionFactory.Create&lt;/code&gt; turns a normal C# method into an &lt;code&gt;AITool&lt;/code&gt; by providing the method reference and the name that will be visible to the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Combining MCP and local tools
&lt;/h3&gt;

&lt;p&gt;Now we can retrieve the agent-specific local tools, combine them with the MCP tools, and pass them to the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;localTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetLocalTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AITool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;allTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="n"&gt;localTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="n"&gt;mcpTools&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// passed to the agent factory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same &lt;code&gt;allTools&lt;/code&gt; collection we provided earlier when creating the agent.&lt;/p&gt;

&lt;p&gt;That concludes the entire end-to-end flow of exposing tools via MCP and consuming them using agents. Of course I had to take some shortcuts in the blog, but feel free to explore the full &lt;a href="https://github.com/anton-makarevich/MakaMek" rel="noopener noreferrer"&gt;solution on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So, with all the tools and “LLM wisdom” available to the bot, can it actually play the game? And if it can, how does it compare to a traditional rule-based bot?&lt;/p&gt;

&lt;p&gt;The answer is both yes and no. It does play the game, and with all the guardrails in place it always takes valid actions, but those actions often feel very random and don’t make much sense, even when the full tactical situation is available to the model. This is especially noticeable with smaller local models (I use &lt;code&gt;qwen3-vl-8b&lt;/code&gt;), which very often just picks one of the first available options. Frontier GPT-5.2 behaves a bit more “believably”, but there is still no comparison with a rule-based bot that consistently chooses the best available option and is therefore almost impossible to beat when the dice gods are not on your side. On top of that, the rule-based bot is much faster and cheaper to run: depending on the number of units controlled by the bot, a single game can easily exceed a million input tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This real-world result demonstrates that AI isn't always the right solution, even when it's technically feasible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even though the LLM-powered bot turned out to be practically useless from a gameplay perspective, I don’t regret building it. It gave me a great agentic playground in a domain I really enjoy and can continue improving.  &lt;/p&gt;

&lt;p&gt;Some obvious areas for improvement would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refining the prompts (they are definitely not optimised yet),&lt;/li&gt;
&lt;li&gt;converting tool outputs to natural language instead of the current structured format,&lt;/li&gt;
&lt;li&gt;introducing another type of tool, such as RAG, to provide BattleTech rules relevant to a specific situation,&lt;/li&gt;
&lt;li&gt;and, as a longer-term idea, retraining or fine-tuning a model once I have enough gameplay logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So there are plenty of ideas left to explore in my spare time (if I can find some 😄).&lt;br&gt;&lt;br&gt;
Do you see anything else worth adding to the list?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>ai</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>How I Teach LLMs to Play BattleTech (Part 1): Architecture, Agents, and Tools</title>
      <dc:creator>Anton Makarevich</dc:creator>
      <pubDate>Tue, 27 Jan 2026 11:33:46 +0000</pubDate>
      <link>https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-1-architecture-agents-and-tools-18om</link>
      <guid>https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-1-architecture-agents-and-tools-18om</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is Part 1 of a series on building an LLM-powered BattleTech bot.&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-2-building-an-mcp-server-and-agents-in-c-e86"&gt;Part 2: Building an MCP Server and Agents in C#&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to Expect
&lt;/h2&gt;

&lt;p&gt;Here’s how the series is structured:&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1. Theory &amp;amp; Architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Introduction
&lt;/li&gt;
&lt;li&gt;MakaMek Architecture Overview
&lt;/li&gt;
&lt;li&gt;How LLM Models Use Tools
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Part 2. Hands-On Implementation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Building an MCP Server to Query Game State
&lt;/li&gt;
&lt;li&gt;Creating Agents with the Microsoft Agent Framework
&lt;/li&gt;
&lt;li&gt;Empowering Agents with Tools
&lt;/li&gt;
&lt;li&gt;Conclusion
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Nowadays there seems to be a tendency to solve almost every problem with a solution that implies use of AI agents. "There is not enough AI in this report", or "this proposal is great, but where are your AI agents?" is something that I hear frequently. But are we really supposed to throw AI on every problem? I do find the technology extremely useful for many use cases, but at the same time there are many cases where traditional automation is still a valid option.&lt;/p&gt;

&lt;p&gt;To illustrate this, I want to describe how after building a rule-based bot for my pet-project &lt;a href="https://makamek.nl" rel="noopener noreferrer"&gt;MakaMek&lt;/a&gt; (which is a computer implementation of a turn-based tactical wargame BattleTech), I've decided to make one step further and create an LLM-powered bot too.&lt;/p&gt;

&lt;p&gt;Someone has actually suggested: “just use ChatGPT for the bot” when I asked for an advice on bot's strategy. I was quite skeptical about the idea: as I explained in my &lt;a href="https://dev.to/antonmakarevich/control-over-speed-my-take-on-ai-coding-5g7b"&gt;previous&lt;/a&gt; (now almost one-year-old) blog post, I do not believe that predicting the next token has anything to do with “true intelligence”, but at the same time I already had all the functions and scripts to evaluate the actual tactical situation on the board, coming from my rules-based implementation. So I wondered: if I provide that information with the available options in a clear text format, could the model pick choices that actually make sense?&lt;/p&gt;

&lt;p&gt;Working on this feature was a lot of fun, and it gave me valuable hands-on experience in building agents and connecting them with various tools, including a custom Model Context Protocol (MCP) server.&lt;/p&gt;

&lt;p&gt;In this post, I cover some fundamentals of agentic systems and the journey behind my LLM-powered bot. I’ll explain the architecture that allowed me to add it without changing the main application, and demonstrate how to build an MCP server, AI agents, LLM providers, and tool integrations using .NET and C#.&lt;/p&gt;

&lt;h2&gt;
  
  
  MakaMek Architecture
&lt;/h2&gt;

&lt;p&gt;MakaMek is my own computer version of BattleTech that I use as a playground for experimenting with new technology. The game is FOSS and &lt;a href="https://github.com/anton-makarevich/MakaMek" rel="noopener noreferrer"&gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a server–client application written in .NET. The server holds the authoritative game state, which can be changed by applying commands received from clients. A single client can host one or more players, including human players and bots. The server and clients communicate via a custom pub-sub–based command protocol that supports multiple transports, including reactive commands, pure WebSockets, and SignalR. Both server and client are UI-agnostic and can be hosted in any process.&lt;/p&gt;

&lt;p&gt;Given this architecture, the obvious solution for an LLM bot was to implement it as a standalone, headless application hosting the game client and the bot logic. I was able to reuse the &lt;code&gt;Bot&lt;/code&gt; class from the rules-based implementation. The bot itself is generic: it contains no decision logic and only observes the client game state. For decision-making, the bot relies on an &lt;code&gt;IDecisionEngine&lt;/code&gt; interface where the actual logic is implemented. The rules-based bot has a dedicated engine per game phase. I took the same approach for the LLM-powered bot and introduced four &lt;code&gt;llm*&lt;/code&gt; decision engines for the phases that require user input. These engines delegate the actual decision-making to an AI agent.&lt;/p&gt;

&lt;p&gt;For the AI agents, I decided to introduce a separate host application/service to improve flexibility and scalability. In my setup, the bot and the agent host are two independent applications packaged as Docker containers that communicate over HTTP. The bot (via the LLM decision engine) sends a request to the agent application containing a high-level description of the game state, including all units.  &lt;/p&gt;

&lt;p&gt;The agent application contains four phase-specific agents. A router redirects each request to the appropriate agent based on the current game phase. The agent converts the structured request into an LLM-friendly text prompt and invokes the model. The model produces a decision, which is then returned to the bot as an HTTP response.&lt;/p&gt;

&lt;p&gt;While &lt;a href="https://github.com/anton-makarevich/MakaMek/wiki/llm-bot-system-design" rel="noopener noreferrer"&gt;this design&lt;/a&gt; provides a solid foundation, there are still two problems not addressed by the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Information about the units alone is often not enough. An LLM cannot truly “understand” a tactical situation from raw state data. It needs additional context, such as where units are allowed to move, which enemies pose a threat, hit probabilities, and similar factors.
&lt;/li&gt;
&lt;li&gt;The agent is expected to return a well-structured, schema-compliant response that can be safely executed by the game.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To address both issues, we can equip our agents with tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bot application can run an MCP server exposing tools that provide tactical data by querying the game client.
&lt;/li&gt;
&lt;li&gt;The agent can include helpers to validate and format responses according to the expected command schema.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1payuqfsg0abz1c0fcmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1payuqfsg0abz1c0fcmc.png" alt="Connecting LLM-powered bot to MakaMek" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This leads to a question that still confuses many people, including experienced engineers: what tools actually are, what types of tools exist, and how agents and models actually use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How LLM Models Use Tools
&lt;/h2&gt;

&lt;p&gt;Let’s approach these questions one by one. So, &lt;strong&gt;what is a “tool”?&lt;/strong&gt; The simple answer is: any custom code or script written in any programming language. It can be a local function running in the same process as the agent (a local tool), a CLI program exposed via a local MCP server, or a function available on a remote server through a REST API or MCP. Based on this definition, a tool can effectively do “anything”: provide additional data, perform calculations, or execute actions.&lt;/p&gt;

&lt;p&gt;But how do &lt;strong&gt;LLM models call those tools?&lt;/strong&gt; And the simple answer to this question is: they don’t — at least not directly. An LLM is text-in, text-out; it is not capable of taking actions on its own. Instead, the model receives a list of available tools and their descriptions as part of the prompt and can respond with the name of the tool to use along with the required arguments. The actual execution is delegated to orchestration code, which we usually call an &lt;em&gt;agent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here is a sample generic flow showing a scenario in which a model is provided with a list of tools of different types and “executes” them one by one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk1xotfg6mh3t9njin4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk1xotfg6mh3t9njin4k.png" alt=" " width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key takeaway is that every time a model decides to use a tool, it returns that decision to the agent. The agent executes the tool and then resubmits the original prompt together with the tool result back to the model so it can continue reasoning.&lt;/p&gt;

&lt;p&gt;Each tool execution therefore requires another round-trip to the model, which means extra latency and additional token usage. In practice, this results in roughly double the tokens being spent for every additional tool call. If data is available upfront, it often makes sense to include it directly in the initial prompt instead of relying on tool calls, which introduce extra cost and delay.&lt;/p&gt;




&lt;p&gt;This concludes the theoretical part of the series.&lt;/p&gt;

&lt;p&gt;👉 In &lt;a href="https://dev.to/antonmakarevich/how-i-teach-llms-to-play-battletech-part-2-building-an-mcp-server-and-agents-in-c-e86"&gt;&lt;strong&gt;Part 2&lt;/strong&gt;&lt;/a&gt; we’ll get more practical and build a remote MCP server using the C# MCP SDK, define agents with the Microsoft Agent Framework, and connect the agents to a local and cloud LLM.&lt;/p&gt;

&lt;p&gt;We’ll also deploy the two bot implementations against each other. Curious who will win? Read Part 2 to the end.&lt;br&gt;
Or maybe you already know the answer?😄 Let me know in the comments.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>dotnet</category>
      <category>ai</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Control Over Speed: My Take on AI Coding</title>
      <dc:creator>Anton Makarevich</dc:creator>
      <pubDate>Mon, 21 Apr 2025 20:56:56 +0000</pubDate>
      <link>https://dev.to/antonmakarevich/control-over-speed-my-take-on-ai-coding-5g7b</link>
      <guid>https://dev.to/antonmakarevich/control-over-speed-my-take-on-ai-coding-5g7b</guid>
      <description>&lt;p&gt;Recently, I’ve seen a lot of discussion around using AI agents to create software. IDEs and plugins supporting agentic workflows like Cursor, Windsurf, Firebase Studio, you name it... are becoming extremely popular. But there’s no clear consensus in the community on whether that’s a good or bad thing. Some praise them for a huge productivity boost, others blame them for producing an incredible amount of meaningless, non-maintainable code - a recipe for disaster in the near future. I hear voices of people that I know and respect and they are coming from both groups.&lt;/p&gt;

&lt;p&gt;So where’s the truth? Maybe only time will tell, but I believe it lies somewhere in the middle. And “vibe coding” is not necessarily the (only) way to utilize AI Agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context
&lt;/h2&gt;

&lt;p&gt;I haven't done any blogging in years, (maybe even decades), but the topic is so hot that even I cannot help myself and break the silence to share my own thoughts and experience.&lt;/p&gt;

&lt;p&gt;Let’s start with a quick quiz covering my current situation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do you use AI agents in your daily work?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes, I do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do you use them for any client work?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No, I don’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do you use them blindly, without much thinking?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; I hope not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do they boost your productivity?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes, they do, but nowhere near the hype of &lt;em&gt;"I built this website with one prompt in 10 minutes".&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do they affect the quality of your code?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; That’s for you to judge - feel free to check my &lt;a href="https://github.com/anton-makarevich/MakaMek" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Personally, I still see it as &lt;em&gt;my&lt;/em&gt; code, even if technically I didn't type all of it using my keyboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is what you do considered “vibe coding”?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No idea. I hope not… but I might not be the best judge of that either. Let me know in the comments. 🙂&lt;/p&gt;
&lt;h3&gt;
  
  
  What Do I Want to Build and Why?
&lt;/h3&gt;

&lt;p&gt;I’m known as a self-taught programmer with multiple degrees (including a PhD) in civil engineering but zero formal computer science education. Everything I know, I learned from books, blogs, videos, and working alongside many highly skilled colleagues. My learning has always been pragmatic: I pick up what I need to build something specific.&lt;/p&gt;

&lt;p&gt;That started early. In the '90s, I was “famous” (at least within my family 😄) for a few projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;VBA script&lt;/strong&gt; that made a button jump between cells in Excel.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;FoxPro-based DB and forms&lt;/strong&gt; for cataloging paper models (another hobby of mine) I built.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;point-and-click adventure&lt;/strong&gt; featuring my cousin Vovachka on a quest to kill the neighbor’s dog (yeah, sounds scary now 😬), written in VB6 (even more scary, right?).&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwxmzfep55axewglxu1v.png" alt=" "&gt;
&lt;/li&gt;
&lt;li&gt;An attempt to create a &lt;strong&gt;computer version of the BattleTech&lt;/strong&gt; board game.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I especially remember the last one. It was so much fun, my first hands-on experience trying to implement the things right (as they taught in the books), and having some primitive architecture in place. Although I got the first prototype working with a hex map, unit deployment and movement logic, I never managed to finish it. I just didn't have enough skills, experience and eventually motivation. So I abandoned the thing but it kept bugging me to this day.&lt;/p&gt;

&lt;p&gt;That's why at some point last November, 26 years after the original attempt, I decided to give it another try – with some (AI) assistance this time.&lt;/p&gt;

&lt;p&gt;Creating the game is not the aim, is kind of pointless: BattleTech is copyrighted and its IP is a mess with different companies owning different bits. Also, the thing I wanted to build already exists. &lt;a href="https://megamek.org/" rel="noopener noreferrer"&gt;MegaMek&lt;/a&gt; is a mature implementation that’s been in development for 20+ years. So the main focus is on having fun, and they say to pursue your dreams, right? 😄&lt;/p&gt;

&lt;p&gt;With that, on the 1st of December 2024, I opened a Windsurf editor, selected Claude 3.5 and put in my first prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please create me a computer implementation of a BattleTech game, use MegaMek as reference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Of course I didn't get what I wanted with 1 prompt and within 10 minutes, the prompt above is just a joke. Even after almost 5 months (and roughly 80 actual hours) spent in the company of an AI agent it still feels like the very beginning of the journey.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why AI, and How I Use It
&lt;/h2&gt;

&lt;p&gt;I’m a bit of a skeptic. The recent AI boom is largely thanks to the “rise of transformers,” but there’s not much “intelligence” in these models. They don’t "think", they just predict the next token based on prior context and probabilities from training data. Fundamentally, it’s not that different from labeling images.&lt;/p&gt;

&lt;p&gt;I doubt we’ll reach true intelligence (hello AGI 😄) with this tech alone. That might require a new kind of model to make the next big step. Still, LLMs are amazingly good at what they are supposed to do, and text proofreading and code generation are among the most valid use cases to me. And a personal open-source project seemed like the perfect playground to give AI a try.&lt;/p&gt;

&lt;p&gt;I’ve experimented with different workflows to fit my style and needs. And eventually it feels like it made me more productive. How much "more", I do not know. Definitely not 10x, not even 5x, 2x is a more realistic number, but there is no scientific way to say for sure. You spend less time typing the code, but more typing and tuning the prompt, reviewing and editing the output, and, what is also important, - consider all the time just waiting for the agent finishing its job... &lt;/p&gt;

&lt;p&gt;Ultimately, it’s about a balance between &lt;strong&gt;speed&lt;/strong&gt; and &lt;strong&gt;quality&lt;/strong&gt; or more precisely, deciding how responsibility is shared between you and the agent. You can go faster initially by giving it more freedom, but that can quickly spiral into chaos causing delays later on. I think that “quick way” only works for tiny greenfield projects. The ones you can do in one day and won't be bothered maintaining or updating in the future. In my setup, I act more as an architect. The agent is just a coder, predicting relevant pieces of C# and XAML.&lt;/p&gt;

&lt;p&gt;That way, the project stays 100% &lt;em&gt;mine&lt;/em&gt;: my architecture, my structure, my usual practices, including using my 15-year-old libraries for &lt;a href="https://github.com/anton-makarevich/Sanet.MVVM" rel="noopener noreferrer"&gt;MVVM&lt;/a&gt; and the &lt;a href="https://github.com/anton-makarevich/Sanet.Transport" rel="noopener noreferrer"&gt;transport layer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To achieve this, I had to sacrifice a great part of that productivity boost you would normally expect using an AI toolchain, with multiple rejections of proposed code, adjusting the prompts, and refactoring or even implementing some parts manually writing the code myself or leveraging traditional "copilot" mode which is still relevant and in some cases even more useful than full agentic flow.&lt;/p&gt;

&lt;p&gt;And the fact that I love to work with technologies not many other people are enjoying (.NET and AvaloniaUI) "helps" a lot. As sometimes the model just doesn't "know" how to write the Avalonia markup correctly, confusing it with other XAML frameworks' syntaxes, and I just don't have a choice other than fixing something myself. It might be better for those of you working with tech stacks where more training data is available. At the same time I would not rely on AI "decisions" in the areas where you're not very strong, as what it proposes could be (and in the most cases is) a good practice indeed, but it could be also just a "hallucinated" nonsense - you should always be able to tell the difference.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Works for Me (So Far):
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Know exactly what you want to build&lt;/strong&gt; down to technical details, so you can compare what the AI outputs to what’s in your head, and that leads us to:&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review all code, written by AI thoroughly.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have unit tests&lt;/strong&gt; for everything you can test. Many people also suggest following TDD. But for me, it really worked only if you create a test and implementation in different sessions: first ask to create a test, then create a new session, use the test as input/context and ask to implement the functionality. Otherwise, the agent tends to mess it up all the time anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep prompts focused.&lt;/strong&gt; Break features into small tasks. Start fresh sessions often. Only provide enough context for that task. Context windows are still a limiting factor, and how they’re filled is a black box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be flexible.&lt;/strong&gt; You don't need to work with an agent for everything: in many cases utilizing your IDE's refactoring capabilities or relying on old good autocomplete mode make more sense. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I could dive into the technical details, but this post is long enough. Maybe next time, if people are interested.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Takeaway
&lt;/h3&gt;

&lt;p&gt;The title says it all: &lt;strong&gt;control over speed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That mindset resonates with another of my hobbies: flying FPV drones: it always starts very exciting but if you go too fast the chances to lose control and eventually crash are getting much higher. That's why I prefer calm cinematic flights over racing or freestyle. It works for me, however if you’ve got the confidence and skills, you &lt;em&gt;can&lt;/em&gt; go fast and have more fun. Just know the risks.&lt;br&gt;
&lt;/p&gt;
&lt;div class="instagram-position"&gt;
  &lt;iframe id="instagram-liquid-tag" src="https://www.instagram.com/p/DItqPvzMb8v/embed/captioned/"&gt;
  &lt;/iframe&gt;
  
&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;So, is it worth it, even if it doesn’t make you 10x more productive?&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Absolutely.&lt;/strong&gt; It’s fun, it challenges you in new ways, and no, it won’t replace you as a software engineer. It’s just another tool in the toolbox.&lt;/p&gt;

&lt;p&gt;My main concerns aren’t technical, but ethical. Energy usage, training data, all that. But hey, at least to me it makes much more sense than mining Bitcoin.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>vibecoding</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
