Add UserStores and StorePickups tools; implement endpoints and update documentation
This commit is contained in:
28
.github/plans/plan-managed-stores-pickups.prompt.md
vendored
Normal file
28
.github/plans/plan-managed-stores-pickups.prompt.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Plan: Implement Managed Stores Pickups MCP Tools
|
||||||
|
|
||||||
|
To minimize API calls, we will implement the two necessary endpoints to first get the user's managed stores, and then fetch the upcoming pickups for each managed store.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
1. **Analyze API Models**: Extract the response schemas for both endpoints from `fsopenapi.json` to create fully typed C# `record` responses (e.g., `StoreTeamMembershipWithPickupStatus` and the `occupiedSlots` structure).
|
||||||
|
2. **Update Endpoints list**: Add `GetStoresOfUser` and `ListPickupsForStore` relative URLs to `FsMcp/Endpoints.cs`.
|
||||||
|
3. **Implement User Stores Tool** (*parallel with step 4*): Create `FsMcp/Tools/UserStoresTools.cs` exposing a tool to call `GET /api/users/{userId}/stores`. Ensure `EnsureLoginAsync()` is called before making requests, and fully apply `[Description]` attributes to fields.
|
||||||
|
4. **Implement Store Pickups Tool** (*parallel with step 3*): Create `FsMcp/Tools/StorePickupsTools.cs` exposing a tool to call `GET /api/stores/{storeId}/pickups`. Include the `occupiedSlots` with their `isConfirmed` property.
|
||||||
|
5. **Register MCP Tools**: Add `.WithTools<UserStoresTools>()` and `.WithTools<StorePickupsTools>()` inside `FsMcp/Program.cs`.
|
||||||
|
6. **Update Documents**: Add usage instructions, auth behaviors, and endpoint mappings to `FsMcp/README.md`.
|
||||||
|
|
||||||
|
**Relevant files**
|
||||||
|
- `fsopenapi.json` — Source of truth for OpenAPI schema properties
|
||||||
|
- `FsMcp/Endpoints.cs` — Add static endpoint URLs
|
||||||
|
- `FsMcp/Tools/UserStoresTools.cs` — Implements `GET /api/users/{userId}/stores`
|
||||||
|
- `FsMcp/Tools/StorePickupsTools.cs` — Implements `GET /api/stores/{storeId}/pickups`
|
||||||
|
- `FsMcp/Program.cs` — Tool registration
|
||||||
|
- `FsMcp/README.md` — Documentation
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
1. Execute `dotnet build FsMcp/FsMcp.csproj -c Debug` to verify compilation.
|
||||||
|
2. Launch the MCP server using the provided workspace task: `Run FsMcp Debug`.
|
||||||
|
3. Once running, interactively test the tools by asking: "List stores I manage and show their open unconfirmed pickups" via this chat, ensuring both tools are successfully invoked by the MCP client.
|
||||||
|
|
||||||
|
**Decisions**
|
||||||
|
- `userId` in `GET /api/users/{userId}/stores` can receive `"current"` to simplify queries.
|
||||||
|
- We implement two distinct tool classes rather than merging them to adhere to the existing `Tools/*Tools.cs` domain-driven naming pattern in the project.
|
||||||
12
.github/prompts/list-open-slot-requests.prompt.md
vendored
Normal file
12
.github/prompts/list-open-slot-requests.prompt.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
agent: 'agent'
|
||||||
|
model: Gemini 3 Flash (Preview) (copilot)
|
||||||
|
tools: [foodsharing-mcp/get_user_stores, foodsharing-mcp/get_store_pickups]
|
||||||
|
description: 'List open slot requests for my managed foodsharing stores'
|
||||||
|
---
|
||||||
|
|
||||||
|
Your goal is to look at all my stores that i manage (isManaging = true), retrieve the pickups and find where the slot's isConfirmed property is false
|
||||||
|
|
||||||
|
tools to use:
|
||||||
|
'Foodsharing Mcp/get_user_stores'
|
||||||
|
'Foodsharing Mcp/get_store_pickups'
|
||||||
104
FsMcp/Tools/StorePickupsTools.cs
Normal file
104
FsMcp/Tools/StorePickupsTools.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using FsMcp;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace FsMcp.Tools;
|
||||||
|
|
||||||
|
internal sealed class StorePickupsTools
|
||||||
|
{
|
||||||
|
private readonly FoodsharingApiClient _apiClient;
|
||||||
|
|
||||||
|
public StorePickupsTools(FoodsharingApiClient apiClient)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("List upcoming pickups for a store from GET /api/stores/{storeId}/pickups.")]
|
||||||
|
public async Task<IReadOnlyList<StorePickup>> GetStorePickupsAsync(
|
||||||
|
[Description("Store ID as positive integer.")]
|
||||||
|
int storeId)
|
||||||
|
{
|
||||||
|
if (storeId <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(storeId), "storeId must be a positive integer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiClient.EnsureLoginAsync();
|
||||||
|
|
||||||
|
var pickups = await _apiClient.HttpClient.GetFromJsonAsync<IReadOnlyList<StorePickup>>(Endpoints.StorePickups(storeId));
|
||||||
|
return pickups ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StorePickup
|
||||||
|
{
|
||||||
|
[Description("Date and time of the pickup.")]
|
||||||
|
[JsonPropertyName("date")]
|
||||||
|
public DateTime Date { get; init; }
|
||||||
|
|
||||||
|
[Description("Total number of available slots for this pickup.")]
|
||||||
|
[JsonPropertyName("totalSlots")]
|
||||||
|
public int TotalSlots { get; init; }
|
||||||
|
|
||||||
|
[Description("List of slots already occupied by users.")]
|
||||||
|
[JsonPropertyName("occupiedSlots")]
|
||||||
|
[JsonConverter(typeof(ObjectOrArrayConverter<OccupiedSlot>))]
|
||||||
|
public IReadOnlyList<OccupiedSlot> OccupiedSlots { get; init; } = [];
|
||||||
|
|
||||||
|
[Description("Whether slots are still available for this pickup.")]
|
||||||
|
[JsonPropertyName("isAvailable")]
|
||||||
|
[JsonConverter(typeof(NumberOrBoolConverter))]
|
||||||
|
public bool IsAvailable { get; init; }
|
||||||
|
|
||||||
|
[Description("Description or instructions for the pickup.")]
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OccupiedSlot
|
||||||
|
{
|
||||||
|
[Description("Whether the user's participation is confirmed.")]
|
||||||
|
[JsonPropertyName("isConfirmed")]
|
||||||
|
[JsonConverter(typeof(NumberOrBoolConverter))]
|
||||||
|
public bool IsConfirmed { get; init; }
|
||||||
|
|
||||||
|
[Description("Profile of the user who occupied the slot.")]
|
||||||
|
[JsonPropertyName("profile")]
|
||||||
|
public PickupUserProfile Profile { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PickupUserProfile
|
||||||
|
{
|
||||||
|
[Description("User identifier.")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
[Description("User display name.")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("URL to the user's avatar image.")]
|
||||||
|
[JsonPropertyName("avatar")]
|
||||||
|
public string? Avatar { get; init; }
|
||||||
|
|
||||||
|
[Description("Whether the user is currently in sleep mode.")]
|
||||||
|
[JsonPropertyName("isSleeping")]
|
||||||
|
[JsonConverter(typeof(NumberOrBoolConverter))]
|
||||||
|
public bool IsSleeping { get; init; }
|
||||||
|
|
||||||
|
[Description("Mobile phone number.")]
|
||||||
|
[JsonPropertyName("mobile")]
|
||||||
|
public string? Mobile { get; init; }
|
||||||
|
|
||||||
|
[Description("Landline phone number.")]
|
||||||
|
[JsonPropertyName("landline")]
|
||||||
|
public string? Landline { get; init; }
|
||||||
|
|
||||||
|
[Description("Whether the user is a store manager.")]
|
||||||
|
[JsonPropertyName("isManager")]
|
||||||
|
[JsonConverter(typeof(NumberOrBoolConverter))]
|
||||||
|
public bool IsManager { get; init; }
|
||||||
|
}
|
||||||
58
FsMcp/Tools/UserStoresTools.cs
Normal file
58
FsMcp/Tools/UserStoresTools.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using FsMcp;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace FsMcp.Tools;
|
||||||
|
|
||||||
|
internal sealed class UserStoresTools
|
||||||
|
{
|
||||||
|
private readonly FoodsharingApiClient _apiClient;
|
||||||
|
|
||||||
|
public UserStoresTools(FoodsharingApiClient apiClient)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("Returns the stores where a user is a member of from GET /api/users/{userId}/stores. Typically used with userId='current' to get the current user's stores.")]
|
||||||
|
public async Task<IReadOnlyList<StoreTeamMembershipWithPickupStatus>> GetUserStoresAsync(
|
||||||
|
[Description("User ID as positive integer or 'current'. Defaults to 'current'.")]
|
||||||
|
string userId = "current",
|
||||||
|
[Description("Whether to exclude inactive stores.")]
|
||||||
|
bool? excludeInactive = null)
|
||||||
|
{
|
||||||
|
await _apiClient.EnsureLoginAsync();
|
||||||
|
|
||||||
|
var stores = await _apiClient.HttpClient.GetFromJsonAsync<IReadOnlyList<StoreTeamMembershipWithPickupStatus>>(Endpoints.UserStores(userId, excludeInactive));
|
||||||
|
return stores ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StoreTeamMembershipWithPickupStatus
|
||||||
|
{
|
||||||
|
[Description("Unique identifier of the store.")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
[Description("Name of the store.")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Whether the user is a manager of this store.")]
|
||||||
|
[JsonPropertyName("isManaging")]
|
||||||
|
public bool IsManaging { get; init; }
|
||||||
|
|
||||||
|
[Description("Membership status of the user in the store team.")]
|
||||||
|
[JsonPropertyName("membershipStatus")]
|
||||||
|
public int MembershipStatus { get; init; }
|
||||||
|
|
||||||
|
[Description("Category type of the store.")]
|
||||||
|
[JsonPropertyName("categoryType")]
|
||||||
|
public int CategoryType { get; init; }
|
||||||
|
|
||||||
|
[Description("Pickup status for the user in this store. Can be null.")]
|
||||||
|
[JsonPropertyName("pickupStatus")]
|
||||||
|
public int? PickupStatus { get; init; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user