
- WebMCP와 MCP는 실행 위치가 다릅니다
- 2026년 9월 현재 지원 상태와 전제 조건
- 웹 개발자는 무엇을 구현해야 할까?
- 읽기 전용 상품 검색 도구 전체 코드
- 실행과 확인 방법
- 실서비스에서 확인할 보안과 한계
WebMCP와 MCP는 실행 위치가 다릅니다
MCP(Model Context Protocol)는 AI 호스트 안의 클라이언트와 외부 MCP 서버가 연결되는 구조입니다. 서버는 도구뿐 아니라 리소스와 프롬프트도 제공할 수 있고, 기본 메시지는 JSON-RPC 2.0을 사용합니다. 사용자가 특정 웹페이지를 열어 두지 않아도 서버 연결이 유지되는 구성이 가능합니다.
WebMCP는 현재 탭의 페이지가 브라우저에 기능을 도구로 알리는 브라우저 API 제안입니다. 도구는 페이지가 열려 있을 때만 존재하며 현재 DOM, 세션, 페이지 상태를 재사용할 수 있습니다. Chrome 공식 비교 문서도 WebMCP를 MCP의 JavaScript 구현이나 대체품이 아니라 “MCP에서 영감을 받은” 프런트엔드 API로 설명합니다.
| 구분 | MCP | WebMCP |
|---|---|---|
| 실행 경계 | AI 호스트·클라이언트와 MCP 서버 | 브라우저 에이전트와 현재 웹페이지 |
| 수명 | 서버·연결 정책에 따라 지속 | 탭과 페이지에 묶인 일시적 도구 |
| 주요 대상 | 외부 데이터, 백그라운드 작업, 여러 AI 클라이언트 | 현재 UI, DOM, 로그인된 페이지 상태 |
| 개발 단위 | MCP 서버와 전송·인증 구성 | JavaScript 등록 또는 HTML 폼 주석 |
| 관계 | 서로 대체하지 않으며 한 서비스에서 함께 사용할 수 있음 | |

2026년 9월 현재 지원 상태와 전제 조건
2026년 9월 10일 확인 기준으로 WebMCP는 확정된 범용 웹 표준이 아니라 변경 가능한 제안입니다. Chrome 공식 안내는 Chrome 149부터 Origin Trial에 참여할 수 있다고 설명하며, 로컬 개발에서는 chrome://flags/#enable-webmcp-testing 플래그를 안내합니다. 따라서 모든 Chrome 사용자에게 기본 제공된다고 가정해서는 안 됩니다.
최신 Chrome Imperative API 문서에서 확인되는 등록 진입점은 document.modelContext.registerTool()입니다. 등록 객체에는 name, description, inputSchema, execute를 넣고, 선택 사항인 annotations.readOnlyHint로 읽기 전용임을 표시할 수 있습니다. 공식 구현 현황에는 Chrome 149 Origin Trial, Brave Leo의 실험 지원, ChatGPT Desktop 지원이 적혀 있으나 제품별 버전·설정·에이전트 연결 여부가 다르므로 배포 전 대상 환경에서 다시 확인해야 합니다.
WebMCP는 origin-isolated 문서에서만 제공됩니다. document.domain을 켜 origin isolation을 해제한 문서에서는 API가 비활성화됩니다. 두 API는 tools Permissions Policy의 제어를 받고 기본 허용 범위는 self입니다. 교차 출처 iframe에 도구 권한을 위임하려면 별도 검토가 필요합니다.
웹 개발자는 무엇을 구현해야 할까?
먼저 기능이 어느 경계에 있어야 하는지 정합니다. 사용자가 페이지를 보지 않아도 재고 조회나 사내 검색이 필요하고 여러 AI 클라이언트에서 호출해야 한다면 MCP 서버가 맞습니다. 반대로 사용자가 보고 있는 필터, 선택된 상품, 현재 편집 상태처럼 탭의 UI와 함께 움직여야 한다면 WebMCP가 맞습니다.
- 페이지의 기존 함수를 재사용합니다. 에이전트 전용으로 업무 로직을 복제하지 말고 사람이 누르는 검색 버튼과 WebMCP 도구가 같은 검색 함수를 호출하게 합니다.
- 작고 명확한 도구를 등록합니다. 이 예제는 공개 상품 목록을 읽는
search_products하나만 노출합니다. - 기능 감지는 필수입니다.
document.modelContext와registerTool의 존재를 확인하고, 없거나 등록이 실패해도 HTML 폼을 그대로 사용할 수 있어야 합니다. - 부작용을 실제와 일치시킵니다. 이 예제는 저장·결제·삭제가 없으므로
readOnlyHint: true입니다. 이 힌트는 권한 검사를 대신하지 않습니다.
읽기 전용 상품 검색 도구 전체 코드
아래 두 파일을 같은 폴더에 저장합니다. 첫 파일은 검색과 도구 등록을 담당하고, 두 번째 파일은 일반 HTML 폼과 결과 UI를 제공합니다. 에이전트와 사용자가 같은 searchProducts()와 renderResults()를 사용하므로 화면 상태가 갈라지지 않습니다.
webmcp-demo-core.mjs
export const PRODUCTS = Object.freeze([
Object.freeze({
id: 'desk-lamp',
name: '무광 데스크 램프',
category: '조명',
description: '눈부심을 줄인 책상용 LED 조명',
price: 39000,
}),
Object.freeze({
id: 'bottle',
name: '스테인리스 보틀',
category: '아웃도어',
description: '휴대하기 쉬운 500mL 보온·보냉 물병',
price: 28000,
}),
Object.freeze({
id: 'timer',
name: '마그네틱 타이머',
category: '주방',
description: '큰 숫자로 남은 시간을 보여 주는 타이머',
price: 17000,
}),
]);
export function searchProducts(products, query, maxResults = 5) {
const normalized = String(query ?? '').trim().toLocaleLowerCase('ko-KR');
const matches = normalized
? products.filter((product) => (
`${product.name} ${product.category} ${product.description}`
.toLocaleLowerCase('ko-KR')
.includes(normalized)
))
: products;
return matches.slice(0, Math.max(0, maxResults)).map((product) => ({ ...product }));
}
export function createProductSearchTool(products, renderResults) {
return {
name: 'search_products',
description: '현재 페이지의 공개 상품 목록에서 검색어와 일치하는 상품을 찾습니다. 데이터는 변경하지 않습니다.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '상품 이름, 분류 또는 설명에서 찾을 검색어',
},
},
required: ['query'],
additionalProperties: false,
},
annotations: {
readOnlyHint: true,
untrustedContentHint: false,
consequentialHint: false,
},
async execute({ query }) {
const results = searchProducts(products, query);
renderResults?.(results, String(query ?? ''));
return { count: results.length, products: results };
},
};
}
export async function registerProductSearch(documentLike, products, renderResults) {
const registerTool = documentLike?.modelContext?.registerTool;
if (typeof registerTool !== 'function') {
return { supported: false, registered: false };
}
await registerTool.call(
documentLike.modelContext,
createProductSearchTool(products, renderResults),
);
return { supported: true, registered: true };
}
webmcp-demo.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebMCP 읽기 전용 상품 검색 데모</title>
<style>
body { max-width: 720px; margin: 40px auto; padding: 0 20px; color: #171717; font: 16px/1.6 system-ui, sans-serif; }
form { display: flex; gap: 8px; margin: 20px 0; }
input { flex: 1; min-width: 0; padding: 10px; border: 1px solid #a3a3a3; }
button { padding: 10px 16px; border: 1px solid #171717; background: #171717; color: #fff; }
li { margin: 12px 0; }
#status { color: #525252; }
</style>
</head>
<body>
<main>
<h1>상품 검색</h1>
<p id="status" role="status">브라우저 기능을 확인하고 있습니다.</p>
<form id="search-form" role="search">
<label for="query">검색어</label>
<input id="query" name="query" type="search" placeholder="예: 조명" required>
<button type="submit">검색</button>
</form>
<p id="result-summary" aria-live="polite"></p>
<ul id="results"></ul>
</main>
<script type="module">
import {
PRODUCTS,
registerProductSearch,
searchProducts,
} from './webmcp-demo-core.mjs';
const form = document.querySelector('#search-form');
const queryInput = document.querySelector('#query');
const status = document.querySelector('#status');
const summary = document.querySelector('#result-summary');
const list = document.querySelector('#results');
function renderResults(products, query) {
list.replaceChildren(...products.map((product) => {
const item = document.createElement('li');
const name = document.createElement('strong');
name.textContent = product.name;
item.append(name, ` — ${product.category}, ${product.price.toLocaleString('ko-KR')}원`);
return item;
}));
summary.textContent = `“${query}” 검색 결과 ${products.length}개`;
}
form.addEventListener('submit', (event) => {
event.preventDefault();
renderResults(searchProducts(PRODUCTS, queryInput.value), queryInput.value);
});
try {
const result = await registerProductSearch(document, PRODUCTS, renderResults);
status.textContent = result.registered
? 'WebMCP 도구가 등록되었습니다. 아래 검색창도 그대로 사용할 수 있습니다.'
: '이 브라우저에서는 WebMCP를 사용할 수 없습니다. 아래 일반 검색 기능은 정상 동작합니다.';
} catch (error) {
console.error('WebMCP 도구 등록 실패:', error);
status.textContent = 'WebMCP 도구를 등록하지 못했습니다. 아래 일반 검색 기능은 정상 동작합니다.';
}
</script>
</body>
</html>
DOM에 문자열을 넣을 때 innerHTML 대신 textContent와 노드 생성을 사용해 상품 문자열이 HTML로 실행되지 않게 했습니다.
실행과 확인 방법
Python 3가 설치된 환경에서 두 파일을 같은 폴더에 저장하고 그 폴더에서 터미널을 엽니다. ES 모듈을 로드할 수 있도록 운영체제에 맞는 명령으로 로컬 HTTP 서버를 실행합니다. 127.0.0.1에만 바인딩해 같은 컴퓨터에서만 접속하도록 제한합니다.
# Windows
py -m http.server 8000 --bind 127.0.0.1
# macOS 또는 Linux
python3 -m http.server 8000 --bind 127.0.0.1
그다음 http://localhost:8000/webmcp-demo.html을 엽니다. 기본 상태에서 “조명”을 검색하면 “무광 데스크 램프” 1개가 표시됩니다. WebMCP 미지원 환경에서는 상태 문구가 “사용할 수 없습니다”로 바뀌지만 같은 검색 폼은 계속 작동합니다.
작성 과정에서는 별도 Node 내장 테스트로 검색어 정규화, 결과 제한, 읽기 전용 힌트, 미지원 폴백, 등록된 도구의 실행 결과를 확인했습니다. 결과는 테스트 5개 통과, 실패 0개였습니다. 이 테스트의 modelContext는 최소 mock이므로 실제 Chrome이 도구를 발견하고 에이전트가 호출하는 과정까지 검증한 것은 아닙니다.
실서비스에서 확인할 보안과 한계
- 도구 힌트는 보안 경계가 아닙니다. 읽기 전용 여부와 무관하게 서버는 인증·인가·입력 검증을 직접 해야 합니다.
- 민감 정보는 결과에 넣지 않습니다. 현재 페이지에 보인다는 이유만으로 계정 정보나 비공개 데이터를 도구 결과에 복사해서는 안 됩니다.
- 쓰기 작업은 별도로 설계합니다. 결제·예약·삭제 같은 작업은 이 예제를 확장해 바로 붙이지 말고 사용자 확인, 중복 실행 방지, 취소·오류 처리를 먼저 정해야 합니다.
- 일반 UI를 기준선으로 둡니다. WebMCP 등록 실패가 검색·구매·문의 같은 핵심 페이지 기능을 막지 않도록 점진적 향상으로 구현합니다.
- 대상 브라우저에서 다시 시험합니다. 이 글의 코드는 Node 테스트와 일반 HTML UI까지 확인했으며, Chrome 플래그나 Origin Trial을 켠 실제 WebMCP 에이전트 호출은 현장에서 검증하지 못했습니다.
공식 출처(2026년 9월 10일 확인): Chrome WebMCP 개요, Chrome의 WebMCP와 MCP 비교, Chrome Imperative API, WebMCP 구현 현황, MCP 공식 명세.
함께 읽기: AI 도구 안전 사용 가이드에서 데이터 공개 범위를 점검하고, AEO 프로젝트 구현 예제에서 웹 콘텐츠를 기계가 읽을 수 있게 구성하는 별도의 접근을 확인하세요. WebMCP 도구 등록은 검색 노출 최적화와 같은 작업이 아닙니다.
이 글이 도움이 되었나요?
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.