#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
// Parse command-line arguments for default glossary file path
// Usage: node index.js --glossary-path /path/to/vault/Glossary/개발용어사전.md
function parseArgs(): { defaultGlossaryPath: string | null } {
const args = process.argv.slice(2);
let defaultGlossaryPath: string | null = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--glossary-path" && args[i + 1]) {
defaultGlossaryPath = args[i + 1];
break;
}
}
return { defaultGlossaryPath };
}
const { defaultGlossaryPath } = parseArgs();
// Helper to get file path with fallback to default
function getFilePath(providedPath: string | undefined): string {
if (providedPath) {
return providedPath;
}
if (defaultGlossaryPath) {
return defaultGlossaryPath;
}
throw new Error(
"No file_path provided and no default glossary path configured. " +
"Either provide file_path parameter or start the server with --glossary-path argument."
);
}
// Tool definitions
const TOOLS: Tool[] = [
{
name: "append_entry",
description:
"Append a new glossary entry to the dictionary file. Use this to add new technical terms to the Obsidian glossary.",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description:
"Path to the glossary markdown file (e.g., /path/to/vault/Glossary/개발용어사전.md)",
},
term: {
type: "string",
description: "The technical term to add",
},
dev_explanation: {
type: "string",
description: "Developer-focused explanation in Korean (1-2 sentences)",
},
simple_explanation: {
type: "string",
description:
"Simple explanation for non-developers in Korean (1 sentence)",
},
example: {
type: "string",
description: "One short example in Korean",
},
},
required: ["term", "dev_explanation", "simple_explanation", "example"],
},
},
{
name: "search_entry",
description:
"Check if a term already exists in the glossary. Returns true/false and the entry content if found.",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the glossary markdown file",
},
term: {
type: "string",
description: "The term to search for",
},
},
required: ["term"],
},
},
{
name: "get_entry",
description:
"Retrieve a specific entry from the glossary by term name. Returns only that entry, not the whole file.",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the glossary markdown file",
},
term: {
type: "string",
description: "The term to retrieve",
},
},
required: ["term"],
},
},
{
name: "list_terms",
description:
"List all terms currently in the glossary (returns only term names, not full content).",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the glossary markdown file",
},
},
required: [],
},
},
];
// Helper function to format an entry
function formatEntry(
term: string,
devExplanation: string,
simpleExplanation: string,
example: string
): string {
return `### ${term}
- 개발자용 설명: ${devExplanation}
- 비개발자용 설명: ${simpleExplanation}
- 예시: ${example}
`;
}
// Helper function to extract an entry for a specific term
function extractEntry(content: string, term: string): string | null {
// Match the term header and everything until the next ### or end of file
const regex = new RegExp(
`### ${escapeRegex(term)}\\s*\\n([\\s\\S]*?)(?=\\n### |$)`,
"i"
);
const match = content.match(regex);
if (match) {
return `### ${term}\n${match[1].trim()}`;
}
return null;
}
// Helper function to check if term exists
function termExists(content: string, term: string): boolean {
const regex = new RegExp(`^### ${escapeRegex(term)}\\s*$`, "im");
return regex.test(content);
}
// Helper function to extract all terms
function extractAllTerms(content: string): string[] {
const regex = /^### (.+)$/gm;
const terms: string[] = [];
let match;
while ((match = regex.exec(content)) !== null) {
terms.push(match[1].trim());
}
return terms;
}
// Helper function to escape regex special characters
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Helper function to ensure directory exists
async function ensureDir(filePath: string): Promise<void> {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
}
// Helper function to read file or return empty string
async function readFileOrEmpty(filePath: string): Promise<string> {
try {
return await fs.readFile(filePath, "utf-8");
} catch {
return "";
}
}
// Create server
const server = new Server(
{
name: "obsidian-dictionary-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "append_entry": {
const { file_path, term, dev_explanation, simple_explanation, example } =
args as {
file_path?: string;
term: string;
dev_explanation: string;
simple_explanation: string;
example: string;
};
const resolvedPath = getFilePath(file_path);
// Check if term already exists
const existingContent = await readFileOrEmpty(resolvedPath);
if (termExists(existingContent, term)) {
return {
content: [
{
type: "text",
text: `Term "${term}" already exists in the glossary. Use search_entry to view it.`,
},
],
};
}
// Ensure directory exists
await ensureDir(resolvedPath);
// Format and append entry
const entry = formatEntry(
term,
dev_explanation,
simple_explanation,
example
);
// If file is empty, add a header
let contentToAppend = entry;
if (!existingContent.trim()) {
contentToAppend = `# 개발 용어 사전\n\n${entry}`;
}
await fs.appendFile(resolvedPath, contentToAppend, "utf-8");
return {
content: [
{
type: "text",
text: `Successfully added "${term}" to the glossary.`,
},
],
};
}
case "search_entry": {
const { file_path, term } = args as { file_path?: string; term: string };
const resolvedPath = getFilePath(file_path);
const content = await readFileOrEmpty(resolvedPath);
if (!content) {
return {
content: [
{
type: "text",
text: `Glossary file not found or empty: ${resolvedPath}`,
},
],
};
}
const exists = termExists(content, term);
if (exists) {
const entry = extractEntry(content, term);
return {
content: [
{
type: "text",
text: `Term "${term}" found:\n\n${entry}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Term "${term}" not found in glossary.`,
},
],
};
}
case "get_entry": {
const { file_path, term } = args as { file_path?: string; term: string };
const resolvedPath = getFilePath(file_path);
const content = await readFileOrEmpty(resolvedPath);
if (!content) {
return {
content: [
{
type: "text",
text: `Glossary file not found or empty: ${resolvedPath}`,
},
],
};
}
const entry = extractEntry(content, term);
if (entry) {
return {
content: [
{
type: "text",
text: entry,
},
],
};
}
return {
content: [
{
type: "text",
text: `Term "${term}" not found in glossary.`,
},
],
};
}
case "list_terms": {
const { file_path } = args as { file_path?: string };
const resolvedPath = getFilePath(file_path);
const content = await readFileOrEmpty(resolvedPath);
if (!content) {
return {
content: [
{
type: "text",
text: `Glossary file not found or empty: ${resolvedPath}`,
},
],
};
}
const terms = extractAllTerms(content);
if (terms.length === 0) {
return {
content: [
{
type: "text",
text: "No terms found in glossary.",
},
],
};
}
return {
content: [
{
type: "text",
text: `Glossary contains ${terms.length} terms:\n${terms.map((t) => `- ${t}`).join("\n")}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
if (defaultGlossaryPath) {
console.error(
`Obsidian Dictionary MCP server running on stdio (default glossary: ${defaultGlossaryPath})`
);
} else {
console.error(
"Obsidian Dictionary MCP server running on stdio (no default glossary path configured)"
);
}
}
main().catch(console.error);