Add StoreLogTools and endpoint for retrieving store log entries; update README
This commit is contained in:
19
.github/prompts/get-aldi-breakers.prompt.md
vendored
Normal file
19
.github/prompts/get-aldi-breakers.prompt.md
vendored
Normal 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
|
||||
@@ -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";
|
||||
}
|
||||
@@ -15,6 +15,7 @@ builder.Services
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<CurrentUserTools>()
|
||||
.WithTools<RegionStoresTools>()
|
||||
.WithTools<StoreMembersTools>();
|
||||
.WithTools<StoreMembersTools>()
|
||||
.WithTools<StoreLogTools>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@@ -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
|
||||
|
||||
143
FsMcp/Tools/StoreLogTools.cs
Normal file
143
FsMcp/Tools/StoreLogTools.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user