Model Context Protocol (MCP)은 Cursor와 같은 AI 클라이언트가 시스템에 존재하는 도구를 발견하고 호출하는 표준 방법을 제공합니다. 모든 어시스턴트마다 사용자 지정 플러그인을 작성할 필요 없이, MCP 서버 하나만 작성하면 어떤 MCP 지원 클라이언트든 이를 사용할 수 있습니다.
저는 Go API, React 및 PostgreSQL로 구축한 Jira 스타일의 프로젝트 관리 제품인 sprint-management를 위한 MCP 서버를 구축했습니다. 이 서버는 Cursor가 에디터에서 작업 항목을 직접 읽고 업데이트할 수 있게 합니다. 본 게시물에서는 이러한 종류의 서버를 Go로 구축하는 방법과 프로토콜 자체보다 더 중요한 설계 선택 사항들을 다룹니다. 코드는 제품 소스 코드가 아닌 단순화된 예시입니다.
MCP 서버가 실제로 무엇인지
핵심적으로, MCP 서버는 다음을 수행하는 프로세스입니다:
- 클라이언트에게 자신이 제공하는 도구(tools) 목록(이름, 설명, 입력 및 출력에 대한 JSON Schema)을 알려줍니다.
- JSON 인수를 가진 **도구 호출(tool calls)**을 받습니다.
- 모델이 읽을 수 있는 결과를 반환합니다.
이는 전송 계층(transport)을 통해 JSON-RPC로 통신합니다: 일반적으로 stdio(클라이언트가 사용자의 바이너리를 실행함) 또는 스트리밍 가능 HTTP (streamable HTTP)(사용자 서버가 서비스로 실행됨)를 사용합니다. Go는 적합한 언어입니다. 단일 정적 바이너리는 클라이언트가 가리키기 쉽고, 구조체 태그(struct tags)를 통해 거의 무료로 스키마를 얻을 수 있기 때문입니다.
설정 (Setup)
공식 Go SDK를 사용하세요:
go mod init example.com/sprint-mcp
go get github.com/modelcontextprotocol/go-sdk/mcp
공식 SDK를 사용하면, 도구는 타입이 지정된 입력과 출력을 가진 일반 Go 함수가 됩니다. SDK는 구조체(structs)에서 JSON Schema를 추론하며, jsonschema 태그는 모델이 보는 필드 설명이 됩니다.
type ListIssuesInput struct {
ProjectKey string `json:"project_key" jsonschema:"project key, for example CORE"`
Status string `json:"status,omitempty" jsonschema:"optional filter: todo, in_progress or done"`
...
오류 메시지에 주목하세요. 이 메시지는 로그 파일용이 아니라 모델을 위해 작성된 것입니다. 모델이 잘못된 상태를 전달할 경우, 유효한 값에 대한 명확한 메시지를 제공하면 다음 호출에서 스스로 수정할 수 있습니다. 공식 SDK의 경우, 도구 핸들러에서 반환되는 오류는 IsError가 설정된 도구 결과로 클라이언트에게 전송되므로 모델이 이를 보고 재시도할 수 있습니다. 또한 SDK는 핸들러가 실행되기 전에 생성된 스키마와 인수를 검증하지만, 스키마 검사는 비즈니스 규칙을 알지 못하므로 이 부분은 직접 검증해야 합니다.
읽기 도구(Read tools)는 위험도가 낮습니다. 반면, 에이전트가 실제 피해를 입힐 수 있는 곳이 바로 쓰기 도구(Write tools)이므로 더 엄격한 처리가 필요합니다:
type MoveIssueInput struct {
IssueKey string `json:"issue_key" jsonschema:"issue key, for example CORE-123"`
ToStatus string `json:"to_status" jsonschema:"target status: todo, in_progress or done"`
...
}
제가 모든 쓰기 도구에 적용하는 네 가지 규칙은 다음과 같습니다:
- 좁고 타입이 지정된 작업(Narrow, typed operations).
run_sql이나 update_issue(fields map[string]any) 대신 열거형(enum)을 사용하는 move_issue와 같이 구체적이어야 합니다. 표면적이 작을수록 잘못할 수 있는 방법도 적습니다.
- 실제 권한 부여 재사용(Reuse your real authorization). MCP 서버는 REST API에서 사용되는 것과 동일한 도메인 서비스 및 RBAC(Role-Based Access Control) 검사를 호출해야 합니다. 절대로 이들을 우회하는 특권 데이터베이스 연결을 제공해서는 안 됩니다.
- 변경된 내용 반환(Return what changed).
from_status를 포함하여 모델(그리고 사용자)이 효과를 확인할 수 있도록 해야 합니다.
- 모든 것 감사 추적(Audit everything). 에이전트가 시작한 쓰기 작업은 사람의 작업과 분리하여 추적이 가능해야 합니다.
서버 연결 설정 (Wiring up the server)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) // stdio transport에서는 절대 stdout 사용 금지
...
}
stdio의 함정: stdio 전송 방식(transport)을 사용할 경우, stdout은 프로토콜 채널 그 자체입니다. 임의의 fmt.Println 호출이나 logger가 stdout에 쓰는 것은 JSON-RPC 스트림을 손상시키고 클라이언트가 혼란스러운 방식으로 실패하게 만듭니다. 모든 로그는 반드시 stderr로 전송하세요.
설명(Descriptions)이 외관보다 중요합니다. 모델은 오직 도구의 이름과 설명을 통해서만 도구를 선택하기 때문입니다. 해당 도구가 무엇을 하는지, 데이터 변경 여부, 그리고 어떤 도구를 먼저 호출해야 하는지를 명시해야 합니다.
Cursor와 연결하기
바이너리를 빌드하고 프로젝트의 .cursor/mcp.json에 등록합니다:
{
"mcpServers": {
"sprint-management": {
...
Cursor는 프로세스를 실행하고, 사용 가능한 도구 목록을 나열하며, 작업하는 동안 에이전트는 list_issues와 move_issue를 호출할 수 있게 됩니다. 토큰은 사용자(user)를 식별하므로, 에이전트는 해당 사용자의 권한 내에서만 동작합니다.
stdio 대신 HTTP를 사용해야 하는 경우
Stdio는 단일 머신에서 개발자 도구로 사용할 때 가장 간단한 선택지입니다. 여러 사용자나 원격 에이전트가 동일한 서버가 필요한 경우, SDK의 스트리밍 가능한(streamable) HTTP 핸들러를 사용하여 서비스로 실행해야 합니다:
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, nil)
...
HTTP를 사용할 경우, 인증은 개발자의 몫입니다. 미들웨어에 구현하고, 요청으로부터 사용자를 파생시키며, 각 도구 내부에서 동일한 사용자별 권한 검사를 적용해야 합니다.
테스트(Testing)
도구 핸들러는 일반 Go 함수이므로 대부분의 테스트에서는 프로토콜 자체가 필요하지 않습니다. 가짜 스토어(fake store)를 사용하여 srv.moveIssue를 직접 호출하고, 유효성 검사(validation), 권한 부여(authorization), 감사(audit) 동작에 대해 단언(assert)할 수 있습니다. 엔드투엔드 테스트가 한두 개 필요한 경우, SDK는 클라이언트(mcp.NewClient)와 인메모리 전송 계층(in-memory transport)을 포함하므로, 실제 클라이언트를 서버 프로세스 내에서 연결하고 도구를 이름으로 호출할 수 있습니다.
설계 체크리스트(Design checklist)
- 명확한 의도당 하나의 도구; '무엇이든 하는' 도구는 피합니다.
- 설명과 열거형(enums)을 갖춘 타입 지정 입력값; 그래도 항상 유효성 검사를 수행합니다.
- 모델에게 호출 방법을 알려주는 오류 메시지.
- 쓰기(write) 도구를 명확하게 표시하고 범위를 좁게 유지합니다.
- REST API와 동일한 인증, RBAC 및 감사 기능을 적용합니다.
- stdio의 경우 stderr로 로그를 기록합니다.
MCP 자체는 얇은 계층(thin layer)에 불과합니다. 좋은 MCP 서버의 가치는 좋은 API가 갖추어야 할 것들, 즉 작고 타입이 지정된 작업, 강력한 권한 부여, 그리고 정직한 오류 처리에서 나옵니다.
Pradip Parmar는 Go 백엔드 및 AI 에이전트 엔지니어입니다 (2018년부터 Go를 작성). 그는 기업을 위해 MCP 서버, RAG 시스템, LLM 에이전트를 구축하며, Jira 스타일 제품을 Cursor에 연결하는 MCP 서버도 포함합니다. 만약 Go로 MCP 서버 개발자를 찾고 있다면, 다음 플랫폼에서 그에게 연락하세요: LinkedIn, Upwork 또는 Contra. 코드: GitHub.