Add SearchAll tool and update endpoints; enhance README documentation

This commit is contained in:
2026-05-16 13:28:33 +02:00
parent 931dae79be
commit 776af859f0
4 changed files with 103 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ public static class Endpoints
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 RegionUsers(int regionId) => $"{ApiBase}/api/regions/{regionId}/users"; public static string RegionUsers(int regionId) => $"{ApiBase}/api/regions/{regionId}/users";
public static string StoreMembers(int storeId) => $"{ApiBase}/api/stores/{storeId}/members"; public static string StoreMembers(int storeId) => $"{ApiBase}/api/stores/{storeId}/members";
public static string SearchAll(string q, bool? global = null) => $"{ApiBase}/api/search/all?q={Uri.EscapeDataString(q)}" + (global.HasValue ? $"&global={global.Value.ToString().ToLower()}" : "");
public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) => public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) =>
$"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}"; $"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}";

View File

@@ -17,6 +17,6 @@ builder.Services
.WithTools<RegionStoresTools>() .WithTools<RegionStoresTools>()
.WithTools<StoreMembersTools>() .WithTools<StoreMembersTools>()
.WithTools<FsMcp.Tools.RegionUsersTools>() .WithTools<FsMcp.Tools.RegionUsersTools>()
.WithTools<StoreLogTools>(); .WithTools<FsMcp.Tools.SearchAllTools>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@@ -187,7 +187,23 @@ Notes for consumers:
- `20` declined team invitations / `Team-Einladungen ablehnen` - `20` declined team invitations / `Team-Einladungen ablehnen`
The tool is strongly typed in `Tools/StoreLogTools.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/StoreLogTools.cs`, with per-field `Description` attributes so MCP clients can surface schema-aware guidance directly in tool UIs.
### Search all tool
This server also exposes `get_search_all`.
- Purpose: Returns all kinds of searchable entry types from `GET /api/search/all`.
- Input:
- `q` (required, string; query to search for, must match `/^.+$/`)
- `global` (optional, boolean; whether to broaden search globally)
- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header.
- Output: Typed `MixedSearchResult` object with collections (`regions`, `workingGroups`, `stores`, `foodSharePoints`, `chats`, `threads`, `users`, `mails`, `events`, `polls`) of entities (`id`, `name`, `searchString`).
Notes for consumers:
- `q` cannot be empty.
- To reduce verbosity, all sub-entity arrays map to a shared base search result object (`id`, `name`, `searchString`). Detailed payloads for each entity type are ignored for simplicity.
The tool is strongly typed in `Tools/SearchAllTools.cs`, with per-field `Description` attributes.
## 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,85 @@
using System.ComponentModel;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using FsMcp;
using ModelContextProtocol.Server;
namespace FsMcp.Tools;
internal sealed class SearchAllTools
{
private readonly FoodsharingApiClient _apiClient;
public SearchAllTools(FoodsharingApiClient apiClient)
{
_apiClient = apiClient;
}
[McpServerTool]
[Description("General search endpoint returning all kinds of searchable entry types from GET /api/search/all.")]
public async Task<MixedSearchResult> GetSearchAllAsync(
[Description("Search query. Value must follow pattern /^.+$/ (cannot be empty).")]
string q,
[Description("Optional boolean to broaden the search to global results.")]
bool? global = null)
{
if (string.IsNullOrEmpty(q) || !Regex.IsMatch(q, "^.+$"))
{
throw new ArgumentException("Search query 'q' must not be empty and must match pattern /^.+$/.", nameof(q));
}
await _apiClient.EnsureLoginAsync();
var result = await _apiClient.HttpClient.GetFromJsonAsync<MixedSearchResult>(Endpoints.SearchAll(q, global));
return result ?? new MixedSearchResult();
}
}
public sealed record MixedSearchResult
{
[JsonPropertyName("regions")]
public IReadOnlyList<BaseSearchResult> Regions { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("workingGroups")]
public IReadOnlyList<BaseSearchResult> WorkingGroups { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("stores")]
public IReadOnlyList<BaseSearchResult> Stores { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("foodSharePoints")]
public IReadOnlyList<BaseSearchResult> FoodSharePoints { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("chats")]
public IReadOnlyList<BaseSearchResult> Chats { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("threads")]
public IReadOnlyList<BaseSearchResult> Threads { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("users")]
public IReadOnlyList<BaseSearchResult> Users { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("mails")]
public IReadOnlyList<BaseSearchResult> Mails { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("events")]
public IReadOnlyList<BaseSearchResult> Events { get; init; } = Array.Empty<BaseSearchResult>();
[JsonPropertyName("polls")]
public IReadOnlyList<BaseSearchResult> Polls { get; init; } = Array.Empty<BaseSearchResult>();
}
public sealed record BaseSearchResult
{
[Description("Unique identifier of the entity represented by the search result.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Name of the entity represented by the search result.")]
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[Description("Search criteria to test the search against.")]
[JsonPropertyName("searchString")]
public string? SearchString { get; init; }
}