From cebe292e2e6227ac7790852edfc6cbb858ef6b78 Mon Sep 17 00:00:00 2001 From: Andre Beging Date: Sat, 16 May 2026 17:12:13 +0200 Subject: [PATCH] Add UserStores, StorePickups, and ConfirmPickup tools; implement endpoints and update README documentation --- FsMcp/Converters.cs | 57 +++++++++++++++++++++++++++++++ FsMcp/Endpoints.cs | 11 ++++++ FsMcp/Program.cs | 5 ++- FsMcp/README.md | 44 ++++++++++++++++++++++++ FsMcp/Tools/ConfirmPickupTools.cs | 49 ++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 FsMcp/Converters.cs create mode 100644 FsMcp/Tools/ConfirmPickupTools.cs diff --git a/FsMcp/Converters.cs b/FsMcp/Converters.cs new file mode 100644 index 0000000..c70ab5b --- /dev/null +++ b/FsMcp/Converters.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FsMcp; + +public class ObjectOrArrayConverter : JsonConverter> +{ + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.StartArray) + { + return JsonSerializer.Deserialize>(ref reader, options) ?? []; + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + var dict = JsonSerializer.Deserialize>(ref reader, options); + if (dict != null) + { + var list = new List(dict.Count); + foreach (var kvp in dict) list.Add(kvp.Value); + return list; + } + return []; + } + + using (JsonDocument.ParseValue(ref reader)) { } + return []; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } +} + +public class NumberOrBoolConverter : JsonConverter +{ + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) return true; + if (reader.TokenType == JsonTokenType.False) return false; + if (reader.TokenType == JsonTokenType.Number) return reader.GetInt32() != 0; + if (reader.TokenType == JsonTokenType.String) + { + var str = reader.GetString(); + return str == "1" || str?.ToLower() == "true"; + } + return false; + } + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } +} diff --git a/FsMcp/Endpoints.cs b/FsMcp/Endpoints.cs index ca25d80..7fea8d5 100644 --- a/FsMcp/Endpoints.cs +++ b/FsMcp/Endpoints.cs @@ -21,6 +21,17 @@ public static class Endpoints public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) => $"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}"; + public static string UserStores(string userId, bool? excludeInactive = null) + { + var query = new List(); + if (excludeInactive.HasValue) query.Add($"excludeInactive={excludeInactive.Value.ToString().ToLower()}"); + var qs = query.Count > 0 ? "?" + string.Join("&", query) : ""; + return $"{ApiBase}/api/users/{userId}/stores{qs}"; + } + + public static string StorePickups(int storeId) => $"{ApiBase}/api/stores/{storeId}/pickups"; + public static string ConfirmPickup(int storeId, string pickupDate, int userId) => $"{ApiBase}/api/stores/{storeId}/pickups/{pickupDate}/users/{userId}"; + public static string Conversations(int? limit = null, int? offset = null) { var query = new List(); diff --git a/FsMcp/Program.cs b/FsMcp/Program.cs index 2c77ee9..3f5400a 100644 --- a/FsMcp/Program.cs +++ b/FsMcp/Program.cs @@ -20,6 +20,9 @@ builder.Services .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools() + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/FsMcp/README.md b/FsMcp/README.md index b60eff8..052973b 100644 --- a/FsMcp/README.md +++ b/FsMcp/README.md @@ -98,6 +98,50 @@ Notes for consumers: 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. +### User stores tool + +This server also exposes `get_user_stores`. + +- Purpose: Returns the stores where a user is a member of from `GET /api/users/{userId}/stores`. +- Input: + - `userId` (optional, defaults to `"current"`) + - `excludeInactive` (optional, boolean) +- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header. +- Output: Array of typed store team membership entries including: + - identity (`id`, `name`) + - membership status (`isManaging`, `membershipStatus`) + - category and pickup status (`categoryType`, `pickupStatus`) + +The tool is strongly typed in `Tools/UserStoresTools.cs`. + +### Store pickups tool + +This server also exposes `get_store_pickups`. + +- Purpose: List upcoming pickups for a store from `GET /api/stores/{storeId}/pickups`. +- Input: + - `storeId` (required, positive integer) +- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header. +- Output: Array of typed pickup entries including: + - `date`, `totalSlots`, `isAvailable`, `description` + - `occupiedSlots`: List of users who have signed up, including their `isConfirmed` status. + +The tool is strongly typed in `Tools/StorePickupsTools.cs`. + +### Confirm pickup tool + +This server exposes `confirm_pickup`. + +- Purpose: Confirm a pickup slot for a user at a store from `PATCH /api/stores/{storeId}/pickups/{pickupDate}/users/{userId}`. +- Input: + - `storeId` (required, positive integer) + - `pickupDate` (required, string, e.g. `2024-05-18T12:00:00.000Z`) + - `userId` (required, positive integer) +- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header. +- Output: Simple "Success" string payload when the slot is confirmed. + +The tool is strongly typed in `Tools/ConfirmPickupTools.cs`. + ### Store members tool This server also exposes `get_store_members`. diff --git a/FsMcp/Tools/ConfirmPickupTools.cs b/FsMcp/Tools/ConfirmPickupTools.cs new file mode 100644 index 0000000..53ee356 --- /dev/null +++ b/FsMcp/Tools/ConfirmPickupTools.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using FsMcp; +using ModelContextProtocol.Server; + +namespace FsMcp.Tools; + +internal sealed class ConfirmPickupTools +{ + private readonly FoodsharingApiClient _apiClient; + + public ConfirmPickupTools(FoodsharingApiClient apiClient) + { + _apiClient = apiClient; + } + + [McpServerTool] + [Description("Confirm a pickup slot for a user at a store from PATCH /api/stores/{storeId}/pickups/{pickupDate}/users/{userId}.")] + public async Task ConfirmPickupAsync( + [Description("Store ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")] + int storeId, + [Description("Pickup date as string (OpenAPI path pattern: [0-9]{4}-...T...Z). Example: 2024-05-18T12:00:00.000Z.")] + string pickupDate, + [Description("User ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")] + int userId) + { + if (storeId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(storeId), "storeId must be a positive integer."); + } + if (userId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(userId), "userId must be a positive integer."); + } + if (string.IsNullOrWhiteSpace(pickupDate)) + { + throw new ArgumentException("pickupDate must not be empty.", nameof(pickupDate)); + } + + await _apiClient.EnsureLoginAsync(); + + string url = Endpoints.ConfirmPickup(storeId, pickupDate, userId); + using var request = new HttpRequestMessage(HttpMethod.Patch, url); + using var response = await _apiClient.HttpClient.SendAsync(request); + + response.EnsureSuccessStatusCode(); + + return "Success"; + } +}