Files
wx-win-agent/node-agent/WxAgent.Service/AgentTools.cs
T
rogee c7c0ab273f
Build web service image / build (push) Successful in 1m9s
feat: validate single-client broadcast operations
2026-09-19 14:33:26 +08:00

98 lines
9.5 KiB
C#

using System.ComponentModel;
using System.Text.Json;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace WxAgent.Service;
[McpServerToolType]
public sealed class AgentTools(AgentService service)
{
[McpServerTool(Name = "agent_status", ReadOnly = true), Description("Read authenticated agent and WeChat availability. Service online does not imply WeChat available.")]
public Task<CallToolResult> Status(CancellationToken cancellationToken) =>
Result(() => service.StatusAsync(cancellationToken));
[McpServerTool(Name = "agent_capabilities", ReadOnly = true), Description("Read implemented, validated and enabled capabilities and their limits. Disabled capabilities are not callable tools.")]
public Task<CallToolResult> Capabilities() => Result(() => Task.FromResult<object>(service.Capabilities()));
[McpServerTool(Name = "agent_diagnose", ReadOnly = true), Description("Run redacted diagnostics without changing the WeChat UI.")]
public Task<CallToolResult> Diagnose(CancellationToken cancellationToken = default) => Result(async () => await service.DiagnoseAsync(cancellationToken));
[McpServerTool(Name = "accounts_list", ReadOnly = true), Description("List database accounts and explicit binding state; a fingerprint does not prove current UI identity.")]
public Task<CallToolResult> Accounts(int limit = 50, int offset = 0, CancellationToken cancellationToken = default) => Result(async () => await service.AccountsAsync(limit, offset, cancellationToken));
[McpServerTool(Name = "ui_targets", ReadOnly = true), Description("List current visible WeChat window targets for explicit account binding.")]
public Task<CallToolResult> UiTargets(CancellationToken cancellationToken = default) => Result(async () => await service.UiTargetsAsync(cancellationToken));
[McpServerTool(Name = "account_bind"), Description("Explicitly bind one verified database account to one current WeChat window after identity verification.")]
public Task<CallToolResult> BindAccount(string accountId, string targetId, CancellationToken cancellationToken = default) => Result(async () => await service.BindAccountAsync(accountId, targetId, cancellationToken));
[McpServerTool(Name = "account_unbind"), Description("Remove one explicit account-to-window binding; it does not affect WeChat or database data.")]
public Task<CallToolResult> UnbindAccount(string accountId, CancellationToken cancellationToken = default) => Result(async () => { await service.UnbindAccountAsync(accountId, cancellationToken); return new { accountId, unbound = true }; });
[McpServerTool(Name = "sessions_list", ReadOnly = true), Description("List visible sessions for an explicitly bound account.")]
public Task<CallToolResult> Sessions(string accountId, int limit = 50, int offset = 0, CancellationToken cancellationToken = default) => Result(async () => await service.SessionsAsync(accountId, limit, offset, cancellationToken));
[McpServerTool(Name = "sessions_search", ReadOnly = true), Description("Search sessions for an explicitly bound account; ambiguous matches are not guessed.")]
public Task<CallToolResult> SearchSessions(string query, string accountId, bool exactOnly = false, int limit = 50, int offset = 0, CancellationToken cancellationToken = default) => Result(async () => await service.SearchSessionsAsync(accountId, query, exactOnly, limit, offset, cancellationToken));
[McpServerTool(Name = "session_current", ReadOnly = true), Description("Read the current session for an explicitly bound account.")]
public Task<CallToolResult> CurrentSession(string accountId, CancellationToken cancellationToken = default) => Result(async () => await service.CurrentSessionAsync(accountId, cancellationToken));
[McpServerTool(Name = "messages_read", ReadOnly = true), Description("Read bounded visible messages for an explicitly bound account; content requires the content permission.")]
public Task<CallToolResult> Messages(string accountId, string? session = null, int limit = 50, int offset = 0, bool includeContent = false, CancellationToken cancellationToken = default) => Result(async () => await service.MessagesAsync(accountId, session, limit, offset, includeContent, cancellationToken));
[McpServerTool(Name = "contacts_list", ReadOnly = true), Description("Read stable-ID contacts from an explicitly selected read-only database account.")]
public Task<CallToolResult> Contacts(string accountId, string? contains = null, bool? groupsOnly = null, int limit = 50, int offset = 0, CancellationToken cancellationToken = default) => Result(async () => await service.ContactsAsync(accountId, contains, groupsOnly, limit, offset, cancellationToken));
[McpServerTool(Name = "group_members", ReadOnly = true), Description("Read stable-ID members of an explicitly selected group and account.")]
public Task<CallToolResult> GroupMembers(string accountId, string group, int limit = 50, int offset = 0, CancellationToken cancellationToken = default) => Result(async () => await service.GroupMembersAsync(accountId, group, limit, offset, cancellationToken));
[McpServerTool(Name = "db_messages", ReadOnly = true), Description("Read bounded SQLCipher database messages for an explicitly verified account fingerprint; disabled until key scope is validated.")]
public Task<CallToolResult> DatabaseMessages(string accountId, string chatId, int limit = 50, int offset = 0, long? localId = null, CancellationToken cancellationToken = default) => Result(async () => await service.DatabaseMessagesAsync(accountId, chatId, limit, offset, localId, cancellationToken));
[McpServerTool(Name = "db_merged", ReadOnly = true), Description("Read one bounded merged database record for an explicitly verified account fingerprint; disabled until key scope is validated.")]
public Task<CallToolResult> DatabaseMerged(string accountId, string chatId, long localId, CancellationToken cancellationToken = default) => Result(async () => await service.DatabaseMergedAsync(accountId, chatId, localId, cancellationToken));
[McpServerTool(Name = "operations_list", ReadOnly = true), Description("List bounded operations owned by the current identity; accountId is a caller-side view filter, not an authorization boundary.")]
public Task<CallToolResult> Operations(string? accountId = null, int limit = 50, int offset = 0) => Result(() => Task.FromResult<object>(service.Operations(accountId, limit, offset)));
[McpServerTool(Name = "operation_submit"), Description("Submit one explicitly confirmed send-text operation. The targetId must be a frozen visible session automation ID.")]
public Task<CallToolResult> SubmitOperation(string kind, string accountId, string targetId, string? text, string idempotencyKey, bool confirmed = false, CancellationToken cancellationToken = default) =>
Result(async () => await service.SubmitOperationAsync(new OperationSubmitRequest(kind, accountId, targetId, text, idempotencyKey, confirmed), cancellationToken));
[McpServerTool(Name = "broadcast_text"), Description("Submit an explicitly confirmed broadcast-text operation for a frozen list of 1..20 visible session automation IDs. Sends sequentially, stops on the first known failure by default, and returns per-target results; replaying the same idempotency key never replays terminal writes.")]
public Task<CallToolResult> BroadcastText(string accountId, string[] targets, string text, string idempotencyKey, bool confirmed = false, bool stopOnError = true, CancellationToken cancellationToken = default) =>
Result(async () => await service.SubmitOperationAsync(new OperationSubmitRequest("broadcast-text", accountId, null, text, idempotencyKey, confirmed, targets, stopOnError), cancellationToken));
[McpServerTool(Name = "operation_get", ReadOnly = true), Description("Read one operation owned by the current identity; terminal writes are never replayed.")]
public Task<CallToolResult> Operation(string operationId) => Result(() => Task.FromResult<object>(service.Operation(operationId)));
[McpServerTool(Name = "operation_cancel"), Description("Request cancellation of your operation. An in-flight write may remain Unconfirmed; cancellation does not undo a side effect.")]
public Task<CallToolResult> CancelOperation(string operationId) => Result(() => Task.FromResult<object>(service.CancelOperation(operationId)));
internal static async Task<CallToolResult> Result(Func<Task<object>> action)
{
try
{
var value = await action();
return new CallToolResult { Content = [new TextContentBlock { Text = JsonSerializer.Serialize(value, ServiceHost.Json) }] };
}
catch (ServiceException e)
{
return new CallToolResult { IsError = true, Content = [new TextContentBlock
{ Text = JsonSerializer.Serialize(new { error = new { e.Code, e.Message, stage = "validation", retry = false } }, ServiceHost.Json) }] };
}
catch (OperationCanceledException)
{
return new CallToolResult { IsError = true, Content = [new TextContentBlock
{ Text = JsonSerializer.Serialize(new { error = new { code = "Cancelled", message = "Operation cancelled.", stage = "execution", retry = true } }, ServiceHost.Json) }] };
}
catch
{
return new CallToolResult { IsError = true, Content = [new TextContentBlock
{ Text = JsonSerializer.Serialize(new { error = new { code = "InternalError", message = "Tool failed; use the HTTP correlation ID for diagnosis.", stage = "execution", retry = false } }, ServiceHost.Json) }] };
}
}
}