← Back to list

Building a Handlebars/Mustache Engine with ANTL4 MCP Server

A working parser in an afternoon

Shashank Shailabh · 2026-01-01 21:15 · 50 claps · 4.0 min read
#domain-specific-languages #antlr4 #claude #mcp-server #language-development
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents

Building a Handlebars/Mustache Engine with ANTL4 MCP Server

A working parser in an afternoon

I wanted to build a templating engine.

Not use one — build one. A Mustache-style syntax that could render templates like this:

Hello, {{name}}!
{{#users}}
- {{name}} ({{email}})
{{/users}}
{{^premium}}
Upgrade to premium for more features.
{{/premium}}

The Mustache spec defines the syntax precisely. Variables, sections, inverted sections, partials, comments — all documented in YAML test cases.

Here’s how I built it using the ANTLR4 MCP Server.

Setup ANTLR4 MCP Server

Ensure Docker is running and Pull the Docker image:

docker pull sshailabh1/antlr4-mcp-server:latest

Configure Claude Desktop:

{
  "mcpServers": {
    "antlr4": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        " - rm",
        "sshailabh1/antlr4-mcp-server:latest"
      ]
    }
  }
}

Restart Claude. Done.

The Conversation

I started by describing what I needed:

> **Me:** I want to parse Mustache templates. The syntax has:
> - Variables: `{{name}}`
> - Unescaped: `{{{name}}}` or `{{&name}}`
> - Sections: `{{#items}}…{{/items}}`
> - Inverted sections: `{{^items}}…{{/items}}`
> - Comments: `{{! this is ignored }}`
> - Partials: `{{> header}}`
>
> Plain text outside tags should be captured as-is.
>
> Write a grammar with lexer modes and validate it.
**Claude:**
> I'll create a grammar using lexer modes to switch between text and tag contexts.

ANTLR4 grammar:

lexer grammar MustacheLexer;
// Default mode - capturing text
TEXT : (~[{] | '{' ~[{])+ ;
OPEN_UNESCAPED : '{{{' -> pushMode(TAG_MODE) ;
OPEN_COMMENT : '{{!' -> pushMode(COMMENT_MODE) ;
OPEN : '{{' -> pushMode(TAG_MODE) ;
mode TAG_MODE;
CLOSE_UNESCAPED : '}}}' -> popMode ;
CLOSE : '}}' -> popMode ;
HASH : '#' ;
SLASH : '/' ;
CARET : '^' ;
AMP : '&' ;
GT : '>' ;
DOT : '.' ;
ID : [a-zA-Z_][a-zA-Z0–9_.]* ;
WS_TAG : [ \t]+ -> skip ;
mode COMMENT_MODE;
COMMENT_TEXT : (~[}] | '}' ~[}])* ;
CLOSE_COMMENT : '}}' -> popMode ;

✅ Lexer grammar validates.

ANTLR4 parser:

parser grammar MustacheParser;
options { tokenVocab = MustacheLexer; }
template : element* EOF ;
element
: TEXT
| variable
| unescapedVariable
| section
| invertedSection
| partial
| comment
;
variable : OPEN ID CLOSE ;
unescapedVariable
: OPEN_UNESCAPED ID CLOSE_UNESCAPED
| OPEN AMP ID CLOSE
;
section : OPEN HASH ID CLOSE element* OPEN SLASH ID CLOSE ;
invertedSection : OPEN CARET ID CLOSE element* OPEN SLASH ID CLOSE ;
partial : OPEN GT ID CLOSE ;
comment : OPEN_COMMENT COMMENT_TEXT? CLOSE_COMMENT ;

✅ Parser grammar validates. Let me test with your template…

Input:

Hello, {{name}}!
{{#users}}
- {{name}} ({{email}})
{{/users}}
{{^premium}}
Upgrade to premium.
{{/premium}}

Parse tree:

(template
(element Hello, )
(element (variable {{ name }}))
(element !\n\n)
(element (section {{# users }}
(element \n - )
(element (variable {{ name }}))
(element ()
(element (variable {{ email }}))
(element )\n)
{{/ users }}))
…
)

✅ Parses correctly. Sections are properly nested.

Catching Edge Cases

I pushed further:

> **Me:** What about standalone tags? In Mustache spec, if a tag is alone on a line, the surrounding whitespace should be stripped.
**Claude:**
> That's a semantic rule, not a syntactic one. The grammar captures all whitespace as TEXT. You'd handle standalone detection in a post-processing visitor.
>
> However, let me check if there's any ambiguity with the current rules…
> ⚠️ Running ambiguity detection…
>
> Found: The TEXT rule `(~[{] | '{' ~[{])+` can conflict when input contains a single `{` followed by EOF. Adding boundary handling…
The MCP server caught an edge case automatically.

Handling Triple Braces

The Mustache spec supports {{{unescaped}}} — three braces for raw HTML output.

> **Me:** How do we handle `{{{name}}}` vs `{{name}}`?
**Claude:**
> In the lexer, rule order matters. `OPEN_UNESCAPED : '{{{'` is defined before `OPEN : '{{'`, so it matches first. ANTLR uses longest-match-first, so `{{{` wins over `{{`.
>
> Let me verify with a test case…
**Input:** `Hello {{{raw}}} and {{escaped}}`
**Parse tree:**

(template (element Hello ) (element (unescapedVariable {{{ raw }}})) (element and ) (element (variable {{ escaped }})) )

> ✅ Correctly distinguishes triple from double braces.

Adding Dot Notation

Mustache supports {{user.name}} for nested properties.

> **Me:** Add support for dotted paths like `{{user.address.city}}`
**Claude:**
> For proper AST building, let's make the path explicit:
```antlr
path : ID (DOT ID)* ;
variable : OPEN path CLOSE ;

✅ Validates. Testing {{user.address.city}}

Parse tree shows: (path user . address . city)

Each segment is now separately accessible in your visitor.

Generating the Parser

When satisfied:

> **Me:** Generate a Python parser with visitor support.

✅ Generated 6 files:

  • MustacheLexer.py
  • MustacheParser.py
  • MustacheListener.py
  • MustacheVisitor.py
  • MustacheLexer.interp
  • MustacheParser.interp

I implemented a simple renderer in Python:

Spec Compliance

The Mustache spec defines test cases in YAML:

- name: Basic variable
  template: "Hello, {{name}}!"
  data: { name: "World" }
  expected: "Hello, World!"

Running my implementation against the spec and passes all tests except whitespace issue ones.

Why Templating Grammars Are Interesting

Templating languages have unique challenges:

  1. Lexer modes — Switching between text and tag context

  2. Mixed content — Most input is passthrough, some is parsed

  3. Overlapping patterns — {{{ vs {{

  4. Balanced structures— Sections must match

  5. Whitespace sensitivity — Standalone tag rules

The MCP server handles these well because:

  • It validates each piece as you build

  • It catches ambiguities before they become runtime bugs

  • It tests parsing without regenerating code

  • It explains why things work

Beyond Mustache

The same approach works for any templating language:

Handlebars:

{{#each users}}
  {{#if active}}
    Hello, {{name}}!
  {{/if}}
{{/each}}

Jinja2:

{% for user in users %}
  {{ user.name | upper }}
{% endfor %}

EJS:

<% users.forEach(function(user) { %>
<li><%= user.name %></li>
<% }); %>

Each has the same fundamental challenge: mixed content with mode switching.

Try It Yourself

  1. Pull the Docker run image for MCP Server:
docker pull sshailabh1/antlr4-mcp-server:latest
  1. Add to Claude/ChatGPT/Cursor/Cline config:**
{
  "mcpServers": {
    "antlr4": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        " - rm",
        "sshailabh1/antlr4-mcp-server:latest"
      ]
    }
  }
}
  1. Start with a simple template and iterate:

“I want to parse templates with {{variable}} syntax. Write a grammar with lexer modes.”

References

  1. ANTLR4 MCP Server
  2. Mustache Spec
  3. DSL-Starter

If this helped you, give it a clap 👏 and [⭐ star the ANTLR4 MCP Server] on GitHub. Happy Coding!!


메타데이터
post_id
ef7104b3b341
slug
building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
url
https://medium.com/@shashankshailabh/building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
canonical_url
https://medium.com/@shashankshailabh/building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
author_url
https://medium.com/@shashankshailabh
status
ok
fetched_at
2026-07-13 18:19:34