DalSoft.RestClient 5.1 is a feature release with two headlines: a handler that lets you talk to MCP (Model Context Protocol) servers as if they were REST APIs, and first class typed clients via AddRestClient<TClient>().
MCP in one line
If you've been following the AI tooling space you'll know MCP is the protocol LLM agents use to call tools. More and more services are exposing an MCP endpoint alongside (or instead of) a REST API - and from a .NET app there hasn't been a lightweight way to just call an endpoint.
Now there is. All you need to do is point a DalSoft.RestClient at the MCP endpoint, add the handler, and call tools:
IRestClient mcp = new RestClient("https://gateway.mcpservers.org/yahoo-finance/mcp", new Config().UseMcpHandler());
var tools = await mcp.ListTools();
Console.WriteLine(tools.tools[0].name); // get_quote
var quotes = await mcp.CallToolJson("get_quote", new { symbols = new[] { "MSFT", "AAPL" } });
Console.WriteLine($"{quotes[0].shortName} {quotes[0].regularMarketPrice}"); // Microsoft Corporation 481.5
That's a real, public MCP server - it's what our integration tests run against.
Underneath, MCP is JSON-RPC over the Streamable HTTP transport, and there is a surprising amount of ceremony involved: an initialize handshake, a session id header, a protocol version header, responses that might arrive as plain JSON or as a Server-Sent Events stream, and a JSON-RPC envelope around everything. The McpHandler does all of it:
- Wraps whatever you post in the JSON-RPC envelope (
jsonrpc,id). - Initializes the session lazily on your first call, then tracks
Mcp-Session-Idand the negotiatedMCP-Protocol-Versionon every request - and transparently re-initializes if the server expires the session. - Reads SSE response streams until your response arrives, surfacing any
notifications/progressthe server sends along the way viaOnNotification. - Unwraps the envelope so the response is the
result, and throwsMcpExceptionfor JSON-RPC errors.
It's a DelegatingHandler like every other handler in the pipeline, so authentication, retries, logging, IHttpClientFactory and unit testing with UseUnitTestHandler() all work exactly as they do for everything else in DalSoft.RestClient.
Tool results aren't JSON (but they usually are)
One thing that surprised us building this: MCP tool results aren't data. They're content blocks for an LLM - text, image, audio, resource - because an agent could be handed any media. So a finance tool that returns a quote may give you a JSON string inside a text block, and you'd be writing result.content[0].text and parsing it yourself.
If the MCP endpoint's result.content[0].text is returning JSON, CallToolJson() has got your back - dynamically typed like every other DalSoft.RestClient response, or strongly typed with CallToolJson<T>() - and if the server supports the spec's newer structuredContent it uses that instead. Crucially it throws McpException when the tool returns isError, because a tool execution error is a successful JSON-RPC response in MCP and it's far too easy to mistake one for data.
The plain CallTool() still gives you the raw content blocks when that's what you want.
Why not the official SDK?
Fair question - there's an official ModelContextProtocol package from Microsoft and Anthropic, plus DotnetFastMCP and MCPSharp. If you're building an agent - wiring tools into an LLM loop, implementing a server, handling sampling or elicitation - use the official SDK. It's the reference implementation and it has full protocol coverage.
McpHandler is for a different job: you just need to call an MCP server like any other API. Nothing new to learn, no new dependencies, automatically handled types or dynamic (if that's your thing), plain HttpClient underneath, works on .NET Standard 2.0 and .NET Framework - and testable the way you already test. There's a fuller comparison in the docs.
Typed clients done properly
The second headline fixes something that has bugged us for a while. The static typing docs used to recommend this for a typed client:
public GitHubClient(HttpClient httpClient)
{
_restClient = new RestClient(
new HttpClientWrapper(httpClient, new Headers(new { UserAgent = "MyClient" })),
"https://api.github.com");
}
Ugly, and not obvious. 5.1 adds AddRestClient<TClient>(), the AddHttpClient<TClient>() pattern you already know, and your typed client just takes an IRestClient:
builder.Services
.AddRestClient<GitHubClient>("https://api.github.com", new Headers(new { UserAgent = "MyClient" }))
.UseRetryHandler(); // Any Use*Handler applies to this client only
public class GitHubClient
{
private readonly IRestClient _restClient;
public GitHubClient(IRestClient restClient) => _restClient = restClient;
public Task<List<Repository>> GetRepositories(string user) =>
_restClient.Resource($"users/{user}/repos").Get<List<Repository>>();
}
Typed clients are transient (like AddHttpClient<TClient>()), the underlying HttpClient is pooled by IHttpClientFactory, and your constructor can take other dependencies alongside IRestClient. AddRestClient<TClient, TImplementation>() registers an interface with its implementation.
Taking IRestClient rather than RestClient keeps your client testable, and because Resource(), Get<T>(), Headers() and friends are all on the interface you lose nothing. And yes, the two features combine - register a typed client for an MCP server with AddRestClient<WeatherMcpClient>(url).UseMcpHandler() and you've got an SDK for it.
Documentation
https://restclient.dalsoft.io/docs/mcphandler/ and https://restclient.dalsoft.io/docs/static-typing/
Get it
> dotnet add package DalSoft.RestClient
The full notes are in the readme. If you hit anything we missed, raise an issue - and if RestClient is useful to you or your company, please consider becoming a sponsor ❤️