@zonuexe/techbook-mcp 0.2.3 → 0.2.4

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.
Files changed (135) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +39 -20
  3. package/dist/adapters/publishers/google-books.d.ts +4 -0
  4. package/dist/adapters/publishers/google-books.d.ts.map +1 -0
  5. package/dist/adapters/publishers/google-books.js +75 -0
  6. package/dist/adapters/publishers/google-books.js.map +1 -0
  7. package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
  8. package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
  9. package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
  10. package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
  11. package/dist/adapters/publishers/juse-p.d.ts +3 -0
  12. package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
  13. package/dist/adapters/publishers/juse-p.js +110 -0
  14. package/dist/adapters/publishers/juse-p.js.map +1 -0
  15. package/dist/adapters/publishers/registry.d.ts.map +1 -1
  16. package/dist/adapters/publishers/registry.js +4 -0
  17. package/dist/adapters/publishers/registry.js.map +1 -1
  18. package/dist/application/get-book-by-isbn.d.ts +3 -2
  19. package/dist/application/get-book-by-isbn.d.ts.map +1 -1
  20. package/dist/application/get-book-by-isbn.js +22 -3
  21. package/dist/application/get-book-by-isbn.js.map +1 -1
  22. package/dist/config/credentials.d.ts +8 -0
  23. package/dist/config/credentials.d.ts.map +1 -0
  24. package/dist/config/credentials.js +32 -0
  25. package/dist/config/credentials.js.map +1 -0
  26. package/dist/main.js +15 -1
  27. package/dist/main.js.map +1 -1
  28. package/dist/setup.d.ts +2 -0
  29. package/dist/setup.d.ts.map +1 -0
  30. package/dist/setup.js +43 -0
  31. package/dist/setup.js.map +1 -0
  32. package/flake.lock +61 -0
  33. package/package.json +1 -1
  34. package/.claude/settings.local.json +0 -38
  35. package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
  36. package/.github/workflows/test.yml +0 -72
  37. package/.oxlintrc.json +0 -12
  38. package/AGENTS.md +0 -100
  39. package/deno.json +0 -3
  40. package/src/adapters/cache/memory-cache.ts +0 -31
  41. package/src/adapters/cache/null-cache.ts +0 -8
  42. package/src/adapters/calil.ts +0 -57
  43. package/src/adapters/html/cheerio-parser.ts +0 -50
  44. package/src/adapters/http/fetch-client.ts +0 -47
  45. package/src/adapters/http/mock-client.ts +0 -77
  46. package/src/adapters/openbd.ts +0 -142
  47. package/src/adapters/publishers/base.ts +0 -279
  48. package/src/adapters/publishers/book-tech.ts +0 -117
  49. package/src/adapters/publishers/born-digital.ts +0 -143
  50. package/src/adapters/publishers/coronasha.ts +0 -139
  51. package/src/adapters/publishers/gihyo.ts +0 -120
  52. package/src/adapters/publishers/impress.ts +0 -103
  53. package/src/adapters/publishers/lambdanote.ts +0 -146
  54. package/src/adapters/publishers/manatee.ts +0 -113
  55. package/src/adapters/publishers/maruzen-publishing.ts +0 -129
  56. package/src/adapters/publishers/optronics.ts +0 -113
  57. package/src/adapters/publishers/oreilly-japan.ts +0 -133
  58. package/src/adapters/publishers/peaks.ts +0 -98
  59. package/src/adapters/publishers/personal-media.ts +0 -168
  60. package/src/adapters/publishers/registry.ts +0 -38
  61. package/src/adapters/publishers/rutles.ts +0 -149
  62. package/src/adapters/publishers/saiensu.ts +0 -136
  63. package/src/adapters/publishers/seshop.ts +0 -121
  64. package/src/adapters/publishers/tatsu-zine.ts +0 -142
  65. package/src/adapters/publishers/techbookfest.ts +0 -179
  66. package/src/application/get-book-by-isbn.ts +0 -50
  67. package/src/application/get-book-detail.ts +0 -40
  68. package/src/application/search-books.ts +0 -64
  69. package/src/domain/book.ts +0 -35
  70. package/src/domain/publisher.ts +0 -18
  71. package/src/main.ts +0 -14
  72. package/src/mcp/server.ts +0 -113
  73. package/src/mcp/tools.ts +0 -71
  74. package/src/ports/cache.ts +0 -5
  75. package/src/ports/html-parser.ts +0 -15
  76. package/src/ports/http.ts +0 -17
  77. package/tests/fixtures/book-tech-detail.html +0 -51
  78. package/tests/fixtures/book-tech-search.html +0 -91
  79. package/tests/fixtures/born-digital-detail.html +0 -62
  80. package/tests/fixtures/born-digital-search.html +0 -51
  81. package/tests/fixtures/calil-book.html +0 -987
  82. package/tests/fixtures/coronasha-detail.html +0 -41
  83. package/tests/fixtures/coronasha-search.html +0 -61
  84. package/tests/fixtures/gihyo-detail.html +0 -42
  85. package/tests/fixtures/gihyo-search.json +0 -54
  86. package/tests/fixtures/impress-detail-epub.html +0 -746
  87. package/tests/fixtures/impress-detail-social.html +0 -689
  88. package/tests/fixtures/lambdanote-search.html +0 -66
  89. package/tests/fixtures/manatee-detail.html +0 -53
  90. package/tests/fixtures/manatee-search.html +0 -59
  91. package/tests/fixtures/maruzen-detail.html +0 -51
  92. package/tests/fixtures/maruzen-search.html +0 -60
  93. package/tests/fixtures/openbd-response.json +0 -110
  94. package/tests/fixtures/optronics-detail.html +0 -30
  95. package/tests/fixtures/optronics-search.html +0 -75
  96. package/tests/fixtures/oreilly-detail.html +0 -52
  97. package/tests/fixtures/oreilly-ebook-list.html +0 -53
  98. package/tests/fixtures/peaks-detail.html +0 -39
  99. package/tests/fixtures/peaks-top.html +0 -50
  100. package/tests/fixtures/personal-media-detail.html +0 -32
  101. package/tests/fixtures/personal-media-search.html +0 -39
  102. package/tests/fixtures/rutles-detail.html +0 -32
  103. package/tests/fixtures/rutles-search.html +0 -62
  104. package/tests/fixtures/saiensu-detail.html +0 -41
  105. package/tests/fixtures/saiensu-search.html +0 -65
  106. package/tests/fixtures/seshop-detail.html +0 -45
  107. package/tests/fixtures/seshop-search.html +0 -58
  108. package/tests/fixtures/tatsu-zine-detail-free.html +0 -24
  109. package/tests/fixtures/tatsu-zine-search.html +0 -40
  110. package/tests/fixtures/techbookfest-search.json +0 -73
  111. package/tests/unit/adapters/base.test.ts +0 -441
  112. package/tests/unit/adapters/calil.test.ts +0 -69
  113. package/tests/unit/adapters/openbd.test.ts +0 -185
  114. package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
  115. package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
  116. package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
  117. package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
  118. package/tests/unit/adapters/publishers/impress.test.ts +0 -129
  119. package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
  120. package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
  121. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
  122. package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
  123. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
  124. package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
  125. package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
  126. package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
  127. package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
  128. package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
  129. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
  130. package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
  131. package/tests/unit/adapters/registry.test.ts +0 -37
  132. package/tests/unit/application/get-book-by-isbn.test.ts +0 -176
  133. package/tests/unit/application/get-book-detail.test.ts +0 -102
  134. package/tests/unit/application/search-books.test.ts +0 -137
  135. package/tsconfig.json +0 -17
package/dist/setup.js ADDED
@@ -0,0 +1,43 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { loadCredentials, saveCredentials, getCredentialsPath, } from "./config/credentials.js";
3
+ function maskKey(key) {
4
+ if (key.length <= 8)
5
+ return "****";
6
+ return `${key.slice(0, 4)}****${key.slice(-4)}`;
7
+ }
8
+ async function prompt(rl, question) {
9
+ // プロンプトを stderr に出してMCPのstdoutを汚染しない
10
+ process.stderr.write(question);
11
+ const answer = await rl.question("");
12
+ return answer.trim();
13
+ }
14
+ export async function runSetup() {
15
+ const rl = createInterface({ input: process.stdin, output: process.stdin });
16
+ try {
17
+ process.stderr.write("techbook-mcp セットアップ\n");
18
+ process.stderr.write("=".repeat(40) + "\n\n");
19
+ const current = await loadCredentials();
20
+ const updated = { ...current };
21
+ // Google Books API キー
22
+ const envKey = process.env["GOOGLE_BOOKS_API_KEY"];
23
+ const savedKey = current.googleBooksApiKey ?? "";
24
+ const effectiveKey = envKey ?? savedKey;
25
+ const hint = effectiveKey
26
+ ? ` [現在: ${maskKey(effectiveKey)}]`
27
+ : " [未設定]";
28
+ const input = await prompt(rl, `Google Books API キー${hint} (Enterでスキップ): `);
29
+ if (input) {
30
+ updated.googleBooksApiKey = input;
31
+ }
32
+ else if (!savedKey && envKey) {
33
+ // 環境変数のみで設定済みの場合は設定ファイルには書かない
34
+ process.stderr.write("\n環境変数 GOOGLE_BOOKS_API_KEY が設定されています。設定ファイルへの保存をスキップします。\n");
35
+ }
36
+ await saveCredentials(updated);
37
+ process.stderr.write(`\n設定を保存しました: ${getCredentialsPath()}\n`);
38
+ }
39
+ finally {
40
+ rl.close();
41
+ }
42
+ }
43
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,eAAe,EACf,eAAe,EACf,kBAAkB,GAEnB,MAAM,yBAAyB,CAAC;AAEjC,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACnC,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,EAAsC,EAAE,QAAgB;IAC5E,qCAAqC;IACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAE5E,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAC9C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;QAE9C,MAAM,OAAO,GAAG,MAAM,eAAe,EAAE,CAAC;QACxC,MAAM,OAAO,GAAgB,EAAE,GAAG,OAAO,EAAE,CAAC;QAE5C,sBAAsB;QACtB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,iBAAiB,IAAI,EAAE,CAAC;QACjD,MAAM,YAAY,GAAG,MAAM,IAAI,QAAQ,CAAC;QACxC,MAAM,IAAI,GAAG,YAAY;YACvB,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,CAAC,GAAG;YACnC,CAAC,CAAC,QAAQ,CAAC;QAEb,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE,sBAAsB,IAAI,iBAAiB,CAAC,CAAC;QAE5E,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,iBAAiB,GAAG,KAAK,CAAC;QACpC,CAAC;aAAM,IAAI,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC;YAC/B,8BAA8B;YAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,kBAAkB,EAAE,IAAI,CAAC,CAAC;IACjE,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
package/flake.lock ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "nodes": {
3
+ "flake-utils": {
4
+ "inputs": {
5
+ "systems": "systems"
6
+ },
7
+ "locked": {
8
+ "lastModified": 1731533236,
9
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10
+ "owner": "numtide",
11
+ "repo": "flake-utils",
12
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13
+ "type": "github"
14
+ },
15
+ "original": {
16
+ "owner": "numtide",
17
+ "repo": "flake-utils",
18
+ "type": "github"
19
+ }
20
+ },
21
+ "nixpkgs": {
22
+ "locked": {
23
+ "lastModified": 1776169885,
24
+ "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
25
+ "owner": "NixOS",
26
+ "repo": "nixpkgs",
27
+ "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "NixOS",
32
+ "ref": "nixos-unstable",
33
+ "repo": "nixpkgs",
34
+ "type": "github"
35
+ }
36
+ },
37
+ "root": {
38
+ "inputs": {
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs"
41
+ }
42
+ },
43
+ "systems": {
44
+ "locked": {
45
+ "lastModified": 1681028828,
46
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47
+ "owner": "nix-systems",
48
+ "repo": "default",
49
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50
+ "type": "github"
51
+ },
52
+ "original": {
53
+ "owner": "nix-systems",
54
+ "repo": "default",
55
+ "type": "github"
56
+ }
57
+ }
58
+ },
59
+ "root": "root",
60
+ "version": 7
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zonuexe/techbook-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "MCP server for searching Japanese technical book bibliographic information",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -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,3 +0,0 @@
1
- {
2
- "nodeModulesDir": "auto"
3
- }
@@ -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
- }
@@ -1,8 +0,0 @@
1
- import type { CacheStore } from "../../ports/cache.js";
2
-
3
- /** テスト・デバッグ用。キャッシュを一切行わない。 */
4
- export class NullCacheStore implements CacheStore {
5
- async get(_key: string): Promise<null> { return null; }
6
- async set(_key: string, _value: string, _ttlSeconds?: number): Promise<void> {}
7
- async delete(_key: string): Promise<void> {}
8
- }
@@ -1,57 +0,0 @@
1
- import type { PublisherDeps } from "../domain/publisher.js";
2
- import type { BookRecord } from "../domain/book.js";
3
- import { fetchText } from "./publishers/base.js";
4
-
5
- export const CALIL_BASE_URL = "https://calil.jp";
6
-
7
- /**
8
- * カーリルの書籍詳細ページ (/book/{isbn}) から書誌情報を取得する。
9
- * openBD に存在しない書籍(廃業出版社など)のフォールバックとして使用する。
10
- * @returns 書誌情報が見つかれば BookRecord、ページが存在しなければ null。
11
- */
12
- export async function fetchCalilBook(
13
- isbn: string,
14
- deps: PublisherDeps,
15
- ): Promise<BookRecord | null> {
16
- const url = `${CALIL_BASE_URL}/book/${isbn}`;
17
- let html: string;
18
- try {
19
- html = await fetchText(url, deps);
20
- } catch {
21
- return null;
22
- }
23
-
24
- const doc = deps.parser.parse(html);
25
-
26
- const title = doc.selectOne("h1.title[itemprop='name']")?.text().trim();
27
- if (!title) return null;
28
-
29
- // 著者: div.author 内の <a> テキストを収集する
30
- // <div class="author" itemprop="author">
31
- // <a href="/search?q=author:...">WebビジネスPHP研究部会</a><span>(著)</span>
32
- // </div>
33
- const authorLinks = doc.select("div[itemprop='author'] a");
34
- const authors = authorLinks.map(el => el.text().trim()).filter(Boolean);
35
-
36
- const publisher = doc.selectOne("span[itemprop='publisher']")?.text().trim() || undefined;
37
-
38
- // "(2002-02-01)" → "2002-02-01"
39
- const rawDate = doc.selectOne("span[itemprop='datePublished']")?.text().trim();
40
- const publishedAt = rawDate ? rawDate.replace(/^\(|\)$/g, "").trim() || undefined : undefined;
41
-
42
- // ISBN-13: <span itemprop="isbn">ISBN-13:</span> 9784901676038 の形式
43
- const isbn13Match = html.match(/ISBN-13:[^<]*<\/span>[^<\d]*(97[89]\d{10})/);
44
- const isbn13 = isbn13Match?.[1] ?? isbn;
45
-
46
- const coverImageUrl = doc.selectOne("img[itemprop='image']")?.attr("src") || undefined;
47
-
48
- return {
49
- title,
50
- authors,
51
- publisher: publisher ?? "",
52
- isbn: isbn13,
53
- publishedAt,
54
- url,
55
- coverImageUrl,
56
- };
57
- }
@@ -1,50 +0,0 @@
1
- import * as cheerio from "cheerio";
2
- import type { Element } from "domhandler";
3
- import type { HtmlParser, HtmlDocument, HtmlElement } from "../../ports/html-parser.js";
4
-
5
- class CheerioElement implements HtmlElement {
6
- constructor(
7
- private readonly $: cheerio.CheerioAPI,
8
- private readonly el: Element,
9
- ) {}
10
-
11
- text(): string {
12
- return this.$(this.el).text().trim();
13
- }
14
-
15
- html(): string | null {
16
- return this.$(this.el).html();
17
- }
18
-
19
- attr(name: string): string | undefined {
20
- return this.$(this.el).attr(name);
21
- }
22
-
23
- find(selector: string): HtmlElement[] {
24
- return this.$(this.el)
25
- .find(selector)
26
- .toArray()
27
- .map(el => new CheerioElement(this.$, el as Element));
28
- }
29
- }
30
-
31
- class CheerioDocument implements HtmlDocument {
32
- constructor(private readonly $: cheerio.CheerioAPI) {}
33
-
34
- select(selector: string): HtmlElement[] {
35
- return this.$(selector)
36
- .toArray()
37
- .map(el => new CheerioElement(this.$, el as Element));
38
- }
39
-
40
- selectOne(selector: string): HtmlElement | null {
41
- return this.select(selector)[0] ?? null;
42
- }
43
- }
44
-
45
- export class CheerioHtmlParser implements HtmlParser {
46
- parse(html: string): HtmlDocument {
47
- const $ = cheerio.load(html);
48
- return new CheerioDocument($);
49
- }
50
- }
@@ -1,47 +0,0 @@
1
- import type { HttpClient, RequestOptions, HttpResponse } from "../../ports/http.js";
2
-
3
- class FetchHttpResponse implements HttpResponse {
4
- constructor(private readonly response: Response) {}
5
-
6
- get status(): number {
7
- return this.response.status;
8
- }
9
-
10
- get url(): string {
11
- return this.response.url;
12
- }
13
-
14
- text(): Promise<string> {
15
- return this.response.text();
16
- }
17
-
18
- header(name: string): string | null {
19
- return this.response.headers.get(name);
20
- }
21
- }
22
-
23
- export class FetchHttpClient implements HttpClient {
24
- async get(url: string, options?: RequestOptions): Promise<HttpResponse> {
25
- const init: RequestInit = {
26
- headers: options?.headers,
27
- };
28
- if (options?.timeout !== undefined) {
29
- init.signal = AbortSignal.timeout(options.timeout);
30
- }
31
- const response = await fetch(url, init);
32
- return new FetchHttpResponse(response);
33
- }
34
-
35
- async post(url: string, body: string, options?: RequestOptions): Promise<HttpResponse> {
36
- const init: RequestInit = {
37
- method: "POST",
38
- body,
39
- headers: { "Content-Type": "application/json", ...options?.headers },
40
- };
41
- if (options?.timeout !== undefined) {
42
- init.signal = AbortSignal.timeout(options.timeout);
43
- }
44
- const response = await fetch(url, init);
45
- return new FetchHttpResponse(response);
46
- }
47
- }