Files
Playa/extensions/Volcanic.AgenticCore.Extensions.Mcp/Remote/RemoteMcpTool.cs
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

45 lines
2.2 KiB
C#

using System.Text.Json;
using ModelContextProtocol.Protocol;
using Volcanic.AgenticCore.Abstractions.Messages;
using Volcanic.AgenticCore.Abstractions.Tools;
namespace Volcanic.AgenticCore.Extensions.Mcp.Remote;
/// <summary>
/// A remote server tool as an agent tool. The registry can't drop or replace a tool, so the same instance is re-bound when
/// its server is added again or reconnects with a new schema, and unbound (and deactivated) when the server is removed.
/// </summary>
internal sealed class RemoteMcpTool(string exposedName) : ITool
{
private volatile Binding? _binding;
public string Name { get; } = exposedName;
public string Description => _binding?.Description ?? "Unavailable MCP tool.";
public JsonElement Parameters => _binding?.Parameters ?? McpTool.NormalizeSchema(default);
public string? PromptSnippet => null;
public ToolExecutionMode ExecutionMode => ToolExecutionMode.Sequential;
/// <summary>The server this tool currently calls, or null once the server is gone.</summary>
public string? Server => _binding?.Connection.Name;
public void Bind(Tool tool, RemoteServerConnection connection)
{
var description = !string.IsNullOrWhiteSpace(tool.Description) ? tool.Description!
: !string.IsNullOrWhiteSpace(tool.Annotations?.Title ?? tool.Title) ? (tool.Annotations?.Title ?? tool.Title)!
: tool.Name;
_binding = new Binding(tool.Name, $"[{connection.Name} MCP] {description}", McpTool.NormalizeSchema(tool.InputSchema), connection);
}
public void Unbind() => _binding = null;
public async Task<ToolResult> ExecuteAsync(ToolCallContent call, ToolInvocation invocation, CancellationToken cancellationToken)
{
if (_binding is not { } binding)
throw new InvalidOperationException($"{Name}: its MCP server was removed from this chat.");
var result = await binding.Connection.CallAsync(binding.McpName, call.Arguments, cancellationToken).ConfigureAwait(false);
return McpResultMapper.Map(binding.Connection.Name, binding.McpName, result);
}
private sealed record Binding(string McpName, string Description, JsonElement Parameters, RemoteServerConnection Connection);
}