Skip to main content
Glama
index.ts13.3 kB
import { z } from 'zod'; import { renderMarkdown, generateTableOfContents, extractHeadings, extractFrontmatter } from '../utils/markdownUtils.js'; import { searchNotes, searchByTags, searchByDateRange } from '../utils/searchUtils.js'; import { getDirectoryStats, findContentFiles, readContentFile } from '../utils/fileUtils.js'; // Tool argument schemas with Zod validation export const RenderMarkdownArgsSchema = z.object({ content: z.string().min(1, 'Content cannot be empty'), generateToc: z.boolean().default(true), sanitizeHtml: z.boolean().default(true), enableCodeHighlight: z.boolean().default(true) }); export const SearchNotesArgsSchema = z.object({ query: z.string().min(1, 'Query cannot be empty'), directory: z.string().optional(), includeContent: z.boolean().default(true), maxResults: z.number().int().positive().default(10), fuzzy: z.boolean().default(true) }); export const SearchByTagsArgsSchema = z.object({ tags: z.array(z.string()).min(1, 'At least one tag is required'), directory: z.string().default(process.cwd()) }); export const SearchByDateRangeArgsSchema = z.object({ startDate: z.string().refine((val) => !isNaN(Date.parse(val)), 'Invalid start date'), endDate: z.string().refine((val) => !isNaN(Date.parse(val)), 'Invalid end date'), directory: z.string().default(process.cwd()) }); export const GenerateTableOfContentsArgsSchema = z.object({ content: z.string().min(1, 'Content cannot be empty') }); export const ExtractHeadingsArgsSchema = z.object({ content: z.string().min(1, 'Content cannot be empty') }); export const ExtractFrontmatterArgsSchema = z.object({ content: z.string().min(1, 'Content cannot be empty') }); export const GetDirectoryStatsArgsSchema = z.object({ directory: z.string().default(process.cwd()) }); export const FindContentFilesArgsSchema = z.object({ directory: z.string().default(process.cwd()), extensions: z.array(z.string()).default(['.md', '.markdown', '.txt', '.mdx']) }); export const ReadContentFileArgsSchema = z.object({ filePath: z.string().min(1, 'File path cannot be empty') }); // Tool implementations export const tools = { render_markdown: { description: 'Render Markdown content to HTML with optional table of contents generation and HTML sanitization', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The Markdown content to render' }, generateToc: { type: 'boolean', description: 'Whether to generate a table of contents', default: true }, sanitizeHtml: { type: 'boolean', description: 'Whether to sanitize the resulting HTML', default: true }, enableCodeHighlight: { type: 'boolean', description: 'Whether to enable code syntax highlighting', default: true } }, required: ['content'] }, handler: async (args: any) => { const { content, generateToc, sanitizeHtml, enableCodeHighlight } = RenderMarkdownArgsSchema.parse(args); const html = renderMarkdown(content, { generateToc, sanitizeHtml, enableCodeHighlight }); return { content: [{ type: 'text', text: html }] }; } }, search_notes: { description: 'Search through notes and documents using fuzzy or exact matching', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The search query' }, directory: { type: 'string', description: 'The directory to search in (defaults to current directory)' }, includeContent: { type: 'boolean', description: 'Whether to include file content in results', default: true }, maxResults: { type: 'number', description: 'Maximum number of results to return', default: 10 }, fuzzy: { type: 'boolean', description: 'Whether to use fuzzy search', default: true } }, required: ['query'] }, handler: async (args: any) => { const searchOptions = SearchNotesArgsSchema.parse(args); const results = await searchNotes(searchOptions); const formattedResults = results.map((result, index) => { const { file, score, matches } = result; return `**Result ${index + 1}** (Score: ${(score * 100).toFixed(1)}%) **File:** ${file.name} **Path:** ${file.path} **Last Modified:** ${file.lastModified.toISOString()} **Matches:** ${matches.map(match => `- ${match}`).join('\\n')} ${searchOptions.includeContent && file.content ? `\\n**Content Preview:**\\n${file.content.slice(0, 200)}...` : ''} ---`; }).join('\\n\\n'); return { content: [{ type: 'text', text: results.length > 0 ? formattedResults : 'No results found for the given query.' }] }; } }, search_by_tags: { description: 'Search for files by tags in their frontmatter', inputSchema: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' }, description: 'Array of tags to search for' }, directory: { type: 'string', description: 'The directory to search in', default: process.cwd() } }, required: ['tags'] }, handler: async (args: any) => { const { tags, directory } = SearchByTagsArgsSchema.parse(args); const results = await searchByTags(directory, tags); if (results.length === 0) { return { content: [{ type: 'text', text: `No files found with tags: ${tags.join(', ')}` }] }; } const formattedResults = results.map((file, index) => { const fileTags = file.frontmatter?.tags || []; return `**${index + 1}. ${file.name}** Path: ${file.path} Tags: ${Array.isArray(fileTags) ? fileTags.join(', ') : fileTags} Last Modified: ${file.lastModified.toISOString()} Size: ${file.size} bytes`; }).join('\\n\\n'); return { content: [{ type: 'text', text: formattedResults }] }; } }, search_by_date_range: { description: 'Search for files modified within a specific date range', inputSchema: { type: 'object', properties: { startDate: { type: 'string', description: 'Start date in ISO format (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date in ISO format (YYYY-MM-DD)' }, directory: { type: 'string', description: 'The directory to search in', default: process.cwd() } }, required: ['startDate', 'endDate'] }, handler: async (args: any) => { const { startDate, endDate, directory } = SearchByDateRangeArgsSchema.parse(args); const results = await searchByDateRange( directory, new Date(startDate), new Date(endDate) ); if (results.length === 0) { return { content: [{ type: 'text', text: `No files found between ${startDate} and ${endDate}` }] }; } const formattedResults = results.map((file, index) => { return `**${index + 1}. ${file.name}** Path: ${file.path} Last Modified: ${file.lastModified.toISOString()} Size: ${file.size} bytes`; }).join('\\n\\n'); return { content: [{ type: 'text', text: formattedResults }] }; } }, generate_table_of_contents: { description: 'Generate a table of contents from Markdown content', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The Markdown content to analyze' } }, required: ['content'] }, handler: async (args: any) => { const { content } = GenerateTableOfContentsArgsSchema.parse(args); const toc = generateTableOfContents(content); return { content: [{ type: 'text', text: toc || 'No headings found in the content.' }] }; } }, extract_headings: { description: 'Extract all headings from Markdown content', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The Markdown content to analyze' } }, required: ['content'] }, handler: async (args: any) => { const { content } = ExtractHeadingsArgsSchema.parse(args); const headings = extractHeadings(content); if (headings.length === 0) { return { content: [{ type: 'text', text: 'No headings found in the content.' }] }; } const formattedHeadings = headings.map(heading => { const indent = ' '.repeat(heading.level - 1); return `${indent}${'#'.repeat(heading.level)} ${heading.text} (id: ${heading.id})`; }).join('\\n'); return { content: [{ type: 'text', text: formattedHeadings }] }; } }, extract_frontmatter: { description: 'Extract frontmatter from Markdown content', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The Markdown content to analyze' } }, required: ['content'] }, handler: async (args: any) => { const { content } = ExtractFrontmatterArgsSchema.parse(args); const { frontmatter, content: contentWithoutFrontmatter } = extractFrontmatter(content); const result = { frontmatter, contentLength: contentWithoutFrontmatter.length, hasFrontmatter: Object.keys(frontmatter).length > 0 }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } }, get_directory_stats: { description: 'Get statistics about content files in a directory', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'The directory to analyze', default: process.cwd() } } }, handler: async (args: any) => { const { directory } = GetDirectoryStatsArgsSchema.parse(args); const stats = await getDirectoryStats(directory); const result = { directory, ...stats, lastUpdated: new Date().toISOString(), averageFileSize: stats.totalFiles > 0 ? Math.round(stats.totalSize / stats.totalFiles) : 0 }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } }, find_content_files: { description: 'Find all content files in a directory with specific extensions', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'The directory to search in', default: process.cwd() }, extensions: { type: 'array', items: { type: 'string' }, description: 'File extensions to search for', default: ['.md', '.markdown', '.txt', '.mdx'] } } }, handler: async (args: any) => { const { directory, extensions } = FindContentFilesArgsSchema.parse(args); const files = await findContentFiles(directory, extensions); if (files.length === 0) { return { content: [{ type: 'text', text: `No files found with extensions: ${extensions.join(', ')}` }] }; } const formattedFiles = files.map((file, index) => `${index + 1}. ${file}` ).join('\\n'); return { content: [{ type: 'text', text: `Found ${files.length} files:\\n\\n${formattedFiles}` }] }; } }, read_content_file: { description: 'Read and parse a content file (supports Markdown with frontmatter)', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the file to read' } }, required: ['filePath'] }, handler: async (args: any) => { const { filePath } = ReadContentFileArgsSchema.parse(args); const file = await readContentFile(filePath); const result = { name: file.name, path: file.path, size: file.size, lastModified: file.lastModified.toISOString(), hasFrontmatter: !!file.frontmatter && Object.keys(file.frontmatter).length > 0, frontmatter: file.frontmatter, contentPreview: file.content.slice(0, 500) + (file.content.length > 500 ? '...' : ''), contentLength: file.content.length }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } } } as const;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Talljack/content-manager-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server