MCP 이벤트를 사용하면 ChatGPT가 MCP 서버에서 새 메시지, 콘텐츠 업데이트, 상태 변경 등의 소식을 구독할 수 있습니다. 사용자는 무엇을 모니터링할지, 업데이트가 도착하면 ChatGPT가 어떤 작업을 수행할지 정합니다.
| 사용 사례 | 사용자 요청 | MCP 이벤트 |
|---|---|---|
| 피드백을 바탕으로 Pull Request 작성 | #product-feedback에 버그 제보가 올라오면 수정 사항과 테스트를 담은 초안 Pull Request를 열어 줘. | message.created, channel_id로 필터링 |
| 문서에 피드백 반영 | 이 문서에 리뷰 댓글이 달리는지 확인하고 요청받은 수정 사항을 반영해 줘. | comment.created, document_id로 필터링 |
시작하기 전에
MCP 이벤트는 ChatGPT 웹의 Work 모드 채팅, 데스크톱 앱에서 클라우드를 선택한 Work 모드 채팅, dot에서 사용할 수 있습니다. 플러그인과 이벤트로 실행되는 작업에는 워크스페이스 설정이 적용됩니다.
ChatGPT는 initialize를 사용하는 Streamable HTTP 또는 MCP 2.0 HTTP(프로토콜 버전 2026-07-28)를 통해 MCP 이벤트를 지원합니다. 플러그인에서 서버를 구성하고, 구독 정보를 영구 저장할 수 있도록 저장소를 마련하세요. 서버에서 콜백 URL로 아웃바운드 HTTPS 연결도 허용하세요.
ChatGPT는 MCP 이벤트 명세 초안에 정의된 웹훅 전송과 콜백 검증을 지원합니다. 이 연동에서는 폴링, 스트리밍, 초안의 gap 및 terminated 제어 알림을 지원하지 않습니다.
작동 방식
- 서버가 지원하는 이벤트 목록을 제공합니다.
- 사용자가 ChatGPT에 모니터링할 대상과 대응 방법을 알려 줍니다.
- ChatGPT가 MCP 서버를 통해 이벤트를 구독하고 콜백 URL과 서명용 비밀 키를 제공합니다.
- 서버가 구독 조건에 맞는 이벤트를 해당 URL로 보냅니다.
- ChatGPT가 구독을 설정한 채팅에서 이벤트를 수신하고 사용자의 지시에 따라 대응합니다.
이벤트 지원 여부 알리기
이벤트를 검색하려면 먼저 서버가 지원하는 기능을 알려야 합니다. 초기화를 사용하는 MCP 버전에서는 events를 initialize 응답의 기능 목록에 추가하세요.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"events": {}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
}
}
MCP 2.0(프로토콜 버전 2026-07-28)에서는 events를 server/discover 응답의 기능 목록에 추가하세요.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"events": {}
}
}
}
도구와 같은 MCP 엔드포인트에 다음 세 가지 이벤트 메서드를 구현하세요. 이 엔드포인트는 인증을 요구해야 합니다.
| 메서드 | 서버 동작 |
|---|---|
events/list |
사용 가능한 이벤트와 각 이벤트의 필터를 설명합니다. |
events/subscribe |
구독을 생성하거나 갱신합니다. |
events/unsubscribe |
구독을 중지합니다. |
이벤트 정의하기
이벤트 정의는 사용자가 구독할 수 있는 이벤트와 사용 가능한 필터를 ChatGPT에 알려 줍니다. events/list에서 이벤트 이름, 지원하는 전송 방식, 구독 인자, 페이로드 스키마를 포함한 이벤트 정의를 반환하세요.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"events": [
{
"name": "comment.created",
"description": "A new review comment was added to the specified document.",
"delivery": ["webhook"],
"inputSchema": {
"type": "object",
"properties": {
"document_id": {
"type": "string",
"description": "ID of the document to monitor for new review comments."
}
},
"required": ["document_id"],
"additionalProperties": false
},
"payloadSchema": {
"type": "object",
"properties": {
"document_id": { "type": "string" },
"comment_id": { "type": "string" },
"text": { "type": "string" },
"url": { "type": "string" }
},
"required": ["document_id", "comment_id", "text", "url"],
"additionalProperties": false
}
}
]
}
}
inputSchema는 ChatGPT가 구독할 때 전달하는 인자를 정의하고, payloadSchema는 전송되는 각 이벤트의 data 객체를 정의합니다.
이벤트 이름은 일관되게 유지하고 설명은 구체적으로 작성하세요. 문서 ID, 프로젝트 ID, 큐 ID 등을 필터로 제공하고 이벤트를 전송하기 전에 서버에서 필터를 적용하세요. 연결된 계정에 조회 권한이 있는 이벤트만 반환하세요.
목록이 여러 페이지에 걸쳐 있으면 nextCursor를 반환하세요. 서버는 이 값을 cursor로 지정한 다음 events/list 요청을 처리해야 합니다.
구독 생성하기
사용자가 이벤트 모니터링을 요청하면 ChatGPT는 events/subscribe를 호출하면서 이벤트 이름, 필터 인자, 웹훅 전송 대상을 지정합니다.
{
"jsonrpc": "2.0",
"id": 2,
"method": "events/subscribe",
"params": {
"name": "comment.created",
"arguments": {
"document_id": "doc_123"
},
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123",
"secret": "whsec_<base64-encoded-signing-key>"
},
"cursor": null
}
}
구독을 수락하기 전에 다음 절차를 따르세요.
- 사용자가 요청에 지정한 인자로 해당 이벤트를 구독할 권한이 있는지 확인하세요.
- 이벤트 이름과 인자가 이벤트 정의에 맞는지 확인하세요. 서명용 비밀 키는
whsec_로 시작해야 하며, 접두사 뒤의 base64 값을 디코딩한 결과는 24~64바이트여야 합니다. - 콜백 URL의 유효성을 검사하고 콜백을 검증하세요.
- 구독 정보와 함께 소유자, 필터, 콜백 URL, 서명용 비밀 키, 만료 시각을 저장하세요.
인증된 주체, 콜백 URL, 이벤트 이름, 인자가 같으면 항상 같은 구독 ID가 생성되도록 하세요. 서버가 정한 만료 시각과 함께 이 ID를 반환하세요.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
refreshBefore를 서버가 정한 만료 시각으로 설정하세요. 재전송을 지원하지 않는 이벤트 유형에는 cursor: null을 반환하세요.
구독 생성은 멱등성을 보장해야 합니다. 구독을 식별하는 정보가 일치하면 기존 구독을 업데이트하세요. 객체 키 순서 때문에 구독이 중복 생성되지 않도록 인자는 정규화된 JSON으로 비교하세요.
콜백 검증하기
애플리케이션 데이터를 보내기 전에 유효 기간이 짧은 일회용 챌린지를 새로 생성하고, 이를 담은 서명된 요청을 보내 콜백을 검증하세요.
{
"type": "verification",
"challenge": "a-single-use-random-value"
}
검증 요청에 고유한 webhook-id(예: msg_verification_123)를 부여하고 구독의 비밀 키로 요청 본문에 서명하세요. webhook-timestamp, webhook-signature, X-MCP-Subscription-Id 헤더를 포함하세요. 요청이 성공하면 ChatGPT는 응답에 챌린지 값을 그대로 반환합니다.
{
"challenge": "a-single-use-random-value"
}
2xx 응답을 받았는지 확인하고, 반환된 챌린지를 원래 값과 상수 시간으로 비교한 뒤 일치할 때만 전송을 활성화하세요. 구독 요청이 반복될 때마다 챌린지를 보내지 않도록 인증된 주체와 콜백 URL을 기준으로 검증 성공 결과를 일정 기간만 캐시하세요. 검증에 실패하면 JSON-RPC 오류 -32015(CallbackEndpointError)를 반환하고, data.reason에 challenge_failed 또는 timeout처럼 실패 원인을 나타내는 값을 지정하세요.
콜백에는 HTTPS만 허용하세요. 연결 시점에 대상 호스트 이름을 주소로 변환하고 해당 주소를 검증하세요. 검증된 주소에 연결하되 TLS 검증에는 원래 호스트 이름을 사용하세요. 사설 주소와 로컬 주소를 비롯해 공개적으로 접근할 수 없는 모든 주소를 차단하고 리디렉션을 따르지 마세요. 검증 요청과 이벤트 전송 모두에 이 검사를 적용하세요.
이벤트 보내기
새 댓글이 활성 구독의 조건에 맞으면 해당 구독의 콜백 URL로 이벤트 객체 하나를 POST 요청으로 보내세요.
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Can we add the rollout dates to this section?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
고유한 이벤트 ID를 사용하고 재시도할 때도 같은 ID를 유지하세요. timestamp에는 이벤트 발생 시각을 시간대가 포함된 ISO 8601 형식으로 지정하세요. name은 구독한 이벤트 이름과 일치해야 하고 data 객체는 해당 이벤트의 payloadSchema를 따라야 합니다. 애플리케이션 필드는 data 안에 두세요. 최상위 type은 프로토콜 제어 알림을 식별하는 데 사용합니다.
레코드가 크면 요약을 보내고 전체 레코드를 조회할 수 있는 읽기 도구를 제공하세요. 댓글을 비롯해 사용자가 작성한 텍스트는 데이터로 취급하세요. 이벤트 페이로드에 모델의 행동을 지시하는 내용을 추가하지 마세요.
요청에 서명하기
ChatGPT는 Standard Webhooks를 사용해 전송된 요청을 검증합니다. 다음 헤더를 보내세요.
| 헤더 | 값 |
|---|---|
Content-Type |
application/json |
webhook-id |
본문의 eventId와 같은 값 |
webhook-timestamp |
Unix 시간으로 표시한 서명 시각(초 단위) |
webhook-signature |
Standard Webhooks HMAC 서명 |
X-MCP-Subscription-Id |
events/subscribe가 반환한 ID |
Standard Webhooks와 구독의 서명용 비밀 키를 사용해 전송 요청에 서명하세요. 서명에는 이벤트 ID, 서명 시각, 요청 본문의 정확한 바이트열이 사용되므로 본문을 한 번만 직렬화하고 그 바이트열을 그대로 보내세요.
Node.js로 서명된 이벤트 보내기
Standard Webhooks 라이브러리를 설치하세요.
npm install standardwebhooks
webhookFetch 함수를 전달하세요. 이 함수는 fetch와 인터페이스가 같아야 하며, 연결할 때마다 콜백 주소를 검증하고 리디렉션을 차단해야 합니다.
import { Webhook } from "standardwebhooks";
export async function sendEvent(subscription, event, webhookFetch) {
const body = JSON.stringify(event);
if (Buffer.byteLength(body, "utf8") > 256 * 1024) {
throw new Error("Event payload exceeds 256 KiB");
}
const signedAt = new Date();
const signer = new Webhook(subscription.secret);
const response = await webhookFetch(subscription.url, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(10_000),
headers: {
"Content-Type": "application/json",
"webhook-id": event.eventId,
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
"webhook-signature": signer.sign(event.eventId, signedAt, body),
"X-MCP-Subscription-Id": subscription.id,
},
body,
});
return { accepted: response.ok, status: response.status };
}subscription.url과 subscription.secret은 구독 요청의 delivery 객체에 담긴 값으로 설정하세요.
전송 응답 처리하기
2xx 응답은 ChatGPT가 웹훅 요청을 수신했다는 뜻입니다. ChatGPT는 이벤트를 비동기적으로 처리합니다.
요청 하나당 이벤트 하나를 보내고, 요청 본문의 전체 크기는 256 KiB(262,144바이트) 이하로 유지하세요. ChatGPT는 작업의 일괄 처리 설정에 따라 개별 요청으로 수신한 여러 이벤트를 한 번의 작업 실행으로 처리할 수 있습니다.
일시적인 오류가 발생하면 지수 백오프를 적용해 재시도하되 횟수를 제한하세요. 이벤트 ID는 그대로 유지하고 시도할 때마다 새 서명 시각과 서명을 생성하세요. 410 또는 413 응답을 받은 요청은 재시도하지 마세요.
이벤트는 발생 순서와 다르게 도착할 수 있습니다. 쓰기 도구를 반복해서 호출해도 변경 사항이 중복 적용되지 않도록 멱등성을 보장하세요.
구독 관리하기
서버가 정한 구독 유효 기간 동안에는 서버를 재시작해도 구독 상태가 유지되도록 하세요. 구독 기간 중에도 사용자의 접근 권한을 다시 확인하고 권한이 취소되면 전송을 중지하세요.
구독 갱신하기
ChatGPT는 만료가 임박한 구독을 갱신하기 위해 events/subscribe를 refreshBefore 시각 전에 호출합니다. 이때 기존 구독과 동일한 식별 정보와 마지막으로 저장한 커서를 사용합니다. 기존 구독을 업데이트하고 새 만료 시각을 refreshBefore에 담아 반환하세요.
ttlMs가 생략되면 서버의 기본 구독 유효 기간을 사용하세요. 값이 지정되어 있으면 요청한 유효 기간을 밀리초 단위로 나타냅니다. 과도한 갱신 요청을 방지하기 위해 최소 유효 기간을 적용하는 경우를 제외하고는 요청한 기간을 초과하지 마세요.
ttlMs: null은 만료 기한이 없는 구독을 요청하는 값입니다. 이 요청을 수락할 때만 refreshBefore: null을 반환하세요. 그 외에는 만료 시각을 지정해 반환하고 해당 시각이 지나면 전송을 중지하세요.
갱신 요청에 새 서명용 비밀 키가 포함되어 있으면 저장된 키를 교체하세요. 키를 전환하는 짧은 기간에는 기존 키와 새 키로 각각 서명하고, Standard Webhooks 형식에 따라 두 서명을 공백으로 구분하세요.
재전송을 지원하는 이벤트는 구독이 만료되거나 서버가 재시작된 후 요청의 cursor를 사용해 전송을 재개하세요. 구독 응답과 이벤트 페이로드에는 아직 전송을 기다리는 이벤트를 건너뛰지 않는 커서를 반환하세요. 요청한 이력을 더 이상 제공할 수 없으면 truncated: true를 반환하세요. 재전송을 지원하지 않는 이벤트 유형에는 cursor: null을 반환하세요. 이 경우 전송이 중단된 동안 누락된 이벤트는 프로토콜을 통해 복구할 수 없습니다.
구독 중지하기
events/unsubscribe 요청은 구독을 생성할 때 사용한 이벤트 이름, 인자, 콜백 URL로 처리하세요.
{
"jsonrpc": "2.0",
"id": 3,
"method": "events/unsubscribe",
"params": {
"name": "comment.created",
"arguments": {
"document_id": "doc_123"
},
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123"
}
}
}
조건이 일치하는 구독에 대한 이벤트 전송을 중지하고 빈 결과를 반환하세요.
{
"jsonrpc": "2.0",
"id": 3,
"result": {}
}
연결된 계정에 해당 구독을 해지할 권한이 있는지 확인하고, 구독 해지 요청을 멱등하게 처리하세요.
ChatGPT에서 테스트하기
이벤트 메서드와 웹훅 전송을 구현한 후 플러그인을 통해 MCP 서버를 ChatGPT에 연결하고 구독의 전체 수명 주기를 테스트하세요.
-
서버가
events를 지원한다고initialize또는server/discover응답에서 알리는지 확인하세요. 이어서events/list요청을 받고 예상한 이벤트 정의를 반환하는지 확인하세요. -
플러그인 페이지에 이벤트가 도구와 함께 표시되는지 확인하세요. 도구나 이벤트를 변경할 때마다 MCP 서버를 다시 스캔하세요.
플러그인 이벤트. 검색된 이벤트는 플러그인 페이지에 도구와 함께 표시됩니다.
-
ChatGPT 웹에서 Work 모드 채팅을 시작하거나 데스크톱 앱에서 Work와 클라우드를 선택하세요. ChatGPT에 이벤트 하나를 구독해 달라고 요청하고 이벤트가 도착하면 어떤 작업을 수행할지 지정하세요. dot으로도 테스트할 수 있습니다.
-
서버가 예상한 이벤트 이름과 인자가 포함된
events/subscribe요청을 받는지 확인하세요. -
콜백 검증에 성공하고 구독 정보가 저장되는지 확인하세요.
-
앱에서 구독 조건에 맞는 이벤트를 발생시키고 웹훅 전송 요청이
2xx응답을 받는지 확인하세요. -
ChatGPT가 예상한 이벤트 데이터를 수신하고 지시한 대로 대응하는지 확인하세요.
-
필터가 있는 구독에서는 필터 조건에 맞지 않는 이벤트를 발생시키고 해당 구독으로 이벤트가 전송되지 않는지 확인하세요.
-
ChatGPT에서 모니터링을 중지하세요. 서버가
events/unsubscribe요청을 처리하고 전송을 중지하는지 확인하세요.
반복되는 구독 요청, 서버 재시작 전후의 구독 만료와 갱신, 계정 연결 해제, 구독 대상 리소스에 대한 접근 권한 취소, 유효하지 않은 서명, 중복 전송도 테스트하세요. 일괄 처리를 켠 상태와 끈 상태에서 각각 이벤트가 한꺼번에 몰리는 경우도 테스트하세요. 요청한 작업이 원본 앱의 데이터를 변경한다면 그 변경으로 발생한 이벤트가 같은 작업을 반복해서 실행시키지 않는지 확인하세요.
프로토콜 참고 문서: MCP 이벤트 설계 개요.