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