INITIALIZING
c#

BattleBots + MCP = Fun: Building Custom Tools in C# That Your IDE and Workflows Understand

BattleBots + MCP = Fun: Building Custom Tools in C# That Your IDE and Workflows Understand

Opening

AI harnesses and IDE extensions are evolving fast, and Microsoft’s Model Context Protocol (MCP) is emerging as the glue that makes custom tools discoverable and usable by editors like VS Code and Visual Studio Insiders.

For this post, I’ll be using .NET 10 along with Visual Studio Insiders (which you can download here 👉 https://visualstudio.microsoft.com/insiders/).

And because I absolutely love BattleBots (yes, I’m one of those people who cheers every time Tombstone spins up), I thought it would be fun to wrap a simple BattleBots API in MCP and show how it can power workflows, AI harnesses, and automation.

We’ll build something fun and practical:

  • A C# BattleBots API exposing competitors’ names.
  • Wrap it with an MCP server using the official Microsoft SDK.
  • Debug it with MCP Inspector.
  • Wire it into VS Code Insiders / Visual Studio Insiders.
  • And finally, automate it inside n8n.

If you’ve ever thought, “How do I bring my own APIs into AI-powered tools?” — this is your starting point.


Step 1: The BattleBots API

We’ll begin with a simple .NET 10 minimal API that returns a list of BattleBots competitors.

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/battlebots", () =>
{
    return new[]
    {
        "Tombstone",
        "Witch Doctor",
        "Hydra",
        "End Game",
        "SawBlaze"
    };
});

app.Run();

Run it with:

dotnet run

Visit http://localhost:5000/battlebots and you’ll get back JSON like:

["Tombstone","Witch Doctor","Hydra","End Game","SawBlaze"]

That’s our foundation.


Step 2: Wrapping with an MCP Server

Now that we have our API, let’s expose it as an MCP Tool using Microsoft’s Model Context Protocol SDK.

The MCP project is slightly more involved than the API project because we want:

  1. A strongly typed MCP Server
  2. HTTP client resiliency (using IHttpClientFactory)
  3. A reusable tool class (BattleBotsTools)
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);

var apiBase = Environment.GetEnvironmentVariable("BATTLEBOTS_API_BASE") ?? "https://localhost:7000";

builder.Services
    .AddHttpClient("BattleBotsApi", client =>
    {
        client.BaseAddress = new Uri(apiBase);
        client.DefaultRequestHeaders.Accept.Add(
            new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
            builder.Environment.IsDevelopment() &&
            apiBase.StartsWith("https://localhost", StringComparison.OrdinalIgnoreCase)
                ? true
                : errors == System.Net.Security.SslPolicyErrors.None;
        return handler;
    });

builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();
BattleBotsTools.Configure(app.Services);
app.MapMcp();
app.Run("http://localhost:3001");
[McpServerToolType]
public static class BattleBotsTools
{
    private static IHttpClientFactory _httpClientFactory = default!;

    public static void Configure(IServiceProvider services)
    {
        _httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
    }

    [McpServerTool, Description("Return BattleBots competitor names.")]
    public static string ListBattleBots()
    {
        if (_httpClientFactory is null)
            throw new InvalidOperationException("Not configured. Call Configure(app.Services).");

        var client = _httpClientFactory.CreateClient("BattleBotsApi");
        var path = Environment.GetEnvironmentVariable("BATTLEBOTS_API_LIST_PATH") ?? "/battlebots";

        using var response = client.GetAsync(path).GetAwaiter().GetResult();
        response.EnsureSuccessStatusCode();
        return response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    }
}

One C# gotcha worth knowing before you hit compile: if BattleBotsTools lives in the same Program.cs as the top-level statements (which is exactly how the repo does it), the class declaration must come after app.Run() — C# refuses to let type declarations sit above top-level statements, no exceptions, no negotiating. Or just give the class its own file and move on with your life.

Why IHttpClientFactory?

  • Resiliency: DNS refresh, pooling, retries.
  • Configurable via env vars.
  • Extensible for logging/auth/policies.

The “Magic” of [McpServerTool]

That attribute is what generates the tool specification that MCP clients like Inspector, VS Code, and n8n use to discover/invoke your tool.

Tool spec example:

{
  "name": "list-battlebots",
  "description": "Return BattleBots competitor names.",
  "inputSchema": { "type": "object", "properties": {} },
  "outputSchema": { "type": "string" }
}

In VS Code Insiders, you’ll see it like this:

BattleBots (MCP Server)
└── Tools
    └── list-battlebots
        Description: Return BattleBots competitor names.
        Input: none
        Output: string

Now you can execute both projects and it will use the MCP server tool to proxy our API


Step 3: Inspect with MCP Inspector

Install & run:

npx @modelcontextprotocol/inspector
  • UI → http://localhost:6274
  • Proxy → port 6277

Or Docker:

docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest

In the UI, connect via Streamable HTTPhttp://localhost:3001/.
You’ll see and run list-battlebots.

The Inspector lists every tool the server exposes — in our case the lonely but proud list-battlebots — and lets you invoke it right there, showing the raw streamed response. Think of it as Postman for MCP: if something's wrong with your tool, this is where you find out before your AI does.


Step 4: Integrating with VS Code & Visual Studio Insiders

Create .mcp.json:

{
  "servers": {
    "battlebots": {
      "type": "http",
      "url": "http://localhost:3001/"
    }
  }
}

Put it in %USERPROFILE%\.mcp.json (global) or <SOLUTIONDIR>\.mcp.json (per repo).
Now in VS Code/Visual Studio Insiders → tools panel → run list-battlebots.

Also you can add it from the chat window using the + sign

Pick “Add MCP Server,” choose HTTP, paste the URL, and the tool shows up in your chat's tool list, ready to be invoked.

Now we can start interacting with the tool.

Ask the chat something like “list the battlebots” and you'll watch it call your tool and come back with the roster — your own API, answering inside your IDE's chat, no extension published, no marketplace approval, nothing.

Ok ok that is interesting, but how can you use the tool for more powerful integration? glad you asked.


Step 5: Automating with n8n

Example workflow:

  • Trigger: When chat message received
  • Ollama Chat Model (llama3.1:8b)
  • AI Agent orchestrates MCP calls
  • MCP battlebots tool fetches competitor names

The workflow itself is small enough to rebuild by hand in a couple of minutes: a chat trigger feeding an AI Agent node, with the Ollama chat model attached as the brain and the MCP Client tool pointed at our server as the hands.

For the MCP battlebots tool configuration, set the endpoint to your MCP server's URL and the transport to HTTP Streamable.

Note that since we are running n8n locally, it will need the host.docker.internal approach we did on the post https://darthseldon.net/n8n-workflows-ollama-ai-agents-docker-edition-host-your-own-ai-and-create-your-own-automation-army/

Finally we connect the chat model (llama3.1:8b in my case) and the magic that connects the AI to the tool: the prompt. That's genuinely all the “integration code” there is — a sentence telling the agent what the tool does and when to use it.

Example 1: Filter by Letter

Ask the agent to return only bots starting with “s”, and it calls the tool, gets the full roster back, and filters it down on its own — the tool never learned how to filter, the model just did it. Division of labor at its finest.

Example 2: Order & Count

We can also ask the agent to order the names alphabetically and count the characters in each name — and it does, returning a tidy sorted list with per-name character counts, no extra code written anywhere.

This demonstrates that the combination of Ollama + MCP tool + n8n isn’t limited to raw lookups — you can transform and enrich the data however you like. (RAG pattern)

In another post I will show you how can you use multiple tools and follow the RAG pattern (Retrieve Augment and Generate). This way you can use the battlebot api, use them to store the length in airtable or find information and "augment" the information about an specific bot.


Step 6: Architecture with C4

Here’s a C4 Container Diagram (PlantUML + C4 library), updated to show the IHttpClientFactory.

@startuml
!include <C4/C4_Container>

Person(dev, "Developer", "Builds & tests tools")
System_Ext(vscode, "VS Code Insiders", "MCP-aware editor")
System_Ext(inspector, "MCP Inspector", "Debug/inspect MCP tools")
System_Ext(n8n, "n8n Automation", "Schedules and orchestrates calls")

System_Boundary(battlebots, "BattleBots Tooling") {
  Container(api, "BattleBots API", "ASP.NET Core Minimal API", "GET /battlebots => JSON list of competitors")

  Container_Boundary(mcpSrv, "BattleBots MCP Server (ASP.NET Core + MCP SDK)") {
    Container(mcp, "MCP Server Host", "Microsoft MCP SDK", "Exposes tool: ListBattleBots")
    Container(httpFactory, "IHttpClientFactory", ".NET Built-in", "Creates resilient HttpClient instances")
  }
}

Rel(dev, api, "Calls directly for local testing", "HTTP")
Rel(mcp, api, "Fetches competitor names\nvia HttpClient created by IHttpClientFactory", "HTTP")
Rel(httpFactory, mcp, "Provides HttpClient", "DI")
Rel(inspector, mcp, "Connects via HTTP transport", "HTTP stream")
Rel(vscode, mcp, "Discovers & executes MCP tool", "MCP")
Rel(n8n, mcp, "Automates tool execution", "HTTP")

SHOW_LEGEND()
@enduml
C4 container diagram: BattleBots API, MCP Server with IHttpClientFactory, and the external clients (VS Code, MCP Inspector, n8n)

Step 7: Sequence Diagram of n8n Execution

@startuml
title n8n + MCP + BattleBots — Request Flow

actor User as user
participant "AI Agent\n(n8n)" as agent
participant "Ollama\n(llama3.1:8b)" as llm
participant "MCP battlebots tool\n(httpStreamable)" as tool
participant "MCP Server Host\n(Microsoft MCP SDK)" as mcp
participant "IHttpClientFactory" as factory
participant "BattleBots API\n(ASP.NET Core Minimal API)" as api

user -> agent: "Who are the BattleBots competitors?"
agent -> llm: Analyze prompt / plan
llm -> tool: Invoke tool: list-battlebots
tool -> mcp: Connect (streamable HTTP)
mcp -> factory: CreateClient("BattleBotsApi")
factory --> mcp: HttpClient
mcp -> api: GET /battlebots
api --> mcp: 200 OK\n["Tombstone","Witch Doctor",...]
mcp --> tool: Stream tool result
tool --> llm: Tool output (competitor names)
llm --> agent: Draft final message
agent --> user: Answer with competitor list

@enduml
Sequence diagram: user prompt flowing through n8n AI Agent, Ollama, MCP tool, MCP server, IHttpClientFactory, and the BattleBots API

Conclusion

We’ve gone end-to-end:

  • Built a C# API
  • Wrapped it with an MCP server
  • Validated with MCP Inspector
  • Integrated into VS Code/Visual Studio Insiders
  • Automated with n8n

MCP isn’t just for your IDE’s AI — it’s a universal protocol for turning APIs into discoverable tools.

Next time, we’ll explore making the tool smarter — maybe even adding OData filters so the AI can query bots directly.

Happy coding!!!


Code Repository

All the source code used in this post — including the BattleBots API and MCP server — is available on GitHub:

👉 https://github.com/jtenoriodseldon/mcpblogdemo

Clone the repo, run the API + MCP server locally, and wire up the n8n workflow following Step 5 to get started right away.