Add StoreLogTools and endpoint for retrieving store log entries; update README

This commit is contained in:
Andre Beging
2026-03-04 11:36:34 +01:00
parent a9b59567b7
commit a145c059ac
5 changed files with 213 additions and 1 deletions

View File

@@ -0,0 +1,19 @@
---
agent: 'agent'
model: Grok Code Fast 1 (copilot)
tools: ['foodsharing-mcp/*']
description: 'Get duplicate ALDI memberships in the siegen region'
---
Region ID: 139 (Siegen)
Search for all stores in that region that name contains "ALDI"
For all found stores, use the "storeId" to get the store members that are assigned to that store and have a membership status of "active"
Collect all the members in one big list, group them by name.
The goal is to see which members is in how many of those stores.
It is only of interest if a member is in more than two stores, so only show those members that are in three or more stores.
return a list of members with the following information:
- Name
- Number of stores they are assigned to
- List of store names they are assigned to

View File

@@ -6,6 +6,8 @@ public static class Endpoints
public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details";
public static string RegionStores(int regionId) => $"{ApiBase}/api/regions/{regionId}/stores";
public static string StoreMembers(int storeId) => $"{ApiBase}/api/stores/{storeId}/members";
public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) =>
$"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}";
private static string ApiBase => "https://beta.foodsharing.de";
}

View File

@@ -15,6 +15,7 @@ builder.Services
.WithStdioServerTransport()
.WithTools<CurrentUserTools>()
.WithTools<RegionStoresTools>()
.WithTools<StoreMembersTools>();
.WithTools<StoreMembersTools>()
.WithTools<StoreLogTools>();
await builder.Build().RunAsync();

View File

@@ -122,6 +122,53 @@ 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.
### Store log entries tool
This server also exposes `get_store_log_entries`.
- Purpose: Returns store log entries from `GET /api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}` for auditing team and slot activity.
- Input:
- `storeId` (required, positive integer; OpenAPI path pattern: `[1-9][0-9]*`)
- `fromDate` (required, `DateTimeOffset`; serialized to UTC path format `yyyy-MM-ddTHH:mm:ss.fffZ`)
- `toDate` (required, `DateTimeOffset`; serialized to UTC path format `yyyy-MM-ddTHH:mm:ss.fffZ`)
- `storeLogActionIds` (required, non-empty list of positive integers; serialized as comma-separated path segment matching `\d+(,\d+)*`)
- `limit` (optional, query parameter, must be `> 0`)
- `offset` (optional, query parameter, must be `>= 0`)
- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header.
- Output: Array of typed store log entries including:
- timing fields (`performedAt`, `dateReference`)
- type field (`actionType`)
- actor/target profiles (`actor`, `target` with `id`, `name`, `avatar`, `isSleeping`)
- optional text fields (`content`, `reason`)
Notes for consumers:
- The backend may reject requests beyond the allowed history window (OpenAPI documents `400` for more than 6 months back).
- `fromDate` must be less than or equal to `toDate`.
- `actionType` values currently observed/mapped:
- `1` membership requests made / `Beitrittsanfragen stellen`
- `2` membership requests rejected / `Beitrittsanfragen ablehnen`
- `3` membership requests accepted / `Beitrittsanfragen annehmen`
- `4` members added manually / `Foodsaver:innen ohne Anfrage ins Team aufnehmen`
- `5` team members made standby / `Foodsaver:innen auf die Springerliste setzen`
- `6` team members moved to active team / `Foodsaver:innen ins aktive Team aufnehmen`
- `7` team members removed from team / `Foodsaver:innen aus dem Team entfernen`
- `8` team members left team / `Team verlassen`
- `9` store coordinators appointed / `Betriebsverantwortliche eintragen`
- `10` store coordinators removed / `Betriebsverantwortliche austragen`
- `11` signed up for a slot / `Eintragen für Slot`
- `12` signed out from a slot / `Austragen für Slot`
- `13` removed from a slot / `Für Slot ausgetragen werden`
- `14` confirmed a slot / `Slot bestätigen`
- `15` wall posts deleted / `Pinnwandeintrag löschen`
- `16` team applications withdrawn / `Beitrittsanfrage zurückziehen`
- `17` invitations to the team / `Foodsaver:innen ins Team einladen`
- `18` withdrawn team invitations / `Team-Einladungen zurückziehen`
- `19` accepted team invitations / `Team-Einladungen annehmen`
- `20` declined team invitations / `Team-Einladungen ablehnen`
The tool is strongly typed in `Tools/StoreLogTools.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

@@ -0,0 +1,143 @@
using System.ComponentModel;
using System.Globalization;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using FsMcp;
using ModelContextProtocol.Server;
internal sealed class StoreLogTools
{
private readonly FoodsharingApiClient _apiClient;
public StoreLogTools(FoodsharingApiClient apiClient)
{
_apiClient = apiClient;
}
[McpServerTool]
[Description("Returns store log entries from GET /api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}. Useful for auditing team membership changes, slot signups/signouts, and invitation activity over a time range with selected action types.")]
public async Task<IReadOnlyList<StoreLogEntryItem>> GetStoreLogEntriesAsync(
[Description("Store ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")]
int storeId,
[Description("Inclusive start timestamp in UTC. Will be serialized to API path format yyyy-MM-ddTHH:mm:ss.fffZ.")]
DateTimeOffset fromDate,
[Description("Inclusive end timestamp in UTC. Will be serialized to API path format yyyy-MM-ddTHH:mm:ss.fffZ.")]
DateTimeOffset toDate,
[Description("Store log action type IDs to include. Values are joined as comma-separated path segment (OpenAPI pattern: \\d+(,\\d+)*).")]
IReadOnlyList<int> storeLogActionIds,
[Description("Optional page size limit (query parameter).")]
int? limit = null,
[Description("Optional pagination offset (query parameter).")]
int? offset = null)
{
if (storeId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(storeId), "storeId must be a positive integer.");
}
if (storeLogActionIds.Count == 0)
{
throw new ArgumentException("At least one action type ID is required.", nameof(storeLogActionIds));
}
if (storeLogActionIds.Any(id => id <= 0))
{
throw new ArgumentOutOfRangeException(nameof(storeLogActionIds), "All action type IDs must be positive integers.");
}
if (fromDate > toDate)
{
throw new ArgumentException("fromDate must be less than or equal to toDate.");
}
if (limit is <= 0)
{
throw new ArgumentOutOfRangeException(nameof(limit), "limit must be greater than 0 when provided.");
}
if (offset is < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), "offset must be greater than or equal to 0 when provided.");
}
await _apiClient.EnsureLoginAsync();
string fromDatePath = FormatDatePath(fromDate);
string toDatePath = FormatDatePath(toDate);
string actionTypesPath = string.Join(',', storeLogActionIds);
string endpoint = Endpoints.StoreLogActions(storeId, fromDatePath, toDatePath, actionTypesPath);
List<string> queryParts = [];
if (limit is not null)
{
queryParts.Add($"limit={limit.Value}");
}
if (offset is not null)
{
queryParts.Add($"offset={offset.Value}");
}
if (queryParts.Count > 0)
{
endpoint = $"{endpoint}?{string.Join("&", queryParts)}";
}
var entries = await _apiClient.HttpClient.GetFromJsonAsync<IReadOnlyList<StoreLogEntryItem>>(endpoint);
return entries ?? [];
}
private static string FormatDatePath(DateTimeOffset value) =>
value.UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
}
public sealed record StoreLogEntryItem
{
[Description("Timestamp when the action was performed (ISO-8601).")]
[JsonPropertyName("performedAt")]
public DateTimeOffset? PerformedAt { get; init; }
[Description("Numeric store log action type ID. Known mappings: 1 membership requests made / Beitrittsanfragen stellen; 2 membership requests rejected / Beitrittsanfragen ablehnen; 3 membership requests accepted / Beitrittsanfragen annehmen; 4 members added manually / Foodsaver:innen ohne Anfrage ins Team aufnehmen; 5 team members made standby / Foodsaver:innen auf die Springerliste setzen; 6 team members moved to active team / Foodsaver:innen ins aktive Team aufnehmen; 7 team members removed from team / Foodsaver:innen aus dem Team entfernen; 8 team members left team / Team verlassen; 9 store coordinators appointed / Betriebsverantwortliche eintragen; 10 store coordinators removed / Betriebsverantwortliche austragen; 11 signed up for a slot / Eintragen für Slot; 12 signed out from a slot / Austragen für Slot; 13 removed from a slot / Für Slot ausgetragen werden; 14 confirmed a slot / Slot bestätigen; 15 wall posts deleted / Pinnwandeintrag löschen; 16 team applications withdrawn / Beitrittsanfrage zurückziehen; 17 invitations to the team / Foodsaver:innen ins Team einladen; 18 withdrawn team invitations / Team-Einladungen zurückziehen; 19 accepted team invitations / Team-Einladungen annehmen; 20 declined team invitations / Team-Einladungen ablehnen.")]
[JsonPropertyName("actionType")]
public int? ActionType { get; init; }
[Description("Profile of the user who performed the action.")]
[JsonPropertyName("actor")]
public StoreLogProfile? Actor { get; init; }
[Description("Profile of the action target user, if applicable.")]
[JsonPropertyName("target")]
public StoreLogProfile? Target { get; init; }
[Description("Additional timestamp reference associated with the log entry (for example affected slot time).")]
[JsonPropertyName("dateReference")]
public DateTimeOffset? DateReference { get; init; }
[Description("Text content for the log entry, if provided by the API.")]
[JsonPropertyName("content")]
public string? Content { get; init; }
[Description("Optional reason text attached to the action.")]
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
public sealed record StoreLogProfile
{
[Description("Unique user ID.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Display name of the user.")]
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[Description("Relative API path to avatar image.")]
[JsonPropertyName("avatar")]
public string Avatar { get; init; } = string.Empty;
[Description("Whether the user currently has sleeping-hat mode enabled.")]
[JsonPropertyName("isSleeping")]
public bool? IsSleeping { get; init; }
}