콘텐츠로 이동

MCP 서버 및 도구 작성

이 문서는 MCP 서버 정의가 코딩 에이전트에서 호출 가능한 mcp_* 도구로 변환되는 방식과, 설정이 유효하지 않거나, 중복되거나, 비활성화되거나, 인증이 필요한 경우 운영자가 예상해야 할 사항을 설명합니다.

Config sources (.xcsh/.claude/.cursor/.vscode/mcp.json, mcp.json, etc.)
-> discovery providers normalize to canonical MCPServer
-> capability loader dedupes by server name (higher provider priority wins)
-> loadAllMCPConfigs converts to MCPServerConfig + skips enabled:false
-> MCPManager connects/listTools (with auth/header/env resolution)
-> MCPTool/DeferredMCPTool bridge exposes tools as mcp_<server>_<tool>
-> AgentSession.refreshMCPTools replaces live MCP tools immediately

1) 서버 설정 모델 및 유효성 검사

섹션 제목: “1) 서버 설정 모델 및 유효성 검사”

src/mcp/types.ts는 MCP 설정 작성자와 런타임에서 사용하는 작성 형식을 정의합니다:

  • stdio (type이 누락된 경우 기본값): command 필수, args, env, cwd 선택
  • http: url 필수, headers 선택
  • sse: url 필수, headers 선택 (호환성을 위해 유지)
  • 공유 필드: enabled, timeout, auth

validateServerConfig() (src/mcp/config.ts)는 전송 계층 기본 사항을 강제합니다:

  • commandurl을 동시에 설정한 설정을 거부합니다
  • stdio에는 command 필수
  • http/sse에는 url 필수
  • 알 수 없는 type을 거부합니다

config-writer.ts는 추가/업데이트 작업에 이 유효성 검사를 적용하며 서버 이름도 검증합니다:

  • 비어 있지 않아야 함
  • 최대 100자
  • [a-zA-Z0-9_.-]만 허용
  • type이 생략되면 stdio를 의미합니다. HTTP/SSE를 의도했지만 type을 생략한 경우 command가 필수가 됩니다.
  • sse는 여전히 허용되지만 내부적으로는 HTTP 전송(createHttpTransport)으로 처리됩니다.
  • 유효성 검사는 구조적이며 연결 가능성을 검사하지 않습니다: 구문적으로 유효한 URL도 연결 시점에 실패할 수 있습니다.

loadAllMCPConfigs() (src/mcp/config.ts)는 loadCapability(mcpCapability.id)를 통해 정규 MCPServer 항목을 로드합니다.

기능 계층(src/capability/index.ts)은 다음을 수행합니다:

  1. 우선순위 순서로 프로바이더를 로드합니다
  2. server.name 기준으로 중복을 제거합니다 (먼저 발견된 것이 승리 = 가장 높은 우선순위)
  3. 중복 제거된 항목을 검증합니다

결과: 여러 소스에 걸친 중복 서버 이름은 병합되지 않습니다. 하나의 정의만 적용되며, 낮은 우선순위의 중복은 가려집니다.

src/discovery/mcp-json.ts의 전용 폴백 프로바이더는 프로젝트 루트의 mcp.json.mcp.json을 읽습니다 (낮은 우선순위).

실제로 MCP 서버는 더 높은 우선순위의 프로바이더에서도 제공됩니다 (예: 네이티브 .xcsh/... 및 도구별 설정 디렉터리). 작성 지침:

  • 명시적 제어를 위해 .xcsh/mcp.json (프로젝트) 또는 ~/.xcsh/mcp.json (사용자)를 선호하세요.
  • 폴백 호환성이 필요할 때 루트 mcp.json / .mcp.json을 사용하세요.
  • 여러 소스에서 동일한 서버 이름을 재사용하면 병합이 아닌 우선순위에 의한 가려짐이 발생합니다.

convertToLegacyConfig() (src/mcp/config.ts)는 정규 MCPServer를 런타임 MCPServerConfig로 매핑합니다.

주요 동작:

  • 전송 계층은 server.transport ?? (command ? "stdio" : url ? "http" : "stdio")로 추론됩니다
  • 비활성화된 서버(enabled === false)는 연결 전에 제거됩니다
  • 선택적 필드는 존재하는 경우 보존됩니다

mcp-json.tsexpandEnvVarsDeep()를 사용하여 문자열 필드의 환경 변수 플레이스홀더를 확장합니다:

  • ${VAR}${VAR:-default}를 지원합니다
  • 해석되지 않은 값은 리터럴 ${VAR} 문자열로 유지됩니다

mcp-json.ts는 또한 사용자 JSON에 대해 런타임 타입 검사를 수행하며, 전체 파일을 실패 처리하는 대신 유효하지 않은 enabled/timeout 값에 대해 경고를 로그합니다.

MCPManager.prepareConfig()/#resolveAuthConfig() (src/mcp/manager.ts)는 연결 전 최종 처리 단계입니다.

설정에 다음이 있는 경우:

auth: { type: "oauth", credentialId: "..." }

인증 저장소에 자격 증명이 존재하면:

  • http/sse: Authorization: Bearer <access_token> 헤더를 주입합니다
  • stdio: OAUTH_ACCESS_TOKEN 환경 변수를 주입합니다

자격 증명 조회가 실패하면 매니저는 경고를 로그하고 해석되지 않은 인증 상태로 계속 진행합니다.

연결 전에 매니저는 resolveConfigValue() (src/config/resolve-config-value.ts)를 통해 각 헤더/환경 변수 값을 해석합니다:

  • !로 시작하는 값 => 셸 명령을 실행하고, 트리밍된 stdout을 사용합니다 (캐시됨)
  • 그 외에는 값을 환경 변수 이름으로 먼저 처리하고 (process.env[name]), 리터럴 값으로 폴백합니다
  • 해석되지 않은 명령/환경 변수 값은 최종 헤더/환경 변수 맵에서 제외됩니다

운영상 주의사항: 잘못 입력된 시크릿 명령/환경 변수 키가 해당 헤더/환경 변수 항목을 조용히 제거하여 다운스트림에서 401/403 또는 서버 시작 실패를 유발할 수 있습니다.

4) 도구 브릿지: MCP -> 에이전트 호출 가능 도구

섹션 제목: “4) 도구 브릿지: MCP -> 에이전트 호출 가능 도구”

src/mcp/tool-bridge.ts는 MCP 도구 정의를 CustomTool로 변환합니다.

도구 이름은 다음과 같이 생성됩니다:

mcp_<sanitized_server_name>_<sanitized_tool_name>

규칙:

  • 소문자로 변환
  • [a-z_]가 아닌 문자는 _로 변환
  • 반복되는 밑줄은 하나로 축약
  • 도구 이름에서 중복되는 <server>_ 접두사는 한 번 제거

이것은 많은 충돌을 방지하지만 전부는 아닙니다. 서로 다른 원본 이름이 동일한 식별자로 정제될 수 있으며 (예: my-servermy.server는 유사하게 정제됨), 레지스트리 삽입은 마지막 쓰기가 우선합니다.

convertSchema()는 MCP JSON Schema를 대부분 그대로 유지하지만, properties가 누락된 객체 스키마에 프로바이더 호환성을 위해 {}를 패치합니다.

MCPTool.execute() / DeferredMCPTool.execute():

  • MCP tools/call을 호출합니다
  • MCP 콘텐츠를 표시 가능한 텍스트로 평탄화합니다
  • 구조화된 상세 정보를 반환합니다 (serverName, mcpToolName, 프로바이더 메타데이터)
  • 서버가 보고한 isErrorError: ... 텍스트 결과로 매핑합니다
  • 발생한 전송/런타임 실패를 MCP error: ...로 매핑합니다
  • AbortError를 ToolAbortError로 변환하여 중단 시맨틱을 보존합니다

5) 운영자 라이프사이클: 추가/편집/제거 및 실시간 업데이트

섹션 제목: “5) 운영자 라이프사이클: 추가/편집/제거 및 실시간 업데이트”

인터랙티브 모드는 src/modes/controllers/mcp-command-controller.ts에서 /mcp를 노출합니다.

지원되는 작업:

  • add (마법사 또는 빠른 추가)
  • remove / rm
  • enable / disable
  • test
  • reauth / unauth
  • reload

설정 쓰기는 원자적입니다 (writeMCPConfigFile: 임시 파일 + 이름 변경).

변경 후 컨트롤러는 #reloadMCP()를 호출합니다:

  1. mcpManager.disconnectAll()
  2. mcpManager.discoverAndConnect()
  3. session.refreshMCPTools(mcpManager.getTools())

refreshMCPTools()는 모든 mcp_ 레지스트리 항목을 교체하고 최신 MCP 도구 세트를 즉시 다시 활성화하므로, 세션을 재시작하지 않고도 변경 사항이 적용됩니다.

  • 인터랙티브/TUI 모드: /mcp는 앱 내 UX를 제공합니다 (마법사, OAuth 흐름, 연결 상태 텍스트, 즉시 런타임 재바인딩).
  • SDK/헤드리스 통합: discoverAndLoadMCPTools() (src/mcp/loader.ts)는 로드된 도구 + 서버별 오류를 반환하며, /mcp 명령 UX는 없습니다.

6) 사용자에게 표시되는 오류 화면

섹션 제목: “6) 사용자에게 표시되는 오류 화면”

사용자/운영자가 보게 되는 일반적인 오류 문자열:

  • 추가/업데이트 유효성 검사 실패:
    • Invalid server config: ...
    • Server "<name>" already exists in <path>
  • 빠른 추가 인수 문제:
    • Use either --url or -- <command...>, not both.
    • --token requires --url (HTTP/SSE transport).
  • 연결/테스트 실패:
    • Failed to connect to "<name>": <message>
    • 타임아웃 도움말 텍스트는 타임아웃 증가를 제안합니다
    • 401/403에 대한 인증 도움말 텍스트
  • 인증/OAuth 흐름:
    • Authentication required ... OAuth endpoints could not be discovered
    • OAuth flow timed out. Please try again.
    • OAuth authentication failed: ...
  • 비활성화된 서버 사용:
    • Server "<name>" is disabled. Run /mcp enable <name> first.

검색 중 잘못된 소스 JSON은 일반적으로 경고/로그로 처리됩니다; config-writer 경로는 명시적 오류를 던집니다.

이 코드베이스에서 견고한 MCP 작성을 위해:

  1. 모든 MCP 지원 설정 소스에서 서버 이름을 전역적으로 고유하게 유지하세요.
  2. 생성된 mcp_* 도구 이름에서 정제된 이름 충돌을 방지하기 위해 영숫자/밑줄 이름을 선호하세요.
  3. 우발적인 stdio 기본값을 방지하기 위해 명시적 type을 사용하세요.
  4. enabled: false는 완전 비활성화로 처리하세요: 서버가 런타임 연결 세트에서 제외됩니다.
  5. OAuth 설정의 경우 유효한 credentialId를 저장하세요; 그렇지 않으면 인증 주입이 건너뛰어집니다.
  6. 명령 기반 시크릿 해석(!cmd)을 사용하는 경우 명령 출력이 안정적이고 비어 있지 않은지 확인하세요.