@zonuexe/techbook-mcp 0.2.2 → 0.2.3
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 +3 -1
- package/CHANGELOG.md +14 -1
- package/dist/adapters/calil.d.ts +10 -0
- package/dist/adapters/calil.d.ts.map +1 -0
- package/dist/adapters/calil.js +45 -0
- package/dist/adapters/calil.js.map +1 -0
- package/dist/adapters/openbd.d.ts +57 -0
- package/dist/adapters/openbd.d.ts.map +1 -0
- package/dist/adapters/openbd.js +87 -0
- package/dist/adapters/openbd.js.map +1 -0
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.js +6 -18
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -1
- package/dist/application/get-book-by-isbn.d.ts +12 -0
- package/dist/application/get-book-by-isbn.d.ts.map +1 -0
- package/dist/application/get-book-by-isbn.js +42 -0
- package/dist/application/get-book-by-isbn.js.map +1 -0
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +16 -1
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +20 -0
- package/dist/application/search-books.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +16 -0
- package/dist/mcp/tools.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/calil.ts +57 -0
- package/src/adapters/openbd.ts +142 -0
- package/src/adapters/publishers/tatsu-zine.ts +7 -19
- package/src/application/get-book-by-isbn.ts +50 -0
- package/src/application/get-book-detail.ts +17 -1
- package/src/application/search-books.ts +20 -0
- package/src/mcp/server.ts +10 -0
- package/src/mcp/tools.ts +17 -0
- package/tests/fixtures/calil-book.html +987 -0
- package/tests/fixtures/openbd-response.json +110 -0
- package/tests/fixtures/tatsu-zine-detail-free.html +14 -12
- package/tests/unit/adapters/calil.test.ts +69 -0
- package/tests/unit/adapters/openbd.test.ts +185 -0
- package/tests/unit/application/get-book-by-isbn.test.ts +176 -0
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
const OPENBD_API_URL = "https://api.openbd.jp/v1/get";
|
|
6
|
+
|
|
7
|
+
// --- 型定義 ---
|
|
8
|
+
|
|
9
|
+
interface OpenBDSummary {
|
|
10
|
+
isbn: string;
|
|
11
|
+
title: string;
|
|
12
|
+
publisher: string;
|
|
13
|
+
pubdate: string; // "YYYYMMDD"
|
|
14
|
+
cover: string; // "https://cover.openbd.jp/{isbn}.jpg"
|
|
15
|
+
author: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface OpenBDTextContent {
|
|
19
|
+
TextType: string; // "02": 短い説明, "03": 説明文, "04": 目次
|
|
20
|
+
ContentAudience: string;
|
|
21
|
+
Text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface OpenBDPrice {
|
|
25
|
+
PriceType: string; // "03": 税込定価
|
|
26
|
+
PriceAmount: string;
|
|
27
|
+
CurrencyCode: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface OpenBDHanmoto {
|
|
31
|
+
isbn: string;
|
|
32
|
+
storelink?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpenBDEntry {
|
|
37
|
+
summary: OpenBDSummary;
|
|
38
|
+
hanmoto?: OpenBDHanmoto;
|
|
39
|
+
onix: {
|
|
40
|
+
CollateralDetail?: {
|
|
41
|
+
TextContent?: OpenBDTextContent[];
|
|
42
|
+
};
|
|
43
|
+
ProductSupply?: {
|
|
44
|
+
SupplyDetail?: {
|
|
45
|
+
Price?: OpenBDPrice[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- ユーティリティ ---
|
|
52
|
+
|
|
53
|
+
function parsePubDate(pubdate: string): string | undefined {
|
|
54
|
+
if (!pubdate || pubdate.length < 8) return undefined;
|
|
55
|
+
return `${pubdate.slice(0, 4)}-${pubdate.slice(4, 6)}-${pubdate.slice(6, 8)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findTextByType(entry: OpenBDEntry, ...types: string[]): string | undefined {
|
|
59
|
+
const texts = entry.onix.CollateralDetail?.TextContent;
|
|
60
|
+
if (!texts) return undefined;
|
|
61
|
+
for (const type of types) {
|
|
62
|
+
const found = texts.find(t => t.TextType === type);
|
|
63
|
+
if (found?.Text) return found.Text;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getTaxIncludedPrice(entry: OpenBDEntry): number | undefined {
|
|
69
|
+
const prices = entry.onix.ProductSupply?.SupplyDetail?.Price;
|
|
70
|
+
if (!prices) return undefined;
|
|
71
|
+
// PriceType "03" = 税込定価
|
|
72
|
+
const price = prices.find(p => p.PriceType === "03");
|
|
73
|
+
if (!price) return undefined;
|
|
74
|
+
const amount = parseInt(price.PriceAmount, 10);
|
|
75
|
+
return isNaN(amount) ? undefined : amount;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- 公開API ---
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* openBD API から複数ISBNの書誌情報を一括取得する。
|
|
82
|
+
* @returns ISBNをキーとするMapを返す。該当なし・取得失敗のISBNは含まれない。
|
|
83
|
+
*/
|
|
84
|
+
export async function fetchOpenBDBooks(
|
|
85
|
+
isbns: string[],
|
|
86
|
+
deps: PublisherDeps,
|
|
87
|
+
): Promise<Map<string, OpenBDEntry>> {
|
|
88
|
+
if (isbns.length === 0) return new Map();
|
|
89
|
+
|
|
90
|
+
const url = `${OPENBD_API_URL}?isbn=${isbns.join(",")}`;
|
|
91
|
+
const text = await fetchText(url, deps);
|
|
92
|
+
const data: (OpenBDEntry | null)[] = JSON.parse(text);
|
|
93
|
+
|
|
94
|
+
const result = new Map<string, OpenBDEntry>();
|
|
95
|
+
for (let i = 0; i < isbns.length; i++) {
|
|
96
|
+
const entry = data[i];
|
|
97
|
+
if (entry !== null && entry !== undefined) {
|
|
98
|
+
result.set(isbns[i], entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* openBD エントリを BookRecord に変換する。
|
|
106
|
+
* 出版社サイトから取得できない場合のフォールバック用。
|
|
107
|
+
* url には hanmoto.storelink を使用し、なければ openBD API URL を使用する。
|
|
108
|
+
*/
|
|
109
|
+
export function openBDEntryToBookRecord(entry: OpenBDEntry): BookRecord {
|
|
110
|
+
const { summary } = entry;
|
|
111
|
+
const storelink = entry.hanmoto?.storelink;
|
|
112
|
+
|
|
113
|
+
const authors = summary.author
|
|
114
|
+
? summary.author.split(/[\//、,,]/).map(a => a.trim()).filter(Boolean)
|
|
115
|
+
: [];
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title: summary.title,
|
|
119
|
+
authors,
|
|
120
|
+
publisher: summary.publisher,
|
|
121
|
+
isbn: summary.isbn,
|
|
122
|
+
publishedAt: parsePubDate(summary.pubdate),
|
|
123
|
+
url: storelink ?? `https://api.openbd.jp/v1/get?isbn=${summary.isbn}`,
|
|
124
|
+
price: getTaxIncludedPrice(entry),
|
|
125
|
+
coverImageUrl: summary.cover || undefined,
|
|
126
|
+
description: findTextByType(entry, "03", "02"),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* openBD の書誌情報で BookRecord の欠損フィールドを補完する。
|
|
132
|
+
* 既存のフィールドは上書きしない。
|
|
133
|
+
*/
|
|
134
|
+
export function enrichWithOpenBD(book: BookRecord, entry: OpenBDEntry): BookRecord {
|
|
135
|
+
return {
|
|
136
|
+
...book,
|
|
137
|
+
publishedAt: book.publishedAt ?? parsePubDate(entry.summary.pubdate),
|
|
138
|
+
price: book.price ?? getTaxIncludedPrice(entry),
|
|
139
|
+
coverImageUrl: book.coverImageUrl ?? (entry.summary.cover || undefined),
|
|
140
|
+
description: book.description ?? findTextByType(entry, "03", "02"),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -118,25 +118,13 @@ export const tatsuZineAdapter: PublisherAdapter = {
|
|
|
118
118
|
const imgSrc = imgEl?.attr("src");
|
|
119
119
|
const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
|
|
120
120
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
for (const el of candidates) {
|
|
131
|
-
const text = el.text().trim();
|
|
132
|
-
if (!authors.length && /[((][著訳監編]/.test(text)) {
|
|
133
|
-
authors = parseAuthors(text);
|
|
134
|
-
}
|
|
135
|
-
if (price === undefined && /^\d/.test(text) && /円/.test(text)) {
|
|
136
|
-
price = parsePrice(text);
|
|
137
|
-
}
|
|
138
|
-
if (authors.length && price !== undefined) break;
|
|
139
|
-
}
|
|
121
|
+
// 著者: <p itemprop="author"> を優先使用
|
|
122
|
+
const authorText = doc.selectOne("p[itemprop='author']")?.text().trim() ?? "";
|
|
123
|
+
const authors = authorText ? parseAuthors(authorText) : [];
|
|
124
|
+
|
|
125
|
+
// 価格: <span itemprop="price"> を優先使用
|
|
126
|
+
const priceText = doc.selectOne("span[itemprop='price']")?.text().trim() ?? "";
|
|
127
|
+
const price = priceText ? parsePrice(priceText) : undefined;
|
|
140
128
|
|
|
141
129
|
// 達人出版会は全書籍で購入者情報を各ページに印字 (ソーシャルDRM)
|
|
142
130
|
const ebookStores: EbookStore[] = [{ name: "達人出版会", url, drm: "social" }];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BookRecord } from "../domain/book.js";
|
|
2
|
+
import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
3
|
+
import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
4
|
+
import { fetchOpenBDBooks, openBDEntryToBookRecord } from "../adapters/openbd.js";
|
|
5
|
+
import { fetchCalilBook } from "../adapters/calil.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ISBNから書籍情報を取得する。
|
|
9
|
+
*
|
|
10
|
+
* 1. openBD で書誌情報と出版社ストアリンクを取得する
|
|
11
|
+
* 2. ストアリンクが既知アダプターと一致する場合は出版社サイトから詳細取得を試みる
|
|
12
|
+
* 3. 取得できない場合は openBD データをそのまま返す
|
|
13
|
+
* 4. openBD にも存在しない場合はカーリルから書誌情報を取得する(廃業出版社など)
|
|
14
|
+
*/
|
|
15
|
+
export async function getBookByIsbn(
|
|
16
|
+
isbn: string,
|
|
17
|
+
publishers: readonly PublisherAdapter[],
|
|
18
|
+
deps: PublisherDeps,
|
|
19
|
+
): Promise<BookRecord> {
|
|
20
|
+
const normalizedIsbn = isbn.replace(/-/g, "");
|
|
21
|
+
|
|
22
|
+
const openBDMap = await fetchOpenBDBooks([normalizedIsbn], deps);
|
|
23
|
+
const entry = openBDMap.get(normalizedIsbn);
|
|
24
|
+
|
|
25
|
+
if (!entry) {
|
|
26
|
+
// openBD にない場合はカーリルをフォールバックとして試みる(廃業出版社など)
|
|
27
|
+
const calilBook = await fetchCalilBook(normalizedIsbn, deps);
|
|
28
|
+
if (calilBook) return calilBook;
|
|
29
|
+
throw new Error(`書誌情報が見つかりません: ${isbn}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// hanmoto.storelink が既知アダプターの baseUrl と前方一致する場合は
|
|
33
|
+
// 出版社サイトから詳細取得を試みる
|
|
34
|
+
const storelink = entry.hanmoto?.storelink;
|
|
35
|
+
if (storelink) {
|
|
36
|
+
const publisher = publishers.find(p => storelink.startsWith(p.baseUrl));
|
|
37
|
+
if (publisher) {
|
|
38
|
+
const allowed = await checkRobotsTxt(storelink, deps);
|
|
39
|
+
if (allowed) {
|
|
40
|
+
try {
|
|
41
|
+
return await publisher.getDetail(storelink, deps);
|
|
42
|
+
} catch {
|
|
43
|
+
// 出版社サイトからの取得失敗は無視して openBD データで返す
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return openBDEntryToBookRecord(entry);
|
|
50
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BookRecord } from "../domain/book.js";
|
|
2
2
|
import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
3
3
|
import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
4
|
+
import { fetchOpenBDBooks, enrichWithOpenBD } from "../adapters/openbd.js";
|
|
4
5
|
|
|
5
6
|
export async function getBookDetail(
|
|
6
7
|
url: string,
|
|
@@ -20,5 +21,20 @@ export async function getBookDetail(
|
|
|
20
21
|
throw new Error(`robots.txt によりアクセスが禁止されています: ${url}`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const book = await publisher.getDetail(url, deps);
|
|
25
|
+
|
|
26
|
+
// ISBNが特定できる場合はopenBDで欠損フィールドを補完
|
|
27
|
+
if (book.isbn !== undefined) {
|
|
28
|
+
try {
|
|
29
|
+
const openBDMap = await fetchOpenBDBooks([book.isbn], deps);
|
|
30
|
+
const entry = openBDMap.get(book.isbn);
|
|
31
|
+
if (entry !== undefined) {
|
|
32
|
+
return enrichWithOpenBD(book, entry);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// openBD の取得失敗は無視して出版社から取得できた情報を返す
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return book;
|
|
24
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BookRecord, SearchQuery } from "../domain/book.js";
|
|
2
2
|
import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
3
3
|
import { checkRobotsTxt } from "../adapters/publishers/base.js";
|
|
4
|
+
import { fetchOpenBDBooks, enrichWithOpenBD } from "../adapters/openbd.js";
|
|
4
5
|
|
|
5
6
|
export interface SearchBooksResult {
|
|
6
7
|
books: BookRecord[];
|
|
@@ -40,5 +41,24 @@ export async function searchBooks(
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// ISBNが特定できる書籍をopenBDで一括補完
|
|
45
|
+
const isbns = books.map(b => b.isbn).filter((isbn): isbn is string => isbn !== undefined);
|
|
46
|
+
if (isbns.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
const openBDMap = await fetchOpenBDBooks(isbns, deps);
|
|
49
|
+
for (let i = 0; i < books.length; i++) {
|
|
50
|
+
const isbn = books[i].isbn;
|
|
51
|
+
if (isbn !== undefined) {
|
|
52
|
+
const entry = openBDMap.get(isbn);
|
|
53
|
+
if (entry !== undefined) {
|
|
54
|
+
books[i] = enrichWithOpenBD(books[i], entry);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// openBD の取得失敗は無視して出版社から取得できた情報を返す
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
return { books, errors };
|
|
44
64
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { PublisherAdapter, PublisherDeps } from "../domain/publisher.js";
|
|
|
8
8
|
import type { BookRecord, EbookStore, DrmType, SearchQuery } from "../domain/book.js";
|
|
9
9
|
import { searchBooks } from "../application/search-books.js";
|
|
10
10
|
import { getBookDetail } from "../application/get-book-detail.js";
|
|
11
|
+
import { getBookByIsbn } from "../application/get-book-by-isbn.js";
|
|
11
12
|
import { TOOLS } from "./tools.js";
|
|
12
13
|
|
|
13
14
|
// --- 出力フォーマット ---
|
|
@@ -85,6 +86,15 @@ export function createServer(
|
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
case "get_book_by_isbn": {
|
|
90
|
+
const isbn = args["isbn"];
|
|
91
|
+
if (typeof isbn !== "string") throw new Error("isbn は必須です");
|
|
92
|
+
const book = await getBookByIsbn(isbn, publishers, deps);
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: JSON.stringify(formatBook(book), null, 2) }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
default:
|
|
89
99
|
throw new Error(`未知のツール: ${name}`);
|
|
90
100
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -51,4 +51,21 @@ export const TOOLS = [
|
|
|
51
51
|
properties: {},
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "get_book_by_isbn",
|
|
56
|
+
description:
|
|
57
|
+
"ISBNから書誌情報を取得します。" +
|
|
58
|
+
"openBDで出版社を特定し、可能であれば出版社サイトから詳細情報を取得します。" +
|
|
59
|
+
"出版社サイトから取得できない場合はopenBDのデータを返します。",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
required: ["isbn"],
|
|
63
|
+
properties: {
|
|
64
|
+
isbn: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "ISBN-13(ハイフンあり・なし両対応、例: 978-4-908686-20-7)",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
54
71
|
] as const;
|