diff --git a/helpers/markov.ts b/helpers/markov.ts new file mode 100644 index 0000000..4bf7223 --- /dev/null +++ b/helpers/markov.ts @@ -0,0 +1,64 @@ +export interface MarkovOptions { + minLength?: number + maxLength?: number + order?: number + startWith?: string + maxAttempts?: number +} + +export const buildMarkovChain = (words: string[], order: number, startWith?: string): Map => { + const chain: Map = new Map() + + for (const word of words) { + const paddedWord = startWith ? startWith + word : word + for (let i = 0; i <= paddedWord.length - order; i++) { + const state = paddedWord.slice(i, i + order) + const nextChar = i + order < paddedWord.length ? paddedWord.charAt(i + order) : "" + if (!chain.has(state)) { + chain.set(state, []) + } + ;(chain.get(state) as string[]).push(nextChar) + } + } + + return chain +} + +export const generateWordFromChain = ( + chain: Map, + minLength: number, + maxLength: number, + maxAttempts: number +): string | null => { + let attempts = 0 + while (attempts < maxAttempts) { + attempts++ + + // Choose random starting state + const states = Array.from(chain.keys()) + if (states.length === 0) return null + + const currentIndex = Math.floor(Math.random() * states.length) + let current = states[currentIndex] as string + let word = current + + // Generate until we reach desired length + while (word.length < maxLength) { + const possibilities = chain.get(current) || [] + if (possibilities.length === 0) break + + const nextIndex = Math.floor(Math.random() * possibilities.length) + const next = possibilities[nextIndex] as string + if (next === "") break // End of word + + word += next + current = current.slice(1) + next + } + + // Check if word meets length requirements + if (word.length >= minLength && word.length <= maxLength) { + return word + } + } + return null // Could not generate valid word +} diff --git a/services/makeUpWords.ts b/services/makeUpWords.ts new file mode 100644 index 0000000..9e072f2 --- /dev/null +++ b/services/makeUpWords.ts @@ -0,0 +1,29 @@ +import { buildMarkovChain, generateWordFromChain, type MarkovOptions } from "@/helpers/markov" + +export const generateMadeUpWords = (words: string[], count: number, options: MarkovOptions = {}): string[] => { + const { minLength, maxLength, order = 2, startWith, maxAttempts = 1000 } = options + + // If no words provided, return empty array + if (words.length === 0) { + return [] + } + + // Calculate min/max lengths from dictionary if not provided + const lengths = words.map((w) => w.length) + const actualMinLength = minLength ?? Math.min(...lengths) + const actualMaxLength = maxLength ?? Math.max(...lengths) + + // Build Markov chain + const chain = buildMarkovChain(words, order, startWith) + + // Generate the requested number of words + const result: string[] = [] + for (let i = 0; i < count; i++) { + const word = generateWordFromChain(chain, actualMinLength, actualMaxLength, maxAttempts) + if (word) { + result.push(word) + } + } + + return result +} diff --git a/stdio.ts b/stdio.ts index 6c8c6da..c90d79e 100644 --- a/stdio.ts +++ b/stdio.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { registerDictionariesFunctionality } from "@/resources/dictionaries" import { addRandomWordsTool } from "@/tools/getRandomWords" +import { addMakeUpWordsTool } from "@/tools/makeUpWords" const server = new McpServer({ name: "writer-helpers", @@ -10,6 +11,7 @@ const server = new McpServer({ registerDictionariesFunctionality(server) addRandomWordsTool(server) +addMakeUpWordsTool(server) const transport = new StdioServerTransport() await server.connect(transport) diff --git a/tools/makeUpWords.ts b/tools/makeUpWords.ts new file mode 100644 index 0000000..0eaef87 --- /dev/null +++ b/tools/makeUpWords.ts @@ -0,0 +1,103 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import z from "zod" +import { readDictionary } from "@/services/dictionaries" +import { generateMadeUpWords } from "@/services/makeUpWords" + +export const addMakeUpWordsTool = (server: McpServer) => { + server.registerTool( + "generate_made_up_words_from_dict", + { + title: "Generate Made-up Words from Dictionary", + description: "Generate new words using Markov chains based on a dictionary.", + inputSchema: z.object({ + dictionary_name: z.string(), + count: z.number().optional().default(1), + minLength: z.number().optional(), + maxLength: z.number().optional(), + order: z.number().optional().default(2), + startWith: z.string().optional(), + maxAttempts: z.number().optional().default(1000) + }) + }, + async (input) => { + try { + if (input.count <= 0) { + return { + isError: true, + content: [{ type: "text", text: "Count must be a positive number" }] + } + } + if (input.minLength !== undefined && input.maxLength !== undefined && input.minLength > input.maxLength) { + return { + isError: true, + content: [{ type: "text", text: "minLength cannot be greater than maxLength" }] + } + } + + const words: string[] = (await readDictionary(input.dictionary_name)).split("\n").filter((w) => w.trim()) + + const generatedWords = generateMadeUpWords(words, input.count, { + minLength: input.minLength, + maxLength: input.maxLength, + order: input.order, + startWith: input.startWith, + maxAttempts: input.maxAttempts + }) + + return { content: [{ type: "text", text: generatedWords.join("\n") }] } + } catch (error) { + if (error instanceof Error) { + return { + isError: true, + content: [{ type: "text", text: `Error generating words: ${error.message}` }] + } + } + return { + isError: true, + content: [{ type: "text", text: "Unknown error generating words" }] + } + } + } + ) + + server.registerTool( + "generate_made_up_words_from_list", + { + title: "Generate Made-up Words from List", + description: "Generate new words using Markov chains based on a list of words.", + inputSchema: z.object({ + words: z.array(z.string()), + count: z.number().optional().default(1), + minLength: z.number().optional(), + maxLength: z.number().optional(), + order: z.number().optional().default(2), + startWith: z.string().optional(), + maxAttempts: z.number().optional().default(1000) + }) + }, + async (input) => { + if (input.count <= 0) { + return { + isError: true, + content: [{ type: "text", text: "Count must be a positive number" }] + } + } + if (input.minLength !== undefined && input.maxLength !== undefined && input.minLength > input.maxLength) { + return { + isError: true, + content: [{ type: "text", text: "minLength cannot be greater than maxLength" }] + } + } + + const generatedWords = generateMadeUpWords(input.words, input.count, { + minLength: input.minLength, + maxLength: input.maxLength, + order: input.order, + startWith: input.startWith, + maxAttempts: input.maxAttempts + }) + + return { content: [{ type: "text", text: generatedWords.join("\n") }] } + } + ) +}