---
title: "Serve tools over HTTP in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/serve-over-http-without-aspnet/
description: "Streamable HTTP exposes tools at a URL. OneModelContext includes a loopback HTTP listener, so a local server doesn’t need ASP.NET Core."
version: "1.0.1"
---

# Serve tools over HTTP in C#

**OneModelContext** is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/)

Serve Model Context Protocol® tools on a local URL without ASP.NET.

```shell
dotnet add package RecursiveAI.OneModelContext
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModelContext;
await ModelContext.ServeHttpAsync(
    new Calculator(), port: 3001,
    onListening: url =>
        Console.WriteLine(url));

public partial class Calculator
{
    [Tool] public int Add(int a,int b)=>a+b;
}
```

**[Download this example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/serve-over-http-without-aspnet/serve-over-http-without-aspnet.zip)**: unzip, then `dotnet run`.

The listener accepts local connections only. The snippet at the top keeps serving until stopped and prints:

```text
http://127.0.0.1:3001/mcp
```

## The full program

The download picks a free port, waits for the listener to bind, makes an HTTP call and cancels the listener. The snippet at the top is the long-running version on port 3001.

```csharp
using RecursiveAI.OneModelContext;
using var stop = new CancellationTokenSource();
var ready = new TaskCompletionSource<Uri>();
var serving = ModelContext.ServeHttpAsync(
    new Calculator(), port: 0,
    onListening: url => ready.SetResult(url),
    cancellationToken: stop.Token);
var url = await ready.Task;
await using (var client = await ModelContext.ConnectAsync(
    ContextTransport.Http(url)))
{
    Console.WriteLine("Transport: Streamable HTTP");
    Console.WriteLine((await client.CallToolAsync("add", new { a = 2, b = 40 })).Text);
}
stop.Cancel();
await serving;

public partial class Calculator
{
    /// <summary>Add two integers.</summary>
    [Tool]
    public int Add(int a, int b) => a + b;
}
```

`dotnet run` prints:

```text
Transport: Streamable HTTP
42
```

A port already in use prevents the listener from binding; use port 0 when you want the system to choose one.

The core package includes the source generator for `[Tool]` methods. Keep the class `partial`; the generator supplies its registration code. The download pins the package version in `Example.csproj`.

## Variations

Change the arguments. Replace the tool call inside the full program with:

```csharp
Console.WriteLine((await client.CallToolAsync(
    "add", new { a = -2, b = 44 })).Text);
```

```text
42
```

## Related recipes

- [Build a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)
- [Host a Model Context Protocol® server on ASP.NET Core](https://recursiveai.org/onemodelcontext/cookbook/host-on-aspnet-core/)
- [Require OAuth sign-in on a Model Context Protocol® server](https://recursiveai.org/onemodelcontext/cookbook/require-sign-in-on-a-server/)
- [Sign in to a protected Model Context Protocol® server in .NET](https://recursiveai.org/onemodelcontext/cookbook/sign-in-to-a-protected-server/)

---

## The short answer

For an MCP Streamable HTTP server in C# (Model Context Protocol®), call `ModelContext.ServeHttpAsync` with a generated tool class. OneModelContext’s core package includes an HTTP listener, so a loopback tool service can remain a console project without ASP.NET Core.

This host is specifically local: the convenience API listens on loopback. Choose ASP.NET Core when you want a web application host instead. The HTTP transport alone does not turn the local listener into a public service.

## How do I create a Streamable HTTP server in C#?

Install the core package in a .NET 10 console project. Supply a partial class with `[Tool]` methods, then await `ServeHttpAsync`. The method returns a task for the serving lifetime, not an immediate result from a tool call. Keep awaiting it while clients should be able to connect.

The snippet at the top uses port 3001 and prints the endpoint URL when binding has completed. The full program uses port 0 instead, which lets the system choose a free port. Its `onListening` callback receives the complete endpoint URI. Waiting for that callback avoids racing the client against listener startup.

The default path is `/mcp`, a code-level endpoint identifier. Changing the port does not change the path. The complete client uses the URI from the callback rather than reconstructing it. This is particularly helpful when the program is one of several local tools running at once.

## What is the difference between stdio and Streamable HTTP?

With stdio, the client launches a process and owns its input and output streams. With HTTP, the server listens first and the client connects to a URL. The calculator’s tool method is unchanged: only the transport and serving lifecycle differ.

Use stdio when a desktop application should start and stop the tool process. Use the built-in HTTP listener when a separate local process needs a URL. Use an ASP.NET Core endpoint when the protocol should live in an existing web app with its web-host features.

The downloadable HTTP example deliberately starts both sides in one process. It waits for binding, makes a real HTTP call, disposes the client and cancels the listener. That is why it exits after printing the result instead of leaving a server running. The short snippet is the long-running version.

## How is Streamable HTTP different from legacy SSE?

Streamable HTTP replaces the old HTTP+SSE transport, and this endpoint also serves older clients.

The library’s endpoint handles both modern and legacy protocol eras. As the [HTTP limits documentation](https://recursiveai.org/onemodelcontext/docs/http-limits/) explains, modern requests carry no session, while legacy requests are pinned to a session and can resume an event stream. Clients should use protocol negotiation rather than assume every server has the same handshake or session behavior.

This recipe does not implement a separate older SSE transport. It exposes the library’s Streamable HTTP endpoint and lets the client negotiate. Server-sent event streams used within that endpoint are not a reason to add an unrelated second route.

## Can I expose the built-in HTTP listener remotely?

Not with `ServeHttpAsync`: its documented network boundary is loopback. The [API and HTTP guide](https://recursiveai.org/onemodelcontext/docs/http-limits/) also document host and origin checks and request-size bounds. Those defaults matter even for a local listener because a browser can send requests toward local ports.

For a remotely reachable web app, use the [ASP.NET Core recipe](https://recursiveai.org/onemodelcontext/cookbook/host-on-aspnet-core/) and configure the host and authorization deliberately. Don’t disable origin checks as a substitute for deciding which clients should call tools. A listener being reachable is not an access-control policy.
