Add RegionStores endpoint and tools; remove RandomNumberTools
This commit is contained in:
@@ -4,6 +4,7 @@ public static class Endpoints
|
|||||||
{
|
{
|
||||||
public static string UserLogin => $"{ApiBase}/api/login";
|
public static string UserLogin => $"{ApiBase}/api/login";
|
||||||
public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details";
|
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";
|
private static string ApiBase => "https://beta.foodsharing.de";
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ builder.Services.AddSingleton<FoodsharingApiClient>();
|
|||||||
builder.Services
|
builder.Services
|
||||||
.AddMcpServer()
|
.AddMcpServer()
|
||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
.WithTools<RandomNumberTools>()
|
.WithTools<CurrentUserTools>()
|
||||||
.WithTools<CurrentUserTools>();
|
.WithTools<RegionStoresTools>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|||||||
@@ -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.
|
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
|
## Publishing to NuGet.org
|
||||||
|
|
||||||
1. Run `dotnet pack -c Release` to create the NuGet package
|
1. Run `dotnet pack -c Release` to create the NuGet package
|
||||||
|
|||||||
@@ -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)]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
86
FsMcp/Tools/RegionStoresTools.cs
Normal file
86
FsMcp/Tools/RegionStoresTools.cs
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user