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 서버를 운영 중인 분들에게 중요한 점은, 아무것도 깨지지 않는다는 것입니다. 서버는 두 시대를 모두 지원합니다. VS Code나 Claude처럼 2025-11-25를 사용하는 클라이언트는 이전과 똑같이 initialize를 호출해 세션을 얻고, 2026-07-28 클라이언트는 동일한 엔드포인트에서 새로운 상태 비저장 모델을 사용합니다. 스위치나 두 번째 서버는 필요하지 않습니다.

MCP 2026-07-28에서 달라진 점

쉽게 말해, 서버나 클라이언트를 만들 때 중요한 변경 사항은 다음과 같습니다:

Delphi에서 두 시대를 지원하는 MCP 서버

TsgcWSAPIServer_MCP 컴포넌트는 모든 요청의 시대를 감지합니다. _meta가 2026-07-28을 가리키는 요청은 상태 비저장 경로로 처리되고, initialize와 이전 버전은 이미 알고 계신 세션 경로로 처리됩니다. tool, prompt, resource 핸들러는 양쪽에서 공유됩니다. 새 옵션은 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 세션 모두에 전달됩니다. 서버는 유휴 상태인 스트림에 킵얼라이브(keep-alive) 코멘트를 보내며, 서버가 비활성화되면 열려 있던 모든 구독이 최종 결과와 함께 정상적으로 종료됩니다.

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를 채우고 반환하는 방식으로 추가 입력을 요청합니다. 클라이언트는 답변을 모아 tool을 다시 호출합니다. 두 번째 라운드에서는 aRequest.InputResponse로 답변을 읽습니다. 아래 예제는 서버 데모의 ask_name tool입니다.

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 tool

MCPOptions.Tasks에서 tasks를 활성화하고 tool 핸들러에서 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가 발생합니다. 작업은 이를 생성한 주체(principal)에 귀속되며 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를 선언한 tool에 대한 Mcp-Param-*을 포함한 라우팅 헤더를 전송하며, SubscriptionsListen으로 구독을 엽니다. 목록 변경은 OnMCPListChanged로, 리소스 업데이트는 OnMCPResourcesUpdated로 전달됩니다. MCPOptions.Tasks.Enabled가 설정되면 클라이언트는 Tasks 확장을 선언하고 스스로 작업을 폴링하며, OnMCPTaskCreated, OnMCPTaskStatus, OnMCPTaskCompleted를 발생시킵니다.

OnMCPInputRequired로 MRTR 질문에 답하기

서버가 input_required로 응답하면, 클라이언트는 대기 중인 질문과 함께 OnMCPInputRequired를 발생시킵니다. 각 키에 답변하고 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가 됩니다. roots, sampling, elicitation 요청을 여전히 직접 보내는 레거시 서버는 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;

자격 증명은 발급자(issuer)별로 저장됩니다. 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와 subscriptions를 켜고 끌 수 있는 스위치가 있으며, ask_name과 long_job tool을 구현합니다. 클라이언트 데모(02.MCP_Client)는 시대를 선택하고, Discover를 호출하고, 알림을 구독하고, MRTR 질문에 답하고, 작업을 실행할 수 있게 해줍니다.

전체 레퍼런스는 MCP 서버 문서와 MCP 클라이언트 문서에 있으며, Delphi용 MCP 컴포넌트 페이지에서 컴포넌트가 수행하는 모든 기능의 개요를 확인할 수 있습니다. sgcWebSockets 2026.10.0은 sgcWebSockets 다운로드 페이지에서 받을 수 있습니다.

질문이나 피드백, 마이그레이션 관련 도움이 필요하신가요? 문의하기, 코드를 직접 작성한 사람들에게서 답변을 받으실 수 있습니다.

관련 자료