Add RegionStores endpoint and tools; remove RandomNumberTools

This commit is contained in:
2026-03-03 22:15:42 +01:00
parent 311004790d
commit 31e8db1ec3
5 changed files with 113 additions and 53 deletions

View File

@@ -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";
}

View File

@@ -13,7 +13,7 @@ builder.Services.AddSingleton<FoodsharingApiClient>();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<RandomNumberTools>()
.WithTools<CurrentUserTools>();
.WithTools<CurrentUserTools>()
.WithTools<RegionStoresTools>();
await builder.Build().RunAsync();

View File

@@ -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

View File

@@ -1,51 +0,0 @@
using System.ComponentModel;
using FsMcp;
using ModelContextProtocol.Server;
using System.Linq;
/// <summary>
/// Sample MCP tools for demonstration purposes.
/// These tools can be invoked by MCP clients to perform various operations.
/// </summary>
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<string> 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)]));
}
}

View File

@@ -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<IReadOnlyList<RegionStoreListItem>> 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<IReadOnlyList<RegionStoreListItem>>(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;
}