From 3b56aa5149cc992a02badf31384ed578bc0dad2f Mon Sep 17 00:00:00 2001 From: Andre Beging Date: Sat, 16 May 2026 13:12:37 +0200 Subject: [PATCH] Add RegionUsers tool and update README; enhance endpoint documentation --- .../create-mcp-tool-from-openapi.prompt.md | 3 + FsMcp/Endpoints.cs | 1 + FsMcp/Program.cs | 3 +- FsMcp/README.md | 20 +++++ FsMcp/Tools/RegionUsersTools.cs | 81 +++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 FsMcp/Tools/RegionUsersTools.cs diff --git a/.github/prompts/create-mcp-tool-from-openapi.prompt.md b/.github/prompts/create-mcp-tool-from-openapi.prompt.md index 96b37af..e05af5f 100644 --- a/.github/prompts/create-mcp-tool-from-openapi.prompt.md +++ b/.github/prompts/create-mcp-tool-from-openapi.prompt.md @@ -4,6 +4,9 @@ Use this prompt when adding a new MCP tool to this project. --- +Here is your documentation of the available endpoints (openapi) +https://beta.foodsharing.de/api/doc + You are working in the `fsmcp` workspace. ## Goal diff --git a/FsMcp/Endpoints.cs b/FsMcp/Endpoints.cs index 223610d..d3141df 100644 --- a/FsMcp/Endpoints.cs +++ b/FsMcp/Endpoints.cs @@ -5,6 +5,7 @@ public static class Endpoints public static string UserLogin => $"{ApiBase}/api/login"; public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details"; public static string RegionStores(int regionId) => $"{ApiBase}/api/regions/{regionId}/stores"; + public static string RegionUsers(int regionId) => $"{ApiBase}/api/regions/{regionId}/users"; public static string StoreMembers(int storeId) => $"{ApiBase}/api/stores/{storeId}/members"; private static string ApiBase => "https://beta.foodsharing.de"; diff --git a/FsMcp/Program.cs b/FsMcp/Program.cs index 9f25bbc..b4b554b 100644 --- a/FsMcp/Program.cs +++ b/FsMcp/Program.cs @@ -15,6 +15,7 @@ builder.Services .WithStdioServerTransport() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/FsMcp/README.md b/FsMcp/README.md index cebd8d8..e799b76 100644 --- a/FsMcp/README.md +++ b/FsMcp/README.md @@ -122,6 +122,26 @@ Notes for consumers: The tool is strongly typed in `Tools/StoreMembersTools.cs`, with per-field `Description` attributes so MCP clients can surface schema-aware guidance directly in tool UIs. +### Region users tool + +This server also exposes `get_region_users`. + +- Purpose: Returns all members of a region from `GET /api/regions/{regionId}/users`. +- Input: + - `regionId` (required, positive integer; OpenAPI path pattern: `[1-9][0-9]*`) +- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header. +- Output: Array of typed member entries including: + - identity fields (`id`, `name`, `lastName`, `avatar`, `isSleeping`, `isAdminOrAmbassadorOfRegion`, `isVerified`, `role`) + - extra observed fields (`isHomeRegion`, `lastActivity`, `lastPassDate`) + +Notes for consumers: + +- `isSleeping` indicates whether the member currently uses the sleeping hat function. +- `isAdminOrAmbassadorOfRegion` indicates if the member has admin/ambassador privileges in this region. +- Some fields (`lastName`, `isHomeRegion`, `lastActivity`, `role`, `lastPassDate`, `isVerified`) are added to handle varying response payloads. + +The tool is strongly typed in `Tools/RegionUsersTools.cs`, with per-field `Description` attributes so MCP clients can surface schema-aware guidance directly in tool UIs. + ## Publishing to NuGet.org 1. Run `dotnet pack -c Release` to create the NuGet package diff --git a/FsMcp/Tools/RegionUsersTools.cs b/FsMcp/Tools/RegionUsersTools.cs new file mode 100644 index 0000000..1f1a18b --- /dev/null +++ b/FsMcp/Tools/RegionUsersTools.cs @@ -0,0 +1,81 @@ +using System.ComponentModel; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using FsMcp; +using ModelContextProtocol.Server; + +namespace FsMcp.Tools; + +internal sealed class RegionUsersTools +{ + private readonly FoodsharingApiClient _apiClient; + + public RegionUsersTools(FoodsharingApiClient apiClient) + { + _apiClient = apiClient; + } + + [McpServerTool] + [Description("Returns a list of all members for a region. Useful for discovering users within a specific foodsharing region.")] + public async Task> GetRegionUsersAsync( + [Description("Region ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")] + int regionId) + { + if (regionId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(regionId), "regionId must be a positive integer."); + } + + await _apiClient.EnsureLoginAsync(); + + var users = await _apiClient.HttpClient.GetFromJsonAsync>(Endpoints.RegionUsers(regionId)); + return users ?? []; + } +} + +public sealed record UnitMemberListItem +{ + [Description("Unique user ID of the member.")] + [JsonPropertyName("id")] + public int Id { get; init; } + + [Description("Display name of the member.")] + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + [Description("Relative API path of the member avatar image.")] + [JsonPropertyName("avatar")] + public string? Avatar { get; init; } + + [Description("Whether the user is currently using the sleeping hat function.")] + [JsonPropertyName("isSleeping")] + public bool? IsSleeping { get; init; } + + [Description("Whether the user is an admin or ambassador of the region.")] + [JsonPropertyName("isAdminOrAmbassadorOfRegion")] + public bool IsAdminOrAmbassadorOfRegion { get; init; } + + [Description("Last name of the member (if available or returned by the API).")] + [JsonPropertyName("lastName")] + public string? LastName { get; init; } + + [Description("Whether the region is the user's home region (if available).")] + [JsonPropertyName("isHomeRegion")] + public bool? IsHomeRegion { get; init; } + + [Description("Last activity timestamp of the user (if available).")] + [JsonPropertyName("lastActivity")] + public DateTimeOffset? LastActivity { get; init; } + + [Description("Numeric role enum from OpenAPI Role: 0, 1, 2, 3, 4, 5.")] + [JsonPropertyName("role")] + public int? Role { get; init; } + + [Description("Date and time of the last generated pass, format: YYYY-MM-DD HH:mm:ss.")] + [JsonPropertyName("lastPassDate")] + public string? LastPassDate { get; init; } + + [Description("Whether the member account is verified.")] + [JsonPropertyName("isVerified")] + public bool? IsVerified { get; init; } +} \ No newline at end of file