Namu Watcher
by mwl313
README.md
# Namu Watch π
**AIλ‘ λ무μν€λ₯Ό μ½λ€**
Namu Watchλ AI μ±λ΄μ΄ μ¬μ©μμ μμ°μ΄ μμ²μ λ°λΌ λ무μν€ λ¬Έμλ₯Ό κ²μνκ³ μ½μ μ μκ² ν΄μ£Όλ MCP μλ²μ
λλ€.
"**λ무μν€μμ μ΄μμ μλ €μ€**" β μ΄ νλ§λλ‘ AIκ° λ무μν€ λ¬Έμλ₯Ό μ°Ύμ μμ½ν΄μ€λλ€. μ€μκ° κ²μμ΄ νΈλ λμ κ° ν€μλκ° μ λ¨λμ§λ ν¨κ» νμΈν μ μμ΅λλ€.
---
## μ£Όμ κΈ°λ₯
| κΈ°λ₯ | λꡬλͺ
| μ€λͺ
|
|:-----|:-------|:------|
| π **λ무μν€ κ²μ** | `search` | λ무μν€ λ¬Έμ κ²μ λ° λ΄μ© μ‘°ν (8000μ λ¨μ μ²ν¬ λΆν , λ©ν°λΈλ‘ μλ΅). **ν΅μ¬ κΈ°λ₯** |
| π₯ **μ€μκ° κ²μμ΄** | `trending` | λ무μν€ κ³΅μ API κΈ°λ° μ€κ² TOP N μ‘°ν |
| β **μ λ΄λμ§** | `why` | νΉμ ν€μλκ° μ€κ²μ μ€λ₯Έ μ΄μ μ€λͺ
(μμΉ΄λΌμ΄λΈ β λ무μν€ fallback) |
| π
**κ³Όκ±° μ΄λ ₯** | `history` | νΉμ λ μ§μ μ€κ² μμ λλ ν€μλ μκ°λ³ λ±μ₯ κΈ°λ‘ |
| π **κΈμμΉ κ°μ§** | `velocity` | μ΅κ·Ό Nμκ° λμ μ‘°νμ κΈμ¦ ν€μλ νμ§ |
| π **μ°κ΄ ν€μλ** | `related` | νΉμ ν€μλμ ν¨κ» μμ£Ό λ±μ₯νλ μ°κ΄ ν€μλ |
| π« **ν¨κ» νΈλ λ©** | `trending_together` | λμκ°λ ν¨κ» λ±μ₯νλ ν€μλ μ λΆμ |
| π **μ€λμ 리ν¬νΈ** | `trend_of_day` | μ€λμ μ€κ² μ’
ν© λ¦¬ν¬νΈ |
## μ¬μ© μμ
μ¬μ©μλ μΉ΄μΉ΄μ€ν‘ μ±ν
μμ μμ°μ΄λ‘ μ§λ¬Ένλ©΄ λ©λλ€. λͺ¨λ λͺ
λ Ήμ΄λ AIκ° μλμΌλ‘ μΈμν©λλ€.
```
π€ "λ무μν€μμ μ΄μμ μ λν΄ μλ €μ€"
β AIκ° search("μ΄μμ ") νΈμΆ β λ¬Έμ μ 체λ₯Ό μ²ν¬ λ¨μλ‘ μμ
π€ "μ΄μμ μ μ‘°μ μ€κΈ°μ 무μ μΌλ‘..."
π€ "γ±γ±γ
μ΄ λꡬμΌ?"
β AIκ° search("γ±γ±γ
") νΈμΆ β fallback chain: "γ±γ±γ
" β "γ±γ±" β μ μ ν λ¬Έμ νμ
π€ "κ³ κ΅¬λ§μμ΄λΌκ³ λ λΆλ¦¬λ..."
π€ "μ§κΈ μ€κ² λ λ¨κ³ μμ΄?"
β AIκ° trending() νΈμΆ β λ무μν€ κ³΅μ μ€κ² API
π€ "π₯ μ€μκ° κ²μμ΄ TOP 10..."
π€ "νΈλ λ μ μ€κ²μ λ΄μ΄?"
β AIκ° why("νΈλ λ") νΈμΆ β μμΉ΄λΌμ΄λΈ κ²μκΈ κ²μ β λ무μν€ fallback
π€ "μ€λ μ€νμΈκ³Όμ 16κ°μ μ μ λ° μΆμ νμΌλ..."
```
## λ°μ΄ν° μμ€
- **μ£Ό μμ€**: [λ무μν€](https://namu.wiki) β AIκ° μ¬μ©μμ μ§λ¬Έμ λ΅νκΈ° μν΄ λ¬Έμλ₯Ό κ²μνκ³ μμ½ν©λλ€.
- 3λ¨κ³ μ μ μ°ν: μ§μ μ μ β μΏ ν€ νλ β Google Cache
- **μ€κ² λ°μ΄ν°**: [λ무μν€ κ³΅μ μ€κ² API](https://search.namu.wiki/api/ranking) β μ€μκ° κ²μμ΄ μμ μ 곡
- **μ€κ² μ΄μ **: [μμΉ΄λΌμ΄λΈ "λ무μν€ μ€κ² μλ €μ£Όλ μ±λ"](https://arca.live/b/namuhotnow)
- κ° κ²μκΈ = νλμ μ€κ² ν€μλ + μ¬λμ΄ μμ±ν μ΄μ μ€λͺ
## κΈ°μ μ€ν
| νλͺ© | κΈ°μ |
|:-----|:------|
| **μΈμ΄** | TypeScript (Node.js 20+) |
| **λ°νμ** | Node.js 20+ (ESM, `"type": "module"`) |
| **MCP νλ‘ν μ½** | Streamable HTTP β `POST /mcp` (JSON-RPC 2.0) |
| **HTTP μλ²** | Node.js λ΄μ₯ `http` λͺ¨λ |
| **HTML νμ±** | cheerio |
| **λ°μ΄ν°λ² μ΄μ€** | sql.js (SQLite β WASM, λ€μ΄ν°λΈ λΉλ λΆνμ) |
| **μ
λ ₯ κ²μ¦** | zod |
| **λ°°ν¬** | Docker (linux/amd64) |
## λΉ λ₯Έ μμ
### μ¬μ μꡬμ¬ν
- Node.js 20+
- npm
### μ€μΉ λ° μ€ν
```bash
git clone https://github.com/mwl313/NamuWatcher.git
cd NamuWatcher
npm install
npm run build
node dist/index.js
```
μλ²κ° `http://localhost:3000`μμ μ€νλ©λλ€.
### Docker
```bash
docker build --platform linux/amd64 -t namu-watch .
docker run --platform linux/amd64 -p 3000:3000 -v namu-data:/app/data namu-watch
```
## API μλν¬μΈνΈ
| λ©μλ | κ²½λ‘ | μ€λͺ
|
|:-------|:-----|:------|
| `POST` | `/mcp` | MCP Streamable HTTP (JSON-RPC 2.0) |
| `GET` | `/healthz` | ν¬μ€μ²΄ν¬ (μν JSON λ°ν) |
| `OPTIONS` | `*` | CORS preflight |
### MCP νλ‘ν μ½
ν΄λΌμ΄μΈνΈλ νμ€ MCP JSON-RPC λ©μμ§λ₯Ό `POST /mcp`λ‘ μ μ‘ν©λλ€.
**μ΄κΈ°ν:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
```
**λꡬ μ‘°ν:**
```json
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
```
**λꡬ νΈμΆ (μ: search):**
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search",
"arguments": { "keyword": "μ΄μμ " }
}
}
```
### μλ΅ νμ (search λꡬ)
search λꡬλ λ©ν°λΈλ‘ μλ΅μ λ°νν©λλ€.
```json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"type\":\"search\",\"title\":\"μ΄μμ \",\"url\":\"...\",\"total_chunks\":5}"
},
{
"type": "text",
"text": "[1/5]\nμ΄μμ μ μ‘°μ μ€κΈ°μ 무μ μΌλ‘..."
},
{
"type": "text",
"text": "[2/5]\n1592λ
μμ§μλμ΄ λ°λ°νμ..."
}
]
}
}
```
- 첫 λ²μ§Έ λΈλ‘: λ©νμ 보 JSON (`title`, `url`, `total_chunks`)
- μ΄ν λΈλ‘: `[i/total]` λΌλ²¨ + 8000μ λ¨μ λ³Έλ¬Έ
- AIλ λ©νμ 보λ₯Ό μ½κ³ , νμν μ²ν¬λ₯Ό μ ννμ¬ λ΅λ³μ μμ±ν©λλ€.
## νκ²½ λ³μ
| λ³μ | κΈ°λ³Έκ° | μ€λͺ
|
|:-----|:-------|:------|
| `PORT` | `3000` | HTTP μλ² ν¬νΈ |
| `DB_PATH` | `/app/data/namu_watch.db` | SQLite νμΌ κ²½λ‘ |
| `NODE_ENV` | `production` | μ€ν λͺ¨λ |
## νλ‘μ νΈ κ΅¬μ‘°
```
namu-watch/
βββ src/
β βββ index.ts # HTTP μλ² + JSON-RPC νΈλ€λ¬
β βββ types.ts # κ³΅ν΅ νμ
μ μ
β βββ utils.ts # μ νΈλ¦¬ν° ν¨μ (fetch, delay, μΉ΄ν
κ³ λ¦¬ λΆλ₯)
β βββ tools/
β β βββ search.ts # π λ무μν€ λ¬Έμ κ²μ (μ²ν¬ λΆν + λ©ν°λΈλ‘ μλ΅)
β β βββ trending.ts # π₯ λ무μν€ κ³΅μ API μ€κ² TOP N
β β βββ why.ts # β μ€κ² μ΄μ μ€λͺ
(μμΉ΄λΌμ΄λΈ β λ무μν€ fallback)
β β βββ history.ts # π
κ³Όκ±° μ€κ² μ΄λ ₯ μ‘°ν
β β βββ velocity.ts # π κΈμμΉ ν€μλ κ°μ§
β β βββ related.ts # π μ°κ΄ ν€μλ
β β βββ trending-together.ts # π« ν¨κ» λ¨λ ν€μλ
β β βββ trend-of-day.ts # π μ’
ν© λ¦¬ν¬νΈ
β βββ scrapers/
β β βββ namuwiki.ts # π λ무μν€ λ¬Έμ νμ± (3λ¨κ³ μ°ν: μ§ν β μΏ ν€ β Google Cache)
β β βββ ranking.ts # π search.namu.wiki 곡μ μ€κ² API
β β βββ arca.ts # ποΈ μμΉ΄λΌμ΄λΈ μ€κ²μ±λ μ€ν¬λν (vrow ꡬ쑰)
β βββ db/
β βββ schema.ts # SQLite μ΄κΈ°ν (snapshots + articles)
β βββ queries.ts # DB CRUD ν¨μ
βββ Dockerfile
βββ package.json
βββ tsconfig.json
```
## λΌμ΄μ μ€
MITThis server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues