diff --git a/.gitignore b/.gitignore index a14702c..97bcc7e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + +# Data +data diff --git a/package.json b/package.json index 59ad74c..25665bf 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "prepare": "bun .husky/install.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0" + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^4.3.6" } } diff --git a/resources/dictionaries.ts b/resources/dictionaries.ts new file mode 100644 index 0000000..f4d5128 --- /dev/null +++ b/resources/dictionaries.ts @@ -0,0 +1,130 @@ +import { type McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js" +import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js" +import type { ListResourcesResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js" +import z from "zod" +import { + createDictionary, + deleteDictionary, + listDictionaries, + readDictionary, + updateDictionary +} from "@/services/dictionaries" + +export const registerDictionariesFunctionality = (server: McpServer) => { + server.registerResource( + "dictionaries", + new ResourceTemplate(new UriTemplate(`dictionary://{name}`), { + list: async () => { + try { + const dictionaries = await listDictionaries() + return { + resources: dictionaries.map((dictionary) => ({ + name: dictionary, + uri: `dictionary://${dictionary}`, + mimeType: "text/plain" + })) + } satisfies ListResourcesResult + } catch (_) { + return { + resources: [] + } + } + } + }), + { mimeType: "text/plain" }, + async (uri: URL) => { + try { + const dictionary = await readDictionary(uri.href.replace("dictionary://", "")) + return { contents: [{ uri: uri.toString(), text: dictionary }] } satisfies ReadResourceResult + } catch (error) { + if (error instanceof Error) { + return { + isError: true, + contents: [{ uri: uri.toString(), text: error.message }] + } satisfies ReadResourceResult + } + console.error(error) + return { + isError: true, + contents: [{ uri: uri.toString(), text: "An unknown error occurred." }] + } satisfies ReadResourceResult + } + } + ) + + server.registerTool( + "add_dictionary", + { + title: "Add Dictionary", + description: "Add a dictionary. Dictionaries are text files containing words or phrases separated by newlines.", + inputSchema: z.object({ + name: z.string(), + content: z.string() + }) + }, + async ({ name, content }) => { + try { + const dictionaryName = await createDictionary(name, content) + return { + content: [ + { type: "text", text: `Dictionary ${name} created` }, + { type: "resource_link", uri: `dictionary://${dictionaryName}`, name: name } + ] + } + } catch (error) { + if (error instanceof Error) { + return { isError: true, content: [{ type: "text", text: `An error occurred: ${error.message}` }] } + } + console.error(error) + return { isError: true, content: [{ type: "text", text: "An unknown error occurred." }] } + } + } + ) + + server.registerTool( + "delete_dictionary", + { + title: "Delete Dictionary", + description: "Delete a dictionary.", + inputSchema: z.object({ + name: z.string() + }) + }, + async ({ name }) => { + try { + await deleteDictionary(name) + return { content: [{ type: "text", text: "Dictionary deleted" }] } + } catch (error) { + if (error instanceof Error) { + return { isError: true, content: [{ type: "text", text: `An error occurred: ${error.message}` }] } + } + console.error(error) + return { isError: true, content: [{ type: "text", text: "An unknown error occurred." }] } + } + } + ) + + server.registerTool( + "update_dictionary", + { + title: "Update Dictionary", + description: "Update a dictionary, overwriting the existing content.", + inputSchema: z.object({ + name: z.string(), + content: z.string() + }) + }, + async ({ name, content }) => { + try { + await updateDictionary(name, content) + return { content: [{ type: "text", text: "Dictionary updated" }] } + } catch (error) { + if (error instanceof Error) { + return { isError: true, content: [{ type: "text", text: `An error occurred: ${error.message}` }] } + } + console.error(error) + return { isError: true, content: [{ type: "text", text: "An unknown error occurred." }] } + } + } + ) +} diff --git a/services/dictionaries.ts b/services/dictionaries.ts new file mode 100644 index 0000000..1a56d85 --- /dev/null +++ b/services/dictionaries.ts @@ -0,0 +1,60 @@ +import { readdir } from "node:fs/promises" +import { join } from "node:path" + +const DICTIONARIES_PATH = join(process.cwd(), "data", "dictionaries") + +export const sanitizeDictionaryName = (name: string) => { + return name.replace(/[^a-zA-Z0-9_]/g, "_") +} + +export const createDictionary = async (name: string, content: string): Promise => { + const dictionaryName = sanitizeDictionaryName(name) + const path = join(DICTIONARIES_PATH, `${dictionaryName}.txt`) + const file = Bun.file(path) + + if (await file.exists()) { + throw new Error(`Dictionary "${dictionaryName}" already exists`) + } + + await file.write(content) + return dictionaryName +} + +export const readDictionary = async (name: string): Promise => { + const path = join(DICTIONARIES_PATH, `${sanitizeDictionaryName(name)}.txt`) + const file = Bun.file(path) + + if (!(await file.exists())) { + throw new Error(`Dictionary "${name === "" || name === undefined ? "undefined" : name}" does not exist`) + } + return Bun.file(path).text() +} + +export const listDictionaries = async (): Promise => { + const files = await readdir(DICTIONARIES_PATH, { recursive: true }) + + return files.map((file) => file.replace(".txt", "")) +} + +export const updateDictionary = async (name: string, content: string): Promise => { + const path = join(DICTIONARIES_PATH, `${sanitizeDictionaryName(name)}.txt`) + + const file = Bun.file(path) + + if (!(await file.exists())) { + throw new Error(`Dictionary "${name}" does not exist`) + } + + await file.write(content) +} + +export const deleteDictionary = async (name: string): Promise => { + const path = join(DICTIONARIES_PATH, `${sanitizeDictionaryName(name)}.txt`) + + const file = Bun.file(path) + if (!(await file.exists())) { + throw new Error(`Dictionary "${name}" does not exist`) + } + + await file.delete() +} diff --git a/stdio.ts b/stdio.ts index 2f2e87f..7ea9603 100644 --- a/stdio.ts +++ b/stdio.ts @@ -1,10 +1,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { registerDictionariesFunctionality } from "@/resources/dictionaries" const server = new McpServer({ name: "writer-helpers", version: "0.0.1" }) +registerDictionariesFunctionality(server) + const transport = new StdioServerTransport() await server.connect(transport)