diff --git a/FsMcp/Endpoints.cs b/FsMcp/Endpoints.cs index 07d7854..242f6e0 100644 --- a/FsMcp/Endpoints.cs +++ b/FsMcp/Endpoints.cs @@ -7,6 +7,7 @@ public static class Endpoints 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 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) => $"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}"; diff --git a/FsMcp/Program.cs b/FsMcp/Program.cs index 12915c9..0869ba9 100644 --- a/FsMcp/Program.cs +++ b/FsMcp/Program.cs @@ -17,6 +17,6 @@ builder.Services .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools(); await builder.Build().RunAsync(); diff --git a/FsMcp/README.md b/FsMcp/README.md index f76a7d1..16cb9af 100644 --- a/FsMcp/README.md +++ b/FsMcp/README.md @@ -187,7 +187,23 @@ Notes for consumers: - `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. +### 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 1. Run `dotnet pack -c Release` to create the NuGet package diff --git a/FsMcp/Tools/SearchAllTools.cs b/FsMcp/Tools/SearchAllTools.cs new file mode 100644 index 0000000..0b9ef33 --- /dev/null +++ b/FsMcp/Tools/SearchAllTools.cs @@ -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 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(Endpoints.SearchAll(q, global)); + return result ?? new MixedSearchResult(); + } +} + +public sealed record MixedSearchResult +{ + [JsonPropertyName("regions")] + public IReadOnlyList Regions { get; init; } = Array.Empty(); + + [JsonPropertyName("workingGroups")] + public IReadOnlyList WorkingGroups { get; init; } = Array.Empty(); + + [JsonPropertyName("stores")] + public IReadOnlyList Stores { get; init; } = Array.Empty(); + + [JsonPropertyName("foodSharePoints")] + public IReadOnlyList FoodSharePoints { get; init; } = Array.Empty(); + + [JsonPropertyName("chats")] + public IReadOnlyList Chats { get; init; } = Array.Empty(); + + [JsonPropertyName("threads")] + public IReadOnlyList Threads { get; init; } = Array.Empty(); + + [JsonPropertyName("users")] + public IReadOnlyList Users { get; init; } = Array.Empty(); + + [JsonPropertyName("mails")] + public IReadOnlyList Mails { get; init; } = Array.Empty(); + + [JsonPropertyName("events")] + public IReadOnlyList Events { get; init; } = Array.Empty(); + + [JsonPropertyName("polls")] + public IReadOnlyList Polls { get; init; } = Array.Empty(); +} + +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; } +} \ No newline at end of file