@ecubelabs/elasticsearch-mcp
v1.0.0
Published
Elasticsearch **직접 조회** MCP. ECK 클러스터의 ES HTTP 서비스로 `kubectl port-forward`를 **MCP가 스스로 열어** 붙는다. Kibana를 거치지 않고 ES REST를 직접 호출하므로 `_search` 외에 `_mapping`·`_resolve/index`·`_settings`·`_alias` 같은 비검색 API까지 쓸 수 있는 게 존재 이유다.
Readme
@ecubelabs/elasticsearch-mcp
Elasticsearch 직접 조회 MCP. ECK 클러스터의 ES HTTP 서비스로 kubectl port-forward를
MCP가 스스로 열어 붙는다. Kibana를 거치지 않고 ES REST를 직접 호출하므로 _search 외에
_mapping·_resolve/index·_settings·_alias 같은 비검색 API까지 쓸 수 있는 게 존재 이유다.
조회 도구는 전부 읽기 전용이고, 쓰기는 rollover 하나뿐이다 — 삭제/reindex/close 도구는 없다.
붙을 클러스터
서버 하나가 kube 컨텍스트 하나를 본다. --context <이름> 으로 지정하고, 생략하면 kubeconfig 의
현재 컨텍스트를 따른다(사용자가 kubectl config use-context 로 바꾸면 터널을 다시 세운다).
여러 클러스터를 쓰려면 서버를 그만큼 등록한다. 어느 클러스터였는지는 모든 응답의 context 필드에
담겨 나온다.
터널 생존 판정은 포트 열림이 아니라 실제 ES 요청(/_security/_authenticate)으로 한다 —
port-forward는 파드가 재시작돼도 로컬 포트를 열어두기 때문에 포트만 보면 죽은 터널을 계속 쓰게 된다.
죽었으면 죽이고 다시 띄운다. 로컬 포트는 kubectl 이 빈 것을 골라 주고, 서버 종료 시 터널을 정리한다.
도구
| 도구 | 용도 |
| -------------- | --------------------------------------------------------------------------- |
| search | 인덱스 조회 — filters(정확 매칭)·query(Lucene)·페이지네이션, 실행 DSL 반환 |
| get_mapping | 매핑을 필드별 { type, keyword, ignore_above } 로 평탄화 (전체 덤프 안 함) |
| list_indices | 패턴 → 인덱스·별칭·data stream 해석 (_resolve/index) |
| count | 문서 수 (+ groupBy 로 값 분포) |
| get_document | _id 로 문서 한 건 |
| rollover | 데이터스트림/별칭 롤오버 — 유일한 쓰기 도구 |
권한 롤(developer) 기준 허용 엔드포인트: <index>/_search·_mapping·_settings,
/_resolve/index, /_alias. /·_cluster/health·_cat/*·_analyze는 403이라 도구로 만들지 않았다.
그래서 list_indices는 _cat/indices가 아니라 _resolve/index를 쓴다.
롤오버 (rollover)
POST /<index>/_rollover와 같다. 데이터스트림의 현재 write 인덱스를 봉인하고 새 backing 인덱스를
연다 — 기존 문서는 삭제되지 않고 봉인된 인덱스에 남아 계속 검색된다. 매핑 충돌(같은 필드가 다른
타입으로 들어와 색인이 거부됨)·필드 수 한도 초과·샤드 비대처럼 "새 인덱스에서 다시 시작"해야 풀리는
상황에 쓴다. ILM이 붙어 있으면 보통 자동이고, 이 도구는 "지금 당장 끊어야 할 때"용이다.
rollover({ index: "logs-filebeat-container", dryRun: true }) // 조건 평가만
rollover({ index: "logs-filebeat-container" }) // 조건 없이 즉시 롤오버
rollover({ index: "logs-filebeat-container", conditions: { max_age: "7d" } })conditions생략 → 무조건 롤오버. 주면 ES가 조건을 평가해 충족할 때만 굴린다.dryRun: true→ 굴리지 않고 조건 평가 결과만. 권한 확인용으로 먼저 돌려볼 것.index는 패턴이 아니라 이름 하나여야 한다(경로 한 세그먼트로 인코딩해 보낸다).- 응답은
{ oldIndex, newIndex, rolledOver, dryRun, conditions }.
권한 주의: 위 developer 롤 허용 목록에 manage가 포함된다는 보장이 없다. 없으면
ES 403 POST /<index>/_rollover ...로 떨어진다. 실행 전 dryRun으로 확인하는 게 안전하다.
검색 규칙 (로그 인덱스)
logs-filebeat-ecs 등 로그 인덱스의 text 필드는 .keyword 서브필드가 전부 ignore_above: 256.
즉 256자를 넘는 값은 keyword로 색인되지 않아 term/wildcard로 못 찾는다.
- 짧은 ID 정밀 매칭:
.keywordterm 사용 —trace.id.keyword(32자),http.request.id,url.path,service.name. - 긴 값 속 ID:
message·url.query처럼 길고 URL 인코딩으로 잘리는 값은 풀-ID phrase 검색이 0건 난다. text 필드에 조각 wildcard(*23eac3ee*)를 써야 걸린다. - 어떤 필드에
.keyword가 있고ignore_above가 얼마인지는get_mapping으로 먼저 확인.
search는 실행한 실제 DSL을 항상 응답에 담고, 0건이면 위 규칙을 진단 힌트로 붙인다.
시간 범위와 정렬은 호출자가 정한다
인덱스 이름으로 성격을 추측하지 않는다. from/to 를 주지 않으면 시간 필터가 걸리지 않고,
sort 를 주지 않으면 정렬 없이 돌려준다. 시계열 인덱스를 조회할 때는 둘 다 지정하는 것이 좋다 —
전 기간 스캔은 백킹 인덱스가 수백 개인 데이터스트림에서 비싸다.
search({ index: "logs-filebeat-ecs", fields: ["message"],
from: "now-24h", sort: [{ "@timestamp": "desc" }] })searchAfter 로 페이지를 넘길 때는 문서마다 고유한 필드를 tiebreaker 로 sort 에 포함해야
한다. 정렬 값이 겹치면 페이지 경계에서 문서가 누락되거나 중복될 수 있다.
안전장치
fields사실상 필수 —_source전체 덤프/PII 노출 방지. 전체가 필요하면fields: ["*"]로 명시.- size: 기본 200, 상한 10000. 그 이상은
searchAfter페이지네이션. track_total_hits기본 off,timeout+ 선택적terminate_after.- 응답이 커지면(≈256KB 초과) 전체를 임시파일로 쓰고 프리뷰만 반환.
설정
비밀은 환경변수로, 어느 클러스터를 볼지는 실행 인자로 받는다. 넣는 위치는 클라이언트마다 다르지만 모양은 같다.
| 항목 | 통로 | 비고 |
| ----------------------------- | ---- | ------------------------------------ |
| ES_API_KEY | env | 있으면 ApiKey 인증으로 붙는다 |
| ES_USERNAME / ES_PASSWORD | env | API 키가 없을 때 Basic 인증에 쓴다 |
| --context <이름> | args | 생략하면 kubeconfig 의 현재 컨텍스트 |
Claude Code — .mcp.json:
{
"mcpServers": {
"elasticsearch-stage": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@ecubelabs/elasticsearch-mcp", "--context", "ecubelabs2-stage.k8s.local"],
"env": { "ES_USERNAME": "...", "ES_PASSWORD": "..." }
},
"elasticsearch-prod": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@ecubelabs/elasticsearch-mcp", "--context", "ecubelabs2.k8s.local"],
"env": { "ES_USERNAME": "...", "ES_PASSWORD": "..." }
}
}
}Codex — ~/.codex/config.toml:
[mcp_servers.elasticsearch-stage]
command = "npx"
args = ["-y", "@ecubelabs/elasticsearch-mcp", "--context", "ecubelabs2-stage.k8s.local"]
env = { ES_USERNAME = "...", ES_PASSWORD = "..." }나머지 클러스터 설정은 코드에 있다
아래 값들은 ECK 배포가 정하는 사실이라 모든 사용자에게 같고, 바뀐다면 코드를 고쳐 릴리스하는 게
맞다는 판단으로 src/config.ts 에 고정돼 있다.
| 항목 | 값 |
| ------------------ | ------------------------------------- |
| 로컬 포트 | 열 때마다 kubectl 이 빈 포트를 고른다 |
| 네임스페이스 | elastic-system |
| 서비스 | elasticsearch-es-http |
| 리모트 포트 / 스킴 | 9200 / http (TLS 미사용) |
| kubectl | PATH 에서 찾는다 |
| 반환 행 하드캡 | 10000 |
로컬 테스트
publish 전 코드를 확인할 때는 빌드 결과를 직접 띄운다.
1. 빌드
yarn workspace @ecubelabs/elasticsearch-mcp build2. MCP 설정
레포 루트 .mcp.json에 빌드 결과를 가리키는 stage 서버가 이미 등록돼 있다. 빌드하지 않았거나 자격증명이 없으면 skynet 세션에서 이 서버가 연결에 실패한다.
{
"mcpServers": {
"elasticsearch": {
"type": "stdio",
"command": "node",
"args": ["./packages/elasticsearch-mcp/dist/index.js", "--context", "ecubelabs2-stage.k8s.local"]
}
}
}자격증명(ES_USERNAME/ES_PASSWORD 또는 ES_API_KEY)은 MCP 클라이언트를 실행하는 셸의 환경변수로 두거나, 위 항목에 env 로 넣는다.
3. 반영
코드를 고친 뒤에는 다시 빌드하고, MCP 클라이언트에서 해당 서버를 재연결해야 새 코드가 뜬다.
전제
kubectl이 PATH에 있고 대상 컨텍스트에 접근 가능해야 한다. 첫 port-forward는 SAML 롤 assume 때문에 몇 초 걸릴 수 있다.svc/port-forward는 kubectl이 클라이언트에서 팟으로 바꿔 주는 4단계다 —services get→pods list→pods get→pods/portforward create. 어느 단계든 권한이 빠지면 막힌다.pods/portforward는 ES 팟 이름 목록으로 한정돼 있어, 팟이 새로 뜨면 그 팟에 걸린 실행만 실패해 간헐적으로 보인다.- kubectl 실패는 보통 컨텍스트 이름 불일치 / 위 네 단계 중 하나의 권한 없음 / 서비스명 오타다. 에러 메시지가 무엇인지 짚어준다.
- prod 클러스터는 실 데이터다. prod 컨텍스트를 가리키는 서버는 그 사실이 드러나는 이름으로
등록하고(예:
elasticsearch-prod), 응답의context필드로 어디를 봤는지 확인할 것.
