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 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";
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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