Add UserStores, StorePickups, and ConfirmPickup tools; implement endpoints and update README documentation
This commit is contained in:
57
FsMcp/Converters.cs
Normal file
57
FsMcp/Converters.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
49
FsMcp/Tools/ConfirmPickupTools.cs
Normal file
49
FsMcp/Tools/ConfirmPickupTools.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user