Archive

Posts Tagged ‘mcp’

Building an MCP Server for a Vehicle Registration API in C# .NET 8

If you’ve spent any time with Claude recently, you may have noticed it can do things that feel surprisingly connected to the real world — looking up domain expiry dates, checking live weather, querying your calendar. Most of that happens through MCP: the Model Context Protocol, an open standard that lets AI assistants talk directly to external APIs and services.

I’ve been running RegCheck since 2004 — a vehicle registration lookup API covering 50+ countries, built on a .NET/IIS stack. This is the story of how I built an MCP server for it over the course of a few hours, deployed it to Google Cloud Run, and made it available for anyone to connect to Claude.

The full source is on GitHub: github.com/infiniteloopltd/RegCheckMCP


What is MCP, and why does it matter for API developers?

MCP is essentially a standardised way for AI assistants to discover and call your API tools. Instead of a user having to copy and paste a plate number into a lookup form, then copy the result back into their conversation, Claude can just do it — invisibly, mid-conversation, whenever it recognises that a vehicle lookup is relevant.

The interesting thing for API developers is that MCP is model-agnostic. The same server works with Claude, and potentially other AI clients that implement the standard. You build it once.


The architecture

The RegCheck API is a classic ASMX web service — it returns XML, with the vehicle data serialised as JSON inside a vehicleJson node. Each country has its own web method: Check for UK, CheckIreland for Ireland, CheckGermany for Germany, and so on across 50+ endpoints.

The MCP server is a thin ASP.NET Core 8 layer that:

  1. Accepts MCP tool calls over Streamable HTTP
  2. Reads the caller’s RegCheck username from an X-Api-Key header
  3. Maps the request to the correct ASMX endpoint for the given country
  4. Parses the XML response and returns the vehicleJson content back to Claude

The .NET MCP SDK

Microsoft maintains an official C# SDK for MCP — ModelContextProtocol.AspNetCore on NuGet. It integrates cleanly with ASP.NET Core’s dependency injection and minimal API hosting. Setting up a basic server is straightforward:

csharp

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("regcheck");
builder.Services.AddMcpServer()
.WithHttpTransport(options => options.Stateless = true)
.WithTools<VehicleLookupTools>();
var app = builder.Build();
app.MapMcp("/mcp");
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
app.Run($"http://0.0.0.0:{port}");

The Stateless = true option is important for Cloud Run — it removes the requirement for a session ID header, which is the right choice for a public API where each request is independently authenticated.


Defining tools

Tools are just C# methods decorated with attributes. The [Description] attributes are doing real work here — they’re what Claude reads to decide when to invoke the tool. Vague descriptions mean missed invocations; specific ones mean Claude reliably picks up the tool when a user mentions a plate number.

csharp

[McpServerTool(Name = "lookup_vehicle_uk")]
[Description("Look up a UK vehicle registration plate. Returns make, model, colour, fuel type, " +
"MOT expiry, tax status, and engine size. Use when the user provides a UK vehicle " +
"registration number such as 'AB12 CDE' or 'AB12CDE'.")]
public async Task<string> LookupVehicleUk(
[Description("UK vehicle registration number, e.g. 'AB12CDE'. Spaces are ignored.")]
string registration)
{
return await CallEndpoint("Check", registration, null);
}

For the multi-country case, rather than writing 50+ separate methods, a single generic tool with a country code parameter keeps things maintainable. The country-to-endpoint mapping lives in a dictionary:

csharp

private static readonly Dictionary<string, string> CountryEndpoints = new()
{
{ "IE", "CheckIreland" },
{ "DE", "CheckGermany" },
{ "FR", "CheckFrance" },
// ... 50+ entries
};

A handful of countries (Australia, USA, Canada, Pakistan) require a state parameter as well, handled by a HashSet<string> check with a helpful error message if the caller omits it.


Authentication

The RegCheck API authenticates by username, passed as a query parameter. Rather than building OAuth (which the Anthropic Connectors Directory requires for directory listing, but not for custom connectors), I went with a simple X-Api-Key header — users pass their RegCheck username as the key value.

Because Streamable HTTP sends headers with every request, reading it is trivial via IHttpContextAccessor:

csharp

var apiKey = _httpContextAccessor.HttpContext?.Request.Headers["X-Api-Key"].FirstOrDefault();

No session management, no token storage — just read it fresh on every tool call.


Parsing the ASMX response

The RegCheck ASMX endpoints return XML. The vehicle data itself is JSON, serialised inside a vehicleJson node. Extracting it with LINQ to XML is a one-liner:

csharp

var vehicleJson = XDocument.Parse(xml)
.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "vehicleJson")
?.Value;

Using LocalName handles XML namespace variations cleanly. The result is passed straight back to Claude as a JSON string — Claude can reason over the field names without any further transformation.

A typical response looks like this:

json

{
"Description": "1997 Vauxhall Corsa Breeze, 1389CC Petrol, 5DR, Manual",
"RegistrationYear": "1997",
"CarMake": { "CurrentTextValue": "Vauxhall" },
"CarModel": { "CurrentTextValue": "Corsa" },
"FuelType": { "CurrentTextValue": "Petrol" },
"EngineSize": { "CurrentTextValue": "1389CC" }
}

Testing with MCP Inspector

Before deploying anywhere, I tested locally using the official MCP Inspector — a browser-based tool that acts like Postman for MCP servers:

npx @modelcontextprotocol/inspector

Point it at http://localhost:8080/mcp, add the X-Api-Key header in the connection config, connect, and you get a UI listing all registered tools. You can invoke them manually, see the raw JSON input and output, and iterate on descriptions without touching Claude at all. It significantly tightens the development loop.


Deploying to Cloud Run

The server deploys to Google Cloud Run with a single command:

gcloud run deploy regcheckmcp \
--source . \
--region europe-west2 \
--allow-unauthenticated

Cloud Run injects a PORT environment variable (always 8080), which the server reads at startup. Binding to 0.0.0.0 rather than localhost is essential — Cloud Run won’t route external traffic to a loopback-only listener.

The live endpoint is:

https://regcheckmcp-526628810409.europe-west2.run.app/mcp

Connecting to Claude

In Claude (claude.ai), go to Settings → Connectors → Add custom connector, enter the server URL, and add X-Api-Key as a custom header with your RegCheck username as the value. That’s it — Claude will now invoke the vehicle lookup tools automatically whenever you mention a registration plate in conversation.

You can try something like:

“What year is the car with plate AB12CDE?”

Claude will call lookup_vehicle_uk, get the response, and answer directly — no copying, no form filling.


What’s next

The immediate next step is submitting to the Anthropic Connectors Directory, which requires adding OAuth 2.1 with PKCE — so that users can authenticate via a proper consent flow rather than pasting an API key manually. The MCP server logic doesn’t change; it’s purely an auth layer addition.

Longer term, a VIN lookup tool is a natural addition — the RegCheck API supports it via a VinCheck endpoint, and it’s a different enough use case to warrant its own tool with its own description.


Code

Everything is open source and available at github.com/infiniteloopltd/RegCheckMCP. The full implementation including the country endpoint map, state validation, and XML parsing is under 150 lines of C#.

If you have a REST or ASMX API and you’re wondering whether it’s worth building an MCP server — based on this experience, the answer is yes, and it’s less work than you might think.