Voltaic 2.1.2

dotnet add package Voltaic --version 2.1.2
                    
NuGet\Install-Package Voltaic -Version 2.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Voltaic" Version="2.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Voltaic" Version="2.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Voltaic" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Voltaic --version 2.1.2
                    
#r "nuget: Voltaic, 2.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Voltaic@2.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Voltaic&version=2.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Voltaic&version=2.1.2
                    
Install as a Cake Tool

<div align="center"> <img src="assets/logo.png" alt="Voltaic Logo" width="192" height="192"> </div>

Voltaic

NuGet Downloads License .NET

Modern, lightweight JSON-RPC 2.0, Model Context Protocol (MCP), and Agent2Agent (A2A) implementations for .NET 8.0 and .NET 10.0

Voltaic gives .NET applications a small, direct way to expose and consume structured agent protocols. Use it when you need JSON-RPC 2.0, MCP tools/resources/prompts, or A2A agents without adopting a larger application framework.

Voltaic v2.1.2 recognizes five MCP protocol revisions (2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and the stateless 2026-07-28) and targets A2A protocol version 1.0. The public API and source tree are split into Voltaic.Core, Voltaic.Mcp, and Voltaic.A2A.

An initialize handshake negotiates at most the newest handshake-era revision, 2025-11-25 (configurable with MaximumHandshakeProtocolVersion), because the stateless 2026-07-28 revision defines no initialize and no sessions. Clients reach 2026-07-28 through the stateless request path instead: server/discover followed by per-request MCP-Protocol-Version, Mcp-Method, and _meta signals. That is how current clients such as Claude Code 2.1.x connect, and Voltaic's stateless responses carry the resultType, ttlMs, and cacheScope fields the revision requires. Version selection is driven by the McpProtocol registry and McpVersionResolver, and it behaves identically with or without an AuthenticationHandler. See Protocol version negotiation.

v2.1.2 fixes the remaining specification deviations and hardens A2A. Invalid tool arguments are reported as tool execution errors (isError: true) the model can act on, initialize with an unknown version negotiates instead of failing, browser clients may send custom headers to allowed origins, non-browser clients may omit Accept/Content-Type, A2AHttpServer now delivers push notifications, and A2AGrpcServer gains origin, loopback, and extended-card protection. Verified against the MCP Inspector, the official Python SDK, and Claude Code. See Specification conformance and Upgrading to v2.1.2.

v2.1.1 is a spec-conformance patch: JSON-RPC responses POSTed by a client get 202, header-less requests assume 2025-03-26, stateless requests must carry their _meta protocol version, and McpHttpServer can publish OAuth protected resource metadata. See Specification conformance.

Upgrading to v2.1.0? v2.1.0 is a security release with safer defaults. HTTP and WebSocket servers validate the browser Origin (foreign origins get 403), a server bound to localhost serves loopback clients only, Streamable HTTP sessions are created only by a successful initialize (sessionless requests get 400, unknown session IDs get 404), McpWebsocketsServer gains an AuthenticationHandler, and the TCP transports reject anything that is not LSP-style framing. See Security defaults and Upgrading to v2.1.0.

Upgrading from v1.x? v2.0.0 is a breaking release. MCP servers no longer publish Voltaic's demo tools (ping, echo, getTime, getSessions/getClients) unless you opt in, ping returns {} as the MCP specification requires, tools are callable only through tools/call, and tool input schemas enforce additionalProperties. See Upgrading from v1.x and MIGRATE_V1_TO_V2.md.


What Is Voltaic?

Voltaic is a protocol library, not an application framework. It provides:

  • JSON-RPC 2.0 clients and servers over TCP with LSP-style Content-Length framing
  • MCP stdio servers and clients for subprocess-hosted tools
  • MCP Streamable HTTP on /mcp with MCP-Session-Id sessions and SSE notifications
  • MCP TCP and WebSocket transports for networked or full-duplex scenarios
  • A2A Agent Card discovery, JSON-RPC, HTTP+JSON, gRPC, SSE streaming, task lifecycle, push notification config APIs, and extended Agent Cards
  • Shared request/response, notification, lifecycle, and event handling across transports

You bring your business logic. Voltaic handles the protocol surface, message framing, method dispatch, session headers, and transport-specific plumbing.

What Can It Do?

  • Register JSON-RPC methods with synchronous, asynchronous, or cancellation-aware handlers.
  • Register MCP tools with input schema metadata, output schema metadata, structured content, annotations, icons, and full McpToolCallResult returns.
  • Validate common JSON Schema cases (type, required, nested properties, patternProperties, and additionalProperties) for tool input and structured output.
  • Expose MCP resources, resource templates, prompts, and completion providers.
  • Handle MCP initialize, tools/list, tools/call, resources/*, prompts/*, completion/complete, logging/setLevel, and utility notifications.
  • Send list-changed, resource-updated, progress, cancellation, and log-message notifications where the transport supports server-to-client notifications.
  • Host Voltaic's own request/response endpoints (/rpc and /events) alongside the Streamable HTTP endpoint (/mcp). They are a Voltaic convenience, not the deprecated 2024-11-05 HTTP+SSE transport.
  • Expose and consume A2A agents through dependency-light A2AClient, A2AHttpJsonClient, A2AGrpcClient, A2AHttpServer, and A2AGrpcServer classes without ASP.NET Core.
  • Run the same 521-case Touchstone suite through console, xUnit, and NUnit projects under src/.

MCP Endpoint Requirements

MCP uses JSON-RPC method names for protocol endpoints. For Streamable HTTP, those JSON-RPC messages are sent through the HTTP /mcp endpoint. /rpc (plain request/response JSON-RPC) and /events (an SSE stream for a session opened on /rpc) are Voltaic's own endpoints for simple clients and scripts. They are not the 2024-11-05 HTTP+SSE transport: /events sends no endpoint event, and results come back in the POST response.

Every MCP connection starts with the lifecycle methods:

  • initialize - required first request for handshake-era revisions. The client sends its supported protocol version, capabilities, and client info; the server responds with the negotiated protocol version, capabilities, and server info. See Protocol version negotiation for the version the server answers with.
  • notifications/initialized - required client notification after successful initialization. Normal operation starts after this notification.

Base utility methods:

  • ping - utility request that either side may send to check liveness. A receiver must respond promptly when it receives one. Voltaic MCP servers answer with the empty result {} (plus resultType: "complete" under 2026-07-28).

Streamable HTTP transport requirements:

  • POST /mcp receives JSON-RPC requests and notifications.
  • GET /mcp opens the SSE stream when the client wants server notifications.
  • A successful initialize creates the session and returns MCP-Session-Id; clients send it on every later request. A request without it (other than initialize or ping) gets 400, and an ID the server did not issue, has expired, or was terminated gets 404, which tells the client to re-initialize. See Sessions.
  • After initialization, HTTP clients send MCP-Protocol-Version on subsequent requests. When the header is absent and the session has no negotiated version, the server assumes 2025-03-26, as the specification requires.
  • A JSON-RPC response or error POSTed by the client is accepted with 202 and no body.

Server feature endpoints are capability-driven. If your server advertises a capability, it must support the corresponding methods:

  • tools capability: tools/list and tools/call.
  • resources capability: resources/list and resources/read. If resource templates are exposed, also support resources/templates/list. If resources.subscribe is advertised, also support resources/subscribe, resources/unsubscribe, and notifications/resources/updated.
  • prompts capability: prompts/list and prompts/get.
  • completions capability: completion/complete for prompt argument or resource-template argument suggestions.
  • logging capability: logging/setLevel from client to server, plus server notifications/message when logs are emitted.

Voltaic always registers the protocol methods for its MCP server types, including ping. Your application registers the handlers and data behind those methods with RegisterTool, RegisterResource, RegisterResourceTemplate, RegisterPrompt, and RegisterCompletionProvider.

Tools the server publishes

An MCP server publishes only the tools your application registers. tools/list shows exactly those tools, so an AI client's model sees only your product's tools.

  • Diagnostic tools are opt-in. Pass includeDiagnosticTools: true to a server constructor to also publish echo and getTime. They are useful while developing a server and are off by default. Voltaic no longer ships a getSessions/getClients tool, because it disclosed other clients' session identifiers.
  • Tools are invoked only through tools/call. tools/call validates arguments against the tool's input schema before your handler runs. A tool is not callable as a bare JSON-RPC method, so the schema check cannot be skipped. A tool may share a name with a protocol method (for example, a tool named ping) without replacing it.
  • Tools can be removed. UnregisterTool(name) removes a tool and returns true if it existed. Registering and unregistering tools does not notify clients by itself; call NotifyToolsChanged() (HTTP) or NotifyToolsChangedAsync() (TCP, WebSocket) afterwards to send notifications/tools/list_changed.
McpHttpServer server = new McpHttpServer("localhost", 8080);                               // only your tools
McpHttpServer devServer = new McpHttpServer("localhost", 8081, includeDiagnosticTools: true); // plus echo and getTime

server.RegisterTool("lookup", "Looks up a record", schema, handler);
server.UnregisterTool("lookup");
server.NotifyToolsChanged();

Tool input schemas are enforced for type, required, nested properties, patternProperties, and additionalProperties. With additionalProperties: false, a tools/call that sends an argument the schema does not declare is rejected with -32602 and a message naming the property.

Protocol version negotiation

MCP revisions fall into two eras. The handshake era (2024-11-05 through 2025-11-25) opens every connection with initialize and, on Streamable HTTP, an MCP-Session-Id. The stateless era (2026-07-28) has no initialize and no sessions: each request carries its own version in the MCP-Protocol-Version header and _meta, plus an Mcp-Method routing header, and clients learn what the server offers from server/discover.

Because initialize belongs to the handshake era, every Voltaic MCP server (HTTP, stdio, TCP, and WebSocket) answers it as follows:

Client requests in initialize Server answers
No version ProtocolVersion (default 2025-11-25), lowered to the cap if it is newer
A handshake-era version at or below the cap That version
A handshake-era version above the cap The cap
A stateless-era version (2026-07-28) The cap
An unknown version (older, newer, or malformed) The cap, as the specification requires ("respond with another protocol version it supports"); the client disconnects if it cannot use it

The cap is MaximumHandshakeProtocolVersion. It defaults to McpProtocol.NewestHandshakeProtocolVersion (2025-11-25), accepts only handshake-era revisions, and throws ArgumentException for anything else. Lower it to pin clients to an older revision:

McpHttpServer server = new McpHttpServer("localhost", 8080);
server.MaximumHandshakeProtocolVersion = McpProtocol.ProtocolVersion20250618;

A client that wants 2026-07-28 uses the stateless path on McpHttpServer, which is the only transport that serves it. Under that revision every result carries resultType: complete for a final result, input_required for a Multi Round-Trip result, or task for a created task. Cacheable results (tools/list, resources/list, resources/templates/list, prompts/list, resources/read, and server/discover) also carry ttlMs and cacheScope. Voltaic fills these in on the way out. Values a handler sets itself are kept, as are ListCacheTtlMs and ListCacheScope when configured. Otherwise the defaults are ttlMs: 0 and cacheScope: "private", which mean "do not cache; the result is specific to the caller". Handshake-era responses never include these fields.

server/discover does not advertise listChanged or resources.subscribe. Under 2026-07-28 those notifications are delivered through subscriptions/listen, which Voltaic does not implement yet. Handshake-era sessions still advertise and deliver them over SSE.

A method you register yourself with RegisterMethod gets the same treatment when it returns an McpResult subclass (for example McpEmptyResult or McpToolCallResult). A plain object, such as an anonymous type, is serialized unmodified, so a custom method that stateless clients call should return an McpResult subclass.

A2A Endpoint Requirements

A2A support lives in the Voltaic.A2A namespace. It follows the A2A v1.0 JSON wire shape used by the official a2a-dotnet SDK while keeping Voltaic's direct, dependency-light style.

  • Public Agent Card discovery: GET /.well-known/agent-card.json.
  • Version header sent by Voltaic clients: A2A-Version: 1.0.
  • JSON-RPC endpoint: configurable, default /a2a.
  • JSON-RPC methods: SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask, SubscribeToTask, push notification config CRUD, and GetExtendedAgentCard.
  • HTTP+JSON routes: POST /message:send, POST /message:stream, GET /tasks/{id}, GET /tasks, POST /tasks/{id}:cancel, POST /tasks/{id}:subscribe, /tasks/{id}/pushNotificationConfigs, and GET /extendedAgentCard.
  • gRPC service: lf.a2a.v1.A2AService over HTTP/2, with unary and server-streaming RPCs matching the A2A v1 service shape.
  • Streaming methods use SSE with data: events. JSON-RPC streaming sends JSON-RPC response envelopes; HTTP+JSON streaming sends direct StreamResponse payloads.

A2AHttpServer is built on HttpListener, not ASP.NET Core. It hosts Agent Card discovery, JSON-RPC, HTTP+JSON, SSE streams, task projection, in-memory task storage, push notification delivery, CORS, and an optional authentication hook. Applications provide agent behavior through IA2AAgentHandler.

A2AGrpcServer is built on the Watson HTTP/2 server package, not ASP.NET Core. It applies the same origin and loopback checks as A2AHttpServer, serves only the public Agent Card without authentication, and reports internal errors to clients generically (details go to its Log event). A2AGrpcClient uses plain HttpClient with gRPC framing and protobuf messages.

JSON-RPC errors are returned with HTTP 200 and the JSON-RPC error body, the JSON-RPC-over-HTTP convention; the HTTP+JSON binding uses the A2A status mapping (404 for task not found, 500 for internal errors, 400 otherwise). A2AClient reads the JSON-RPC error body whatever the HTTP status, so it also works with servers that send 4xx.

Push notifications

When the Agent Card advertises Capabilities.PushNotifications = true, A2AHttpServer (and A2AGrpcServer, which shares its task engine) delivers every event of a task to each webhook registered for it, either through the push notification config operations or through SendMessageConfiguration.PushNotificationConfig (taskPushNotificationConfig on the wire). Each delivery is an HTTP POST with a StreamResponse body (Content-Type: application/a2a+json), Authorization: {scheme} {credentials} from the config's authentication, and X-A2A-Notification-Token when the config has a token. Deliveries to one webhook are sent in order, time out after PushNotificationTimeoutMs (default 10 seconds), and are retried with exponential backoff up to PushNotificationMaxAttempts (default 3).

Webhook URLs are protected against server-side request forgery, as the A2A specification recommends. By default a URL must be http or https without user information, must not name localhost or a loopback, private, link-local, or carrier-grade NAT address, and is re-checked when the server connects, so a host name that resolves only to such addresses (including through DNS rebinding) is refused. Redirects are not followed and no proxy is used. Replace the policy with PushNotificationUrlValidator, for example to allow a list of webhook hosts, or loopback during local development:

server.PushNotificationUrlValidator = uri => uri.Host == "hooks.example.com" || uri.IsLoopback;

Why Use Voltaic?

  • Small API surface: Register handlers and start a transport; avoid framework-level ceremony.
  • Current MCP coverage: Tools, resources, prompts, completions, Streamable HTTP, sessions, and utility notifications are first-class.
  • A2A coverage: Agent Cards, JSON-RPC, HTTP+JSON, gRPC, task lifecycle, streaming, push config APIs, and extended Agent Cards are first-class.
  • Transport choice: Use stdio for local MCP servers, Streamable HTTP for MCP clients and inspectors, TCP for service-to-service RPC, WebSockets for full-duplex web-facing systems, or HTTP/SSE/gRPC for A2A agents.
  • Plain .NET: Works with normal C# delegates, System.Text.Json, Task, CancellationToken, and IDisposable.
  • Testable behavior: Protocol behavior is covered by shared Touchstone descriptors and adapter-backed test projects.

Who Is This For?

Voltaic is designed for developers building:

  • AI assistant integrations that need to expose MCP tools, resources, prompts, or completions from .NET.
  • Services that need structured JSON-RPC calls, REST-style HTTP+JSON routes, or a small gRPC binding without adopting a large hosting framework.
  • Local agents, CLIs, and desktop tools that launch MCP subprocesses over stdio.
  • Language-server-style protocols that use Content-Length message framing.
  • Web integrations that need Streamable HTTP, SSE notifications, or WebSocket communication.
  • Libraries and products that need protocol primitives without handing control to a large host framework.

Getting Started

Installation

dotnet add package Voltaic

A2A Server Example

using Voltaic.A2A;

string baseUrl = "http://localhost:8080";

AgentCard card = new AgentCard
{
    Name = "Echo Agent",
    Description = "A simple Voltaic A2A agent.",
    Version = "1.0.0",
    SupportedInterfaces = new List<AgentInterface>
    {
        new AgentInterface { Url = baseUrl + "/a2a", ProtocolBinding = "JSONRPC" },
        new AgentInterface { Url = baseUrl, ProtocolBinding = "HTTP+JSON" },
        new AgentInterface { Url = "http://localhost:8081", ProtocolBinding = "GRPC" }
    },
    Capabilities = new AgentCapabilities
    {
        Streaming = true,
        PushNotifications = true,
        StateTransitionHistory = true,
        ExtendedAgentCard = true
    },
    Skills = new List<AgentSkill>
    {
        new AgentSkill { Id = "echo", Name = "Echo", Description = "Echoes text." }
    },
    DefaultInputModes = new List<string> { "text/plain" },
    DefaultOutputModes = new List<string> { "text/plain" }
};

using A2AHttpServer server = new A2AHttpServer("localhost", 8080, card, new EchoAgent())
{
    ExtendedAgentCard = card
};
using A2AGrpcServer grpcServer = new A2AGrpcServer("localhost", 8081, card, new EchoAgent())
{
    ExtendedAgentCard = card
};

await server.StartAsync();
await grpcServer.StartAsync();
Console.WriteLine("A2A server listening on http://localhost:8080");
Console.WriteLine("A2A gRPC listening on http://localhost:8081");
await Task.Delay(Timeout.Infinite);

sealed class EchoAgent : IA2AAgentHandler
{
    public async Task ExecuteAsync(A2ARequestContext context, A2AAgentEventQueue eventQueue, CancellationToken token)
    {
        A2ATaskUpdater updater = new A2ATaskUpdater(eventQueue, context.TaskId, context.ContextId);
        await updater.SubmitAsync(token: token);
        await updater.StartAsync(token: token);

        string text = context.Message.Parts.FirstOrDefault()?.Text ?? string.Empty;
        Message response = new Message
        {
            Role = Role.Agent,
            MessageId = Guid.NewGuid().ToString("N"),
            TaskId = context.TaskId,
            ContextId = context.ContextId,
            Parts = new List<Part> { Part.FromText("echo: " + text) }
        };

        await updater.CompleteAsync(response, token);
    }
}

A2A Client Example

using Voltaic.A2A;

using HttpClient http = new HttpClient();
using A2ACardResolver resolver = new A2ACardResolver(http);
AgentCard card = await resolver.GetAgentCardAsync("http://localhost:8080");

AgentInterface jsonRpc = card.SupportedInterfaces.First(item => item.ProtocolBinding == "JSONRPC");
using A2AClient client = new A2AClient(jsonRpc.Url, http);

SendMessageRequest request = new SendMessageRequest
{
    Message = new Message
    {
        Role = Role.User,
        MessageId = Guid.NewGuid().ToString("N"),
        Parts = new List<Part> { Part.FromText("hello") }
    }
};

SendMessageResponse response = await client.SendMessageAsync(request);
Console.WriteLine(response.Task?.Status.State);

await foreach (StreamResponse item in client.SendStreamingMessageAsync(request))
{
    Console.WriteLine(item.StatusUpdate?.Status.State);
}

AgentInterface httpJson = card.SupportedInterfaces.First(item => item.ProtocolBinding == "HTTP+JSON");
using A2AHttpJsonClient restClient = new A2AHttpJsonClient(httpJson.Url, http);
SendMessageResponse restResponse = await restClient.SendMessageAsync(request);
Console.WriteLine(restResponse.Task?.Id);

AgentInterface? grpc = card.SupportedInterfaces.FirstOrDefault(item => item.ProtocolBinding == "GRPC");
if (grpc != null)
{
    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    using HttpClient grpcHttp = new HttpClient(new SocketsHttpHandler { EnableMultipleHttp2Connections = true });
    using A2AGrpcClient grpcClient = new A2AGrpcClient(grpc.Url, grpcHttp);
    SendMessageResponse grpcResponse = await grpcClient.SendMessageAsync(request);
    Console.WriteLine(grpcResponse.Task?.Id);
}

End-to-End MCP Example (Streamable HTTP)

This example creates a small MCP HTTP server with a tool, resource, resource template, prompt, and completion provider. The client connects with McpHttpClient, performs the MCP initialization flow, and calls the endpoint families those handlers power.

Create the server:

dotnet new console -n CalculatorServer
cd CalculatorServer
dotnet add package Voltaic

Replace Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Voltaic.Core;
using Voltaic.Mcp;

using McpHttpServer server = new McpHttpServer("localhost", 8080)
{
    ServerName = "CalculatorServer",
    ServerVersion = "1.0.0"
};

server.RegisterTool(
    "add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number", description = "First number" },
            b = new { type = "number", description = "Second number" }
        },
        required = new[] { "a", "b" }
    },
    (RpcParameters? args) =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;

        return (object)(a + b);
    });

server.RegisterResource(
    "voltaic://calculator/status",
    "status",
    "text/plain",
    () => new McpReadResourceResult
    {
        Contents = new List<object>
        {
            new McpTextResourceContents
            {
                Uri = "voltaic://calculator/status",
                MimeType = "text/plain",
                Text = "Calculator server is running."
            }
        }
    });

server.RegisterResourceTemplate(
    "voltaic://calculator/help/{topic}",
    "help-topic",
    "text/plain",
    uri => new McpReadResourceResult
    {
        Contents = new List<object>
        {
            new McpTextResourceContents
            {
                Uri = uri,
                MimeType = "text/plain",
                Text = $"Help content for {uri}."
            }
        }
    });

server.RegisterPrompt(
    "explain",
    "Creates an explanation prompt",
    new[]
    {
        new McpPromptArgument
        {
            Name = "topic",
            Description = "Topic to explain",
            Required = true
        }
    },
    args =>
    {
        string topic = args?.GetString("topic") ?? "the topic";

        return new McpGetPromptResult
        {
            Messages = new List<McpPromptMessage>
            {
                new McpPromptMessage
                {
                    Role = "user",
                    Content = new McpTextContent
                    {
                        Text = $"Explain {topic} with a calculator example."
                    }
                }
            }
        };
    });

server.RegisterCompletionProvider(
    "ref/prompt",
    "explain",
    "topic",
    (request, token) => Task.FromResult(new McpCompleteResult
    {
        Completion = new McpCompletion
        {
            Values = new List<string> { "addition", "subtraction", "multiplication", "division" }
                .Where(value => value.StartsWith(request.Argument.Value, StringComparison.OrdinalIgnoreCase))
                .Take(100)
                .ToList()
        }
    }));

await server.StartAsync();
Console.WriteLine("MCP server listening at http://localhost:8080/mcp");
await Task.Delay(Timeout.Infinite);

Run it:

dotnet run

Create the client in a second terminal:

dotnet new console -n CalculatorClient
cd CalculatorClient
dotnet add package Voltaic

Replace Program.cs:

using Voltaic.Core;
using Voltaic.Mcp;

using McpHttpClient client = new McpHttpClient();

await client.ConnectStreamableAsync("http://localhost:8080");

await client.CallAsync("initialize", new
{
    protocolVersion = "2025-11-25",
    capabilities = new { },
    clientInfo = new
    {
        name = "CalculatorClient",
        version = "1.0.0"
    }
});
await client.NotifyAsync("notifications/initialized");

JsonRpcResponse tools = await client.CallAsync("tools/list");
Console.WriteLine(tools.Result);

JsonRpcResponse resources = await client.CallAsync("resources/list");
Console.WriteLine(resources.Result);

JsonRpcResponse templates = await client.CallAsync("resources/templates/list");
Console.WriteLine(templates.Result);

JsonRpcResponse prompts = await client.CallAsync("prompts/list");
Console.WriteLine(prompts.Result);

JsonRpcResponse sum = await client.CallAsync("tools/call", new
{
    name = "add",
    arguments = new
    {
        a = 2,
        b = 3
    }
});
Console.WriteLine(sum.Result);

JsonRpcResponse resource = await client.CallAsync("resources/read", new
{
    uri = "voltaic://calculator/status"
});
Console.WriteLine(resource.Result);

JsonRpcResponse prompt = await client.CallAsync("prompts/get", new
{
    name = "explain",
    arguments = new
    {
        topic = "addition"
    }
});
Console.WriteLine(prompt.Result);

JsonRpcResponse completion = await client.CallAsync("completion/complete", new
{
    @ref = new
    {
        type = "ref/prompt",
        name = "explain"
    },
    argument = new
    {
        name = "topic",
        value = "ad"
    }
});
Console.WriteLine(completion.Result);

await client.CallAsync("logging/setLevel", new { level = "info" });
await client.CallAsync("resources/subscribe", new { uri = "voltaic://calculator/status" });
await client.CallAsync("resources/unsubscribe", new { uri = "voltaic://calculator/status" });

Run it:

dotnet run

McpHttpClient automatically uses the required Streamable HTTP headers, including Accept: application/json, text/event-stream and the MCP-Session-Id returned by the server. Call StartSseAsync() after ConnectStreamableAsync() if the client also needs server-sent notifications.

The server registrations above are the handlers behind the MCP endpoints:

  • RegisterTool(...) backs tools/list and tools/call.
  • RegisterResource(...) backs resources/list and resources/read.
  • RegisterResourceTemplate(...) backs resources/templates/list and template-based resources/read.
  • RegisterPrompt(...) backs prompts/list and prompts/get.
  • RegisterCompletionProvider(...) backs completion/complete.
  • initialize, notifications/initialized, notifications/cancelled, resources/subscribe, resources/unsubscribe, logging/setLevel, and ping are always registered.

More Quick Starts

JSON-RPC Server (TCP)

using System.Net;
using System.Text.Json;
using Voltaic.Core;
using Voltaic.Mcp;

// Exposes only the methods you register. Pass includeDiagnosticMethods: true to also
// register the ping, echo, getTime, and add diagnostic methods.
JsonRpcServer server = new JsonRpcServer(IPAddress.Any, 8080);

// Subscribe to events
server.ClientConnected += (sender, client) =>
    Console.WriteLine($"Client connected: {client.SessionId}");

server.RequestReceived += (sender, e) =>
    Console.WriteLine($"Request: {e.Method} from {e.Client.SessionId}");

server.ResponseSent += (sender, e) =>
    Console.WriteLine($"Response: {e.Method} took {e.Duration.TotalMilliseconds}ms");

// Register a synchronous method
server.RegisterMethod("greet", (RpcParameters? args) =>
{
    string? name = args?.GetString("name") ?? "World";
    return $"Hello, {name}!";
});

// Register an asynchronous method (for I/O-bound work like DB queries, HTTP calls, etc.)
server.RegisterMethod("fetchData", async (RpcParameters? args) =>
{
    // Async handlers avoid blocking the thread pool
    await Task.Delay(100); // Simulate async work
    return (object)"async result";
});

// Register an async method with cancellation support
server.RegisterMethod("longRunningTask", async (RpcParameters? args, CancellationToken token) =>
{
    // The token is the server's connection processing token
    await Task.Delay(5000, token); // Cancels if client disconnects
    return (object)"completed";
});

// Start the server
await server.StartAsync();
Console.WriteLine("Server running on port 8080");

// Keep it running
await Task.Delay(Timeout.Infinite, server.TokenSource.Token);

JSON-RPC Client (TCP)

using Voltaic.Core;
using Voltaic.Mcp;

JsonRpcClient client = new JsonRpcClient();

// Subscribe to notification events from server
client.NotificationReceived += (sender, request) =>
    Console.WriteLine($"Server notification: {request.Method}");

await client.ConnectAsync("localhost", 8080);

// Call a method with typed response
string greeting = await client.CallAsync<string>("greet", new { name = "Developer" });
Console.WriteLine(greeting); // "Hello, Developer!"

// Send a notification (no response expected)
await client.NotifyAsync("logEvent", new { level = "info", message = "User logged in" });

MCP Endpoint Handler Pattern

Use the same registration pattern with McpServer, McpHttpServer, McpTcpServer, and McpWebsocketsServer. Voltaic registers the protocol methods described in MCP Endpoint Requirements; your handlers supply the application data returned by the capability-driven endpoint families. The snippet uses System, System.Collections.Generic, System.Linq, System.Text.Json, and System.Threading.Tasks.

// tools/list and tools/call
server.RegisterTool(
    "add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number" },
            b = new { type = "number" }
        },
        required = new[] { "a", "b" }
    },
    args =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;
        return (object)(a + b);
    });

// resources/list and resources/read
server.RegisterResource("voltaic://example/status", "status", "text/plain",
    () => new McpReadResourceResult
    {
        Contents = new List<object>
        {
            new McpTextResourceContents
            {
                Uri = "voltaic://example/status",
                MimeType = "text/plain",
                Text = "Service is running."
            }
        }
    });

// resources/templates/list and template-based resources/read
server.RegisterResourceTemplate("voltaic://example/{name}", "example-item", "text/plain",
    uri => new McpReadResourceResult
    {
        Contents = new List<object>
        {
            new McpTextResourceContents
            {
                Uri = uri,
                MimeType = "text/plain",
                Text = $"Dynamic resource for {uri}."
            }
        }
    });

// prompts/list and prompts/get
server.RegisterPrompt("summarize", "Creates a summary prompt",
    new[] { new McpPromptArgument { Name = "topic", Required = true } },
    args => new McpGetPromptResult
    {
        Messages = new List<McpPromptMessage>
        {
            new McpPromptMessage
            {
                Role = "user",
                Content = new McpTextContent { Text = "Summarize the requested topic." }
            }
        }
    });

// completion/complete
server.RegisterCompletionProvider("ref/prompt", "summarize", "topic",
    (request, token) => Task.FromResult(new McpCompleteResult
    {
        Completion = new McpCompletion
        {
            Values = new List<string> { "Voltaic", "MCP", "JSON-RPC" }
                .Where(value => value.StartsWith(request.Argument.Value, StringComparison.OrdinalIgnoreCase))
                .Take(100)
                .ToList()
        }
    }));

initialize, notifications/initialized, notifications/cancelled, resources/subscribe, resources/unsubscribe, logging/setLevel, and ping are always registered.

MCP Server (stdio)

using System.Text.Json;
using Voltaic.Core;
using Voltaic.Mcp;

McpServer server = new McpServer();

// Customize server identity (optional)
server.ServerName = "MyMcpServer";
server.ServerVersion = "2.0.0";

// Register a tool with metadata for MCP tool discovery
server.RegisterTool("add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number", description = "First number" },
            b = new { type = "number", description = "Second number" }
        },
        required = new[] { "a", "b" }
    },
    (RpcParameters? args) =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;
        return (object)(a + b);
    });

// Protocol methods are always registered:
// - initialize (returns capabilities and serverInfo)
// - ping (returns {})
// - tools/list (returns all registered tools)
// - tools/call (invokes a tool by name)
// - resources/list, resources/templates/list, resources/read
// - resources/subscribe, resources/unsubscribe
// - prompts/list, prompts/get
// - completion/complete
// - logging/setLevel
// - notifications/initialized (handles client init notification)
// - notifications/cancelled
//
// tools/list returns only the tools registered above. Construct with
// new McpServer(includeDiagnosticTools: true) to also publish echo and getTime.

// Run the server (reads from stdin, writes to stdout)
await server.RunAsync();

MCP Client (stdio)

using Voltaic.Core;
using Voltaic.Mcp;

McpClient client = new McpClient();

// Launch an MCP server as a subprocess
await client.LaunchServerAsync("dotnet", new[] { "run", "--project", "MyMcpServer" });

// Call methods on the server
JsonRpcResponse response = await client.CallAsync("tools/list");
Console.WriteLine(response.Result);

MCP Server (TCP)

using System.Net;
using System.Text.Json;
using Voltaic.Core;
using Voltaic.Mcp;

McpTcpServer server = new McpTcpServer(IPAddress.Any, 8080);

// Subscribe to events
server.ClientConnected += (sender, client) =>
    Console.WriteLine($"Client connected: {client.SessionId}");

server.ClientDisconnected += (sender, client) =>
    Console.WriteLine($"Client disconnected: {client.SessionId}");

server.RegisterTool(
    "add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number", description = "First number" },
            b = new { type = "number", description = "Second number" }
        },
        required = new[] { "a", "b" }
    },
    (RpcParameters? args) =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;
        return (object)(a + b);
    });

// Start the server
await server.StartAsync();
Console.WriteLine("MCP server running on port 8080");
await Task.Delay(Timeout.Infinite, server.TokenSource.Token);

MCP Client (TCP)

using Voltaic.Core;
using Voltaic.Mcp;

McpTcpClient client = new McpTcpClient();

// Subscribe to server notifications
client.NotificationReceived += (sender, request) =>
    Console.WriteLine($"Server notification: {request.Method}");

// Connect to the TCP server
await client.ConnectAsync("localhost", 8080);

// Call methods on the server
object? tools = await client.CallAsync<object>("tools/list");
Console.WriteLine(tools);

MCP Server (HTTP)

using System.Text.Json;
using Voltaic.Core;
using Voltaic.Mcp;

McpHttpServer server = new McpHttpServer("localhost", 8080);

// Subscribe to events
server.ClientConnected += (sender, client) =>
    Console.WriteLine($"Session started: {client.SessionId}");

server.RequestReceived += (sender, e) =>
    Console.WriteLine($"Request: {e.Method} from session {e.Client.SessionId}");

// Register a tool (automatically added to tools/list and tools/call)
server.RegisterTool("add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number", description = "First number" },
            b = new { type = "number", description = "Second number" }
        },
        required = new[] { "a", "b" }
    },
    (RpcParameters? args) =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;
        return (object)(a + b);
    });

// Start the server
await server.StartAsync();
Console.WriteLine("MCP HTTP server running on http://localhost:8080");
await Task.Delay(Timeout.Infinite, server.TokenSource.Token);

The default McpHttpServer listens on all three HTTP endpoints:

  • /mcp for MCP Streamable HTTP (the specification's transport; use this for MCP clients)
  • /rpc for Voltaic's plain request/response JSON-RPC (scripts, curl, McpHttpClient.ConnectAsync)
  • /events for a Voltaic SSE notification stream tied to a session opened with initialize on /rpc

/rpc and /events are not the deprecated 2024-11-05 HTTP+SSE transport, so clients built for that transport cannot connect to them. Clients speaking 2024-11-05 connect over Streamable HTTP (/mcp), stdio, TCP, or WebSocket.

Set mcpPath: null in the constructor if you want to disable the Streamable HTTP endpoint.

The same /mcp endpoint serves both handshake-era clients (initialize plus a session) and stateless 2026-07-28 clients (server/discover plus per-request headers), such as Claude Code 2.1.x. No configuration is needed for either; see Protocol version negotiation.

MCP Client (HTTP)

using Voltaic.Core;
using Voltaic.Mcp;

McpHttpClient client = new McpHttpClient();

// Connect to the HTTP server
await client.ConnectAsync("http://localhost:8080");

// Start SSE connection for server notifications
await client.StartSseAsync();

// Call methods on the server
object? result = await client.CallAsync<object>("tools/list");
Console.WriteLine(result);

MCP Client (Streamable HTTP)

using Voltaic.Core;
using Voltaic.Mcp;

McpHttpClient client = new McpHttpClient();

// Establish the RPC/session side on POST /mcp
await client.ConnectStreamableAsync("http://localhost:8080");

// Open the SSE side on GET /mcp for notifications
await client.StartSseAsync();

// Call methods on the same session
object? result = await client.CallAsync<object>("tools/list");
Console.WriteLine(result);

ConnectStreamableAsync() performs the MCP handshake: it sends initialize (requesting client.ProtocolVersion and reporting ClientName/ClientVersion), stores the session ID and the negotiated protocol version, and sends notifications/initialized. ConnectAsync() does the same on /rpc, and the session it receives is the one /events streams to. Call StartSseAsync() when you want the SSE notification stream to become active. Connecting fails (returns false) when initialize returns an error.

When McpHttpClient talks to a server over the stateless 2026-07-28 path, it treats a result with resultType: "input_required" as a Multi Round-Trip request and any other result as final. A result with no resultType is also treated as final, so the client keeps working against servers that predate the field.

MCP Server (WebSocket)

using System.Text.Json;
using Voltaic.Core;
using Voltaic.Mcp;

McpWebsocketsServer server = new McpWebsocketsServer("localhost", 8080);

// Subscribe to events
server.ClientConnected += (sender, client) =>
    Console.WriteLine($"WebSocket client connected: {client.SessionId}");

server.ResponseSent += (sender, e) =>
    Console.WriteLine($"Sent response for {e.Method} in {e.Duration.TotalMilliseconds}ms");

server.RegisterTool(
    "add",
    "Adds two numbers",
    new
    {
        type = "object",
        properties = new
        {
            a = new { type = "number", description = "First number" },
            b = new { type = "number", description = "Second number" }
        },
        required = new[] { "a", "b" }
    },
    (RpcParameters? args) =>
    {
        double a = args?.GetDouble("a") ?? 0;
        double b = args?.GetDouble("b") ?? 0;
        return (object)(a + b);
    });

// Start the server
await server.StartAsync();
Console.WriteLine("MCP WebSocket server running on ws://localhost:8080/mcp");
await Task.Delay(Timeout.Infinite, server.TokenSource.Token);

MCP Client (WebSocket)

using Voltaic.Core;
using Voltaic.Mcp;

McpWebsocketsClient client = new McpWebsocketsClient();

// Subscribe to server notifications
client.NotificationReceived += (sender, request) =>
    Console.WriteLine($"Server notification: {request.Method}");

// Optional: credentials for a server with an AuthenticationHandler (sent on the upgrade request)
client.SetRequestHeader("Authorization", "Bearer " + token);

// Connect to the WebSocket server
await client.ConnectAsync("ws://localhost:8080/mcp");

// Call methods on the server
object? result = await client.CallAsync<object>("tools/list");
Console.WriteLine(result);

// Send a notification
await client.NotifyAsync("log", new { message = "Hello from WebSocket client" });

Security defaults

Voltaic servers usually run on a developer workstation or next to data they expose, often without authentication. Since v2.1.0 the defaults assume a web page in the user's browser, or another host on the network, may try to reach them.

Protection Applies to Default How to change it
Browser Origin validation (MCP spec requirement; blocks cross-site calls and DNS rebinding) McpHttpServer, McpWebsocketsServer, A2AHttpServer Requests without Origin and loopback origins (http(s)://localhost, 127.0.0.0/8, [::1], any port) are allowed; every other origin gets 403 before preflight or authentication server.OriginPolicy.AllowedOrigins.Add("https://app.example.com"), AllowLoopbackOrigins, or a custom OriginValidator
Browser Origin validation A2AGrpcServer Same rules; only the public Agent Card skips authentication OriginPolicy
CORS McpHttpServer, A2AHttpServer The allowed origin is echoed (never *) with Vary: Origin. A preflight gets back the header names it asks for (valid tokens only), so custom headers such as X-API-Key or Mcp-Param-* work; otherwise an explicit Access-Control-Allow-Headers list that includes Authorization is sent. WWW-Authenticate is exposed so browser clients can read an OAuth challenge EnableCors, CorsHeaders
Loopback-only clients for loopback binds McpHttpServer, McpWebsocketsServer, A2AHttpServer, A2AGrpcServer On when the host name is localhost, 127.x.x.x, or ::1: a request from any other address gets 403. On Windows, HttpListener serves a localhost prefix on every interface and routes by the spoofable Host header, so this check is what keeps it local Bind to +/* or a specific address, or set RestrictToLoopbackClients = false
JSON bodies on POST /mcp McpHttpServer A Content-Type other than application/json gets 415. A request with no Content-Type is accepted only without an Origin header (a non-browser client), because a browser can send such a body without a CORS preflight. A missing Accept header counts as */* None needed
Webhook targets A2AHttpServer, A2AGrpcServer Push notification URLs must not target loopback, private, or link-local addresses; checked when the config is created and again when connecting PushNotificationUrlValidator
Initialize-only sessions, principal binding McpHttpServer See Sessions RequireInitializedSessions = false for older ping-first clients
Strict LSP framing JsonRpcServer, McpTcpServer, JsonRpcClient, McpTcpClient Only Content-Length and Content-Type header lines (1024 bytes at most) are accepted, so an HTTP request from a browser fetch() is dropped before anything runs None; Voltaic and LSP-style clients send only those headers
McpHttpServer server = new McpHttpServer("localhost", 8080);

// Allow a browser app served from another origin (loopback origins are already allowed)
server.OriginPolicy.AllowedOrigins.Add("https://tools.example.com");

// Serve other machines: bind to all interfaces (requires a URL ACL or admin rights on Windows)
McpHttpServer lanServer = new McpHttpServer("+", 8080);

The TCP and stdio transports stay unauthenticated by design; strict framing is what stops browsers from talking to them.

Sessions

On the handshake-era Streamable HTTP path, session IDs are always generated by the server:

  • A successful initialize (on /mcp or /rpc) creates the session, raises ClientConnected, and returns MCP-Session-Id. A rejected initialize creates nothing and returns no header.
  • POST /mcp without MCP-Session-Id gets 400 (-32600), except initialize and ping. A sessionless ping is answered without creating a session, so it still works as a connectivity check.
  • POST /rpc without a session runs on a temporary connection and returns no session header, so plain request/response callers (scripts, curl) keep working without leaving sessions behind.
  • An MCP-Session-Id the server did not issue, has expired, or was terminated gets 404 (-32001) on every endpoint; it is never adopted. The client should send initialize again.
  • With an AuthenticationHandler, a session belongs to the principal that created it. Another principal presenting that ID gets 404.
  • Every request on a session counts as activity, so SessionTimeoutSeconds expires only idle sessions.
  • The ?session= query parameter is accepted only on GET streams (/events, GET /mcp), for browser EventSource clients that cannot set headers.

Clients built on Voltaic 2.0.0 or earlier open their session with ping instead of initialize. To serve them during a transition, set server.RequireInitializedSessions = false: any successful sessionless request is then issued a new session. Unknown IDs are still rejected and failed requests still create nothing. The stateless 2026-07-28 path never uses sessions.

Authentication

McpHttpServer supports an optional async authentication handler that runs before request processing. When set, every request that passes the loopback and origin checks (see Security defaults) is passed through the handler, except the exceptions listed below. The handler receives the full HttpListenerRequest and returns an AuthenticationResult. If authentication fails, the server returns the result's status code, headers, and error message without processing the request. When no handler is set, requests are not authenticated, but the loopback and origin checks still apply.

using System.Net;
using Voltaic.Core;
using Voltaic.Mcp;

McpHttpServer server = new McpHttpServer("localhost", 8080);

server.AuthenticationHandler = async (HttpListenerRequest request) =>
{
    string? token = request.Headers["Authorization"];
    if (string.IsNullOrEmpty(token) || !token.StartsWith("Bearer "))
    {
        return new AuthenticationResult
        {
            IsAuthenticated = false,
            StatusCode = 401,
            ErrorMessage = "Missing or invalid Authorization header"
        };
    }

    // Validate the token with your JWT validator, database, identity provider, etc.
    bool isValid = await ValidateTokenAsync(token.Substring("Bearer ".Length));

    return new AuthenticationResult
    {
        IsAuthenticated = isValid,
        Principal = "my-user",
        Claims = new Dictionary<string, string> { { "role", "admin" } }
    };
};

await server.StartAsync();

The following requests are served even when authentication fails, so infrastructure can validate connectivity and clients can discover how to authenticate. Origin validation and the loopback check still apply to them:

  • Health check (GET /) - returns {"status":"Ok"} for load balancer probes. The handler is not called.
  • Protected resource metadata (GET /.well-known/oauth-protected-resource, and the same path followed by the MCP endpoint path) - served when ProtectedResourceMetadata is set. The handler is not called.
  • Ping (the MCP ping request via any RPC endpoint) - returns {} for application-layer connectivity checks. The handler still runs, so an authenticated ping carries its caller (and can use that caller's session); a ping that fails authentication is answered anyway, without a caller. The bypass covers only the protocol ping; a tool named ping is invoked through tools/call and is authenticated like any other tool.
  • CORS preflight (OPTIONS requests) - returns 204 with CORS headers for an allowed origin. The handler is not called.

To shape the rejection, add headers to the failed result. AuthenticationResult.BearerChallenge() builds the RFC 6750 challenge MCP clients use to discover your authorization server:

server.AuthenticationHandler = request =>
{
    if (request.Headers["Authorization"] != "Bearer " + expectedToken)
    {
        // 401 with WWW-Authenticate: Bearer resource_metadata="...", error="invalid_token"
        return Task.FromResult(AuthenticationResult.BearerChallenge(
            "https://api.example.com/.well-known/oauth-protected-resource", "invalid_token"));
    }

    return Task.FromResult(new AuthenticationResult { IsAuthenticated = true, Principal = "user" });
};

// Any other header works too, for example Retry-After on a throttled rejection:
// result.Headers["Retry-After"] = "30";

McpHttpServer, McpWebsocketsServer, and A2AHttpServer write AuthenticationResult.Headers on every rejection.

Setting an AuthenticationHandler never changes protocol behavior. Once a request is authenticated (or bypasses authentication, like ping), it runs through exactly the same MCP pipeline as on a server without a handler: version resolution, stateless 2026-07-28 routing, the batching rules, session tracking, and the resultType/cache fields on stateless results. Before v1.1.0, authenticated requests took a separate path that skipped most of these, and stateless clients such as Claude Code could not list tools on an authenticated server.

OAuth and protected resource metadata

The MCP authorization specification makes the MCP server an OAuth 2.1 resource server: it must publish OAuth 2.0 Protected Resource Metadata (RFC 9728) naming its authorization server, answer unauthenticated requests with 401 and a WWW-Authenticate challenge, and validate every access token, including that the token was issued for this server (its audience). Voltaic covers the resource-server plumbing:

McpHttpServer server = new McpHttpServer("+", 8443);

// Served without authentication at /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp
server.ProtectedResourceMetadata = new McpProtectedResourceMetadata
{
    Resource = "https://mcp.example.com/mcp",
    AuthorizationServers = new List<string> { "https://auth.example.com" },
    ScopesSupported = new List<string> { "tools:read", "tools:write" }
};

server.AuthenticationHandler = async request =>
{
    string? header = request.Headers["Authorization"];
    AccessToken? token = header != null && header.StartsWith("Bearer ")
        ? await ValidateJwtAsync(header.Substring(7), expectedAudience: "https://mcp.example.com/mcp")
        : null;

    if (token == null)
    {
        return AuthenticationResult.BearerChallenge(
            "https://mcp.example.com" + McpProtocol.ProtectedResourceMetadataPath, "invalid_token");
    }

    return new AuthenticationResult { IsAuthenticated = true, Principal = token.Subject };
};

The authorization server itself, token issuance, and token validation (signature, expiry, audience, scopes) remain the application's responsibility; ValidateJwtAsync above stands for your validator. Serve OAuth-protected servers over HTTPS in production, for example behind a TLS-terminating reverse proxy.

Sending authentication from the client

McpHttpClient.SetRequestHeader(name, value) attaches a header to every request the client sends — the JSON-RPC POST requests and the SSE GET stream, including the initial connection handshake. Use it to send a bearer token or a custom API-key header to an authenticated server. Set the header before connecting so it is present on the handshake. Passing a null or empty value removes a previously set header, and header names are matched case-insensitively.

using Voltaic.Mcp;

using McpHttpClient client = new McpHttpClient();

// Bearer token: Authorization: Bearer <token>
client.SetRequestHeader("Authorization", "Bearer " + token);

// Or a custom API-key header instead:
// client.SetRequestHeader("X-API-Key", apiKey);

await client.ConnectStreamableAsync("http://localhost:8080");

This pairs with the server-side AuthenticationHandler above: the client sends the credential and the handler validates it. The header is present on the initialize handshake, so the session the server creates belongs to the authenticated principal.

WebSocket authentication

McpWebsocketsServer.AuthenticationHandler takes the same delegate as McpHttpServer and runs on the upgrade request, after the origin and loopback checks. A failure refuses the upgrade with the result's status code, headers, and message. On success, the caller is stored in ClientConnection.Caller (visible to ClientConnected subscribers) and is the ambient RpcCallContext.Current for every request on that socket. On the client, McpWebsocketsClient.SetRequestHeader sends credentials on the upgrade request.

McpWebsocketsServer server = new McpWebsocketsServer("localhost", 8080);
server.AuthenticationHandler = request => Task.FromResult(
    request.Headers["Authorization"] == "Bearer " + expectedToken
        ? new AuthenticationResult { IsAuthenticated = true, Principal = "user" }
        : AuthenticationResult.BearerChallenge());

McpWebsocketsClient client = new McpWebsocketsClient();
client.SetRequestHeader("Authorization", "Bearer " + expectedToken);
await client.ConnectAsync("ws://localhost:8080/mcp");

Authorizing inside a handler

AuthenticationHandler decides whether a request is allowed in; to make per-caller authorization decisions inside a tool or method handler (scope reads to a tenant, gate writes by role, and so on) the handler needs the caller's identity. Voltaic carries it there for you.

After a successful AuthenticationResult, McpHttpServer (per request) and McpWebsocketsServer (per socket) publish the caller's Principal and Claims as an ambient Voltaic.Core.RpcCallContext. Because it flows on the request's async call chain, any handler can read RpcCallContext.Current without changing its signature. Current is null when no AuthenticationHandler is configured, on transports that do not authenticate (TCP and stdio), and for a ping that failed authentication.

using Voltaic.Core;
using Voltaic.Mcp;

server.RegisterTool("list_orders", "List the caller's orders", inputSchema, async (RpcParameters? args, CancellationToken token) =>
{
    RpcCallContext? caller = RpcCallContext.Current;
    if (caller == null) throw new InvalidOperationException("Unauthenticated.");

    string tenantId = caller.Claims.TryGetValue("tenantId", out string? t) ? t : throw new InvalidOperationException("No tenant.");
    bool isAdmin = caller.Claims.TryGetValue("isAdmin", out string? a) && a == "true";

    return await LoadOrdersAsync(tenantId, caller.Principal, isAdmin, token);
});

If you prefer the caller passed in explicitly rather than read from an ambient, every RegisterMethod/RegisterTool surface also offers an overload that receives an RpcCallContext? parameter (it simply forwards RpcCallContext.Current):

server.RegisterTool("list_orders", "List the caller's orders", inputSchema,
    async (RpcParameters? args, RpcCallContext? caller, CancellationToken token) =>
    {
        // caller == RpcCallContext.Current
        return await LoadOrdersAsync(caller, token);
    });

Both forms are additive: existing handlers that ignore the caller compile and behave exactly as before.

Authorizing inside an A2A agent

The A2A servers carry the caller the same way, but through the context object the handler already receives rather than an ambient. When A2AHttpServer or A2AGrpcServer authenticates a request, it copies the AuthenticationResult's Principal and Claims onto the A2ARequestContext before your IA2AAgentHandler runs — so the identity is available even though A2A agents execute on a background task and may stream results after the request returns. Principal/Claims are null when no AuthenticationHandler is configured or for public Agent Card requests.

public async Task ExecuteAsync(A2ARequestContext context, A2AAgentEventQueue eventQueue, CancellationToken token)
{
    string? principal = context.Principal;                 // who is calling
    string? tenantId = context.Claims != null && context.Claims.TryGetValue("tenantId", out string? t) ? t : null;

    // ... scope the agent's work to the authenticated caller ...
}

Specification conformance

Voltaic implements the MCP revisions 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and 2026-07-28. The table lists where it deliberately differs from the specification text, and the optional features it does not implement.

Area Voltaic behavior Specification
Sessionless ping on /mcp Answered (no session is created), so it can serve as a connectivity check Servers that require sessions SHOULD answer requests without MCP-Session-Id (other than initialize) with 400. The lifecycle permits ping before initialization.
RequireInitializedSessions = false Issues a session for any successful sessionless request Sessions are assigned at initialization. The option exists only for clients built on Voltaic 2.0.0 or earlier.
x-mcp-header tool annotations (2026-07-28) Not validated: Mcp-Param-* headers are ignored Servers MUST validate Mcp-Param-* headers against the body for tools that declare x-mcp-header. Do not use the annotation in Voltaic tool schemas.
SSE priming and resumability (2025-11-25) Streams start with a : connected comment; Last-Event-ID is not supported The server SHOULD send a priming event with an ID; resumability is optional.
HTTP+SSE transport (2024-11-05) Not implemented; /rpc and /events are different, Voltaic-specific endpoints Deprecated since 2025-03-26. 2024-11-05 itself is supported over Streamable HTTP, stdio, TCP, and WebSocket.
Server-to-client requests (sampling, elicitation, roots) on handshake-era sessions Not sent Optional. On 2026-07-28, handlers can return McpInputRequiredResult (Multi Round-Trip Requests) instead.
subscriptions/listen (2026-07-28) Not implemented; server/discover does not advertise listChanged or subscribe Optional, capability-driven. Handshake-era sessions still receive change notifications over SSE.
OAuth authorization Resource-server plumbing only: ProtectedResourceMetadata, BearerChallenge, and AuthenticationHandler The authorization server and token validation are application responsibilities. See OAuth and protected resource metadata.
2026-07-28 on stdio, TCP, and WebSocket Not served: these servers speak the handshake-era revisions only (McpHttpServer serves both eras) Permitted: servers may support only legacy revisions. Dual-era clients, such as the official Python SDK, fall back to initialize automatically.
Tool handler exceptions Returned as JSON-RPC -32603 (internal error) The specification lists API and business-logic failures as tool execution errors. To report one to the model, return McpToolCallResult with IsError = true from the handler instead of throwing.

Behavior that follows the specification, for reference:

  • Invalid Origin gets 403; sessions come only from a successful initialize; a missing session gets 400 and an unknown or terminated one gets 404; DELETE ends a session; notifications and client-sent responses get 202 with no body; a batch with nothing to answer gets 202.
  • A missing MCP-Protocol-Version header, with no negotiated version, is treated as 2025-03-26; an unsupported value gets 400.
  • initialize with a version the server does not know is answered with MaximumHandshakeProtocolVersion, never an error.
  • Tool arguments that fail the input schema produce a tool result with isError: true and a message naming the problem (a tool execution error the model can correct); the handler does not run. Unknown tools and malformed tools/call requests are JSON-RPC protocol errors.
  • On 2026-07-28, notification POSTs need no routing headers (the revision defines none for them); requests do.
  • On 2026-07-28: MCP-Protocol-Version must match params._meta["io.modelcontextprotocol/protocolVersion"] (a missing or different value gets 400 -32020), Mcp-Method and Mcp-Name are required and validated (base64 sentinel values are decoded), an unsupported version gets 400 -32022 with the supported list, an unknown method gets 404 -32601, and server/discover is always available.

Upgrading to v2.1.2

v2.1.2 changes a few behaviors to match the specifications:

Change Who is affected What to do
Invalid tool arguments return a result with isError: true instead of JSON-RPC -32602 Code that checked for -32602 after tools/call Check isError on the result
initialize with an unknown version negotiates MaximumHandshakeProtocolVersion instead of failing Tests that expected an error Expect the negotiated version
A2A JSON-RPC errors use HTTP 200 (the error is in the body) Clients that relied on 4xx statuses for JSON-RPC errors Read the JSON-RPC error object; A2AClient already does
A2A push notification configs require an existing task and an allowed webhook URL Callers that registered configs for unknown tasks or local URLs Create the task first; set PushNotificationUrlValidator to allow local webhooks during development
A2AGrpcServer requires authentication for GET /extendedAgentCard, validates Origin, and serves loopback clients only when bound to localhost Unauthenticated callers of the extended card; remote clients of a localhost-bound gRPC server Authenticate, or bind to */+, or set RestrictToLoopbackClients = false
SendMessageConfiguration.PushNotificationConfig is written as taskPushNotificationConfig (the A2A v1.0 name); both names are read Servers built on Voltaic 2.1.1 or earlier ignore the new name Upgrade servers; they never delivered pushes before 2.1.2

Upgrading to v2.1.0

v2.1.0 changes defaults for security. Most applications need no code changes; check this list if browsers, remote machines, or older clients call your server.

Symptom after upgrading Cause Fix
A browser app on another origin gets 403 Origin validation server.OriginPolicy.AllowedOrigins.Add("https://your.app")
Another machine gets 403 from a server bound to localhost Loopback-only clients Bind to +/* or a specific address, or set RestrictToLoopbackClients = false
A client gets 400 "Missing MCP-Session-Id" on /mcp Sessions come only from initialize Send initialize first (Voltaic 2.1.0 clients do); for Voltaic 2.0.0 clients set RequireInitializedSessions = false
A client gets 404 for a session ID it chose or that expired Unknown IDs are no longer adopted Re-initialize and use the server-issued ID
A sessionless /rpc caller no longer receives MCP-Session-Id Sessionless /rpc requests run without a session Send initialize to /rpc when you need a session (for example for /events)
A text/plain POST to /mcp gets 415 Streamable HTTP requires JSON Send Content-Type: application/json
A TCP client is disconnected Strict LSP framing Send only Content-Length and Content-Type header lines
A browser expects Access-Control-Allow-Origin: * CORS echoes the allowed origin Allow the origin in OriginPolicy

McpHttpClient.ConnectAsync and ConnectStreamableAsync now perform the initialize handshake instead of a ping, so upgrade clients alongside servers that keep the default RequireInitializedSessions = true.

Upgrading from v1.x

v2.0.0 changes what an MCP server publishes by default and tightens tool invocation. Most applications need one or two edits:

v1.x v2.0.0
includeDefaultMethods constructor parameter, default true, controls protocol methods and demo tools together MCP servers: includeDiagnosticTools, default false, controls only echo and getTime; protocol methods are always registered. JsonRpcServer: includeDiagnosticMethods, default false
Demo tools ping, echo, getTime, getSessions/getClients published in tools/list Nothing is published unless you register it; getSessions/getClients removed
ping returns "pong" ping returns {} (with resultType under 2026-07-28)
Every tool is also a bare JSON-RPC method Tools are invoked only through tools/call
No way to remove a tool UnregisterTool(name) on every MCP server
additionalProperties ignored additionalProperties and patternProperties enforced
RegisterBuiltInMethods() (protected virtual) RegisterProtocolMethods() and RegisterDiagnosticTools(); JsonRpcServer.RegisterDiagnosticMethods()

Clients: use the new PingAsync() on McpHttpClient, McpClient, and McpWebsocketsClient instead of CallAsync<string>("ping"). It accepts both {} and a v1.x server's "pong". A v1.x McpHttpClient cannot connect to a v2.0.0 server, because its connection probe expects "pong"; upgrade clients and servers together.

MIGRATE_V1_TO_V2.md lists every change with before-and-after code.


When NOT to Use This

Voltaic might not be the right fit if you need:

  • Advanced gRPC Ecosystem Features: If you need code-first service hosting, interceptors, advanced load balancing, or broad gRPC framework integration, use a dedicated gRPC stack
  • REST Conventions: If you need resource-oriented APIs with standard HTTP verbs, use web APIs or a REST microservice
  • High-level Abstractions: Voltaic is a protocol library, not a framework; you'll write your own business logic

Resource Management

All server and client classes implement IDisposable using the full Dispose pattern (protected virtual void Dispose(bool disposing)) with double-disposal protection. Use using statements or call Dispose() to ensure proper resource cleanup:

// Recommended: using statement ensures cleanup
using McpHttpServer server = new McpHttpServer("localhost", 8080);
await server.StartAsync();

// Or manually dispose
McpHttpServer server2 = new McpHttpServer("localhost", 8081);
try
{
    await server2.StartAsync();
}
finally
{
    server2.Dispose();
}

Key points:

  • Dispose() is safe to call multiple times; subsequent calls are no-ops
  • For servers, Dispose() calls Stop() internally, disconnecting all clients and releasing the listening port
  • For clients, Dispose() calls Disconnect() internally, cancelling pending requests
  • Disconnect()/Stop() manage connection state only; Dispose() releases underlying resources (sockets, listeners, cancellation tokens)
  • All classes support the protected virtual void Dispose(bool disposing) pattern for subclass extensibility

Example Projects

Check out the src/Test.* projects for working examples:

  • Test.JsonRpcServer / Test.JsonRpcClient: Interactive JSON-RPC demos over TCP
  • Test.McpServer / Test.McpClient: MCP stdio examples
  • Test.McpHttpServer / Test.McpHttpClient: MCP HTTP with SSE examples
  • Test.McpWebsocketsServer / Test.McpWebsocketsClient: MCP WebSocket examples
  • Sample.McpServer: MCP tool, structured-output, resource, template, and prompt sample
  • Sample.A2AServer: A2A Agent Card, JSON-RPC, HTTP+JSON, gRPC, streaming, push config, and extended-card sample
  • Test.A2AServer: Manual A2A server harness with JSON-RPC, HTTP+JSON, gRPC, task inspection, and push config commands
  • Test.A2AClient: Manual A2A client for Agent Card discovery, JSON-RPC, HTTP+JSON, gRPC, streaming, and push config calls
  • Test.Shared: Shared Touchstone descriptors and the central 521-case API/protocol matrix
  • Test.Automated: Touchstone console runner
  • Test.Xunit / Test.Nunit: Touchstone adapter projects for dotnet test

Run examples:

# JSON-RPC Server (TCP)
dotnet run --project src/Test.JsonRpcServer/Test.JsonRpcServer.csproj -- 8080

# JSON-RPC Client (TCP)
dotnet run --project src/Test.JsonRpcClient/Test.JsonRpcClient.csproj -- 8080

# MCP Stdio Client (launches server subprocess)
dotnet run --project src/Test.McpClient/Test.McpClient.csproj

# MCP HTTP Server
dotnet run --project src/Test.McpHttpServer/Test.McpHttpServer.csproj -- 8080

# MCP HTTP Client
dotnet run --project src/Test.McpHttpClient/Test.McpHttpClient.csproj -- 8080

# MCP WebSocket Server
dotnet run --project src/Test.McpWebsocketsServer/Test.McpWebsocketsServer.csproj -- 8080

# MCP WebSocket Client
dotnet run --project src/Test.McpWebsocketsClient/Test.McpWebsocketsClient.csproj -- 8080

# A2A sample server
# Starts JSON-RPC and HTTP+JSON on the selected port, and gRPC on port + 1
dotnet run --project src/Sample.A2AServer/Sample.A2AServer.csproj -- 8080

# A2A manual server
# Starts JSON-RPC and HTTP+JSON on the selected port, and gRPC on port + 1
dotnet run --project src/Test.A2AServer/Test.A2AServer.csproj -- 8080

# A2A manual client
dotnet run --project src/Test.A2AClient/Test.A2AClient.csproj -- http://localhost:8080 "hello from A2A"

Connecting with MCP Inspector

The MCP Inspector is a visual tool for testing and debugging MCP servers. To connect MCP Inspector to a Voltaic MCP HTTP server:

  1. Start your MCP HTTP server:

    dotnet run --project src/Test.McpHttpServer/Test.McpHttpServer.csproj -- 8080
    
  2. Open MCP Inspector in your web browser

  3. Configure the connection:

    • Transport Type: Select Streamable HTTP
    • URL: Enter http://{hostname}:{port}/mcp
      • For example: http://localhost:8080/mcp
      • If you specified a custom mcpPath when creating the server, use that instead of /mcp
  4. Click Connect

  5. Verify the connection: The inspector should display the list of registered tools and allow you to call them interactively

Note: Use the Streamable HTTP transport in MCP Inspector for Voltaic's /mcp endpoint. For other Voltaic transports (TCP, WebSocket, stdio), use the corresponding client implementations or command-line tools.


Building

# Build everything
dotnet build src/Voltaic.sln

# Build the library
dotnet build src/Voltaic/Voltaic.csproj

# Run Touchstone console tests
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0

# The shared suite currently projects 521 cases through the console, xUnit, and NUnit runners

# Export Touchstone JSON results
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0 -- --results artifacts/test-results/voltaic-touchstone.json

# Filter by descriptor tag
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0 -- --tag mcp
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0 -- --tag a2a
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0 -- --tag compatibility

# Run adapter-backed tests
dotnet test src/Test.Xunit/Test.Xunit.csproj --framework net8.0
dotnet test src/Test.Nunit/Test.Nunit.csproj --framework net8.0

# Cross-target the console runner
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net10.0

API Surface

Public types are grouped by protocol namespace:

  • Voltaic.Core: JsonRpcServer, JsonRpcClient, JSON-RPC request/response/error models, TCP framing, connection models, shared authentication/error helpers, and the OriginPolicy and LoopbackAddresses access helpers.
  • Voltaic.Mcp: MCP stdio, HTTP, TCP, and WebSocket clients/servers plus MCP tools, resources, prompts, completions, capabilities, and utility models.
  • Voltaic.A2A: A2A Agent Cards, task/message/artifact models, A2AClient, A2AHttpJsonClient, A2ACardResolver, A2AHttpServer, task storage, event queue, updater, and protocol errors.

The library source mirrors those namespaces:

  • src/Voltaic/Core: shared JSON-RPC, framing, connection, authentication, and event types.
  • src/Voltaic/Mcp: MCP protocol models, endpoint infrastructure, clients, and servers.
  • src/Voltaic/A2A: A2A protocol models, clients, servers, task infrastructure, JSON helpers, and gRPC wire support.
  • src/Voltaic/A2A/Protos: the A2A protobuf contract used by the internal gRPC binding.

The core server pattern is the same across protocols: configure identity, register handlers or capabilities, subscribe to lifecycle events if needed, then start the server. Clients connect, call protocol methods, stream SSE events where supported, and dispose when finished.

For exact overloads and model types, use your IDE's IntelliSense, the generated XML documentation in src/Voltaic/Voltaic.xml, and the sample/test projects listed above. src/Test.Shared/API_COVERAGE.md tracks the public API areas covered by the Touchstone suite.


License

Voltaic is released under the MIT License. Use it freely in your projects, commercial or otherwise.


Support

Need help or found a bug?

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Voltaic:

Package Downloads
RestDb.McpServer

MCP server for RestDb. Exposes the RestDb HTTP API over MCP HTTP, TCP, WebSocket, and stdio transports.

Conductor.McpServer

MCP Server integration for Conductor - Exposes Conductor APIs as MCP tools

Mux.Core

The mux engine: agent loop with streaming events, built-in tools, MCP client, skills, subagents, session persistence, checkpoints, approval/sandbox governance, and usage telemetry — backend-agnostic across Ollama, OpenAI, Azure, Anthropic, Gemini, Vertex, Bedrock, and any OpenAI-compatible API. Build your own experiences on top of mux.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on Voltaic:

Repository Stars
litegraphdb/litegraph
LiteGraph is a multi-modal AI data platform - a property graph with relational, vector, and MCP support, to power knowledge and AI persistence and retrieval
Version Downloads Last Updated
2.1.2 0 9/26/2026
2.1.1 0 9/26/2026
2.1.0 0 9/26/2026
2.0.0 8 9/25/2026
1.1.0 71 9/24/2026
0.7.1 503 9/7/2026
0.7.0 104 9/7/2026
0.6.1 457 8/16/2026
0.6.0 238 8/6/2026
0.5.1 161 8/4/2026
0.4.0 358 6/14/2026
0.2.0 467 5/5/2026
0.1.11 634 3/23/2026
0.1.9 162 3/12/2026
0.1.8 141 3/6/2026
0.1.7 234 3/6/2026
0.1.6 268 1/23/2026
0.1.5 500 11/12/2025
0.1.4 323 11/12/2025
0.1.3 320 11/12/2025
Loading failed

v2.1.2 (spec conformance and A2A hardening): Invalid tool arguments return isError results (tool execution errors) instead of -32602; initialize with an unknown version negotiates MaximumHandshakeProtocolVersion. Relaxed: missing Accept counts as */*, non-browser POST /mcp may omit Content-Type, 2026-07-28 notifications need no routing headers, CORS preflights get requested headers back and WWW-Authenticate is exposed. A2A: push notifications are now delivered (StreamResponse, Authorization, X-A2A-Notification-Token, retries) with SSRF protection (PushNotificationUrlValidator); push config operations return TaskNotFound for missing tasks; JSON-RPC errors use HTTP 200 and A2AClient reads error bodies on any status; internal error details are no longer sent to clients. A2AGrpcServer: extended Agent Card GET now requires authentication (security fix), OriginPolicy and RestrictToLoopbackClients added, auth failure headers written. Servers can restart after Stop. All public APIs documented; CS1591 is a build error. Tested with MCP Inspector, the official Python MCP SDK, and Claude Code. Prior release, v2.1.1 (spec conformance): Client-sent JSON-RPC responses on /mcp and /rpc get 202 Accepted instead of a -32601 error, and batches of only responses and notifications get 202. Handshake-era requests without MCP-Protocol-Version and without a negotiated version assume 2025-03-26 (McpProtocol.HeaderlessProtocolVersion), as the spec requires. Stateless 2026-07-28 requests without a _meta protocol version are rejected with 400 -32020. Adds McpHttpServer.ProtectedResourceMetadata (RFC 9728, served unauthenticated at /.well-known/oauth-protected-resource) and McpProtectedResourceMetadata. README adds a Specification conformance section and corrects the AuthenticationHandler documentation. Prior release, v2.1.0 (security): HTTP and WebSocket servers validate the browser Origin (new OriginPolicy; foreign origins get 403, loopback origins and requests without Origin are allowed) and CORS echoes the allowed origin instead of *. Servers bound to localhost/127.x/::1 serve loopback clients only (RestrictToLoopbackClients), closing Host-header spoofing through http.sys on Windows. McpHttpServer creates Streamable HTTP sessions only on a successful initialize: sessionless /mcp requests get 400 (except initialize and ping), unknown or expired session IDs get 404 and are never adopted, sessionless /rpc requests run without a session, sessions are bound to the authenticated principal, POST requests count as session activity, and RequireInitializedSessions=false restores session issuance for older ping-first clients. POST /mcp requires application/json. McpWebsocketsServer gains AuthenticationHandler (caller flows into RpcCallContext and ClientConnection.Caller) and McpWebsocketsClient gains SetRequestHeader. AuthenticationResult gains Headers and BearerChallenge (WWW-Authenticate). MessageFraming accepts only Content-Length/Content-Type header lines, so TCP servers drop cross-protocol HTTP requests. McpHttpClient connects with the initialize handshake instead of ping. See README "Upgrading to v2.1.0". Prior release, v2.0.0 (breaking): MCP servers publish only the tools the application registers. Protocol methods are always registered; the includeDefaultMethods constructor parameter is replaced by includeDiagnosticTools (default false), which publishes only the echo and getTime diagnostic tools. The ping tool and the getSessions/getClients tools are removed (getSessions disclosed every session ID). ping now returns {} (with resultType under 2026-07-28) and cannot be replaced by a tool; its authentication bypass no longer reaches tools. Tools are invoked only through tools/call, never as bare JSON-RPC methods. Tool input schemas enforce additionalProperties and patternProperties. RegisterBuiltInMethods is replaced by RegisterProtocolMethods/RegisterDiagnosticTools (and RegisterDiagnosticMethods on JsonRpcServer, whose includeDiagnosticMethods now defaults to false). Adds UnregisterTool on every MCP server and PingAsync on McpHttpClient, McpClient, and McpWebsocketsClient. The McpHttpServer JSON-RPC endpoint now serves stateless 2026-07-28 requests. A v1.x McpHttpClient cannot connect to a v2.0.0 server; upgrade clients with servers. See MIGRATE_V1_TO_V2.md. Prior release, v1.1.0: Fixes MCP clients on the stateless 2026-07-28 revision (for example Claude Code 2.1.x) seeing zero tools. Results served under 2026-07-28 now carry the required resultType, and cacheable results (list results, resources/read, server/discover) carry ttlMs and cacheScope (defaults 0 and private unless ListCacheTtlMs/ListCacheScope are set). McpHttpServer now runs authenticated requests through the same protocol pipeline as unauthenticated ones, which also stops stateless traffic from creating a session per request. initialize never agrees to 2026-07-28 on any transport; it negotiates at most MaximumHandshakeProtocolVersion (new on McpHttpServer, McpServer, McpTcpServer, McpWebsocketsServer; default 2025-11-25). server/discover no longer advertises listChanged or subscribe. Adds McpProtocol.NegotiateHandshakeVersion, NewestHandshakeProtocolVersion, IsHandshakeVersion, McpResult.ResultType, and McpEmptyResult. v1.0.0 was published in error and is superseded by this release. Prior release, v0.7.1: Extends authenticated-caller propagation to the A2A servers. A2ARequestContext now carries the caller's Principal and Claims, copied from the AuthenticationHandler result before the agent handler runs, over both the A2A HTTP (JSON-RPC and HTTP+JSON) and gRPC transports. Additive and backward compatible: Principal/Claims are null when no AuthenticationHandler is configured or for public Agent Card requests, and IA2AAgentHandler implementations that ignore them are unaffected. Prior release: v0.7.0: Adds a supported way for MCP/JSON-RPC method and tool handlers to see the authenticated caller for the current request. Introduces Voltaic.Core.RpcCallContext, an ambient AsyncLocal (Principal + Claims) that McpHttpServer populates immediately after a successful AuthenticationHandler result and restores when the request ends, flowing untouched into tool.Handler. Adds optional explicit-context RegisterMethod/RegisterTool overloads that pass RpcCallContext to the handler. Fully additive and backward compatible: RpcCallContext.Current is null when no AuthenticationHandler is configured (or on transports that do not authenticate), and all existing registration overloads compile and behave unchanged. Prior release: v0.6.1: Dependency refresh with no public API or behavioral changes. Updates the runtime dependency Watson to 7.1.0 and the build-time dependency Grpc.Tools to 2.83.0 (Google.Protobuf stays at the latest stable 3.35.1). The public surface of Voltaic.Core, Voltaic.Mcp, and Voltaic.A2A is unchanged. Prior release: v0.6.0: Begins multi-version MCP support across all five published revisions (2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and the stateless 2026-07-28). Adds the version registry and era model (McpProtocol, McpProtocolVersionInfo, McpProtocolEra), a deterministic version resolver (McpVersionResolver), stateless-era error factories (HeaderMismatch -32020, MissingRequiredClientCapability -32021, UnsupportedProtocolVersion -32022), and additive protocol models: server/discover (McpDiscoverResult), Multi Round-Trip Requests (McpInputRequiredResult), cacheable list results (ttlMs/cacheScope), the 2026-07-28 tasks extension, and the 2025-11-25 in-core tasks. Fully additive and backward compatible: the server still defaults to the 2025-11-25 handshake and existing clients are unaffected. Prior release: v0.5.1: Documentation fixes; no code changes from v0.5.0. Prior release: v0.5.0: Adds McpHttpClient.SetRequestHeader(name, value) so HTTP/Streamable-HTTP MCP clients can attach authentication headers to every request. Prior release: v0.4.0: Splits public APIs into Voltaic.Core, Voltaic.Mcp, and Voltaic.A2A; adds dependency-light A2A v1.0 support.