
MCP 서버를 구축하면서 정보의 소비자로서의 에이전트를 바라볼 수 있는 진귀한 경험을 했습니다. 에이전트가 잘 소화할 수 있는 정보와 맥락의 전달 방식에 대해서 고민했던 지점들을 모아 정리해 보았습니다. MCP 서버의 설계를 고민하시는 개발자 분들께 좋은 인사이트를 드리고픈 마음에 글을 남겨 봅니다.
최근 디자인 시스템 라이브러리(@dotss/ui, NPM)의 MCP(Model Context Protocol) 서버(Streamable HTTP 기반)를 구축했습니다. 표면적으로는 (1) 디자인 토큰과 컴포넌트 정보를, (2) 텍스트 기반의 문자열을 통해 JSON 기반의 UI spec(화면의 설계도-DOM-를 컴포넌트와 토큰을 활용해 구성한 것)을 추출하는 도구를 외부에 제공하는 서버에 불과합니다. 그러나 설계 과정에서 가장 많이 고민했던 것은 기능이 아니라 어떤 정보를 어디까지 허용할 것인가에 대한 것이었습니다(👉 지난 글에서는 이 고민에서 출발해 AUX라는 개념을 제안하기도 했죠).
이 글은 디자인 시스템 MCP 서버를 구축하며 고민했던 설계 전략을 개발자의 시선에서 정리한 기록입니다. 특히 AUX의 관점에서 해석의 여지가 줄이기 위해 MCP의 응답 유형인 리소스(Resource)와 도구(Tool)을 어떻게 구조화했는지를 중심으로 이야기해보려 합니다(서버 설계에 관해서는 다른 좋은 글과 사례에서 충분히 얻어가실 수 있으니까요!).
MCP 서버를 설계하면서 가장 먼저 정리해야 했던 것은 '무엇을 제공할 것인가'가 아니라, '어떤 상태의 정보를 제공할 것인가'에 대한 것들이었습니다. 단순히 디자인 토큰과 컴포넌트 데이터를 외부로 노출하는 것은 어렵지 않은 문제죠. 하지만 그 정보가 에이전트와 사람 모두에게 같은 방식으로 이해되고 같은 결과로 이어지도록 만드는 것은 조금 다른 문제라고 생각했기 때문이었습니다.
@dotss/ui MCP 서버의 목적은 크게 두 가지였기 때문에 가장 중요한 설계 기준은 정보의 해석 가능성을 얼마나 줄일 수 있는가, 즉 일관성이 핵심이 될 수밖에 없었습니다. MCP 서버가 제공하는 정보는 구체적이고, 구조화되고, 정제되어 임의 추론과 해석의 폭을 줄이는 방향으로 구성되어야 한 것이죠.
단순한 문서 형태의 설명이 아니라 에이전트가 별도의 해석 없이도 바로 사용할 수 있는 구조화된 정보여야 했습니다. 사람이 읽기 좋은 문서가 아니라 에이전트가 오해하지 않는 데이터를 만드는 것이 목표였습니다(물론 사람이 읽기 좋은 문서가 에이전트가 읽기 좋은 데이터일 수 있겠죠).
기획 문서는 본질적으로 모호합니다. 같은 문장을 읽더라도 사람마다 다르게 이해할 수 있고 도구 역시 해석의 편차를 가지기 마련이죠. 저는 이 지점을 줄이기 위해 자연어 입력이 들어오더라도 최종적으로는 정해진 스키마를 따르는 구조화된 결과(UI Spec)로 수렴하도록 만드는 것을 목표로 삼았습니다. 이 과정에서 MCP 서버는 단순한 데이터 제공자가 아니라 해석의 범위를 제한하는 가이드처럼 동작하는 것이죠.
여느 다른 MCP 서버의 인터페이스처럼 Resource와 Tool 두 축을 분리하며, Resource는 맥락과 구조를 담는 정적인 정보로, Tool은 의도를 드러내는 최소 단위의 행위로 정의했습니다.
| 명칭 | 기능 | 비고 |
|---|---|---|
| component://list | 모든 컴포넌트 이름 & uri 제공 | |
| component://{name} | 단일 컴포넌트 상세 정보 제공 | |
| token://list | 모든 토큰 그룹 이름 & uri 제공 | |
| token://{name} | 단일 토큰 그룹 상세 정보 제공 | |
| ui-spec://schema | UI Spec의 표준 규격 정의 | UI Spec 추출시 사용 |
| ui-spec://rules | UI Spec의 규칙 가이드 | UI Spec 추출시 사용 |
| ui-spec://examples | UI Spec 예제 샘플 | UI Spec 추출시 사용 |
| ui-spec://examples/{name} | 단일 UI Spec 예제 샘플 | UI Spec 추출시 사용 |
| 명칭(tool name) | 기능 | 비고 |
|---|---|---|
| get-token-list | 모든 디자인 토큰 가져오기 | 토큰명과 uri만 응답 |
| get-component-list | 모든 컴포넌트 나열하기 | 컴포넌트명과 uri만 응답 |
| get-token-detail | 특정 디자인 토큰의 정보 가져오기 | |
| get-component-detail | 특정 컴포넌트의 정보(프로퍼티, 타입 등) 가져오기 | |
| recommend-color | 색상 추천받기 | 키워드 & 점수 기반 |
| recommend-component | 컴포넌트 추천받기 | 키워드 & 점수 기반 |
일부 Tool을 통해 디자인 토큰과 컴포넌트의 정보를 제공합니다. 엄밀하게 말하자면 이러한 도구는 Tool의 본질적인 의미와는 맞지 않습니다. 다만 사용자나 에이전트가 의도적으로 도구를 짚어내 정보를 얻어올 수 있다는 지점에서 수동적인 Resource와는 조금 다른 역할을 할 수 있다고 생각해 추가해 두었습니다(아래에서 조금 더 구체적으로 서술해보겠습니다).
MCP 서버의 Resource는 단순한 데이터 제공 수단이 아니라 에이전트의 해석을 최소화하기 위한 인터페이스라고 생각했습니다. 이러한 관점에서 Resource의 역할은 많은 정보를 주는 것이 아니라, 오해 없이 이해되도록 만드는 것에 가까운 것입니다.
디자인 토큰과 컴포넌트 정보는 본질적으로 복잡한 관계를 가집니다. 단순히 값이나 속성만 나열한다고 해서 이 정보를 활용하기는 어렵습니다. 어떤 컴포넌트와 함께 쓰일 수 있는지, 어떤 상황에서 대체 가능한지, 어떤 제약이 있는지와 같은 맥락 정보가 함께 전달되어야 하기 때문입니다.
그래서 선택한 방식은 비교적 구조가 명확한 .md 기반의 템플릿을 활용해 정보를 강제된 형태로 구조화하는 것이었습니다. 각 토큰과 컴포넌트는 동일한 템플릿을 따르도록 설계했고, 이를 통해 세 지점을 고려하여 템플릿 문서를 구조화했습니다.
특히 컴포넌트 템플릿에서는 상위/하위 컴포넌트, 관련 컴포넌트, 대체 컴포넌트를 명시적으로 연결하여 에이전트가 컴포넌트 간의 관계를 정확하게 이해할 수 있도록 구성했습니다. 예를 들어 <Select /> 컴포넌트 하위에는 <Option /> 컴포넌트를 채워넣어야 한다는 점이나, <Menu /> 컴포넌트를 열어주는 버튼에는 <Button />이 아니라 <MenuButton />을 사용해야 한다는 것처럼 말이죠. 이러한 맥락/관계 데이터는 화면의 UI Spec이라는 설계도를 구성하려는 목적 아래에서 풍부하게 활용될 수 있다는 점에서 의미가 있다고 할 수 있었습니다.
이러한 템플릿 파일의 정형성에서 각 디자인 토큰과 컴포넌트의 구체적인 명세가 파생될 수 있었고, 이 명세는 별도의 에이전트를 통해 자동적으로 생성하는 방식으로 조금 더 편의성을 높여 가져갔습니다.
👉 컴포넌트 템플릿
# {Component Name}
## Component Metadata
{
"name": "{Component Name}", // 컴포넌트 이름
"compound": false, // 컴파운드 패턴 여부
"superComponents": [], // 상위 컴포넌트 이름
"subComponents": [], // 하위 컴포넌트 이름
"relatedComponents": [], // 함께 조합해서 쓰면 좋은 컴포넌트 이름
"alternativeComponents": [], // 대체/대안 관계의 컴포넌트 이름
"tokens": [] // 사용되는 주요 디자인 토큰 키들
}
## Import Statement
- 정확한 import 방법과 사용 예시
## Component Interface
- TypeScript 인터페이스 정의
- Props별 상세 설명과 예시
## Accessibility
- 키보드 인터랙션 규칙 (예: Space/Enter 동작)
- ARIA 속성 가이드 (예: iconOnly일 때 aria-label 필수)
## Design & Tokens
- Variant 별 사용되는 색상/타이포/레이아웃 토큰 요약
- Size 별 높이, 패딩, 타이포 요약 (표나 리스트 형태)
## Composition & Patterns
- 이 컴포넌트를 ${relatedComponents}와(과) 어떻게 조합해서 사용하는지
- 어떤 상황에서 ${alternativeComponents}로 대체하는 것이 좋은지
## Usage Examples
- 기본 사용법부터 고급 사용법
- 실제 서비스에서 사용하는 예시
## Best Practices & Anti-patterns
- 올바른 사용법
- 피해야 할 사용법
템플릿 파일과 컴포넌트 명세 사례는 아래 링크에서 확인 가능합니다.
👉 토큰 템플릿
👉 컴포넌트 템플릿으로 생성한 Button 컴포넌트 명세 사례
구조화된 문서를 만드는 것만큼이나 중요한 것은 이 문서를 에이전트에 어떻게 전달할 것이냐에 대한 문제였습니다. 데이터를 정확하게 이해시키고 해당 데이터를 활용할 수 있는 방법을 충분히 고지하는 것이 중요하기 때문이었죠.
그래서 저는 기본적으로 MCP의 표준 응답 스키마를 따르되, 아래 기준을 추가로 고려하여 응답을 구성했습니다.
// component://list 핸들러 사례
export const componentListResource: Resource<ComponentListResourceResp> = {
name: 'component-list',
description:
'List all available components in @dotss/ui. Provides a JSON array of component objects with name and URI.',
// 각 엔드포인트마다 ctx 메서드를 두어 응답값에 필요한 데이터를 확보한 상태로 응답을 구성하도록 설계
async ctx() {
try {
const componentNames = (await getComponentNames()) || [];
const componentList: ComponentListResourceResp['componentList'] = componentNames.map(
(name) => ({
name,
uri: `component://${name}`
})
);
return { componentList };
} catch (error) {
throw new Error(
`Failed to initialize component list resource: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
},
exec(server, { ctx, name, description }) {
const uri = 'component://list';
const mimeType = MimeType.JSON;
server.registerResource(name, uri, { mimeType, description }, async (uri) => {
// ✅ Resource 응답 ✅
return {
contents: [
{
uri: uri.toString(),
mimeType,
text: JSON.stringify(
{
components: ctx.componentList,
uriFormat: 'component://{ComponentName}',
order: 'alphabetical'
},
null,
2
)
}
]
};
});
}
};
예를 들어 위의 사례에서처럼 component://list는 (1) 응답의 components를 통해 컴포넌트의 리스트라는 핵심 데이터를 전달하는 한편, (2) uriFormat을 통해 각 컴포넌트의 상세 명세를 요청할 수 있는 다음 액션에 대한 힌트를 전달하고, (3) order를 통해 해당 리스트가 어떠한 순서로 구성되었는지를 알려 쉽게 탐색할 수 있도록 구성했습니다.
이런 설계는 UX에서의 affordance의 개념과 유사합니다. 사용자가 버튼을 보면 누를 수 있음을 인지하듯이 에이전트도 응답을 보고 다음에 무엇을 해야 하는지 자연스럽게 결정할 수 있는 것입니다.
UI Spec 생성과 관련된 Resource는 조금 다른 접근이 필요했습니다. 디자인 토큰이나 컴포넌트와 달리 UI Spec은 에이전트의 입장에서 조금 생소하고 추상적인 개념이었기 때문이었습니다.
하나의 큰 문서로 모든 것을 설명하는 대신 정보의 성격에 따라 다음과 같이 리소스를 쪼개 좁은 범위의 구체적인 정보가 전달될 수 있도록 구성했습니다.
무엇보다 ui-spec://scheme 의 구성이 가장 중요하다고 생각했습니다. 이 설계도는 다른 에이전트 도구(예: 피그마 MCP 등)에서 다시 활용되어 화면을 구성하는 데 사용될 것이라 가장 엄격한 구조화가 필요했기 때문이었습니다. 그래서 일반적인 .md파일 대신 표준 스키마의 형태로 구성하여 리소스의 응답이 엄격하게 관리되도록 구성하였습니다.
응답 구조는 아래 링크에서 확인 가능합니다.
👉 표준 스키마로 구성된 ui-spec 응답 구조
Resource가 맥락을 전달하는 역할이라면 Tool은 의도를 드러냅니다. Resource와 달리 Tool은 의도를 가지고 호출되는 것이기 때문에 조금은 다른 접근이 필요했습니다.
MCP 문서에서는 Resource와 Tool을 본질적으로 다른 것으로 설명하고 있습니다.
| MCP에서 설명하는 Resource |
|---|
![]() |
| MCP에서 설명하는 Tools |
|---|
![]() |
다만 실제 MCP 사례들을 살펴보니 많은 경우 Resource 없이 Tool만으로 정보를 제공하는 경우가 대다수였습니다(예: Chakra-ui의 mcp). 아마도 Tool은 호출 시점이 명확하기 때문에 에이전트의 흐름을 제어하기 쉽기 때문일 것입니다.
실제로 MCP 서버를 개발하며 초기에는 MCP의 인터페이스를 Resource만으로 구성하기도 했습니다. 다만 실제 결과물을 확인해보니 에이전트가 탐색을 반복하며 비일관적인 선택을 하는 경우가 빈번했습니다. 호출 시점과 사용자 의도가 명확하지 않으니 데이터의 탐색만 수행하다 사용자의 의도를 적절하게 해석하지 못하고 서로 다른 결과를 내뱉었던 것이죠(의도가 모호하니 제한된 경로 안에서만 데이터를 탐색하지 않았겠죠). 결국 데이터의 형태 측면에서는 Resource의 형태가 적절하지만, 실질적인 동작에서는 Tool 형태가 적절할 수 있다고 생각한 지점이었습니다.
🤐 믿거나 말거나
GPT에 이 사안에 대해서 질문하니, 현실적으로 대다수의 툴(예: Cursor, OpenCode 등)이 Resource보다는 Tool 활용에 더 특화된 설계라서 Tool만을 사용하는 MCP 서버가 많은 것이라고도 합디다.
그래서 저는 기본적으로 Resource와 Tool의 표준 의미를 준수하되, 일부 Tool에 한해서는 실제 Resource를 래핑하는 형태로 제공하는 것으로 타협을 보았습니다. 예를 들어 get-component-detail은 component://{name}의 Resource와 내부 로직이 동일합니다.
엄밀히 보면 이는 Tool의 순수한 형태는 아닙니다만 에이전트 혹은 사용자의 의도가 분명하게 드러난다는 점에서 Resource와는 다른 UX를 제공할 수 있다고 판단한 것입니다.
🚨 주의할 점
제 결정이 "Tool이 항상 더 낫다"라는 의도로 읽히지 않아야 합니다.
Tool 인터페이스는 호출 시점과 의도를 명시적으로 드러내기 때문에 에이전트의 탐색 경로를 제한하고 결과의 분산을 줄이는 데에는 강점이 있습니다. 반면 이 과정에서 탐색의 자유도가 줄어들어 새로운 조합이나 확장적인 사용에는 제약이 생길 수 있습니다.Resource 인터페이스는 반대로 탐색 가능한 정보의 범위와 확장성에는 강점이 있지만 호출 시점이 명확하지 않기 때문에 에이전트가 선택 기준 없이 여러 경로를 시도하며 결과가 수렴하지 않는 문제가 발생할 수 있습니다.
따라서 결과의 일관성과 제어가 중요한 경우에는 Tool 중심의 설계가 유리하고, 탐색과 조합의 자유도가 중요한 경우에는 Resource 중심의 설계가 더 적합할 수 있습니다.
Resource와 다르게 Tool은 제공하는 기능과 정보의 범위가 최소화되어야 했습니다. 분명 Resource의 경우에는 더 넓은 맥락을 전달하는 것이 유리한 경우가 있었습니다(위에서 살펴본 component://list의 사례가 그렇죠!). 이와 다르게 Tool은 그 의도가 비교적 명확하기 때문에 각 Tool은 정확하게 단일 기능을 수행하는 것이 에이전트의 판단을 저해하지 않을 수 있었습니다.
그렇기 때문에 Tool은 입력의 모호성이 증가하는 지점에서 분리되어야 했습니다. 이 지점을 가장 중요하게 고민했던 Tool이 바로 recommend-color와 recommend-component 이었습니다. 두 Tool은 명확하게 색상값만 추천하거나 컴포넌트만 추천합니다(물론 한 개의 결과만 제공하는 것은 아니긴 합니다).
물론 이 과정에서 추천 알고리즘은 명확하게 구성했습니다. 키워드와 연관 키워드 포함 여부에 대한 점수를 바탕으로 최적의 색상값과 컴포넌트를 추천한 것이죠. 응답값도 위 Resource의 사례에서처럼 명료한 형태로 제공하여 에이전트가 잘 소화할 수 있도록 했습니다.
// recommend-color의 응답 형태
recommendationsWithScore.push({
tokenGroup: tokenName,
uri: `token://${tokenName}`,
name: metadata.name,
category: metadata.category,
type: metadata.type,
path: metadata.path,
reason: match.reason,
score: match.score
});
전통적인 API나 라이브러리들은 명확한 입력과 출력, 그리고 검증 가능한 결과를 가지기 마련입니다. 하지만 MCP 서버는 그 위에 올라가는 에이전트의 해석 과정을 전제로 동작한다는 점에서 조금 다른 성질을 가지고 있었습니다.
MCPEvals과 같은 도구들이 등장하고 있지만 아직까지는 '이 MCP 서버가 좋은가?'를 명확하게 판단할 수 있는 기준은 부족한 상태인 것 같습니다. 이유는 어찌보면 단순할지도 모르겠습니다. MCP를 이용한 에이전트의 최종 결과물은 항상 에이전트를 통해 생성되고 우리는 그 중간 과정을 관찰할 수 없기 때문이죠.
| 개발 도구 MCP Inspector로 살펴본 @dotss/ui MCP 서버 |
|---|
![]() |
![]() |
특히 UI Spec과 같이 구조화된 결과를 기대하는 경우에 이 한계를 뼈져리게 느꼈습니다. 같은 기획물에서 같은 UI Spec(적어도 매우 유사한 형태의 결과물)을 바랬지만, 실제 출력이 완전히 동일하게 수렴하는 경우가 거의 없었습니다. 수많은 예제를 제공하고, 정보의 구조를 아무리 정제하더라도 에이전트의 내부 상태나 시점에 따라서 결과가 조금씩 달랐죠.
그래서 앞으로는 MCP 서버는 앞으로 이러한 방향으로 나아가야 할지도 모르겠다는 생각을 합니다.
MCP 서버는 단일 결과의 정확도가 아니라 결과의 분산 관점에서 얼마나 일관성있는 결과물을 내뱉는지를 기준으로 평가되어야 합니다.
Resource만으로는 해석의 여지를 줄이기 어렵다는 생각입니다. 결국 사용자나 에이전트의 의도가 여실히 드러나는 Tool이 MCP 인터페이스의 중심이 되어야 하지 않나 하는 생각입니다.
최근 Geek News의 글들을 보면 웹 MCP가 부상하기도 하는 한편, MCP라는 프로토콜 자체에 대한 회의적인 글들도 만나볼 수 있는데요. 어느 방향이 되었던 이러한 논의들이 활발하게 이루어졌으면 좋겠다는 생각으로 글을 마칩니다. 긴 글 읽어주셔서 감사합니다.