From 771b1090531dfbf9de1c3b9c669ac13eb5948ac9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 18:58:28 +0000 Subject: [PATCH 1/6] Rename MCP tools to getBibleReference and fetchChapter Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/README.md | 4 ++-- packages/seed-bible-mcp/src/mcp.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/seed-bible-mcp/README.md b/packages/seed-bible-mcp/README.md index b300e22..593c041 100644 --- a/packages/seed-bible-mcp/README.md +++ b/packages/seed-bible-mcp/README.md @@ -2,8 +2,8 @@ An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bible API](https://bible.helloao.org) (BSB, WEB, Hebrew WLC, SBL Greek NT) through two tools: -- `search` — turns a reference such as `John 3:16` or `Gen 1:1-3 WEB` into a passage result id. -- `fetch` — returns the passage text for a result id from `search`. +- `getBibleReference` — turns a reference such as `John 3:16` or `Gen 1:1-3 WEB` into a passage result id. +- `fetchChapter` — returns the passage text for a result id from `getBibleReference`. The server speaks the Streamable HTTP transport at `/mcp` and runs statelessly, so the same code runs on Node.js and on Cloudflare Workers. diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index e6f87c1..1bac4ba 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -43,8 +43,8 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { }, { instructions: - 'Use search() to interpret a user query into a Bible passage result. ' + - 'Then use fetch() to retrieve the full passage text. ' + + 'Use getBibleReference() to interpret a user query into a Bible passage result. ' + + 'Then use fetchChapter() to retrieve the full passage text. ' + 'Supports BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), and SBL Greek NT (grc_sbl).', // The default Ajv validator compiles schemas with `new Function`, // which Cloudflare Workers forbids. The cfworker validator is pure JS @@ -54,7 +54,7 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { ); server.registerTool( - 'search', + 'getBibleReference', { description: 'Interpret a natural-language query into a single Bible passage result.', @@ -84,12 +84,14 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { ); server.registerTool( - 'fetch', + 'fetchChapter', { description: - 'Retrieve the full passage text for a result id from search().', + 'Retrieve the full passage text for a result id from getBibleReference().', inputSchema: { - id: z.string().describe('A result id returned by search().'), + id: z + .string() + .describe('A result id returned by getBibleReference().'), }, }, async ({ id }) => { From e6f7ce051d4e3f60b78fd0d3dffd06f15fd2a27b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:06:09 +0000 Subject: [PATCH 2/6] Use simplified chapter JSON endpoint in fetchChapter tool Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/src/bible.ts | 51 +++++++++++----------------- packages/seed-bible-mcp/src/mcp.ts | 4 +-- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/seed-bible-mcp/src/bible.ts b/packages/seed-bible-mcp/src/bible.ts index b0767e9..9f3fb1c 100644 --- a/packages/seed-bible-mcp/src/bible.ts +++ b/packages/seed-bible-mcp/src/bible.ts @@ -129,49 +129,37 @@ export async function parseQueryToRef( return [translation, bookId, chapter, verses]; } -export async function fetchChapterJson( +export function simpleChapterUrl( apiBase: string, translation: string, book: string, chapter: number -): Promise { - const res = await fetch(chapterUrl(apiBase, translation, book, chapter)); - if (!res.ok) throw new Error(`chapter request failed: ${res.status}`); - return res.json(); +): string { + return `${apiBase}/${translation}/${book}/${chapter}.simple.json`; } /** - * Verse content entries can be: - * - strings - * - formatted text objects { "text": "...", ... } - * - inline objects like { "lineBreak": true } or { "noteId": 0 } - * We keep readable text and turn line breaks into newlines; ignore footnote refs. + * Fetches a chapter in the simplified format, where each verse's content is + * already flattened into a single `text` string. */ -function flattenVerseContent(items: any[]): string { - const out: string[] = []; - for (const x of items) { - if (typeof x === 'string') { - out.push(x); - } else if (x && typeof x === 'object') { - if (typeof x.text === 'string') { - out.push(x.text); - } else if (x.lineBreak === true) { - out.push('\n'); - } - // ignore noteId, headings, etc for plain verse text - } - } - let s = out.join(''); - s = s.replace(/\s+\n/g, '\n'); - s = s.replace(/\n\s+/g, '\n'); - return s.trim(); +export async function fetchSimpleChapterJson( + apiBase: string, + translation: string, + book: string, + chapter: number +): Promise { + const res = await fetch( + simpleChapterUrl(apiBase, translation, book, chapter) + ); + if (!res.ok) throw new Error(`chapter request failed: ${res.status}`); + return res.json(); } export function extractVerses( - chapterJson: any, + simpleChapterJson: any, verseRange: string | null ): string { - const content: any[] = chapterJson?.chapter?.content ?? []; + const content: any[] = simpleChapterJson?.chapter?.content ?? []; let start: number | null = null; let end: number | null = null; if (verseRange) { @@ -192,8 +180,7 @@ export function extractVerses( if (typeof num !== 'number' || !Number.isInteger(num)) continue; if (start !== null && (num < start || num > (end as number))) continue; - const verseText = flattenVerseContent(item.content ?? []); - lines.push(`${num}. ${verseText}`); + lines.push(`${num}. ${(item.text ?? '').trim()}`); } return lines.join('\n').trim(); diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index 1bac4ba..8d07d5b 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -6,7 +6,7 @@ import { DEFAULT_BIBLE_API_BASE, chapterUrl, extractVerses, - fetchChapterJson, + fetchSimpleChapterJson, makeResultId, parseQueryToRef, parseResultId, @@ -96,7 +96,7 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { }, async ({ id }) => { const [translation, book, chapter, verses] = parseResultId(id); - const chapterJson = await fetchChapterJson( + const chapterJson = await fetchSimpleChapterJson( apiBase, translation, book, From c433f4d857d4dc9fa8e7f22433bd9126b88ce3bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:08:48 +0000 Subject: [PATCH 3/6] Add fetchVerse tool to MCP server Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/README.md | 3 +- packages/seed-bible-mcp/src/mcp.ts | 62 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/seed-bible-mcp/README.md b/packages/seed-bible-mcp/README.md index 593c041..2d59582 100644 --- a/packages/seed-bible-mcp/README.md +++ b/packages/seed-bible-mcp/README.md @@ -1,9 +1,10 @@ # Free Use Bible MCP Server -An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bible API](https://bible.helloao.org) (BSB, WEB, Hebrew WLC, SBL Greek NT) through two tools: +An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bible API](https://bible.helloao.org) (BSB, WEB, Hebrew WLC, SBL Greek NT) through three tools: - `getBibleReference` — turns a reference such as `John 3:16` or `Gen 1:1-3 WEB` into a passage result id. - `fetchChapter` — returns the passage text for a result id from `getBibleReference`. +- `fetchVerse` — returns the text of a single verse for a reference such as `John 3:16` or `Gen 1:1 WEB`. The server speaks the Streamable HTTP transport at `/mcp` and runs statelessly, so the same code runs on Node.js and on Cloudflare Workers. diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index 8d07d5b..ee0db6a 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -24,6 +24,13 @@ function json(value: unknown) { }; } +function error(message: string) { + return { + content: [{ type: 'text' as const, text: message }], + isError: true, + }; +} + /** * Create a new MCP server instance with the Bible tools registered. * @@ -45,6 +52,7 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { instructions: 'Use getBibleReference() to interpret a user query into a Bible passage result. ' + 'Then use fetchChapter() to retrieve the full passage text. ' + + 'To get the text of one verse directly, use fetchVerse(). ' + 'Supports BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), and SBL Greek NT (grc_sbl).', // The default Ajv validator compiles schemas with `new Function`, // which Cloudflare Workers forbids. The cfworker validator is pure JS @@ -113,5 +121,59 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { } ); + server.registerTool( + 'fetchVerse', + { + description: + 'Retrieve the text of a single Bible verse from a reference such as "John 3:16".', + inputSchema: { + reference: z + .string() + .describe( + 'A single verse reference, optionally followed by a translation, e.g. "John 3:16" or "Gen 1:1 WEB".' + ), + }, + }, + async ({ reference }) => { + const ref = await parseQueryToRef(apiBase, reference); + if (!ref || !ref[3] || ref[3].includes('-')) { + return error( + `Could not interpret "${reference}" as a single verse reference.` + ); + } + + const [translation, book, chapter, verse] = ref; + const chapterJson = await fetchSimpleChapterJson( + apiBase, + translation, + book, + chapter + ); + // Drop the "N. " prefix that extractVerses() adds to each verse. + const text = extractVerses(chapterJson, verse).replace( + /^\d+\. /, + '' + ); + if (!text) { + return error( + `${passageTitle(translation, book, chapter, verse)} was not found.` + ); + } + + return json({ + id: makeResultId(translation, book, chapter, verse), + title: passageTitle(translation, book, chapter, verse), + text, + url: chapterUrl(apiBase, translation, book, chapter), + metadata: { + translation, + book, + chapter, + verse: parseInt(verse, 10), + }, + }); + } + ); + return server; } From 08dd4bd163c17342d7aa4e11db78a0d683d43906 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:10:06 +0000 Subject: [PATCH 4/6] Support verse ranges in fetchVerse tool Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/README.md | 2 +- packages/seed-bible-mcp/src/mcp.ts | 39 +++++++++++++----------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/packages/seed-bible-mcp/README.md b/packages/seed-bible-mcp/README.md index 2d59582..f47bbac 100644 --- a/packages/seed-bible-mcp/README.md +++ b/packages/seed-bible-mcp/README.md @@ -4,7 +4,7 @@ An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bibl - `getBibleReference` — turns a reference such as `John 3:16` or `Gen 1:1-3 WEB` into a passage result id. - `fetchChapter` — returns the passage text for a result id from `getBibleReference`. -- `fetchVerse` — returns the text of a single verse for a reference such as `John 3:16` or `Gen 1:1 WEB`. +- `fetchVerse` — returns the text of a verse or verse range within one chapter, for a reference such as `John 3:16` or `Gen 1:1-3 WEB`. The server speaks the Streamable HTTP transport at `/mcp` and runs statelessly, so the same code runs on Node.js and on Cloudflare Workers. diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index ee0db6a..80ed37a 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -52,7 +52,7 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { instructions: 'Use getBibleReference() to interpret a user query into a Bible passage result. ' + 'Then use fetchChapter() to retrieve the full passage text. ' + - 'To get the text of one verse directly, use fetchVerse(). ' + + 'To get the text of a verse or verse range directly, use fetchVerse(). ' + 'Supports BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), and SBL Greek NT (grc_sbl).', // The default Ajv validator compiles schemas with `new Function`, // which Cloudflare Workers forbids. The cfworker validator is pure JS @@ -125,52 +125,47 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { 'fetchVerse', { description: - 'Retrieve the text of a single Bible verse from a reference such as "John 3:16".', + 'Retrieve the text of a single Bible verse or a range of verses within one chapter, from a reference such as "John 3:16" or "John 3:16-18".', inputSchema: { reference: z .string() .describe( - 'A single verse reference, optionally followed by a translation, e.g. "John 3:16" or "Gen 1:1 WEB".' + 'A verse or verse range reference, optionally followed by a translation, e.g. "John 3:16", "Gen 1:1-3" or "Ps 23:1-6 WEB".' ), }, }, async ({ reference }) => { const ref = await parseQueryToRef(apiBase, reference); - if (!ref || !ref[3] || ref[3].includes('-')) { + if (!ref || !ref[3]) { return error( - `Could not interpret "${reference}" as a single verse reference.` + `Could not interpret "${reference}" as a verse reference.` ); } - const [translation, book, chapter, verse] = ref; + const [translation, book, chapter, verses] = ref; + const title = passageTitle(translation, book, chapter, verses); const chapterJson = await fetchSimpleChapterJson( apiBase, translation, book, chapter ); - // Drop the "N. " prefix that extractVerses() adds to each verse. - const text = extractVerses(chapterJson, verse).replace( - /^\d+\. /, - '' - ); + let text = extractVerses(chapterJson, verses); if (!text) { - return error( - `${passageTitle(translation, book, chapter, verse)} was not found.` - ); + return error(`${title} was not found.`); + } + if (!verses.includes('-')) { + // Drop the "N. " prefix that extractVerses() adds; it's + // redundant for a single verse. + text = text.replace(/^\d+\. /, ''); } return json({ - id: makeResultId(translation, book, chapter, verse), - title: passageTitle(translation, book, chapter, verse), + id: makeResultId(translation, book, chapter, verses), + title, text, url: chapterUrl(apiBase, translation, book, chapter), - metadata: { - translation, - book, - chapter, - verse: parseInt(verse, 10), - }, + metadata: { translation, book, chapter, verses }, }); } ); From 3ee70e4ced9cf9690180c5a50ceb6aef29c88393 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:23:52 +0000 Subject: [PATCH 5/6] Add listTranslations tool with language and name filters Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/README.md | 3 +- packages/seed-bible-mcp/src/bible.ts | 94 ++++++++++++++++++++++++++++ packages/seed-bible-mcp/src/mcp.ts | 42 +++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/packages/seed-bible-mcp/README.md b/packages/seed-bible-mcp/README.md index f47bbac..61a14c7 100644 --- a/packages/seed-bible-mcp/README.md +++ b/packages/seed-bible-mcp/README.md @@ -1,10 +1,11 @@ # Free Use Bible MCP Server -An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bible API](https://bible.helloao.org) (BSB, WEB, Hebrew WLC, SBL Greek NT) through three tools: +An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bible API](https://bible.helloao.org) (BSB, WEB, Hebrew WLC, SBL Greek NT) through four tools: - `getBibleReference` — turns a reference such as `John 3:16` or `Gen 1:1-3 WEB` into a passage result id. - `fetchChapter` — returns the passage text for a result id from `getBibleReference`. - `fetchVerse` — returns the text of a verse or verse range within one chapter, for a reference such as `John 3:16` or `Gen 1:1-3 WEB`. +- `listTranslations` — lists the available translations, optionally filtered by language (e.g. `spa` or `Spanish`) and by name. The server speaks the Streamable HTTP transport at `/mcp` and runs statelessly, so the same code runs on Node.js and on Cloudflare Workers. diff --git a/packages/seed-bible-mcp/src/bible.ts b/packages/seed-bible-mcp/src/bible.ts index 9f3fb1c..12390b2 100644 --- a/packages/seed-bible-mcp/src/bible.ts +++ b/packages/seed-bible-mcp/src/bible.ts @@ -58,6 +58,100 @@ async function getBooksMap( return m; } +export interface TranslationSummary { + id: string; + name: string; + englishName: string; + shortName?: string; + language: string; + languageName?: string; + languageEnglishName?: string; + textDirection: string; + numberOfBooks: number; +} + +const translationsCache = new Map(); + +/** + * Fetches the list of translations from the API's available_translations.json endpoint. + */ +export async function getAvailableTranslations( + apiBase: string +): Promise { + const cached = translationsCache.get(apiBase); + if (cached) return cached; + + const res = await fetch(`${apiBase}/available_translations.json`); + if (!res.ok) { + throw new Error( + `available_translations.json request failed: ${res.status}` + ); + } + const data: any = await res.json(); + + const translations: TranslationSummary[] = (data.translations ?? []).map( + (t: any) => ({ + id: t.id, + name: t.name, + englishName: t.englishName, + shortName: t.shortName, + language: t.language, + languageName: t.languageName, + languageEnglishName: t.languageEnglishName, + textDirection: t.textDirection, + numberOfBooks: t.numberOfBooks, + }) + ); + + translationsCache.set(apiBase, translations); + return translations; +} + +function includesIgnoreCase( + values: (string | undefined)[], + search: string +): boolean { + const s = search.toLowerCase(); + return values.some((v) => !!v && v.toLowerCase().includes(s)); +} + +/** + * Filters translations by language and/or name. + * + * - `language` matches the ISO 639 language code exactly, or any part of the + * language's native or English name (e.g. "spa", "Spanish", "Español"). + * - `name` matches any part of the translation's id, name, English name, or short name. + */ +export function filterTranslations( + translations: TranslationSummary[], + filter: { language?: string; name?: string } +): TranslationSummary[] { + const language = filter.language?.trim(); + const name = filter.name?.trim(); + return translations.filter((t) => { + if ( + language && + t.language?.toLowerCase() !== language.toLowerCase() && + !includesIgnoreCase( + [t.languageName, t.languageEnglishName], + language + ) + ) { + return false; + } + if ( + name && + !includesIgnoreCase( + [t.id, t.name, t.englishName, t.shortName], + name + ) + ) { + return false; + } + return true; + }); +} + function chooseTranslation(query: string): string { const uq = query.toUpperCase(); for (const [alias, tid] of Object.entries(TRANSLATION_ALIASES)) { diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index 80ed37a..cadc7dd 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -7,6 +7,8 @@ import { chapterUrl, extractVerses, fetchSimpleChapterJson, + filterTranslations, + getAvailableTranslations, makeResultId, parseQueryToRef, parseResultId, @@ -53,6 +55,7 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { 'Use getBibleReference() to interpret a user query into a Bible passage result. ' + 'Then use fetchChapter() to retrieve the full passage text. ' + 'To get the text of a verse or verse range directly, use fetchVerse(). ' + + 'Use listTranslations() to find available translations by language or name. ' + 'Supports BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), and SBL Greek NT (grc_sbl).', // The default Ajv validator compiles schemas with `new Function`, // which Cloudflare Workers forbids. The cfworker validator is pure JS @@ -170,5 +173,44 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { } ); + server.registerTool( + 'listTranslations', + { + description: + 'List the Bible translations available in the Free Use Bible API, optionally filtered by language and/or name.', + inputSchema: { + language: z + .string() + .optional() + .describe( + 'Filter by language: an ISO 639-3 code (e.g. "spa") or part of the language name in English or the language itself (e.g. "Spanish", "Español").' + ), + name: z + .string() + .optional() + .describe( + 'Filter by part of the translation id, name, English name, or short name (e.g. "BSB", "Reina", "King James").' + ), + limit: z + .number() + .int() + .min(1) + .max(500) + .optional() + .describe( + 'The maximum number of translations to return. Defaults to 50.' + ), + }, + }, + async ({ language, name, limit }) => { + const all = await getAvailableTranslations(apiBase); + const matches = filterTranslations(all, { language, name }); + return json({ + total: matches.length, + translations: matches.slice(0, limit ?? 50), + }); + } + ); + return server; } From e98c13111faae534ca943d4b92c7f437b1f856ab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 20:27:38 +0000 Subject: [PATCH 6/6] Add translation parameter to getBibleReference and fetchVerse Accepts any translation id from listTranslations() in addition to the BSB/WEB/WLC/SBL aliases, and falls back to the translation's own book names when a book isn't found in BSB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J6xhi8xAhoc83gHsJj2Luf --- packages/seed-bible-mcp/README.md | 2 ++ packages/seed-bible-mcp/src/bible.ts | 38 +++++++++++++++++++++--- packages/seed-bible-mcp/src/mcp.ts | 44 ++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/seed-bible-mcp/README.md b/packages/seed-bible-mcp/README.md index 61a14c7..b351f0a 100644 --- a/packages/seed-bible-mcp/README.md +++ b/packages/seed-bible-mcp/README.md @@ -7,6 +7,8 @@ An [MCP](https://modelcontextprotocol.io) server that exposes the [Free Use Bibl - `fetchVerse` — returns the text of a verse or verse range within one chapter, for a reference such as `John 3:16` or `Gen 1:1-3 WEB`. - `listTranslations` — lists the available translations, optionally filtered by language (e.g. `spa` or `Spanish`) and by name. +`getBibleReference` and `fetchVerse` take an optional `translation` parameter that accepts any translation id from `listTranslations` (e.g. `spa_rv1909`). Without it, the translation is picked from the reference (BSB, WEB, WLC, or SBL), defaulting to BSB. + The server speaks the Streamable HTTP transport at `/mcp` and runs statelessly, so the same code runs on Node.js and on Cloudflare Workers. ## Layout diff --git a/packages/seed-bible-mcp/src/bible.ts b/packages/seed-bible-mcp/src/bible.ts index 12390b2..3c085c9 100644 --- a/packages/seed-bible-mcp/src/bible.ts +++ b/packages/seed-bible-mcp/src/bible.ts @@ -197,14 +197,41 @@ export function passageTitle( return `${book} ${chapter}${verses ? ':' + verses : ''} (${translation})`; } +/** + * Resolves a user-supplied translation to an API translation id. + * Accepts the short aliases (e.g. "WEB", "Hebrew") or any id from + * available_translations.json, ignoring case. + * Returns null if the translation isn't available. + */ +export async function resolveTranslationId( + apiBase: string, + translation: string +): Promise { + const t = translation.trim(); + const alias = TRANSLATION_ALIASES[t.toUpperCase()]; + if (alias) return alias; + + const translations = await getAvailableTranslations(apiBase); + const match = translations.find( + (x) => x.id.toLowerCase() === t.toLowerCase() + ); + return match?.id ?? null; +} + +/** + * Parses a passage reference such as "John 3:16" or "Gen 1:1-3 WEB". + * + * @param translation An API translation id to use instead of picking one from the query. + */ export async function parseQueryToRef( apiBase: string, - query: string + query: string, + translation?: string ): Promise { const m = REF_RE.exec(query); if (!m || !m.groups) return null; - const translation = chooseTranslation(query); + translation ??= chooseTranslation(query); const bookRaw = m.groups.book.trim(); const chapter = parseInt(m.groups.chapter, 10); const verses = m.groups.verses ?? null; @@ -216,8 +243,11 @@ export async function parseQueryToRef( // Otherwise map from book name -> id using API book metadata. // Using BSB book list is generally fine because IDs are standard across translations. - const booksMap = await getBooksMap(apiBase, 'BSB'); - const bookId = booksMap[norm(bookRaw)]; + // Fall back to the translation's own book names (e.g. "Juan" in a Spanish translation). + let bookId = (await getBooksMap(apiBase, 'BSB'))[norm(bookRaw)]; + if (!bookId && translation !== 'BSB') { + bookId = (await getBooksMap(apiBase, translation))[norm(bookRaw)]; + } if (!bookId) return null; return [translation, bookId, chapter, verses]; diff --git a/packages/seed-bible-mcp/src/mcp.ts b/packages/seed-bible-mcp/src/mcp.ts index cadc7dd..d28b2d0 100644 --- a/packages/seed-bible-mcp/src/mcp.ts +++ b/packages/seed-bible-mcp/src/mcp.ts @@ -12,6 +12,7 @@ import { makeResultId, parseQueryToRef, parseResultId, + resolveTranslationId, passageTitle, } from './bible.js'; @@ -33,6 +34,19 @@ function error(message: string) { }; } +const translationParam = z + .string() + .optional() + .describe( + 'The translation to use: any translation id from listTranslations() (e.g. "spa_rv1909"), or one of BSB, WEB, WLC, SBL. Overrides a translation named in the reference. Defaults to BSB.' + ); + +function unknownTranslation(translation: string) { + return error( + `Unknown translation "${translation}". Use listTranslations() to find available translation ids.` + ); +} + /** * Create a new MCP server instance with the Bible tools registered. * @@ -55,8 +69,8 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { 'Use getBibleReference() to interpret a user query into a Bible passage result. ' + 'Then use fetchChapter() to retrieve the full passage text. ' + 'To get the text of a verse or verse range directly, use fetchVerse(). ' + - 'Use listTranslations() to find available translations by language or name. ' + - 'Supports BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), and SBL Greek NT (grc_sbl).', + 'Use listTranslations() to find available translations by language or name, then pass a translation id as the translation parameter. ' + + 'References can name BSB, WEB (ENGWEBP), Hebrew WLC (heb_wlc), or SBL Greek NT (grc_sbl) directly; other translations need the translation parameter.', // The default Ajv validator compiles schemas with `new Function`, // which Cloudflare Workers forbids. The cfworker validator is pure JS // and works on every runtime. @@ -75,10 +89,17 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { .describe( 'A passage reference, e.g. "John 3:16" or "Gen 1:1-3 WEB".' ), + translation: translationParam, }, }, - async ({ query }) => { - const ref = await parseQueryToRef(apiBase, query); + async ({ query, translation: requestedTranslation }) => { + const translationId = requestedTranslation + ? await resolveTranslationId(apiBase, requestedTranslation) + : undefined; + if (translationId === null) { + return unknownTranslation(requestedTranslation!); + } + const ref = await parseQueryToRef(apiBase, query, translationId); if (!ref) return json({ results: [] }); const [translation, book, chapter, verses] = ref; @@ -135,10 +156,21 @@ export function createBibleMcpServer(options: BibleMcpOptions = {}): McpServer { .describe( 'A verse or verse range reference, optionally followed by a translation, e.g. "John 3:16", "Gen 1:1-3" or "Ps 23:1-6 WEB".' ), + translation: translationParam, }, }, - async ({ reference }) => { - const ref = await parseQueryToRef(apiBase, reference); + async ({ reference, translation: requestedTranslation }) => { + const translationId = requestedTranslation + ? await resolveTranslationId(apiBase, requestedTranslation) + : undefined; + if (translationId === null) { + return unknownTranslation(requestedTranslation!); + } + const ref = await parseQueryToRef( + apiBase, + reference, + translationId + ); if (!ref || !ref[3]) { return error( `Could not interpret "${reference}" as a verse reference.`