Add StoreMembers tool and update endpoints; enhance README

This commit is contained in:
2026-03-03 22:27:46 +01:00
parent 31e8db1ec3
commit a9b59567b7
5 changed files with 200 additions and 1 deletions

View File

@@ -0,0 +1,74 @@
# Reusable Prompt: Add a new MCP tool from `fsopenapi.json`
Use this prompt when adding a new MCP tool to this project.
---
You are working in the `fsmcp` workspace.
## Goal
Add a new MCP tool for this endpoint:
- **Endpoint path:** `<ENDPOINT_PATH>`
- **HTTP method:** `<HTTP_METHOD>`
Use `fsopenapi.json` as the source of truth for request/response schemas and constraints.
## Required implementation rules
1. **Read OpenAPI docs first**
- Find endpoint details in `fsopenapi.json`:
- summary/description
- parameters (path/query/body)
- response schema(s)
- relevant referenced component schemas and enums
- Use exact API field names via `JsonPropertyName`.
2. **Update endpoint constants**
- Add a new endpoint constant/function in `FsMcp/Endpoints.cs`.
- Follow existing style (`ApiBase`, interpolation, naming consistency).
3. **Create a new tool class**
- Create file in `FsMcp/Tools/` named after the endpoint purpose, e.g. `RegionStoresTools.cs`.
- Add one MCP tool method with `[McpServerTool]` and detailed `[Description]`.
- Inject and use `FoodsharingApiClient`.
- Always call `await _apiClient.EnsureLoginAsync();` before HTTP calls.
- Validate input parameters where appropriate (e.g. IDs > 0).
- Use strongly typed request/response models (`record` types).
- Add rich per-field `[Description]` metadata on model properties.
- Reuse existing shared models if already present (avoid duplication).
4. **Register tool in host**
- Add `.WithTools<YourNewToolsClass>()` in `FsMcp/Program.cs`.
5. **Extend documentation**
- Update `FsMcp/README.md` with a new section for the tool including:
- tool name
- endpoint + purpose
- input parameters
- auth behavior (`USERNAME`, `PASSWORD`, CSRF)
- output shape highlights
- enum/date/data-quality notes that help consumers
6. **Validate**
- Build with:
- `dotnet build FsMcp/FsMcp.csproj -c Debug`
- If build fails due to pre-existing unrelated issues, clearly state that and confirm whether new files are clean.
## Style constraints
- Keep changes minimal and focused.
- Match existing C# style and naming.
- Do not add unrelated refactors.
- Preserve API payload shape; do not rename wire-level JSON fields.
## Output format expected from you
At the end, provide:
1. Short summary of what changed.
2. List of changed files.
3. Validation result.
4. Any blockers or follow-up suggestions.
## Variables to fill before running this prompt
- `<ENDPOINT_PATH>` e.g. `/api/regions/{regionId}/stores`
- `<HTTP_METHOD>` e.g. `GET`
## Example invocation
"Add a new MCP tool for `GET /api/regions/{regionId}/stores` using this reusable prompt and implement all required steps."

View File

@@ -5,6 +5,7 @@ public static class Endpoints
public static string UserLogin => $"{ApiBase}/api/login"; public static string UserLogin => $"{ApiBase}/api/login";
public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details"; public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details";
public static string RegionStores(int regionId) => $"{ApiBase}/api/regions/{regionId}/stores"; public static string RegionStores(int regionId) => $"{ApiBase}/api/regions/{regionId}/stores";
public static string StoreMembers(int storeId) => $"{ApiBase}/api/stores/{storeId}/members";
private static string ApiBase => "https://beta.foodsharing.de"; private static string ApiBase => "https://beta.foodsharing.de";
} }

View File

@@ -14,6 +14,7 @@ builder.Services
.AddMcpServer() .AddMcpServer()
.WithStdioServerTransport() .WithStdioServerTransport()
.WithTools<CurrentUserTools>() .WithTools<CurrentUserTools>()
.WithTools<RegionStoresTools>(); .WithTools<RegionStoresTools>()
.WithTools<StoreMembersTools>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@@ -98,6 +98,30 @@ 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.
### Store members tool
This server also exposes `get_store_members`.
- Purpose: Returns all members of a store team from `GET /api/stores/{storeId}/members`.
- Input:
- `storeId` (required, positive integer; OpenAPI path pattern: `[1-9][0-9]*`)
- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header.
- Output: Array of typed member entries including:
- identity/profile fields (`id`, `name`, `avatar`, `firstName`, `isVerified`, `isSleeping`)
- team/role fields (`role`, `membershipStatus`, `isResponsible`, `memberSince`)
- activity/certificate fields (`fetchCount`, `hygieneCertificateUntil`)
- live-response compatibility fields (`handy`, `telefon`, `lastFetch`, `distanceInKm`)
Notes for consumers:
- `role` maps to OpenAPI enum `Role` with values `0, 1, 2, 3, 4, 5`.
- `membershipStatus` is a numeric API status code.
- Timestamp fields are parsed as ISO-8601 (`memberSince`, `hygieneCertificateUntil`, `lastFetch`) and may be `null` for some users.
- `distanceInKm` is numeric distance in kilometers and may be `null` when unavailable.
- Some fields present in live responses (for example `handy`, `telefon`, `lastFetch`, `distanceInKm`) are included to preserve practical API payload shape for consumers.
The tool is strongly typed in `Tools/StoreMembersTools.cs`, with per-field `Description` attributes so MCP clients can surface schema-aware guidance directly in tool UIs.
## Publishing to NuGet.org ## Publishing to NuGet.org
1. Run `dotnet pack -c Release` to create the NuGet package 1. Run `dotnet pack -c Release` to create the NuGet package

View File

@@ -0,0 +1,99 @@
using System.ComponentModel;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using FsMcp;
using ModelContextProtocol.Server;
internal sealed class StoreMembersTools
{
private readonly FoodsharingApiClient _apiClient;
public StoreMembersTools(FoodsharingApiClient apiClient)
{
_apiClient = apiClient;
}
[McpServerTool]
[Description("Returns all members of a store team from GET /api/stores/{storeId}/members. Useful for team roster visibility, responsibility checks, and member activity insights. Returns a typed list based on OpenAPI schema StoreStandbyTeamMember, with additional fields observed in live API responses.")]
public async Task<IReadOnlyList<StoreMemberListItem>> GetStoreMembersAsync(
[Description("Store ID as positive integer (OpenAPI path pattern: [1-9][0-9]*). Example: 19246.")]
int storeId)
{
if (storeId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(storeId), "storeId must be a positive integer.");
}
await _apiClient.EnsureLoginAsync();
var members = await _apiClient.HttpClient.GetFromJsonAsync<IReadOnlyList<StoreMemberListItem>>(Endpoints.StoreMembers(storeId));
return members ?? [];
}
}
public sealed record StoreMemberListItem
{
[Description("Unique user ID of the store member.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Display name of the store member.")]
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[Description("Relative API path of the member avatar image.")]
[JsonPropertyName("avatar")]
public string Avatar { get; init; } = string.Empty;
[Description("Whether the user currently has sleeping-hat mode enabled.")]
[JsonPropertyName("isSleeping")]
public bool IsSleeping { get; init; }
[Description("First name of the member.")]
[JsonPropertyName("firstName")]
public string FirstName { get; init; } = string.Empty;
[Description("Numeric role enum from OpenAPI Role: 0, 1, 2, 3, 4, 5.")]
[JsonPropertyName("role")]
public int Role { get; init; }
[Description("Whether the member account is verified.")]
[JsonPropertyName("isVerified")]
public bool IsVerified { get; init; }
[Description("Hygiene certificate validity end timestamp (ISO-8601). Can be null in live responses.")]
[JsonPropertyName("hygieneCertificateUntil")]
public DateTimeOffset? HygieneCertificateUntil { get; init; }
[Description("Whether the member has responsibility status for this store team.")]
[JsonPropertyName("isResponsible")]
public bool IsResponsible { get; init; }
[Description("Numeric membership status value from API.")]
[JsonPropertyName("membershipStatus")]
public int MembershipStatus { get; init; }
[Description("Number of fetches/pickups attributed to this member.")]
[JsonPropertyName("fetchCount")]
public int FetchCount { get; init; }
[Description("Member since timestamp (ISO-8601).")]
[JsonPropertyName("memberSince")]
public DateTimeOffset? MemberSince { get; init; }
[Description("Mobile/contact phone as returned by API (field name preserved from wire format).")]
[JsonPropertyName("handy")]
public string? Handy { get; init; }
[Description("Additional phone/contact field as returned by API (field name preserved from wire format).")]
[JsonPropertyName("telefon")]
public string? Telefon { get; init; }
[Description("Timestamp of last fetch/pickup (ISO-8601). Can be null.")]
[JsonPropertyName("lastFetch")]
public DateTimeOffset? LastFetch { get; init; }
[Description("Distance to store in kilometers. Can be null when unavailable.")]
[JsonPropertyName("distanceInKm")]
public int? DistanceInKm { get; init; }
}