Files
EugeneTes 891a644590 feat(mcp): remote MCP servers per chat with sealed keys and owner-only commands
Streamable HTTP/SSE servers from the workspace's .volcanic/mcp/servers.json.
Headers are sealed with AES-GCM; the chat and the visible fields are the
associated data, so only /mcp_add|remove|tools change servers. Tools
<server>__<tool> go live in the session. An unreachable server is skipped
and reported in the system prompt. Keys are redacted from tool results, and
Telegram deletes messages that carry one.
2026-10-09 21:11:01 +00:00

53 lines
2.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Telegram.Bot.Types;
using Volcanic.AgenticCore.Extensions.Mcp.Remote;
using Volcanic.AgenticCore.Features.Extensions;
using Volcanic.AgenticCore.Features.Processes;
namespace Volcanic.AgenticCore.Telegram;
/// <summary>
/// Remote MCP servers per chat (<see cref="RemoteMcpExtension"/>). Keys are sealed with <c>MCP_ENCRYPTION_SECRET</c>, or
/// the bot token when that is unset; agent processes see neither (both match the default deny-list).
/// </summary>
public static class RemoteMcpSetup
{
public const string SecretVariable = "MCP_ENCRYPTION_SECRET";
public static readonly IReadOnlyList<BotCommand> Menu =
[
new() { Command = "mcp_list", Description = "Remote MCP servers of this chat" },
new() { Command = "mcp_add", Description = "Add an MCP server: /mcp_add <name> <url>" },
new() { Command = "mcp_remove", Description = "Remove an MCP server: /mcp_remove <name>" },
new() { Command = "mcp_tools", Description = "Show or limit a server's tools" },
];
public const string HelpLines =
"\n/mcp_list, /mcp_add <name> <url>, /mcp_remove <name>, /mcp_tools <name> [a,b|all] – this chat's remote MCP servers";
public static IReadOnlyList<BotCommand> MenuFor(IReadOnlyList<BotCommand> menu) => [.. menu, .. Menu];
/// <summary>The dedicated secret, else the bot token. Adds the dedicated one to the redacted secret values.</summary>
public static RemoteMcpOptions Options(TelegramOptions telegram, Func<string, string?> getEnv, ProcessEnvironmentOptions processEnvironment)
{
ArgumentNullException.ThrowIfNull(telegram);
ArgumentNullException.ThrowIfNull(getEnv);
ArgumentNullException.ThrowIfNull(processEnvironment);
var dedicated = getEnv(SecretVariable) is { } value && !string.IsNullOrWhiteSpace(value) ? value.Trim() : null;
if (dedicated is not null) processEnvironment.SecretValues.Add(dedicated);
return new RemoteMcpOptions { Secret = dedicated ?? (string.IsNullOrWhiteSpace(telegram.BotToken) ? null : telegram.BotToken) };
}
public static string Describe(Func<string, string?> getEnv) =>
string.IsNullOrWhiteSpace(getEnv(SecretVariable))
? $"Remote MCP: keys sealed with the bot token (set {SecretVariable} to keep them across a token change)"
: $"Remote MCP: keys sealed with {SecretVariable}";
/// <summary>A command whose message holds a key: the router deletes it from the chat after handing it over.</summary>
public static bool CarriesKey(string command, string args) =>
command == "mcp_add" && (args.Contains("--bearer", StringComparison.Ordinal) || args.Contains("--header", StringComparison.Ordinal));
/// <summary>A new extension instance per chat session.</summary>
public static ExtensionRegistration Registration(RemoteMcpOptions options) =>
new(RemoteMcpExtension.ExtensionName, _ => new RemoteMcpExtension(options), "telegram");
}