@zonuexe/techbook-mcp 0.1.0
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/.claude/settings.local.json +23 -0
- package/.github/workflows/test.yml +36 -0
- package/AGENTS.md +72 -0
- package/CLAUDE.md +2 -0
- package/LICENSE +661 -0
- package/README.md +154 -0
- package/dist/adapters/cache/memory-cache.d.ts +8 -0
- package/dist/adapters/cache/memory-cache.d.ts.map +1 -0
- package/dist/adapters/cache/memory-cache.js +23 -0
- package/dist/adapters/cache/memory-cache.js.map +1 -0
- package/dist/adapters/cache/null-cache.d.ts +8 -0
- package/dist/adapters/cache/null-cache.d.ts.map +1 -0
- package/dist/adapters/cache/null-cache.js +7 -0
- package/dist/adapters/cache/null-cache.js.map +1 -0
- package/dist/adapters/html/cheerio-parser.d.ts +5 -0
- package/dist/adapters/html/cheerio-parser.d.ts.map +1 -0
- package/dist/adapters/html/cheerio-parser.js +45 -0
- package/dist/adapters/html/cheerio-parser.js.map +1 -0
- package/dist/adapters/http/fetch-client.d.ts +6 -0
- package/dist/adapters/http/fetch-client.d.ts.map +1 -0
- package/dist/adapters/http/fetch-client.js +43 -0
- package/dist/adapters/http/fetch-client.js.map +1 -0
- package/dist/adapters/http/mock-client.d.ts +19 -0
- package/dist/adapters/http/mock-client.d.ts.map +1 -0
- package/dist/adapters/http/mock-client.js +59 -0
- package/dist/adapters/http/mock-client.js.map +1 -0
- package/dist/adapters/publishers/base.d.ts +24 -0
- package/dist/adapters/publishers/base.d.ts.map +1 -0
- package/dist/adapters/publishers/base.js +88 -0
- package/dist/adapters/publishers/base.js.map +1 -0
- package/dist/adapters/publishers/gihyo.d.ts +3 -0
- package/dist/adapters/publishers/gihyo.d.ts.map +1 -0
- package/dist/adapters/publishers/gihyo.js +75 -0
- package/dist/adapters/publishers/gihyo.js.map +1 -0
- package/dist/adapters/publishers/lambdanote.d.ts +3 -0
- package/dist/adapters/publishers/lambdanote.d.ts.map +1 -0
- package/dist/adapters/publishers/lambdanote.js +113 -0
- package/dist/adapters/publishers/lambdanote.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts +3 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -0
- package/dist/adapters/publishers/registry.js +11 -0
- package/dist/adapters/publishers/registry.js.map +1 -0
- package/dist/adapters/publishers/tatsu-zine.d.ts +3 -0
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -0
- package/dist/adapters/publishers/tatsu-zine.js +110 -0
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -0
- package/dist/adapters/publishers/techbookfest.d.ts +3 -0
- package/dist/adapters/publishers/techbookfest.d.ts.map +1 -0
- package/dist/adapters/publishers/techbookfest.js +134 -0
- package/dist/adapters/publishers/techbookfest.js.map +1 -0
- package/dist/application/get-book-detail.d.ts +4 -0
- package/dist/application/get-book-detail.d.ts.map +1 -0
- package/dist/application/get-book-detail.js +9 -0
- package/dist/application/get-book-detail.js.map +1 -0
- package/dist/application/search-books.d.ts +11 -0
- package/dist/application/search-books.d.ts.map +1 -0
- package/dist/application/search-books.js +23 -0
- package/dist/application/search-books.js.map +1 -0
- package/dist/domain/book.d.ts +32 -0
- package/dist/domain/book.d.ts.map +1 -0
- package/dist/domain/book.js +2 -0
- package/dist/domain/book.js.map +1 -0
- package/dist/domain/publisher.d.ts +17 -0
- package/dist/domain/publisher.d.ts.map +1 -0
- package/dist/domain/publisher.js +2 -0
- package/dist/domain/publisher.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +79 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +47 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +53 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ports/cache.d.ts +6 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +2 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/html-parser.d.ts +14 -0
- package/dist/ports/html-parser.d.ts.map +1 -0
- package/dist/ports/html-parser.js +2 -0
- package/dist/ports/html-parser.js.map +1 -0
- package/dist/ports/http.d.ts +16 -0
- package/dist/ports/http.d.ts.map +1 -0
- package/dist/ports/http.js +2 -0
- package/dist/ports/http.js.map +1 -0
- package/docs/design-doc.md +365 -0
- package/flake.nix +50 -0
- package/package.json +29 -0
- package/src/adapters/cache/memory-cache.ts +31 -0
- package/src/adapters/cache/null-cache.ts +8 -0
- package/src/adapters/html/cheerio-parser.ts +49 -0
- package/src/adapters/http/fetch-client.ts +47 -0
- package/src/adapters/http/mock-client.ts +77 -0
- package/src/adapters/publishers/base.ts +129 -0
- package/src/adapters/publishers/book-tech.ts +117 -0
- package/src/adapters/publishers/born-digital.ts +158 -0
- package/src/adapters/publishers/coronasha.ts +139 -0
- package/src/adapters/publishers/gihyo.ts +120 -0
- package/src/adapters/publishers/lambdanote.ts +146 -0
- package/src/adapters/publishers/manatee.ts +112 -0
- package/src/adapters/publishers/maruzen-publishing.ts +141 -0
- package/src/adapters/publishers/optronics.ts +113 -0
- package/src/adapters/publishers/oreilly-japan.ts +138 -0
- package/src/adapters/publishers/peaks.ts +98 -0
- package/src/adapters/publishers/personal-media.ts +168 -0
- package/src/adapters/publishers/registry.ts +36 -0
- package/src/adapters/publishers/rutles.ts +161 -0
- package/src/adapters/publishers/saiensu.ts +149 -0
- package/src/adapters/publishers/seshop.ts +121 -0
- package/src/adapters/publishers/tatsu-zine.ts +129 -0
- package/src/adapters/publishers/techbookfest.ts +179 -0
- package/src/application/get-book-detail.ts +17 -0
- package/src/application/search-books.ts +39 -0
- package/src/domain/book.ts +35 -0
- package/src/domain/publisher.ts +18 -0
- package/src/main.ts +13 -0
- package/src/mcp/server.ts +103 -0
- package/src/mcp/tools.ts +54 -0
- package/src/ports/cache.ts +5 -0
- package/src/ports/html-parser.ts +15 -0
- package/src/ports/http.ts +17 -0
- package/tests/fixtures/book-tech-detail.html +51 -0
- package/tests/fixtures/book-tech-search.html +91 -0
- package/tests/fixtures/born-digital-detail.html +62 -0
- package/tests/fixtures/born-digital-search.html +51 -0
- package/tests/fixtures/coronasha-detail.html +41 -0
- package/tests/fixtures/coronasha-search.html +61 -0
- package/tests/fixtures/gihyo-detail.html +42 -0
- package/tests/fixtures/gihyo-search.json +54 -0
- package/tests/fixtures/lambdanote-search.html +66 -0
- package/tests/fixtures/manatee-detail.html +53 -0
- package/tests/fixtures/manatee-search.html +59 -0
- package/tests/fixtures/maruzen-detail.html +51 -0
- package/tests/fixtures/maruzen-search.html +60 -0
- package/tests/fixtures/optronics-detail.html +30 -0
- package/tests/fixtures/optronics-search.html +75 -0
- package/tests/fixtures/oreilly-detail.html +52 -0
- package/tests/fixtures/oreilly-ebook-list.html +53 -0
- package/tests/fixtures/peaks-detail.html +39 -0
- package/tests/fixtures/peaks-top.html +50 -0
- package/tests/fixtures/personal-media-detail.html +32 -0
- package/tests/fixtures/personal-media-search.html +39 -0
- package/tests/fixtures/rutles-detail.html +32 -0
- package/tests/fixtures/rutles-search.html +62 -0
- package/tests/fixtures/saiensu-detail.html +41 -0
- package/tests/fixtures/saiensu-search.html +65 -0
- package/tests/fixtures/seshop-detail.html +45 -0
- package/tests/fixtures/seshop-search.html +58 -0
- package/tests/fixtures/tatsu-zine-detail-free.html +22 -0
- package/tests/fixtures/tatsu-zine-search.html +24 -0
- package/tests/fixtures/techbookfest-search.json +73 -0
- package/tests/unit/adapters/publishers/book-tech.test.ts +183 -0
- package/tests/unit/adapters/publishers/born-digital.test.ts +191 -0
- package/tests/unit/adapters/publishers/coronasha.test.ts +201 -0
- package/tests/unit/adapters/publishers/gihyo.test.ts +135 -0
- package/tests/unit/adapters/publishers/lambdanote.test.ts +84 -0
- package/tests/unit/adapters/publishers/manatee.test.ts +163 -0
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +177 -0
- package/tests/unit/adapters/publishers/optronics.test.ts +205 -0
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +191 -0
- package/tests/unit/adapters/publishers/peaks.test.ts +174 -0
- package/tests/unit/adapters/publishers/personal-media.test.ts +196 -0
- package/tests/unit/adapters/publishers/rutles.test.ts +170 -0
- package/tests/unit/adapters/publishers/saiensu.test.ts +167 -0
- package/tests/unit/adapters/publishers/seshop.test.ts +171 -0
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +130 -0
- package/tests/unit/adapters/publishers/techbookfest.test.ts +93 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAuB9E,wBAAgB,YAAY,CAC1B,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,MAAM,CA4DR;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { searchBooks } from "../application/search-books.js";
|
|
5
|
+
import { getBookDetail } from "../application/get-book-detail.js";
|
|
6
|
+
import { TOOLS } from "./tools.js";
|
|
7
|
+
// --- 出力フォーマット ---
|
|
8
|
+
const DRM_LABELS = {
|
|
9
|
+
free: "DRMフリー",
|
|
10
|
+
social: "DRMフリー (ソーシャル)",
|
|
11
|
+
drm: "DRM付き",
|
|
12
|
+
};
|
|
13
|
+
function formatEbookStore(store) {
|
|
14
|
+
return { ...store, drmLabel: DRM_LABELS[store.drm] };
|
|
15
|
+
}
|
|
16
|
+
function formatBook(book) {
|
|
17
|
+
if (!book.ebookStores)
|
|
18
|
+
return book;
|
|
19
|
+
return { ...book, ebookStores: book.ebookStores.map(formatEbookStore) };
|
|
20
|
+
}
|
|
21
|
+
export function createServer(publishers, deps) {
|
|
22
|
+
const server = new Server({
|
|
23
|
+
name: "@zonuexe/techbook-mcp",
|
|
24
|
+
version: "0.1.0",
|
|
25
|
+
}, {
|
|
26
|
+
capabilities: { tools: {} },
|
|
27
|
+
});
|
|
28
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
29
|
+
tools: TOOLS,
|
|
30
|
+
}));
|
|
31
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
32
|
+
const { name, arguments: args = {} } = request.params;
|
|
33
|
+
switch (name) {
|
|
34
|
+
case "search_books": {
|
|
35
|
+
const query = {
|
|
36
|
+
title: typeof args["title"] === "string" ? args["title"] : undefined,
|
|
37
|
+
author: typeof args["author"] === "string" ? args["author"] : undefined,
|
|
38
|
+
publisherId: typeof args["publisher"] === "string" ? args["publisher"] : undefined,
|
|
39
|
+
limit: typeof args["limit"] === "number" ? Math.min(args["limit"], 50) : 10,
|
|
40
|
+
};
|
|
41
|
+
const { books, errors } = await searchBooks(query, publishers, deps);
|
|
42
|
+
const output = { books: books.map(formatBook) };
|
|
43
|
+
if (errors.length > 0)
|
|
44
|
+
output["errors"] = errors;
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
case "get_book_detail": {
|
|
50
|
+
const url = args["url"];
|
|
51
|
+
if (typeof url !== "string")
|
|
52
|
+
throw new Error("url は必須です");
|
|
53
|
+
const book = await getBookDetail(url, publishers, deps);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify(formatBook(book), null, 2) }],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case "list_publishers": {
|
|
59
|
+
const list = publishers.map(p => ({
|
|
60
|
+
id: p.id,
|
|
61
|
+
name: p.name,
|
|
62
|
+
baseUrl: p.baseUrl,
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`未知のツール: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return server;
|
|
73
|
+
}
|
|
74
|
+
export async function startServer(publishers, deps) {
|
|
75
|
+
const server = createServer(publishers, deps);
|
|
76
|
+
const transport = new StdioServerTransport();
|
|
77
|
+
await server.connect(transport);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const TOOLS: readonly [{
|
|
2
|
+
readonly name: "search_books";
|
|
3
|
+
readonly description: string;
|
|
4
|
+
readonly inputSchema: {
|
|
5
|
+
readonly type: "object";
|
|
6
|
+
readonly properties: {
|
|
7
|
+
readonly title: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
readonly description: "書名(部分一致)";
|
|
10
|
+
};
|
|
11
|
+
readonly author: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
readonly description: "著者名(部分一致)";
|
|
14
|
+
};
|
|
15
|
+
readonly publisher: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly description: string;
|
|
18
|
+
};
|
|
19
|
+
readonly limit: {
|
|
20
|
+
readonly type: "number";
|
|
21
|
+
readonly description: "1出版社あたりの最大取得件数(デフォルト: 10、最大: 50)";
|
|
22
|
+
readonly default: 10;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "get_book_detail";
|
|
28
|
+
readonly description: "書籍の公式ページURLから詳細な書誌情報を取得します。";
|
|
29
|
+
readonly inputSchema: {
|
|
30
|
+
readonly type: "object";
|
|
31
|
+
readonly required: readonly ["url"];
|
|
32
|
+
readonly properties: {
|
|
33
|
+
readonly url: {
|
|
34
|
+
readonly type: "string";
|
|
35
|
+
readonly description: "書籍の公式ページURL(出版社サイトのURL)";
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}, {
|
|
40
|
+
readonly name: "list_publishers";
|
|
41
|
+
readonly description: "対応している出版社の一覧とIDを返します。";
|
|
42
|
+
readonly inputSchema: {
|
|
43
|
+
readonly type: "object";
|
|
44
|
+
readonly properties: {};
|
|
45
|
+
};
|
|
46
|
+
}];
|
|
47
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDR,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const TOOLS = [
|
|
2
|
+
{
|
|
3
|
+
name: "search_books",
|
|
4
|
+
description: "書名・著者名から日本語技術書を検索し、書誌情報の一覧を返します。" +
|
|
5
|
+
"複数の出版社を横断して検索します。",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
title: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "書名(部分一致)",
|
|
12
|
+
},
|
|
13
|
+
author: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "著者名(部分一致)",
|
|
16
|
+
},
|
|
17
|
+
publisher: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "出版社IDで検索対象を絞り込みます。指定しない場合は全出版社を検索します。" +
|
|
20
|
+
"利用可能なIDは list_publishers で確認できます。",
|
|
21
|
+
},
|
|
22
|
+
limit: {
|
|
23
|
+
type: "number",
|
|
24
|
+
description: "1出版社あたりの最大取得件数(デフォルト: 10、最大: 50)",
|
|
25
|
+
default: 10,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "get_book_detail",
|
|
32
|
+
description: "書籍の公式ページURLから詳細な書誌情報を取得します。",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
required: ["url"],
|
|
36
|
+
properties: {
|
|
37
|
+
url: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "書籍の公式ページURL(出版社サイトのURL)",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "list_publishers",
|
|
46
|
+
description: "対応している出版社の一覧とIDを返します。",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB;QACE,IAAI,EAAE,cAAc;QACpB,WAAW,EACT,kCAAkC;YAClC,mBAAmB;QACrB,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,KAAK,EAAE;oBACL,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,UAAU;iBACxB;gBACD,MAAM,EAAE;oBACN,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,WAAW;iBACzB;gBACD,SAAS,EAAE;oBACT,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,uCAAuC;wBACvC,mCAAmC;iBACtC;gBACD,KAAK,EAAE;oBACL,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,kCAAkC;oBAC/C,OAAO,EAAE,EAAE;iBACZ;aACF;SACF;KACF;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,WAAW,EAAE,6BAA6B;QAC1C,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,CAAC,KAAK,CAAC;YACjB,UAAU,EAAE;gBACV,GAAG,EAAE;oBACH,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,yBAAyB;iBACvC;aACF;SACF;KACF;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,WAAW,EAAE,uBAAuB;QACpC,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,EAAE;SACf;KACF;CACO,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/ports/cache.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/ports/cache.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface HtmlElement {
|
|
2
|
+
text(): string;
|
|
3
|
+
html(): string | null;
|
|
4
|
+
attr(name: string): string | undefined;
|
|
5
|
+
find(selector: string): HtmlElement[];
|
|
6
|
+
}
|
|
7
|
+
export interface HtmlDocument {
|
|
8
|
+
select(selector: string): HtmlElement[];
|
|
9
|
+
selectOne(selector: string): HtmlElement | null;
|
|
10
|
+
}
|
|
11
|
+
export interface HtmlParser {
|
|
12
|
+
parse(html: string): HtmlDocument;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=html-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-parser.d.ts","sourceRoot":"","sources":["../../src/ports/html-parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,IAAI,IAAI,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IACxC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-parser.js","sourceRoot":"","sources":["../../src/ports/html-parser.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RequestOptions {
|
|
2
|
+
headers?: Record<string, string>;
|
|
3
|
+
timeout?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface HttpResponse {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
text(): Promise<string>;
|
|
9
|
+
/** ヘッダー値を取得する。複数値は `, ` 結合。存在しない場合は null。 */
|
|
10
|
+
header(name: string): string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface HttpClient {
|
|
13
|
+
get(url: string, options?: RequestOptions): Promise<HttpResponse>;
|
|
14
|
+
post(url: string, body: string, options?: RequestOptions): Promise<HttpResponse>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/ports/http.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,6CAA6C;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAClE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAClF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/ports/http.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# techbook-mcp 設計書
|
|
2
|
+
|
|
3
|
+
パッケージ名: `@zonuexe/techbook-mcp`
|
|
4
|
+
ライセンス: AGPL-3.0-only
|
|
5
|
+
|
|
6
|
+
## 概要
|
|
7
|
+
|
|
8
|
+
日本語技術書の書誌情報を出版社公式サイト・APIから取得するMCPサーバー。
|
|
9
|
+
書名・著者名での検索と、URLからの詳細情報取得を提供する。
|
|
10
|
+
|
|
11
|
+
## アーキテクチャ
|
|
12
|
+
|
|
13
|
+
ポート&アダプター(Hexagonal Architecture)を採用し、HTTP・HTML解析・キャッシュの各I/Oを抽象化する。
|
|
14
|
+
これによりビジネスロジックをランタイムやネットワーク環境から分離し、ユニットテストを容易にする。
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────────┐
|
|
18
|
+
│ MCP Layer (stdio) │
|
|
19
|
+
│ search_books / get_book_detail / list_publishers │
|
|
20
|
+
└──────────────────────┬──────────────────────────────┘
|
|
21
|
+
│
|
|
22
|
+
┌──────────────────────▼──────────────────────────────┐
|
|
23
|
+
│ Application Layer │
|
|
24
|
+
│ searchBooks() getBookDetail() │
|
|
25
|
+
└──────────┬───────────────────────┬──────────────────┘
|
|
26
|
+
│ │
|
|
27
|
+
┌──────────▼──────────┐ ┌─────────▼──────────────────┐
|
|
28
|
+
│ Domain Layer │ │ Publisher Registry │
|
|
29
|
+
│ BookRecord │ │ PublisherAdapter[] │
|
|
30
|
+
│ SearchQuery │ │ │
|
|
31
|
+
└─────────────────────┘ └────────────┬───────────────┘
|
|
32
|
+
│ uses ports
|
|
33
|
+
┌─────────────────────────────────────▼───────────────┐
|
|
34
|
+
│ Ports (interfaces) │
|
|
35
|
+
│ HttpClient HtmlParser CacheStore │
|
|
36
|
+
└──────┬─────────────────┬────────────────┬───────────┘
|
|
37
|
+
│ │ │
|
|
38
|
+
┌──────▼──────┐ ┌───────▼──────┐ ┌─────▼───────────┐
|
|
39
|
+
│ Adapters │ │ Adapters │ │ Adapters │
|
|
40
|
+
│ FetchHttp │ │CheerioParser │ │ MemoryCache │
|
|
41
|
+
│ MockHttp │ │ │ │ NullCache │
|
|
42
|
+
└─────────────┘ └──────────────┘ └─────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## ディレクトリ構成
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
techbook-mcp/
|
|
49
|
+
├── flake.nix # Nix flake (devShell + package build)
|
|
50
|
+
├── package.json
|
|
51
|
+
├── tsconfig.json
|
|
52
|
+
├── vitest.config.ts
|
|
53
|
+
├── docs/
|
|
54
|
+
│ └── design-doc.md # 本ドキュメント
|
|
55
|
+
├── src/
|
|
56
|
+
│ ├── domain/
|
|
57
|
+
│ │ ├── book.ts # BookRecord, SearchQuery, DrmType 型定義
|
|
58
|
+
│ │ └── publisher.ts # PublisherAdapter インターフェース
|
|
59
|
+
│ ├── ports/
|
|
60
|
+
│ │ ├── http.ts # HttpClient インターフェース
|
|
61
|
+
│ │ ├── html-parser.ts # HtmlParser インターフェース
|
|
62
|
+
│ │ └── cache.ts # CacheStore インターフェース
|
|
63
|
+
│ ├── adapters/
|
|
64
|
+
│ │ ├── http/
|
|
65
|
+
│ │ │ ├── fetch-client.ts # fetch() ベース実装
|
|
66
|
+
│ │ │ └── mock-client.ts # テスト用モック
|
|
67
|
+
│ │ ├── html/
|
|
68
|
+
│ │ │ └── cheerio-parser.ts
|
|
69
|
+
│ │ ├── cache/
|
|
70
|
+
│ │ │ ├── memory-cache.ts
|
|
71
|
+
│ │ │ └── null-cache.ts
|
|
72
|
+
│ │ └── publishers/
|
|
73
|
+
│ │ ├── base.ts # 共通ユーティリティ (fetchText, parsePrice, EBOOK_STORE_PATTERNS)
|
|
74
|
+
│ │ ├── book-tech.ts # BOOK TECH
|
|
75
|
+
│ │ ├── born-digital.ts # ボーンデジタル
|
|
76
|
+
│ │ ├── coronasha.ts # コロナ社
|
|
77
|
+
│ │ ├── gihyo.ts # 技術評論社
|
|
78
|
+
│ │ ├── lambdanote.ts # ラムダノート
|
|
79
|
+
│ │ ├── manatee.ts # マナティ (マイナビ出版直販)
|
|
80
|
+
│ │ ├── maruzen-publishing.ts # 丸善出版
|
|
81
|
+
│ │ ├── optronics.ts # オプトロニクス社
|
|
82
|
+
│ │ ├── oreilly-japan.ts # オライリー・ジャパン
|
|
83
|
+
│ │ ├── peaks.ts # PEAKS
|
|
84
|
+
│ │ ├── personal-media.ts # パーソナルメディア
|
|
85
|
+
│ │ ├── rutles.ts # ラトルズ
|
|
86
|
+
│ │ ├── saiensu.ts # サイエンス社
|
|
87
|
+
│ │ ├── seshop.ts # SEshop (翔泳社)
|
|
88
|
+
│ │ ├── tatsu-zine.ts # 達人出版会
|
|
89
|
+
│ │ ├── techbookfest.ts # 技術書典
|
|
90
|
+
│ │ └── registry.ts # 出版社リスト (DEFAULT_PUBLISHERS)
|
|
91
|
+
│ ├── application/
|
|
92
|
+
│ │ ├── search-books.ts
|
|
93
|
+
│ │ └── get-book-detail.ts
|
|
94
|
+
│ ├── mcp/
|
|
95
|
+
│ │ ├── server.ts
|
|
96
|
+
│ │ └── tools.ts
|
|
97
|
+
│ └── main.ts
|
|
98
|
+
└── tests/
|
|
99
|
+
├── unit/
|
|
100
|
+
│ └── adapters/publishers/
|
|
101
|
+
│ └── *.test.ts # 各アダプターのユニットテスト
|
|
102
|
+
└── fixtures/
|
|
103
|
+
└── * # HTTPレスポンスのスナップショット
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 対応出版社
|
|
107
|
+
|
|
108
|
+
| ID | 名称 | 取得方式 | 備考 |
|
|
109
|
+
|----|------|---------|------|
|
|
110
|
+
| `book-tech` | BOOK TECH | HTML scraping | カラーミーショップ |
|
|
111
|
+
| `born-digital` | ボーンデジタル | HTML scraping | カラーミーショップ・EUC-JP エンコード必須 |
|
|
112
|
+
| `coronasha` | コロナ社 | HTML scraping | 電子版フラグで絞り込み・外部ストアへ委託販売 |
|
|
113
|
+
| `gihyo` | 技術評論社 | JSON API | `/api_gh/site/search` |
|
|
114
|
+
| `lambdanote` | ラムダノート | HTML scraping | Shopify ストア |
|
|
115
|
+
| `manatee` | マナティ (マイナビ出版直販) | HTML scraping | 複数出版社を委託販売 |
|
|
116
|
+
| `maruzen-publishing` | 丸善出版 | HTML scraping | Referer ヘッダー必須 |
|
|
117
|
+
| `optronics` | オプトロニクス社 | HTML scraping | EC-CUBE ベース |
|
|
118
|
+
| `oreilly-japan` | オライリー・ジャパン | HTML scraping | 検索APIなし・ローカルフィルタ |
|
|
119
|
+
| `peaks` | PEAKS | HTML scraping | 検索APIなし・ローカルフィルタ |
|
|
120
|
+
| `personal-media` | パーソナルメディア | HTML scraping | 検索APIなし・ローカルフィルタ |
|
|
121
|
+
| `rutles` | ラトルズ | HTML scraping | クエリを EUC-JP エンコード必須 |
|
|
122
|
+
| `saiensu` | サイエンス社 | HTML scraping | 電子書籍のみ (`mediaName === "電子"`) |
|
|
123
|
+
| `seshop` | SEshop (翔泳社) | HTML scraping | `category_id=327` で電子書籍に絞り込み |
|
|
124
|
+
| `tatsu-zine` | 達人出版会 | HTML scraping | 複数出版社を委託販売 |
|
|
125
|
+
| `techbookfest` | 技術書典オンラインマーケット | GraphQL POST API | XSRF-TOKEN 必須 |
|
|
126
|
+
|
|
127
|
+
### 各アダプターの実装メモ
|
|
128
|
+
|
|
129
|
+
**BOOK TECH (book-tech)** — カラーミーショップ
|
|
130
|
+
```
|
|
131
|
+
GET https://book-tech.com/books?q%5Btitle_or_overview_or_identification_number_1_or_product_code_cont%5D={keyword}
|
|
132
|
+
```
|
|
133
|
+
- `div.contents-index-item` が書籍アイテム
|
|
134
|
+
- 著者: `a[href*='author_relations']` テキストから役割語(`(著)` 等)を除去
|
|
135
|
+
- ISBN: `.contents-book-about-id` の13桁数字列
|
|
136
|
+
- 価格: `.price` テキストから税込価格を取得
|
|
137
|
+
- DRM: `"social"`
|
|
138
|
+
|
|
139
|
+
**ボーンデジタル (born-digital)** — カラーミーショップ
|
|
140
|
+
```
|
|
141
|
+
GET https://wgn-obs.shop-pro.jp/?mode=srh&keyword={EUC-JP encoded keyword}
|
|
142
|
+
```
|
|
143
|
+
- クエリを **EUC-JP** でパーセントエンコード(`iconv-lite` 使用)
|
|
144
|
+
- 電子書籍の絞り込み: タイトルが `【` で始まるもの(`【PDFダウンロード版】`・`【電子書籍版】`)
|
|
145
|
+
- アイテム: `li.c-product-list__item`
|
|
146
|
+
- 価格: `var Colorme = {...}` JSON の `product.sales_price_including_tax`
|
|
147
|
+
- 著者・発行日: 詳細ページの説明テキストをタブまたは全角コロン `:` で分割して解析
|
|
148
|
+
- DRM: `"social"`(PDFにメールアドレスが印字)
|
|
149
|
+
|
|
150
|
+
**コロナ社 (coronasha)**
|
|
151
|
+
```
|
|
152
|
+
GET https://www.coronasha.co.jp/np/result.html?q={keyword}
|
|
153
|
+
```
|
|
154
|
+
- 電子版フラグ: `ul.status-list li` に `"電子版あり"` があるものだけ対象
|
|
155
|
+
- タイトル: `.tunogaki` と `.book-title` を結合(例: `"1から始める"` + `"Juliaプログラミング大全"`)
|
|
156
|
+
- 書誌情報: `.book-info dl` の dt/dd から定価・ISBN・発行年月日を取得
|
|
157
|
+
- 価格は詳細ページのサイドバー `.price` から取得(`.book-info dl` には含まれない)
|
|
158
|
+
- 電子書籍ストアは `extractEbookStoresFromDoc()` で自動検出(Kindle, Kinoppy, VarsityWave eBooks 等)
|
|
159
|
+
- Knowledge Worker (`kw.maruzen.co.jp`) はパターン未登録のため自動除外
|
|
160
|
+
|
|
161
|
+
**技術評論社 (gihyo)** — JSON API
|
|
162
|
+
```
|
|
163
|
+
GET https://gihyo.jp/api_gh/site/search?search={keyword}&limit={n}
|
|
164
|
+
```
|
|
165
|
+
レスポンス: `list[isbn]` オブジェクト。`author` は `{ 役割: { 名前: "<ruby>markup</ruby>" } }` 形式なのでHTML除去が必要。
|
|
166
|
+
|
|
167
|
+
**ラムダノート (lambdanote)** — Shopify
|
|
168
|
+
```
|
|
169
|
+
GET https://www.lambdanote.com/search?q={keyword}&type=product
|
|
170
|
+
```
|
|
171
|
+
詳細ページの `<script type="application/json">` 埋め込みJSONからISBN・著者情報を取得。
|
|
172
|
+
|
|
173
|
+
**マナティ (manatee)** — マイナビ出版直販
|
|
174
|
+
```
|
|
175
|
+
GET https://book.mynavi.jp/manatee/list/?topics_keyword={keyword}
|
|
176
|
+
```
|
|
177
|
+
ソーシャルDRM(公式 about ページに明記)。
|
|
178
|
+
|
|
179
|
+
**丸善出版 (maruzen-publishing)**
|
|
180
|
+
```
|
|
181
|
+
GET https://www.maruzen-publishing.co.jp/search/?search_keyword={keyword}&format=1
|
|
182
|
+
```
|
|
183
|
+
- **Referer ヘッダー必須**(なければ 403)
|
|
184
|
+
- 価格・ISBNはJS動的ロードのため取得不可
|
|
185
|
+
- `kw.maruzen.co.jp`(Knowledge Worker / Maruzen eBook Library)は機関向けなので除外
|
|
186
|
+
|
|
187
|
+
**オプトロニクス社 (optronics)** — EC-CUBE ベース
|
|
188
|
+
```
|
|
189
|
+
GET https://optronics-ebook.com/products/list.php?name={keyword}&category_id=1
|
|
190
|
+
```
|
|
191
|
+
`listcomment` / `main_comment` の自由テキストから `著者:` / `発行:` 行を正規表現で解析。
|
|
192
|
+
|
|
193
|
+
**オライリー・ジャパン (oreilly-japan)** — ローカルフィルタ
|
|
194
|
+
```
|
|
195
|
+
GET https://www.oreilly.co.jp/ebook/
|
|
196
|
+
```
|
|
197
|
+
検索APIなし。全一覧ページをタイトルキーワードでローカルフィルタリング。著者のみ検索は非対応。
|
|
198
|
+
|
|
199
|
+
**PEAKS (peaks)** — ローカルフィルタ
|
|
200
|
+
```
|
|
201
|
+
GET https://peaks.cc/
|
|
202
|
+
```
|
|
203
|
+
検索APIなし。トップページに全書籍(27冊程度)が掲載されており、ローカルフィルタリング。
|
|
204
|
+
|
|
205
|
+
**ラトルズ (rutles)** — EUC-JP ショッピングカート
|
|
206
|
+
```
|
|
207
|
+
GET https://shop.rutles.net/?mode=srh&keyword={EUC-JP encoded keyword}
|
|
208
|
+
```
|
|
209
|
+
- クエリを **EUC-JP** でパーセントエンコード(UTF-8では検索ヒットなし)
|
|
210
|
+
- `iconv-lite` を使用してエンコード
|
|
211
|
+
- 電子書籍は `【電子版】` がタイトルに含まれる
|
|
212
|
+
- 詳細ページの `var Colorme = {...}` JSON から ISBN・価格を取得
|
|
213
|
+
|
|
214
|
+
**サイエンス社 (saiensu)**
|
|
215
|
+
```
|
|
216
|
+
GET https://www.saiensu.co.jp/search/?keyword={keyword}
|
|
217
|
+
```
|
|
218
|
+
電子書籍のみ: `article.bookListItem` の `.bookListItemData_mediaName` が `"電子"` のもの。
|
|
219
|
+
DRM: `"password_pdf"`(パスワード付きPDF)
|
|
220
|
+
|
|
221
|
+
**SEshop / 翔泳社 (seshop)**
|
|
222
|
+
```
|
|
223
|
+
GET https://www.seshop.com/search?keyword={keyword}&category_id=327&sort=newer
|
|
224
|
+
```
|
|
225
|
+
- `category_id=327` が電子書籍(PDF版)カテゴリ
|
|
226
|
+
- さらに `div.product-data[data-category]` が `"電子書籍"` 始まりのものだけ返す
|
|
227
|
+
- 詳細ページの `cxenseparse:sho-*` メタタグから ISBN・価格・発売日を取得
|
|
228
|
+
- PDFにメールアドレスと著作権情報が埋め込まれる → `"social"` DRM
|
|
229
|
+
|
|
230
|
+
**パーソナルメディア (personal-media)** — ローカルフィルタ
|
|
231
|
+
```
|
|
232
|
+
GET https://www.personal-media.co.jp/webshop/book/
|
|
233
|
+
```
|
|
234
|
+
- 検索APIなし。PDF直販書籍の全一覧テーブルをタイトルキーワードでローカルフィルタリング
|
|
235
|
+
- 著者のみ検索は非対応(`!query.title` のとき `[]` を返す)
|
|
236
|
+
- 詳細ページにセマンティックHTMLなし。`body` 全テキストを行分割して正規表現でメタデータを抽出
|
|
237
|
+
- 著者行: `^(.+?)\s+(?:著|監修|編|訳|...)$` パターンで役割語を検出・除去
|
|
238
|
+
- ISBN: `"ISBN"` を含む行から `\d[\d-]{12,}` で抽出
|
|
239
|
+
- 発行日: `"発売"` を含む行から `(\d{4})年(\d{1,2})月` を抽出 → `"YYYY-MM-01"` 形式
|
|
240
|
+
- 電子書籍ストアは相対URLのため `extractEbookStoresFromDoc()` は不使用。パス文字列で手動検出
|
|
241
|
+
- `/webshop/book/` を含むリンク → パーソナルメディア (PDF版, `"social"`)
|
|
242
|
+
- `/smoothreader/store/` を含むリンク → Smooth Reader (専用ビューアー, `"drm"`)
|
|
243
|
+
|
|
244
|
+
**達人出版会 (tatsu-zine)**
|
|
245
|
+
```
|
|
246
|
+
GET https://tatsu-zine.com/books/?search={keyword}
|
|
247
|
+
```
|
|
248
|
+
複数出版社の電子書籍を委託販売。全書籍ソーシャルDRM。
|
|
249
|
+
|
|
250
|
+
**技術書典 (techbookfest)** — GraphQL
|
|
251
|
+
```
|
|
252
|
+
POST https://techbookfest.org/api/graphql
|
|
253
|
+
```
|
|
254
|
+
- **XSRF-TOKEN 必須**: GETホームページ → Set-Cookie から取得 → Cookie と `X-XSRF-TOKEN` ヘッダー両方に付与
|
|
255
|
+
- インラインフラグメント必須: `node { ... on ProductInfoSearchResult { product { ... } } }`
|
|
256
|
+
|
|
257
|
+
## DrmType
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
type DrmType = "free" | "social" | "password_pdf" | "drm";
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
| 値 | 意味 |
|
|
264
|
+
|----|------|
|
|
265
|
+
| `"free"` | 技術的DRMなし (DRM-free PDF/EPUB) |
|
|
266
|
+
| `"social"` | ソーシャルDRM (購入者情報の透かし入りPDF、技術的制限なし) |
|
|
267
|
+
| `"password_pdf"` | パスワード付きPDF (標準PDFビューアで閲覧可、制限あり) |
|
|
268
|
+
| `"drm"` | 技術的DRM付き (専用ビューアー必須) |
|
|
269
|
+
|
|
270
|
+
## 電子書籍ストア分類 (EBOOK_STORE_PATTERNS)
|
|
271
|
+
|
|
272
|
+
`src/adapters/publishers/base.ts` の `EBOOK_STORE_PATTERNS` で URL パターンから DRM 種別を自動判定する。
|
|
273
|
+
|
|
274
|
+
| ストア | drm | 根拠 |
|
|
275
|
+
|--------|-----|------|
|
|
276
|
+
| 技術書典 | `free` | 公式方針 |
|
|
277
|
+
| オライリー・ジャパン | `free` | 公式方針 |
|
|
278
|
+
| ラトルズ | `free` | 購入・確認済み |
|
|
279
|
+
| PEAKS | `free` | 利用規約に明記 |
|
|
280
|
+
| オプトロニクス社 | `free` | 購入・確認済み |
|
|
281
|
+
| Gihyo Digital Publishing | `social` | 公式方針 |
|
|
282
|
+
| SEshop (翔泳社) | `social` | メールアドレス埋め込み透かし |
|
|
283
|
+
| BOOK TECH | `social` | 購入者情報透かし |
|
|
284
|
+
| ボーンデジタル | `social` | PDFにメールアドレス印字 |
|
|
285
|
+
| マナティ | `social` | 公式 about ページに明記 |
|
|
286
|
+
| ラムダノート | `social` | 公式方針 |
|
|
287
|
+
| 達人出版会 | `social` | 公式方針 |
|
|
288
|
+
| インプレスブックス | `social` | 公式方針 |
|
|
289
|
+
| サイエンス社 | `password_pdf` | パスワード付きPDF |
|
|
290
|
+
| Kindle | `drm` | — |
|
|
291
|
+
| Kinoppy | `drm` | `kinokuniya.co.jp/kinoppystore` および `kinokuniya.co.jp/f/dsg-08` 形式 |
|
|
292
|
+
| VarsityWave eBooks | `drm` | 大学生協電子書籍 (coop-ebook.jp) |
|
|
293
|
+
| 楽天Kobo | `drm` | — |
|
|
294
|
+
| BookLive | `drm` | — |
|
|
295
|
+
| honto | `drm` | — |
|
|
296
|
+
| BOOK☆WALKER | `drm` | — |
|
|
297
|
+
| eBookJapan | `drm` | — |
|
|
298
|
+
| LINEマンガ | `drm` | — |
|
|
299
|
+
|
|
300
|
+
## MCPツール
|
|
301
|
+
|
|
302
|
+
| ツール名 | 説明 | 主な引数 |
|
|
303
|
+
|---------|------|---------|
|
|
304
|
+
| `search_books` | 書名・著者名で検索 | `title?`, `author?`, `publisher?`, `limit?` |
|
|
305
|
+
| `get_book_detail` | URLから詳細情報取得 | `url` |
|
|
306
|
+
| `list_publishers` | 対応出版社一覧 | なし |
|
|
307
|
+
|
|
308
|
+
## 新しいアダプターの追加手順
|
|
309
|
+
|
|
310
|
+
1. `src/adapters/publishers/{id}.ts` を作成し `PublisherAdapter` を実装
|
|
311
|
+
- `search()`: 検索APIまたはHTMLスクレイピングで `BookRecord[]` を返す
|
|
312
|
+
- `getDetail()`: 詳細ページをスクレイピングして `BookRecord` を返す
|
|
313
|
+
2. `tests/fixtures/{id}-search.html` (または `.json`) を作成
|
|
314
|
+
3. `tests/fixtures/{id}-detail.html` を作成
|
|
315
|
+
4. `tests/unit/adapters/publishers/{id}.test.ts` を作成
|
|
316
|
+
5. 必要なら `base.ts` の `EBOOK_STORE_PATTERNS` にストアパターンを追加
|
|
317
|
+
6. `src/adapters/publishers/registry.ts` に登録
|
|
318
|
+
|
|
319
|
+
よく使う共通ユーティリティ (`base.ts`):
|
|
320
|
+
- `fetchText(url, deps, extraHeaders?)` — キャッシュ付きHTTP GET
|
|
321
|
+
- `parseJapanesePrice(text)` — "3,740円(税込)" → 3740
|
|
322
|
+
- `resolveUrl(base, path)` — 相対URLを絶対URLに解決
|
|
323
|
+
- `extractEbookStoresFromDoc(doc)` — ページ内リンクから電子書籍ストアを自動検出
|
|
324
|
+
|
|
325
|
+
## ランタイム対応
|
|
326
|
+
|
|
327
|
+
| ランタイム | 起動方法 |
|
|
328
|
+
|----------|---------|
|
|
329
|
+
| Node.js 22+ | `node dist/main.js` |
|
|
330
|
+
| Bun | `bun src/main.ts` |
|
|
331
|
+
| Deno | `deno run --allow-net src/main.ts` |
|
|
332
|
+
|
|
333
|
+
グローバル `fetch` APIを使用しており、Node.js 18以降・Bun・Denoで動作する。
|
|
334
|
+
|
|
335
|
+
## 開発環境 (Nix)
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
nix develop # devShell に入る (Node.js 22, Bun, Deno が利用可能)
|
|
339
|
+
npm install # 初回のみ: node_modules と package-lock.json を生成
|
|
340
|
+
npm test # テスト実行
|
|
341
|
+
npm run build # TypeScript コンパイル (→ dist/)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## テスト戦略
|
|
345
|
+
|
|
346
|
+
- `MockHttpClient` にフィクスチャデータを登録してネットワーク不要のユニットテストを実現
|
|
347
|
+
- `NullCacheStore` でキャッシュをバイパス
|
|
348
|
+
- `CheerioHtmlParser` を実際のパーサーとして使用(モック不要)
|
|
349
|
+
- `tests/fixtures/` に各サイトのレスポンススナップショットを配置
|
|
350
|
+
|
|
351
|
+
### MockHttpClient の使い方
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const http = new MockHttpClient()
|
|
355
|
+
.addResponse("https://example.com/search", { status: 200, body: searchHtml })
|
|
356
|
+
.addResponse("https://example.com/book/1", { status: 200, body: detailHtml });
|
|
357
|
+
// URLプレフィックスで前方一致マッチ
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### POST エンドポイントのテスト
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
const http = new MockHttpClient()
|
|
364
|
+
.addPostResponse("https://api.example.com/graphql", { status: 200, body: jsonStr });
|
|
365
|
+
```
|