Add Conversations tools and endpoints; update README documentation

This commit is contained in:
2026-05-16 15:37:50 +02:00
parent e4f259a1d1
commit 2418966b82
4 changed files with 233 additions and 1 deletions

View File

@@ -21,5 +21,32 @@ public static class Endpoints
public static string StoreLogActions(int storeId, string fromDate, string toDate, string storeLogActionIds) =>
$"{ApiBase}/api/stores/{storeId}/log/{fromDate}/{toDate}/actions/{storeLogActionIds}";
public static string Conversations(int? limit = null, int? offset = null)
{
var query = new List<string>();
if (limit.HasValue) query.Add($"limit={limit.Value}");
if (offset.HasValue) query.Add($"offset={offset.Value}");
var qs = query.Count > 0 ? "?" + string.Join("&", query) : "";
return $"{ApiBase}/api/conversations{qs}";
}
public static string Conversation(int conversationId, int? limit = null, bool? markAsRead = null)
{
var query = new List<string>();
if (limit.HasValue) query.Add($"limit={limit.Value}");
if (markAsRead.HasValue) query.Add($"markAsRead={markAsRead.Value.ToString().ToLower()}");
var qs = query.Count > 0 ? "?" + string.Join("&", query) : "";
return $"{ApiBase}/api/conversations/{conversationId}{qs}";
}
public static string ConversationMessages(int conversationId, int? olderThanId = null, int? limit = null)
{
var query = new List<string>();
if (olderThanId.HasValue) query.Add($"olderThanId={olderThanId.Value}");
if (limit.HasValue) query.Add($"limit={limit.Value}");
var qs = query.Count > 0 ? "?" + string.Join("&", query) : "";
return $"{ApiBase}/api/conversations/{conversationId}/messages{qs}";
}
private static string ApiBase => "https://beta.foodsharing.de";
}

View File

@@ -19,6 +19,7 @@ builder.Services
.WithTools<FsMcp.Tools.RegionUsersTools>()
.WithTools<FsMcp.Tools.SearchAllTools>()
.WithTools<FsMcp.Tools.RegionForumThreadsTools>()
.WithTools<FsMcp.Tools.ForumThreadTools>();
.WithTools<FsMcp.Tools.ForumThreadTools>()
.WithTools<FsMcp.Tools.ConversationsTools>();
await builder.Build().RunAsync();

View File

@@ -267,3 +267,26 @@ For both VS Code and Visual Studio, the configuration file uses the following se
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
- [GitHub Organization](https://github.com/modelcontextprotocol)
- [MCP C# SDK](https://modelcontextprotocol.github.io/csharp-sdk)
## get_conversations
- **Endpoint:** GET /api/conversations
- **Purpose:** Get the list of conversations for the current user.
- **Input Parameters:** limit (int, optional), offset (int, optional).
- **Auth:** Requires login credentials environment variables (USERNAME, PASSWORD). Uses CSRF token handling.
- **Output:** Returns an array of Conversation objects along with ConversationProfile metadata.
## get_conversation
- **Endpoint:** GET /api/conversations/{conversationId}
- **Purpose:** Get a specific conversation including a peek at its messages.
- **Input Parameters:** conversationId (int, required), limit (int, optional), markAsRead (boolean, optional).
- **Auth:** Requires login credentials environment variables (USERNAME, PASSWORD). Uses CSRF token handling.
- **Output:** Returns a Conversation object and an array of ConversationProfile data.
## get_conversation_messages
- **Endpoint:** GET /api/conversations/{conversationId}/messages
- **Purpose:** Get messages from a specific conversation.
- **Input Parameters:** conversationId (int, required), olderThanId (int, optional - used to paginate older messages), limit (int, optional).
- **Auth:** Requires login credentials environment variables (USERNAME, PASSWORD). Uses CSRF token handling.
- **Output:** Returns a MessageCollection object with array of Message and ConversationProfile objects.

View File

@@ -0,0 +1,181 @@
using System.ComponentModel;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using FsMcp;
using ModelContextProtocol.Server;
namespace FsMcp.Tools;
internal sealed class ConversationsTools
{
private readonly FoodsharingApiClient _apiClient;
public ConversationsTools(FoodsharingApiClient apiClient)
{
_apiClient = apiClient;
}
[McpServerTool]
[Description(@"Get the list of conversations for the current user from GET /api/conversations.
Conversations with titles beginning with 'Team' are very likely store-chats. Conversations titled
with a first name are very likely private conversations with other users. The conversation title is
not necessarily the same as the store name or user name, though.")]
public async Task<ConversationsResponse> GetConversationsAsync(
[Description("Maximum number of results to return.")] int? limit = null,
[Description("The 0-based index of the first entry that is included.")] int? offset = null)
{
await _apiClient.EnsureLoginAsync();
var response = await _apiClient.HttpClient.GetAsync(Endpoints.Conversations(limit, offset));
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"API request failed with status {response.StatusCode}: {content}");
}
var result = await response.Content.ReadFromJsonAsync<ConversationsResponse>();
return result ?? new ConversationsResponse();
}
[McpServerTool]
[Description("Get a conversation including some messages from GET /api/conversations/{conversationId}.")]
public async Task<ConversationResponse> GetConversationAsync(
[Description("Conversation ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")] int conversationId,
[Description("Maximum number of results to return.")] int? limit = null,
[Description("Set to true to mark the conversation as read.")] bool? markAsRead = null)
{
if (conversationId <= 0) throw new ArgumentOutOfRangeException(nameof(conversationId), "conversationId must be positive.");
await _apiClient.EnsureLoginAsync();
var response = await _apiClient.HttpClient.GetAsync(Endpoints.Conversation(conversationId, limit, markAsRead));
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"API request failed with status {response.StatusCode}: {content}");
}
var result = await response.Content.ReadFromJsonAsync<ConversationResponse>();
return result ?? new ConversationResponse();
}
[McpServerTool]
[Description("Get messages from a conversation from GET /api/conversations/{conversationId}/messages.")]
public async Task<MessageCollection> GetConversationMessagesAsync(
[Description("Conversation ID as positive integer (OpenAPI path pattern: [1-9][0-9]*).")] int conversationId,
[Description("ID of a message to get older messages than this one.")] int? olderThanId = null,
[Description("Maximum number of results to return.")] int? limit = null)
{
if (conversationId <= 0) throw new ArgumentOutOfRangeException(nameof(conversationId), "conversationId must be positive.");
await _apiClient.EnsureLoginAsync();
var response = await _apiClient.HttpClient.GetAsync(Endpoints.ConversationMessages(conversationId, olderThanId, limit));
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"API request failed with status {response.StatusCode}: {content}");
}
var result = await response.Content.ReadFromJsonAsync<MessageCollection>();
return result ?? new MessageCollection();
}
}
public sealed record ConversationsResponse
{
[Description("List of conversations.")]
[JsonPropertyName("conversations")]
public IReadOnlyList<Conversation> Conversations { get; init; } = Array.Empty<Conversation>();
[Description("List of user profiles referenced in the conversations.")]
[JsonPropertyName("profiles")]
public IReadOnlyList<ConversationProfile> Profiles { get; init; } = Array.Empty<ConversationProfile>();
}
public sealed record ConversationResponse
{
[Description("The conversation object.")]
[JsonPropertyName("conversation")]
public Conversation? Conversation { get; init; }
[Description("List of user profiles referenced in this conversation.")]
[JsonPropertyName("profiles")]
public IReadOnlyList<ConversationProfile> Profiles { get; init; } = Array.Empty<ConversationProfile>();
}
public sealed record MessageCollection
{
[Description("List of messages.")]
[JsonPropertyName("messages")]
public IReadOnlyList<Message> Messages { get; init; } = Array.Empty<Message>();
[Description("List of user profiles referenced in the messages.")]
[JsonPropertyName("profiles")]
public IReadOnlyList<ConversationProfile> Profiles { get; init; } = Array.Empty<ConversationProfile>();
}
public sealed record Conversation
{
[Description("Unique identifier of the conversation.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Title of the conversation.")]
[JsonPropertyName("title")]
public string? Title { get; init; }
[Description("Store ID associated with the conversation, if any.")]
[JsonPropertyName("storeId")]
public int? StoreId { get; init; }
[Description("Number of unread messages.")]
[JsonPropertyName("unreadMessages")]
public int? UnreadMessages { get; init; }
[Description("The latest message in the conversation.")]
[JsonPropertyName("lastMessage")]
public Message? LastMessage { get; init; }
[Description("Messages in the conversation.")]
[JsonPropertyName("messages")]
public IReadOnlyList<Message> Messages { get; init; } = Array.Empty<Message>();
}
public sealed record Message
{
[Description("Unique identifier of the message.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Body of the message.")]
[JsonPropertyName("body")]
public string? Body { get; init; }
[Description("Timestamp when the message was sent.")]
[JsonPropertyName("sentAt")]
public DateTimeOffset? SentAt { get; init; }
[Description("ID of the author of the message.")]
[JsonPropertyName("authorId")]
public int? AuthorId { get; init; }
}
public sealed record ConversationProfile
{
[Description("ID of the user.")]
[JsonPropertyName("id")]
public int Id { get; init; }
[Description("Name of the user.")]
[JsonPropertyName("name")]
public string? Name { get; init; }
[Description("Avatar path of the user.")]
[JsonPropertyName("avatar")]
public string? Avatar { get; init; }
[Description("Whether the user is currently using the sleeping hat function.")]
[JsonPropertyName("isSleeping")]
public bool? IsSleeping { get; init; }
}