Skip to content

Enhancement: Add automatic markdown formatting to Apple Notes MCP server #2290

Open
@Malagadrive

Description

@Malagadrive

Is your feature request related to a problem? Please describe.
When creating notes through Claude using markdown syntax (headers, bullet points, bold text, checkboxes), the content appears as raw markdown in Apple Notes instead of properly formatted text. Users have to either write HTML manually or create notes with poor formatting. This makes the note creation workflow less intuitive and productive.

Describe the solution you'd like
Add automatic markdown-to-HTML conversion in the Apple Notes MCP server before content is sent to Apple Notes. The server should detect markdown patterns (# headers, bold, - bullets, - [ ] checkboxes) and convert them to appropriate HTML formatting that Apple Notes can display properly. Checkbox syntax (- [ ] and - [x]) should convert to visual symbols (☐ ☑) that can easily be highlighted and converted to interactive Apple Notes checklists.

Describe alternatives you've considered

  1. Writing HTML manually - verbose and error-prone
  2. Using external markdown converters - breaks workflow integration
  3. Creating notes without formatting - results in poor readability
  4. UI scripting to format after creation - unreliable and complex

Additional context
I have asked Claude to fix the issue with me and they came up with and implemented this enhancement by modifying the built-in Apple Notes MCP server. The solution adds a formatContentAsHTML() function that:

  • Detects markdown patterns intelligently
  • Converts headers (# ## ###) to proper HTML
  • Handles bullet points and numbered lists
  • Converts - [ ] to ☐ and - [x] to ☑ symbols for easy checklist conversion
  • Preserves existing HTML and non-markdown content
  • Works seamlessly with Apple Notes' native checklist features

This function would be called in both "add_note" and "update_note_content" functions before the content is passed to AppleScript. The implementation is backward compatible and significantly improves the note-taking workflow.

Here's the implementation:

function formatContentAsHTML(content) {
  if (!content || typeof content !== 'string') {
    return content;
  }
  
  // Only auto-format if content contains markdown-style patterns
  const hasMarkdownPatterns = /^#{1,6}\s|^\*\s|^-\s|\*\*.*\*\*|^\d+\.\s|^-\s*\[[ x]\]/m.test(content);
  
  if (!hasMarkdownPatterns) {
    return content;
  }
  
  let formatted = content;
  
  // Convert headers
  formatted = formatted.replace(/^# (.*$)/gm, '<div><h1>$1</h1></div>');
  formatted = formatted.replace(/^## (.*$)/gm, '<div><h2>$1</h2></div>');
  formatted = formatted.replace(/^### (.*$)/gm, '<div><h3>$1</h3></div>');
  
  // Convert bold text
  formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  
  // Convert markdown checkboxes to symbols before processing other lists
  formatted = formatted.replace(/^-\s*\[\s*\]\s*(.*)$/gm, '☐ $1');
  formatted = formatted.replace(/^-\s*\[x\]\s*(.*)$/gmi, '☑ $1');
  
  // Convert bullet lists (but skip checkbox items we just converted)
  const lines = formatted.split('\n');
  let inList = false;
  let result = [];
  
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const isBullet = /^[\*\-]\s(.*)/.test(line) && !/^[]/.test(line.trim());
    const isCheckbox = /^[]/.test(line.trim());
    
    if (isBullet) {
      if (!inList) {
        result.push('<ul>');
        inList = true;
      }
      const content = line.replace(/^[\*\-]\s(.*)/, '$1');
      result.push(`<li>${content}</li>`);
    } else if (isCheckbox) {
      if (inList) {
        result.push('</ul>');
        inList = false;
      }
      result.push(`<div>${line}</div>`);
    } else {
      if (inList) {
        result.push('</ul>');
        inList = false;
      }
      result.push(line);
    }
  }
  
  if (inList) {
    result.push('</ul>');
  }
  
  // Convert numbered lists
  formatted = result.join('\n');
  const numberedLines = formatted.split('\n');
  inList = false;
  result = [];
  
  for (let i = 0; i < numberedLines.length; i++) {
    const line = numberedLines[i];
    const isNumbered = /^\d+\.\s(.*)/.test(line);
    
    if (isNumbered) {
      if (!inList) {
        result.push('<ol>');
        inList = true;
      }
      const content = line.replace(/^\d+\.\s(.*)/, '$1');
      result.push(`<li>${content}</li>`);
    } else {
      if (inList) {
        result.push('</ol>');
        inList = false;
      }
      result.push(line);
    }
  }
  
  if (inList) {
    result.push('</ol>');
  }
  
  return result.join('\n');
}`

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions