# korean-patent-mcp **특허 출원번호나 기술 설명을 넣으면, 그 권리가 지금 살아있는지를 판정해서 돌려주는 MCP 서버.** KIPRIS Plus 기반. 도구는 3개다. 늘리지 않는다. ```bash npx korean-patent-mcp@latest setup ``` ![korean-patent-mcp 데모](https://raw.githubusercontent.com/didrod205/korean-patent-mcp/main/docs/demo.gif) 도구 3개의 실제 실행 결과입니다. 재현한 화면이 아니라 KIPRIS·등록원부에서 받은 값입니다. --- ## 왜 필요한가 LLM에게 특허를 물으면 세 가지 방식으로 틀린다. | 거짓말 | 이 서버가 잡는 법 | |---|---| | 없는 특허번호를 지어낸다 | `exists: false` | | 실재하는 번호에 엉뚱한 명칭을 붙인다 | `title_match: "mismatch"` | | **20년 전에 소멸한 특허를 유효하다고 쓴다** | `alive: false` | 세 번째가 제일 위험하다. 번호도 실재하고 명칭도 맞는데, 그 권리는 이미 죽어 있다. 검색만 하는 도구는 이걸 절대 못 잡는다. 그래서 이 서버는 **검색 결과에도 `alive`를 강제로 붙인다.** --- ## 이렇게 물어보면 된다 설치하고 나면 클라이언트에서 그냥 한국어로 물으면 된다. > **"10-2019-0123456 이 특허 아직 살아있어?"** > **"이 IR 자료에 적힌 특허번호들 진짜인지 전부 확인해줘"** *(문서를 그대로 붙여넣기)* > **"무선 충전 코일 정렬 관련 국내 특허 중에 아직 유효한 것만 찾아줘"** --- ## 도구 3개 ### 1. `rights_alive(number)` 번호 하나 → 생사 판정. 이 서버의 존재 이유. ```json { "alive": false, "status": "소멸(연차료 불납)", "stage": "소멸", "number": "10-2000-0012345", "title": "카세트 테이프 권취 장치", "holder": "다라산업", "expiry": "2020-05-10", "expiry_estimated": true, "raw_status": "소멸(연차료 불납)", "basis": "상태 문자열 \"소멸(연차료 불납)\"에서 소멸 신호 검출", "warnings": ["holder는 출원인 기준입니다. 등록 후 권리가 양도되었으면 현재 권리자와 다를 수 있습니다."], "latest_event": { "date": "2020-08-01", "description": "연차료 불납에 의한 소멸" }, "checked_at": "2026-09-04", "source_url": "https://www.kipris.or.kr/khome/search/searchResult.do?tab=patent&query=10-2000-0012345" } ``` **`alive`만 보지 말고 `stage`도 보라.** `alive: false`에는 정반대 두 가지가 섞여 있다. | stage | alive | 뜻 | |---|---|---| | `등록유효` | `true` | 지금 행사 가능한 권리 | | `소멸` | `false` | 있었는데 죽었다. 등록원부를 붙이면 **연차료 불납**인지 **존속기간 만료**인지까지 갈린다 | | `출원종료` | `false` | 등록 못 하고 끝났다 (거절·취하·포기) | | `출원계속` | `false` | **아직 안 태어났다.** 심사 중이라 장래 등록될 수 있다 | | `불명` | `false` | 상태값을 해석 못 했다. `raw_status`를 직접 보라 | FTO 관점에서 `소멸`은 안전 신호이고 `출원계속`은 위험 신호다. 둘 다 `alive: false`지만 의미가 반대다. ### 2. `verify_citations(text)` LLM 답변을 통째로 넣으면 인용된 특허번호를 전부 검증한다. ```json { "all_clear": false, "total_found": 3, "checked": 3, "citations": [ { "number": "10-2019-0123456", "cited_as": "10-2019-0123456", "exists": true, "claimed_title": "인공지능 기반 신약 후보물질 탐색 방법", "actual_title": "무선 충전 장치 및 그 제어 방법", "title_match": "mismatch", "title_similarity": 0.04, "alive": true, "verdict": "title_mismatch", "note": "명칭 불일치. 인용: \"인공지능 기반 신약 후보물질 탐색 방법\" / 실제: \"무선 충전 장치 및 그 제어 방법\"" } ], "warnings": ["명칭 불일치 1건: 10-2019-0123456 — 번호는 실재하나 다른 발명입니다."], "summary": "인용 3건 중 1건에 문제가 있습니다." } ``` `verdict`는 `ok` / `dead` / `title_mismatch` / `not_found` / `pending` / `unknown` 중 하나다. ### 3. `search_ip(query, ...)` 검색. 단, **모든 결과에 `alive`가 붙는다.** 죽은 권리는 `display: "dimmed"`로 표시되고 뒤로 정렬된다. ```json { "query": "무선 충전 코일 정렬", "total_matched": 412, "inspected": 20, "alive_in_inspected": 7, "dead_in_inspected": 13, "coverage_warning": "전체 412건 중 20건만 판정했습니다. ...", "results": [ { "alive": true, "display": "normal", "status": "등록유효 (만료예정 2039-03-14)", "number": "10-2019-0123456", "…": "…" }, { "alive": false, "display": "dimmed", "status": "소멸(존속기간 만료)", "number": "10-2001-0009999", "…": "…" } ], "notes": ["생사 판정은 검색 결과의 등록상태 문자열에 근거합니다. 특정 건을 근거로 삼기 전에 그 번호로 rights_alive를 호출해 확정하세요."] } ``` `alive_only: true`를 주면 살아있는 것만 나온다. > **집계는 `inspected` 기준이지 `total_matched` 기준이 아니다.** 전체를 다 보지 못했으면 > `coverage_warning`이 채워진다. `alive_in_inspected: 0`을 "그런 유효 특허가 없다"로 >읽으면 안 된다 — 21,060건 중 10건만 본 결과일 수 있다. --- ## 설치 ### 1. KIPRIS 서비스 키 발급 (무료, 승인 대기 있음) **KIPRIS Plus에서 직접 신청한다.** 공공데이터포털(data.go.kr)이 아니다. → 회원가입 → 서비스 신청 → **"특허·실용 공개·등록공보"** (국내 IP데이터 > 공보 > 특허·실용, REST) 발급된 **ServiceKey** 를 `KIPRIS_SERVICE_KEY` 에 넣는다. **무료 이용자 구간이 있다.** 초당 호출 제한은 무료 50회 / 유료 75회다. > **공공데이터포털에서 찾지 마세요.** 포털의 KIPRISPlus 항목들은 "활용신청"이 아니라 > **"제공처 바로가기"** 로 KIPRIS Plus를 가리키기만 합니다. 예전에 있던 > `data.go.kr/data/15058788` 항목은 현재 폐지(404)됐습니다. > 포털에서 직접 신청 가능한 건 별개 API인 등록원부 실시간 조회(`15124946`) 쪽입니다. > **다른 KIPRIS 서비스 키로는 동작하지 않습니다.** KIPRIS에는 등록사항·해외특허·심판 등 > 서비스가 여러 개 있고 신청은 각각 따로 승인됩니다. 이 서버는 `patUtiModInfoSearchSevice` > 하나만 호출하므로 위 서비스를 신청해야 합니다. > 엉뚱한 서비스 키를 넣으면 모든 호출이 `SERVICE KEY IS NOT REGISTERED` 로 실패합니다. 호출 한도보다 **호출량**을 먼저 신경 쓰세요. `verify_citations`가 번호 20개를 검증하면 그것만으로 20회입니다. 서버는 같은 번호를 프로세스 내에서 1시간 캐시하고 (`KIPRIS_CACHE_TTL`로 조절), 동시 요청을 3개로 묶어 초당 제한에 여유를 둡니다. ### 2. 설치 ```bash npx korean-patent-mcp@latest setup ``` 키를 프롬프트로 받아 **실제 호출로 검증한 뒤** 선택한 클라이언트 설정 파일에 등록한다. Claude Desktop / Claude Code / Cursor / VS Code / Windsurf / Gemini CLI / Zed 지원. > **설치 전에 MCP 클라이언트를 완전히 종료하세요(Cmd+Q).** > Claude Desktop은 `claude_desktop_config.json` 을 자기 preferences 저장소로도 씁니다. > 켜진 채로 고치면 앱이 종료할 때 덮어써서 방금 추가한 서버가 조용히 사라집니다. > `setup` 이 실행 중인 클라이언트를 감지해 막아주지만, 수동 설정 시에는 직접 확인하세요. > **최초 실행은 30초쯤 걸립니다.** `npx` 가 패키지를 받는 시간입니다(2회차부터 1초 내외). > 그 사이 "연결 실패"로 보이면 클라이언트를 한 번 더 재시작하세요. > 매번 빠르게 뜨길 원하면 `npm i -g korean-patent-mcp` 후 `command` 를 > `korean-patent-mcp` 로 바꾸면 됩니다. ### 수동 설정 ```json { "mcpServers": { "korean-patent": { "command": "npx", "args": ["-y", "korean-patent-mcp@latest"], "env": { "KIPRIS_SERVICE_KEY": "여기에-키" } } } } ``` ### 키를 어디에 둘 것인가 셋 중 하나. 위에서부터 권장한다. **1) MCP 클라이언트에서 쓸 때 — `setup`이 알아서 넣는다** ```bash npx korean-patent-mcp@latest setup ``` 클라이언트 설정 파일의 `env` 블록에 기록하고 파일 권한을 `600`으로 조인다. **2) 여러 곳에서 공용으로 — 홈 설정 파일** ```bash mkdir -p ~/.config/korean-patent-mcp printf 'KIPRIS_SERVICE_KEY=발급받은키\n' > ~/.config/korean-patent-mcp/.env chmod 600 ~/.config/korean-patent-mcp/.env ``` MCP 클라이언트는 서버를 임의의 작업 디렉토리에서 띄우므로, 프로젝트 `.env`보다 이 경로가 안정적이다. 서버가 시작할 때 자동으로 읽는다. **3) 이 저장소에서 개발할 때 — 프로젝트 `.env`** ```bash cp .env.example .env && chmod 600 .env ``` `.gitignore`에 이미 들어 있어 커밋되지 않는다. 우선순위는 **실제 환경변수 > 프로젝트 `.env` > 홈 `.env`** 다. 일회성으로 다른 키를 쓰려면 그냥 앞에 붙이면 된다: ```bash KIPRIS_SERVICE_KEY='다른키' npx korean-patent-mcp probe ``` > `~/.zshrc`에 `export` 하는 방법도 되지만 권하지 않는다. > 셸을 띄우는 모든 프로세스에 키가 노출되고, dotfiles를 저장소에 올리는 사람이 많다. > **키를 이슈·PR·채팅에 붙여넣지 마세요.** 노출됐으면 KIPRIS Plus 마이페이지에서 재발급하세요. | 환경변수 | 기본값 | 설명 | |---|---|---| | `KIPRIS_SERVICE_KEY` | (필수) | KIPRIS Plus ServiceKey. 위 3가지 방법 중 하나로 설정 | | `KIPRIS_CACHE_TTL` | `3600` | 응답 캐시 TTL(초) | | `DATA_GO_KR_SERVICE_KEY` | (선택) | 등록원부 API 키 — 연차료·확정 만료일 | | `LEDGER_ENDPOINT` | (선택) | 등록원부 오퍼레이션 URL (활용가이드 PDF 참조) | | `KIPRIS_BASE_URL` | KIPRIS Plus 공식 | 엔드포인트 오버라이드 | --- ## 선택: 등록원부 연동 — 추정을 확정으로 기본 상태에서 `rights_alive`의 만료일은 **출원일 + 법정 존속기간**으로 계산한 추정치이고, 연차료 납부 여부는 확인되지 않는다. 등록원부 API를 붙이면 둘 다 확정된다. | | 기본 | 등록원부 연동 | |---|---|---| | `expiry` | 추정 (`expiry_estimated: true`) | **확정** (연장등록 반영) | | `holder` | 출원인 | **실제 등록권자** (양도 반영) | | `annual_fee` | `null` | `{ paid_year, paid_until }` | **KIPRIS Plus와 별개 API다.** 키도 신청도 따로 한다. 1. 에서 활용신청 (자동승인) 2. `DATA_GO_KR_SERVICE_KEY` = 일반 인증키(Decoding) **이게 전부다.** 엔드포인트는 기본값이 들어 있고, 번호의 권리구분을 보고 특허는 `getPatentRegisterHistory`로, 실용신안은 `getUtilityModelHistory`로 자동으로 나뉜다. 설정하면 이렇게 바뀐다: ```json { "expiry": "2034-11-26", "expiry_estimated": false, "holder": "삼성전자주식회사", "annual_fee": { "paid_year": 6, "last_paid_date": "2026-03-27", "payment_count": 4 }, "sources": ["KIPRIS 서지상세", "등록원부"], "warnings": [] } ``` `warnings`가 빈 배열인 것에 주목. 추정치도 미확인 항목도 없으니 붙일 경고가 없다. ```bash npx korean-patent-mcp probe --ledger 10-2245822 ``` **설정하지 않아도 서버는 그대로 동작한다.** 등록원부는 부가정보라, 호출이 실패해도 생사 판정은 KIPRIS 서지정보만으로 나간다. 어떤 소스를 봤는지는 응답의 `sources` 필드에 적힌다. ## `probe` — 데이터를 믿어도 되는지 먼저 확인 이 서버 전체는 두 가지 전제 위에 서 있다. 하나라도 무너지면 코드가 아무리 좋아도 거짓말을 하게 된다. 1. **소멸·포기된 권리가 응답에서 명확히 구분되는가** 2. **상태 갱신이 며칠이나 지연되는가** 이걸 눈으로 확인하지 말고 명령 하나로 판정한다. ```bash KIPRIS_SERVICE_KEY='...' npx korean-patent-mcp probe ``` ```bash # 상태를 확실히 아는 번호로 재는 쪽이 훨씬 정확하다 npx korean-patent-mcp probe 10-2019-0123456 10-1234567 # 응답 XML 원문까지 npx korean-patent-mcp probe --raw 10-2019-0123456 ``` probe가 하는 일: - 응답에 판정용 필드(`registerStatus`, `finalDisposal`, `applicationDate` …)가 실제로 있는지 확인 - 관측된 **상태값 분포**를 뽑아, 등록 계열과 소멸 계열이 서로 다른 값으로 갈리는지 판정 - **판정 규칙이 모르는 상태값**을 잡아내 (→ `src/lib/status.ts`에 추가하면 된다) - 법적상태 이력 날짜로 갱신 지연을 가늠 - 마지막에 **"이 기획은 성립한다 / 확인포인트 1 미통과"** 를 한 줄로 결론 --- ## 판정 규칙 `src/lib/status.ts` 하나에 모여 있다. 나머지는 전부 배관이다. 1. **"등록"이라는 글자만 보고 살아있다고 하지 않는다.** 죽음 어휘(소멸·무효·취소·말소·연차료 불납)를 먼저 본다. 등록된 뒤 소멸한 권리가 압도적으로 많고, 그게 정확히 사람들이 틀리는 지점이다. 2. **등록 상태라도 존속기간을 다시 계산한다.** KIPRIS 상태가 "등록"에 머물러 있어도 만료일이 지났으면 그 권리는 죽었다. 특허 = 출원일 + 20년, 실용신안 = 출원일 + 10년. 3. **모르면 모른다고 한다.** 모든 응답에 `basis`(판정 근거)와 `warnings`가 붙는다. 해석 못 한 상태값은 `stage: "불명"` + `alive: false`다 — 모르는 걸 살아있다고 하지 않는다. KIPRIS 문서가 명시한 `registerStatus` 값은 **공개 · 등록 · 거절 · 무효 · 소멸 · 취하 · 포기** 7개다. 7개 모두 규칙이 커버하며([kipris-fixture.test.ts](src/lib/kipris-fixture.test.ts)에서 고정), 이 중 `alive: true`가 되는 값은 **`등록` 하나뿐**이다. --- ## 한계 — 읽고 쓰세요 - **`expiry`는 대개 추정치다.** `expiry_estimated: true`면 출원일 + 법정 존속기간으로 계산한 값이다. **존속기간 연장등록**(의약품·농약)이 있으면 실제 만료일은 더 뒤다. - **연차료 납부 여부는 확정하지 못한다.** 등록 상태로 나와도 최근 연차료를 안 냈으면 곧 소멸한다. KIPRIS 상태 반영에는 지연이 있다. - **`holder`는 출원인 기준이다.** 등록 후 권리가 양도됐으면 현재 권리자와 다르다. 정확한 권리자는 **등록원부**를 봐야 한다. - **거래·소송·실시 판단에 이 서버의 출력을 그대로 쓰지 마세요.** 스크리닝 도구다. 최종 확인은 등록원부와 변리사다. - `search_ip`의 생사 판정은 검색 응답의 상태 문자열에만 근거한다. 특정 건을 근거로 삼기 전에 그 번호로 `rights_alive`를 다시 부르세요. --- ## 안 하는 것 (의도한 부재) - **`fto_screen`** — 침해 가능성 판단. 유사도 로직이 필요하고, 틀렸을 때 손해가 크다. - **상표·디자인** — 특허·실용신안만. `30-`·`40-` 번호는 명시적으로 거절한다. - **해외 특허** — 국내만. - **도구 10개** — 도구가 늘면 LLM이 어느 걸 부를지 헷갈리고, 그 순간 "생사 판정"이라는 유일한 약속이 흐려진다. --- ## 개발 ```bash npm install npm test # 93 tests npm run typecheck npm run build ``` ``` src/ ├── index.ts 진입점 (stdio / setup / probe) ├── tool-registry.ts 도구 3개 등록 — 여기가 늘어나면 안 된다 ├── setup.ts 대화형 설치 (키 검증 포함) ├── probe.ts 응답 진단 — 기획 폐기 여부 판정 ├── lib/ │ ├── status.ts ★ 생사 판정 엔진. 이 프로젝트의 실체 │ ├── number.ts 번호 파싱·정규화·텍스트 추출 │ ├── kipris-client.ts KIPRIS Plus HTTP (재시도·캐시·오류 해석) │ ├── ledger-client.ts 등록원부 실시간 조회 (선택 — 연차료·확정 만료일) │ ├── title-match.ts 인용 명칭 ↔ 실제 명칭 대조 │ ├── xml.ts 의존성 없는 XML 추출 │ ├── cache.ts TTL 캐시 (호출량 방어) │ └── errors.ts 오류 → 도구 응답 └── tools/ ├── rights-alive.ts ├── verify-citations.ts └── search-ip.ts ``` **상태값을 하나 더 알게 됐다면** `src/lib/status.ts`의 `DEAD_PATTERNS` / `PENDING_PATTERNS`에 추가하고 `src/lib/status.test.ts`에 케이스를 하나 넣으면 된다. `probe`가 모르는 상태값을 알려준다. --- ## 배포 `main`에 푸시하면 CI가 Node 20.19/22/24에서 typecheck·test·build·패키지 검증을 돌린다. 릴리스를 만들면 npm에 자동 배포된다: ```bash npm version patch && git push --follow-tags gh release create "v$(node -p "require('./package.json').version")" --generate-notes ``` `publish.yml`이 **릴리스 태그와 `package.json` 버전 일치를 강제**한다. 로컬 버전과 배포 버전이 어긋나는 사고를 구조적으로 막는 게이트다. 인증은 **Trusted Publishing(OIDC)** 을 쓴다. `NPM_TOKEN` 시크릿이 없다 — npm granular token은 최대 90일이라 분기마다 만료되고 그때마다 배포가 조용히 깨진다. OIDC는 GitHub가 워크플로 실행마다 발급하는 단기 증명이라 갱신할 것도, 유출될 장기 비밀도 없다. 최초 1회만 npmjs.com > 패키지 > Settings > Trusted Publisher 에 등록한다: | 항목 | 값 | |---|---| | Repository | `didrod205/korean-patent-mcp` | | Workflow | `publish.yml` | 등록 전까지는 로컬에서 `npm publish` 하면 된다 (`prepublishOnly`가 빌드와 `verify:package`를 자동으로 돌려 빌드-버전 불일치를 막는다). ## 문제가 생기면 ### 서버가 안 붙습니다 클라이언트 로그를 먼저 보세요. Claude Desktop은 `~/Library/Logs/Claude/mcp*.log` 에 원인이 그대로 찍힙니다. | 로그에 보이는 것 | 원인 | 해결 | |---|---|---| | `notarget No matching version found` | 방금 배포된 버전을 `@latest` 가 가리키는데 레지스트리 전파가 끝나지 않음 | 몇 분 뒤 클라이언트를 다시 시작 | | 아무 로그도 없음 | 클라이언트가 설정을 아직 안 읽음 | 완전 종료(Cmd+Q) 후 재시작 | | `Server disconnected` 만 반복 | 최초 실행 다운로드(30초) 중 타임아웃 | 재시작하면 캐시가 있어 1초 내에 뜸 | | `KIPRIS_SERVICE_KEY 가 없습니다` | 설정의 `env` 가 비었음 | 아래 "설정이 사라졌습니다" 참조 | ### 설정이 사라졌습니다 `mcpServers` 에 넣었는데 없어졌다면, **클라이언트가 켜진 상태에서 파일을 고쳤을 가능성이 큽니다.** Claude Desktop은 `claude_desktop_config.json` 을 자기 preferences 저장소로도 써서, 앱이 종료할 때 메모리 상태로 덮어씁니다. 완전히 종료한 뒤 다시 설정하세요. `setup` 은 실행 중인 클라이언트를 감지해 이 상황을 막습니다. ### 매번 최신을 받는 게 부담스럽다면 `args` 의 `@latest` 를 빼면 캐시된 버전을 쓰므로 전파 지연에 걸리지 않습니다. 버전을 고정하고 싶으면 `korean-patent-mcp@1.3.3` 처럼 명시하세요. ## 데이터 출처 특허·실용신안 서지정보와 등록상태는 **KIPRIS Plus**(지식재산처 / 한국특허정보원)에서, 존속기간 만료일·연차 납부 이력·등록권자는 **등록원부 실시간 정보 조회 서비스** (지식재산처, 공공데이터포털 15124946)에서 조회합니다. API 인증키는 **각자 발급**받아야 하며, 발급받은 본인만 사용할 수 있습니다. 이 소프트웨어는 인증키를 수집하거나 전송하지 않습니다. **법적 효력이 필요한 판단에는 반드시 등록원부 원본을 확인하세요.** 이 도구는 조회 결과를 가공·요약하며, 상태 반영에는 지연이 있을 수 있습니다. ## 라이선스 [MIT](./LICENSE) 제3자 구현 참조 및 데이터 출처 고지는 [NOTICE](./NOTICE)를 참조하세요. --- Made by 양경찬 @E:LAB STUDIO