From 13477fc0fe52ca1d3fb90c0f46c0650e65813ea8 Mon Sep 17 00:00:00 2001 From: Rasul Kireev Date: Thu, 9 Jul 2026 16:42:51 +0200 Subject: [PATCH 1/4] fix(reader): expose article language in cli --- README.md | 2 + src/commands.ts | 22 +++++++---- src/config.ts | 3 +- src/mcp.ts | 89 ++++++++++++++++++++++++++++++++++++++++-- tests/commands.test.ts | 8 ++++ tests/config.test.ts | 4 ++ tests/mcp.test.ts | 57 ++++++++++++++++++++++++++- 7 files changed, 172 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f20c49e..821bef0 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ readwise reader-create-document --url "https://example.com/article" readwise reader-create-document \ --url "https://example.com" \ --title "My Article" \ + --language "en" \ --tags "reading-list,research" \ --notes "Found via HN" ``` @@ -114,6 +115,7 @@ readwise reader-move-documents --document-ids --location archive readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","title":"Better Title"}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","seen":true}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","notes":"Updated notes"}]' +readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","language":"es"}]' ``` ### Highlight management diff --git a/src/commands.ts b/src/commands.ts index b126e82..0230691 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -48,6 +48,18 @@ export function optionFlag(name: string, prop: SchemaProperty): string { return `${flag} `; } +export function optionDescription(prop: SchemaProperty, isRequired: boolean): string | undefined { + const parts: string[] = []; + if (prop.description) parts.push(prop.description); + if (isRequired) parts.push("(required)"); + const enumValues = prop.enum || prop.items?.enum; + if (enumValues) parts.push(`[${enumValues.join(", ")}]`); + if (prop.maxLength !== undefined) parts.push(`(max length: ${prop.maxLength})`); + if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); + + return parts.join(" ") || undefined; +} + export function parseValue(value: string, prop: SchemaProperty): unknown { if (prop.type === "integer" || prop.type === "number") { const n = Number(value); @@ -122,14 +134,8 @@ export function registerTools(program: Command, tools: ToolDef[]): void { for (const [propName, rawProp] of Object.entries(properties)) { const prop = resolveProperty(rawProp, defs); const flag = optionFlag(propName, prop); - const parts: string[] = []; - if (prop.description) parts.push(prop.description); - if (required.has(propName)) parts.push("(required)"); - const enumValues = prop.enum || prop.items?.enum; - if (enumValues) parts.push(`[${enumValues.join(", ")}]`); - if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); - - cmd.option(flag, parts.join(" ") || undefined); + + cmd.option(flag, optionDescription(prop, required.has(propName))); } cmd.action(async (options: Record) => { diff --git a/src/config.ts b/src/config.ts index 05c59f1..ac6e6a1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,7 @@ export interface SchemaProperty { format?: string; description?: string; enum?: string[]; + maxLength?: number; items?: SchemaProperty; default?: unknown; examples?: unknown[]; @@ -58,7 +59,7 @@ export interface Config { config?: CLIConfig; } -export const TOOLS_CACHE_VERSION = 2; +export const TOOLS_CACHE_VERSION = 3; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours diff --git a/src/mcp.ts b/src/mcp.ts index ec83eb5..c29062c 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,6 +1,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { loadConfig, saveConfig, isCacheValid, TOOLS_CACHE_VERSION, type ToolDef } from "./config.js"; +import { loadConfig, saveConfig, isCacheValid, TOOLS_CACHE_VERSION, type SchemaProperty, type ToolDef } from "./config.js"; import { VERSION } from "./version.js"; const MCP_URL = "https://mcp2.readwise.io/mcp"; @@ -26,6 +26,89 @@ export function createMcpFetch(baseFetch: typeof fetch = fetch): typeof fetch { export const mcpFetch = createMcpFetch(); +const CREATE_DOCUMENT_LANGUAGE_PROPERTY: SchemaProperty = { + anyOf: [{ type: "string", maxLength: 30 }, { type: "null" }], + default: null, + description: "Language code for the document. When omitted, Reader will auto-detect it.", + examples: ["de", "en-US"], +}; + +const BULK_EDIT_LANGUAGE_PROPERTY: SchemaProperty = { + anyOf: [{ type: "string", maxLength: 30 }, { type: "null" }], + default: null, + description: "The new language code for the document", + examples: ["de", "en-US"], +}; + +function insertPropertyAfter( + properties: Record, + afterName: string, + name: string, + prop: SchemaProperty, +): Record { + if (properties[name]) return properties; + + const next: Record = {}; + let inserted = false; + for (const [key, value] of Object.entries(properties)) { + next[key] = value; + if (key === afterName) { + next[name] = prop; + inserted = true; + } + } + if (!inserted) next[name] = prop; + return next; +} + +function addCreateDocumentLanguageSchema(tool: ToolDef): ToolDef { + const properties = tool.inputSchema.properties; + if (!properties || properties.language) return tool; + + return { + ...tool, + inputSchema: { + ...tool.inputSchema, + properties: insertPropertyAfter(properties, "summary", "language", CREATE_DOCUMENT_LANGUAGE_PROPERTY), + }, + }; +} + +function addBulkEditLanguageSchema(tool: ToolDef): ToolDef { + const defs = tool.inputSchema.$defs; + const item = defs?.BulkEditDocumentMetadataItem; + const properties = item?.properties; + if (!defs || !item || !properties || properties.language) return tool; + + return { + ...tool, + inputSchema: { + ...tool.inputSchema, + $defs: { + ...defs, + BulkEditDocumentMetadataItem: { + ...item, + properties: insertPropertyAfter(properties, "summary", "language", BULK_EDIT_LANGUAGE_PROPERTY), + }, + }, + }, + }; +} + +export function applyKnownToolSchemaUpdates(tools: ToolDef[]): ToolDef[] { + // Keep dynamic CLI/TUI forms aligned with backend capabilities that may ship + // before every production MCP listTools response includes the updated schema. + return tools.map((tool) => { + if (tool.name === "reader_create_document") { + return addCreateDocumentLanguageSchema(tool); + } + if (tool.name === "reader_bulk_edit_document_metadata") { + return addBulkEditLanguageSchema(tool); + } + return tool; + }); +} + function createTransport(token: string, authType: "oauth" | "token"): StreamableHTTPClientTransport { const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`; return new StreamableHTTPClientTransport(new URL(MCP_URL), { @@ -42,7 +125,7 @@ export async function getTools(token: string, authType: "oauth" | "token", force if (!forceRefresh) { const config = await loadConfig(); if (isCacheValid(config)) { - return config.tools_cache!.tools; + return applyKnownToolSchemaUpdates(config.tools_cache!.tools); } } @@ -53,7 +136,7 @@ export async function getTools(token: string, authType: "oauth" | "token", force await client.connect(transport, { timeout: MCP_TIMEOUT_MS }); const result = await client.listTools({}, { timeout: MCP_TIMEOUT_MS }); - const tools = result.tools as ToolDef[]; + const tools = applyKnownToolSchemaUpdates(result.tools as ToolDef[]); // Cache const config = await loadConfig(); diff --git a/tests/commands.test.ts b/tests/commands.test.ts index 2d28ec6..3fcd9ac 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + optionDescription, optionFlag, parseValue, resolveProperty, @@ -18,6 +19,13 @@ test("optionFlag formats boolean and value options", () => { assert.equal(optionFlag("document_id", { type: "string" }), "--document-id "); }); +test("optionDescription includes schema constraints for generated help", () => { + assert.equal( + optionDescription({ type: "string", description: "Article language", maxLength: 30 }, true), + "Article language (required) (max length: 30)", + ); +}); + test("parseValue handles numbers, booleans, arrays, and strings", () => { assert.equal(parseValue("42", { type: "integer" }), 42); assert.equal(parseValue("3.14", { type: "number" }), 3.14); diff --git a/tests/config.test.ts b/tests/config.test.ts index eb2b97c..25fc2da 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -68,6 +68,10 @@ test("isCacheValid requires a matching cache version and fresh timestamp", () => assert.equal(isCacheValid({}), false); }); +test("tool cache version invalidates schemas before Reader language support", () => { + assert.equal(TOOLS_CACHE_VERSION, 3); +}); + test("filterReadOnlyTools keeps only tools explicitly annotated read-only", () => { const tools: ToolDef[] = [ { diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 9a60f8e..13de347 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { createMcpFetch } from "../src/mcp.js"; +import { applyKnownToolSchemaUpdates, createMcpFetch } from "../src/mcp.js"; +import type { ToolDef } from "../src/config.js"; test("mcp fetch opts out of the optional GET SSE stream", async () => { let delegated = false; @@ -44,3 +45,57 @@ test("mcp fetch reads the method from Request inputs", async () => { assert.equal(response.status, 200); assert.equal(delegated, true); }); + +test("known schema updates add Reader create language support when missing", () => { + const tools: ToolDef[] = [{ + name: "reader_create_document", + inputSchema: { + type: "object", + properties: { + url: { type: "string" }, + summary: { anyOf: [{ type: "string" }, { type: "null" }], default: null }, + title: { anyOf: [{ type: "string" }, { type: "null" }], default: null }, + }, + }, + }]; + + const [tool] = applyKnownToolSchemaUpdates(tools); + const properties = tool!.inputSchema.properties!; + + assert.deepEqual(Object.keys(properties), ["url", "summary", "language", "title"]); + assert.equal(properties.language?.description, "Language code for the document. When omitted, Reader will auto-detect it."); + assert.equal(properties.language?.anyOf?.[0]?.maxLength, 30); +}); + +test("known schema updates add Reader bulk edit language support when missing", () => { + const tools: ToolDef[] = [{ + name: "reader_bulk_edit_document_metadata", + inputSchema: { + type: "object", + properties: { + documents: { + type: "array", + items: { $ref: "#/$defs/BulkEditDocumentMetadataItem" }, + }, + }, + $defs: { + BulkEditDocumentMetadataItem: { + type: "object", + properties: { + document_id: { type: "string" }, + summary: { anyOf: [{ type: "string" }, { type: "null" }], default: null }, + seen: { anyOf: [{ type: "boolean" }, { type: "null" }], default: null }, + }, + required: ["document_id"], + }, + }, + }, + }]; + + const [tool] = applyKnownToolSchemaUpdates(tools); + const properties = tool!.inputSchema.$defs!.BulkEditDocumentMetadataItem!.properties!; + + assert.deepEqual(Object.keys(properties), ["document_id", "summary", "language", "seen"]); + assert.equal(properties.language?.description, "The new language code for the document"); + assert.equal(properties.language?.anyOf?.[0]?.maxLength, 30); +}); From 93abd2b11b5991881b8111980279007aea77427c Mon Sep 17 00:00:00 2001 From: Rasul Kireev Date: Thu, 9 Jul 2026 17:04:33 +0200 Subject: [PATCH 2/4] fix(reader): trim cli language diff --- README.md | 2 -- src/commands.ts | 22 ++++++++-------------- tests/commands.test.ts | 8 -------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 821bef0..f20c49e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ readwise reader-create-document --url "https://example.com/article" readwise reader-create-document \ --url "https://example.com" \ --title "My Article" \ - --language "en" \ --tags "reading-list,research" \ --notes "Found via HN" ``` @@ -115,7 +114,6 @@ readwise reader-move-documents --document-ids --location archive readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","title":"Better Title"}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","seen":true}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","notes":"Updated notes"}]' -readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","language":"es"}]' ``` ### Highlight management diff --git a/src/commands.ts b/src/commands.ts index 0230691..b126e82 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -48,18 +48,6 @@ export function optionFlag(name: string, prop: SchemaProperty): string { return `${flag} `; } -export function optionDescription(prop: SchemaProperty, isRequired: boolean): string | undefined { - const parts: string[] = []; - if (prop.description) parts.push(prop.description); - if (isRequired) parts.push("(required)"); - const enumValues = prop.enum || prop.items?.enum; - if (enumValues) parts.push(`[${enumValues.join(", ")}]`); - if (prop.maxLength !== undefined) parts.push(`(max length: ${prop.maxLength})`); - if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); - - return parts.join(" ") || undefined; -} - export function parseValue(value: string, prop: SchemaProperty): unknown { if (prop.type === "integer" || prop.type === "number") { const n = Number(value); @@ -134,8 +122,14 @@ export function registerTools(program: Command, tools: ToolDef[]): void { for (const [propName, rawProp] of Object.entries(properties)) { const prop = resolveProperty(rawProp, defs); const flag = optionFlag(propName, prop); - - cmd.option(flag, optionDescription(prop, required.has(propName))); + const parts: string[] = []; + if (prop.description) parts.push(prop.description); + if (required.has(propName)) parts.push("(required)"); + const enumValues = prop.enum || prop.items?.enum; + if (enumValues) parts.push(`[${enumValues.join(", ")}]`); + if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); + + cmd.option(flag, parts.join(" ") || undefined); } cmd.action(async (options: Record) => { diff --git a/tests/commands.test.ts b/tests/commands.test.ts index 3fcd9ac..2d28ec6 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -1,7 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; import { - optionDescription, optionFlag, parseValue, resolveProperty, @@ -19,13 +18,6 @@ test("optionFlag formats boolean and value options", () => { assert.equal(optionFlag("document_id", { type: "string" }), "--document-id "); }); -test("optionDescription includes schema constraints for generated help", () => { - assert.equal( - optionDescription({ type: "string", description: "Article language", maxLength: 30 }, true), - "Article language (required) (max length: 30)", - ); -}); - test("parseValue handles numbers, booleans, arrays, and strings", () => { assert.equal(parseValue("42", { type: "integer" }), 42); assert.equal(parseValue("3.14", { type: "number" }), 3.14); From 58deed80aa816e3573182f78464a0d05675a1b99 Mon Sep 17 00:00:00 2001 From: Rasul Kireev Date: Thu, 9 Jul 2026 17:11:23 +0200 Subject: [PATCH 3/4] Revert "fix(reader): trim cli language diff" This reverts commit 93abd2b11b5991881b8111980279007aea77427c. --- README.md | 2 ++ src/commands.ts | 22 ++++++++++++++-------- tests/commands.test.ts | 8 ++++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f20c49e..821bef0 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ readwise reader-create-document --url "https://example.com/article" readwise reader-create-document \ --url "https://example.com" \ --title "My Article" \ + --language "en" \ --tags "reading-list,research" \ --notes "Found via HN" ``` @@ -114,6 +115,7 @@ readwise reader-move-documents --document-ids --location archive readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","title":"Better Title"}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","seen":true}]' readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","notes":"Updated notes"}]' +readwise reader-bulk-edit-document-metadata --documents '[{"document_id":"","language":"es"}]' ``` ### Highlight management diff --git a/src/commands.ts b/src/commands.ts index b126e82..0230691 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -48,6 +48,18 @@ export function optionFlag(name: string, prop: SchemaProperty): string { return `${flag} `; } +export function optionDescription(prop: SchemaProperty, isRequired: boolean): string | undefined { + const parts: string[] = []; + if (prop.description) parts.push(prop.description); + if (isRequired) parts.push("(required)"); + const enumValues = prop.enum || prop.items?.enum; + if (enumValues) parts.push(`[${enumValues.join(", ")}]`); + if (prop.maxLength !== undefined) parts.push(`(max length: ${prop.maxLength})`); + if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); + + return parts.join(" ") || undefined; +} + export function parseValue(value: string, prop: SchemaProperty): unknown { if (prop.type === "integer" || prop.type === "number") { const n = Number(value); @@ -122,14 +134,8 @@ export function registerTools(program: Command, tools: ToolDef[]): void { for (const [propName, rawProp] of Object.entries(properties)) { const prop = resolveProperty(rawProp, defs); const flag = optionFlag(propName, prop); - const parts: string[] = []; - if (prop.description) parts.push(prop.description); - if (required.has(propName)) parts.push("(required)"); - const enumValues = prop.enum || prop.items?.enum; - if (enumValues) parts.push(`[${enumValues.join(", ")}]`); - if (prop.default !== undefined) parts.push(`(default: ${JSON.stringify(prop.default)})`); - - cmd.option(flag, parts.join(" ") || undefined); + + cmd.option(flag, optionDescription(prop, required.has(propName))); } cmd.action(async (options: Record) => { diff --git a/tests/commands.test.ts b/tests/commands.test.ts index 2d28ec6..3fcd9ac 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + optionDescription, optionFlag, parseValue, resolveProperty, @@ -18,6 +19,13 @@ test("optionFlag formats boolean and value options", () => { assert.equal(optionFlag("document_id", { type: "string" }), "--document-id "); }); +test("optionDescription includes schema constraints for generated help", () => { + assert.equal( + optionDescription({ type: "string", description: "Article language", maxLength: 30 }, true), + "Article language (required) (max length: 30)", + ); +}); + test("parseValue handles numbers, booleans, arrays, and strings", () => { assert.equal(parseValue("42", { type: "integer" }), 42); assert.equal(parseValue("3.14", { type: "number" }), 3.14); From bdcc2b87e6dca15773603f67a2d668c87d97b067 Mon Sep 17 00:00:00 2001 From: Rasul Kireev Date: Thu, 9 Jul 2026 17:13:25 +0200 Subject: [PATCH 4/4] fix(reader): move language schema shim out of mcp --- src/mcp.ts | 90 ++----------------------------------- src/readerLanguageSchema.ts | 53 ++++++++++++++++++++++ tests/mcp.test.ts | 11 ++--- 3 files changed, 63 insertions(+), 91 deletions(-) create mode 100644 src/readerLanguageSchema.ts diff --git a/src/mcp.ts b/src/mcp.ts index c29062c..e490e7b 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,6 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { loadConfig, saveConfig, isCacheValid, TOOLS_CACHE_VERSION, type SchemaProperty, type ToolDef } from "./config.js"; +import { loadConfig, saveConfig, isCacheValid, TOOLS_CACHE_VERSION, type ToolDef } from "./config.js"; +import { addReaderLanguageSchemas } from "./readerLanguageSchema.js"; import { VERSION } from "./version.js"; const MCP_URL = "https://mcp2.readwise.io/mcp"; @@ -26,89 +27,6 @@ export function createMcpFetch(baseFetch: typeof fetch = fetch): typeof fetch { export const mcpFetch = createMcpFetch(); -const CREATE_DOCUMENT_LANGUAGE_PROPERTY: SchemaProperty = { - anyOf: [{ type: "string", maxLength: 30 }, { type: "null" }], - default: null, - description: "Language code for the document. When omitted, Reader will auto-detect it.", - examples: ["de", "en-US"], -}; - -const BULK_EDIT_LANGUAGE_PROPERTY: SchemaProperty = { - anyOf: [{ type: "string", maxLength: 30 }, { type: "null" }], - default: null, - description: "The new language code for the document", - examples: ["de", "en-US"], -}; - -function insertPropertyAfter( - properties: Record, - afterName: string, - name: string, - prop: SchemaProperty, -): Record { - if (properties[name]) return properties; - - const next: Record = {}; - let inserted = false; - for (const [key, value] of Object.entries(properties)) { - next[key] = value; - if (key === afterName) { - next[name] = prop; - inserted = true; - } - } - if (!inserted) next[name] = prop; - return next; -} - -function addCreateDocumentLanguageSchema(tool: ToolDef): ToolDef { - const properties = tool.inputSchema.properties; - if (!properties || properties.language) return tool; - - return { - ...tool, - inputSchema: { - ...tool.inputSchema, - properties: insertPropertyAfter(properties, "summary", "language", CREATE_DOCUMENT_LANGUAGE_PROPERTY), - }, - }; -} - -function addBulkEditLanguageSchema(tool: ToolDef): ToolDef { - const defs = tool.inputSchema.$defs; - const item = defs?.BulkEditDocumentMetadataItem; - const properties = item?.properties; - if (!defs || !item || !properties || properties.language) return tool; - - return { - ...tool, - inputSchema: { - ...tool.inputSchema, - $defs: { - ...defs, - BulkEditDocumentMetadataItem: { - ...item, - properties: insertPropertyAfter(properties, "summary", "language", BULK_EDIT_LANGUAGE_PROPERTY), - }, - }, - }, - }; -} - -export function applyKnownToolSchemaUpdates(tools: ToolDef[]): ToolDef[] { - // Keep dynamic CLI/TUI forms aligned with backend capabilities that may ship - // before every production MCP listTools response includes the updated schema. - return tools.map((tool) => { - if (tool.name === "reader_create_document") { - return addCreateDocumentLanguageSchema(tool); - } - if (tool.name === "reader_bulk_edit_document_metadata") { - return addBulkEditLanguageSchema(tool); - } - return tool; - }); -} - function createTransport(token: string, authType: "oauth" | "token"): StreamableHTTPClientTransport { const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`; return new StreamableHTTPClientTransport(new URL(MCP_URL), { @@ -125,7 +43,7 @@ export async function getTools(token: string, authType: "oauth" | "token", force if (!forceRefresh) { const config = await loadConfig(); if (isCacheValid(config)) { - return applyKnownToolSchemaUpdates(config.tools_cache!.tools); + return addReaderLanguageSchemas(config.tools_cache!.tools); } } @@ -136,7 +54,7 @@ export async function getTools(token: string, authType: "oauth" | "token", force await client.connect(transport, { timeout: MCP_TIMEOUT_MS }); const result = await client.listTools({}, { timeout: MCP_TIMEOUT_MS }); - const tools = applyKnownToolSchemaUpdates(result.tools as ToolDef[]); + const tools = addReaderLanguageSchemas(result.tools as ToolDef[]); // Cache const config = await loadConfig(); diff --git a/src/readerLanguageSchema.ts b/src/readerLanguageSchema.ts new file mode 100644 index 0000000..b4e6ee7 --- /dev/null +++ b/src/readerLanguageSchema.ts @@ -0,0 +1,53 @@ +import type { SchemaProperty, ToolDef } from "./config.js"; + +function languageProperty(description: string): SchemaProperty { + return { + anyOf: [{ type: "string", maxLength: 30 }, { type: "null" }], + default: null, + description, + examples: ["de", "en-US"], + }; +} + +function addLanguageProperty( + properties: Record | undefined, + description: string, +): Record | undefined { + if (!properties || properties.language) return properties; + return { ...properties, language: languageProperty(description) }; +} + +function addCreateDocumentLanguage(tool: ToolDef): ToolDef { + const properties = addLanguageProperty( + tool.inputSchema.properties, + "Language code for the document. When omitted, Reader will auto-detect it.", + ); + if (!properties || properties === tool.inputSchema.properties) return tool; + + return { ...tool, inputSchema: { ...tool.inputSchema, properties } }; +} + +function addBulkEditLanguage(tool: ToolDef): ToolDef { + const item = tool.inputSchema.$defs?.BulkEditDocumentMetadataItem; + const properties = addLanguageProperty(item?.properties, "The new language code for the document"); + if (!item || !properties || properties === item.properties) return tool; + + return { + ...tool, + inputSchema: { + ...tool.inputSchema, + $defs: { + ...tool.inputSchema.$defs, + BulkEditDocumentMetadataItem: { ...item, properties }, + }, + }, + }; +} + +export function addReaderLanguageSchemas(tools: ToolDef[]): ToolDef[] { + return tools.map((tool) => { + if (tool.name === "reader_create_document") return addCreateDocumentLanguage(tool); + if (tool.name === "reader_bulk_edit_document_metadata") return addBulkEditLanguage(tool); + return tool; + }); +} diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 13de347..45daab2 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { applyKnownToolSchemaUpdates, createMcpFetch } from "../src/mcp.js"; +import { createMcpFetch } from "../src/mcp.js"; +import { addReaderLanguageSchemas } from "../src/readerLanguageSchema.js"; import type { ToolDef } from "../src/config.js"; test("mcp fetch opts out of the optional GET SSE stream", async () => { @@ -59,10 +60,10 @@ test("known schema updates add Reader create language support when missing", () }, }]; - const [tool] = applyKnownToolSchemaUpdates(tools); + const [tool] = addReaderLanguageSchemas(tools); const properties = tool!.inputSchema.properties!; - assert.deepEqual(Object.keys(properties), ["url", "summary", "language", "title"]); + assert.deepEqual(Object.keys(properties), ["url", "summary", "title", "language"]); assert.equal(properties.language?.description, "Language code for the document. When omitted, Reader will auto-detect it."); assert.equal(properties.language?.anyOf?.[0]?.maxLength, 30); }); @@ -92,10 +93,10 @@ test("known schema updates add Reader bulk edit language support when missing", }, }]; - const [tool] = applyKnownToolSchemaUpdates(tools); + const [tool] = addReaderLanguageSchemas(tools); const properties = tool!.inputSchema.$defs!.BulkEditDocumentMetadataItem!.properties!; - assert.deepEqual(Object.keys(properties), ["document_id", "summary", "language", "seen"]); + assert.deepEqual(Object.keys(properties), ["document_id", "summary", "seen", "language"]); assert.equal(properties.language?.description, "The new language code for the document"); assert.equal(properties.language?.anyOf?.[0]?.maxLength, 30); });