From e7a167606f92a39c9671d4b1908ba5826a347bf3 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Wed, 7 Oct 2026 13:14:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AC=B4=EC=84=A4=EC=B9=98=20MCP=20?= =?UTF-8?q?=EC=B2=AB=20=EC=A1=B0=ED=9A=8C=20=EC=95=88=EB=82=B4=EC=99=80=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 1 + .github/workflows/codeql.yml | 1 + .github/workflows/coverage.yml | 1 + .github/workflows/deploy.yml | 1 + .../workflows/workers-invocations-chart.yml | 2 +- README.md | 735 ++---------------- docs/ai-instruction.md | 21 +- docs/client-guide.md | 249 ++++++ docs/service-reference.md | 172 +++- scripts/ops/README.md | 157 +++- .../ops/update-workers-invocations-chart.ts | 12 +- scripts/ops/workers-chart-helpers.ts | 6 +- skills/daiso-cli/SKILL.md | 25 +- src/index.ts | 5 + src/pages/agentStart.ts | 40 + src/pages/prompt.ts | 2 + src/pages/promptCoreText.ts | 2 +- tests/app/app-info-pages.test.ts | 13 + tests/app/app-mcp-client.test.ts | 29 + tests/pages/agentStart.test.ts | 52 ++ tests/scripts/repository-config.test.ts | 30 +- .../scripts/workers-chart-entrypoint.test.ts | 13 +- tests/scripts/workers-chart-helpers.test.ts | 7 +- 23 files changed, 885 insertions(+), 691 deletions(-) create mode 100644 docs/client-guide.md create mode 100644 src/pages/agentStart.ts create mode 100644 tests/pages/agentStart.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c754b2..38c88608 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: - main paths-ignore: - 'README.md' + - 'scripts/ops/README.md' - 'assets/analytics/**' permissions: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6c897237..f439ebc2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,6 +6,7 @@ on: - main paths-ignore: - 'README.md' + - 'scripts/ops/README.md' - 'assets/analytics/**' pull_request: branches: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f5eacac1..4afb7623 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,6 +9,7 @@ on: - main paths-ignore: - 'README.md' + - 'scripts/ops/README.md' - 'assets/analytics/**' workflow_dispatch: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 64745f51..8e307536 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,7 @@ on: - main paths-ignore: - 'README.md' + - 'scripts/ops/README.md' - 'assets/analytics/**' workflow_dispatch: diff --git a/.github/workflows/workers-invocations-chart.yml b/.github/workflows/workers-invocations-chart.yml index 5f846251..68b06122 100644 --- a/.github/workflows/workers-invocations-chart.yml +++ b/.github/workflows/workers-invocations-chart.yml @@ -69,7 +69,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png + git add scripts/ops/README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png if git diff --cached --quiet; then echo "No chart changes to commit" diff --git a/README.md b/README.md index 262452b4..d1b3e7dc 100644 --- a/README.md +++ b/README.md @@ -1,707 +1,136 @@ -[![MCP Toplist](https://mcptoplist.com/badge/glama%2Fhmmhmmhm%2Fdaiso-mcp.svg)](https://mcptoplist.com/server/glama%2Fhmmhmmhm%2Fdaiso-mcp) - -
- -Daiso MCP - -
-
- # Daiso MCP 및 Skill -다이소(제품/매장/재고), 상품 가격 비교, 주변 음식점/카페, 주유소/유가, 개발자 요청 제출, GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표), 디트릭스(독립·예술영화관/상영작/잔여 좌석) 조회 기능을 MCP, CLI, Codex Skill로 AI에 연결합니다. - -디트릭스의 지원 범위와 CLI 예시는 [독립·예술영화관 조회 가이드](docs/dtryx.md)를 참고하세요. - -
- -**MCP:** `https://mcp.aka.page` · **CLI:** `npx daiso` · **Skill:** `clawhub install daiso-cli` - -**ClawHub:** [clawhub.ai/hmmhmmhm/daiso-cli](https://clawhub.ai/hmmhmmhm/daiso-cli) - -한국 로컬 리테일, 생활 정보, 영화관 조회를 MCP, CLI, Codex Skill로 연결하는 도구입니다. -사용자는 별도 API 키를 준비하지 않고 바로 사용할 수 있습니다. - -
- -

지원 서비스

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
분류서비스조회 기능
장소네이버 지역 검색음식점, 카페, 디저트, 주변 장소
교통오피넷전국 평균 유가, 최저가 주유소, 위치 기반 주유소, 주유소 상세정보
비교다이소, GS25, 세븐일레븐, 이마트24같은 상품의 판매처별 가격 후보 비교
운영SupabaseMCP 오류, 개선 요청, 신규 기능 요청 저장
리테일다이소, 올리브영상품, 매장, 재고
편의점GS25, 세븐일레븐, CU, 이마트24상품, 매장, 재고, 인기검색어, 카탈로그
영화관CGV, 메가박스, 롯데시네마극장, 영화, 시간표, 잔여 좌석
- -
- -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020.svg)](https://workers.cloudflare.com/) -[![MCP](https://img.shields.io/badge/MCP-Protocol-8B5CF6.svg)](https://modelcontextprotocol.io/) -[![Code Coverage](https://img.shields.io/badge/Code%20Coverage-100%25-brightgreen.svg)](https://github.com/hmmhmmhm/daiso-mcp/actions/workflows/coverage.yml) -[![Coverage](https://github.com/hmmhmmhm/daiso-mcp/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/hmmhmmhm/daiso-mcp/actions/workflows/coverage.yml) -[![Better Stack Badge](https://uptime.betterstack.com/status-badges/v2/monitor/2mmhx.svg)](https://aka-page.betteruptime.com/) - -**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)** - - -

Cloudflare 요청 수 (2026-09-07 ~ 2026-10-06, 30일)

- -Cloudflare 요청 수 그래프 (2026-09-07 ~ 2026-10-06) - -기준 워커: daiso-mcp · 마지막 갱신: 2026-10-07 05:21 KST -
집계: Worker 실행 + 루트 GET 리디렉션 요청 · 사용자 수와 다릅니다. - -
- -> [!IMPORTANT] -> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요. - -
- - - -
+다이소·편의점 재고, 주변 장소, 주유소 가격, 영화 시간표를 AI에서 조회합니다. +**공개 원격 MCP에 연결하면 로컬 설치·빌드·API 키 준비 없이 바로 사용할 수 있습니다.** +에이전트는 [짧은 시작 안내](https://mcp.aka.page/llms.txt)를 참고하세요. 무설치 요청에는 npm 설치나 전체 스키마 탐색이 필요하지 않습니다. -
+## 원격 MCP로 바로 시작하기 -   +| 연결 항목 | 값 | +| :-------- | :---------------------------------- | +| 서버 URL | `https://mcp.aka.page` | +| 전송 방식 | Streamable HTTP (`http`) | +| 인증 | 없음: API 키·토큰·OAuth 설정 불필요 | -
+1. 사용하는 AI 앱의 **원격 MCP 서버 추가** 화면을 엽니다. +2. 위 URL을 입력하고 전송 방식을 묻는 경우 **HTTP / Streamable HTTP**를 선택합니다. +3. 연결된 도구를 활성화하고 아래 첫 조회를 요청합니다. -
- ---- - -
- -## AI 앱에서 MCP 연결하기 - -ChatGPT, Claude, Home Assistant, Grok 같은 AI 앱에서 바로 연결해 사용할 수 있습니다. -아래 앱별 가이드에서 먼저 연동한 뒤 검색/재고/영화 조회를 요청하세요. - -
- -### ![ChatGPT](https://img.shields.io/badge/ChatGPT-74aa9c?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0id2hpdGUiIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTE0Ljk0OSA2LjU0N2EzLjk0IDMuOTQgMCAwIDAtLjM0OC0zLjI3MyA0LjExIDQuMTEgMCAwIDAtNC40LTEuOTM0QTQuMSA0LjEgMCAwIDAgOC40MjMuMiA0LjE1IDQuMTUgMCAwIDAgNi4zMDUuMDg2YTQuMSA0LjEgMCAwIDAtMS44OTEuOTQ4IDQuMDQgNC4wNCAwIDAgMC0xLjE1OCAxLjc1MyA0LjEgNC4xIDAgMCAwLTEuNTYzLjY3OUE0IDQgMCAwIDAgLjU1NCA0LjcyYTMuOTkgMy45OSAwIDAgMCAuNTAyIDQuNzMxIDMuOTQgMy45NCAwIDAgMCAuMzQ2IDMuMjc0IDQuMTEgNC4xMSAwIDAgMCA0LjQwMiAxLjkzM2MuMzgyLjQyNS44NTIuNzY0IDEuMzc3Ljk5NS41MjYuMjMxIDEuMDk1LjM1IDEuNjcuMzQ2IDEuNzguMDAyIDMuMzU4LTEuMTMyIDMuOTAxLTIuODA0YTQuMSA0LjEgMCAwIDAgMS41NjMtLjY4IDQgNCAwIDAgMCAxLjE0LTEuMjUzIDMuOTkgMy45OSAwIDAgMC0uNTA2LTQuNzE2bS02LjA5NyA4LjQwNmEzLjA1IDMuMDUgMCAwIDEtMS45NDUtLjY5NGwuMDk2LS4wNTQgMy4yMy0xLjgzOGEuNTMuNTMgMCAwIDAgLjI2NS0uNDU1di00LjQ5bDEuMzY2Ljc3OHEuMDIuMDExLjAyNS4wMzV2My43MjJjLS4wMDMgMS42NTMtMS4zNjEgMi45OTItMy4wMzcgMi45OTZtLTYuNTMtMi43NWEyLjk1IDIuOTUgMCAwIDEtLjM2LTIuMDFsLjA5NS4wNTdMNS4yOSAxMi4wOWEuNTMuNTMgMCAwIDAgLjUyNyAwbDMuOTQ5LTIuMjQ2djEuNTU1YS4wNS4wNSAwIDAgMS0uMDIyLjA0MUw2LjQ3MyAxMy4zYy0xLjQ1NC44MjYtMy4zMTEuMzM1LTQuMTUtMS4wOThtLS44NS02Ljk0QTMuMDIgMy4wMiAwIDAgMSAzLjA3IDMuOTQ5djMuNzg1YS41MS41MSAwIDAgMCAuMjYyLjQ1MWwzLjkzIDIuMjM3LTEuMzY2Ljc3OWEuMDUuMDUgMCAwIDEtLjA0OCAwTDIuNTg1IDkuMzQyYTIuOTggMi45OCAwIDAgMS0xLjExMy00LjA5NHptMTEuMjE2IDIuNTcxTDguNzQ3IDUuNTc2bDEuMzYyLS43NzZhLjA1LjA1IDAgMCAxIC4wNDggMGwzLjI2NSAxLjg2YTMgMyAwIDAgMSAxLjE3MyAxLjIwNyAyLjk2IDIuOTYgMCAwIDEtLjI3IDMuMiAzLjA1IDMuMDUgMCAwIDEtMS4zNi45OTdWOC4yNzlhLjUyLjUyIDAgMCAwLS4yNzYtLjQ0NW0xLjM2LTIuMDE1LS4wOTctLjA1Ny0zLjIyNi0xLjg1NWEuNTMuNTMgMCAwIDAtLjUzIDBMNi4yNDkgNi4xNTNWNC41OThhLjA0LjA0IDAgMCAxIC4wMTktLjA0TDkuNTMzIDIuN2EzLjA3IDMuMDcgMCAwIDEgMy4yNTcuMTM5Yy40NzQuMzI1Ljg0My43NzggMS4wNjYgMS4zMDMuMjIzLjUyNi4yODkgMS4xMDMuMTkxIDEuNjY0ek01LjUwMyA4LjU3NSA0LjEzOSA3LjhhLjA1LjA1IDAgMCAxLS4wMjYtLjAzN1Y0LjA0OWMwLS41Ny4xNjYtMS4xMjcuNDc2LTEuNjA3cy43NTItLjg2NCAxLjI3NS0xLjEwNWEzLjA4IDMuMDggMCAwIDEgMy4yMzQuNDFsLS4wOTYuMDU0LTMuMjMgMS44MzhhLjUzLjUzIDAgMCAwLS4yNjUuNDU1em0uNzQyLTEuNTc3IDEuNzU4LTEgMS43NjIgMXYybC0xLjc1NSAxLTEuNzYyLTF6Ii8+PC9zdmc+) - -> MCP 연동이 어렵다는 피드백이 있어 바로 사용 가능한 GPT 앱을 추가했습니다. -> 아래 링크로 모바일에서도 간편하게 이용 가능합니다! - -**[Daiso MCP GPT 앱 바로가기](https://chatgpt.com/g/g-69a5266c32108191b71a24642dc63f9e-daiso-mcp)** - -빠른 사용 예시: - -``` +```text 다이소 mcp로 수납박스 검색해줘 -콜라 어디가 싼지 비교해줘 -올리브영 mcp로 명동 근처 매장 찾아줘 -이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘 -GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘 -세븐일레븐 mcp로 삼각김밥 검색해줘 -세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘 -세븐일레븐 mcp로 안산 중앙역 근처 세븐일레븐에서 핫식스 재고 알려줘 -세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘 -강남역 근처 카페 찾아줘 -성수동 근처 브런치 음식점 찾아줘 -강남역 근처 제일 싼 주유소 찾아줘 -오늘 전국 평균 휘발유 가격 알려줘 -메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘 -롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘 -롯데시네마 mcp로 월드타워 잔여 좌석 알려줘 -CGV mcp로 강남 상영 영화와 시간표 알려줘 ``` -에이전트가 고르면 좋은 대표 흐름: +에이전트가 `daiso_search_products`에 `{"query":"수납박스"}`를 전달하고 +상품 목록과 가격을 반환하면 연결이 완료된 것입니다. +앱별 화면은 [연결 가이드](docs/client-guide.md#원격-mcp-연결)를 참고하세요. -- 가격 비교: `콜라 어디가 싸?` → `compare_products` -- 주변 장소: `강남역 근처 카페 찾아줘` → `places_search_nearby` -- 주유소/유가: `강남역 근처 제일 싼 주유소 찾아줘` → `opinet_search_stations_around` -- 브랜드 명시 재고: `다이소 핫식스 재고 찾아줘` → 먼저 다이소에서 검색 후 결과 없을 때만 대안 제안 -- 편의점 재고: `GS25 강남 오감자 재고` → 상품 후보 확인 후 재고 조회 -- 영화 시간표: `오늘 강남 CGV 시간표` → KST 오늘 날짜로 극장 검색 후 시간표 조회 -- 개발자 요청: `올리브영 재고 도구가 계속 실패한다고 개발자에게 알려줘` → `submit_developer_request` +### HTTP POST로 첫 MCP 조회 -
+HTTP POST를 실행할 수 있는 에이전트나 터미널에서는 로컬 패키지 설치 없이 호출합니다. +이 서버의 루트 POST는 initialize 없이 `tools/call`을 지원합니다. -### ![Claude](https://img.shields.io/badge/Claude-D4A27F?logo=anthropic&logoColor=white) - -> Pro / Max / Team / Enterprise 플랜 필요 · 웹에서 설정 시 모바일 앱에서도 사용 가능 - -1. [claude.ai](https://claude.ai)에서 **Settings** → **Connectors** 이동 -2. **Add custom connector** 클릭 -3. 원격 MCP 서버 URL 입력: `https://mcp.aka.page` -4. **Add** 클릭하여 완료 -5. 대화창에서 **+** 버튼 → **Connectors** → 토글로 활성화 - -사용 예시: - -``` -다이소 mcp를 사용해서 수납박스 검색해줘 -다이소 mcp를 사용해서 강남역 근처 매장 찾아줘 -올리브영 mcp를 사용해서 명동 근처 매장 찾아줘 -올리브영 mcp를 사용해서 선크림 재고 확인해줘 -이마트24 mcp를 사용해서 강남 매장 찾고 두바이 재고 확인해줘 -GS25 mcp를 사용해서 강남 매장 찾고 오감자 재고 확인해줘 -세븐일레븐 mcp를 사용해서 안산 중앙역 근처 매장 찾고 핫식스 재고 확인해줘 -메가박스 mcp를 사용해서 강남역 근처 지점 찾아줘 -메가박스 mcp를 사용해서 강남점 영화 목록이랑 잔여 좌석 확인해줘 -롯데시네마 mcp를 사용해서 잠실 근처 지점 찾아줘 -롯데시네마 mcp를 사용해서 월드타워 영화 목록이랑 잔여 좌석 확인해줘 -CGV mcp를 사용해서 서울 지역 극장 목록 찾아줘 -CGV mcp를 사용해서 강남 CGV 영화랑 시간표 확인해줘 +```bash +curl -sS -N --max-time 20 'https://mcp.aka.page/' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":1}}}' ``` -참고: [Claude Remote MCP 가이드](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) +응답의 SSE `data:` 안에 `error`와 `result.isError: true`가 없고, +`result.structuredContent` 또는 `result.content`에 상품 결과가 있으면 성공입니다. +일반 MCP 클라이언트의 initialize 흐름은 그대로 지원하며 `/mcp` 경로는 initialize가 필요합니다. -
+### Claude Code -### ![Claude Code](https://img.shields.io/badge/Claude_Code-D4A27F?logo=anthropic&logoColor=white) - -> Claude Code CLI에서 MCP 서버 추가 +이미 Claude Code를 사용한다면 다음 한 줄로 공개 서버를 등록합니다. ```bash claude mcp add daiso-mcp https://mcp.aka.page --transport http ``` -
- -### Home Assistant - -> Home Assistant Core 2026.7.1에서 MCP 도구 40개를 모두 정상적으로 불러오는 것을 확인했습니다. - -1. Home Assistant에서 **Settings** → **Devices & services**로 이동 -2. **Add Integration**을 선택하고 **Model Context Protocol**을 검색 -3. 서버 URL 입력: `https://mcp.aka.page` -4. 연동을 마친 뒤 사용할 대화 에이전트가 MCP 도구를 사용하도록 설정 - -참고: [Home Assistant Model Context Protocol 통합 가이드](https://www.home-assistant.io/integrations/mcp) +새 대화에서 `다이소 mcp로 수납박스 검색해줘`를 요청하세요. -
- -### ![Grok](https://img.shields.io/badge/Grok-000000?logo=x&logoColor=white) - -> 웹 및 모바일 앱 모두 지원 - -**프롬프트 페이지 URL:** - -``` -https://mcp.aka.page/prompt -``` - -사용 방법: - -1. Grok 모바일 앱에서 `https://mcp.aka.page/prompt` 페이지를 읽어달라고 요청 -2. 에이전트가 API 사용법을 이해하고 GET 요청으로 기능 실행 - -예시 대화: +### 더 해볼 수 있는 조회 +```text +다이소 mcp로 강남역 근처 매장 찾아줘 +GS25 mcp로 강남 근처 오감자 재고 알려줘 +콜라 어디가 싼지 비교해줘 +오늘 강남 CGV 시간표 알려줘 ``` -사용자: https://mcp.aka.page/prompt 를 읽어줘 -AI: (페이지를 읽고 API 사용법 이해) -사용자: 수납박스 검색해줘 -AI: (https://mcp.aka.page/api/daiso/products?q=수납박스 호출 후 결과 제공) +재고 조회는 먼저 상품을 검색해 ID를 확인한 뒤 진행합니다. +자세한 선택 규칙은 [AI 지시문](docs/ai-instruction.md)에 있습니다. -사용자: 안산 중앙역 근처 메가박스 지점 찾아줘 -AI: (https://mcp.aka.page/api/megabox/theaters?keyword=안산%20중앙역 호출 후 결과 제공) +## 지원 서비스 -사용자: 잠실 근처 롯데시네마 지점 찾아줘 -AI: (https://mcp.aka.page/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4 호출 후 결과 제공) +| 분류 | 서비스 | 주요 조회 | +| :-------- | :---------------------------------- | :---------------------- | +| 리테일 | 다이소, 올리브영 | 상품·매장·재고 | +| 편의점 | GS25, 세븐일레븐, CU, 이마트24 | 상품·매장·재고 | +| 가격 비교 | 다이소, GS25, 세븐일레븐, 이마트24 | 같은 상품의 가격 후보 | +| 장소·유가 | 네이버 지역 검색, 오피넷 | 카페·음식점·주유소·유가 | +| 영화관 | CGV, 메가박스, 롯데시네마, 디트릭스 | 지점·영화·시간표·좌석 | -사용자: 강남 CGV 시간표 알려줘 -AI: (https://mcp.aka.page/api/cgv/timetable?playDate=20260304&theaterCode=0056 호출 후 결과 제공) +서비스별 지원 범위와 입력은 [서비스 레퍼런스](docs/service-reference.md)와 +[디트릭스 가이드](docs/dtryx.md)를 참고하세요. +롯데마트는 지원이 종료되었습니다. 과거 분석과 호환 경로만 보존합니다. -사용자: 안산 중앙역 근처 CGV 찾아서 오늘 영화랑 시간표 알려줘 -AI: (https://mcp.aka.page/api/cgv/theaters?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공) -AI: (https://mcp.aka.page/api/cgv/movies?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공) -AI: (https://mcp.aka.page/api/cgv/timetable?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공) -``` +## MCP 연결을 사용할 수 없는 환경 -
+에이전트가 웹페이지를 읽고 GET 요청을 실행할 수 있다면 +[REST 사용법 페이지](https://mcp.aka.page/prompt)를 읽은 뒤 조회할 수 있습니다. +이 경로는 HTTP GET 기반 REST이며 원격 MCP 연결과 별개의 대안입니다. -### 바로 실행해보기 - -AI 앱 연동 없이 터미널에서 바로 확인할 수 있습니다. +터미널에서는 Node.js 환경이 있을 때 다음 명령으로 조회할 수 있습니다. ```bash npx daiso products 수납박스 --json -npx daiso places 강남역 --category cafe --limit 5 --json -npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 5 --json -npx daiso cgv-movies --playDate 20260520 --theaterCode 0056 --json ``` -
- -### MCP 서버 URL / CLI (고급) - -AI 앱 대신 직접 연결하거나 스크립트에서 사용할 때만 참고하세요. - -MCP 서버 URL: - -``` -https://mcp.aka.page -``` - -CLI (npx): - -```bash -# 인터랙티브 모드 (추천) -npx daiso - -# 인터랙티브 비활성화 (CI/스크립트) -npx daiso --non-interactive - -# 명령형 모드 -npx daiso help -npx daiso help products -npx daiso url -npx daiso health -npx daiso claude - -# AI 없이 직접 조회 -npx daiso products 수납박스 -npx daiso product 1034604 -npx daiso stores 강남역 -npx daiso inventory 1034604 --keyword 강남역 -npx daiso display-location 1034604 04515 -npx daiso compare 콜라 --limit 3 -npx daiso places 강남역 --category cafe --limit 5 -npx daiso places 성수동 --keyword 브런치 --limit 5 -npx daiso get /api/opinet/lowest --fuelCode B027 --areaCode 0113 --count 5 --json -npx daiso get /api/opinet/average --json -npx daiso cu-stores 강남 -npx daiso cu-inventory 과자 --storeKeyword 강남 -npx daiso cgv-theaters 강남 --limit 10 -npx daiso cgv-movies --playDate 20260307 --theaterCode 0056 -npx daiso cgv-timetable --playDate 20260307 --theaterCode 0056 -npx daiso emart24-stores 강남 --service24h true -npx daiso emart24-products 두바이 --pageSize 20 -npx daiso emart24-inventory 8800244010504 --bizNoArr 28339,05015 -npx daiso gs25-stores 강남 --limit 10 -npx daiso gs25-products 오감자 --limit 20 -npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 10 -npx daiso seveneleven-products 삼각김밥 --size 20 -npx daiso seveneleven-stores 안산 중앙역 --limit 10 -npx daiso seveneleven-inventory 핫식스 --storeKeyword "안산 중앙역" --storeLimit 10 -npx daiso seveneleven-popwords --label home -npx daiso seveneleven-catalog --includeIssues true --includeExhibition true --limit 10 - -# 원본 JSON 필요 시 -npx daiso products 수납박스 --json -``` +CLI는 로컬 npm 패키지를 실행합니다. 공개 원격 MCP 연결에는 이 명령이 필요하지 않습니다. +앱·CLI·OpenAPI 대안은 [연결 가이드](docs/client-guide.md)에 있습니다. ### Codex Skill -이 저장소는 MCP 서버뿐 아니라 에이전트가 `npx daiso` CLI를 직접 고를 수 있게 하는 Codex Skill도 제공합니다. - -- ClawHub: `clawhub install daiso-cli` -- 공개 페이지: `https://clawhub.ai/hmmhmmhm/daiso-cli` -- 스킬 파일: `skills/daiso-cli/SKILL.md` -- 명령 맵: `skills/daiso-cli/references/cli-command-map.md` -- 기본 원칙: 쉘 실행이 가능한 환경에서는 CLI를 우선 사용하고, AI 앱 연결이 필요할 때는 MCP 서버 URL `https://mcp.aka.page`를 사용합니다. -- 구조화 결과가 필요하면 스킬은 `npx daiso ... --json` 형태를 선택합니다. -- 상황별 레시피는 `콜라 어디가 싸?`, `강남역 근처 카페`, `다이소 핫식스 재고`, `오늘 강남 CGV 시간표`처럼 실제 사용자 문장 기준으로 정리되어 있습니다. - -### OpenAPI 스펙 - -- OpenAI 챗봇 등록용 축약 스펙: `https://mcp.aka.page/openapi.json` -- OpenAI 챗봇 등록용 YAML: `https://mcp.aka.page/openapi.yaml` -- 전체 개별 엔드포인트 스펙(JSON): `https://mcp.aka.page/openapi-full.json` -- 전체 개별 엔드포인트 스펙(YAML): `https://mcp.aka.page/openapi-full.yaml` - -### MCP 표준 응답 모델 - -MCP 도구 응답은 각 서비스의 원본 필드를 유지하면서, 클라이언트가 공통 UI를 만들 수 있도록 `standard` 필드를 함께 제공합니다. - -- `standard.products`: 상품/영화 목록을 `code`, `name`, `price`, `imageUrl`, `raw` 형태로 정규화합니다. -- `standard.stores`: 매장 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다. -- `standard.theaters`: 영화관 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다. -- `standard.places`: 음식점/카페 같은 장소 목록을 `name`, `address`, `raw` 중심으로 정규화합니다. - -### 통합 상품 가격 비교 - -여러 리테일/편의점 상품 검색 결과를 묶어 같은 상품의 판매처별 가격 후보를 비교합니다. - -```bash -npx daiso compare 콜라 --limit 3 --json -``` - -REST: - -```text -GET /api/compare/products?keyword=콜라&limit=3 -``` - -지원 서비스는 `daiso`, `gs25`, `seveneleven`, `emart24`입니다. GS25처럼 상품 검색 응답에 가격이 없는 서비스는 후보에는 포함되지만 최저가 계산에서는 제외됩니다. - -### 오피넷 유가 정보 - -한국석유공사 오피넷 유가정보 API로 전국 평균 유가, 지역별 최저가 주유소, 반경 내 주유소, 주유소 상세정보를 조회합니다. - -MCP 도구: - -```text -opinet_get_average_prices -opinet_get_lowest_price_stations -opinet_search_stations_around -opinet_get_station_detail -``` - -REST: - -```text -GET /api/opinet/average -GET /api/opinet/lowest?fuelCode=B027&areaCode=0113&count=5 -GET /api/opinet/stations/around?lat=37.4979&lng=127.0276&radiusMeters=3000&fuelCode=B027&sort=price -GET /api/opinet/stations/around?location=강남역&radiusMeters=3000&fuelCode=B027&sort=price -GET /api/opinet/stations/around?x=314681.8&y=544837&radiusMeters=3000&fuelCode=B027&sort=price -GET /api/opinet/station?id=A0010207 -``` - -오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `KAKAO_REST_API_KEY`를 설정합니다. 역명·상호명은 네이버 지역 검색으로 보완합니다. 자세한 설정은 [무료 위치 검색 가이드](./docs/free-location-search.md)를 확인하세요. - -운영 제약 및 캐싱 정책: - -- 오피넷 무료 API는 한국석유공사 공공데이터 활용 가이드 기준 **1일 1,500 call** 한도를 기준으로 운영합니다. -- 모든 오피넷 응답에는 `source: "한국석유공사 오피넷"`과 `fetchedAt`을 포함합니다. -- Cloudflare Edge Cache로 동일 GET 요청의 원본 오피넷 호출을 줄입니다. -- `GET /api/opinet/average`: 60분 캐시, 30분 stale-while-revalidate -- `GET /api/opinet/lowest`: 30분 캐시, 10분 stale-while-revalidate -- `GET /api/opinet/stations/around`: 20분 캐시, 5분 stale-while-revalidate -- `GET /api/opinet/station`: 60분 캐시, 10분 stale-while-revalidate -- 주소·장소명 변환은 카카오 주소/키워드 검색을 사용합니다. 직접 좌표를 입력하면 위치 변환 API를 호출하지 않습니다. -- 호출 한도에 자주 도달하면 운영자가 담당 기관인 한국석유공사/오피넷에 문의해 추가 할당량 또는 별도 이용 조건을 협의할 예정입니다. - -### 개발자 요청 제출 - -AI 에이전트가 MCP 기능 오류, 개선 요청, 신규 기능 요청, 문서 문제를 바로 개발자에게 전달할 수 있습니다. 요청은 Supabase `agent_requests` 테이블에 저장됩니다. - -MCP 도구: - -```text -submit_developer_request -``` - -REST: - -```text -POST /api/feedback/requests -GET /api/feedback/requests?type=bug&title=제목&description=설명 -``` - -로컬 실행이나 배포 환경에는 `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`를 설정하세요. 테이블 스키마는 [supabase-agent-requests.sql](./docs/supabase-agent-requests.sql)에 있습니다. - -### 무료 위치 검색 - -주소·역명·상호명을 좌표로 바꾸는 기능은 카카오맵 무료 API를 사용합니다. Google Geocoding과 Zyte는 호출하지 않습니다. 카카오 무료 쿼터 대상 확인, 환경 변수와 실패 시 동작은 [무료 위치 검색 가이드](./docs/free-location-search.md)에 있습니다. - -### 운영 헬스 체크 - -공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)** +에이전트가 CLI 명령을 선택하도록 하는 [Skill](skills/daiso-cli/SKILL.md)도 제공합니다. +`clawhub install daiso-cli` 설치와 사용 예시는 [연결 가이드](docs/client-guide.md#codex-skill)에 있습니다. -주변 음식점/카페 검색은 네이버 지역 검색 API를 사용합니다. 로컬 실행이나 배포 환경에는 `NAVER_CLIENT_ID`, `NAVER_CLIENT_SECRET`을 설정하세요. +## 응답과 이용 제한 -서비스별 API 상태를 즉시 확인할 때는 `GET /api/health/checks`를 사용합니다. 이 엔드포인트는 `HEALTH_CHECK_SECRET` 환경 변수가 설정되어 있어야 하며, 요청에는 `Authorization: Bearer ` 또는 `x-health-check-key: ` 헤더가 필요합니다. 내부 체크 요청의 기준 URL은 `HEALTH_CHECK_BASE_URL`로 지정할 수 있습니다. - -```bash -curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \ - "https://mcp.aka.page/api/health/checks?mode=full&fresh=true&includeSamples=true&timeoutMs=20000&slowThresholdMs=9000" -``` - -주요 쿼리: - -- `service=gs25`: 특정 서비스만 확인 -- `check=daiso.products`: 특정 체크만 확인 -- `mode=quick|deep|full`: 체크 모드 선택 -- `fresh=true`: 60초 캐시 우회 -- `includeSamples=true`: 첫 결과 이름 샘플 포함 -- `timeoutMs=20000`: 체크별 요청 제한 시간 -- `slowThresholdMs=9000`: 지정 시간보다 느린 성공 응답을 degraded로 표시 - -Health Checks 알림은 모든 실패·저하 서비스 ID를 상세 오류보다 먼저 표시합니다. 푸시 알림은 3,500자로 제한되며 전체 오류와 샘플은 해당 실행의 GitHub Actions 요약에 남습니다. 올리브영 릴레이의 설정 완전성은 `config.oliveyoungRelay`로 확인할 수 있습니다. 이 값은 연결 성공을 뜻하지 않으며, [설정 진단 문서](./docs/oliveyoung-free-relay.md#설정-진단)를 참고하세요. - -Zyte 유료 요청은 키 설정 여부와 관계없이 비활성화되어 있습니다. 직접 JSON 요청이 실패하면 원래 HTTP 상태와 오류를 유지합니다. - -상태 기준: - -- `ok`: 필수 응답 구조와 최소 결과가 정상입니다. -- `degraded`: 기능은 살아 있지만 빈 결과, 느린 응답, 응답 구조 변화 등 확인이 필요합니다. -- `fail`: 외부 API 오류, 타임아웃, 인증 문제처럼 실제 장애로 봐야 합니다. - -Better Stack 같은 외부 모니터링에서는 `fail`을 장애 알림 기준으로 보고, `degraded`는 느린 외부 API나 응답 품질 저하를 추적하는 경고 신호로 봅니다. - -### 운영 통계 - -일일 호출 제한으로 차단한 요청의 집계는 인증된 `GET /api/rate-limit/stats`에서 조회합니다. 헬스 체크와 같은 `HEALTH_CHECK_SECRET`을 사용하며, 다음 두 인증 헤더를 모두 지원합니다. - -```bash -curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \ - "https://mcp.aka.page/api/rate-limit/stats" - -curl -H "x-health-check-key: $HEALTH_CHECK_SECRET" \ - "https://mcp.aka.page/api/rate-limit/stats?service=cgv" -``` - -쿼리 필터는 `from`, `to`, `service`입니다. `from`과 `to`는 함께 지정하거나 둘 다 생략해야 하며 날짜 형식은 `YYYY-MM-DD`입니다. `service`에는 `oliveyoung`, `cgv`, `cu`, `gs25`, `lottemart`만 사용할 수 있습니다. `lottemart` 필터는 지원 중단 전 호출 제한 통계를 조회하기 위해 유지합니다. 날짜를 생략하면 현재 KST 일자를 포함한 최근 7일을 조회합니다. 조회 가능한 보관 범위는 현재 KST 일자와 그 이전 29일이며, 한 번에 KST 달력 날짜 기준 최대 30일을 요청할 수 있습니다. - -성공 응답은 전체 합계와 일별·서비스별 차단 요청 수와 고유 차단 주체 수를 제공합니다. - -```json -{ - "success": true, - "data": { - "totals": { - "blockedRequests": 3, - "uniqueIdentities": 2 - }, - "daily": [ - { - "day": "2026-07-22", - "blockedRequests": 3, - "uniqueIdentities": 2 - } - ], - "services": [ - { - "day": "2026-07-22", - "service": "cgv", - "blockedRequests": 3, - "uniqueIdentities": 2 - } - ] - } -} -``` - -데이터는 30일 동안 보관하며 집계 응답은 원본 호출 주체나 IP를 노출하지 않습니다. Worker가 생성한 `DAILY_RATE_LIMIT_EXCEEDED` 결정 중 원장 커밋에 성공한 경우만 정확한 집계 범위에 포함됩니다. Cloudflare 또는 네트워크 계층의 429와 클라이언트 전송 결과, 연결 종료 결과는 이 범위에 포함되지 않습니다. 원장 쓰기에 실패하면 요청을 fail-open 처리하고 애플리케이션 429를 반환하지 않습니다. 통계는 이 기능의 배포 시점부터 수집하며 이전 429는 소급 집계하지 않습니다. - -배포 전 로컬에서 CLI 모드까지 확인할 때는 아래 명령을 사용합니다. - -```bash -npm run cli:smoke -``` +도구 응답은 원본 필드와 함께 공통 결과를 제공합니다. +`standard.products`는 상품, `standard.stores`는 매장, +`standard.theaters`는 영화관 목록입니다. +[응답 모델](docs/service-reference.md#mcp-표준-응답-모델)에서 필드 정의를 확인하세요. -기본 `openapi.json`은 OpenAI Actions import 제한에 맞추기 위해 `GET /api/actions/query` 단일 facade만 노출합니다. -기존 서비스별 GET API는 유지되며, 자세한 배경은 [OpenAPI Actions Facade 문서](./docs/openapi-actions-facade.md)에 정리했습니다. - -인터랙티브 예시: - -```text -$ npx daiso -daiso 인터랙티브 모드 - -[서비스 선택] -1. 다이소 -2. 올리브영 -3. CU -서비스 번호를 선택하세요 (0: 종료): 1 - -매장 검색 키워드를 입력하세요: 강남 - -[매장 선택] -1. 다이소 강남점 | 서울 강남구 ... -2. 다이소 강남역점 | 서울 강남구 ... -입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색 -선택: /역점 -선택: 1 - -[선택한 매장 정보] -- 매장명: 다이소 강남역점 -- 주소: 서울 강남구 ... -- 전화: 02-... - -찾을 상품 키워드를 입력하세요: 수납박스 - -[상품 선택] -1. 손잡이 수납박스 (2000원, ID: 1034604) -2. 접이식 수납박스 (3000원, ID: 1034605) -입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소 -선택: 1 - -[재고 결과] -- 상품: 손잡이 수납박스 -- 매장: 다이소 강남역점 -- 재고 수량: 7 - -[다음 동작] -1. 같은 매장에서 다른 상품 찾기 -2. 다른 매장/서비스 다시 선택하기 -3. 종료하기 -번호를 선택하세요: 3 -인터랙티브 모드를 종료합니다. -``` - -
- -### 미지원 서비스 - -| 서비스 | 상태 | -| :---------------------------------------------------------------------------------------------------- | :-------- | -| ![Gemini](https://img.shields.io/badge/Gemini-8E75B2?logo=googlegemini&logoColor=white) Google Gemini | ❌ 미지원 | -| ![Copilot](https://img.shields.io/badge/Copilot-000000?logo=github&logoColor=white) GitHub Copilot | ❌ 미지원 | - -
- -## Special Thanks - -이 프로젝트에 도움 주신 분들께 감사드립니다. - -- [@thecats1105](https://github.com/thecats1105): 다이소 진열 위치 조회 도구(`daiso_get_display_location`) 구현 및 API/테스트 연동 -- [@betterthanhajin](https://github.com/betterthanhajin): CGV 서비스 프로바이더 구현(극장/영화/시간표 도구, 라우트·스펙·테스트 추가) -- [@LLagoon3](https://github.com/LLagoon3): 다이소 매장 재고 조회 신규 인증 흐름 전환 및 관련 테스트 보강 -- [제로초님](https://youtube.com/shorts/ZgIqA1NCEp0?si=UW0pKsSpqmEi7lXG): 프로젝트 홍보 도움 - -
- ---- - -
+공개 GET API 중 올리브영·CGV·CU·GS25에는 IP당 하루 합산 3,000회(KST 기준) 제한이 적용됩니다. +조회가 실패하면 [서비스 상태](https://aka-page.betteruptime.com/)를 확인하세요. +서버 운영과 제한 집계는 [운영 가이드](scripts/ops/README.md)에 있습니다. ## 상세 문서 -`Special Thanks` 이후에 있던 상세 설명은 별도 문서로 분리했습니다. - -- [서비스 레퍼런스](./docs/service-reference.md) -- [CONTRIBUTING 가이드](./CONTRIBUTING.md) - -
- ---- - -
- -## docs 문서 - -### 공통 가이드 - -- [GS25 정상 로그인·직접 인증 복구 운영 절차](./docs/gs25-auth-recovery-runbook.md) -- [서비스 레퍼런스](./docs/service-reference.md) -- [스크래핑 플레이북](./docs/scraping-playbook.md) -- [mitmproxy 가이드](./docs/mitmproxy-guide.md) -- [AI 지시문](./docs/ai-instruction.md) - -### 다이소 +이전 README 절의 링크는 아래 상세 가이드로 이어집니다. -- [다이소 네트워크 분석 결과](./docs/daiso-network-analysis-result.md) -- [다이소 Playwright 네트워크 분석](./docs/daiso-playwright-network-analysis.md) -- [다이소 리플레이 세션 테스트 HTML](./docs/daiso-replay-session-test.html) -- [다이소 테스트 리플레이 스크립트](./docs/daiso-test-replay.ts) + -### CU +- [앱 연결·CLI·Skill·OpenAPI](docs/client-guide.md) + -- [CU 네트워크 분석 결과](./docs/cu-network-analysis-result.md) -- [CU 앱 요청 캡처 가이드](./docs/cu-app-request-capture-guide.md) -- [CU 앱 스크래핑 리플레이 가이드](./docs/cu-app-scraping-replay-guide.md) +- [서비스 도구·REST API·아키텍처](docs/service-reference.md) +- [AI 도구 선택·조회 흐름](docs/ai-instruction.md) +- [개발·설치·직접 배포](CONTRIBUTING.md) + -### 이마트24 +- [서버 운영·헬스 체크·요청량 차트](scripts/ops/README.md) + -- [이마트24 네트워크 분석 결과](./docs/emart24-network-analysis-result.md) -- [이마트24 앱 스크래핑 준비 가이드](./docs/emart24-app-scraping-preparation-guide.md) -- [이마트24 앱 스크래핑 리플레이 가이드](./docs/emart24-app-scraping-replay-guide.md) +- [서비스 분석 문서](docs/service-reference.md#분석-문서) -### 롯데마트 과거 분석 - -롯데마트 지원은 중단했습니다. 기존 REST 경로는 `410 SERVICE_RETIRED`, 기존 CLI 명령은 지원 중단 오류를 반환합니다. - -- [롯데마트 모바일 도와센터 스크래핑 리플레이 계획](./docs/lottemart-mobile-scraping-replay-plan.md) - -### 공통 - -- [OpenAPI Actions facade 리팩토링 배경](./docs/openapi-actions-facade.md) - -### 올리브영 - -- [올리브영 네트워크 분석 결과](./docs/oliveyoung-network-analysis-result.md) -- [올리브영 Playwright MCP 온보딩](./docs/oliveyoung-playwright-mcp-onboarding.md) -- [올리브영 Playwright 네트워크 분석](./docs/oliveyoung-playwright-network-analysis.md) -- [올리브영 Lightpanda 검증](./docs/oliveyoung-lightpanda-validation.md) -- [올리브영 리플레이 세션 테스트 스크립트](./docs/oliveyoung-replay-session-test.ts) -- [올리브영 Zyte 대역폭 테스트](./docs/oliveyoung-zyte-bandwidth-test.ts) -- [올리브영 Zyte 리플레이 테스트](./docs/oliveyoung-zyte-replay-test.ts) - -### 영화관 - -- [CGV 네트워크 분석 결과](./docs/cgv-network-analysis-result.md) -- [메가박스 네트워크 분석 결과](./docs/megabox-network-analysis-result.md) -- [롯데시네마 네트워크 분석 결과](./docs/lottecinema-network-analysis-result.md) - -### GS25 - -- [GS25 API 리플레이 방법론 (최종)](./docs/gs25-final-replay-methodology.md) -- [GS25 네트워크 분석 결과 (아카이브)](./docs/archive/gs25-network-analysis-result.md) -- [GS25 안드로이드 우회 캡처 가이드 (아카이브)](./docs/archive/gs25-android-bypass-capture-guide.md) -- [GS25 앱 캡처 시도 로그 (2026-03-08, 아카이브)](./docs/archive/gs25-app-capture-attempt-log-20260308.md) -- [GS25 앱 스크래핑 준비 가이드 (아카이브)](./docs/archive/gs25-app-scraping-preparation-guide.md) -- [GS25 세션 인계 문서 (2026-03-09, 아카이브)](./docs/archive/gs25-session-handoff-20260309.md) - -
- ---- - -
+## Special Thanks -
+- [@thecats1105](https://github.com/thecats1105): 다이소 진열 위치 조회 +- [@betterthanhajin](https://github.com/betterthanhajin): CGV 서비스 +- [@LLagoon3](https://github.com/LLagoon3): 다이소 재고 인증 +- [제로초님](https://youtube.com/shorts/ZgIqA1NCEp0?si=UW0pKsSpqmEi7lXG): 프로젝트 홍보 MIT License - -
- -
- ---- - -## 신규 MCP 기능 추가 시 유의사항 - -새로운 서비스나 도구를 추가할 때는 구현만 끝내지 말고 아래 반영 범위를 함께 확인해야 합니다. - -- `MCP`: `src/index.ts` 서비스 등록, 루트 서비스/도구 목록, 관련 테스트 반영 -- `HTTPS`: GET API 핸들러/라우트, 프롬프트 페이지(`src/pages/prompt.ts`), 앱 통합 테스트 반영 -- `CLI`: `src/cli.ts`, `src/cliHelp.ts`, CLI 테스트 반영 -- `AI instruction`: [ai-instruction.md](./docs/ai-instruction.md) 사용 규칙/워크플로우 반영 -- `README`: 지원 서비스 설명, 예시, 문서 링크 반영 -- `OpenAPI`: 기본 `/openapi.json` facade 스펙, `/openapi-full.json` 전체 스펙, 관련 테스트 반영 - -기능 추가 후 최소 검증 기준: - -- `npm run typecheck` -- `npm test` diff --git a/docs/ai-instruction.md b/docs/ai-instruction.md index acd03be8..bf3cb7cb 100644 --- a/docs/ai-instruction.md +++ b/docs/ai-instruction.md @@ -1,10 +1,25 @@ +## 설치 없이 첫 조회 + +사용자가 "설치 없이", "무설치"를 명시하면 로컬 CLI·스킬 설치 없이 원격 조회를 먼저 실행합니다. 연결된 원격 MCP 도구가 있으면 `daiso_search_products`를 바로 호출합니다. 원격 MCP 클라이언트의 서버 주소는 `https://mcp.aka.page`입니다. + +셸에서 직접 확인할 때는 아래 POST가 실제 MCP 도구 호출입니다. 이 공개 서버는 세션 없이 호출할 수 있으며 응답은 SSE의 `data:` JSON입니다. 범용 MCP 클라이언트 연결은 클라이언트가 initialize와 세션 절차를 처리합니다. + +```bash +curl -fsS --max-time 20 'https://mcp.aka.page' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + --data-binary '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}' +``` + +첫 조회 전에 `npx`, 패키지·스킬 설치, health/help, 전체 스키마나 도구 목록 탐색을 하지 않습니다. 위 예제의 검색어를 사용자 요청으로 바꾸고 조회 결과부터 제공합니다. 사용자 조건이나 호출 오류 때문에 필요한 경우에만 추가 안내를 읽습니다. 호출에 성공하면 추가 진단 없이 결과를 보여줍니다. + +`GET https://mcp.aka.page/api/daiso/products?q=수납박스`는 같은 서비스의 REST 직접 조회입니다. GET만 가능한 환경에서는 사용할 수 있지만 MCP 도구를 호출했다고 설명하지 않습니다. HTTP·네트워크 오류만으로 로그인 필요나 서버 장애를 단정하지 않고 실제 오류와 실행 환경을 확인합니다. + 당신은 다이소/통합 가격 후보 비교/주변 음식점·카페/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 생활·쇼핑·영화 도우미입니다. 다이소/통합 가격 후보 비교/주변 음식점·카페/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 관련 정보는 모두 mcp.aka.page 작업 기능만 사용하고, 웹검색은 하지 마세요. -mcp.aka.page 작업 기능 호출 중 에러가 나면 아래 문구로 안내하세요: -"ChatGPT 계정 로그인이 필요합니다. (다이소/통합 가격 후보 비교/주변 음식점·카페/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 서비스 -자체 로그인은 필요 없습니다.)" +실제 인증 오류를 확인한 경우에만 해당 클라이언트의 로그인 안내를 제공합니다. 공개 원격 MCP·REST 조회에는 별도 서비스 로그인이나 API 키가 필요하지 않습니다. ## 언어 감지 및 역할 설정 diff --git a/docs/client-guide.md b/docs/client-guide.md new file mode 100644 index 00000000..81e7638f --- /dev/null +++ b/docs/client-guide.md @@ -0,0 +1,249 @@ +# 앱 연결·CLI·Skill 가이드 + +[무설치 원격 MCP 첫 조회](../README.md#원격-mcp로-바로-시작하기)부터 시작하세요. +원격 서버는 `https://mcp.aka.page`, 전송 방식은 Streamable HTTP, 사용자 인증은 없습니다. + +## 원격 MCP 연결 + +Claude, Claude Code, Home Assistant 등 원격 MCP를 지원하는 앱에서 연결할 수 있습니다. +ChatGPT GPT 앱과 Grok의 GET 조회는 아래 REST 대안으로 구분합니다. +아래 앱별 가이드에서 먼저 연동한 뒤 검색/재고/영화 조회를 요청하세요. + +
+ +### ChatGPT GPT 앱 (REST 대안) + +> MCP 연동이 어렵다는 피드백이 있어 바로 사용 가능한 GPT 앱을 추가했습니다. +> 이용 가능한 환경과 조건은 현재 ChatGPT 앱에서 확인하세요. + +**[Daiso MCP GPT 앱 바로가기](https://chatgpt.com/g/g-69a5266c32108191b71a24642dc63f9e-daiso-mcp)** + +빠른 사용 예시: + +``` +다이소 mcp로 수납박스 검색해줘 +콜라 어디가 싼지 비교해줘 +올리브영 mcp로 명동 근처 매장 찾아줘 +이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘 +GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘 +세븐일레븐 mcp로 삼각김밥 검색해줘 +세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘 +세븐일레븐 mcp로 안산 중앙역 근처 세븐일레븐에서 핫식스 재고 알려줘 +세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘 +강남역 근처 카페 찾아줘 +성수동 근처 브런치 음식점 찾아줘 +강남역 근처 제일 싼 주유소 찾아줘 +오늘 전국 평균 휘발유 가격 알려줘 +메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘 +롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘 +롯데시네마 mcp로 월드타워 잔여 좌석 알려줘 +CGV mcp로 강남 상영 영화와 시간표 알려줘 +``` + +에이전트가 고르면 좋은 대표 흐름: + +- 가격 비교: `콜라 어디가 싸?` → `compare_products` +- 주변 장소: `강남역 근처 카페 찾아줘` → `places_search_nearby` +- 주유소/유가: `강남역 근처 제일 싼 주유소 찾아줘` → `opinet_search_stations_around` +- 브랜드 명시 재고: `다이소 핫식스 재고 찾아줘` → 먼저 다이소에서 검색 후 결과 없을 때만 대안 제안 +- 편의점 재고: `GS25 강남 오감자 재고` → 상품 후보 확인 후 재고 조회 +- 영화 시간표: `오늘 강남 CGV 시간표` → KST 오늘 날짜로 극장 검색 후 시간표 조회 +- 개발자 요청: `올리브영 재고 도구가 계속 실패한다고 개발자에게 알려줘` → `submit_developer_request` + +
+ +### Claude + +> 연결 메뉴와 이용 조건은 [Claude 공식 연결 가이드](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp)에서 확인하세요. + +1. [claude.ai](https://claude.ai)에서 **Settings** → **Connectors** 이동 +2. **Add custom connector** 클릭 +3. 원격 MCP 서버 URL 입력: `https://mcp.aka.page` +4. **Add** 클릭하여 완료 +5. 대화창에서 **+** 버튼 → **Connectors** → 토글로 활성화 + +사용 예시: + +``` +다이소 mcp를 사용해서 수납박스 검색해줘 +다이소 mcp를 사용해서 강남역 근처 매장 찾아줘 +올리브영 mcp를 사용해서 명동 근처 매장 찾아줘 +올리브영 mcp를 사용해서 선크림 재고 확인해줘 +이마트24 mcp를 사용해서 강남 매장 찾고 두바이 재고 확인해줘 +GS25 mcp를 사용해서 강남 매장 찾고 오감자 재고 확인해줘 +세븐일레븐 mcp를 사용해서 안산 중앙역 근처 매장 찾고 핫식스 재고 확인해줘 +메가박스 mcp를 사용해서 강남역 근처 지점 찾아줘 +메가박스 mcp를 사용해서 강남점 영화 목록이랑 잔여 좌석 확인해줘 +롯데시네마 mcp를 사용해서 잠실 근처 지점 찾아줘 +롯데시네마 mcp를 사용해서 월드타워 영화 목록이랑 잔여 좌석 확인해줘 +CGV mcp를 사용해서 서울 지역 극장 목록 찾아줘 +CGV mcp를 사용해서 강남 CGV 영화랑 시간표 확인해줘 +``` + +참고: [Claude Remote MCP 가이드](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) + +
+ +### Claude Code + +> Claude Code CLI에서 MCP 서버 추가 + +```bash +claude mcp add daiso-mcp https://mcp.aka.page --transport http +``` + +
+ +### Home Assistant + +> 과거 검증 환경은 Home Assistant Core 2026.7.1입니다. 현재 제공 도구는 서버의 `tools/list`로 확인하세요. + +1. Home Assistant에서 **Settings** → **Devices & services**로 이동 +2. **Add Integration**을 선택하고 **Model Context Protocol**을 검색 +3. 서버 URL 입력: `https://mcp.aka.page` +4. 연동을 마친 뒤 사용할 대화 에이전트가 MCP 도구를 사용하도록 설정 + +참고: [Home Assistant Model Context Protocol 통합 가이드](https://www.home-assistant.io/integrations/mcp) + +
+ +### Grok (REST 대안) + +> 사용할 앱에서 웹페이지 읽기와 GET 요청 실행을 지원하는지 현재 앱 문서를 확인하세요. + +**프롬프트 페이지 URL:** + +``` +https://mcp.aka.page/prompt +``` + +사용 방법: + +1. Grok 모바일 앱에서 `https://mcp.aka.page/prompt` 페이지를 읽어달라고 요청 +2. 에이전트가 API 사용법을 이해하고 GET 요청으로 기능 실행 + +예시 대화 (`YYYYMMDD`는 실행일의 KST 날짜로 바꿉니다): + +``` +사용자: https://mcp.aka.page/prompt 를 읽어줘 +AI: (페이지를 읽고 API 사용법 이해) + +사용자: 수납박스 검색해줘 +AI: (https://mcp.aka.page/api/daiso/products?q=수납박스 호출 후 결과 제공) + +사용자: 안산 중앙역 근처 메가박스 지점 찾아줘 +AI: (https://mcp.aka.page/api/megabox/theaters?keyword=안산%20중앙역 호출 후 결과 제공) + +사용자: 잠실 근처 롯데시네마 지점 찾아줘 +AI: (https://mcp.aka.page/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4 호출 후 결과 제공) + +사용자: 오늘 강남 CGV 시간표 알려줘 +AI: (https://mcp.aka.page/api/cgv/timetable?playDate=YYYYMMDD&theaterCode=0056 호출 후 결과 제공) + +사용자: 안산 중앙역 근처 CGV 찾아서 오늘 영화랑 시간표 알려줘 +AI: (https://mcp.aka.page/api/cgv/theaters?playDate=YYYYMMDD&keyword=안산%20중앙역 호출 후 결과 제공) +AI: (https://mcp.aka.page/api/cgv/movies?playDate=YYYYMMDD&keyword=안산%20중앙역 호출 후 결과 제공) +AI: (https://mcp.aka.page/api/cgv/timetable?playDate=YYYYMMDD&keyword=안산%20중앙역 호출 후 결과 제공) +``` + +
+ +### 바로 실행해보기 + +Node.js 환경이 있는 터미널에서 npm 패키지를 실행하는 대안입니다. 원격 MCP 연결에는 설치가 필요하지 않습니다. + +```bash +npx daiso products 수납박스 --json +npx daiso places 강남역 --category cafe --limit 5 --json +npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 5 --json +npx daiso cgv-movies --playDate "$(TZ=Asia/Seoul date +%Y%m%d)" --theaterCode 0056 --json +``` + +
+ +### MCP 서버 URL / CLI (고급) + +AI 앱 대신 직접 연결하거나 스크립트에서 사용할 때만 참고하세요. + +MCP 서버 URL: + +``` +https://mcp.aka.page +``` + +CLI (npx): + +```bash +# 인터랙티브 모드 (추천) +npx daiso + +# 인터랙티브 비활성화 (CI/스크립트) +npx daiso --non-interactive + +# 명령형 모드 +npx daiso help +npx daiso help products +npx daiso url +npx daiso health +npx daiso claude + +# AI 없이 직접 조회 +npx daiso products 수납박스 +npx daiso product 1034604 +npx daiso stores 강남역 +npx daiso inventory 1034604 --keyword 강남역 +npx daiso display-location 1034604 04515 +npx daiso compare 콜라 --limit 3 +npx daiso places 강남역 --category cafe --limit 5 +npx daiso places 성수동 --keyword 브런치 --limit 5 +npx daiso get /api/opinet/lowest --fuelCode B027 --areaCode 0113 --count 5 --json +npx daiso get /api/opinet/average --json +npx daiso cu-stores 강남 +npx daiso cu-inventory 과자 --storeKeyword 강남 +npx daiso cgv-theaters 강남 --limit 10 +npx daiso cgv-movies --playDate "$(TZ=Asia/Seoul date +%Y%m%d)" --theaterCode 0056 +npx daiso cgv-timetable --playDate "$(TZ=Asia/Seoul date +%Y%m%d)" --theaterCode 0056 +npx daiso emart24-stores 강남 --service24h true +npx daiso emart24-products 두바이 --pageSize 20 +npx daiso emart24-inventory 8800244010504 --bizNoArr 28339,05015 +npx daiso gs25-stores 강남 --limit 10 +npx daiso gs25-products 오감자 --limit 20 +npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 10 +npx daiso seveneleven-products 삼각김밥 --size 20 +npx daiso seveneleven-stores 안산 중앙역 --limit 10 +npx daiso seveneleven-inventory 핫식스 --storeKeyword "안산 중앙역" --storeLimit 10 +npx daiso seveneleven-popwords --label home +npx daiso seveneleven-catalog --includeIssues true --includeExhibition true --limit 10 + +# 원본 JSON 필요 시 +npx daiso products 수납박스 --json +``` + +### Codex Skill + +이 저장소는 MCP 서버뿐 아니라 에이전트가 `npx daiso` CLI를 직접 고를 수 있게 하는 Codex Skill도 제공합니다. + +- ClawHub: `clawhub install daiso-cli` +- 공개 페이지: `https://clawhub.ai/hmmhmmhm/daiso-cli` +- 스킬 파일: [skills/daiso-cli/SKILL.md](../skills/daiso-cli/SKILL.md) +- 명령 맵: [CLI 명령 맵](../skills/daiso-cli/references/cli-command-map.md) +- 사용자가 무설치 조회를 요청하면 공개 원격 MCP 또는 HTTP POST를 우선 사용합니다. `npx`나 Skill 설치, 전체 스키마 탐색은 필요하지 않습니다. +- CLI를 사용하기로 선택했고 쉘 실행이 가능한 환경에서만 `npx daiso`를 사용합니다. AI 앱 연결에는 MCP 서버 URL `https://mcp.aka.page`를 사용합니다. +- 구조화 결과가 필요하면 스킬은 `npx daiso ... --json` 형태를 선택합니다. +- 상황별 레시피는 `콜라 어디가 싸?`, `강남역 근처 카페`, `다이소 핫식스 재고`, `오늘 강남 CGV 시간표`처럼 실제 사용자 문장 기준으로 정리되어 있습니다. + +### OpenAPI 스펙 + +- OpenAI 챗봇 등록용 축약 스펙: `https://mcp.aka.page/openapi.json` +- OpenAI 챗봇 등록용 YAML: `https://mcp.aka.page/openapi.yaml` +- 전체 개별 엔드포인트 스펙(JSON): `https://mcp.aka.page/openapi-full.json` +- 전체 개별 엔드포인트 스펙(YAML): `https://mcp.aka.page/openapi-full.yaml` + +기본 OpenAPI 스펙은 OpenAI Actions용 `GET /api/actions/query` 단일 facade를 노출합니다. +기존 서비스별 GET 경로는 유지하며 [facade 배경](./openapi-actions-facade.md)을 참고하세요. + +## 연결할 수 없는 앱 + +앱의 원격 MCP 지원 여부는 해당 앱 문서에서 확인하세요. 웹 GET 요청을 실행할 수 있다면 +[REST 사용법 페이지](https://mcp.aka.page/prompt)를 읽고 조회할 수 있습니다. +REST GET 조회는 MCP 프로토콜 연결이 아닙니다. diff --git a/docs/service-reference.md b/docs/service-reference.md index 4cec3b2f..459cdea4 100644 --- a/docs/service-reference.md +++ b/docs/service-reference.md @@ -2,6 +2,92 @@ README 하단에 있던 기능, REST API, 개발, 아키텍처 설명을 분리한 문서입니다. +### MCP 표준 응답 모델 + +MCP 도구 응답은 각 서비스의 원본 필드를 유지하면서, 클라이언트가 공통 UI를 만들 수 있도록 `standard` 필드를 함께 제공합니다. + +- `standard.products`: 상품/영화 목록을 `code`, `name`, `price`, `imageUrl`, `raw` 형태로 정규화합니다. +- `standard.stores`: 매장 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다. +- `standard.theaters`: 영화관 목록을 `code`, `name`, `address`, `distanceMeters`, `raw` 형태로 정규화합니다. +- `standard.places`: 음식점/카페 같은 장소 목록을 `name`, `address`, `raw` 중심으로 정규화합니다. + +### 통합 상품 가격 비교 + +여러 리테일/편의점 상품 검색 결과를 묶어 같은 상품의 판매처별 가격 후보를 비교합니다. + +```bash +npx daiso compare 콜라 --limit 3 --json +``` + +REST: + +```text +GET /api/compare/products?keyword=콜라&limit=3 +``` + +지원 서비스는 `daiso`, `gs25`, `seveneleven`, `emart24`입니다. GS25처럼 상품 검색 응답에 가격이 없는 서비스는 후보에는 포함되지만 최저가 계산에서는 제외됩니다. + +### 오피넷 유가 정보 + +한국석유공사 오피넷 유가정보 API로 전국 평균 유가, 지역별 최저가 주유소, 반경 내 주유소, 주유소 상세정보를 조회합니다. + +MCP 도구: + +```text +opinet_get_average_prices +opinet_get_lowest_price_stations +opinet_search_stations_around +opinet_get_station_detail +``` + +REST: + +```text +GET /api/opinet/average +GET /api/opinet/lowest?fuelCode=B027&areaCode=0113&count=5 +GET /api/opinet/stations/around?lat=37.4979&lng=127.0276&radiusMeters=3000&fuelCode=B027&sort=price +GET /api/opinet/stations/around?location=강남역&radiusMeters=3000&fuelCode=B027&sort=price +GET /api/opinet/stations/around?x=314681.8&y=544837&radiusMeters=3000&fuelCode=B027&sort=price +GET /api/opinet/station?id=A0010207 +``` + +오피넷 API 키는 `OPINET_API_KEY` 환경 변수 또는 Cloudflare Worker Secret으로 설정합니다. 키 발급은 오피넷 웹사이트의 `유가관련정보 > 유가정보 API > 인증키 발급`에서 진행합니다. 반경 검색은 `lat/lng`, `location`, KATEC `x/y`를 모두 지원하며, `location` 검색에는 `KAKAO_REST_API_KEY`를 설정합니다. 역명·상호명은 네이버 지역 검색으로 보완합니다. 자세한 설정은 [무료 위치 검색 가이드](./free-location-search.md)를 확인하세요. + +운영 제약 및 캐싱 정책: + +- 오피넷 무료 API는 한국석유공사 공공데이터 활용 가이드 기준 **1일 1,500 call** 한도를 기준으로 운영합니다. +- 모든 오피넷 응답에는 `source: "한국석유공사 오피넷"`과 `fetchedAt`을 포함합니다. +- Cloudflare Edge Cache로 동일 GET 요청의 원본 오피넷 호출을 줄입니다. +- `GET /api/opinet/average`: 60분 캐시, 30분 stale-while-revalidate +- `GET /api/opinet/lowest`: 30분 캐시, 10분 stale-while-revalidate +- `GET /api/opinet/stations/around`: 20분 캐시, 5분 stale-while-revalidate +- `GET /api/opinet/station`: 60분 캐시, 10분 stale-while-revalidate +- 주소·장소명 변환은 카카오 주소/키워드 검색을 사용합니다. 직접 좌표를 입력하면 위치 변환 API를 호출하지 않습니다. +- 호출 한도에 자주 도달하면 운영자가 담당 기관인 한국석유공사/오피넷에 문의해 추가 할당량 또는 별도 이용 조건을 협의할 예정입니다. + +### 개발자 요청 제출 + +AI 에이전트가 MCP 기능 오류, 개선 요청, 신규 기능 요청, 문서 문제를 바로 개발자에게 전달할 수 있습니다. 요청은 Supabase `agent_requests` 테이블에 저장됩니다. + +MCP 도구: + +```text +submit_developer_request +``` + +REST: + +```text +POST /api/feedback/requests +GET /api/feedback/requests?type=bug&title=제목&description=설명 +``` + +로컬 실행이나 배포 환경에는 `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`를 설정하세요. 테이블 스키마는 [supabase-agent-requests.sql](./supabase-agent-requests.sql)에 있습니다. + +### 무료 위치 검색 + +주소·역명·상호명을 좌표로 바꾸는 기능은 카카오맵 무료 API를 사용합니다. Google Geocoding과 Zyte는 호출하지 않습니다. 카카오 무료 쿼터 대상 확인, 환경 변수와 실패 시 동작은 [무료 위치 검색 가이드](./free-location-search.md)에 있습니다. + ## 기능 ### daiso_search_products @@ -749,6 +835,10 @@ AI: cgv_search_movies / cgv_get_timetable 도구로 영화/시간표 조회 ## 개발 +공개 서버에 연결하는 사용자는 이 절의 설치·배포 작업이 필요하지 않습니다. +개발 환경과 무료 조회 정책은 [CONTRIBUTING.md](../CONTRIBUTING.md)를 참고하세요. +Zyte 유료 호출은 비활성화되어 있으며 `ZYTE_API_KEY`를 설정해도 활성화되지 않습니다. + ```bash # Node 버전 맞추기 nvm use @@ -758,7 +848,7 @@ npm ci # 환경 변수 설정 cp .env.example .env -# .env 파일에 ZYTE_API_KEY 값 입력 +# 서비스별 환경 변수는 CONTRIBUTING.md와 운영 정책을 참고 # 품질 검사 (포맷/린트/타입/테스트) npm run check @@ -885,3 +975,83 @@ import { createCuService } from './services/cu/index.js'; registry.registerAll([createDaisoService, createCuService]); ``` + +## 분석 문서 + +### 공통 가이드 + +- [GS25 정상 로그인·직접 인증 복구 운영 절차](./gs25-auth-recovery-runbook.md) +- [서비스 레퍼런스](./service-reference.md) +- [스크래핑 플레이북](./scraping-playbook.md) +- [mitmproxy 가이드](./mitmproxy-guide.md) +- [AI 지시문](./ai-instruction.md) + +### 다이소 + +- [다이소 네트워크 분석 결과](./daiso-network-analysis-result.md) +- [다이소 Playwright 네트워크 분석](./daiso-playwright-network-analysis.md) +- [다이소 리플레이 세션 테스트 HTML](./daiso-replay-session-test.html) +- [다이소 테스트 리플레이 스크립트](./daiso-test-replay.ts) + +### CU + +- [CU 네트워크 분석 결과](./cu-network-analysis-result.md) +- [CU 앱 요청 캡처 가이드](./cu-app-request-capture-guide.md) +- [CU 앱 스크래핑 리플레이 가이드](./cu-app-scraping-replay-guide.md) + +### 이마트24 + +- [이마트24 네트워크 분석 결과](./emart24-network-analysis-result.md) +- [이마트24 앱 스크래핑 준비 가이드](./emart24-app-scraping-preparation-guide.md) +- [이마트24 앱 스크래핑 리플레이 가이드](./emart24-app-scraping-replay-guide.md) + +### 롯데마트 과거 분석 + +롯데마트 지원은 중단했습니다. 기존 REST 경로는 `410 SERVICE_RETIRED`, 기존 CLI 명령은 지원 중단 오류를 반환합니다. + +- [롯데마트 모바일 도와센터 스크래핑 리플레이 계획](./lottemart-mobile-scraping-replay-plan.md) + +### 공통 + +- [OpenAPI Actions facade 리팩토링 배경](./openapi-actions-facade.md) + +### 올리브영 + +- [올리브영 네트워크 분석 결과](./oliveyoung-network-analysis-result.md) +- [올리브영 Playwright MCP 온보딩](./oliveyoung-playwright-mcp-onboarding.md) +- [올리브영 Playwright 네트워크 분석](./oliveyoung-playwright-network-analysis.md) +- [올리브영 Lightpanda 검증](./oliveyoung-lightpanda-validation.md) +- [올리브영 리플레이 세션 테스트 스크립트](./oliveyoung-replay-session-test.ts) +- [올리브영 Zyte 대역폭 테스트](./oliveyoung-zyte-bandwidth-test.ts) +- [올리브영 Zyte 리플레이 테스트](./oliveyoung-zyte-replay-test.ts) + +### 영화관 + +- [CGV 네트워크 분석 결과](./cgv-network-analysis-result.md) +- [메가박스 네트워크 분석 결과](./megabox-network-analysis-result.md) +- [롯데시네마 네트워크 분석 결과](./lottecinema-network-analysis-result.md) + +### GS25 + +- [GS25 API 리플레이 방법론 (최종)](./gs25-final-replay-methodology.md) +- [GS25 네트워크 분석 결과 (아카이브)](./archive/gs25-network-analysis-result.md) +- [GS25 안드로이드 우회 캡처 가이드 (아카이브)](./archive/gs25-android-bypass-capture-guide.md) +- [GS25 앱 캡처 시도 로그 (2026-03-08, 아카이브)](./archive/gs25-app-capture-attempt-log-20260308.md) +- [GS25 앱 스크래핑 준비 가이드 (아카이브)](./archive/gs25-app-scraping-preparation-guide.md) +- [GS25 세션 인계 문서 (2026-03-09, 아카이브)](./archive/gs25-session-handoff-20260309.md) + +## 신규 MCP 기능 추가 시 유의사항 + +새로운 서비스나 도구를 추가할 때는 구현만 끝내지 말고 아래 반영 범위를 함께 확인해야 합니다. + +- `MCP`: `src/index.ts` 서비스 등록, 루트 서비스/도구 목록, 관련 테스트 반영 +- `HTTPS`: GET API 핸들러/라우트, 프롬프트 페이지(`src/pages/prompt.ts`), 앱 통합 테스트 반영 +- `CLI`: `src/cli.ts`, `src/cliHelp.ts`, CLI 테스트 반영 +- `AI instruction`: [ai-instruction.md](./ai-instruction.md) 사용 규칙/워크플로우 반영 +- `README`: 지원 서비스 설명, 예시, 문서 링크 반영 +- `OpenAPI`: 기본 `/openapi.json` facade 스펙, `/openapi-full.json` 전체 스펙, 관련 테스트 반영 + +기능 추가 후 최소 검증 기준: + +- `npm run typecheck` +- `npm test` diff --git a/scripts/ops/README.md b/scripts/ops/README.md index 032f05d8..2a0a699d 100644 --- a/scripts/ops/README.md +++ b/scripts/ops/README.md @@ -11,7 +11,7 @@ ## Workers 호출량 차트 -새 데이터를 가져와 `README.md`와 `assets/analytics/` 산출물을 갱신하려면 다음 환경 변수가 필요합니다. +새 데이터를 가져와 `scripts/ops/README.md`와 `assets/analytics/` 산출물을 갱신하려면 다음 환경 변수가 필요합니다. - `CLOUDFLARE_ACCOUNT_ID` - `CLOUDFLARE_API_TOKEN` @@ -20,3 +20,158 @@ `GET /`를 R2로 리다이렉트한 뒤 Worker를 우회하는 루트 요청은 Cloudflare zone analytics 보존기간 안에서만 보정합니다. 기본 보정 기간은 `WORKERS_CHART_ROOT_REQUESTS_RETENTION_DAYS=7`입니다. 기존 JSON으로 그래프만 다시 렌더링할 때는 Cloudflare 키 대신 `WORKERS_CHART_INPUT_JSON=assets/analytics/workers-invocations.json`을 지정할 수 있습니다. + + +
+ +

Cloudflare 요청 수 (2026-09-07 ~ 2026-10-06, 30일)

+ +Cloudflare 요청 수 그래프 (2026-09-07 ~ 2026-10-06) + +기준 워커: daiso-mcp · 마지막 갱신: 2026-10-07 05:21 KST +
집계: Worker 실행 + 루트 GET 리디렉션 요청 · 사용자 수와 다릅니다. + +
+ +> [!IMPORTANT] +> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요. + + + +### 운영 헬스 체크 + +공개 상태 페이지: **[Daiso MCP Status](https://aka-page.betteruptime.com/)** + +주변 음식점/카페 검색은 네이버 지역 검색 API를 사용합니다. 로컬 실행이나 배포 환경에는 `NAVER_CLIENT_ID`, `NAVER_CLIENT_SECRET`을 설정하세요. + +서비스별 API 상태를 즉시 확인할 때는 `GET /api/health/checks`를 사용합니다. 이 엔드포인트는 `HEALTH_CHECK_SECRET` 환경 변수가 설정되어 있어야 하며, 요청에는 `Authorization: Bearer ` 또는 `x-health-check-key: ` 헤더가 필요합니다. 내부 체크 요청의 기준 URL은 `HEALTH_CHECK_BASE_URL`로 지정할 수 있습니다. + +```bash +curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \ + "https://mcp.aka.page/api/health/checks?mode=full&fresh=true&includeSamples=true&timeoutMs=20000&slowThresholdMs=9000" +``` + +주요 쿼리: + +- `service=gs25`: 특정 서비스만 확인 +- `check=daiso.products`: 특정 체크만 확인 +- `mode=quick|deep|full`: 체크 모드 선택 +- `fresh=true`: 60초 캐시 우회 +- `includeSamples=true`: 첫 결과 이름 샘플 포함 +- `timeoutMs=20000`: 체크별 요청 제한 시간 +- `slowThresholdMs=9000`: 지정 시간보다 느린 성공 응답을 degraded로 표시 + +Health Checks 알림은 모든 실패·저하 서비스 ID를 상세 오류보다 먼저 표시합니다. 푸시 알림은 3,500자로 제한되며 전체 오류와 샘플은 해당 실행의 GitHub Actions 요약에 남습니다. 올리브영 릴레이의 설정 완전성은 `config.oliveyoungRelay`로 확인할 수 있습니다. 이 값은 연결 성공을 뜻하지 않으며, [설정 진단 문서](../../docs/oliveyoung-free-relay.md#설정-진단)를 참고하세요. + +Zyte 유료 요청은 키 설정 여부와 관계없이 비활성화되어 있습니다. 직접 JSON 요청이 실패하면 원래 HTTP 상태와 오류를 유지합니다. + +상태 기준: + +- `ok`: 필수 응답 구조와 최소 결과가 정상입니다. +- `degraded`: 기능은 살아 있지만 빈 결과, 느린 응답, 응답 구조 변화 등 확인이 필요합니다. +- `fail`: 외부 API 오류, 타임아웃, 인증 문제처럼 실제 장애로 봐야 합니다. + +Better Stack 같은 외부 모니터링에서는 `fail`을 장애 알림 기준으로 보고, `degraded`는 느린 외부 API나 응답 품질 저하를 추적하는 경고 신호로 봅니다. + +### 운영 통계 + +일일 호출 제한으로 차단한 요청의 집계는 인증된 `GET /api/rate-limit/stats`에서 조회합니다. 헬스 체크와 같은 `HEALTH_CHECK_SECRET`을 사용하며, 다음 두 인증 헤더를 모두 지원합니다. + +```bash +curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \ + "https://mcp.aka.page/api/rate-limit/stats" + +curl -H "x-health-check-key: $HEALTH_CHECK_SECRET" \ + "https://mcp.aka.page/api/rate-limit/stats?service=cgv" +``` + +쿼리 필터는 `from`, `to`, `service`입니다. `from`과 `to`는 함께 지정하거나 둘 다 생략해야 하며 날짜 형식은 `YYYY-MM-DD`입니다. `service`에는 `oliveyoung`, `cgv`, `cu`, `gs25`, `lottemart`만 사용할 수 있습니다. `lottemart` 필터는 지원 중단 전 호출 제한 통계를 조회하기 위해 유지합니다. 날짜를 생략하면 현재 KST 일자를 포함한 최근 7일을 조회합니다. 조회 가능한 보관 범위는 현재 KST 일자와 그 이전 29일이며, 한 번에 KST 달력 날짜 기준 최대 30일을 요청할 수 있습니다. + +성공 응답은 전체 합계와 일별·서비스별 차단 요청 수와 고유 차단 주체 수를 제공합니다. + +```json +{ + "success": true, + "data": { + "totals": { + "blockedRequests": 3, + "uniqueIdentities": 2 + }, + "daily": [ + { + "day": "2026-07-22", + "blockedRequests": 3, + "uniqueIdentities": 2 + } + ], + "services": [ + { + "day": "2026-07-22", + "service": "cgv", + "blockedRequests": 3, + "uniqueIdentities": 2 + } + ] + } +} +``` + +데이터는 30일 동안 보관하며 집계 응답은 원본 호출 주체나 IP를 노출하지 않습니다. Worker가 생성한 `DAILY_RATE_LIMIT_EXCEEDED` 결정 중 원장 커밋에 성공한 경우만 정확한 집계 범위에 포함됩니다. Cloudflare 또는 네트워크 계층의 429와 클라이언트 전송 결과, 연결 종료 결과는 이 범위에 포함되지 않습니다. 원장 쓰기에 실패하면 요청을 fail-open 처리하고 애플리케이션 429를 반환하지 않습니다. 통계는 이 기능의 배포 시점부터 수집하며 이전 429는 소급 집계하지 않습니다. + +배포 전 로컬에서 CLI 모드까지 확인할 때는 아래 명령을 사용합니다. + +```bash +npm run cli:smoke +``` + +기본 `openapi.json`은 OpenAI Actions import 제한에 맞추기 위해 `GET /api/actions/query` 단일 facade만 노출합니다. +기존 서비스별 GET API는 유지되며, 자세한 배경은 [OpenAPI Actions Facade 문서](../../docs/openapi-actions-facade.md)에 정리했습니다. + +인터랙티브 예시: + +```text +$ npx daiso +daiso 인터랙티브 모드 + +[서비스 선택] +1. 다이소 +2. 올리브영 +3. CU +서비스 번호를 선택하세요 (0: 종료): 1 + +매장 검색 키워드를 입력하세요: 강남 + +[매장 선택] +1. 다이소 강남점 | 서울 강남구 ... +2. 다이소 강남역점 | 서울 강남구 ... +입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색 +선택: /역점 +선택: 1 + +[선택한 매장 정보] +- 매장명: 다이소 강남역점 +- 주소: 서울 강남구 ... +- 전화: 02-... + +찾을 상품 키워드를 입력하세요: 수납박스 + +[상품 선택] +1. 손잡이 수납박스 (2000원, ID: 1034604) +2. 접이식 수납박스 (3000원, ID: 1034605) +입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소 +선택: 1 + +[재고 결과] +- 상품: 손잡이 수납박스 +- 매장: 다이소 강남역점 +- 재고 수량: 7 + +[다음 동작] +1. 같은 매장에서 다른 상품 찾기 +2. 다른 매장/서비스 다시 선택하기 +3. 종료하기 +번호를 선택하세요: 3 +인터랙티브 모드를 종료합니다. +``` + +
diff --git a/scripts/ops/update-workers-invocations-chart.ts b/scripts/ops/update-workers-invocations-chart.ts index 3d52d85d..40a10149 100644 --- a/scripts/ops/update-workers-invocations-chart.ts +++ b/scripts/ops/update-workers-invocations-chart.ts @@ -23,7 +23,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const REPO_ROOT = path.resolve(__dirname, '../..'); -const README_PATH = path.join(REPO_ROOT, 'README.md'); +const README_PATH = path.join(REPO_ROOT, 'scripts', 'ops', 'README.md'); const OUTPUT_DIR = path.join(REPO_ROOT, 'assets', 'analytics'); const CHART_PATH = path.join(OUTPUT_DIR, 'workers-invocations.png'); const DATA_PATH = path.join(OUTPUT_DIR, 'workers-invocations.json'); @@ -281,15 +281,7 @@ async function updateReadme(section) { await fs.writeFile(README_PATH, readme.replace(pattern, section), 'utf8'); return; } - const withoutSection = readme.replace(pattern, '').replace(/\n{3,}/g, '\n\n'); - const badgesAnchor = '\n\n
\n\n
\n\n\n\n
\n\n', + '
', + '', `

Cloudflare 요청 수 (${startDate} ~ ${endDate}, ${days}일)

`, '', - `Cloudflare 요청 수 그래프 (${startDate} ~ ${endDate})`, + `Cloudflare 요청 수 그래프 (${startDate} ~ ${endDate})`, '', `기준 워커: ${scriptName} · 마지막 갱신: ${updatedAt}`, `
집계: ${includesRootRedirect ? 'Worker 실행 + 루트 GET 리디렉션 요청' : 'Worker 실행'} · 사용자 수와 다릅니다.`, @@ -308,8 +310,6 @@ export function buildReadmeSection({ '> [!IMPORTANT]', '> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.', '', - '
', - '', '', ].join('\n'); } diff --git a/skills/daiso-cli/SKILL.md b/skills/daiso-cli/SKILL.md index b3fa8424..bb02fea6 100644 --- a/skills/daiso-cli/SKILL.md +++ b/skills/daiso-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: daiso-cli -description: Use this when a user wants to search Daiso/다이소, compare product price candidates, nearby restaurants/cafes/음식점/카페, convenience stores/편의점, marts, Olive Young/올리브영, Megabox/메가박스, Lotte Cinema/롯데시네마, or CGV data through the Daiso project. Prefer the daiso CLI for direct execution, use the MCP endpoint when the host app supports remote MCP, and choose commands for products, stores, inventory/재고, compare, places, movies, showtimes, seats, health checks, and raw JSON output. +description: Use this when a user wants to search Daiso/다이소, compare product price candidates, nearby restaurants/cafes/음식점/카페, convenience stores/편의점, marts, Olive Young/올리브영, Megabox/메가박스, Lotte Cinema/롯데시네마, or CGV data through the Daiso project. Honor explicit no-install requests with remote MCP first; otherwise prefer the daiso CLI for direct execution, use the MCP endpoint when the host app supports remote MCP, and choose commands for products, stores, inventory/재고, compare, places, movies, showtimes, seats, health checks, and raw JSON output. version: 1.0.6 metadata: openclaw: @@ -19,11 +19,28 @@ metadata: Use this skill to operate the Daiso MCP project through `npx daiso` and the public MCP endpoint. +## 설치 없이 첫 조회 + +사용자가 "설치 없이", "무설치"를 명시하면 로컬 CLI·스킬 설치 없이 원격 조회를 먼저 실행합니다. 연결된 원격 MCP 도구가 있으면 `daiso_search_products`를 바로 호출합니다. 원격 MCP 클라이언트의 서버 주소는 `https://mcp.aka.page`입니다. + +셸에서 직접 확인할 때는 아래 POST가 실제 MCP 도구 호출입니다. 이 공개 서버는 세션 없이 호출할 수 있으며 응답은 SSE의 `data:` JSON입니다. 범용 MCP 클라이언트 연결은 클라이언트가 initialize와 세션 절차를 처리합니다. + +```bash +curl -fsS --max-time 20 'https://mcp.aka.page' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + --data-binary '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}' +``` + +첫 조회 전에 `npx`, 패키지·스킬 설치, health/help, 전체 스키마나 도구 목록 탐색을 하지 않습니다. 위 예제의 검색어를 사용자 요청으로 바꾸고 조회 결과부터 제공합니다. 사용자 조건이나 호출 오류 때문에 필요한 경우에만 추가 안내를 읽습니다. 호출에 성공하면 추가 진단 없이 결과를 보여줍니다. + +`GET https://mcp.aka.page/api/daiso/products?q=수납박스`는 같은 서비스의 REST 직접 조회입니다. GET만 가능한 환경에서는 사용할 수 있지만 MCP 도구를 호출했다고 설명하지 않습니다. HTTP·네트워크 오류만으로 로그인 필요나 서버 장애를 단정하지 않고 실제 오류와 실행 환경을 확인합니다. + ## Core Rule -CLI를 우선 사용한다. The CLI is the most reliable path when you can run shell commands. Use the MCP endpoint when the user is configuring an AI app or explicitly asks for MCP connection details. +무설치 요청이 아니고 CLI 사용이 허용된 환경에서는 CLI를 우선 사용한다. The CLI is the preferred path when you can run shell commands and package execution is allowed. Use the MCP endpoint when the user is configuring an AI app or explicitly asks for MCP connection details. -## Quick Checks +## Quick Checks (오류 진단이 필요할 때만) ```bash npx daiso health @@ -65,6 +82,8 @@ For more command selection examples, read `references/cli-command-map.md`. ## Request Recipes +아래 CLI 레시피는 CLI 사용이 허용된 경우에만 적용합니다. 무설치 요청에서는 같은 조회를 원격 MCP 도구로 실행합니다. + - "콜라 어디가 싸?", "컵라면 가격 비교해줘": run `npx daiso compare --json` first. Confirm stock or sale prices with service-specific inventory only when the user asks for store-level availability. - "강남역 근처 카페", "성수동 브런치 음식점": run `npx daiso places --category cafe|restaurant --json` or use `--keyword` for the food or mood. Do not ask which retail brand they mean. - "다이소 핫식스 재고", "올리브영 두바이초콜릿 있어?": keep the named brand. Search that service first even if the product seems unusual for the brand, then suggest alternatives only after no results. diff --git a/src/index.ts b/src/index.ts index 8e02efbc..0736befb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { createPlacesService } from './services/places/index.js'; import { createOpinetService } from './services/opinet/index.js'; import { createCompareService } from './services/compare/index.js'; import { createFeedbackService } from './services/feedback/index.js'; +import { createAgentStartResponse } from './pages/agentStart.js'; import { createPromptResponse } from './pages/prompt.js'; import { createFullOpenApiJsonResponse, @@ -152,6 +153,7 @@ function buildRootInfo() { description: 'Multi-Service MCP Server for Cloudflare Workers', endpoints: { mcp: '/ 또는 /mcp (POST) - MCP 프로토콜 엔드포인트', + quickStart: '/llms.txt (GET) - 설치 없는 첫 조회 안내', health: '/health (GET) - 헬스 체크', healthChecks: '/api/health/checks (GET) - 서비스별 상세 헬스 체크', openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI', @@ -355,6 +357,9 @@ registerRouteHealthRoutes(app); registerRateLimitStatsRoutes(app); registerServiceRoutes(app); +// 설치 없는 첫 조회 안내 +app.get('/llms.txt', (c) => createAgentStartResponse(new URL(c.req.url).origin)); + // 프롬프트 페이지 (MCP 미지원 에이전트용) app.get('/prompt', (c) => { const baseUrl = new URL(c.req.url).origin; diff --git a/src/pages/agentStart.ts b/src/pages/agentStart.ts new file mode 100644 index 00000000..98db9963 --- /dev/null +++ b/src/pages/agentStart.ts @@ -0,0 +1,40 @@ +/** 설치 없이 첫 실제 조회를 실행하는 에이전트 안내 */ +export function buildAgentStartText(baseUrl: string): string { + return `# Daiso MCP: 설치 없이 시작하기 + +공개 원격 MCP: https://mcp.aka.page +전송: Streamable HTTP · 인증: 없음 (사용자 API 키 불필요) +전체 도구 목록이나 OpenAPI를 먼저 읽을 필요는 없습니다. 사용 가능한 실행 수단으로 아래 첫 조회를 실행하세요. + +1. 원격 MCP 클라이언트가 있으면 공개 URL에 연결하세요. 표준 initialize → notifications/initialized 흐름을 사용하고 서버가 발급한 mcp-session-id를 전달합니다. 첫 도구는 daiso_search_products, 인자는 {"query":"수납박스","pageSize":3}입니다. +2. HTTP POST나 셸이 있으면 아래 한 요청으로 실제 MCP 도구를 호출할 수 있습니다. npm 설치는 필요 없습니다. 루트 POST의 stateless 호출은 이 서버가 지원하는 단축 경로이며, /mcp는 initialize가 필요합니다. + +\`\`\`bash +curl --max-time 20 -sS -N ${baseUrl}/ \\ + -H 'Content-Type: application/json' \\ + -H 'Accept: application/json, text/event-stream' \\ + --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}' +\`\`\` + +응답은 SSE의 data: JSON-RPC 메시지입니다. result.isError 또는 error를 확인하고, 상품은 result.structuredContent.products에서 읽으세요. content[].text에도 상품 JSON이 있습니다. 오류를 상품 없음으로 해석하지 마세요. + +3. HTTP GET만 가능하면 공개 REST GET을 사용하세요. 이것은 MCP 프로토콜 호출이 아닙니다. +${baseUrl}/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3 + +실제 상품 결과의 id로 재고를 조회하세요. 예제 ID를 쓰거나 상품 검색 결과만으로 매장 재고를 단정하지 마세요. +상세 API와 서비스 안내: ${baseUrl}/prompt + +--- + +`; +} + +/** 캐시 가능한 시작 안내 응답을 생성합니다. */ +export function createAgentStartResponse(baseUrl: string): Response { + return new Response(buildAgentStartText(baseUrl), { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Cache-Control': 'public, max-age=3600', + }, + }); +} diff --git a/src/pages/prompt.ts b/src/pages/prompt.ts index 88e620a2..8745abab 100644 --- a/src/pages/prompt.ts +++ b/src/pages/prompt.ts @@ -5,6 +5,7 @@ * 에이전트가 이 페이지를 읽고 GET API를 사용할 수 있습니다. */ +import { buildAgentStartText } from './agentStart.js'; import { buildPromptCoreText } from './promptCoreText.js'; import { buildPromptReferenceText } from './promptReferenceText.js'; import { buildPromptRetailCinemaText } from './promptRetailCinemaText.js'; @@ -14,6 +15,7 @@ import { buildPromptRetailCinemaText } from './promptRetailCinemaText.js'; */ export function generatePromptText(baseUrl: string): string { return [ + buildAgentStartText(baseUrl), buildPromptCoreText(baseUrl), buildPromptRetailCinemaText(baseUrl), buildPromptReferenceText(baseUrl), diff --git a/src/pages/promptCoreText.ts b/src/pages/promptCoreText.ts index cd2fee0e..8fd97f22 100644 --- a/src/pages/promptCoreText.ts +++ b/src/pages/promptCoreText.ts @@ -6,7 +6,7 @@ export function buildPromptCoreText(baseUrl: string): string { return `# 다이소 MCP API 다이소 제품 검색, 매장 찾기, 재고 확인을 위한 API입니다. -모든 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다. +아래 상세 REST API 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다. Base URL: ${baseUrl} diff --git a/tests/app/app-info-pages.test.ts b/tests/app/app-info-pages.test.ts index f9fc0480..0cf03d90 100644 --- a/tests/app/app-info-pages.test.ts +++ b/tests/app/app-info-pages.test.ts @@ -18,6 +18,7 @@ describe('GET /', () => { const data = await res.json(); expect(data.name).toBe('multi-service-mcp'); expect(data.version).toBe('1.0.0'); + expect(data.endpoints.quickStart).toContain('/llms.txt'); expect(data.services).toBeDefined(); expect(data.tools).toBeDefined(); expect(data.totalServices).toBeGreaterThan(0); @@ -370,6 +371,18 @@ describe('기본 페이지', () => { }); describe('문서 페이지', () => { + it('GET /llms.txt는 외부 조회 없이 설치 없는 시작 안내를 반환한다', async () => { + const callsBefore = mockFetch.mock.calls.length; + const res = await app.fetch(new Request('https://guide.example/llms.txt')); + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8'); + expect(res.headers.get('Cache-Control')).toBe('public, max-age=3600'); + const text = await res.text(); + expect(text).toContain('https://guide.example/prompt'); + expect(text).toContain('daiso_search_products'); + expect(mockFetch.mock.calls.length).toBe(callsBefore); + }); + it('GET /openapi.json 응답을 반환한다', async () => { const res = await app.request('/openapi.json'); expect(res.status).toBe(200); diff --git a/tests/app/app-mcp-client.test.ts b/tests/app/app-mcp-client.test.ts index dda57406..205a7913 100644 --- a/tests/app/app-mcp-client.test.ts +++ b/tests/app/app-mcp-client.test.ts @@ -5,6 +5,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import app from '../../src/index.js'; +import { buildAgentStartText } from '../../src/pages/agentStart.js'; import type { AppBindings } from '../../src/api/response.js'; import { createMockProductResponse } from '../api/testHelpers.js'; @@ -48,6 +49,34 @@ async function createLocalMcpClient(bindings?: AppBindings): Promise { } describe('MCP client smoke', () => { + it('시작 안내의 단일 POST가 initialize 없이 실제 상품을 반환한다', async () => { + mockFetch.mockResolvedValueOnce(new Response(JSON.stringify( + createMockProductResponse([{ PD_NO: '1049516', PDNM: '수납박스', PD_PRC: '5000' }], 1), + ))); + const guide = buildAgentStartText('https://local.test'); + const body = guide.match(/--data '([^']+)'/)?.[1]; + expect(body).toBeDefined(); + const response = await app.fetch(new Request('https://local.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' }, + body, + })); + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeNull(); + expect(response.headers.get('Content-Type')).toContain('text/event-stream'); + const dataLine = (await response.text()).split('\n').find((line) => line.startsWith('data: ')); + expect(dataLine).toBeDefined(); + const message = JSON.parse(dataLine!.slice(6)); + expect(message.error).toBeUndefined(); + expect(message.result.isError).not.toBe(true); + expect(message.result.structuredContent.products).toEqual([ + expect.objectContaining({ id: '1049516', name: '수납박스', price: 5000 }), + ]); + expect(message.result.structuredContent.query).toBe('수납박스'); + expect(message.result.structuredContent.pageSize).toBe(3); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + it('SDK 도구 목록의 모든 도구가 outputSchema를 노출한다', async () => { const client = await createLocalMcpClient(); try { diff --git a/tests/pages/agentStart.test.ts b/tests/pages/agentStart.test.ts new file mode 100644 index 00000000..37715dae --- /dev/null +++ b/tests/pages/agentStart.test.ts @@ -0,0 +1,52 @@ +/** 설치 없는 에이전트 시작 안내 테스트 */ +import { describe, it, expect } from 'vitest'; +import { buildAgentStartText, createAgentStartResponse } from '../../src/pages/agentStart.js'; +import { generatePromptText } from '../../src/pages/prompt.js'; + +describe('에이전트 시작 안내', () => { + it('첫 MCP 요청을 실행 가능한 JSON-RPC로 제공한다', () => { + const text = buildAgentStartText('https://example.com'); + const body = text.match(/--data '([^']+)'/)?.[1]; + expect(body).toBeDefined(); + expect(JSON.parse(body!)).toEqual({ + jsonrpc: '2.0', id: 1, method: 'tools/call', + params: { name: 'daiso_search_products', arguments: { query: '수납박스', pageSize: 3 } }, + }); + expect(text).toContain('https://example.com/'); + expect(text).toContain('application/json, text/event-stream'); + expect(text).toContain('structuredContent'); + expect(text).toContain('isError'); + }); + + it('원격 MCP 연결과 HTTP POST, REST GET을 구분하고 전체 문서 읽기를 요구하지 않는다', () => { + const text = buildAgentStartText('https://example.com'); + expect(text).toContain('https://mcp.aka.page'); + expect(text).toContain('Streamable HTTP'); + expect(text).toContain('인증: 없음'); + expect(text).toContain('initialize'); + expect(text).toContain('stateless'); + expect(text).toContain('REST GET'); + expect(text).toContain('MCP 프로토콜 호출이 아닙니다'); + expect(text).toContain('/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3'); + expect(text).toContain('https://example.com/prompt'); + expect(text).toContain('전체 도구 목록이나 OpenAPI를 먼저 읽을 필요는 없습니다'); + expect(text.split('\n').length).toBeLessThan(45); + }); + + it('동일한 시작 안내 뒤에 기존 상세 프롬프트를 보존한다', () => { + const baseUrl = 'https://example.com'; + const text = generatePromptText(baseUrl); + expect(text.startsWith(buildAgentStartText(baseUrl))).toBe(true); + expect(text.indexOf('# 다이소 MCP API')).toBeGreaterThan(text.indexOf('REST GET')); + expect(text).toContain('/api/daiso/inventory?productId={제품ID}'); + expect(text).toContain('MCP 지원 서비스'); + }); + + it('캐시 가능한 평문 응답을 반환한다', async () => { + const response = createAgentStartResponse('https://example.com'); + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8'); + expect(response.headers.get('Cache-Control')).toBe('public, max-age=3600'); + expect(await response.text()).toBe(buildAgentStartText('https://example.com')); + }); +}); diff --git a/tests/scripts/repository-config.test.ts b/tests/scripts/repository-config.test.ts index bf469d63..0a717ada 100644 --- a/tests/scripts/repository-config.test.ts +++ b/tests/scripts/repository-config.test.ts @@ -144,9 +144,7 @@ describe('repository maintenance configuration', () => { expect(workflow).toContain( 'CLI_SMOKE_SERVICES: daiso gs25 seveneleven emart24 oliveyoung megabox lottecinema cgv opinet', ); - expect(workflow).toContain( - 'MCP_SMOKE_SERVICES: daiso gs25 seveneleven emart24 opinet', - ); + expect(workflow).toContain('MCP_SMOKE_SERVICES: daiso gs25 seveneleven emart24 opinet'); expect(workflow).not.toContain('lottemart'); expect(workflow).toContain('SMOKE_ATTEMPT_TIMEOUT_SECONDS: 90'); expect(workflow).toContain('for SMOKE_SERVICE in ${CLI_SMOKE_SERVICES}; do'); @@ -206,7 +204,7 @@ describe('repository maintenance configuration', () => { expect(workflow).toContain('CLOUDFLARE_ZONE_ID'); expect(workflow).toContain('WORKERS_CHART_ROOT_REDIRECT_START'); expect(workflow).toContain( - 'git add README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png', + 'git add scripts/ops/README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png', ); expect(workflow).toContain('git pull --rebase --autostash origin main'); expect(workflow).toContain('git push origin HEAD:main'); @@ -225,7 +223,9 @@ describe('repository maintenance configuration', () => { expect(workflow).toContain( '/api/health/checks?mode=full&fresh=true&includeSamples=true&timeoutMs=20000&slowThresholdMs=9000', ); - expect(workflow).toContain('/api/health/checks?mode=quick&fresh=true&timeoutMs=5000&slowThresholdMs=3000'); + expect(workflow).toContain( + '/api/health/checks?mode=quick&fresh=true&timeoutMs=5000&slowThresholdMs=3000', + ); expect(workflow).toContain('HEALTH_CHECK_FORCE_FRESH="true"'); expect(workflow).not.toContain('HEALTH_CHECK_FORCE_FRESH="false"'); expect(workflow).toContain('x-health-check-force-fresh: ${HEALTH_CHECK_FORCE_FRESH}'); @@ -254,6 +254,7 @@ describe('repository maintenance configuration', () => { for (const workflow of [ci, coverage, deploy, codeql]) { expect(workflow).toContain('paths-ignore:'); expect(workflow).toContain("'README.md'"); + expect(workflow).toContain("'scripts/ops/README.md'"); expect(workflow).toContain("'assets/analytics/**'"); } }); @@ -297,10 +298,21 @@ describe('repository maintenance configuration', () => { expect(readme).toContain('Daiso MCP 및 Skill'); expect(readme).toContain('skills/daiso-cli/SKILL.md'); expect(readme).toContain('npx daiso'); + const fastStart = readme.split('\n').slice(0, 100).join('\n'); + expect(fastStart).toContain('https://mcp.aka.page'); + expect(fastStart).toContain('Streamable HTTP'); + expect(fastStart).toContain('API 키·토큰·OAuth 설정 불필요'); + expect(fastStart).toContain('로컬 설치·빌드·API 키 준비 없이'); + expect(fastStart).toContain('Accept: application/json, text/event-stream'); + expect(fastStart).toContain('"method":"tools/call"'); + expect(fastStart).toContain('"name":"daiso_search_products"'); + expect(fastStart).toContain('"query":"수납박스"'); + expect(readme).not.toContain('WORKERS_INVOCATIONS_CHART:START'); + expect(readme).toContain('scripts/ops/README.md'); }); it('운영 문서는 정확한 429 통계 조회 계약과 범위를 설명한다', () => { - const readme = readText('README.md'); + const readme = readText('scripts/ops/README.md'); const serviceReference = readText('docs/service-reference.md'); const readmeOperations = readme.slice( readme.indexOf('### 운영 통계'), @@ -312,17 +324,17 @@ describe('repository maintenance configuration', () => { ); const curlBlockMatch = readmeOperations.match(/```bash\r?\n([\s\S]*?)\r?\n```/); if (!curlBlockMatch?.[1]) { - throw new Error('README 운영 통계 절에 `bash` 코드 블록 호출 예시가 필요합니다.'); + throw new Error('운영 가이드 통계 절에 `bash` 코드 블록 호출 예시가 필요합니다.'); } const jsonBlockMatch = readmeOperations.match(/```json\r?\n([\s\S]*?)\r?\n```/); if (!jsonBlockMatch?.[1]) { - throw new Error('README 운영 통계 절에 `json` 코드 블록 성공 응답 예시가 필요합니다.'); + throw new Error('운영 가이드 통계 절에 `json` 코드 블록 성공 응답 예시가 필요합니다.'); } let statsExample: unknown; try { statsExample = JSON.parse(jsonBlockMatch[1]); } catch (error) { - throw new Error(`README 운영 통계 JSON 예시를 파싱할 수 없습니다: ${String(error)}`); + throw new Error(`운영 가이드 통계 JSON 예시를 파싱할 수 없습니다: ${String(error)}`); } expect(readmeOperations).toContain('GET /api/rate-limit/stats'); diff --git a/tests/scripts/workers-chart-entrypoint.test.ts b/tests/scripts/workers-chart-entrypoint.test.ts index 0df4ff6d..cff51480 100644 --- a/tests/scripts/workers-chart-entrypoint.test.ts +++ b/tests/scripts/workers-chart-entrypoint.test.ts @@ -22,7 +22,8 @@ async function fixture() { ]) { await fs.copyFile(path.join('scripts/ops', name), path.join(root, 'scripts/ops', name)); } - await fs.writeFile(path.join(root, 'README.md'), '# Test\n'); + await fs.writeFile(path.join(root, 'README.md'), '# Public guide\n'); + await fs.writeFile(path.join(root, 'scripts/ops/README.md'), '# Test\n'); const date = formatKstDate( new Date(parseKstDateText(formatKstDate(new Date())).getTime() - 86400000), ); @@ -94,7 +95,7 @@ describe('workers chart entrypoint', () => { }), ).toThrow('Incomplete daily traffic'); expect(await fs.readFile(dataPath, 'utf8')).toBe(original); - expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe('# Test\n'); + expect(await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8')).toBe('# Test\n'); }); it('실시간 실행에서 저장된 완전한 날짜를 읽고 보존한다', async () => { const { root, payload, dataPath } = await fixture(); @@ -116,6 +117,10 @@ describe('workers chart entrypoint', () => { run(root, { WORKERS_CHART_INPUT_JSON: dataPath }); const output = JSON.parse(await fs.readFile(dataPath, 'utf8')); expect(output).toMatchObject(payload); + expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe('# Public guide\n'); + const operations = await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8'); + expect(operations.startsWith('# Test\n')).toBe(true); + expect(operations).toContain('../../assets/analytics/workers-invocations.png'); expect( (await fs.stat(path.join(root, 'assets/analytics/workers-invocations.png'))).size, ).toBeGreaterThan(0); @@ -123,11 +128,11 @@ describe('workers chart entrypoint', () => { it('CRLF 문서에서도 기존 차트 위치와 앞뒤 내용을 보존한다', async () => { const { root, dataPath } = await fixture(); await fs.writeFile( - path.join(root, 'README.md'), + path.join(root, 'scripts/ops/README.md'), '# Header\r\n\r\n\r\nold\r\n\r\n\r\n## Footer\r\n', ); run(root, { WORKERS_CHART_INPUT_JSON: dataPath }); - const readme = await fs.readFile(path.join(root, 'README.md'), 'utf8'); + const readme = await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8'); expect(readme.startsWith('# Header\n\n')).toBe(true); expect(readme.endsWith('\n\n## Footer\n')).toBe(true); expect(readme).toContain('Worker 실행 + 루트 GET 리디렉션 요청'); diff --git a/tests/scripts/workers-chart-helpers.test.ts b/tests/scripts/workers-chart-helpers.test.ts index 2bbca621..a0260f86 100644 --- a/tests/scripts/workers-chart-helpers.test.ts +++ b/tests/scripts/workers-chart-helpers.test.ts @@ -31,7 +31,7 @@ describe('buildReadmeSection', () => { ); }); - it('GitHub 알림 카드가 되도록 중앙 정렬 영역 밖에 안내문을 둔다', () => { + it('운영 문서 상대 이미지 경로와 독립된 중앙 정렬 영역을 사용한다', () => { const section = buildReadmeSection({ scriptName: 'daiso-mcp', updatedAt: '2026-07-17 23:05 KST', @@ -42,7 +42,10 @@ describe('buildReadmeSection', () => { }); expect(section).toContain('
\n\n> [!IMPORTANT]'); - expect(section).toContain('이용해 주세요.\n\n
'); + expect(section).toContain('../../assets/analytics/workers-invocations.png'); + expect(section).toContain('\n
'); + expect(section.match(/
/g)).toHaveLength(1); }); });