모델 컨텍스트 프로토콜(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에서 달라진 점
쉽게 말해, 서버나 클라이언트를 만들 때 중요한 변경 사항은 다음과 같습니다:
- 상태 비저장 요청.
initialize핸드셰이크도Mcp-Session-Id도 없습니다. 각 요청은 프로토콜 버전, 클라이언트 정보, 클라이언트 기능을_meta안에 담고 있으므로, 로드 밸런서 뒤에 있는 어떤 서버 인스턴스든 응답할 수 있습니다. - server/discover. 서버가 제공하는 기능을 알고 싶은 클라이언트는
server/discover를 호출해 지원되는 버전, 기능, 지침을 받습니다. 결과는 캐시할 수 있으며, 캐시 힌트(ttlMs,cacheScope)를 함께 전달합니다. - subscriptions/listen. 하나의 스트리밍 요청이 기존의 GET 스트림을 대체합니다. 클라이언트는 원하는 알림(tools, prompts, resources 목록 변경, 리소스 업데이트)을 지정하고, 서버는 해당 스트림으로 알림을 전송합니다.
- 다중 라운드트립 요청(MRTR). tool, prompt, resource 핸들러가 입력을 요청하기 위해 클라이언트를 다시 호출할 필요가 없습니다. 대신 질문과 함께
input_required를 반환하면, 클라이언트가 답변한 뒤 그 답변과 서명된requestState를 담아 호출을 재시도합니다. - Tasks 확장. 오래 걸리는 tool 호출은 즉시 작업 핸들(task handle)로 응답할 수 있습니다. 클라이언트는
tasks/get으로 폴링하고,tasks/update로 질문에 답하며,tasks/cancel로 중지합니다. - 라우팅 헤더. HTTP 요청은
MCP-Protocol-Version,Mcp-Method,Mcp-Name헤더(그리고x-mcp-header로 표시된 tool 인자에 대한Mcp-Param-*)를 전달하므로, 게이트웨이가 본문을 파싱하지 않고도 라우팅과 필터링을 할 수 있습니다. 서버는 이를 본문과 대조해 검증합니다. - 클라이언트 인증. 클라이언트용 OAuth 흐름이 강화되었습니다: 보호된 리소스 메타데이터 검색(protected resource metadata discovery), Client ID Metadata Documents, 리소스 인디케이터를 포함한 PKCE, RFC 9207
iss검증입니다.
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 다운로드 페이지에서 받을 수 있습니다.
질문이나 피드백, 마이그레이션 관련 도움이 필요하신가요? 문의하기, 코드를 직접 작성한 사람들에게서 답변을 받으실 수 있습니다.
