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.
53 lines
2.9 KiB
C#
53 lines
2.9 KiB
C#
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");
|
||
}
|