mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Accept copied KIPRIS portal keys without request corruption
KIPRIS Plus users commonly paste the percent-encoded ServiceKey shown by the portal. The helper now normalizes that key once before query serialization, adds regression coverage for explicit and env-driven inputs, and clarifies the documentation so copied portal keys remain valid instead of being double- encoded on the wire. Constraint: KIPRIS Plus still expects the standard ServiceKey query parameter contract Rejected: Preserve raw percent signs during urlencode only for ServiceKey | more brittle than normalizing once at the boundary Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep ServiceKey normalization at the input boundary so future request-building changes do not reintroduce double-encoding Tested: python3 scripts/patent_search.py --help; python3 scripts/patent_search.py --query '배터리' --service-key dummy; python3 scripts/patent_search.py --application-number 1020240001234 --service-key dummy; PYTHONPATH=.:scripts python3 -m unittest scripts.test_patent_search; node --test scripts/skill-docs.test.js; npm run lint; npm run typecheck; npm test Not-tested: Live KIPRIS Plus lookup with a real KIPRIS_PLUS_API_KEY
This commit is contained in:
parent
7880436b69
commit
3c5bc5b4e9
8 changed files with 52 additions and 6 deletions
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. `KIPRIS_PLUS_API_KEY` 또는 `--service-key` 로 ServiceKey를 확보한다.
|
||||
1. `KIPRIS_PLUS_API_KEY` 또는 `--service-key` 로 ServiceKey를 확보한다. 공공데이터포털에서 복사한 percent-encoded 값이어도 helper가 한 번 정규화한 뒤 요청한다.
|
||||
2. 키워드 검색이면 `getWordSearch` 를 호출한다.
|
||||
3. 출원번호 상세 조회면 `getBibliographyDetailInfoSearch` 를 호출한다.
|
||||
4. XML `response/header/body/items/item` 구조를 파싱한다.
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ shared HTTP가 필요하면 upstream Docker guide 대로 서버를 한 번 띄
|
|||
|
||||
- helper 환경변수는 `KIPRIS_PLUS_API_KEY`
|
||||
- 실제 API 요청에서는 이 값을 `ServiceKey` 쿼리 파라미터로 보낸다
|
||||
- 공공데이터포털에서 복사한 percent-encoded key를 그대로 넣어도 helper가 한 번 정규화해서 double-encoding 없이 보낸다
|
||||
- KIPRIS Plus / 공공데이터포털 안내 기준으로 개발계정은 자동승인, 운영계정은 심의승인 대상이다
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -68,6 +68,6 @@ KSKILL_PROXY_BASE_URL=https://your-proxy.example.com
|
|||
- `AIR_KOREA_OPEN_API_KEY`
|
||||
- `KSKILL_PROXY_BASE_URL`
|
||||
|
||||
`LAW_OC` 는 `korean-law-mcp` 가 법제처 Open API 를 호출할 때 쓰는 표준 변수명이다. 이 값은 로컬 CLI/로컬 MCP server 경로에서만 사용자 쪽에 필요하고, upstream remote MCP endpoint 예시는 사용자 `LAW_OC` 없이 `url`만 등록한다. `DATA_GO_KR_API_KEY` 는 upstream `real-estate-mcp` 의 로컬/stdio/self-host 경로에서 쓰는 표준 변수명이다. `KIPRIS_PLUS_API_KEY` 는 한국 특허 정보 검색 helper가 KIPRIS Plus Open API에 보낼 `ServiceKey` 값을 담는 표준 변수명이다. public 공유용 Cloudflare Tunnel/Auth0/operator secret은 사용자 기본 secrets 파일에 넣지 않는다. 프록시 운영자 문맥에서는 upstream 환경변수 `SEOUL_OPEN_API_KEY` 도 사용할 수 있다. 다만 일반 사용자/client 쪽 기본 secrets 파일에는 넣지 않는다. `KSKILL_PROXY_BASE_URL` 도 서울 지하철 route가 실제 배포된 proxy URL 로만 넣는다.
|
||||
`LAW_OC` 는 `korean-law-mcp` 가 법제처 Open API 를 호출할 때 쓰는 표준 변수명이다. 이 값은 로컬 CLI/로컬 MCP server 경로에서만 사용자 쪽에 필요하고, upstream remote MCP endpoint 예시는 사용자 `LAW_OC` 없이 `url`만 등록한다. `DATA_GO_KR_API_KEY` 는 upstream `real-estate-mcp` 의 로컬/stdio/self-host 경로에서 쓰는 표준 변수명이다. `KIPRIS_PLUS_API_KEY` 는 한국 특허 정보 검색 helper가 KIPRIS Plus Open API에 보낼 `ServiceKey` 값을 담는 표준 변수명이다. 공공데이터포털에서 복사한 percent-encoded key도 helper가 한 번 정규화한 뒤 요청한다. public 공유용 Cloudflare Tunnel/Auth0/operator secret은 사용자 기본 secrets 파일에 넣지 않는다. 프록시 운영자 문맥에서는 upstream 환경변수 `SEOUL_OPEN_API_KEY` 도 사용할 수 있다. 다만 일반 사용자/client 쪽 기본 secrets 파일에는 넣지 않는다. `KSKILL_PROXY_BASE_URL` 도 서울 지하철 route가 실제 배포된 proxy URL 로만 넣는다.
|
||||
|
||||
이 레포의 credential-bearing skill은 전부 이 정책을 전제로 작성한다. 자세한 공통 설치 절차는 [공통 설정 가이드](setup.md)를 본다.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ remote MCP endpoint는 사용자 `LAW_OC` 없이 `url`만으로 연결한다.
|
|||
|
||||
한국 부동산 실거래가 조회의 로컬/self-host 경로는 upstream `real-estate-mcp` 가 읽는 `DATA_GO_KR_API_KEY` 를 채운다. 2026-04-05 기준 upstream 문서에는 고정 public endpoint가 없어 self-host를 기본으로 보고, Cloudflare Tunnel/operator secret은 운영자별 값이라 기본 client secrets 파일에는 넣지 않는다.
|
||||
|
||||
한국 특허 정보 검색의 KIPRIS Plus 경로용 `KIPRIS_PLUS_API_KEY` 는 helper가 읽는 표준 변수명이다. 실제 HTTP 요청에서는 같은 값을 `ServiceKey` 쿼리 파라미터로 보낸다.
|
||||
한국 특허 정보 검색의 KIPRIS Plus 경로용 `KIPRIS_PLUS_API_KEY` 는 helper가 읽는 표준 변수명이다. 실제 HTTP 요청에서는 같은 값을 `ServiceKey` 쿼리 파라미터로 보낸다. 공공데이터포털에서 복사한 percent-encoded key도 helper가 한 번 정규화해서 그대로 쓸 수 있다.
|
||||
|
||||
## 확인
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ chmod 0600 ~/.config/k-skill/secrets.env
|
|||
|
||||
한국 부동산 실거래가 조회는 upstream `real-estate-mcp` 의 로컬/stdio/self-host 경로를 쓸 때 `DATA_GO_KR_API_KEY` 를 채운다. 2026-04-05 기준 고정 public endpoint는 확인하지 못했으므로 shared URL이 필요하면 self-host + Cloudflare Tunnel + launchd(systemd) 운영을 먼저 설명한다.
|
||||
|
||||
한국 특허 정보 검색은 KIPRIS Plus Open API 경로를 쓸 때 `KIPRIS_PLUS_API_KEY` 를 채운다. helper는 이 값을 읽어 실제 요청에서 `ServiceKey` 쿼리 파라미터로 보낸다.
|
||||
한국 특허 정보 검색은 KIPRIS Plus Open API 경로를 쓸 때 `KIPRIS_PLUS_API_KEY` 를 채운다. helper는 이 값을 읽어 실제 요청에서 `ServiceKey` 쿼리 파라미터로 보낸다. 공공데이터포털에서 복사한 percent-encoded key도 그대로 넣어도 된다.
|
||||
|
||||
### Missing secret response template
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ v1 범위:
|
|||
|
||||
## Workflow
|
||||
|
||||
1. `KIPRIS_PLUS_API_KEY` 또는 `--service-key` 로 ServiceKey를 확보한다.
|
||||
1. `KIPRIS_PLUS_API_KEY` 또는 `--service-key` 로 ServiceKey를 확보한다. 공공데이터포털에서 복사한 percent-encoded 값도 helper가 한 번 정규화해서 그대로 받을 수 있다.
|
||||
2. 키워드 검색이면 `getWordSearch` endpoint를 호출한다.
|
||||
3. 출원번호 상세 조회면 `getBibliographyDetailInfoSearch` endpoint를 호출한다.
|
||||
4. XML 응답의 header/body/items 구조를 파싱한다.
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ def parse_positive_int(raw_value: str) -> int:
|
|||
def resolve_service_key(explicit_key: str | None = None) -> str:
|
||||
candidate = clean_text(explicit_key) or clean_text(os.getenv(SERVICE_KEY_ENV_VAR))
|
||||
if candidate:
|
||||
return candidate
|
||||
return urllib.parse.unquote(candidate)
|
||||
raise ValueError(
|
||||
f"missing {SERVICE_KEY_ENV_VAR}. Export {SERVICE_KEY_ENV_VAR} or pass --service-key "
|
||||
"(mapped to the KIPRIS Plus ServiceKey query parameter)."
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ from scripts.patent_search import (
|
|||
PatentSearchResult,
|
||||
build_detail_params,
|
||||
build_search_params,
|
||||
fetch_xml,
|
||||
get_patent_detail,
|
||||
main,
|
||||
parse_args,
|
||||
parse_patent_detail_response,
|
||||
parse_patent_search_response,
|
||||
resolve_service_key,
|
||||
search_patents,
|
||||
)
|
||||
|
||||
|
|
@ -163,6 +165,49 @@ class RequestBuilderTest(unittest.TestCase):
|
|||
)
|
||||
|
||||
|
||||
class ServiceKeyEncodingTest(unittest.TestCase):
|
||||
def test_resolve_service_key_accepts_percent_encoded_portal_value(self):
|
||||
self.assertEqual(resolve_service_key("abc%2Bdef%3D%3D"), "abc+def==")
|
||||
|
||||
def test_resolve_service_key_decodes_percent_encoded_env_value(self):
|
||||
with mock.patch.dict(
|
||||
"scripts.patent_search.os.environ",
|
||||
{"KIPRIS_PLUS_API_KEY": "abc%2Bdef%3D%3D"},
|
||||
clear=True,
|
||||
):
|
||||
self.assertEqual(resolve_service_key(), "abc+def==")
|
||||
|
||||
def test_fetch_xml_does_not_double_encode_percent_encoded_service_key(self):
|
||||
captured = {}
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return b"<response><header><resultCode>00</resultCode></header></response>"
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
captured["url"] = request.full_url
|
||||
captured["timeout"] = timeout
|
||||
return FakeResponse()
|
||||
|
||||
with mock.patch("urllib.request.urlopen", side_effect=fake_urlopen):
|
||||
fetch_xml(
|
||||
"https://example.test/patent",
|
||||
build_search_params(query="배터리", service_key=resolve_service_key("abc%2Bdef%3D%3D")),
|
||||
timeout=7,
|
||||
)
|
||||
|
||||
self.assertEqual(captured["timeout"], 7)
|
||||
self.assertIn("ServiceKey=abc%2Bdef%3D%3D", captured["url"])
|
||||
self.assertNotIn("%252B", captured["url"])
|
||||
self.assertNotIn("%253D", captured["url"])
|
||||
|
||||
|
||||
class PatentSearchWorkflowTest(unittest.TestCase):
|
||||
def test_search_patents_uses_fetcher_and_returns_parsed_report(self):
|
||||
calls = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue