Servidor y cliente MCP de Delphi: la nueva especificación MCP 2026-07-28

· Componentes
Servidor y cliente MCP de Delphi: la nueva especificación MCP 2026-07-28

El Model Context Protocol tiene una nueva especificación, MCP 2026-07-28. Es el mayor cambio al protocolo desde Streamable HTTP: el handshake de sesión desaparece, cada solicitud lleva lo que el servidor necesita para responderla, y el trabajo largo o interactivo obtiene soporte de primera clase. sgcWebSockets 2026.10.0 lo implementa tanto en el servidor MCP como en el cliente MCP para Delphi y C++Builder.

Lo importante para quien ya tenga un servidor MCP en Delphi en producción: nada se rompe. El servidor es de doble era. Los clientes que hablan 2025-11-25, como VS Code o Claude, siguen llamando a initialize y obtienen una sesión exactamente como antes, mientras que los clientes 2026-07-28 usan el nuevo modelo sin estado en el mismo endpoint. No necesita un interruptor ni un segundo servidor.

Qué cambió en MCP 2026-07-28

En términos sencillos, estos son los cambios que importan al construir un servidor o un cliente:

Un servidor MCP de doble era en Delphi

El componente TsgcWSAPIServer_MCP detecta la era de cada solicitud. Una solicitud cuyo _meta indica 2026-07-28 sigue el camino sin estado, mientras que initialize y las versiones anteriores siguen el camino de sesión que ya conoce. Sus handlers de tool, prompt y resource son compartidos por ambos. Las nuevas opciones solo completan lo que los clientes 2026-07-28 leen desde server/discover y desde las indicaciones de caché.

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;

Cuando se establece ServerInfo.Name, los resultados 2026-07-28 también llevan la información del servidor en _meta. Los errores de protocolo usan los nuevos códigos: una cabecera que no coincide con el cuerpo responde -32020, una capacidad de cliente ausente -32021 y una versión no soportada -32022, con la lista de versiones soportadas en los datos del error.

Subscriptions con subscriptions/listen

Subscriptions está activado de forma predeterminada. Un cliente 2026-07-28 abre un stream subscriptions/listen, y las notificaciones que ya envía con SendNotificationToolsListChanged o SendNotificationResourcesUpdated llegan a esos listeners además de a las sesiones 2025-11-25. El servidor envía un comentario keep-alive en los streams inactivos, y cuando el servidor se desactiva, cada subscription abierta se cierra de forma ordenada con su resultado final.

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: pedir el nombre al usuario

Con MRTR, un handler pide más información completando aResponse.InputRequired y retornando. El cliente recoge las respuestas y vuelve a llamar a la tool. En la segunda ronda las respuestas se leen con aRequest.InputResponse. El ejemplo siguiente es la tool ask_name del demo de servidor.

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;

El requestState viaja firmado con HMAC-SHA256 usando MCPOptions.MRTR.Secret y expira tras MCPOptions.MRTR.StateTTL segundos, de modo que un cliente no puede manipularlo. La elicitation por URL (AddElicitationURL), sampling (AddSampling) y roots (AddRoots) siguen el mismo patrón. Antes de responder input_required, el servidor verifica que el cliente declaró la capacidad correspondiente.

La extensión Tasks: una tool long_job

Active tasks en MCPOptions.Tasks y llame a CreateTask desde el handler de la tool. Cuando el cliente declaró la extensión io.modelcontextprotocol/tasks en esa solicitud, la llamada responde de inmediato con un id de task y su código termina el trabajo en su propio thread. CreateTask devuelve nil cuando tasks está desactivado, la solicitud es 2025-11-25 o el cliente no declaró la extensión, así que mantenga un camino síncrono para esos clientes.

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;

El objeto task es thread safe. OnMCPTaskCancel se dispara cuando el cliente llama a tasks/cancel, y OnMCPTaskUpdate cuando responde a una task que espera información. Las tasks están vinculadas al principal que las creó y expiran tras TTL milisegundos.

El cliente MCP: ProtocolEra y Discover

El componente TsgcWSAPIClient_MCP obtiene una nueva propiedad MCPOptions.ProtocolEra. Déjela en aimcpeAuto y el cliente sondea el servidor con server/discover, usa 2026-07-28 cuando el servidor lo soporta y recurre al handshake de initialize cuando no lo soporta. La era negociada se cachea por endpoint. Establézcala en aimcpeModern o aimcpeLegacy para forzar una de ellas.

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;

En el camino 2026-07-28 el cliente escribe _meta en cada solicitud, envía las cabeceras de enrutamiento, incluida Mcp-Param-* para las tools que declaran x-mcp-header, y abre subscriptions con SubscriptionsListen. Los cambios de lista llegan en OnMCPListChanged y las actualizaciones de resource en OnMCPResourcesUpdated. Con MCPOptions.Tasks.Enabled el cliente declara la extensión Tasks y consulta las tasks por su cuenta, disparando OnMCPTaskCreated, OnMCPTaskStatus y OnMCPTaskCompleted.

Responder preguntas de MRTR con OnMCPInputRequired

Cuando un servidor responde input_required, el cliente dispara OnMCPInputRequired con las preguntas pendientes. Responda cada clave y deje Accept en True. El cliente entonces reintenta la llamada con las respuestas y el requestState firmado, hasta MCPOptions.MRTR.MaxRounds rondas. Establezca MCPOptions.MRTR.Elicitation, Sampling o Roots para declarar esas capacidades.

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;

El mismo evento cubre las tasks que esperan información, en cuyo caso aMethod es tasks/update. Los servidores heredados que todavía envían solicitudes de roots, sampling o elicitation directamente obtienen sus respuestas desde OnMCPListRoots, OnMCPSamplingCreateMessage y OnMCPElicitationCreate.

Autorización de cliente con MCPOptions.Authorization

Establezca MCPOptions.Authorization.Enabled y un 401 o 403 del servidor inicia el flujo de autorización MCP: descubrimiento de metadatos de protected resource, registro (un ClientId preregistrado, un Client ID Metadata Document desde ClientMetadataURL, o registro dinámico), PKCE con el indicador de recurso, el inicio de sesión en el navegador en el RedirectURL de loopback, validación de iss y la solicitud del token. La solicitud original se reintenta después, y los tokens se renuevan antes de expirar.

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;

Las credenciales se guardan por issuer. Gestione OnMCPAuthorizationCredentials para almacenarlas y cargarlas en la siguiente ejecución, de modo que el usuario inicie sesión una sola vez.

También en sgcWebSockets .NET

Todo lo de este artículo también está disponible en sgcWebSockets .NET, con los mismos nombres de clase, propiedad y evento: TsgcWSAPIServer_MCP y TsgcWSAPIClient_MCP exponen MCPOptions.Subscriptions, MCPOptions.MRTR, MCPOptions.Tasks, ProtocolEra, Discover y MCPOptions.Authorization, de modo que una aplicación C# puede hablar con un servidor MCP en Delphi en cualquiera de las dos eras, y viceversa.

Demos y documentación

Los demos en Demos\15.AI\03.MCP muestran todas las funciones. El demo de servidor (01.MCP_Server) registra la era de cada solicitud, tiene interruptores para tasks y subscriptions e implementa las tools ask_name y long_job. El demo de cliente (02.MCP_Client) le permite elegir la era, llamar a Discover, suscribirse a notificaciones, responder preguntas de MRTR y ejecutar tasks.

La referencia completa está en la documentación del servidor MCP y la documentación del cliente MCP, y la página de componentes MCP para Delphi tiene una descripción general de todo lo que hacen los componentes. Puede descargar sgcWebSockets 2026.10.0 desde la página de descarga de sgcWebSockets.

¿Preguntas, comentarios o ayuda con la migración? Contáctenos, recibirá respuesta de las personas que escribieron el código.

Relacionado