MCP Tools

This framework allows to provide tools to AI agents such as Claude via the Model Context Protocol (MCP), in the same way web services are provided with the webservice or controller frameworks. The methods you implement are offered to the agent as tools, which it can decide to call while working on a task.

Hosting Tools

To provide tools, create an endpoint with Tools.Create() and add the classes or functions implementing your tools. Methods marked with the [Tool] attribute will be offered to the agent, described by the summary you provide.

C#
using System.ComponentModel.DataAnnotations;

using GenHTTP.Api.Content;
using GenHTTP.Api.Content.Documentation;
using GenHTTP.Api.Protocol;

using GenHTTP.Engine.Internal;

using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Mcp;

var tools = Tools.Create()
                 .Server("library", "1.0.0", "Library")
                 .Instructions("Search for books before lending them to a reader.")
                 .Add<LibraryTools>()
                 .Add("get_time", "Returns the current time of the library (UTC)", [Tool(ReadOnly = true)] () => DateTime.UtcNow);

var app = Layout.Create()
                .Add("mcp", tools);

await Host.Create()
          .Handler(app)
          .Development()
          .RunAsync();

public record Book(int Id, string Title, string? LentTo);

public class LibraryTools
{
    private static readonly List<Book> Books = [new(1, "SPQR: A History of Ancient Rome", null)];

    [Tool(ReadOnly = true)]
    [Summary("Searches the books of the library by title")]
    public List<Book> FindBooks([Summary("Part of the title, e.g. 'Rome'")] string query, [Range(1, 50)] int limit = 10)
        => Books.Where(b => b.Title.Contains(query, StringComparison.OrdinalIgnoreCase)).Take(limit).ToList();

    [Tool]
    [Summary("Lends a book to a reader")]
    public Book LendBook([Summary("The id of the book, as returned by find_books")] int bookId, [Summary("The name of the reader")] string reader)
    {
        var book = Books.FirstOrDefault(b => b.Id == bookId)
                   ?? throw new ProviderException(ResponseStatus.NotFound, $"There is no book with id {bookId}.");

        if (book.LentTo != null)
        {
            throw new ProviderException(ResponseStatus.Conflict, $"'{book.Title}' is already lent to {book.LentTo}.");
        }

        var lent = book with { LentTo = reader };

        Books[Books.IndexOf(book)] = lent;

        return lent;
    }

}

This example provides the tools find_books, lend_book and get_time via http://localhost:8080/mcp. You can add the endpoint to Claude Code and ask questions such as “Which books about Rome are in the library?”:

Shell
claude mcp add --transport http library http://localhost:8080/mcp

Any other client supporting the Streamable HTTP transport of MCP can connect to the endpoint the same way.

Defining Tools

Tools can be added in several ways, which can be mixed within a single endpoint:

MethodDescription
Add<T>()Adds the methods marked with [Tool] of the given class. A single instance is created via its parameterless constructor.
Add(instance)Adds the methods marked with [Tool] of the given instance.
Add(name, description, function)Adds a single tool implemented by the given function.

The name of a tool defaults to the name of the method in snake case, so FindBooksAsync becomes find_books. You can pass a different name to the attribute ([Tool("search")]). Every tool needs a description that tells the model what it does, which is read from the [Summary] (or [Description]) attribute on the method. Additional [Remarks] are appended to the description.

Tool classes need to be dedicated to tools - classes that also provide webservice or controller methods are rejected.

Hints

Besides the name, the [Tool] attribute allows to describe the behavior of the tool, so clients can decide whether to ask the user before invoking it:

PropertyDescription
TitleA human-readable name of the tool, shown by clients.
ReadOnlyThe tool does not modify its environment.
DestructiveThe tool may perform destructive updates.
IdempotentCalling the tool repeatedly with the same arguments has no additional effect.
OpenWorldThe tool interacts with external entities (such as the web).

On functions, the attribute can be used to set those hints as well, but not the name, which is passed to Add instead.

Arguments

The parameters of a tool become its arguments. The framework generates a JSON schema for them, so the model knows what to pass, and validates the arguments sent by the client before the tool is invoked. Arguments with a default value and nullable members of input records are optional.

Validation attributes from System.ComponentModel.DataAnnotations (such as [Range], [Length], [StringLength], [RegularExpression], [EmailAddress], [Url] or [AllowedValues]) are added to the schema and checked on every call, also for the properties of records passed as arguments. If an argument is invalid, the model receives an error listing all violations, so it can correct the call.

C#
[Tool]
[Summary("Registers a new reader")]
public Reader Register([Length(2, 50)] string name, [EmailAddress] string mail, [Range(16, 120)] int? age = null) => /* ... */;

Values provided by the framework, such as the IRequest or the authenticated user, are injected as they are for web services and are not visible to the model. Streams and binary data are not supported as arguments.

Results

The return value of a tool is converted into the result read by the model:

Return TypeResult
void, TaskAn empty, successful result.
stringThe text, as it is (e.g. Markdown).
Any other typeSerialized to JSON. Objects are also sent as structured content, described by an output schema.
ToolResultFull control over the content sent to the client.

ToolResult allows to return text, JSON, images, audio or links to resources the client can fetch:

C#
[Tool(ReadOnly = true)]
[Summary("Returns the cover of a book")]
public ToolResult GetCover(int bookId)
{
    var image = File.ReadAllBytes($"covers/{bookId}.png");

    return ToolResult.From(new TextContent($"The cover of book {bookId}"),
                           new ImageContent(image, "image/png"));
}

Asynchronous methods (Task<T>, ValueTask<T>) are awaited, and the result types of the service frameworks are unwrapped.

Errors

If a tool throws a ProviderException, the model receives an error with the message of the exception, so you can tell it what went wrong and how to fix it. Alternatively, you can return ToolResult.Error("..."). Other exceptions are reported to the model as an unexpected failure and are logged by the server.

Errors are reported to the model as the result of the tool call, so it can react to them - the HTTP request itself succeeds.

Security

The endpoint is a regular handler, so you can add concerns such as authentication to it. Combined with the [RequireRole] interceptor, access can be restricted per tool. The roles required by a tool are added to its description, so the model knows about them. To inject the authenticated user into your tools, add a user injector.

C#
using GenHTTP.Api.Content.Authentication;
using GenHTTP.Api.Content.Documentation;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.Authentication;
using GenHTTP.Modules.Authentication.ApiKey;
using GenHTTP.Modules.Mcp;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Reflection.Injectors;

var auth = ApiKeyAuthentication.Create()
                               .Authenticator(AuthenticateAsync);

var tools = Tools.Create()
                 .Add<AdminTools>()
                 .Injectors(Injection.Default().Add(new UserInjector<ApiKeyUser>()))
                 .Authentication(auth);

static ValueTask<IUser?> AuthenticateAsync(IRequest request, string key)
    => new(key == "abc" ? new ApiKeyUser(key, "ADMIN") : null);

public class AdminTools
{

    [Tool(Destructive = true)]
    [Summary("Removes all books from the library")]
    [RequireRole("ADMIN")]
    public string Purge(ApiKeyUser user) => $"Purged by {user.DisplayName}";

}

The API key can then be passed by the client, e.g. with Claude Code:

Shell
claude mcp add --transport http library http://localhost:8080/mcp --header "X-API-Key: abc"

Agents do not send an Origin header. Requests from web browsers do, and are rejected by default to prevent malicious web pages from accessing a server running on your machine or within your network (e.g. via DNS rebinding). If you would like to use the endpoint from a web page, allow its origin:

C#
var tools = Tools.Create()
                 .AllowOrigin("https://example.com");

Dependency Injection

With the dependency injection module enabled on your host, tool classes are created per call with their dependencies resolved, and services can be injected into the parameters of your tools:

C#
using GenHTTP.Modules.DependencyInjection;
using GenHTTP.Modules.Mcp;

var tools = DependentTools.Create()
                          .Add<LibraryTools>();

public class LibraryTools(BookRepository books)
{

    [Tool(ReadOnly = true)]
    [Summary("Searches the books of the library by title")]
    public List<Book> FindBooks(string query, AuditLog log) => /* ... */;

}

To create the instances of your tool classes in another way, pass a factory to InstanceProvider(...).

Configuration

The endpoint provides the following options:

MethodDescription
Server(name, version, title)Identifies the server to clients (defaults to the name and version of the entry assembly).
Instructions(text)Explains to the model how to use the tools, e.g. which tool to call first. Keep them short, as clients may shorten them.
CacheDuration(duration)How long clients may cache the list of tools (defaults to 5 minutes).
Serialization(options)The JsonSerializerOptions used to read arguments, write results and generate the schemas. Pass options with a source generated context to avoid reflection.
Injectors(registry)The injectors providing framework values to tools.
AllowOrigin(origin)Allows a web page served from the given origin to use the endpoint.

Protocol Support

The endpoint implements the Streamable HTTP transport of MCP for the stateless protocol revision 2026-07-28 as well as the earlier revisions 2025-03-26 to 2025-11-25, so both current and older clients can connect. As the endpoint does not keep sessions, it can be scaled horizontally without further configuration.

The framework provides tools only. Prompts and resources are not supported yet - if you need them, feel free to get in touch via GitHub or Discord.

Every operation is logged with the category GenHTTP.Modules.Mcp, including the tools called, their duration and the errors they reported.