Server e client MCP per Delphi: la nuova specifica MCP 2026-07-28

· Componenti
Server e client MCP per Delphi: la nuova specifica MCP 2026-07-28

Il Model Context Protocol ha una nuova specifica, MCP 2026-07-28. È il cambiamento più grande al protocollo dai tempi di Streamable HTTP: l'handshake di sessione è scomparso, ogni richiesta porta con sé tutto ciò che serve al server per rispondere, e il lavoro lungo o interattivo riceve un supporto di prima classe. sgcWebSockets 2026.10.0 lo implementa sia nel server MCP sia nel client MCP per Delphi e C++Builder.

La parte importante per chi ha già un server MCP Delphi in funzione: niente si rompe. Il server è a doppia era. I client che parlano 2025-11-25, come VS Code o Claude, continuano a chiamare initialize e ottengono una sessione esattamente come prima, mentre i client 2026-07-28 usano il nuovo modello stateless sullo stesso endpoint. Non serve un interruttore né un secondo server.

Cosa è cambiato in MCP 2026-07-28

In parole semplici, questi sono i cambiamenti che contano quando si costruisce un server o un client:

Un server MCP a doppia era in Delphi

Il componente TsgcWSAPIServer_MCP rileva l'era di ogni richiesta. Una richiesta il cui _meta indica 2026-07-28 segue il percorso stateless, mentre initialize e le versioni precedenti seguono il percorso di sessione che già conosci. I tuoi handler di tool, prompt e resource sono condivisi da entrambi. Le nuove opzioni si limitano a fornire ciò che i client 2026-07-28 leggono da server/discover e dai suggerimenti di cache.

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;

Quando ServerInfo.Name è impostato, i risultati 2026-07-28 portano anche le informazioni sul server in _meta. Gli errori di protocollo usano i nuovi codici: un header che non corrisponde al body risponde con -32020, una capability del client mancante con -32021 e una versione non supportata con -32022, con l'elenco delle versioni supportate nei dati dell'errore.

Subscriptions con subscriptions/listen

Le subscriptions sono attive per impostazione predefinita. Un client 2026-07-28 apre un singolo stream subscriptions/listen, e le notifiche che già invii con SendNotificationToolsListChanged o SendNotificationResourcesUpdated raggiungono quei listener così come le sessioni 2025-11-25. Il server invia un commento keep-alive sugli stream inattivi, e quando il server viene disattivato ogni subscription aperta viene chiusa in modo pulito con il suo risultato finale.

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;

Richieste multi round-trip: chiedere all'utente il proprio nome

Con MRTR un handler chiede ulteriore input compilando aResponse.InputRequired e uscendo. Il client raccoglie le risposte e chiama di nuovo il tool. Al secondo giro le risposte vengono lette con aRequest.InputResponse. L'esempio seguente è il tool ask_name della demo del server.

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;

Il requestState viaggia firmato con HMAC-SHA256 usando MCPOptions.MRTR.Secret e scade dopo MCPOptions.MRTR.StateTTL secondi, così un client non può manometterlo. L'elicitation via URL (AddElicitationURL), il sampling (AddSampling) e i roots (AddRoots) seguono lo stesso schema. Prima di rispondere a input_required, il server verifica che il client abbia dichiarato la capability corrispondente.

L'estensione Tasks: un tool long_job

Abilita i tasks in MCPOptions.Tasks e chiama CreateTask dall'handler del tool. Quando il client ha dichiarato l'estensione io.modelcontextprotocol/tasks su quella richiesta, la chiamata risponde subito con un task id e il tuo codice completa il lavoro sul proprio thread. CreateTask restituisce nil quando i tasks sono disabilitati, la richiesta è 2025-11-25 oppure il client non ha dichiarato l'estensione, quindi mantieni un percorso sincrono per quei client.

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;

L'oggetto task è thread safe. OnMCPTaskCancel scatta quando il client chiama tasks/cancel, e OnMCPTaskUpdate quando risponde a un task in attesa di input. I tasks sono legati al principal che li ha creati e scadono dopo TTL millisecondi.

Il client MCP: ProtocolEra e Discover

Il componente TsgcWSAPIClient_MCP riceve una nuova proprietà MCPOptions.ProtocolEra. Lasciala su aimcpeAuto e il client sonda il server con server/discover, usa 2026-07-28 quando il server lo supporta e torna all'handshake initialize quando non lo supporta. L'era negoziata viene messa in cache per endpoint. Impostala su aimcpeModern o aimcpeLegacy per forzarne una.

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;

Sul percorso 2026-07-28 il client scrive _meta su ogni richiesta, invia gli header di routing, incluso Mcp-Param-* per i tool che dichiarano x-mcp-header, e apre le subscriptions con SubscriptionsListen. I cambi di elenco arrivano in OnMCPListChanged e gli aggiornamenti delle risorse in OnMCPResourcesUpdated. Con MCPOptions.Tasks.Enabled il client dichiara l'estensione Tasks e interroga i tasks per conto proprio, generando OnMCPTaskCreated, OnMCPTaskStatus e OnMCPTaskCompleted.

Rispondere alle domande MRTR con OnMCPInputRequired

Quando un server risponde input_required, il client genera OnMCPInputRequired con le domande in sospeso. Rispondi a ogni chiave e lascia Accept a True. Il client ripete quindi la chiamata con le risposte e il requestState firmato, fino a MCPOptions.MRTR.MaxRounds giri. Imposta MCPOptions.MRTR.Elicitation, Sampling o Roots per dichiarare queste capability.

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;

Lo stesso evento copre i tasks in attesa di input, nel qual caso aMethod è tasks/update. I server legacy che inviano ancora direttamente richieste di roots, sampling o elicitation ricevono le loro risposte da OnMCPListRoots, OnMCPSamplingCreateMessage e OnMCPElicitationCreate.

Autorizzazione del client con MCPOptions.Authorization

Imposta MCPOptions.Authorization.Enabled e un 401 o 403 dal server avvia il flusso di autorizzazione MCP: discovery dei metadata delle risorse protette, registrazione (un ClientId pre-registrato, un Client ID Metadata Document da ClientMetadataURL, oppure registrazione dinamica), PKCE con l'indicatore di risorsa, l'accesso via browser sul RedirectURL in loopback, la validazione iss e la richiesta del token. La richiesta originale viene poi ripetuta, e i token vengono rinnovati prima di scadere.

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;

Le credenziali vengono conservate per issuer. Gestisci OnMCPAuthorizationCredentials per salvarle e caricarle alla successiva esecuzione, così l'utente accede una sola volta.

Anche in sgcWebSockets .NET

Tutto ciò che trovi in questo articolo è disponibile anche in sgcWebSockets .NET, con gli stessi nomi di classi, proprietà ed eventi: TsgcWSAPIServer_MCP e TsgcWSAPIClient_MCP espongono MCPOptions.Subscriptions, MCPOptions.MRTR, MCPOptions.Tasks, ProtocolEra, Discover e MCPOptions.Authorization, così un'applicazione C# può parlare con un server MCP Delphi in entrambe le ere, e viceversa.

Demo e documentazione

Le demo in Demos\15.AI\03.MCP mostrano ogni funzionalità. La demo del server (01.MCP_Server) registra l'era di ogni richiesta, ha interruttori per tasks e subscriptions e implementa i tool ask_name e long_job. La demo del client (02.MCP_Client) ti permette di scegliere l'era, chiamare Discover, iscriverti alle notifiche, rispondere alle domande MRTR ed eseguire i tasks.

Il riferimento completo è nella documentazione del server MCP e nella documentazione del client MCP, e la pagina componenti MCP per Delphi offre una panoramica di tutto ciò che fanno i componenti. Puoi scaricare sgcWebSockets 2026.10.0 dalla pagina di download di sgcWebSockets.

Domande, feedback o serve aiuto con la migrazione? Contattaci, riceverai una risposta da chi ha scritto il codice.

Correlati