From 31e8db1ec3964d6d745c42016eed01d5e5eb4b3d Mon Sep 17 00:00:00 2001 From: Andre Beging Date: Tue, 3 Mar 2026 22:15:42 +0100 Subject: [PATCH] Add RegionStores endpoint and tools; remove RandomNumberTools --- FsMcp/Endpoints.cs | 1 + FsMcp/Program.cs | 4 +- FsMcp/README.md | 24 +++++++++ FsMcp/Tools/RandomNumberTools.cs | 51 ------------------- FsMcp/Tools/RegionStoresTools.cs | 86 ++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 53 deletions(-) delete mode 100644 FsMcp/Tools/RandomNumberTools.cs create mode 100644 FsMcp/Tools/RegionStoresTools.cs diff --git a/FsMcp/Endpoints.cs b/FsMcp/Endpoints.cs index 308d7ed..08828e6 100644 --- a/FsMcp/Endpoints.cs +++ b/FsMcp/Endpoints.cs @@ -4,6 +4,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"; private static string ApiBase => "https://beta.foodsharing.de"; } \ No newline at end of file diff --git a/FsMcp/Program.cs b/FsMcp/Program.cs index 02a7633..93ebc50 100644 --- a/FsMcp/Program.cs +++ b/FsMcp/Program.cs @@ -13,7 +13,7 @@ builder.Services.AddSingleton(); builder.Services .AddMcpServer() .WithStdioServerTransport() - .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/FsMcp/README.md b/FsMcp/README.md index 1e467a0..2a13c4a 100644 --- a/FsMcp/README.md +++ b/FsMcp/README.md @@ -74,6 +74,30 @@ This server also exposes `get_current_user_details`. The tool is strongly typed in `Tools/CurrentUserTools.cs`, where each field is documented with `Description` attributes so MCP clients can surface schema-aware help. +### Region stores tool + +This server also exposes `get_region_stores`. + +- Purpose: Returns all stores for a region from `GET /api/regions/{regionId}/stores`. +- 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 store list entries including: + - identity (`id`, `name`) + - region info (`region.id`, `region.name`) + - geolocation (`location.lat`, `location.lon`) + - address fields (`street`, `city`, `zipCode`) + - operational fields (`cooperationStatus`, `categoryType`, `createdAt`) + +Notes for consumers: + +- `cooperationStatus` is a numeric enum. Known values from the OpenAPI schema: `0, 1, 2, 4, 5, 6, 7`. +- `categoryType` is a numeric enum. Known values from the OpenAPI schema: `0, 1, 2`. +- `createdAt` is delivered as a date string in `YYYY-MM-DD` format. +- Some raw API values may contain trailing spaces or empty strings in address fields; this tool returns the original API data unchanged. + +The tool is strongly typed in `Tools/RegionStoresTools.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/RandomNumberTools.cs b/FsMcp/Tools/RandomNumberTools.cs deleted file mode 100644 index 605a8da..0000000 --- a/FsMcp/Tools/RandomNumberTools.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.ComponentModel; -using FsMcp; -using ModelContextProtocol.Server; -using System.Linq; - -/// -/// Sample MCP tools for demonstration purposes. -/// These tools can be invoked by MCP clients to perform various operations. -/// -internal class RandomNumberTools -{ - private readonly FoodsharingApiClient _apiClient; - - public RandomNumberTools(FoodsharingApiClient apiClient) - { - _apiClient = apiClient; - } - - // Mcp Server tool to get the crsf token for the current session (for demonstration purposes). - [McpServerTool] - [Description("Gets the CSRF token for the current session.")] - public async Task GetCsrfTokenAsync() - { - await _apiClient.EnsureLoginAsync(); - return _apiClient.HttpClient.DefaultRequestHeaders.GetValues("X-CSRF-Token").FirstOrDefault() ?? throw new InvalidOperationException("CSRF token not found."); - } - - [McpServerTool] - [Description("Generates a random number between the specified minimum and maximum values.")] - public int GetRandomNumber( - [Description("Minimum value (inclusive)")] int min = 0, - [Description("Maximum value (exclusive)")] int max = 100) - { - return Random.Shared.Next(min, max); - } - - [McpServerTool] - [Description("Generates a random Lorem Ipsum text with the specified number of words.")] - public string GetLoremIpsum(int wordCount = 10) - { - string[] loremIpsumWords = new[] - { - "Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", - "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", - "incididunt", "ut", "labore", "et", "dolore", "magna", - "aliqua" - }; - - return string.Join(" ", Enumerable.Range(0, wordCount).Select(_ => loremIpsumWords[Random.Shared.Next(loremIpsumWords.Length)])); - } -} diff --git a/FsMcp/Tools/RegionStoresTools.cs b/FsMcp/Tools/RegionStoresTools.cs new file mode 100644 index 0000000..b9efcd6 --- /dev/null +++ b/FsMcp/Tools/RegionStoresTools.cs @@ -0,0 +1,86 @@ +using System.ComponentModel; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using FsMcp; +using ModelContextProtocol.Server; + +internal sealed class RegionStoresTools +{ + private readonly FoodsharingApiClient _apiClient; + + public RegionStoresTools(FoodsharingApiClient apiClient) + { + _apiClient = apiClient; + } + + [McpServerTool] + [Description("Returns all stores for a region from GET /api/regions/{regionId}/stores. Useful for store discovery, geospatial mapping, and operational analysis by cooperation status, category type, and city. The result is a full list of StoreListInformation entries as provided by the API.")] + public async Task> GetRegionStoresAsync( + [Description("Region ID as positive integer (OpenAPI path pattern: [1-9][0-9]*). Example: 139 for Siegen.")] + int regionId) + { + if (regionId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(regionId), "regionId must be a positive integer."); + } + + await _apiClient.EnsureLoginAsync(); + + var stores = await _apiClient.HttpClient.GetFromJsonAsync>(Endpoints.RegionStores(regionId)); + return stores ?? []; + } +} + +public sealed record RegionStoreListItem +{ + [Description("Unique identifier of the store in the foodsharing database.")] + [JsonPropertyName("id")] + public int Id { get; init; } + + [Description("Store name.")] + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + [Description("Region that manages and is responsible for this store.")] + [JsonPropertyName("region")] + public RegionIdentifier? Region { get; init; } + + [Description("Store location in decimal latitude/longitude.")] + [JsonPropertyName("location")] + public GeoCoordinates? Location { get; init; } + + [Description("Street and house number of the store location. API data may include trailing spaces or irregular formatting.")] + [JsonPropertyName("street")] + public string Street { get; init; } = string.Empty; + + [Description("City name of the store location. Can be empty for some records.")] + [JsonPropertyName("city")] + public string City { get; init; } = string.Empty; + + [Description("ZIP/postal code of the store location. Can be empty for some records.")] + [JsonPropertyName("zipCode")] + public string ZipCode { get; init; } = string.Empty; + + [Description("Numeric cooperation status enum from the API. Known values in OpenAPI: 0, 1, 2, 4, 5, 6, 7.")] + [JsonPropertyName("cooperationStatus")] + public int CooperationStatus { get; init; } + + [Description("Date when the store entry was created in the database. API format is date string 'YYYY-MM-DD'.")] + [JsonPropertyName("createdAt")] + public string CreatedAt { get; init; } = string.Empty; + + [Description("Numeric store category type enum from the API. Known values in OpenAPI: 0, 1, 2.")] + [JsonPropertyName("categoryType")] + public int CategoryType { get; init; } +} + +public sealed record RegionIdentifier +{ + [Description("Region ID.")] + [JsonPropertyName("id")] + public int Id { get; init; } + + [Description("Region display name.")] + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; +} \ No newline at end of file