Go SDK로 MCP 서버 기능 만들기
Go SDK로 MCP 서버 기능 만들기
MCP 서버가 클라이언트에 제공할 수 있는 기능들 — 프롬프트, 리소스, 도구 — 을 Go SDK 기준으로 하나씩 살펴볼게요. 각 기능은 서버에 등록하면 초기화 핸드셰이크에서 해당 역량이 자동으로 광고됩니다.
프롬프트 (Prompts)
MCP 서버는 LLM 프롬프트 템플릿(간단히 프롬프트)을 클라이언트에 제공할 수 있어요. 모든 프롬프트는 그것을 식별하는 필수 name과, 문자열로 된 이름 붙은 인자 집합을 가져요.
클라이언트 쪽: 서버의 프롬프트를 나열하려면 ClientSession.Prompts 이터레이터나 저수준 ClientSession.ListPrompts를 사용해요. 프롬프트 목록의 변경을 통지받으려면 ClientOptions.PromptListChangedHandler를 설정하세요. 이름으로 프롬프트를 가져오려면 ClientSession.GetPrompt를 호출해요.
서버 쪽: Server.AddPrompt로 핸들러와 함께 프롬프트를 추가해요. 서버가 클라이언트에 연결되기 전에 프롬프트가 추가되거나 ServerOptions.HasPrompts가 명시적으로 설정되면 서버는 prompts 역량을 가지게 돼요. 프롬프트가 추가되면 이미 연결된 클라이언트에는 notifications/prompts/list_changed 알림이 전송돼요.
func Example_prompts() {
ctx := context.Background()
promptHandler := func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
return &mcp.GetPromptResult{
Description: "Hi prompt",
Messages: []*mcp.PromptMessage{
{
Role: "user",
Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]},
},
},
}, nil
}
// Create a server with a single prompt.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
prompt := &mcp.Prompt{
Name: "greet",
Arguments: []*mcp.PromptArgument{
{
Name: "name",
Description: "the name of the person to greet",
Required: true,
},
},
}
s.AddPrompt(prompt, promptHandler)
// Create a client.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
// Connect the server and client.
t1, t2 := mcp.NewInMemoryTransports()
if _, err := s.Connect(ctx, t1, nil); err != nil {
log.Fatal(err)
}
cs, err := c.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
// List the prompts.
for p, err := range cs.Prompts(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(p.Name)
}
// Get the prompt.
res, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{
Name: "greet",
Arguments: map[string]string{"name": "Pat"},
})
if err != nil {
log.Fatal(err)
}
for _, msg := range res.Messages {
fmt.Println(msg.Role, msg.Content.(*mcp.TextContent).Text)
}
// Output:
// greet
// user Say hi to Pat
}
프롬프트 메시지 콘텐츠
PromptMessage는 두 필드를 가져요: "user" 또는 "assistant"인 Role과 단일 Content. 이는 도구 결과가 쓰는 것과 같은 인터페이스라서(아래 도구 결과 콘텐츠 참고), 프롬프트 메시지는 텍스트뿐 아니라 이미지, 오디오, 리소스 콘텐츠도 담을 수 있어요.
미디어 자체만으로는 모델이 그것으로 무엇을 해야 하는지 알려주지 못하므로, 반드시 텍스트 메시지를 함께 보내야 해요. 리소스를 임베드하면 그 내용이 메시지에 직접 들어가서 클라이언트가 별도로 resources/read를 호출할 필요가 없어져요. ResourceContents.URI는 서버가 등록한 리소스와 대조 검사되지 않는다는 점 참고하세요 — 어떤 리소스에서 온 것인지를 기록하는 필드라서, 실제 리소스가 있으면 그 URI를 사용해요.
리소스 (Resources)
MCP 용어에서 리소스는 URI로 참조되는 어떤 데이터예요. MCP 서버는 리소스를 클라이언트에 제공할 수 있고, 개별 리소스를 등록하거나 URI 패턴으로 리소스 모음을 기술하는 리소스 템플릿을 등록할 수 있어요.
클라이언트 쪽: ClientSession.ReadResource로 리소스를 읽어요. SDK는 URI가 등록된 리소스와 정확히 일치하거나 리소스 템플릿의 URI 패턴과 일치할 때만 읽기가 성공하도록 보장해요. 서버의 리소스와 리소스 템플릿을 나열하려면 ClientSession.Resources와 ClientSession.ResourceTemplates 이터레이터를 쓰고, 변경 통지는 ClientOptions.ResourceListChangedHandler를 설정해요. 클라이언트는 리소스 URI에 구독해서 리소스 내용의 변경을 통지받을 수 있어요. 구독에는 ClientSession.Subscribe, 구독 해제에는 ClientSession.Unsubscribe, 변경 통지는 ClientOptions.ResourceUpdatedHandler를 사용해요. 2026-07-28 이후 세션에서는 이런 알림을 레거시 resources/subscribe RPC 대신 subscriptions/listen 스트림으로 전달해요.
서버 쪽: Server.AddResource나 Server.AddResourceTemplate로 핸들러와 함께 리소스/리소스 템플릿을 추가해요. ResourceHandler는 URI를 텍스트·바이너리 데이터·둘 다일 수 있는 리소스 내용에 매핑해요. 리소스가 추가되면 해당 역량이 생기고, 이미 연결된 클라이언트에는 notifications/resources/list_changed 알림이 가요.
func Example_resources() {
ctx := context.Background()
resources := map[string]string{
"file:///a": "a",
"file:///dir/x": "x",
"file:///dir/y": "y",
}
handler := func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
uri := req.Params.URI
c, ok := resources[uri]
if !ok {
return nil, mcp.ResourceNotFoundError(uri)
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{{URI: uri, Text: c}},
}, nil
}
// Create a server with a single resource.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
s.AddResource(&mcp.Resource{URI: "file:///a"}, handler)
s.AddResourceTemplate(&mcp.ResourceTemplate{URITemplate: "file:///dir/{f}"}, handler)
// Create a client.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
// Connect the server and client.
t1, t2 := mcp.NewInMemoryTransports()
if _, err := s.Connect(ctx, t1, nil); err != nil {
log.Fatal(err)
}
cs, err := c.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
// List resources and resource templates.
for r, err := range cs.Resources(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(r.URI)
}
for r, err := range cs.ResourceTemplates(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(r.URITemplate)
}
// Read resources.
for _, path := range []string{"a", "dir/x", "b"} {
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "file:///" + path})
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res.Contents[0].Text)
}
}
// Output:
// file:///a
// file:///dir/{f}
// a
// x
// calling "resources/read": Resource not found
}
바이너리 리소스
ResourceContents는 데이터를 Text 또는 Blob에 담아요. 바이너리 데이터는 원시 바이트 그대로 Blob에 들어가고(SDK가 와이어에서 base64로 인코딩) Text는 비워두므로, 클라이언트는 어느 필드가 채워졌는지로 둘을 구분해요.
도구 (Tools)
MCP 도구는 클라이언트가 서버에 호출을 요청할 수 있는 명명된 호출 가능 목적지예요. 도구는 검색, 계산, 파일 조작, API 호출 같은 것들을 제공하는 데 쓰여요.
클라이언트 쪽: ClientSession.Tools 이터레이터로 도구를 나열하고, ClientSession.CallTool로 호출해요.
서버 쪽: Server.AddTool로 도구와 핸들러를 등록해요. 도구 핸들러 시그니처는 func(ctx, req, input) (result, output, error) 형태로, input은 JSON 스키마로부터 역직렬화된 구조체예요.
멀티 라운드 트립 요청 (MRTR)
새 프로토콜 버전(2026-07-28 이상)에서는 서버에서 클라이언트로 가는 요청(샘플링, 일렉시테이션, 루트 등)이 더 이상 독립적인 JSON-RPC 호출로 발행되지 않아요. 대신 tools/call, prompts/get, resources/read의 진행 중 응답 안에 담겨서 전달되며, 클라이언트는 생성된 응답들로 원래 요청을 다시 시도해야 해요.
역량 (Capabilities)
서버 역량은 초기화 핸드셰이크 중에 클라이언트에게 광고돼요. 기본적으로 SDK는 logging 역량만 광고하고, 기능이 등록되면(예: 도구 추가 시 tools 역량) 추가 역량이 자동으로 붙습니다.
역량 추론
도구·프롬프트·리소스 같은 기능이 추가되면 그 역량이 자동으로 추론되고, 기본값은 {listChanged:true}예요. 마찬가지로 ServerOptions.SubscribeHandler나 ServerOptions.CompletionHandler를 설정하면 해당 역량이 추가됩니다.
명시적 역량
역량을 명시적으로 선언하거나 기본 추론 값을 덮어쓰려면 ServerOptions.Capabilities를 설정해요. 이 값은 핸들러에 기반해 역량이 추가되기 전의 기본 서버 역량을 정해요.
이를 통해:
- 기본 역량 비활성화: 빈
&ServerCapabilities{}를 넘기면 logging을 포함한 모든 기본값을 끌 수 있어요. - listChanged 알림 비활성화: 특정 역량에
ListChanged: false를 설정해 기능 추가/제거 시 list-changed 알림을 막을 수 있어요. - 역량 사전 선언: 기능 등록 전에 역량을 선언할 수 있는데, 기능을 동적으로 로드하는 서버에 유용해요.
// Disable listChanged notifications for tools
server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{
Logging: &mcp.LoggingCapabilities{},
Tools: &mcp.ToolCapabilities{ListChanged: false},
},
})
Deprecated —
ServerOptions의HasPrompts,HasResources,HasTools필드는 deprecated예요.Capabilities를 사용하세요.
확장 (Extensions)
SEP-2133은 ServerCapabilities에 extensions 맵을 추가해서 코어 프로토콜 밖의 선택적 역량을 와이어에 선언할 수 있게 해요. 키는 "{vendor-prefix}/{extension-name}" 형태로 네임스페이스되고, 값은 확장별 설정 객체예요.
페이지네이션
서버 쪽 기능 목록은 커서를 사용해 페이지네이션될 수 있고, SDK는 기본적으로 이를 지원해요.
클라이언트 쪽: ClientSession은 기능 종류별로 이터레이터를 반환하는 메서드를 제공해요. 이 이터레이터는 iter.Seq2[Feature, error]이며, 에러 값은 페이지 조회 실패 여부를 나타내요.
ClientSession.Prompts— 프롬프트 순회ClientSession.Resource— 리소스 순회ClientSession.ResourceTemplates— 리소스 템플릿 순회ClientSession.Tools— 도구 순회
ClientSession은 페이지네이션을 정밀하게 제어할 수 있는 ListXXX 메서드도 노출해요.
서버 쪽: 페이지네이션은 기본 켜져 있어서 일반적으로 서버 쪽에서 필요한 건 없어요. 다만 ServerOptions.PageSize로 페이지 크기를 조정할 수 있습니다.
더 알아보기 (Learn more)
- Go SDK 퀵스타트 — 첫 서버/클라이언트 만들기
- Go SDK 클라이언트 가이드 — 루트·샘플링·일렉시테이션
- Go SDK 생명주기 — 프로토콜 라이프사이클·트랜스포트