Skip to main content

Building MCP Servers - SDKs with Go and C#

This guide covers building MCP servers with the official SDKs, focusing on Go and C# (.NET). It assumes the stateless model of 2026-07-28. The protocol mechanics are covered in MCP Protocol Basics and Transports - stdio and Streamable HTTP, and tool design in Tool Design Patterns and Anti-patterns. This document focuses on building a working server with an SDK and on designing state, long-running work, and retries.

1. What This Document Covers​

  • Official SDKs and the tier system
  • The minimal server
  • Implementing with the Go SDK
  • Implementing with the C# SDK
  • Implementing with the TypeScript SDK
  • Designing stateful operations
  • Long-running operations
  • Input validation and error handling
  • Idempotency and timeouts
  • SDK v2 and version compatibility

Official SDKs and the tier system​

The official SDKs are classified into three tiers by feature completeness, protocol support, and maintenance commitment.

TierSDKsMain requirements
Tier 1TypeScript, Python, C#, Go, Rust, Ruby100% pass rate on conformance tests. Support new protocol features before a new spec version is released. Triage issues within 2 business days and resolve critical bugs within 7 days. A stable release and documentation with examples for all features are required
Tier 2Java80% pass rate on conformance tests. Support new features within 6 months. Triage issues within a month
Tier 3Swift, PHP, KotlinExperimental or partially implemented. No minimum requirements

A Tier 1 SDK implements every non-experimental feature, including optional capabilities such as sampling and elicitation. Experimental features and extensions, such as Tasks and MCP Apps, are not required for any tier. If you want to use Tasks, check separately whether your SDK supports them. Tiers are reviewed against conformance test results, and an SDK is demoted if tests on its latest stable release keep failing for 4 weeks. When choosing an SDK, check the tier and whether a stable release supports the target protocol version (2026-07-28), in addition to how well the language fits.

The minimal server​

In every SDK, the minimal server has the same shape: create a server instance, register one tool, and connect a transport.

Use stdio to embed a server in a local host application, and Streamable HTTP to share it with a team or organization. Supporting both is the standard approach: share the tool handlers and switch the transport with a startup argument.

With stdio, standard output is a channel reserved for JSON-RPC messages. Writing logs to standard output corrupts the messages and breaks the server. Write logs to standard error or to a file. In Go, use log.Println (which defaults to standard error) instead of fmt.Println. In C#, use a logging library that writes to standard error or a file instead of Console.WriteLine. In TypeScript, use console.error instead of console.log. An HTTP server may write to standard output.

A Streamable HTTP server exposes one endpoint that accepts POST. The response to each request is either a single JSON object or an SSE stream scoped to that request, and the server chooses per request. GET and DELETE are not used, and there is no Mcp-Session-Id.

Implementing with the Go SDK​

The Go SDK is github.com/modelcontextprotocol/go-sdk, and a server uses its mcp package (the official quickstart assumes Go 1.24 or later).

go get github.com/modelcontextprotocol/go-sdk/mcp

Create a server with mcp.NewServer and register tools with the generic mcp.AddTool. A handler is a function that takes the request and a typed input and returns a result, a typed output, and an error. The JSON Schema is generated from the input struct.

  • If you leave Tool.InputSchema unset, the SDK infers the schema from the input type (a struct or a map). OutputSchema is also inferred when the output type is not any
  • Arguments are validated against the input schema and output against the output schema. By the time a handler runs, types and required fields have been validated
  • In a struct, the json tag gives the argument name and the jsonschema tag gives its description. The model reads this description, so write it with the same care as the tool description
  • If a handler returns an ordinary error, the SDK puts it into the result and sets isError: true. Return validation errors you want the LLM to see this way
  • In a stateless deployment that creates a Server per request, create one mcp.NewSchemaCache() at startup and pass it through ServerOptions to avoid regenerating schemas

For stdio, use server.Run(ctx, &mcp.StdioTransport{}). For HTTP, mcp.NewStreamableHTTPHandler returns an http.Handler, so you can register it with http.Handle and wrap it in authorization middleware, as in Web API Development (net/http). The handler takes a function that returns the *mcp.Server to use for each request.

To accept 2026-07-28 protocol requests, set Stateless to true in StreamableHTTPOptions. According to the SDK documentation, 2026-07-28 requests are rejected by a handler that is not Stateless. Stateless: true skips session ID validation and uses a temporary session per request. Resumability through EventStore is not used by 2026-07-28 sessions. The full code is in "4. Code Examples".

Implementing with the C# SDK​

The C# SDK is distributed through NuGet and split into three packages by purpose.

PackagePurpose
ModelContextProtocol.CoreClient or low-level server APIs with minimal dependencies
ModelContextProtocolClient, or stdio server. Includes hosting and attribute-based tool discovery
ModelContextProtocol.AspNetCoreHTTP server in ASP.NET Core. References ModelContextProtocol

The official quickstart assumes .NET 8 or later and adds the package with dotnet add package ModelContextProtocol --prerelease. Because you may be using a prerelease version, decide how you pin and update versions.

The server integrates with the .NET generic host and DI.

  • Register the server with AddMcpServer() and choose the transport with WithStdioServerTransport() or WithHttpTransport()
  • Put [McpServerToolType] on the class that holds tools and [McpServerTool] on each tool method, then discover them from the assembly with WithToolsFromAssembly(). Attach argument descriptions with the [Description] attribute. Prompts and resources have [McpServerPromptType] / [McpServerPrompt] and [McpServerResourceType] / [McpServerResource]
  • A tool method can receive services registered in DI as parameters (such as the HttpClient in the quickstart). Pass database clients and a service that identifies the user through ordinary .NET DI as well
  • In ASP.NET Core, expose the MCP endpoint with app.MapMcp(). As in the example in MCP Authorization, you can apply authorization with RequireAuthorization()

Run an HTTP server in stateless mode. The SDK documentation describes stateless mode as the recommended default for servers that do not use server-to-client requests such as sampling or elicitation. To make it explicit, specify SessionMode = HttpServerSessionMode.Stateless in the WithHttpTransport options. When building a stdio server, use Host.CreateEmptyApplicationBuilder(settings: null) for the host builder. With CreateDefaultBuilder, the host writes extra messages to the console and corrupts stdio messages.

Implementing with the TypeScript SDK​

In v2, the TypeScript SDK is split into @modelcontextprotocol/server for servers and @modelcontextprotocol/client for clients (use zod for input schemas). In the official quickstart, you register tools on an McpServer with registerTool and use serveStdio for stdio.

  • serveStdio takes a factory function that returns a server. It negotiates the protocol version with each client, and it also creates one instance for the server/discover probe, which is discarded if the client falls back to initialize. The factory therefore returns a new McpServer every time it is called
  • Wiring the server by hand with server.connect(new StdioServerTransport()) produces a server that answers only 2025-era clients. Use serveStdio to also accept 2026-07-28 clients
  • For HTTP, createMcpHandler provides a stateless handler that runs on web-standard Request / Response, which makes it easy to run in environments such as Workers. See the SDK documentation for the detailed API

To verify behavior, run the Inspector CLI with --protocol-era modern. The Inspector defaults to legacy (the 2025-era handshake), so run the same command again with --protocol-era legacy and confirm that clients of both eras see the same tools.

Designing stateful operations​

MCP 2026-07-28 has no protocol-level session. A server cannot treat the identity of a connection or process as a substitute for the identity of a conversation or a user. A server must be able to complete processing from the request's _meta and arguments alone, and state that spans multiple requests takes the form of an explicit identifier (handle) that the client passes every time.

A handle is not a protocol concept. On the wire it is an ordinary string in a tool result and an ordinary argument to the next tool call. The (non-normative) guidance in the official specification asks you to consider the following when designing handles.

  • Authorization: For a server that implements authentication, a handle is a name, not a capability. Do not treat possessing a handle as authentication, and validate the caller's authorization against the handle on every call. For an unauthenticated server, the handle is necessarily a bearer token, so generate it with sufficient entropy (such as a UUIDv4) and give it a bounded lifetime
  • Opacity: A handle that encodes internal structure invites parsing and guessing. Use an identifier with no meaning
  • Lifetime: A handle outlives any connection. State the retention policy (for example, "baskets expire after 24 hours of inactivity") in the description of the creation tool so the model can see it before creating state
  • Expiry errors: A call with an expired or unknown handle returns a tool execution error that says so, so the model can recover by creating a new one

For the handle hijacking attack, and the countermeasure of using <user_id>:<handle> as the storage key with user_id taken from the verified token, see MCP Security Best Practices. Assuming the server runs as multiple instances, keep the handle's backing data in an external store, not in process memory. In a stateless deployment, which instance receives a given request is not fixed.

Long-running operations​

For work that takes more than a few seconds, there are three tools.

Progress notifications (progress): A client puts a progressToken (a string or integer, unique among active requests) in the _meta of a request for which it wants progress. The server may optionally send notifications/progress with progress (a value that increases with every notification), and an optional total and message. On Streamable HTTP, these notifications arrive on that request's SSE response stream, before the final response. A server must not send progress for a request without a token, and stops after completion. Decide the frequency with rate limiting in mind.

Cancellation: On Streamable HTTP, the client closing the response stream is the cancellation of that request (notifications/cancelled is not used). The server should stop work as soon as practical and must not send a response for that request. On stdio, the client sends notifications/cancelled. In a handler, pass the cancellation mechanism the SDK provides (such as Go's context.Context) to downstream API calls and database queries, so that cancellation propagates into the middle of the work. Note that cancellation involves a race: processing may already have finished before the notification arrives, and the side effects of finished work are not undone.

The Tasks extension: For work that takes minutes or hours, such as CI pipelines, batch processing, and human approvals, return a durable handle instead of holding the connection open. Tasks are an extension (io.modelcontextprotocol/tasks), not part of the core protocol, and are usable only when both the client and the server declare support.

  1. The client puts io.modelcontextprotocol/tasks in the extensions of the capabilities in each request's _meta. The server lists the same extension in the capabilities of server/discover
  2. After confirming that the client declared support, the server returns a CreateTaskResult with resultType: "task" (taskId, status, ttlMs, pollIntervalMs) for a request that looks long-running. It must not return a task to a client that did not declare support. The task must be durably created before the response is sent
  3. The client polls with tasks/get. The statuses are working, input_required, completed, failed, and cancelled, and the last three are terminal
  4. If input becomes necessary midway, the status becomes input_required and inputRequests is returned. The client returns inputResponses with tasks/update
  5. Cancellation through tasks/cancel is cooperative, and the server does not necessarily stop

The server decides per request whether to use a task. The client must handle either a normal result or a task result. A task ID is a durable handle that lets a client resume across restarts, so, like a state handle, it requires validating the caller.

Input validation and error handling​

Tool errors come in two kinds.

KindExamplesHow to return
Protocol errorUnknown tool, malformed request that does not satisfy the schema, server failureA JSON-RPC error (-32602 for an unknown tool)
Tool execution errorExternal API failure, input validation error, business rule violationisError: true inside a successful response

The LLM can self-correct from the latter. Failures the model can fix by adjusting arguments and retrying, such as a wrong date format, an out-of-range value, or a nonexistent ID, are returned as tool execution errors that say what is wrong and how to fix it (for design, see Tool Design Patterns and Anti-patterns). The SDK performs schema validation, but validation the schema cannot express (existence checks, permissions, business rules) is the handler's job.

The specification requires servers to do the following.

  • Validate all tool inputs
  • Implement access controls
  • Rate limit tool invocations
  • Sanitize tool outputs

At the protocol layer, if a request's _meta lacks a required field (protocolVersion, clientCapabilities), reject it with -32602. If the request needs a capability the client did not declare (such as elicitation), return MissingRequiredClientCapability (-32021). On HTTP both are 400 Bad Request. The SDK handles these for you.

Idempotency and timeouts​

In Streamable HTTP, each request is an independent POST, and when the response stream is closed, the server treats that request as cancelled. Clients are advised to set timeouts and cancel when no response arrives, and there is no resumption of a broken stream (Last-Event-ID). A client therefore sends a request whose result it did not receive again from the start, with a new request ID. From the server's point of view this is a different request (the specification requires that an ID not be reused).

This leads to the following situation.

  • The first attempt may already have caused side effects after the stream broke, before the cancellation arrived
  • The client's retry cannot be distinguished from a resend of the first request

Therefore design operations with side effects (placing orders, transfers, creating tickets, sending email) to be idempotent.

  • Give creating tools a client-generated idempotency key (idempotency_key) as an argument, and return the first result for a second call with the same key
  • Guarantee "treat the same content received twice as one" on the store side, with unique constraints or conditional writes
  • Turn long operations into Tasks (or your own job handle) so that creating and fetching the result are separate
  • When calling an external API, carry your idempotency key through to that API's idempotency key

The model may generate the idempotency key as an argument, but it is useless if the model makes a different key on every retry. Write "use the same key when retrying the same operation" in the tool description, or design it so that the host application can attach the key deterministically.

SDK v2 and version compatibility​

The SDKs have major versions that support 2026-07-28. The Python quickstart requires "Python MCP SDK 2.0.0 or higher", TypeScript uses @modelcontextprotocol/server / @modelcontextprotocol/client, and the C# quickstart adds --prerelease. Use these versions to build a 2026-07-28 server.

Whether to accept clients of an older revision (2025-11-25 and earlier, which use the initialize handshake) is the server's policy. The specification calls an implementation that supports both "dual-era": a request carrying _meta is served statelessly under the new spec, and a request that opens with initialize is served under the legacy semantics.

ItemWhat a 2026-07-28 server does toward a legacy client
Version mismatchReturns UnsupportedProtocolVersion (-32022) with the supported versions in data.supported. The client picks a common version and retries
Response to initialize (a server that supports only 2026-07-28 and later)Names the supported versions in the error. Legacy clients have no fall-forward mechanism, so this message may be their only diagnostic
GET or DELETEReturns 405 Method Not Allowed
Mcp-Session-Id, Last-Event-IDIgnores them. Does not issue or echo session IDs

The SDK configuration determines how far legacy clients are accepted. For example, TypeScript's serveStdio answers both eras. For how settings such as Go's Stateless and C#'s SessionMode affect legacy clients, check the SDK documentation. When accepting legacy clients, separate the processing by era so that an implementation that depends on legacy sessions (server-side session state) does not leak into 2026-07-28 requests.

2. SDKs by Language​

LanguageRepositoryNotes
Gomodelcontextprotocol/go-sdkTier 1. Combine with net/http. Use Stateless: true for HTTP
C# (.NET)modelcontextprotocol/csharp-sdkTier 1. Packages: ModelContextProtocol (stdio, hosting), ModelContextProtocol.AspNetCore (HTTP), ModelContextProtocol.Core (low-level)
TypeScriptmodelcontextprotocol/typescript-sdkTier 1. @modelcontextprotocol/server. Web-standards based, runs on Workers and similar
Pythonmodelcontextprotocol/python-sdkTier 1. SDK 2.0.0 or later for 2026-07-28

3. Common Pitfalls​

A stdio server writes logs to standard output​

fmt.Println, Console.WriteLine, and console.log write to standard output and corrupt JSON-RPC messages. The symptom is only that the host reports "the server does not start" or a JSON parse error, which makes the cause hard to find. Write logs to standard error or a file, and in C# also use CreateEmptyApplicationBuilder.

Forgetting Stateless on a Go HTTP handler​

Requests from 2026-07-28 clients are rejected by a handler that is not Stateless. Specify mcp.StreamableHTTPOptions{Stateless: true}.

Connecting by hand instead of serveStdio in TypeScript​

server.connect(new StdioServerTransport()) compiles and runs, but it answers only 2025-era clients. To also accept 2026-07-28 clients, pass a factory to serveStdio. It is also a mistake for the factory to keep returning the same instance; return a new instance every time it is called.

Keeping session state in server memory​

If you keep a basket or authenticated state in process memory, it breaks as soon as a request reaches a different instance behind a load balancer. Keep state in an external store and take the handle as an argument.

Treating possession of a handle as authorization​

A handle is a name, not a capability. Check the caller against the handle's owner on every call.

Not making tools with side effects idempotent​

A stream disconnect followed by a client retry creates the same order twice. Prevent this with an idempotency key or a unique constraint on the store side.

Returning validation errors as exceptions or protocol errors​

An error the model cannot fix makes the retry wasted. Return input mistakes as tool execution errors with isError: true, including how to fix them.

Not propagating cancellation downstream​

Even after the client disconnects, database queries and external API calls keep running and consuming resources. Pass the cancellation mechanism the SDK provides through to downstream calls.

Holding the connection open for long work​

Proxies and client timeouts cut it off. For work longer than a few seconds, report status with progress notifications or turn it into Tasks (or your own job handle). Note that Tasks are an extension and usable only when the client declares support.

Assuming experimental features​

Extensions such as Tasks are not required in any tier of SDK, and host support varies. Provide a path that returns a normal result for clients that do not support them.

4. Code Examples​

Go: expose over both stdio and Streamable HTTP​

This example uses structs for input and includes a filtering argument, how to return validation errors, an idempotency key, and verification of the handle's owner. Identifying the user (userFromContext) assumes that authorization middleware sets it in the context from the verified token (see MCP Authorization).

package main

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"sync"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

type ctxKey struct{}

// Set by authorization middleware from the verified token.
func userFromContext(ctx context.Context) (string, bool) {
u, ok := ctx.Value(ctxKey{}).(string)
return u, ok && u != ""
}

type SearchOrdersInput struct {
Status string `json:"status" jsonschema:"Order status: one of pending, shipped, cancelled"`
Limit int `json:"limit,omitempty" jsonschema:"Maximum number of results. Default 20, maximum 100"`
}

type OrderSummary struct {
ID string `json:"id"`
Status string `json:"status"`
}

type SearchOrdersOutput struct {
Orders []OrderSummary `json:"orders"`
}

func searchOrders(ctx context.Context, req *mcp.CallToolRequest, in SearchOrdersInput) (
*mcp.CallToolResult, SearchOrdersOutput, error,
) {
switch in.Status {
case "pending", "shipped", "cancelled":
default:
// The SDK turns an ordinary error into a result with isError: true. Write it so the LLM can fix it.
return nil, SearchOrdersOutput{}, fmt.Errorf(
"status %q is not allowed. Specify one of pending, shipped, cancelled", in.Status)
}
limit := in.Limit
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
// ... search the store (pass ctx so cancellation propagates) ...
return nil, SearchOrdersOutput{Orders: []OrderSummary{}}, nil
}

type PlaceOrderInput struct {
BasketID string `json:"basket_id" jsonschema:"Basket ID returned by create_basket. Expires after 24 hours of inactivity"`
IdempotencyKey string `json:"idempotency_key" jsonschema:"Use the same key when retrying the same operation"`
}

type PlaceOrderOutput struct {
OrderID string `json:"order_id"`
}

var (
mu sync.Mutex
results = map[string]PlaceOrderOutput{} // In production, use an external store and a unique constraint
)

func placeOrder(ctx context.Context, req *mcp.CallToolRequest, in PlaceOrderInput) (
*mcp.CallToolResult, PlaceOrderOutput, error,
) {
user, ok := userFromContext(ctx)
if !ok {
return nil, PlaceOrderOutput{}, errors.New("authentication is required")
}

// Guarantee idempotency with a key that includes the owner. It does not collide with other users' keys.
key := user + ":" + in.IdempotencyKey
mu.Lock()
defer mu.Unlock()
if r, done := results[key]; done {
return nil, r, nil // A retry gets the first result
}

// Verify the handle's owner (for example, make the store key user + ":" + basket_id).
// If it is not found or has expired, return an error the model can recover from.
// ... look up <user>:<basket_id> in the store ...

out := PlaceOrderOutput{OrderID: "ord_123"}
results[key] = out
return nil, out, nil
}

func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "orders", Version: "1.0.0"}, nil)

mcp.AddTool(server, &mcp.Tool{
Name: "search_orders",
Description: "Search orders by status. Use it to list orders or check their state. " +
"Use get_order for the details of a single order.",
}, searchOrders)
mcp.AddTool(server, &mcp.Tool{
Name: "place_order",
Description: "Place an order with the contents of a basket. This incurs charges. " +
"Retrying with the same idempotency_key does not place the order twice.",
}, placeOrder)

// With the stdio argument, it runs as a child process launched by a local host.
// Standard output is reserved for JSON-RPC, so logs go to standard error (the default for log).
if len(os.Args) > 1 && os.Args[1] == "stdio" {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
return
}

// Streamable HTTP. Set Stateless: true to accept 2026-07-28 clients.
handler := mcp.NewStreamableHTTPHandler(
func(r *http.Request) *mcp.Server { return server },
&mcp.StreamableHTTPOptions{Stateless: true},
)
http.Handle("/mcp", handler) // In production, wrap it in authorization middleware and Origin validation
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

C#: a stateless HTTP server on ASP.NET Core​

// Program.cs
using System.ComponentModel;
using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
.WithHttpTransport(options =>
{
// No server-to-client requests are used, so make it stateless.
options.SessionMode = HttpServerSessionMode.Stateless;
})
.WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp("/mcp"); // In production, apply authorization with RequireAuthorization()
app.Run();

[McpServerToolType]
public static class OrderTools
{
[McpServerTool, Description(
"Search orders by status. Use it to list orders or check their state. Use get_order for the details of one order.")]
public static string SearchOrders(
// Services registered in DI can be received as tool method parameters.
// OrderStore store,
[Description("Order status: one of pending, shipped, cancelled")] string status,
[Description("Maximum number of results. Default 20, maximum 100")] int limit = 20)
{
var allowed = new[] { "pending", "shipped", "cancelled" };
if (!allowed.Contains(status))
{
return $"status '{status}' is not allowed. Specify one of pending, shipped, cancelled.";
}
limit = Math.Clamp(limit, 1, 100);
// ... search the store and return the result ...
return "[]";
}
}

To make it a stdio server, use AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly() with Host.CreateEmptyApplicationBuilder(settings: null), and write logs to standard error.

TypeScript: a stdio server​

import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

// serveStdio calls this whenever it needs an instance. Return a new instance every time.
function createServer() {
const server = new McpServer({ name: "orders", version: "1.0.0" });

server.registerTool(
"search_orders",
{
description:
"Search orders by status. Use it to list orders or check their state. Use get_order for the details of one order.",
inputSchema: z.object({
status: z
.enum(["pending", "shipped", "cancelled"])
.describe("Order status"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Maximum number of results"),
}),
},
async ({ status, limit }) => {
// ... search the store ...
return { content: [{ type: "text", text: `${status}: 0 results (limit ${limit})` }] };
},
);

return server;
}

serveStdio(createServer, {
onerror: (error) => console.error("orders server error:", error),
});
console.error("orders server running on stdio"); // Do not use console.log

Verifying behavior (Inspector)​

npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era legacy --method tools/list

5. Summary​

  • The official SDKs are split into three tiers, and Tier 1 is TypeScript, Python, C#, Go, Rust, and Ruby. Extensions such as Tasks are not required in any tier
  • The minimal server creates an instance, registers a tool, and connects stdio and Streamable HTTP. With stdio, never write to standard output
  • Go generates the input schema from structs with mcp.AddTool. For HTTP, use NewStreamableHTTPHandler with Stateless: true
  • C# is implemented with ModelContextProtocol and ModelContextProtocol.AspNetCore, attributes, and DI, and runs HTTP statelessly
  • TypeScript passes a factory to serveStdio and accepts clients of both 2026-07-28 and the legacy era
  • Because there is no protocol-level session, pass state as handles in arguments, validate the caller's authorization every time, and keep the backing data in an external store
  • For long-running work, combine progress notifications, cancellation (closing the stream on HTTP), and the Tasks extension
  • Return input mistakes as isError tool execution errors, and make operations with side effects idempotent on the assumption of retries
  • Whether to accept legacy clients is the server's policy, and compatibility is handled with UnsupportedProtocolVersion and the list of supported versions

References​