Add StoreMembers tool and update endpoints; enhance README
This commit is contained in:
74
.github/prompts/create-mcp-tool-from-openapi.prompt.md
vendored
Normal file
74
.github/prompts/create-mcp-tool-from-openapi.prompt.md
vendored
Normal 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."
|
||||
@@ -5,6 +5,7 @@ public static class Endpoints
|
||||
public static string UserLogin => $"{ApiBase}/api/login";
|
||||
public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details";
|
||||
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";
|
||||
}
|
||||
@@ -14,6 +14,7 @@ builder.Services
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<CurrentUserTools>()
|
||||
.WithTools<RegionStoresTools>();
|
||||
.WithTools<RegionStoresTools>()
|
||||
.WithTools<StoreMembersTools>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
1. Run `dotnet pack -c Release` to create the NuGet package
|
||||
|
||||
99
FsMcp/Tools/StoreMembersTools.cs
Normal file
99
FsMcp/Tools/StoreMembersTools.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user