서버에 연결하기
서버에 연결하기 (Connect to a server)
클라이언트는 한 번에 한 서버에 연결 하나를 유지해요. Client 를 만들고 전송(transport) 을 고른 뒤 connect() 를 부르면 됩니다.
클라이언트를 만들고 HTTP로 연결하기
Client 는 이름과 버전을 받고, StreamableHTTPClientTransport 는 서버의 MCP 엔드포인트 URL을 받습니다.
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const client = new Client({ name: 'my-client', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
await client.connect(transport);
connect() 는 initialize 핸드셰이크를 돌리고, 끝나면 resolve 돼요. 이제 클라이언트는 협상된 프로토콜 버전, 서버의 기능(capabilities), 그리고 지시(instructions)를 갖고 있습니다.
v1에서 오셨나요?
Client와 전송 클래스는 이름이 그대로예요. 바뀐 것은 import 경로뿐으로,@modelcontextprotocol/client와 그/stdio서브패스로 옮겨졌죠. codemod 를 실행한 뒤 업그레이드 가이드를 보면 됩니다.
stdio로 로컬 프로세스에 연결하기
자식 프로세스로 실행하는 서버라면 전송만 바꾸면 돼요. @modelcontextprotocol/client/stdio 에서 임포트하는 StdioClientTransport 가 명령을 띄우고, 그 stdin/stdout 위에서 JSON-RPC로 대화합니다.
const client = new Client({ name: 'my-client', version: '1.0.0' });
const transport = new StdioClientTransport({ command: 'node', args: ['server.js'] });
await client.connect(transport);
server.js 는 여러분 프로세스의 자식으로 실행돼요. close() 는 순서대로 정리합니다 — stdin 을 닫고, SIGTERM 을 보내고, 마지막에 SIGKILL 을 보내죠.
::: tip
InMemoryTransport.createLinkedPair() 가 세 번째 전송이에요. 한 프로세스 안에서 Client 와 McpServer 를 네트워크도 자식 프로세스도 없이 이어 줍니다. 서버 테스트하기 가 이 위에서 만들어졌어요.
:::
Streamable HTTP보다 오래된 서버는 SSE로 폴백하기
SSE 전용 서버는 Streamable HTTP 대신 옛 HTTP+SSE 전송으로 말합니다. StreamableHTTPClientTransport 를 먼저 시도하고, 실패하면 새 Client 에 SSEClientTransport 로 다시 시도해 보세요.
try {
const client = new Client({ name: 'my-client', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
return client;
} catch {
const client = new Client({ name: 'my-client', version: '1.0.0' });
await client.connect(new SSEClientTransport(new URL(url)));
return client;
}
어느 분기가 돌아오든 그 뒤로 Client 는 똑같이 동작해요 — 아무것도 전송에 의존하지 않습니다.
::: info
ClientOptions 의 versionNegotiation 은 connect() 가 어떤 프로토콜 리비전을 협상할지 정해요 — 프로토콜 버전 을 보세요.
:::
연결 시점에 서버가 알려준 것 읽기
세 개의 접근자(accessor)가 핸드셰이크 중 서버가 선언한 값을 돌려줍니다. 셋 다 connect() 가 resolve 되기 전에는 undefined 를 반환해요.
console.log(client.getServerVersion());
console.log(client.getServerCapabilities());
console.log(client.getInstructions());
travel 이라는 서버에 연결되었다고 해볼게요. 도구 하나를 등록하고 instructions 를 설정했다면 이렇게 출력됩니다.
{ name: 'travel', version: '2.1.0' }
{ tools: { listChanged: true } }
Call list-trips before book-trip. Dates are ISO 8601.
기능 객체가 다음 페이지 의 모든 동작에 문을 열어 줘요 — 서버가 광고한 것만 요청해야 합니다. getInstructions() 는 서버가 모델에게 주는 사용 안내니까 시스템 프롬프트에 넣어 주면 돼요.
네 번째 접근자 getDiscoverResult() 는 연결 시점에 프로토콜 시대(era)를 구분해 줍니다. 값이 있으면 그것은 현대 시대의 DiscoverResult 로, JSON.stringify 로 영속화할 수 있고 다음 연결에서 prior: { kind: 'modern', discover } 로 써서 probe 를 건너뛸 수 있어요. 연결된 클라이언트에 값이 없으면 그 시대는 legacy 입니다. 이 페이지의 클라이언트는 기본 legacy 핸드셰이크를 썼기 때문에:
// The default mode ran the legacy initialize handshake — no DiscoverResult.
console.log(client.getDiscoverResult());
undefined
versionNegotiation: { mode: 'auto' } 아래에서 2026 시대 서버를 만나면 광고를 돌려줍니다. 캐시된 판정 모양은 프로토콜 버전 에, 호스트 쪽 전체 루프는 discovery 판정 캐싱 에 있어요.
깔끔하게 연결 끊기
Streamable HTTP 위에서는 서버 쪽 세션을 끝낸 뒤 클라이언트를 닫습니다.
await transport.terminateSession();
await client.close();
close() 는 전송을 정리하고 아직 진행 중인 모든 요청을 CONNECTION_CLOSED 오류로 거부해요. terminateSession() 은 서버가 세션 ID를 발급하지 않았다면 아무것도 보내지 않고 그냥 돌아옵니다. 다른 전송에서는 close() 하나만으로 정리가 끝나요.
요약
new Client({ name, version })에 전송과connect()를 더하면 설정이 끝나고,connect()가initialize핸드셰이크를 돌려요.StreamableHTTPClientTransport는 원격 서버에,@modelcontextprotocol/client/stdio의StdioClientTransport는 로컬 서버에 붙고,SSEClientTransport는 SSE 전용 서버용 폴백이에요.InMemoryTransport.createLinkedPair()는 한 프로세스 안에서 클라이언트와 서버를 이어 줍니다.connect()후에는getServerVersion(),getServerCapabilities(),getInstructions()가 서버가 선언한 값을 돌려주고,getDiscoverResult()가 시대를 구분해 줍니다(있으면 modern, 없으면 legacy).close()가 전송을 정리하고 진행 중인 요청을 거부해요.- 프로토콜 리비전 차이는 여기 말고 프로토콜 버전 페이지에서 다룹니다.