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 UserCurrentDetails => $"{ApiBase}/api/users/current/details";
|
||||||
public static string RegionStores(int regionId) => $"{ApiBase}/api/regions/{regionId}/stores";
|
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 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";
|
private static string ApiBase => "https://beta.foodsharing.de";
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ builder.Services
|
|||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
.WithTools<CurrentUserTools>()
|
.WithTools<CurrentUserTools>()
|
||||||
.WithTools<RegionStoresTools>()
|
.WithTools<RegionStoresTools>()
|
||||||
.WithTools<StoreMembersTools>();
|
.WithTools<StoreMembersTools>()
|
||||||
|
.WithTools<StoreLogTools>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
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.
|
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
|
## 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
|
||||||
|
|||||||
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