Add Conversations tools and endpoints; update README documentation
This commit is contained in:
@@ -21,5 +21,32 @@ public static class Endpoints
|
|||||||
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}";
|
||||||
|
|
||||||
|
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";
|
private static string ApiBase => "https://beta.foodsharing.de";
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ builder.Services
|
|||||||
.WithTools<FsMcp.Tools.RegionUsersTools>()
|
.WithTools<FsMcp.Tools.RegionUsersTools>()
|
||||||
.WithTools<FsMcp.Tools.SearchAllTools>()
|
.WithTools<FsMcp.Tools.SearchAllTools>()
|
||||||
.WithTools<FsMcp.Tools.RegionForumThreadsTools>()
|
.WithTools<FsMcp.Tools.RegionForumThreadsTools>()
|
||||||
.WithTools<FsMcp.Tools.ForumThreadTools>();
|
.WithTools<FsMcp.Tools.ForumThreadTools>()
|
||||||
|
.WithTools<FsMcp.Tools.ConversationsTools>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|||||||
@@ -267,3 +267,26 @@ For both VS Code and Visual Studio, the configuration file uses the following se
|
|||||||
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
|
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
|
||||||
- [GitHub Organization](https://github.com/modelcontextprotocol)
|
- [GitHub Organization](https://github.com/modelcontextprotocol)
|
||||||
- [MCP C# SDK](https://modelcontextprotocol.github.io/csharp-sdk)
|
- [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.
|
||||||
|
|
||||||
|
|||||||
181
FsMcp/Tools/ConversationsTools.cs
Normal file
181
FsMcp/Tools/ConversationsTools.cs
Normal 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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user