diff --git a/.github/prompts/create-mcp-tool-from-openapi.prompt.md b/.github/prompts/create-mcp-tool-from-openapi.prompt.md new file mode 100644 index 0000000..96b37af --- /dev/null +++ b/.github/prompts/create-mcp-tool-from-openapi.prompt.md @@ -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:** `` +- **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()` 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 +- `` e.g. `/api/regions/{regionId}/stores` +- `` 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." diff --git a/FsMcp/Endpoints.cs b/FsMcp/Endpoints.cs index 08828e6..223610d 100644 --- a/FsMcp/Endpoints.cs +++ b/FsMcp/Endpoints.cs @@ -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"; } \ No newline at end of file diff --git a/FsMcp/Program.cs b/FsMcp/Program.cs index 93ebc50..9f25bbc 100644 --- a/FsMcp/Program.cs +++ b/FsMcp/Program.cs @@ -14,6 +14,7 @@ builder.Services .AddMcpServer() .WithStdioServerTransport() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/FsMcp/README.md b/FsMcp/README.md index 2a13c4a..cebd8d8 100644 --- a/FsMcp/README.md +++ b/FsMcp/README.md @@ -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 diff --git a/FsMcp/Tools/StoreMembersTools.cs b/FsMcp/Tools/StoreMembersTools.cs new file mode 100644 index 0000000..76cdec3 --- /dev/null +++ b/FsMcp/Tools/StoreMembersTools.cs @@ -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> 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>(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; } +} \ No newline at end of file