@zonuexe/techbook-mcp 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/README.md +54 -22
- package/dist/adapters/http/fetch-client.d.ts.map +1 -1
- package/dist/adapters/http/fetch-client.js +18 -1
- package/dist/adapters/http/fetch-client.js.map +1 -1
- package/dist/adapters/openbd.d.ts.map +1 -1
- package/dist/adapters/openbd.js +18 -5
- package/dist/adapters/openbd.js.map +1 -1
- package/dist/adapters/publishers/base.d.ts +2 -1
- package/dist/adapters/publishers/base.d.ts.map +1 -1
- package/dist/adapters/publishers/base.js +8 -3
- package/dist/adapters/publishers/base.js.map +1 -1
- package/dist/adapters/publishers/cq-publishing.d.ts +3 -0
- package/dist/adapters/publishers/cq-publishing.d.ts.map +1 -0
- package/dist/adapters/publishers/cq-publishing.js +120 -0
- package/dist/adapters/publishers/cq-publishing.js.map +1 -0
- package/dist/adapters/publishers/google-books.d.ts +4 -0
- package/dist/adapters/publishers/google-books.d.ts.map +1 -0
- package/dist/adapters/publishers/google-books.js +76 -0
- package/dist/adapters/publishers/google-books.js.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
- package/dist/adapters/publishers/juse-p.d.ts +3 -0
- package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
- package/dist/adapters/publishers/juse-p.js +110 -0
- package/dist/adapters/publishers/juse-p.js.map +1 -0
- package/dist/adapters/publishers/leanpub.d.ts +3 -0
- package/dist/adapters/publishers/leanpub.d.ts.map +1 -0
- package/dist/adapters/publishers/leanpub.js +96 -0
- package/dist/adapters/publishers/leanpub.js.map +1 -0
- package/dist/adapters/publishers/oreilly-japan.d.ts.map +1 -1
- package/dist/adapters/publishers/oreilly-japan.js +8 -2
- package/dist/adapters/publishers/oreilly-japan.js.map +1 -1
- package/dist/adapters/publishers/peaks.d.ts.map +1 -1
- package/dist/adapters/publishers/peaks.js +3 -2
- package/dist/adapters/publishers/peaks.js.map +1 -1
- package/dist/adapters/publishers/personal-media.d.ts.map +1 -1
- package/dist/adapters/publishers/personal-media.js +3 -2
- package/dist/adapters/publishers/personal-media.js.map +1 -1
- package/dist/adapters/publishers/pragprog.d.ts +3 -0
- package/dist/adapters/publishers/pragprog.d.ts.map +1 -0
- package/dist/adapters/publishers/pragprog.js +120 -0
- package/dist/adapters/publishers/pragprog.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +10 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/techbookfest.d.ts.map +1 -1
- package/dist/adapters/publishers/techbookfest.js +2 -1
- package/dist/adapters/publishers/techbookfest.js.map +1 -1
- package/dist/application/concurrency.d.ts +16 -0
- package/dist/application/concurrency.d.ts.map +1 -0
- package/dist/application/concurrency.js +42 -0
- package/dist/application/concurrency.js.map +1 -0
- package/dist/application/get-book-by-isbn.d.ts +0 -8
- package/dist/application/get-book-by-isbn.d.ts.map +1 -1
- package/dist/application/get-book-by-isbn.js +64 -7
- package/dist/application/get-book-by-isbn.js.map +1 -1
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +3 -0
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts +16 -5
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +46 -9
- package/dist/application/search-books.js.map +1 -1
- package/dist/config/credentials.d.ts +8 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +32 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/domain/authors.d.ts +7 -0
- package/dist/domain/authors.d.ts.map +1 -0
- package/dist/domain/authors.js +22 -0
- package/dist/domain/authors.js.map +1 -0
- package/dist/domain/book.d.ts +2 -0
- package/dist/domain/book.d.ts.map +1 -1
- package/dist/domain/isbn.d.ts +8 -0
- package/dist/domain/isbn.d.ts.map +1 -0
- package/dist/domain/isbn.js +16 -0
- package/dist/domain/isbn.js.map +1 -0
- package/dist/domain/publisher.d.ts +16 -0
- package/dist/domain/publisher.d.ts.map +1 -1
- package/dist/domain/text-match.d.ts +32 -0
- package/dist/domain/text-match.d.ts.map +1 -0
- package/dist/domain/text-match.js +84 -0
- package/dist/domain/text-match.js.map +1 -0
- package/dist/main.js +15 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +40 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +9 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +43 -0
- package/dist/setup.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/docs/design-doc.md +127 -7
- package/package.json +14 -15
- package/.claude/settings.local.json +0 -38
- package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
- package/.github/workflows/test.yml +0 -72
- package/.oxlintrc.json +0 -12
- package/AGENTS.md +0 -100
- package/deno.json +0 -3
- package/src/adapters/cache/memory-cache.ts +0 -31
- package/src/adapters/cache/null-cache.ts +0 -8
- package/src/adapters/calil.ts +0 -57
- package/src/adapters/html/cheerio-parser.ts +0 -50
- package/src/adapters/http/fetch-client.ts +0 -47
- package/src/adapters/http/mock-client.ts +0 -77
- package/src/adapters/openbd.ts +0 -142
- package/src/adapters/publishers/base.ts +0 -279
- package/src/adapters/publishers/book-tech.ts +0 -117
- package/src/adapters/publishers/born-digital.ts +0 -143
- package/src/adapters/publishers/coronasha.ts +0 -139
- package/src/adapters/publishers/gihyo.ts +0 -120
- package/src/adapters/publishers/impress.ts +0 -103
- package/src/adapters/publishers/lambdanote.ts +0 -146
- package/src/adapters/publishers/manatee.ts +0 -113
- package/src/adapters/publishers/maruzen-publishing.ts +0 -129
- package/src/adapters/publishers/optronics.ts +0 -113
- package/src/adapters/publishers/oreilly-japan.ts +0 -133
- package/src/adapters/publishers/peaks.ts +0 -98
- package/src/adapters/publishers/personal-media.ts +0 -168
- package/src/adapters/publishers/registry.ts +0 -38
- package/src/adapters/publishers/rutles.ts +0 -149
- package/src/adapters/publishers/saiensu.ts +0 -136
- package/src/adapters/publishers/seshop.ts +0 -121
- package/src/adapters/publishers/tatsu-zine.ts +0 -142
- package/src/adapters/publishers/techbookfest.ts +0 -179
- package/src/application/get-book-by-isbn.ts +0 -50
- package/src/application/get-book-detail.ts +0 -40
- package/src/application/search-books.ts +0 -64
- package/src/domain/book.ts +0 -35
- package/src/domain/publisher.ts +0 -18
- package/src/main.ts +0 -14
- package/src/mcp/server.ts +0 -113
- package/src/mcp/tools.ts +0 -71
- package/src/ports/cache.ts +0 -5
- package/src/ports/html-parser.ts +0 -15
- package/src/ports/http.ts +0 -17
- package/tests/fixtures/book-tech-detail.html +0 -51
- package/tests/fixtures/book-tech-search.html +0 -91
- package/tests/fixtures/born-digital-detail.html +0 -62
- package/tests/fixtures/born-digital-search.html +0 -51
- package/tests/fixtures/calil-book.html +0 -987
- package/tests/fixtures/coronasha-detail.html +0 -41
- package/tests/fixtures/coronasha-search.html +0 -61
- package/tests/fixtures/gihyo-detail.html +0 -42
- package/tests/fixtures/gihyo-search.json +0 -54
- package/tests/fixtures/impress-detail-epub.html +0 -746
- package/tests/fixtures/impress-detail-social.html +0 -689
- package/tests/fixtures/lambdanote-search.html +0 -66
- package/tests/fixtures/manatee-detail.html +0 -53
- package/tests/fixtures/manatee-search.html +0 -59
- package/tests/fixtures/maruzen-detail.html +0 -51
- package/tests/fixtures/maruzen-search.html +0 -60
- package/tests/fixtures/openbd-response.json +0 -110
- package/tests/fixtures/optronics-detail.html +0 -30
- package/tests/fixtures/optronics-search.html +0 -75
- package/tests/fixtures/oreilly-detail.html +0 -52
- package/tests/fixtures/oreilly-ebook-list.html +0 -53
- package/tests/fixtures/peaks-detail.html +0 -39
- package/tests/fixtures/peaks-top.html +0 -50
- package/tests/fixtures/personal-media-detail.html +0 -32
- package/tests/fixtures/personal-media-search.html +0 -39
- package/tests/fixtures/rutles-detail.html +0 -32
- package/tests/fixtures/rutles-search.html +0 -62
- package/tests/fixtures/saiensu-detail.html +0 -41
- package/tests/fixtures/saiensu-search.html +0 -65
- package/tests/fixtures/seshop-detail.html +0 -45
- package/tests/fixtures/seshop-search.html +0 -58
- package/tests/fixtures/tatsu-zine-detail-free.html +0 -24
- package/tests/fixtures/tatsu-zine-search.html +0 -40
- package/tests/fixtures/techbookfest-search.json +0 -73
- package/tests/unit/adapters/base.test.ts +0 -441
- package/tests/unit/adapters/calil.test.ts +0 -69
- package/tests/unit/adapters/openbd.test.ts +0 -185
- package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
- package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
- package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
- package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
- package/tests/unit/adapters/publishers/impress.test.ts +0 -129
- package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
- package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
- package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
- package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
- package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
- package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
- package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
- package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
- package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
- package/tests/unit/adapters/registry.test.ts +0 -37
- package/tests/unit/application/get-book-by-isbn.test.ts +0 -176
- package/tests/unit/application/get-book-detail.test.ts +0 -102
- package/tests/unit/application/search-books.test.ts +0 -137
- package/tsconfig.json +0 -17
package/docs/design-doc.md
CHANGED
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
## 概要
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
日本語技術書(および一部の海外技術書)の書誌情報を出版社公式サイト・APIから取得するMCPサーバー。
|
|
9
9
|
書名・著者名での検索と、URLからの詳細情報取得を提供する。
|
|
10
|
+
価格は原則 税込円(整数)だが、海外出版社は当該通貨の数値と `BookRecord.currency`(ISO 4217)で表す。
|
|
11
|
+
言語は `BookRecord.language`(ISO 639-1、省略時 `"ja"`)で表す。
|
|
12
|
+
|
|
13
|
+
書籍管理アプリ [Riida](https://github.com/zonuexe/riida) の `riida-mcp` とシームレスに連携することを目標とし、
|
|
14
|
+
利用エージェント側の試行錯誤を減らしてスムーズに書誌を取得できることを重視する。
|
|
15
|
+
`publishedAt`(YYYY-MM-DD)は riida の `release_date` に対応する。
|
|
10
16
|
|
|
11
17
|
## アーキテクチャ
|
|
12
18
|
|
|
@@ -49,20 +55,21 @@ techbook-mcp/
|
|
|
49
55
|
├── flake.nix # Nix flake (devShell + package build)
|
|
50
56
|
├── package.json
|
|
51
57
|
├── tsconfig.json
|
|
52
|
-
├── vitest.config.ts
|
|
53
58
|
├── docs/
|
|
54
59
|
│ └── design-doc.md # 本ドキュメント
|
|
55
60
|
├── src/
|
|
56
61
|
│ ├── domain/
|
|
57
62
|
│ │ ├── book.ts # BookRecord, SearchQuery, DrmType 型定義
|
|
58
|
-
│ │
|
|
63
|
+
│ │ ├── publisher.ts # PublisherAdapter インターフェース (language, scale)
|
|
64
|
+
│ │ ├── text-match.ts # 照合用テキスト正規化・matchScore 算出
|
|
65
|
+
│ │ └── isbn.ts # ISBN 正規化・looksLikeIsbn 判定
|
|
59
66
|
│ ├── ports/
|
|
60
67
|
│ │ ├── http.ts # HttpClient インターフェース
|
|
61
68
|
│ │ ├── html-parser.ts # HtmlParser インターフェース
|
|
62
69
|
│ │ └── cache.ts # CacheStore インターフェース
|
|
63
70
|
│ ├── adapters/
|
|
64
71
|
│ │ ├── http/
|
|
65
|
-
│ │ │ ├── fetch-client.ts # fetch() ベース実装
|
|
72
|
+
│ │ │ ├── fetch-client.ts # fetch() ベース実装 (charset を見て EUC-JP/Shift_JIS をデコード)
|
|
66
73
|
│ │ │ └── mock-client.ts # テスト用モック
|
|
67
74
|
│ │ ├── html/
|
|
68
75
|
│ │ │ └── cheerio-parser.ts
|
|
@@ -74,14 +81,17 @@ techbook-mcp/
|
|
|
74
81
|
│ │ ├── book-tech.ts # BOOK TECH
|
|
75
82
|
│ │ ├── born-digital.ts # ボーンデジタル
|
|
76
83
|
│ │ ├── coronasha.ts # コロナ社
|
|
84
|
+
│ │ ├── cq-publishing.ts # CQ出版社 (Tech Village 書庫&販売)
|
|
77
85
|
│ │ ├── gihyo.ts # 技術評論社
|
|
78
86
|
│ │ ├── lambdanote.ts # ラムダノート
|
|
87
|
+
│ │ ├── leanpub.ts # Leanpub (海外・セルフ出版・DRM-free)
|
|
79
88
|
│ │ ├── manatee.ts # マナティ (マイナビ出版直販)
|
|
80
89
|
│ │ ├── maruzen-publishing.ts # 丸善出版
|
|
81
90
|
│ │ ├── optronics.ts # オプトロニクス社
|
|
82
91
|
│ │ ├── oreilly-japan.ts # オライリー・ジャパン
|
|
83
92
|
│ │ ├── peaks.ts # PEAKS
|
|
84
93
|
│ │ ├── personal-media.ts # パーソナルメディア
|
|
94
|
+
│ │ ├── pragprog.ts # Pragmatic Bookshelf (海外・DRM-free)
|
|
85
95
|
│ │ ├── rutles.ts # ラトルズ
|
|
86
96
|
│ │ ├── saiensu.ts # サイエンス社
|
|
87
97
|
│ │ ├── seshop.ts # SEshop (翔泳社)
|
|
@@ -89,8 +99,10 @@ techbook-mcp/
|
|
|
89
99
|
│ │ ├── techbookfest.ts # 技術書典
|
|
90
100
|
│ │ └── registry.ts # 出版社リスト (DEFAULT_PUBLISHERS)
|
|
91
101
|
│ ├── application/
|
|
92
|
-
│ │ ├── search-books.ts
|
|
93
|
-
│ │
|
|
102
|
+
│ │ ├── search-books.ts # 横断検索・スケジューリング・matchScore 付与
|
|
103
|
+
│ │ ├── get-book-detail.ts
|
|
104
|
+
│ │ ├── get-book-by-isbn.ts
|
|
105
|
+
│ │ └── concurrency.ts # mapWithConcurrency / withTimeout
|
|
94
106
|
│ ├── mcp/
|
|
95
107
|
│ │ ├── server.ts
|
|
96
108
|
│ │ └── tools.ts
|
|
@@ -105,11 +117,14 @@ techbook-mcp/
|
|
|
105
117
|
|
|
106
118
|
## 対応出版社
|
|
107
119
|
|
|
120
|
+
### 国内出版社
|
|
121
|
+
|
|
108
122
|
| ID | 名称 | 取得方式 | 備考 |
|
|
109
123
|
|----|------|---------|------|
|
|
110
124
|
| `book-tech` | BOOK TECH | HTML scraping | カラーミーショップ |
|
|
111
125
|
| `born-digital` | ボーンデジタル | HTML scraping | カラーミーショップ・EUC-JP エンコード必須 |
|
|
112
126
|
| `coronasha` | コロナ社 | HTML scraping | 電子版フラグで絞り込み・外部ストアへ委託販売 |
|
|
127
|
+
| `cq-publishing` | CQ出版社 | HTML scraping | 電子書籍直販サイト「Tech Village」・検索キーワードはパスに埋め込む |
|
|
113
128
|
| `gihyo` | 技術評論社 | JSON API | `/api_gh/site/search` |
|
|
114
129
|
| `lambdanote` | ラムダノート | HTML scraping | Shopify ストア |
|
|
115
130
|
| `manatee` | マナティ (マイナビ出版直販) | HTML scraping | 複数出版社を委託販売 |
|
|
@@ -124,6 +139,15 @@ techbook-mcp/
|
|
|
124
139
|
| `tatsu-zine` | 達人出版会 | HTML scraping | 複数出版社を委託販売 |
|
|
125
140
|
| `techbookfest` | 技術書典オンラインマーケット | GraphQL POST API | XSRF-TOKEN 必須 |
|
|
126
141
|
|
|
142
|
+
### 海外出版社
|
|
143
|
+
|
|
144
|
+
価格は USD(`BookRecord.currency: "USD"`)、`language: "en"`。
|
|
145
|
+
|
|
146
|
+
| ID | 名称 | 取得方式 | 備考 |
|
|
147
|
+
|----|------|---------|------|
|
|
148
|
+
| `pragprog` | Pragmatic Bookshelf | JSON index | 米国・`/search/index.json` をローカルフィルタ・DRM-free |
|
|
149
|
+
| `leanpub` | Leanpub | HTML scraping | 米国・セルフ出版・DRM-free・価格/日付は埋め込みJSONストリームから取得 |
|
|
150
|
+
|
|
127
151
|
### 各アダプターの実装メモ
|
|
128
152
|
|
|
129
153
|
**BOOK TECH (book-tech)** — カラーミーショップ
|
|
@@ -158,6 +182,17 @@ GET https://www.coronasha.co.jp/np/result.html?q={keyword}
|
|
|
158
182
|
- 電子書籍ストアは `extractEbookStoresFromDoc()` で自動検出(Kindle, Kinoppy, VarsityWave eBooks 等)
|
|
159
183
|
- Knowledge Worker (`kw.maruzen.co.jp`) はパターン未登録のため自動除外
|
|
160
184
|
|
|
185
|
+
**CQ出版社 (cq-publishing)** — Tech Village 書庫&販売
|
|
186
|
+
```
|
|
187
|
+
GET https://cc.cqpub.co.jp/lib/system/doclib_search/q={UTF-8 percent-encoded keyword}/
|
|
188
|
+
```
|
|
189
|
+
- CQ出版の電子書籍直販サイト(`cc.cqpub.co.jp/lib/`)。物販サイト `shop.cqpub.co.jp` とは別ドメインで、後者は紙の書籍・雑誌のみ・ネイティブ検索なし
|
|
190
|
+
- 検索キーワードは CakePHP の名前付きパラメータとして**パスに埋め込む**(`?q=` ではない)。複数語はスペース区切り(OR検索)
|
|
191
|
+
- 検索結果: `ul.itemList.books04 li` の `.mainTitle a`(タイトル・リンク)・`.subTitle`・`.price span`・`dt img`
|
|
192
|
+
- 詳細ページ: `/lib/system/doclib_item/{id}/`。`table[summary='商品詳細']` の th/td から著者・発行元・価格(ライセンス料金)・発行日を取得。**ISBN は持たず**コンテンツコード(例 `DP45551`)のみ
|
|
193
|
+
- タイトル末尾の形式マーカー `【PDF版】`(重複表記あり)・`【EPUB版】` 等を除去
|
|
194
|
+
- DRM: `"social"`(2017年導入の電子透かしで購入者情報を埋め込み、標準PDFビューアで閲覧可)
|
|
195
|
+
|
|
161
196
|
**技術評論社 (gihyo)** — JSON API
|
|
162
197
|
```
|
|
163
198
|
GET https://gihyo.jp/api_gh/site/search?search={keyword}&limit={n}
|
|
@@ -247,6 +282,29 @@ GET https://tatsu-zine.com/books/?search={keyword}
|
|
|
247
282
|
```
|
|
248
283
|
複数出版社の電子書籍を委託販売。全書籍ソーシャルDRM。
|
|
249
284
|
|
|
285
|
+
**Pragmatic Bookshelf (pragprog)** — 海外(米国)・JSONインデックス
|
|
286
|
+
```
|
|
287
|
+
GET https://pragprog.com/search/index.json # 全書籍インデックス(lunr.js 用)
|
|
288
|
+
```
|
|
289
|
+
- 唯一の海外(英語)出版社。サイト内検索は lunr.js のクライアントサイド検索なので、インデックス JSON を取得して**ローカルフィルタ**する
|
|
290
|
+
- インデックスの各レコード: `record_type`(`"book"`/`"errata"`)・`href`・`title`・`subtitle`・`author`・`keywords[]`・`code`・`image`。`"book"` のみ対象
|
|
291
|
+
- 検索: タイトル語は title+subtitle+keywords に全トークン一致、著者は `author` 部分一致。インデックスに価格・ISBN・発行日はない
|
|
292
|
+
- 詳細: `/titles/{code}/{slug}/`。`<meta property="book:isbn|book:author|og:*">` と `.book-about-text`("Published: July 2026" → `2026-07-01`)・`.buybox`("$39.95 (USD)")から取得
|
|
293
|
+
- 著者: "A with B, C, and D" を `with`/`and`/カンマで分割(オックスフォードカンマの "and" 残りも除去)
|
|
294
|
+
- 価格は **USD** なので `price` に数値・`currency: "USD"` を付与
|
|
295
|
+
- DRM: `"free"`(PDF/epub/mobi 全フォーマット提供・技術的DRMなし)
|
|
296
|
+
|
|
297
|
+
**Leanpub (leanpub)** — 海外(米国)・セルフ出版プラットフォーム
|
|
298
|
+
```
|
|
299
|
+
GET https://leanpub.com/store?search={keyword} # サーバーレンダリングのストア検索
|
|
300
|
+
```
|
|
301
|
+
- 海外のセルフパブリッシング・プラットフォーム。React Router (Remix系) アプリだが、ストア検索結果は静的HTMLでレンダリングされる
|
|
302
|
+
- 検索結果: 書影付き `<li>`(書影 `cloudfront.net/{slug}/s_featured`)を走査。slug は書影URLから取得し、`a[href="/{slug}"]` のテキストをタイトル、`.text-neutral-500` を著者、`.italic` をサブタイトルとして取得
|
|
303
|
+
- 詳細 `/{slug}`: タイトル・著者・説明・書影は `<meta property="og:*">`・`<meta name="author">` から取得
|
|
304
|
+
- **価格・更新日は埋め込み React Router ストリーム(`<script>`内)から正規表現で取得**: `minimumPaidPrice\",{数値}`(最低価格)・`lastPublishedAt\",\"{YYYY-MM-DD}`。静的HTMLの表示テキスト("Last updated on ..." 等)は CDN/SSR 状態で揺れて不安定なため使わない
|
|
305
|
+
- 価格は pay-what-you-want の**最低価格**・**USD**(`currency: "USD"`)。ISBN は持たない
|
|
306
|
+
- DRM: `"free"`(PDF/EPUB、技術的DRMなし)。`publisher` はセルフ出版のため `"Leanpub"`
|
|
307
|
+
|
|
250
308
|
**技術書典 (techbookfest)** — GraphQL
|
|
251
309
|
```
|
|
252
310
|
POST https://techbookfest.org/api/graphql
|
|
@@ -278,10 +336,12 @@ type DrmType = "free" | "social" | "password_pdf" | "drm";
|
|
|
278
336
|
| ラトルズ | `free` | 購入・確認済み |
|
|
279
337
|
| PEAKS | `free` | 利用規約に明記 |
|
|
280
338
|
| オプトロニクス社 | `free` | 購入・確認済み |
|
|
339
|
+
| Pragmatic Bookshelf | `free` | PDF/epub/mobi 全フォーマット提供・DRMなし |
|
|
281
340
|
| Gihyo Digital Publishing | `social` | 公式方針 |
|
|
282
341
|
| SEshop (翔泳社) | `social` | メールアドレス埋め込み透かし |
|
|
283
342
|
| BOOK TECH | `social` | 購入者情報透かし |
|
|
284
343
|
| ボーンデジタル | `social` | PDFにメールアドレス印字 |
|
|
344
|
+
| CQ出版 Tech Village | `social` | 2017年導入の電子透かしで購入者情報を埋め込み |
|
|
285
345
|
| マナティ | `social` | 公式 about ページに明記 |
|
|
286
346
|
| ラムダノート | `social` | 公式方針 |
|
|
287
347
|
| 達人出版会 | `social` | 公式方針 |
|
|
@@ -303,13 +363,73 @@ type DrmType = "free" | "social" | "password_pdf" | "drm";
|
|
|
303
363
|
|---------|------|---------|
|
|
304
364
|
| `search_books` | 書名・著者名で検索 | `title?`, `author?`, `publisher?`, `limit?` |
|
|
305
365
|
| `get_book_detail` | URLから詳細情報取得 | `url` |
|
|
366
|
+
| `get_book_by_isbn` | ISBNから書誌情報取得(openBD→出版社サイト→カーリル) | `isbn` |
|
|
306
367
|
| `list_publishers` | 対応出版社一覧 | なし |
|
|
307
368
|
|
|
369
|
+
## 検索の挙動
|
|
370
|
+
|
|
371
|
+
利用エージェント側の試行錯誤を減らすため、`search_books` は以下の前処理・後処理を行う。
|
|
372
|
+
|
|
373
|
+
- **ベストマッチ順ソート**: 各候補にクエリとの一致度 `matchScore`(0〜1、1が完全一致)を付与し降順に並べる。
|
|
374
|
+
先頭ほど本命候補なので、PDF奥付から推定した曖昧な title/author でも候補選びに迷わない。
|
|
375
|
+
スコアは `src/domain/text-match.ts` の `normalizeForMatch()`(NFKC で全半角統一・装飾括弧/長音/約物/空白を除去)+
|
|
376
|
+
書名はクエリを空白でトークン分割して候補書名への包含割合で算出する(純日本語の部分語が助詞を跨いでも拾える)。
|
|
377
|
+
`matchScore` はクエリ相対値のためドメインの `BookRecord` には載せず、検索結果境界の `ScoredBook` 型に限定する
|
|
378
|
+
- **ゼロ関連度のフォールバックを除外**: クエリ語があるのに `matchScore` が 0 の候補(検索サイトが「該当なし」時に返す新着順の無関係本)は除外する。
|
|
379
|
+
「該当なし」を空配列で表し、「該当なし」と「誤ヒット」を呼び出し側が区別できるようにする(誤メタデータ混入を防ぐ)
|
|
380
|
+
- **著者の重複排除**: `src/domain/authors.ts` の `dedupeAuthors()` で著者配列の重複を除く(`normalizeForMatch` をキーに表記ゆれも同一視)。search/detail/isbn の全経路に適用
|
|
381
|
+
- **openBD による欠損補完**: ISBN を持つ結果は `enrichWithOpenBD()` で `authors`(空のとき)・`publishedAt`・`price`・`coverImageUrl`・`description` を補完する。出版社の検索APIが著者や紹介文を返さない場合の救済(アダプタ間の取得項目の不揃いを平準化)
|
|
382
|
+
- **大規模出版社を優先スケジュール**: `PublisherAdapter.scale === "minor"` の小規模・専門/ローカルフィルタ型サイトは大規模出版社の後に回す。
|
|
383
|
+
小規模サイトのカタログは変動が少ないため `CATALOG_CACHE_TTL_SECONDS`(24時間)で全キャッシュし、ライブ負荷を抑える
|
|
384
|
+
- **並列度制限・タイムアウト**: `src/application/concurrency.ts` の `mapWithConcurrency`(`SEARCH_CONCURRENCY = 6`)と
|
|
385
|
+
`withTimeout`(`SEARCH_TIMEOUT_MS = 12s`)で、遅い1社が全体をブロックしないようにする(部分結果を返す)
|
|
386
|
+
- **errors の集約**: 失敗理由を `type`(`robots` / `timeout` / `http` / `other`)に分類し、MCP 層で種別×出版社に集約して静音化する
|
|
387
|
+
- **ISBN ショートカット**: `title` が ISBN 形式(`src/domain/isbn.ts` の `looksLikeIsbn`)かつ `author` 未指定なら、
|
|
388
|
+
全社横断をやめて `get_book_by_isbn` 経路に振り分ける
|
|
389
|
+
|
|
390
|
+
## カバレッジの制約
|
|
391
|
+
|
|
392
|
+
各アダプターは出版社の**現行ストアの生カタログ**(検索結果ページ・電子書籍一覧・JSON索引)をスクレイプする。
|
|
393
|
+
このため、出版社がストアから**販売終了・取り下げした旧刊**は構造的に `search_books` でヒットしない。
|
|
394
|
+
共通パターンは2008〜2013年頃の短編・電子書籍専売タイトル。書籍自体は実在するが、現行の検索可能な索引のどこにも載っていない。
|
|
395
|
+
|
|
396
|
+
riida-mcp フィードバック「現象9」で報告された未ヒット例の調査結果(2026-06-02 確認):
|
|
397
|
+
|
|
398
|
+
- **オライリー・ジャパン(旧刊 Ebook版のみタイトル)**: `CSS3の値、単位、色`(9784873116266)・`セレクタ、詳細度、カスケード`(9784873116037)・
|
|
399
|
+
`D3をはじめよう`(9784873115979)・`Web Workers`(9784873115962)・`OAuth 2.0をはじめよう`(9784873115580)・
|
|
400
|
+
`PHP開発者のためのJavaScript`(9784873116433)等。
|
|
401
|
+
これらは `/books/{isbn}/` の詳細ページは生きている(HTTP 200)が、`oreilly-japan` がフィルタする `/ebook/` 一覧
|
|
402
|
+
(現行ストアで販売中の約580冊)にも `/catalog/`・カテゴリページ(`/books/{topic}/`)にも載っていない。
|
|
403
|
+
詳細ページの `buying-options` は空(=現行ストアで購入導線なし。og:description に「本書はEbook版のみの販売となります」と残るが実売は終了)。
|
|
404
|
+
`sitemap.xml` も無く(404)、旧刊を網羅する代替の生インデックスは存在しない → **一覧ソースの差し替えでは救済不能**。
|
|
405
|
+
さらにこれら電子書籍専売 ISBN は openBD にも未登録(紙流通前提の JPRO/openBD に載らない)なため、
|
|
406
|
+
`search_books` だけでなく `get_book_by_isbn` の通常経路(openBD → カーリル)でも失敗する。
|
|
407
|
+
- **翔泳社 / SEshop(旧刊)**: `レガシーソフトウェア改善ガイド`・`実用Common Lisp`・`初めての人のためのLISP[増補改訂版]`・
|
|
408
|
+
`エンジニアのための文章術 再入門講座 新版` 等。SEshop 検索(全カテゴリ)でそもそも 0 件=**ストアに商品ページ自体が残っていない**
|
|
409
|
+
(O'Reilly と違い詳細ページも消えている)。電子版が存在しないか取り下げ済みで、`seshop` アダプタの取りこぼしではない。
|
|
410
|
+
|
|
411
|
+
### 旧刊救済: ISBN からの詳細ページ直引き(`detailUrlForIsbn`)
|
|
412
|
+
|
|
413
|
+
部分的な救済余地があるのはオライリーのみ。詳細ページ `/books/{isbn}/` が生きているため、以下を実装済み:
|
|
414
|
+
|
|
415
|
+
- `PublisherAdapter.detailUrlForIsbn?(isbn)`(任意メソッド)— ISBN ベースの安定 URL を持つサイトが詳細ページ URL を構成する。
|
|
416
|
+
`oreilly-japan` が `/books/{isbn}/` を返す。
|
|
417
|
+
- `isbn-publisher-codes.ts` の `oreilly-japan` に旧記号 `87311` を追加(従来は新記号 `8144` のみ)。
|
|
418
|
+
- `get_book_by_isbn` は openBD ミス時、カーリルより先に `detailUrlForIsbn` 経路(robots.txt 確認 → `getDetail`)を試みる
|
|
419
|
+
(`fetchDetailByIsbnCode`)。これで `CSS3の値、単位、色`(9784873116266)等は**価格を除く書誌**(書名・著者・発行日・説明・書影)が回収できる。
|
|
420
|
+
価格は詳細ページの `buying-options` が空のため取得不可。
|
|
421
|
+
|
|
422
|
+
ただし `search_books`(横断検索)では依然ヒットしない(`/ebook/` 一覧に無いため)。回収には ISBN が分かっている必要がある。
|
|
423
|
+
SEshop 旧刊は詳細ページごと消えているため `detailUrlForIsbn` でも回収不能。
|
|
424
|
+
|
|
308
425
|
## 新しいアダプターの追加手順
|
|
309
426
|
|
|
310
427
|
1. `src/adapters/publishers/{id}.ts` を作成し `PublisherAdapter` を実装
|
|
311
428
|
- `search()`: 検索APIまたはHTMLスクレイピングで `BookRecord[]` を返す
|
|
312
429
|
- `getDetail()`: 詳細ページをスクレイピングして `BookRecord` を返す
|
|
430
|
+
- 海外出版社など日本語以外なら `language`(ISO 639-1)を宣言する(省略時はアプリ層で `"ja"` とみなす)
|
|
431
|
+
- ラインナップが小さい/検索APIがなく全カタログをローカルフィルタするサイトは `scale: "minor"` を宣言し、
|
|
432
|
+
カタログ取得を `fetchText(url, deps, undefined, CATALOG_CACHE_TTL_SECONDS)` で長期キャッシュする
|
|
313
433
|
2. `tests/fixtures/{id}-search.html` (または `.json`) を作成
|
|
314
434
|
3. `tests/fixtures/{id}-detail.html` を作成
|
|
315
435
|
4. `tests/unit/adapters/publishers/{id}.test.ts` を作成
|
|
@@ -317,7 +437,7 @@ type DrmType = "free" | "social" | "password_pdf" | "drm";
|
|
|
317
437
|
6. `src/adapters/publishers/registry.ts` に登録
|
|
318
438
|
|
|
319
439
|
よく使う共通ユーティリティ (`base.ts`):
|
|
320
|
-
- `fetchText(url, deps, extraHeaders?)` — キャッシュ付きHTTP GET
|
|
440
|
+
- `fetchText(url, deps, extraHeaders?, ttlSeconds?)` — キャッシュ付きHTTP GET(`ttlSeconds` 省略時 1時間、小規模カタログは `CATALOG_CACHE_TTL_SECONDS`)
|
|
321
441
|
- `parseJapanesePrice(text)` — "3,740円(税込)" → 3740
|
|
322
442
|
- `resolveUrl(base, path)` — 相対URLを絶対URLに解決
|
|
323
443
|
- `extractEbookStoresFromDoc(doc)` — ページ内リンクから電子書籍ストアを自動検出
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zonuexe/techbook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "MCP server for searching Japanese technical book bibliographic information",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -10,23 +10,22 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"techbook-mcp": "dist/main.js"
|
|
12
12
|
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
15
|
+
"cheerio": "^1.2.0",
|
|
16
|
+
"iconv-lite": "^0.7.2",
|
|
17
|
+
"zod": "^4.4.3"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.9.1",
|
|
21
|
+
"oxlint": "^1.68.0",
|
|
22
|
+
"tsx": "^4.22.4",
|
|
23
|
+
"typescript": "^6.0.3"
|
|
24
|
+
},
|
|
13
25
|
"scripts": {
|
|
14
26
|
"build": "tsc",
|
|
15
27
|
"lint": "oxlint --config .oxlintrc.json src/",
|
|
16
|
-
"prepublishOnly": "npm run build",
|
|
17
28
|
"test": "node --import tsx/esm --test 'tests/**/*.test.ts'",
|
|
18
29
|
"test:watch": "node --import tsx/esm --test --watch 'tests/**/*.test.ts'"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
22
|
-
"cheerio": "^1.0.0",
|
|
23
|
-
"iconv-lite": "^0.6.0",
|
|
24
|
-
"zod": "^3.24.0"
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@types/node": "^22.0.0",
|
|
28
|
-
"oxlint": "^1.59.0",
|
|
29
|
-
"tsx": "^4.0.0",
|
|
30
|
-
"typescript": "^5.8.0"
|
|
31
30
|
}
|
|
32
|
-
}
|
|
31
|
+
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm info:*)",
|
|
5
|
-
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('version:', d.get\\('version'\\)\\); print\\('exports:', list\\(d.get\\('exports', {}\\).keys\\(\\)\\)[:20]\\)\")",
|
|
6
|
-
"WebFetch(domain:gihyo.jp)",
|
|
7
|
-
"WebFetch(domain:www.lambdanote.com)",
|
|
8
|
-
"Bash(curl:*)",
|
|
9
|
-
"Bash(python3 -c ':*)",
|
|
10
|
-
"Bash(npx vitest:*)",
|
|
11
|
-
"Bash(git mv:*)",
|
|
12
|
-
"Bash(git add:*)",
|
|
13
|
-
"Bash(git commit -m ':*)",
|
|
14
|
-
"Bash(node:*)",
|
|
15
|
-
"Bash(npm install:*)",
|
|
16
|
-
"Bash(curl -s \"https://wgn-obs.shop-pro.jp/?mode=srh&keyword=HTML\" -A \"Mozilla/5.0\")",
|
|
17
|
-
"Read(//tmp/**)",
|
|
18
|
-
"Bash(npm test:*)",
|
|
19
|
-
"Bash(python3:*)",
|
|
20
|
-
"WebFetch(domain:www.personal-media.co.jp)",
|
|
21
|
-
"Bash(grep -rn \"export.*Element\" node_modules/cheerio/dist/esm/*.d.ts)",
|
|
22
|
-
"Bash(npx tsc:*)",
|
|
23
|
-
"Bash(npx oxlint:*)",
|
|
24
|
-
"Bash(npm run:*)",
|
|
25
|
-
"Bash(curl -s \"https://book.impress.co.jp/books/1124101031\")",
|
|
26
|
-
"Bash(curl -s \"https://book.impress.co.jp/books/1125101113\")",
|
|
27
|
-
"Bash(cp /tmp/impress-detail-social.html tests/fixtures/impress-detail-social.html)",
|
|
28
|
-
"Bash(cp /tmp/impress-detail-nodrm.html tests/fixtures/impress-detail-epub.html)",
|
|
29
|
-
"Bash(wc -c tests/fixtures/impress-detail-*.html)",
|
|
30
|
-
"Bash(git -C /Users/megurine/repo/js/techbook-mcp status)",
|
|
31
|
-
"Bash(git -C /Users/megurine/repo/js/techbook-mcp diff)",
|
|
32
|
-
"Bash(git:*)",
|
|
33
|
-
"Bash(npm pkg:*)",
|
|
34
|
-
"WebFetch(domain:api.openbd.jp)",
|
|
35
|
-
"WebFetch(domain:calil.jp)"
|
|
36
|
-
]
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: techbook-mcp-release-prep
|
|
3
|
-
description: Prepare a techbook-mcp release by bumping the version, updating the changelog, and running verification. Use when the user asks to prepare the next version, cut a release, or make sure versioned files are consistent before tagging.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# techbook-mcp Release Prep
|
|
7
|
-
|
|
8
|
-
Follow this workflow when preparing a new `techbook-mcp` release.
|
|
9
|
-
|
|
10
|
-
## Decide the Next Version
|
|
11
|
-
|
|
12
|
-
Choose the next semantic version before touching any files.
|
|
13
|
-
|
|
14
|
-
- `patch` (0.x.Y) — bug fixes, no new functionality
|
|
15
|
-
- `minor` (0.X.0) — new adapters, new features, backward-compatible changes
|
|
16
|
-
- `major` (X.0.0) — breaking changes to the MCP tool interface or `BookRecord` schema
|
|
17
|
-
|
|
18
|
-
If the user specifies a version, use it. Otherwise infer from the unreleased commits since the last tag.
|
|
19
|
-
|
|
20
|
-
## Update Versioned Files
|
|
21
|
-
|
|
22
|
-
Update these files together in one pass:
|
|
23
|
-
|
|
24
|
-
- `CHANGELOG.md`
|
|
25
|
-
- `package.json` — `"version"` field
|
|
26
|
-
- `package-lock.json` — `"version"` field in the root object (same value as `package.json`)
|
|
27
|
-
|
|
28
|
-
### CHANGELOG.md Rules
|
|
29
|
-
|
|
30
|
-
- If `CHANGELOG.md` does not exist yet, create it with the standard Keep a Changelog header.
|
|
31
|
-
- Add a new `## [x.y.z] - YYYY-MM-DD` section directly below `## [Unreleased]`.
|
|
32
|
-
- Use Keep a Changelog section headings as needed: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
|
|
33
|
-
- Group the same kinds of changes under the same heading.
|
|
34
|
-
- Write entries user-facing: what changed, not how. Skip internal refactors unless they affect users.
|
|
35
|
-
- List new publisher adapters under `Added`.
|
|
36
|
-
- Preserve the release date in every version heading.
|
|
37
|
-
- Keep the `[Unreleased]` section empty after moving its contents to the new release section.
|
|
38
|
-
- Update the `[Unreleased]` compare link and add the new release compare link at the bottom of the file.
|
|
39
|
-
- Keep version headings and bottom-of-file links consistent so releases and compare ranges remain linkable.
|
|
40
|
-
|
|
41
|
-
### CHANGELOG.md Template (first release)
|
|
42
|
-
|
|
43
|
-
```markdown
|
|
44
|
-
# Changelog
|
|
45
|
-
|
|
46
|
-
All notable changes to this project will be documented in this file.
|
|
47
|
-
|
|
48
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
49
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
50
|
-
|
|
51
|
-
## [Unreleased]
|
|
52
|
-
|
|
53
|
-
## [x.y.z] - YYYY-MM-DD
|
|
54
|
-
|
|
55
|
-
### Added
|
|
56
|
-
|
|
57
|
-
- ...
|
|
58
|
-
|
|
59
|
-
[Unreleased]: https://github.com/zonuexe/techbook-mcp/compare/vx.y.z...HEAD
|
|
60
|
-
[x.y.z]: https://github.com/zonuexe/techbook-mcp/releases/tag/vx.y.z
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Verify Before Committing
|
|
64
|
-
|
|
65
|
-
Run all checks in order:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npm test
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npm run lint
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm run build
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
All three must pass. Do not commit if any check fails.
|
|
80
|
-
|
|
81
|
-
## Commit the Release
|
|
82
|
-
|
|
83
|
-
Prefer a single release-prep commit containing:
|
|
84
|
-
|
|
85
|
-
- version bump in `package.json` and `package-lock.json`
|
|
86
|
-
- `CHANGELOG.md` update
|
|
87
|
-
|
|
88
|
-
Use this commit message format:
|
|
89
|
-
|
|
90
|
-
```text
|
|
91
|
-
Bump up version to x.y.z
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Do not include other unrelated changes in the release commit.
|
|
95
|
-
|
|
96
|
-
## Quick Checklist
|
|
97
|
-
|
|
98
|
-
- Working tree starts clean or you understand every pending change.
|
|
99
|
-
- `CHANGELOG.md` has a new `## [x.y.z] - YYYY-MM-DD` section with user-facing entries.
|
|
100
|
-
- `package.json` and `package-lock.json` both show the new version.
|
|
101
|
-
- Bottom-of-file links in `CHANGELOG.md` are consistent.
|
|
102
|
-
- `npm test` passed.
|
|
103
|
-
- `npm run lint` passed.
|
|
104
|
-
- `npm run build` passed.
|
|
105
|
-
- Commit message follows `chore: release x.y.z`.
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main, master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main, master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
node-version: [22.x]
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
-
uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: ${{ matrix.node-version }}
|
|
24
|
-
cache: npm
|
|
25
|
-
|
|
26
|
-
- name: Install dependencies
|
|
27
|
-
run: npm ci
|
|
28
|
-
|
|
29
|
-
- name: Lint
|
|
30
|
-
run: npm run lint
|
|
31
|
-
|
|
32
|
-
- name: Type check
|
|
33
|
-
run: npx tsc --noEmit
|
|
34
|
-
|
|
35
|
-
- name: Run tests
|
|
36
|
-
run: npm test
|
|
37
|
-
|
|
38
|
-
- name: Build
|
|
39
|
-
run: npm run build
|
|
40
|
-
|
|
41
|
-
bun:
|
|
42
|
-
runs-on: ubuntu-latest
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v4
|
|
45
|
-
|
|
46
|
-
- uses: oven-sh/setup-bun@v2
|
|
47
|
-
|
|
48
|
-
- name: Install dependencies
|
|
49
|
-
run: bun install
|
|
50
|
-
|
|
51
|
-
- name: Run tests
|
|
52
|
-
run: bun test
|
|
53
|
-
|
|
54
|
-
deno:
|
|
55
|
-
runs-on: ubuntu-latest
|
|
56
|
-
steps:
|
|
57
|
-
- uses: actions/checkout@v4
|
|
58
|
-
|
|
59
|
-
- uses: denoland/setup-deno@v2
|
|
60
|
-
with:
|
|
61
|
-
deno-version: v2.x
|
|
62
|
-
|
|
63
|
-
- name: Install Node deps (for node_modules)
|
|
64
|
-
uses: actions/setup-node@v4
|
|
65
|
-
with:
|
|
66
|
-
node-version: 22.x
|
|
67
|
-
cache: npm
|
|
68
|
-
|
|
69
|
-
- run: npm ci
|
|
70
|
-
|
|
71
|
-
- name: Run tests
|
|
72
|
-
run: deno test --allow-read --no-check --sloppy-imports tests/
|
package/.oxlintrc.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
|
3
|
-
"plugins": ["typescript", "unicorn"],
|
|
4
|
-
"rules": {
|
|
5
|
-
"no-unused-vars": "error",
|
|
6
|
-
"no-console": "warn",
|
|
7
|
-
"unicorn/no-array-for-each": "error",
|
|
8
|
-
"unicorn/prefer-string-slice": "error",
|
|
9
|
-
"typescript/consistent-type-imports": "error"
|
|
10
|
-
},
|
|
11
|
-
"ignorePatterns": ["dist/**", "node_modules/**"]
|
|
12
|
-
}
|
package/AGENTS.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# techbook-mcp — AI エージェント向けガイド
|
|
2
|
-
|
|
3
|
-
日本語技術書の書誌情報を出版社公式サイト・APIから取得するMCPサーバー。
|
|
4
|
-
詳細な設計は [docs/design-doc.md](docs/design-doc.md) を参照。
|
|
5
|
-
|
|
6
|
-
## コミットメッセージ規約
|
|
7
|
-
|
|
8
|
-
Conventional Commits は使わない。コミットメッセージは端的な日本語または英語の命令形で書く。
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
robots.txt チェックを追加し結果を6時間キャッシュする
|
|
12
|
-
Add robots.txt check with 6-hour cache
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 開発コマンド
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install
|
|
19
|
-
npm test # ユニットテスト実行 (node:test)
|
|
20
|
-
npm run build # TypeScript コンパイル → dist/
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## コーディング規約
|
|
24
|
-
|
|
25
|
-
- **新しいアダプターを追加するときは必ずテストも書く**(`tests/unit/adapters/publishers/{id}.test.ts`)
|
|
26
|
-
- テストは `MockHttpClient` + `NullCacheStore` + `CheerioHtmlParser` の組み合わせで書く
|
|
27
|
-
- フィクスチャHTMLは `tests/fixtures/` に配置し、実サイトの構造を忠実に再現する
|
|
28
|
-
- `fetchText()` はキャッシュ・ヘッダーを内包するため、アダプター内では直接 `deps.http.get()` を呼ばない
|
|
29
|
-
- Referer ヘッダーが必要なサイトは `fetchText(url, deps, { Referer: "..." })` の第3引数を使う
|
|
30
|
-
- 著者名から役割語(著・訳・編・監修・監訳など)を除去すること
|
|
31
|
-
- 価格は税込み整数(円)で `BookRecord.price` に格納する
|
|
32
|
-
- `publisher` フィールドには実際の出版社名を入れる(ストアプラットフォーム名ではない)
|
|
33
|
-
|
|
34
|
-
## 新しい出版社アダプターを追加するとき
|
|
35
|
-
|
|
36
|
-
`docs/design-doc.md` の「新しいアダプターの追加手順」を参照。要点は以下:
|
|
37
|
-
|
|
38
|
-
1. `src/adapters/publishers/{id}.ts` — `PublisherAdapter` インターフェースを実装
|
|
39
|
-
2. `tests/fixtures/{id}-search.html` + `{id}-detail.html` — 実サイトHTMLのスナップショット
|
|
40
|
-
3. `tests/unit/adapters/publishers/{id}.test.ts` — `MockHttpClient` でユニットテスト
|
|
41
|
-
4. `src/adapters/publishers/base.ts` — 必要に応じて `EBOOK_STORE_PATTERNS` に追加
|
|
42
|
-
5. `src/adapters/publishers/registry.ts` — `DEFAULT_PUBLISHERS` に登録
|
|
43
|
-
|
|
44
|
-
## DRM 分類の判断基準
|
|
45
|
-
|
|
46
|
-
新しいストアを `EBOOK_STORE_PATTERNS` に追加する際の判断順:
|
|
47
|
-
1. **free** — 公式が明言、または購入して透かし等がないことを確認済み
|
|
48
|
-
2. **social** — 購入者情報(メールアドレス等)が埋め込まれるが技術的制限なし
|
|
49
|
-
3. **password_pdf** — PDFにパスワードがかかる(標準ビューアで開ける)
|
|
50
|
-
4. **drm** — 専用ビューアーが必要、または上記いずれでもない場合
|
|
51
|
-
|
|
52
|
-
## アーキテクチャ上の制約
|
|
53
|
-
|
|
54
|
-
- ポート (`HttpClient`, `HtmlParser`, `CacheStore`) はインターフェースのみ。実装を直接 import しない
|
|
55
|
-
- `DrmType` に新しい値を追加するときは `src/domain/book.ts` と `src/mcp/server.ts` の両方を更新する
|
|
56
|
-
|
|
57
|
-
## テスト方針
|
|
58
|
-
|
|
59
|
-
テストフレームワークは `node:test` + `node:assert/strict` を使う(vitest は使わない)。
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { describe, it } from "node:test";
|
|
63
|
-
import assert from "node:assert/strict";
|
|
64
|
-
|
|
65
|
-
// 標準的なテストセットアップ
|
|
66
|
-
function makeDeps(http: MockHttpClient) {
|
|
67
|
-
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// GET のモック(URL前方一致)
|
|
71
|
-
const http = new MockHttpClient()
|
|
72
|
-
.addResponse("https://example.com/search", { status: 200, body: html });
|
|
73
|
-
|
|
74
|
-
// POST のモック (GraphQL等)
|
|
75
|
-
const http = new MockHttpClient()
|
|
76
|
-
.addPostResponse("https://api.example.com/graphql", { status: 200, body: json });
|
|
77
|
-
|
|
78
|
-
// vitest → node:assert の主な対応
|
|
79
|
-
// expect(x).toBe(y) → assert.strictEqual(x, y)
|
|
80
|
-
// expect(x).toEqual(y) → assert.deepStrictEqual(x, y)
|
|
81
|
-
// expect(x).toMatchObject(y) → assert.partialDeepStrictEqual(x, y)
|
|
82
|
-
// expect(x).toHaveLength(n) → assert.strictEqual(x.length, n)
|
|
83
|
-
// expect(x).toContain(s) → assert.ok(x.includes(s))
|
|
84
|
-
// expect(x).toMatch(/r/) → assert.match(x, /r/)
|
|
85
|
-
// await expect(p).rejects.toThrow("msg") → await assert.rejects(p, /msg/)
|
|
86
|
-
|
|
87
|
-
// モック関数: node:test の mock.fn() は Bun 非対応のため、テストファイル内に
|
|
88
|
-
// ローカルで mockFn() ヘルパーを定義して使う(Node.js・Bun・Deno 共通で動作)
|
|
89
|
-
// vi.fn().mockResolvedValue(v) → mockFn(async () => v)
|
|
90
|
-
// fn.toHaveBeenCalledOnce() → assert.strictEqual(fn.mock.callCount(), 1)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## よくある落とし穴
|
|
94
|
-
|
|
95
|
-
- **EUC-JP サイト**: `shop.rutles.net` はクエリを EUC-JP エンコードしないとヒットしない → `iconv-lite` を使用
|
|
96
|
-
- **XSRF-TOKEN**: `techbookfest.org` GraphQL はダブルサブミットCookieパターン必須
|
|
97
|
-
- **Referer 必須**: `maruzen-publishing.co.jp` の検索は Referer なしで 403
|
|
98
|
-
- **機関向けストア除外**: `kw.maruzen.co.jp`(Knowledge Worker)は個人向けではないため除外
|
|
99
|
-
- **ローカルフィルタ型**: `oreilly-japan` と `peaks` は検索APIがなくトップページ/一覧をローカルフィルタ
|
|
100
|
-
- **著者のみ検索不可**: ローカルフィルタ型アダプターは `!query.title` のとき `[]` を返す(HTTP呼ばない)
|
package/deno.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { CacheStore } from "../../ports/cache.js";
|
|
2
|
-
|
|
3
|
-
interface CacheEntry {
|
|
4
|
-
value: string;
|
|
5
|
-
expiresAt?: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class MemoryCacheStore implements CacheStore {
|
|
9
|
-
private readonly store = new Map<string, CacheEntry>();
|
|
10
|
-
|
|
11
|
-
async get(key: string): Promise<string | null> {
|
|
12
|
-
const entry = this.store.get(key);
|
|
13
|
-
if (!entry) return null;
|
|
14
|
-
if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
|
|
15
|
-
this.store.delete(key);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
return entry.value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async set(key: string, value: string, ttlSeconds?: number): Promise<void> {
|
|
22
|
-
this.store.set(key, {
|
|
23
|
-
value,
|
|
24
|
-
expiresAt: ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async delete(key: string): Promise<void> {
|
|
29
|
-
this.store.delete(key);
|
|
30
|
-
}
|
|
31
|
-
}
|