첫 번째 MCP 클라이언트 만들기
첫 번째 MCP 클라이언트 만들기 (Build your first client)
이번엔 서버를 띄우고, 그 도구 목록을 보여 주고, 도구를 호출하는 프로그램 — 즉 MCP 클라이언트 — 를 만들어 볼게요. 대상은 첫 번째 서버 만들기에서 만든 기상 서버예요.
서버에 연결하기
기상 프로젝트에 클라이언트 패키지를 추가합니다. 이 패키지는 @modelcontextprotocol/server 와 따로 배포돼요.
npm install @modelcontextprotocol/client
src/client.ts 를 만드세요. Client 하나에 전송 하나를 더하면 그게 완전한 MCP 클라이언트입니다.
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const client = new Client({ name: 'my-first-client', version: '1.0.0' });
const transport = new StdioClientTransport({
command: 'npx',
args: ['tsx', 'src/index.ts']
});
await client.connect(transport);
connect() 는 npx tsx src/index.ts 를 자식 프로세스로 띄우고, 그 stdin/stdout 위에서 JSON-RPC로 대화한 뒤 initialize 핸드셰이크를 마칩니다. 이 시점부터 클라이언트가 그 프로세스를 소유해요. 프로세스는 전송이 살아있는 동안 정확히 그만큼만 살아갑니다.
::: tip
src/index.ts 를 직접 시작하면 안 돼요 — connect() 가 대신 합니다. 여기서 spawn npx ENOENT 오류가 난다면 command 가 여러분 PATH 에 있는 실행 파일이 아니라는 뜻이에요.
:::
stdio는 로컬 호스트가 쓰는 전송이고, 원격 서버용 HTTP는 서버에 연결하기 에서 다룹니다.
서버의 도구 목록 보기
listTools 는 서버가 등록한 모든 도구를, 각 도구 인자의 JSON Schema와 함께 반환해요.
const { tools } = await client.listTools();
for (const tool of tools) {
console.log(tool.name, '—', tool.description);
}
지금까지 만든 것을 실행해 보세요 — 프로젝트 루트에서 npx tsx src/client.ts 를 입력하면 됩니다. 첫 줄은 자식 프로세스의 stderr에서 전달된 서버 배너이고, 두 번째 줄이 여러분의 반복문 출력이에요.
weather MCP server running on stdio
get-alerts — Get the active weather alerts for a US state
이 스크립트는 스스로 끝나지 않아요. 클라이언트가 아직 살아 있는 서버 프로세스를 소유하고 있으니까요. 지금은 Ctrl+C 로 멈추고, 연결 닫기 에서 제대로 끝내는 법을 볼게요.
도구 호출하기
callTool 은 도구 이름과, 그 inputSchema 를 만족해야 하는 arguments 객체를 받습니다.
const result = await client.callTool({ name: 'get-alerts', arguments: { state: 'CA' } });
for (const block of result.content) {
if (block.type === 'text') console.log(block.text);
}
도구 결과는 타입이 정해진 content 블록들의 목록이고, get-alerts 는 text 블록 하나를 돌려줘요. 그 텍스트는 국립기상청(National Weather Service)이 돌려준 실제 응답입니다 — 캘리포니아의 활성 특보마다 헤드라인 하나씩, 또는 없을 땐 No active alerts for CA. 라는 문장이죠. 그래서 여러분의 출력은 다른 사람의 것과 다를 수밖에 없어요.
핸들러가 던지는 예외나 inputSchema 가 거부하는 인자도, isError: true 가 붙은 똑같은 모양으로 돌아옵니다. 반면 서버가 등록하지 않은 도구 이름은 프로토콜 수준의 실패라서 await callTool 밖으로 throw 돼요 — 경계를 긋는 기준은 오류 에 있어요.
::: tip
인자를 { state: 'California' } 로 바꾸면 SDK가 핸들러(그 안의 네트워크 요청)가 실행되기 전에 거부합니다.
Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters
이 거부는 평범한 isError: true 결과라, 모델은 메시지를 읽고 맞는 인자로 다시 시도해요.
:::
리소스 추가하고 읽기
기상 서버는 아직 리소스를 등록하지 않았어요. 리소스는 클라이언트가 URI로 읽는 데이터이고, 도구는 클라이언트가 호출하는 액션이죠. src/index.ts 에서 return server 줄 위에 리소스 하나를 등록합니다.
server.registerResource('about', 'weather://about', { title: 'About this server', mimeType: 'text/plain' }, async uri => ({
contents: [{ uri: uri.href, text: 'Alert data comes from the US National Weather Service.' }]
}));
읽기 핸들러는 contents 를 반환해요 — 목록인 이유는 한 번의 읽기가 텍스트·바이너리 여러 부분을 돌려줄 수 있기 때문이죠. 리소스 에서 템플릿, 바이너리 내용, 구독을 다룹니다.
src/client.ts 로 돌아와서, 리소스를 목록으로 확인하고 새 리소스를 uri 로 읽어 볼게요.
const { resources } = await client.listResources();
console.log(resources);
const { contents } = await client.readResource({ uri: 'weather://about' });
console.log(contents);
다시 실행해 보세요. 앞서 봤던 줄들 뒤에 새 로그 두 개가 이렇게 나옵니다.
[
{
name: 'about',
title: 'About this server',
uri: 'weather://about',
mimeType: 'text/plain'
}
]
[
{
uri: 'weather://about',
text: 'Alert data comes from the US National Weather Service.'
}
]
listResources 는 여러분이 등록한 메타데이터를 광고하고, readResource 는 핸들러의 contents 를 그대로 돌려줍니다.
연결 닫기
파일을 close 로 끝맺음합니다.
await client.close();
close() 는 띄웠던 서버의 stdin을 끝내고, 스스로 끝나지 않으면 프로세스를 종료합니다. 완성된 스크립트를 한 번 더 실행해 보세요 — 위 내용을 모두 출력하고, Ctrl+C 없이 종료될 거예요.
::: tip
connect 와 close 사이에 예외가 던져질 수 있는 클라이언트라면 close() 를 finally 블록에 넣으세요. 그렇지 않으면 크래시가 나도 서버 프로세스가 계속 살아남습니다.
:::
도구 목록을 모델에 넘기기
이 페이지의 어디에도 모델을 호출하는 코드는 없어요. 넘겨주는 지점은 바로 listTools() 입니다. 각 항목의 name, description, inputSchema — 평범한 JSON Schema — 가 도구 호출( tool-calling ) LLM API가 받는 도구 정의에 일대일로 대응해요. 그 목록을 담아 대화를 보내고, 모델이 도구 호출을 돌려주면 그 name 과 arguments 를 그대로 callTool 에 넘기고 result.content 를 도구 결과로 붙이면 됩니다.
호스트 — 모델이 들어 있는 애플리케이션 — 는 자기만의 클라이언트를 통해 그 루프를 대신 돌려줘요. 실제 호스트에 붙이기 는 클라이언트 코드 없이도 이 기상 서버를 VS Code, Claude Code, Cursor 에 등록해 주고, SDK 저장소의 examples/cli-client 는 이 페이지의 호출만으로 만든 완전하고 공급자 중립적인 호스트입니다.
요약
Client하나에 전송 하나를 더하면 완전한 MCP 클라이언트가 되고,connect()가 initialize 핸드셰이크를 실행해요.StdioClientTransport가 서버 프로세스를 띄우고 소유합니다 — 직접 시작하지 마세요.listTools,callTool,listResources,readResource가 클라이언트의 동작이고, 각각 타입이 정해진 결과를 반환해요.- 실패한 핸들러나 거부된 인자는
isError: true가 붙은 평범한 결과로 돌아옵니다. close()가 전송과 띄웠던 프로세스를 정리해요.- 모델은
listTools()출력 —name,description,inputSchema— 을 그대로 사용합니다.