Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GenHTTP.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Project Path="Modules\IO\GenHTTP.Modules.IO.csproj" />
<Project Path="Modules\Layouting\GenHTTP.Modules.Layouting.csproj" />
<Project Path="Modules\LoadBalancing\GenHTTP.Modules.LoadBalancing.csproj" />
<Project Path="Modules\Mcp\GenHTTP.Modules.Mcp.csproj" Type="Classic C#" />
<Project Path="Modules\OpenApi\GenHTTP.Modules.OpenApi.csproj" />
<Project Path="Modules\Pages\GenHTTP.Modules.Pages.csproj" />
<Project Path="Modules\Practices\GenHTTP.Modules.Practices.csproj" />
Expand Down
24 changes: 24 additions & 0 deletions Modules/Mcp/GenHTTP.Modules.Mcp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<Description>Allows to provide tools for LLMs via the MCP.</Description>
<PackageTags>HTTP Webserver C# Module MCP LLM Tool</PackageTags>

</PropertyGroup>

<ItemGroup>

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />

<ProjectReference Include="..\Reflection\GenHTTP.Modules.Reflection.csproj" />

<ProjectReference Include="..\Websockets\GenHTTP.Modules.Websockets.csproj" />

</ItemGroup>

<ItemGroup>
<PackageReference Include="NJsonSchema" Version="11.5.1" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions Modules/Mcp/ITool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace GenHTTP.Modules.Mcp;

public interface ITool
{

string Name { get; }

string Description { get; }

internal Type InputType { get; }

internal Type OutputType { get; }

internal object CallUntyped(object input);

}

public interface ITool<in TInput, out TOutput> : ITool
{

TOutput Call(TInput input);

Type ITool.InputType => typeof(TInput);

Type ITool.OutputType => typeof(TOutput);

object ITool.CallUntyped(object input) => Call(((TInput)input)!)!;

}
160 changes: 160 additions & 0 deletions Modules/Mcp/Logic/ToolsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Text.Json;

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

using GenHTTP.Modules.Mcp.Types;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Websockets;

using NJsonSchema;

namespace GenHTTP.Modules.Mcp.Logic;

public class ToolsHandler : IHandler
{
private static readonly JsonSerializerOptions SerializationOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};

private readonly IHandler _Websocket;

private readonly Dictionary<string, ITool> _Tools;

#region Initialization

public ToolsHandler(List<ITool> tools)
{
_Tools = tools.ToDictionary(t => t.Name, t => t);

_Websocket = Websocket.Create()
.OnMessage(DispatchMessage)
.Build();
}

#endregion

#region Functionality

public ValueTask PrepareAsync() => ValueTask.CompletedTask;

public ValueTask<IResponse?> HandleAsync(IRequest request) => _Websocket.HandleAsync(request);

private async Task DispatchMessage(IWebsocketConnection connection, string message)

Check warning on line 46 in Modules/Mcp/Logic/ToolsHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Split this method into two, one handling parameters check and the other handling the asynchronous code. (https://rules.sonarsource.com/csharp/RSPEC-4457)

Check warning on line 46 in Modules/Mcp/Logic/ToolsHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Split this method into two, one handling parameters check and the other handling the asynchronous code. (https://rules.sonarsource.com/csharp/RSPEC-4457)

Check warning on line 46 in Modules/Mcp/Logic/ToolsHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Split this method into two, one handling parameters check and the other handling the asynchronous code. (https://rules.sonarsource.com/csharp/RSPEC-4457)

Check warning on line 46 in Modules/Mcp/Logic/ToolsHandler.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Split this method into two, one handling parameters check and the other handling the asynchronous code. (https://rules.sonarsource.com/csharp/RSPEC-4457)
{
var request = JsonSerializer.Deserialize<JsonRpcRequest>(message, SerializationOptions);

if (request == null)
{
throw new ArgumentException("Unable read JsonRpc frame from message");
}

switch (request.Method)
{
case "tools/list":
{
var response = new JsonRpcResponse<ToolList>
{
Id = request.Id,
Result = ListTools()
};

await connection.SendAsync(JsonSerializer.Serialize(response));
break;
}
case "tools/call":
{
var arguments = request.Params?.Deserialize<ToolCallParams>();

if (arguments?.Name is not null)
{
if (_Tools.TryGetValue(arguments.Name, out var tool))
{
try
{
var result = await CallTool(tool, arguments.Arguments);

var response = new JsonRpcResponse<object?>
{
Id = request.Id,
Result = result
};

await connection.SendAsync(JsonSerializer.Serialize(response));
}
catch (Exception e)
{
await SendError(connection, 22, $"Error while executing tool '{arguments.Name}': {e.Message}");
}
}
else
{
await SendError(connection, 20, $"Unrecognized tool '{arguments.Name}'");
}
}
else
{
await SendError(connection, 21, "Unable to read tool from request");
}

break;
}
default:
{
await SendError(connection, 10, $"Unsupported method '{request.Method}'");
break;
}
}
}

private ToolList ListTools()
{
var descriptions = new List<ToolInfo>();

foreach (var tool in _Tools.Values)
{
descriptions.Add(new ToolInfo
{
Name = tool.Name,
Description = tool.Description,
InputSchema = JsonSchema.FromType(tool.InputType),
OutputSchema = JsonSchema.FromType(tool.OutputType)
});
}

return new ToolList()
{
Tools = descriptions
};
}

private static async ValueTask<object?> CallTool(ITool tool, JsonElement? input)
{
var argument = input?.Deserialize(tool.InputType);

var result = tool.CallUntyped(argument!);

return await MethodHandler.UnwrapAsync(result);
}

#endregion

#region Helpers

private static async ValueTask SendError(IWebsocketConnection connection, int code, string message)
{
var response = new JsonRpcError()
{
Code = code,
Message = message
};

await connection.SendAsync(JsonSerializer.Serialize(response));
}

#endregion

}
28 changes: 28 additions & 0 deletions Modules/Mcp/Logic/ToolsHandlerBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using GenHTTP.Api.Content;

namespace GenHTTP.Modules.Mcp.Logic;

public class ToolsHandlerBuilder : IHandlerBuilder<ToolsHandlerBuilder>
{
private readonly List<IConcernBuilder> _Concerns = [];

private readonly List<ITool> _Tools = [];

public ToolsHandlerBuilder Add<TInput, TOutput>(ITool<TInput, TOutput> tool)
{
_Tools.Add(tool);
return this;
}

public ToolsHandlerBuilder Add(IConcernBuilder concern)
{
_Concerns.Add(concern);
return this;
}

public IHandler Build()
{
return Concerns.Chain(_Concerns, new ToolsHandler(_Tools));
}

}
10 changes: 10 additions & 0 deletions Modules/Mcp/Tools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GenHTTP.Modules.Mcp.Logic;

namespace GenHTTP.Modules.Mcp;

public static class Tools
{

public static ToolsHandlerBuilder Create() => new();

}
55 changes: 55 additions & 0 deletions Modules/Mcp/Types/JsonRpc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GenHTTP.Modules.Mcp.Types;

public abstract class JsonRpcBase
{

[JsonPropertyName("jsonrpc")]
public string Version { get; init; } = "2.0";

}

public sealed class JsonRpcRequest : JsonRpcBase
{
[JsonPropertyName("id")]
public string? Id { get; init; }

[JsonPropertyName("method")]
public required string Method { get; init; }

[JsonPropertyName("params")]
public JsonElement? Params { get; init; }
}

public sealed class JsonRpcResponse<TResult> : JsonRpcBase
{

[JsonPropertyName("id")]
public string? Id { get; init; }

[JsonPropertyName("result")]
public TResult? Result { get; init; }

[JsonPropertyName("error")]
public JsonRpcError? Error { get; init; }

[JsonIgnore]
public bool IsError => Error != null;

}

public sealed class JsonRpcError
{

[JsonPropertyName("code")]
public int Code { get; init; }

[JsonPropertyName("message")]
public required string Message { get; init; }

[JsonPropertyName("data")]
public object? Data { get; init; }

}
15 changes: 15 additions & 0 deletions Modules/Mcp/Types/ToolCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GenHTTP.Modules.Mcp.Types;

public sealed class ToolCallParams
{

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("arguments")]
public JsonElement? Arguments { get; init; }

}
28 changes: 28 additions & 0 deletions Modules/Mcp/Types/ToolList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;

namespace GenHTTP.Modules.Mcp.Types;

public sealed class ToolList
{

[JsonPropertyName("tools")]
public List<ToolInfo> Tools { get; set; } = new();

}

public sealed class ToolInfo
{

[JsonPropertyName("name")]
public required string Name { get; set; }

[JsonPropertyName("description")]
public string? Description { get; set; }

[JsonPropertyName("inputSchema")]
public required object InputSchema { get; set; }

[JsonPropertyName("outputSchema")]
public required object OutputSchema { get; set; }

}
2 changes: 1 addition & 1 deletion Modules/Reflection/MethodHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public MethodHandler(Operation operation, Func<IRequest, ValueTask<object>> inst
}
}

private static async ValueTask<object?> UnwrapAsync(object? result)
public static async ValueTask<object?> UnwrapAsync(object? result)
{
if (result == null)
{
Expand Down
1 change: 1 addition & 0 deletions Playground/GenHTTP.Playground.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<ProjectReference Include="..\Modules\Webservices\GenHTTP.Modules.Webservices.csproj"/>
<ProjectReference Include="..\Modules\Websockets\GenHTTP.Modules.Websockets.csproj"/>
<ProjectReference Include="..\Modules\I18n\GenHTTP.Modules.I18n.csproj" />
<ProjectReference Include="..\Modules\Mcp\GenHTTP.Modules.Mcp.csproj" />

</ItemGroup>

Expand Down
Loading