Add UserStores, StorePickups, and ConfirmPickup tools; implement endpoints and update README documentation

This commit is contained in:
2026-05-16 17:12:13 +02:00
parent 2aae8a4e58
commit cebe292e2e
5 changed files with 165 additions and 1 deletions

57
FsMcp/Converters.cs Normal file
View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FsMcp;
public class ObjectOrArrayConverter<T> : JsonConverter<IReadOnlyList<T>>
{
public override IReadOnlyList<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
return JsonSerializer.Deserialize<List<T>>(ref reader, options) ?? [];
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
var dict = JsonSerializer.Deserialize<Dictionary<string, T>>(ref reader, options);
if (dict != null)
{
var list = new List<T>(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<T> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
public class NumberOrBoolConverter : JsonConverter<bool>
{
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);
}
}

View File

@@ -21,6 +21,17 @@ public static class Endpoints
public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) => public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) =>
$"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}"; $"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}";
public static string UserStores(string userId, bool? excludeInactive = null)
{
var query = new List<string>();
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) public static string Conversations(int? limit = null, int? offset = null)
{ {
var query = new List<string>(); var query = new List<string>();

View File

@@ -20,6 +20,9 @@ builder.Services
.WithTools<FsMcp.Tools.SearchAllTools>() .WithTools<FsMcp.Tools.SearchAllTools>()
.WithTools<FsMcp.Tools.RegionForumThreadsTools>() .WithTools<FsMcp.Tools.RegionForumThreadsTools>()
.WithTools<FsMcp.Tools.ForumThreadTools>() .WithTools<FsMcp.Tools.ForumThreadTools>()
.WithTools<FsMcp.Tools.ConversationsTools>(); .WithTools<FsMcp.Tools.ConversationsTools>()
.WithTools<FsMcp.Tools.UserStoresTools>()
.WithTools<FsMcp.Tools.StorePickupsTools>()
.WithTools<FsMcp.Tools.ConfirmPickupTools>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@@ -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. 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 ### Store members tool
This server also exposes `get_store_members`. This server also exposes `get_store_members`.

View File

@@ -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<string> 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";
}
}