Delphi MCP 服务器与客户端:全新 MCP 2026-07-28 规范

· 组件
Delphi MCP 服务器与客户端:全新 MCP 2026-07-28 规范 | eSeGeCe 博客

Model Context Protocol 迎来了新规范,MCP 2026-07-28。这是自 Streamable HTTP 以来该协议最大的一次变化:会话握手不复存在,每个请求都自带服务器应答所需的一切,长时间或交互式的工作也获得了一等支持。sgcWebSockets 2026.10.0 在面向 Delphi 和 C++Builder 的 MCP 服务器与 MCP 客户端中都实现了这套规范。

对已经在运行 Delphi MCP 服务器的用户来说,重要的一点是:什么都不会中断。服务器是双时代兼容的。使用 2025-11-25 版本协议的客户端,例如 VS Code 或 Claude,依旧照常调用 initialize 并获得和以前一样的会话,而 2026-07-28 版本的客户端则会在同一个端点上使用全新的无状态模型。你不需要额外的开关,也不需要第二台服务器。

MCP 2026-07-28 有哪些变化

简单来说,以下是构建服务器或客户端时需要关注的变化:

Delphi 中的双时代兼容 MCP 服务器

TsgcWSAPIServer_MCP 组件会检测每个请求所处的时代。_meta 中标明 2026-07-28 的请求走无状态路径,而 initialize 及更旧版本则走你早已熟悉的会话路径。你的工具、提示词和资源处理函数在两条路径之间是共用的。新增的选项只用来填充 2026-07-28 客户端从 server/discover 和缓存提示中读取的内容。

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;

设置了 ServerInfo.Name 后,2026-07-28 的结果也会在 _meta 中携带服务器信息。协议错误使用新的错误码:请求头与请求体不一致时返回 -32020,缺少客户端能力返回 -32021,版本不受支持返回 -32022,受支持的版本列表则包含在错误数据中。

使用 subscriptions/listen 实现订阅

订阅功能默认开启。2026-07-28 客户端会打开一条 subscriptions/listen 流,而你已经在用 SendNotificationToolsListChanged 或 SendNotificationResourcesUpdated 发送的通知,也会同时送达这些监听者和 2025-11-25 会话。服务器会在空闲的流上发送保活注释,服务器停用时,每一个打开的订阅都会连同其最终结果一起被优雅地关闭。

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;

多轮往返请求:向用户询问姓名

借助 MRTR,处理函数只需填充 aResponse.InputRequired 并返回,就可以请求更多输入。客户端会收集答案并再次调用该工具。在第二轮中,答案通过 aRequest.InputResponse 读取。下面的示例是服务器演示程序中的 ask_name 工具。

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;

requestState 使用 MCPOptions.MRTR.Secret 通过 HMAC-SHA256 签名后传递,并在 MCPOptions.MRTR.StateTTL 秒后过期,因此客户端无法篡改它。URL 征询(AddElicitationURL)、采样(AddSampling)和根目录(AddRoots)也遵循同样的模式。在应答 input_required 之前,服务器会检查客户端是否声明了对应的能力。

Tasks 扩展:一个 long_job 工具

在 MCPOptions.Tasks 中启用 tasks,并在工具处理函数中调用 CreateTask。当客户端在该请求中声明了 io.modelcontextprotocol/tasks 扩展时,调用会立即返回一个任务 id,你的代码则在自己的线程中完成工作。当 tasks 被禁用、请求是 2025-11-25 版本,或客户端没有声明该扩展时,CreateTask 会返回 nil,因此请为这些客户端保留一条同步路径。

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;

任务对象是线程安全的。客户端调用 tasks/cancel 时会触发 OnMCPTaskCancel,它回答一个等待输入的任务时则触发 OnMCPTaskUpdate。任务与创建它的主体绑定,并在 TTL 毫秒后过期。

MCP 客户端:ProtocolEra 与 Discover

TsgcWSAPIClient_MCP 组件新增了 MCPOptions.ProtocolEra 属性。保持默认值 aimcpeAuto,客户端就会用 server/discover 探测服务器,在服务器支持时使用 2026-07-28,否则回退到 initialize 握手。协商出的时代会按端点缓存。将它设为 aimcpeModern 或 aimcpeLegacy 可以强制使用某一种。

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;

在 2026-07-28 路径上,客户端会在每个请求中写入 _meta,发送路由请求头(包括为声明了 x-mcp-header 的工具附加的 Mcp-Param-*),并使用 SubscriptionsListen 打开订阅。列表变化会通过 OnMCPListChanged 送达,资源更新则通过 OnMCPResourcesUpdated 送达。启用 MCPOptions.Tasks.Enabled 后,客户端会声明 Tasks 扩展并自行轮询任务,触发 OnMCPTaskCreated、OnMCPTaskStatus 和 OnMCPTaskCompleted。

使用 OnMCPInputRequired 回答 MRTR 问题

当服务器应答 input_required 时,客户端会携带待处理的问题触发 OnMCPInputRequired。逐个回答每个 key,并让 Accept 保持为 True。之后客户端会带着答案和签名过的 requestState 重试该调用,最多重试 MCPOptions.MRTR.MaxRounds 轮。设置 MCPOptions.MRTR.Elicitation、Sampling 或 Roots 可以声明对应的能力。

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;

同一个事件也覆盖等待输入的任务,这种情况下 aMethod 的值是 tasks/update。仍然直接发送根目录、采样或征询请求的旧版服务器,则分别从 OnMCPListRoots、OnMCPSamplingCreateMessage 和 OnMCPElicitationCreate 获得回复。

使用 MCPOptions.Authorization 实现客户端授权

设置 MCPOptions.Authorization.Enabled 后,服务器返回的 401 或 403 会启动 MCP 授权流程:受保护资源元数据发现、注册(预先注册的 ClientId、来自 ClientMetadataURL 的 Client ID Metadata Document,或动态注册)、带资源指示符的 PKCE、在回环地址 RedirectURL 上进行的浏览器登录、iss 校验以及令牌请求。之后原始请求会被重试,令牌也会在过期前刷新。

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;

凭据按颁发者分别保存。处理 OnMCPAuthorizationCredentials 事件来存储它们,并在下次运行时加载,这样用户只需登录一次。

sgcWebSockets .NET 中同样支持

本文介绍的一切在 sgcWebSockets .NET 中同样可用,并且使用相同的类名、属性名和事件名:TsgcWSAPIServer_MCP 和 TsgcWSAPIClient_MCP 暴露了 MCPOptions.Subscriptions、MCPOptions.MRTR、MCPOptions.Tasks、ProtocolEra、Discover 和 MCPOptions.Authorization,因此 C# 应用可以在任意一个时代与 Delphi MCP 服务器通信,反过来也是一样。

演示程序与文档

Demos\15.AI\03.MCP 中的演示程序展示了每一项功能。服务器演示程序(01.MCP_Server)会记录每个请求所处的时代,带有 tasks 和订阅的开关,并实现了 ask_name 和 long_job 工具。客户端演示程序(02.MCP_Client)可以让你选择时代、调用 Discover、订阅通知、回答 MRTR 问题以及运行任务。

完整的参考文档在MCP 服务器文档和MCP 客户端文档中,而面向 Delphi 的 MCP 组件页面则概述了这些组件的全部功能。你可以从sgcWebSockets 下载页面下载 sgcWebSockets 2026.10.0。

有疑问、反馈,或者需要迁移方面的帮助?联系我们,回复你的会是编写这套代码的人。

相关内容