# @opendata-kr/narajangteo-bid-mcp 나라장터 입찰공고정보서비스(공공데이터포털 data.go.kr) Open API를 감싼 로컬 MCP 서버. [![npm version](https://img.shields.io/npm/v/@opendata-kr/narajangteo-bid-mcp)](https://www.npmjs.com/package/@opendata-kr/narajangteo-bid-mcp) [![CI](https://img.shields.io/github/actions/workflow/status/opendata-kr/narajangteo-bid-mcp/ci.yml?branch=main&label=CI)](https://github.com/opendata-kr/narajangteo-bid-mcp/actions/workflows/ci.yml) [![node](https://img.shields.io/node/v/@opendata-kr/narajangteo-bid-mcp)](https://nodejs.org) [![license](https://img.shields.io/npm/l/@opendata-kr/narajangteo-bid-mcp)](./LICENSE) Claude Desktop 등 MCP 클라이언트에서 입찰공고를 자연어로 검색하고 조회한다. 예를 들어 이렇게 물어볼 수 있다. - "이번 주 올라온 용역 입찰 공고를 찾아줘" - "인천광역시에서 추정가격 5억 원 이상인 공사 입찰을 검색해줘" - "입찰공고번호 R25BK00932003 상세를 알려줘" - "그 공고의 기초금액과 참가 가능 지역을 알려줘" - "그 공고의 첨부파일을 내려받아 제안요청서 내용을 요약해줘" - "그 공고 첨부 중 과업지시서 본문을 읽어줘" ## 특징 - **4개 업무구분 병렬 검색**: 공사/용역/물품/외자를 한 번에 조회한다. 업무구분 미지정 시 기타(etc)를 제외한 4구분(공사/용역/물품/외자)을 동시 검색하며, 기타공고는 `bidKind`에 `etc`를 명시한다. - **첨부 다운로드·본문 읽기**: 공고 첨부(공고문·규격서·제안요청서 등)를 디스크에 내려받고 ZIP은 풀어 파일 목록을 만든 뒤(`download_attachments`), 필요한 파일의 본문(HWPX·구형 HWP·구형 DOC)을 목록 인덱스로 읽는다(`read_attachment`). - **부분 실패 표면화**: 일부 구분 조회가 실패해도 나머지 결과를 반환하고, 실패한 구분은 오류 메시지로 드러낸다(조용한 누락 없음). - **data.go.kr 에러코드 한국어화**: 인증키 만료, 트래픽 초과 등 결과코드를 조치 가능한 한국어 메시지로 정규화한다. - **이중 인코딩 방어**: Encoding 키를 잘못 넣으면 경고하고, 요청은 한 번만 인코딩한다. - **타임아웃**: API 호출에 타임아웃을 둔다. ## 준비물 - **Node.js 24** 이상 (`.nvmrc` = `lts/krypton`). - **data.go.kr 인증키**: 1. [공공데이터포털](https://www.data.go.kr)에서 **나라장터 입찰공고정보서비스**를 활용신청해 `[승인]`을 받는다. 인증키는 계정당 하나지만, 각 API는 저마다 활용신청 승인이 있어야 그 API에서 인증된다. 서비스키가 있어도 이 API를 활용신청하지 않으면 인증 오류(코드 30)가 난다. 2. 마이페이지 → 활용신청 현황 → 개발계정 상세에서 **Decoding 서비스키**를 복사한다. 3. 같은 `DATA_GO_KR_SERVICE_KEY`는 같은 계정으로 활용신청한 다른 data.go.kr API에도 재사용된다. > [!TIP] > 공공데이터포털이 처음이라면 활용신청부터 인증키 복사까지 그림으로 따라 하는 [**data.go.kr 인증키 발급 가이드**](docs/service-key-guide.md)를 참고한다. > 서비스키는 반드시 **Decoding(원본)** 키를 넣는다. Encoding(`%2B` 등 포함) 키를 넣으면 이중 인코딩으로 인증 오류(코드 30)가 난다. ## MCP 클라이언트 설정 MCP 클라이언트에 아래 config를 추가한다: ```json { "mcpServers": { "narajangteo-bid": { "command": "npx", "args": ["-y", "@opendata-kr/narajangteo-bid-mcp@latest"], "env": { "DATA_GO_KR_SERVICE_KEY": "발급받은_Decoding_키" } } } } ``` > [!NOTE] > `@opendata-kr/narajangteo-bid-mcp@latest`를 쓰면 클라이언트가 항상 최신 버전을 받는다. > [!IMPORTANT] > `DATA_GO_KR_SERVICE_KEY`(필수, **Decoding 원본** 키)가 없으면 첫 호출이 인증 오류(코드 30)로 실패한다. 위 config의 `env`에 키를 넣는다. 원클릭 버튼이나 env를 config에 담지 못하는 클라이언트는 설치 후 셸 환경변수로 `DATA_GO_KR_SERVICE_KEY`를 설정한다. ### 클라이언트별 설정
Amp https://ampcode.com/manual#mcp 의 안내를 따르고 위 config를 사용한다. CLI로도 추가할 수 있다: ```bash amp mcp add narajangteo-bid -- npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` 이후 생성된 설정의 `env`(또는 셸 환경변수)에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Antigravity Antigravity 문서의 커스텀 MCP 서버 추가 방법을 따라 아래 config를 MCP servers 설정에 넣는다: ```json { "mcpServers": { "narajangteo-bid": { "command": "npx", "args": ["-y", "@opendata-kr/narajangteo-bid-mcp@latest"], "env": { "DATA_GO_KR_SERVICE_KEY": "발급받은_Decoding_키" } } } } ```
Claude Code Claude Code CLI로 서버를 추가한다 (가이드): ```bash claude mcp add narajangteo-bid --scope user --env DATA_GO_KR_SERVICE_KEY=발급받은_Decoding_키 -- npx -y @opendata-kr/narajangteo-bid-mcp@latest ```
Cline https://docs.cline.bot/mcp/configuring-mcp-servers 의 안내를 따르고 위 config를 사용한다.
Codex MCP 설정 가이드를 따르고 위 config를 사용한다. Codex CLI로도 추가할 수 있다: ```bash codex mcp add narajangteo-bid --env DATA_GO_KR_SERVICE_KEY=발급받은_Decoding_키 -- npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` **Windows** `~/.codex/config.toml`에 `cmd /c` 래핑으로 추가한다: ```toml [mcp_servers.narajangteo-bid] command = "cmd" args = ["/c", "npx", "-y", "@opendata-kr/narajangteo-bid-mcp@latest"] env = { DATA_GO_KR_SERVICE_KEY = "발급받은_Decoding_키" } ```
Command Code Command Code CLI로 서버를 추가한다 (MCP 가이드): ```bash cmd mcp add narajangteo-bid --scope user npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` 이후 생성된 설정의 `env`(또는 셸 환경변수)에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Continue Continue의 MCP 가이드를 따른다. Continue는 `mcpServers`를 배열로 쓴다: ```json { "mcpServers": [ { "name": "narajangteo-bid", "command": "npx", "args": ["-y", "@opendata-kr/narajangteo-bid-mcp@latest"], "env": { "DATA_GO_KR_SERVICE_KEY": "발급받은_Decoding_키" } } ] } ```
Copilot CLI Copilot CLI를 시작한다: ``` copilot ``` MCP 서버 추가 대화를 연다: ``` /mcp add ``` 다음 필드를 입력하고 `CTRL+S`로 저장한다: - **Server name:** `narajangteo-bid` - **Server Type:** `[1] Local` - **Command:** `npx -y @opendata-kr/narajangteo-bid-mcp@latest` - **Environment variables:** `DATA_GO_KR_SERVICE_KEY=발급받은_Decoding_키`
Copilot / VS Code **버튼으로 설치:** [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=io.github.opendata-kr%2Fnarajangteo-bid-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40opendata-kr%2Fnarajangteo-bid-mcp%22%5D%2C%22env%22%3A%7B%7D%7D) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522io.github.opendata-kr%252Fnarajangteo-bid-mcp%2522%252C%2522config%2522%253A%257B%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540opendata-kr%252Fnarajangteo-bid-mcp%2522%255D%252C%2522env%2522%253A%257B%257D%257D%257D) > 버튼은 키를 담지 못한다. 설치 후 `.vscode/mcp.json`(또는 사용자 설정)의 `env`에 `DATA_GO_KR_SERVICE_KEY`를 추가한다. **직접 추가:** VS Code [MCP 설정 가이드](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server)를 따르거나 CLI를 쓴다. macOS·Linux: ```bash code --add-mcp '{"name":"narajangteo-bid","command":"npx","args":["-y","@opendata-kr/narajangteo-bid-mcp@latest"],"env":{"DATA_GO_KR_SERVICE_KEY":"발급받은_Decoding_키"}}' ``` Windows(PowerShell): ```powershell code --add-mcp '{"""name""":"""narajangteo-bid""","""command""":"""npx""","""args""":["""-y""","""@opendata-kr/narajangteo-bid-mcp@latest"""],"""env""":{"""DATA_GO_KR_SERVICE_KEY""":"""발급받은_Decoding_키"""}}' ```
Cursor **버튼으로 설치:** [Add narajangteo-bid MCP server to Cursor](https://cursor.com/en/install-mcp?name=narajangteo-bid&config=eyJjb21tYW5kIjoibnB4IC15IEBvcGVuZGF0YS1rci9uYXJhamFuZ3Rlby1iaWQtbWNwQGxhdGVzdCJ9) > 버튼은 키를 담지 못한다. 설치 후 Cursor의 MCP 설정에서 `env`에 `DATA_GO_KR_SERVICE_KEY`를 추가한다. **직접 추가:** `Cursor Settings` → `MCP` → `New MCP Server`에서 위 config를 사용한다.
Factory CLI Factory CLI로 서버를 추가한다 (가이드): ```bash droid mcp add narajangteo-bid "npx -y @opendata-kr/narajangteo-bid-mcp@latest" ``` 이후 생성된 설정의 `env`(또는 셸 환경변수)에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Gemini CLI Gemini CLI로 서버를 추가한다. **프로젝트 범위:** ```bash gemini mcp add narajangteo-bid npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` **전역:** ```bash gemini mcp add -s user narajangteo-bid npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` 또는 MCP 가이드를 따르고 위 config를 쓴다. `~/.gemini/settings.json`의 서버 정의 `env`에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Gemini Code Assist MCP 설정 가이드를 따르고 위 config를 사용한다.
Grok Build CLI ```bash grok mcp add narajangteo-bid npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` 이후 생성된 설정의 `env`(또는 셸 환경변수)에 `DATA_GO_KR_SERVICE_KEY`를 추가한다. 더 많은 옵션은 문서 참고.
JetBrains AI Assistant & Junie `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` → `Add`에서 위 config를 사용한다. Junie도 같은 방식으로 `Settings | Tools | Junie | MCP Settings` → `Add`에서 위 config를 사용한다.
Katalon Studio Katalon StudioAssist는 MCP 프록시를 통해 stdio 서버를 연결한다. **1단계:** MCP 프록시 설정 가이드로 프록시를 설치한다. **2단계:** 프록시로 서버를 띄운다(같은 셸에 `DATA_GO_KR_SERVICE_KEY`를 export 한 상태): ```bash DATA_GO_KR_SERVICE_KEY=발급받은_Decoding_키 mcp-proxy --transport streamablehttp --port 8080 -- npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` **3단계:** StudioAssist에 다음 설정으로 서버를 추가한다: - **Connection URL:** `http://127.0.0.1:8080/mcp` - **Transport type:** `HTTP`
Kiro **Kiro Settings**에서 `Configure MCP` → `Open Workspace or User MCP Config` → 위 config를 사용한다. 또는 **Activity Bar** → `Kiro` → `MCP Servers` → `Open MCP Config`에서 위 config를 사용한다.
Mistral Vibe `~/.vibe/config.toml`에 추가한다: ```toml [[mcp_servers]] name = "narajangteo-bid" transport = "stdio" command = "npx" args = ["-y", "@opendata-kr/narajangteo-bid-mcp@latest"] env = { DATA_GO_KR_SERVICE_KEY = "발급받은_Decoding_키" } ```
OpenCode `opencode.json`에 추가한다. 없으면 `~/.config/opencode/opencode.json`에 만든다 (가이드): ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "narajangteo-bid": { "type": "local", "command": ["npx", "-y", "@opendata-kr/narajangteo-bid-mcp@latest"], "environment": { "DATA_GO_KR_SERVICE_KEY": "발급받은_Decoding_키" } } } } ```
Qoder **Qoder Settings**에서 `MCP Server` → `+ Add` → 위 config를 사용한다. 또는 MCP 가이드를 따르고 위 config를 쓴다.
Qoder CLI Qoder CLI로 서버를 추가한다 (가이드): **프로젝트 범위:** ```bash qodercli mcp add narajangteo-bid -- npx @opendata-kr/narajangteo-bid-mcp@latest ``` **전역:** ```bash qodercli mcp add -s user narajangteo-bid -- npx @opendata-kr/narajangteo-bid-mcp@latest ``` 이후 생성된 설정의 `env`(또는 셸 환경변수)에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Visual Studio **버튼으로 설치:** [Install in Visual Studio](https://vs-open.link/mcp-install?%7B%22name%22%3A%22narajangteo-bid%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40opendata-kr%2Fnarajangteo-bid-mcp%40latest%22%5D%7D) > 버튼은 키를 담지 못한다. 설치 후 서버 설정의 `env`에 `DATA_GO_KR_SERVICE_KEY`를 추가한다.
Warp `Settings | AI | Manage MCP Servers` → `+ Add`에서 [MCP 서버를 추가](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server)하고 위 config를 사용한다.
Windsurf MCP 설정 가이드를 따르고 위 config를 사용한다. Windsurf는 `mcpServers` 키를 쓴다(`~/.codeium/windsurf/mcp_config.json`).
Zed `~/.config/zed/settings.json`에 추가한다(스키마는 Zed 버전에 따라 다를 수 있으니 Zed 공식 문서를 확인): ```json { "context_servers": { "narajangteo-bid": { "command": { "path": "npx", "args": ["-y", "@opendata-kr/narajangteo-bid-mcp@latest"] }, "env": { "DATA_GO_KR_SERVICE_KEY": "발급받은_Decoding_키" } } } } ```
ChatGPT · 원격 전용 클라이언트 ChatGPT Developer Mode처럼 **원격(HTTPS) MCP만 지원하는 클라이언트**는 로컬 stdio 서버를 직접 붙일 수 없다. stdio→HTTP 브리지(`mcp-proxy`)로 이 서버를 HTTP로 띄우고 공개 HTTPS 엔드포인트(리버스 프록시·터널·호스팅)로 노출한 뒤, 그 URL을 커넥터로 등록한다. ```bash DATA_GO_KR_SERVICE_KEY=발급받은_Decoding_키 mcp-proxy --transport streamablehttp --port 8080 -- npx -y @opendata-kr/narajangteo-bid-mcp@latest ``` `http://127.0.0.1:8080/mcp`를 공개 HTTPS로 노출하는 것은 사용자 몫이다. (`mcp-remote`는 반대로 stdio 클라이언트를 원격 서버에 붙일 때 쓰는 도구라 여기엔 맞지 않는다.)
### 발견성 이 서버는 MCP 레지스트리에 `io.github.opendata-kr/narajangteo-bid-mcp`로 기술된다. [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io)를 지원하는 클라이언트에서 검색·설치할 수 있다. ## 환경변수 | 환경변수 | 필수 | 비밀 | 기본값 | 설명 | |---|---|---|---|---| | `DATA_GO_KR_SERVICE_KEY` | 예 | 예 | (없음) | 공공데이터포털 **Decoding(원본)** 인증키 | | `DATA_GO_KR_BASE_URL` | 아니오 | 아니오 | `https://apis.data.go.kr/1230000/ad/BidPublicInfoService` | 서비스 경로를 포함한 전체 URL 오버라이드 | | `DATA_GO_KR_DOWNLOAD_DIR` | 아니오 | 아니오 | `~/Downloads` | `download_attachments` 저장 기준 디렉터리. 공고번호별 하위폴더로 저장 | | `DATA_GO_KR_DOWNLOAD_TIMEOUT_MS` | 아니오 | 아니오 | `60000` | `download_attachments` 파일 다운로드 타임아웃(ms) | | `DATA_GO_KR_DOWNLOAD_MAX_BYTES` | 아니오 | 아니오 | `104857600` | `download_attachments` 파일당 다운로드 크기 상한(바이트, 기본 100MB) | ## 도구 10개 도구 중 8개는 읽기 전용 조회(`readOnlyHint: true`)다. `download_attachments`·`read_attachment`는 첨부 파일을 디스크에 저장할 수 있어 읽기 전용이 아니다(`readOnlyHint: false`). 업무구분·항목별 병렬 조회 도구는 `results`에 조회 단위(업무구분 또는 항목 라벨)마다 성공 시 `{ status: "ok", totalCount, invalidCount, items }`, 실패 시 `{ status: "error", error }`를 담는다. 일부가 실패해도 나머지 결과는 반환하며(부분 실패 표면화), `anySucceeded`는 하나라도 성공했는지를 나타낸다. `invalidCount`는 응답 스키마 검증에서 탈락해 `items`에서 제외된 건수다. 0이 아니면 API 응답 필드가 예고 없이 바뀐 신호이므로 [이슈로 알려주면](https://github.com/opendata-kr/narajangteo-bid-mcp/issues) 반영한다. 병렬 조회는 조회 단위 수만큼 API 요청을 소모하므로 업무구분을 알면 지정해 인증키 일일 트래픽을 아낀다. | 도구 | 설명 | |---|---| | `search_bid_notices` | 키워드·기간·기관·지역·업종·추정가격으로 입찰공고 검색 | | `get_bid_notice` | 입찰공고번호로 단건 상세 조회 | | `get_bid_basis_amount` | 기초금액·평가기준금액·예비가격범위율 조회 | | `get_bid_evaluation` | 낙찰가 산식A(합산항목)·평가주력분야 조회 | | `get_bid_change_history` | 공고 변경이력(정정·변경 항목) 조회 | | `get_bid_eligibility` | 면허제한·참가가능지역 조회 | | `get_bid_items` | 구매대상물품(품명·수량·단가 등) 조회 | | `get_bid_attachments` | 공고 첨부파일(공고문·규격서·제안요청서 등)의 파일명·URL 조회 | | `download_attachments` | 첨부를 전부 디스크에 내려받고 ZIP은 풀어 읽을 수 있는 파일 목록(매니페스트) 반환 (파일 저장) | | `read_attachment` | 파일 목록의 index로 파일 하나의 본문 텍스트(HWPX·구형 HWP·구형 DOC) 읽기 | ### `search_bid_notices` 키워드, 기간, 기관, 지역, 업종, 추정가격으로 입찰공고를 검색한다. 업무구분 미지정 시 기타(etc) 제외 4구분(공사/용역/물품/외자)을 병렬 검색한다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidKind` | `string[]` | 업무구분 배열: `cnstwk`(공사) `servc`(용역) `thng`(물품) `frgcpt`(외자) `etc`(기타). 미지정 시 기타 제외 4구분(API 요청 4건 소모), 기타공고는 명시로 옵트인 | | `keyword` | `string` | 공고명 부분 검색 | | `startDate` | `string` | 공고게시 시작일 `YYYYMMDD`. 미지정 시 최근 30일 자동 적용 | | `endDate` | `string` | 공고게시 종료일 `YYYYMMDD` | | `institution` | `string` | 공고기관명 | | `demandInstitution` | `string` | 수요기관명 | | `region` | `string` | 참가제한지역명 (예: 인천광역시) | | `industry` | `string` | 업종명 | | `minPrice` | `number` | 추정가격 하한(원) | | `maxPrice` | `number` | 추정가격 상한(원) | | `page` | `number` | 페이지 번호(기본 1) | | `pageSize` | `number` | 페이지당 건수(기본 10, 최대 100) | 조회창(`startDate`~`endDate`)은 최대 31일이다. 하나만 지정하면 그 날짜를 기준으로 30일 창을 채운다. 반환: `{ query, anySucceeded, results }`. `results`는 업무구분별 `BidNotice[]`를 담는다. ### `get_bid_notice` 입찰공고번호로 단건 조회한다. 업무구분 미지정 시 기타(etc)를 제외한 4구분에서 순차 조회하며, 기타공고는 `bidKind`에 `etc`를 명시한다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호 (예: `R25BK00932003`). 필수 | | `bidKind` | `string` | 업무구분(`cnstwk`·`servc`·`thng`·`frgcpt`·`etc`). 미지정 시 기타 제외 4구분에서 순차 조회(API 요청 최대 4건), 기타공고는 `etc` 명시 | 반환: `{ found, bidKind, notice, searchedKinds, errors }`. 찾으면 `found: true`와 `notice`(`BidNotice`), 못 찾으면 `found: false`. `errors`는 조회 중 발생한 오류 메시지다. ### `get_bid_basis_amount` 입찰공고번호로 기초금액·평가기준금액·예비가격범위율을 조회한다. 기초금액은 물품·공사·용역 3구분만 존재한다(외자·기타 없음). | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `bidKind` | `string` | 업무구분(`thng`=물품 `cnstwk`=공사 `servc`=용역). 미지정 시 3구분 병렬 조회(API 요청 3건) | 반환: `{ bidNtceNo, anySucceeded, results }`. `results`는 업무구분별 `BidBasisAmount[]`를 담는다. ### `get_bid_evaluation` 입찰공고번호로 낙찰가 산정 산식A(국민연금·건강보험료 등 합산항목)와 평가대상 주력분야를 조회한다. 업무구분 구분 없이 단일 조회한다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | 반환: `{ bidNtceNo, anySucceeded, results }`. `results`는 `priceFormula`(산식A)와 `targetField`(평가주력분야) 두 키로 `BidEvaluation[]`를 담는다. ### `get_bid_change_history` 입찰공고번호로 공고의 변경이력(정정·변경 항목, 변경 전/후 값)을 조회한다. 변경이력은 물품·공사·용역 3구분만 존재한다(외자·기타 없음). 변경 이력이 없는 공고는 빈 결과를 반환한다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `bidKind` | `string` | 업무구분(`thng`=물품 `cnstwk`=공사 `servc`=용역). 미지정 시 3구분 병렬 조회(API 요청 3건) | 반환: `{ bidNtceNo, anySucceeded, results }`. `results`는 업무구분별 `BidChange[]`를 담는다. ### `get_bid_eligibility` 입찰공고번호와 공고차수로 면허제한과 참가가능지역을 조회한다. 면허제한·참가가능지역은 공고차수 단위로 갈리므로 `bidNtceOrd`를 정확히 넘겨야 한다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `bidNtceOrd` | `string` | 입찰공고차수(예: `000`). `get_bid_notice` 결과의 `bidNtceOrd`에서 확인. 미지정 시 `000` | 반환: `{ bidNtceNo, bidNtceOrd, anySucceeded, results }`. `results`는 `licenseLimit`(면허제한)과 `region`(참가가능지역) 두 키로 `BidEligibility[]`를 담는다. ### `get_bid_items` 입찰공고번호와 공고차수로 구매대상물품(품명·수량·단가·납품장소 등)을 조회한다. 구매대상물품은 물품·용역·외자 3구분만 존재한다(공사 없음). | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `bidNtceOrd` | `string` | 입찰공고차수(예: `000`). `get_bid_notice` 결과의 `bidNtceOrd`에서 확인. 미지정 시 `000` | | `bidKind` | `string` | 업무구분(`thng`=물품 `servc`=용역 `frgcpt`=외자). 미지정 시 3구분 병렬 조회(API 요청 3건) | 반환: `{ bidNtceNo, bidNtceOrd, anySucceeded, results }`. `results`는 업무구분별 `BidItem[]`를 담는다. ### `get_bid_attachments` 입찰공고번호로 그 공고의 첨부파일 파일명·URL을 조회한다. 공고 본문 규격첨부(공고문·규격서·제안요청서·과업지시서 등)를 주 소스로, e발주·혁신장터 최종제안요청서(RFP) 첨부를 함께 반환한다. 파일 자체는 내려받지 않고 URL만 반환한다. 파일을 내려받아 목록을 얻으려면 `download_attachments`, 본문을 읽으려면 `read_attachment`를 쓴다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | 반환: `{ bidNtceNo, anySucceeded, results }`. `results`는 `notice`(공고 규격첨부)·`eorder`(e발주)·`innovationRfp`(혁신장터 RFP) 세 키로 `BidAttachment[]`를 담는다. ### `download_attachments` 공고 첨부를 전부 디스크에 내려받고 ZIP은 풀어(원본 ZIP은 삭제), 읽을 수 있는 **파일 목록(카탈로그)**을 반환한다(본문 텍스트는 담지 않는다). 개별 파일 본문은 이 목록의 `index`를 `read_attachment`에 줘서 읽는다. `get_bid_attachments`가 URL만 돌려주는 데 반해, 이 도구는 실제 파일을 확보하고 목록을 만든다. > [!IMPORTANT] > 이 도구는 읽기 전용이 아니다(`readOnlyHint: false`). 첨부를 `<저장 디렉터리>/<공고번호>/` 아래에 **평탄하게**(ZIP 내부 파일도 같은 폴더로 풀어) 저장하고, 목록을 그 폴더의 `.attachments-manifest.json`에 남긴다. 저장 위치는 `DATA_GO_KR_DOWNLOAD_DIR`(미설정 시 `~/Downloads`)로 정한다. 이미 만든 목록·파일은 재사용한다(전 첨부를 한 번 내려받아야 목록의 `index`가 생기므로, 파일 하나만 읽으려 해도 첫 호출은 전체를 받는다). 첨부가 나중에 바뀌면 `refresh: true`로 다시 호출해 새로 받는다. ZIP은 목록에서 사라지고 내부 파일이 항목으로 펼쳐지며 `container`에 원본 ZIP명이 담긴다(중첩 ZIP은 재귀하지 않고 파일로만 남긴다). 각 항목의 `extractable: true`면 `read_attachment`로 본문(HWPX·구형 HWP·구형 DOC)을 읽을 수 있고, `false`(PDF·이미지 등)면 파일만 저장돼 있으니 `savedPath`로 직접 연다. | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `refresh` | `boolean` | `true`면 디스크 캐시를 무시하고 모든 첨부를 새로 내려받아 목록을 다시 만든다(기본 `false`는 재사용) | 반환: `{ bidNtceNo, anySucceeded, resolveErrors?, files, truncatedFileList? }`. `files`는 파일 카탈로그로, 각 항목은 `index`·`fileNm`·`container?`(담고 있던 ZIP명)·`format`(`hwpx`/`hwp`/`doc`/`other`)·`extractable`·`byteSize`·`savedPath`·`note?`(미해제 사유 등)를 담는다. `resolveErrors`는 첨부 소스(`notice`·`eorder`·`innovationRfp`)별 조회 실패 메시지, `truncatedFileList: true`는 파일 수가 상한(50)을 넘어 목록이 잘렸다는 신호다(본문 잘림을 뜻하는 `read_attachment`의 `truncated`와 다르다). ### `read_attachment` `download_attachments`가 준 파일 목록에서 `index`로 파일 하나를 골라 본문 텍스트를 읽는다. HWPX·구형 HWP·구형 DOC를 추출하며(그 외 `format`은 `extractable: false`라 본문이 비고 `savedPath`로 직접 열어야 한다) ZIP 내부 파일도 `index`로 직접 읽는다(`container`에 원본 ZIP명). 이미 내려받은 파일은 디스크에서 재사용하고, 목록이 아직 없으면 자동으로 먼저 내려받는다. > [!IMPORTANT] > 이 도구도 목록이 없으면 첨부를 내려받으므로 읽기 전용이 아니다(`readOnlyHint: false`). | 파라미터 | 타입 | 설명 | |---|---|---| | `bidNtceNo` | `string` | 입찰공고번호. 필수 | | `index` | `number` | 읽을 파일의 목록 인덱스(0-base). `download_attachments` 응답 `files[].index`. 필수 | | `offset` | `number` | 본문 텍스트의 시작 문자 오프셋(기본 0). 긴 문서를 이어 읽을 때 이전 응답의 `textLength` 위치를 준다 | | `maxChars` | `number` | 반환 문자 상한(기본 50000). `truncated: true`면 `offset`을 이 반환분(`text` 길이)만큼 올려 다음 구간을 읽는다 | 반환: `{ bidNtceNo, index, fileNm, container?, format, extractStatus, extractError?, byteSize, savedPath, text, textLength, truncated }`. `extractStatus`는 `full`(본문 전량)/`preview`(구형 HWP 등에서 미리보기 텍스트만 추출)/`unsupported`(추출 대상 아님)/`error`(추출 실패). `truncated: true`는 본문의 다음 구간이 남았다는 신호다. ## 응답 필드 아래 각 타입은 조회 키인 `bidNtceNo`(공고번호)·`bidNtceOrd`(공고차수, 있는 경우)를 함께 포함한다. ### `BidNotice` `bidNtceNm`(공고명), `ntceInsttNm`(공고기관), `dminsttNm`(수요기관), `bidNtceDt`(공고일시), `bidClseDt`(마감일시), `opengDt`(개찰일시), `presmptPrce`(추정가격), `bidNtceDtlUrl`(상세 URL), `bidMethdNm`(입찰방법), `cntrctCnclsMthdNm`(계약체결방법), `bidPrtcptLmtYn`(투찰제한여부), `prtcptLmtRgnNm`(참가제한지역), `cmmnSpldmdMethdNm`(공동수급방식). ### `BidBasisAmount` `bssamt`(기초금액), `evlBssAmt`(평가기준금액), `rsrvtnPrceRngBgnRate`(예비가격범위율 하한), `rsrvtnPrceRngEndRate`(예비가격범위율 상한), `bssamtOpenDt`(기초금액 개찰일시). ### `BidEvaluation` `prearngPrceDcsnMthdNm`(예정가격결정방법), `bidPrceCalclAOpenDt`(산식A 개찰일시), `npnInsrprm`(국민연금보험료), `mrfnHealthInsrprm`(건강보험료), `qltyMngcst`(품질관리비), `sftyMngcst`(안전관리비), `sftyChckMngcst`(안전점검비), `tmpNm`(평가주력분야명). ### `BidChange` `chgDt`(변경일시), `chgItemNm`(변경항목명), `bfchgVal`(변경전값), `afchgVal`(변경후값), `chgDataDivNm`(변경구분명). ### `BidEligibility` `lcnsLmtNm`(면허제한명), `permsnIndstrytyList`(허용업종목록), `prtcptPsblRgnNm`(참가가능지역명). ### `BidItem` `prdctClsfcNoNm`(품명), `dtilPrdctClsfcNoNm`(세부품명), `qty`(수량), `unit`(단위), `uprc`(단가), `dlvrPlce`(납품장소), `dlvrTmlmtDt`(납품기한). ### `BidAttachment` `fileNm`(파일명), `fileUrl`(파일URL), `docDivNm`(문서구분명). ## 개발 ```bash nvm use # Node 24 pnpm install pnpm test # vitest pnpm typecheck # tsc --noEmit pnpm build # tsup, dist/ 생성 ``` ## 문제 해결 - **인증 오류(코드 30)**: Encoding 키를 넣으면 이중 인코딩으로 실패한다. **Decoding(원본)** 키를 쓴다. 서버가 시작 시 Encoding 키로 보이면 경고 로그를 남긴다. - **결과코드 메시지**: 트래픽 초과, 인증키 만료 등 data.go.kr 결과코드는 한국어 메시지로 정규화되어 반환된다. - **도구 동작 점검**: MCP inspector로 직접 호출해 볼 수 있다. ```bash npx @modelcontextprotocol/inspector npx -y @opendata-kr/narajangteo-bid-mcp ``` ## 라이선스 [MIT](./LICENSE)