The Model Context Protocol has a new specification, MCP 2026-07-28. It is the biggest change to the protocol since Streamable HTTP: the session handshake is gone, every request carries what the server needs to answer it, and long or interactive work gets first-class support. sgcWebSockets 2026.10.0 implements it in both the MCP server and the MCP client for Delphi and C++Builder.
The important part for anyone already running a Delphi MCP server: nothing breaks. The server is dual-era. Clients that speak 2025-11-25, such as VS Code or Claude, keep calling initialize and get a session exactly as before, while 2026-07-28 clients use the new stateless model on the same endpoint. You do not need a switch or a second server.
What changed in MCP 2026-07-28
In plain terms, these are the changes that matter when you build a server or a client:
- Stateless requests. There is no
initializehandshake and noMcp-Session-Id. Each request carries its protocol version, client info and client capabilities inside_meta, so any server instance behind a load balancer can answer it. - server/discover. A client that wants to know what a server offers calls
server/discoverand gets the supported versions, capabilities and instructions back. The result can be cached, and results carry cache hints (ttlMs,cacheScope). - subscriptions/listen. One streaming request replaces the old GET stream. The client says which notifications it wants (tools, prompts and resources list changes, resource updates) and the server pushes them on that stream.
- Multi round-trip requests (MRTR). A tool, prompt or resource handler no longer calls the client back to ask for input. It returns
input_requiredwith the questions, the client answers and retries the call with the answers and a signedrequestState. - Tasks extension. A long tool call can answer at once with a task handle. The client polls it with
tasks/get, answers questions withtasks/updateand stops it withtasks/cancel. - Routing headers. HTTP requests carry
MCP-Protocol-Version,Mcp-MethodandMcp-Nameheaders (plusMcp-Param-*for tool arguments marked withx-mcp-header), so gateways can route and filter without parsing the body. The server checks them against the body. - Client authorization. The OAuth flow for clients is tightened: protected resource metadata discovery, Client ID Metadata Documents, PKCE with the resource indicator and RFC 9207
issvalidation.
A dual-era MCP server in Delphi
The TsgcWSAPIServer_MCP component detects the era of every request. A request whose _meta says 2026-07-28 goes down the stateless path, while initialize and the older versions go down the session path you already know. Your tool, prompt and resource handlers are shared by both. The new options only fill in what 2026-07-28 clients read from server/discover and from the cache hints.
procedure TMainForm.FormCreate(Sender: TObject);
begin
MCPServer.MCPOptions.ServerInfo.Name := 'tickets-mcp';
// returned by server/discover to 2026-07-28 clients
MCPServer.MCPOptions.Instructions := 'Use search_tickets before opening a ticket.';
// cache hints returned with 2026-07-28 results (ttlMs, cacheScope)
MCPServer.MCPOptions.Cache.TTLMs := 60000;
MCPServer.MCPOptions.Cache.Scope := aimcpcsPublic;
MCPServer.Active := True;
end;
When ServerInfo.Name is set, 2026-07-28 results also carry the server info in _meta. Protocol errors use the new codes: a header that does not match the body answers -32020, a missing client capability -32021 and an unsupported version -32022, with the list of supported versions in the error data.
Subscriptions with subscriptions/listen
Subscriptions are on by default. A 2026-07-28 client opens one subscriptions/listen stream, and the notifications you already send with SendNotificationToolsListChanged or SendNotificationResourcesUpdated reach those listeners as well as the 2025-11-25 sessions. The server sends a keep-alive comment on idle streams, and when the server is deactivated every open subscription is closed gracefully with its final result.
procedure TMainForm.FormCreate(Sender: TObject);
begin
MCPServer.MCPOptions.Subscriptions.Enabled := True;
MCPServer.MCPOptions.Subscriptions.KeepAliveInterval := 15000; // 0 disables it
end;
procedure TMainForm.ToolsChanged;
begin
// delivered to legacy sessions and to every subscriptions/listen stream
MCPServer.SendNotificationToolsListChanged;
end;
Multi round-trip requests: asking the user for a name
With MRTR a handler asks for more input by filling aResponse.InputRequired and returning. The client collects the answers and calls the tool again. On the second round the answers are read with aRequest.InputResponse. The example below is the ask_name tool from the server demo.
procedure TMainForm.MCPServerMCPRequestTool(Sender: TObject;
const aSession: TsgcAI_MCP_Session;
const aRequest: TsgcAI_MCP_Request_ToolsCall;
const aResponse: TsgcAI_MCP_Response_ToolsCall);
begin
if aRequest.Params.Name = 'ask_name' then
begin
if not aRequest.HasInputResponse('name') then
begin
// first round: ask the client for the user's name
aResponse.InputRequired.AddElicitation('name', 'What is your name?',
'{"type":"object","properties":{"name":{"type":"string"}},' +
'"required":["name"]}');
Exit;
end;
// second round: the answer arrives as raw JSON
aResponse.Result.Content.AddText('Hello, ' + aRequest.InputResponse('name'));
end;
end;
The requestState travels signed with HMAC-SHA256 using MCPOptions.MRTR.Secret and expires after MCPOptions.MRTR.StateTTL seconds, so a client cannot tamper with it. URL elicitation (AddElicitationURL), sampling (AddSampling) and roots (AddRoots) follow the same pattern. Before answering input_required, the server checks that the client declared the matching capability.
The Tasks extension: a long_job tool
Enable tasks in MCPOptions.Tasks and call CreateTask from the tool handler. When the client declared the io.modelcontextprotocol/tasks extension on that request, the call answers at once with a task id and your code finishes the work on its own thread. CreateTask returns nil when tasks are disabled, the request is 2025-11-25 or the client did not declare the extension, so keep a synchronous path for those clients.
procedure TMainForm.FormCreate(Sender: TObject);
begin
MCPServer.MCPOptions.Tasks.Enabled := True;
MCPServer.MCPOptions.Tasks.TTL := 3600000;
MCPServer.MCPOptions.Tasks.PollInterval := 1000;
end;
procedure TMainForm.MCPServerMCPRequestTool(Sender: TObject;
const aSession: TsgcAI_MCP_Session;
const aRequest: TsgcAI_MCP_Request_ToolsCall;
const aResponse: TsgcAI_MCP_Response_ToolsCall);
var
oTask: TsgcAI_MCP_Task;
begin
if aRequest.Params.Name = 'long_job' then
begin
oTask := MCPServer.CreateTask(aSession, aResponse);
if Assigned(oTask) then
TLongJobThread.Create(oTask) // runs the job, see below
else
aResponse.Result.Content.AddText(RunLongJob);
end;
end;
procedure TLongJobThread.Execute;
var
i: Integer;
oResponse: TsgcAI_MCP_Response_ToolsCall;
begin
for i := 1 to 3 do
begin
if FTask.IsCancelled then
Break;
DoStep(i);
FTask.SetStatusMessage(Format('long_job step %d of 3', [i]));
end;
if FTask.IsCancelled then
FTask.Fail(CS_AI_MCP_INTERNAL_ERROR, 'Cancelled by the client')
else
begin
oResponse := TsgcAI_MCP_Response_ToolsCall.Create;
try
oResponse.Result.Content.AddText('long_job finished after 3 steps');
FTask.Complete(oResponse);
finally
oResponse.Free;
end;
end;
end;
The task object is thread safe. OnMCPTaskCancel fires when the client calls tasks/cancel, and OnMCPTaskUpdate when it answers a task waiting for input. Tasks are bound to the principal that created them and expire after TTL milliseconds.
The MCP client: ProtocolEra and Discover
The TsgcWSAPIClient_MCP component gets a new MCPOptions.ProtocolEra property. Leave it on aimcpeAuto and the client probes the server with server/discover, uses 2026-07-28 when the server supports it and falls back to the initialize handshake when it does not. The negotiated era is cached per endpoint. Set it to aimcpeModern or aimcpeLegacy to force one.
procedure TMainForm.Connect;
begin
MCPClient.MCPOptions.ProtocolEra := aimcpeAuto;
if MCPClient.Initialize then
begin
MemoLog.Lines.Add('protocol: ' + MCPClient.NegotiatedProtocolVersion);
if MCPClient.NegotiatedEra = aimcpeModern then
MCPClient.Discover; // result in OnMCPDiscover and ServerDiscover
MCPClient.ListTools;
end;
end;
On the 2026-07-28 path the client writes _meta on every request, sends the routing headers, including Mcp-Param-* for tools that declare x-mcp-header, and opens subscriptions with SubscriptionsListen. List changes arrive in OnMCPListChanged and resource updates in OnMCPResourcesUpdated. With MCPOptions.Tasks.Enabled the client declares the Tasks extension and polls tasks on its own, firing OnMCPTaskCreated, OnMCPTaskStatus and OnMCPTaskCompleted.
Answering MRTR questions with OnMCPInputRequired
When a server answers input_required, the client fires OnMCPInputRequired with the pending questions. Answer each key and leave Accept as True. The client then retries the call with the answers and the signed requestState, up to MCPOptions.MRTR.MaxRounds rounds. Set MCPOptions.MRTR.Elicitation, Sampling or Roots to declare those capabilities.
procedure TMainForm.MCPClientMCPInputRequired(Sender: TObject;
const aMethod: string;
const aInputRequests: TsgcAI_MCP_Client_InputRequests;
var Accept: Boolean);
var
i: Integer;
begin
for i := 0 to aInputRequests.Count - 1 do
if aInputRequests.Methods[i] = 'elicitation/create' then
aInputRequests.SetElicitationAccept(aInputRequests.Keys[i],
'{"name":"Sergio"}');
Accept := True;
end;
The same event covers tasks waiting for input, in which case aMethod is tasks/update. Legacy servers that still send roots, sampling or elicitation requests directly get their replies from OnMCPListRoots, OnMCPSamplingCreateMessage and OnMCPElicitationCreate.
Client authorization with MCPOptions.Authorization
Set MCPOptions.Authorization.Enabled and a 401 or 403 from the server starts the MCP authorization flow: protected resource metadata discovery, registration (a pre-registered ClientId, a Client ID Metadata Document from ClientMetadataURL, or dynamic registration), PKCE with the resource indicator, the browser sign in on the loopback RedirectURL, iss validation and the token request. The original request is then retried, and tokens are refreshed before they expire.
procedure TMainForm.FormCreate(Sender: TObject);
begin
MCPClient.MCPOptions.Authorization.Enabled := True;
MCPClient.MCPOptions.Authorization.ClientMetadataURL :=
'https://app.example.com/oauth/client.json';
MCPClient.MCPOptions.Authorization.AllowDynamicRegistration := True;
MCPClient.MCPOptions.Authorization.RedirectURL := 'http://127.0.0.1:33418/callback';
MCPClient.OnMCPAuthorizationURL := MCPClientAuthorizationURL;
end;
procedure TMainForm.MCPClientAuthorizationURL(Sender: TObject;
const aURL: string; var Handled: Boolean);
begin
MemoLog.Lines.Add('Sign in at ' + aURL);
Handled := False; // False: the default browser is opened
end;
Credentials are kept per issuer. Handle OnMCPAuthorizationCredentials to store them and load them on the next run, so the user signs in only once.
Also in sgcWebSockets .NET
Everything in this article is also available in sgcWebSockets .NET, with the same class, property and event names: TsgcWSAPIServer_MCP and TsgcWSAPIClient_MCP expose MCPOptions.Subscriptions, MCPOptions.MRTR, MCPOptions.Tasks, ProtocolEra, Discover and MCPOptions.Authorization, so a C# application can talk to a Delphi MCP server in either era, and the other way round.
Demos and documentation
The demos in Demos\15.AI\03.MCP show every feature. The server demo (01.MCP_Server) logs the era of each request, has switches for tasks and subscriptions and implements the ask_name and long_job tools. The client demo (02.MCP_Client) lets you choose the era, call Discover, subscribe to notifications, answer MRTR questions and run tasks.
The full reference is in the MCP server documentation and the MCP client documentation, and the MCP components for Delphi page has an overview of everything the components do. You can download sgcWebSockets 2026.10.0 from the sgcWebSockets download page.
Questions, feedback or migration help? Get in touch, you will get a reply from the people who wrote the code.
