Skip to content

send_email / save_draft / forward_email break ALL tool calling on llama.cpp backends: zod 4 .email() emits a lookahead regex GBNF cannot compile #58

Description

@Fe2-O3

Summary

send_email, save_draft and forward_email make every tool call fail on any llama.cpp-backed local model (LM Studio, Ollama, llama-server). The whole request is rejected in ~0.14s with zero tokens generated:

invalid_request_error: Failed to initialize samplers: failed to parse grammar

One of these three tools anywhere in the tools array poisons the request, so the other 46 email tools become unusable too — and so does every non-email tool in the same array.

Cause

These three tools declare recipients with z.string().email():

  • src/tools/send.tool.tsto / cc / bcc on send_email, and to / cc on forward_email
  • src/tools/drafts.tool.tsto / cc / bcc on save_draft

With zod@^4.3.6, .email() emits its default (Gmail-style) regex into the JSON Schema pattern, and that regex contains negative lookaheads:

^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

llama.cpp compiles tool schemas to GBNF for constrained decoding, and GBNF has no lookahead assertions, so grammar compilation aborts before inference starts.

This is not local-only. The same schema is rejected by OpenAI's tool-schema validator with Invalid JSON schema: regex lookaround is not supported — see SmartBear/smartbear-mcp#491, which is the identical bug in a different MCP server.

Reproduction

10/10 deterministic against LM Studio behind an OpenAI-compatible gateway. Field-isolated on a single property:

schema result
{"type":"string"} ok
{"type":"string","format":"email"} ok — format is ignored
{"type":"string","pattern":"^(?!\\.)(?!.*\\.\\.)..."} fail

And by bisecting a real 76-tool array (76 → 38 → 19 → 10 → 5 → 2 → 1):

tools sent result
all 76 fail
76 minus these 3 ok
76 minus all 49 email tools ok

Suggested fix

Zod 4 ships lookahead-free alternatives, so this is a drop-in at the five call sites:

-  to: z.array(z.string().email()).min(1).describe('Recipient email addresses'),
+  to: z.array(z.email({ pattern: z.regexes.html5Email })).min(1).describe('Recipient email addresses'),

z.regexes.html5Email is the regex browsers use for input[type=email] and has no lookarounds. z.regexes.rfc5322Email also works if you prefer stricter parsing.

Alternatively, plain z.string() at these call sites: for an MCP input schema the pattern is advisory to the model rather than a real validator, and malformed addresses are rejected by the SMTP layer regardless.

Either way the constraint should not be expressed as a lookahead regex, because two major tool-calling runtimes now refuse to compile it.

Environment

  • @codefuturist/email-mcp@0.2.0, zod@^4.3.6
  • macOS 27.0, Node via npm global
  • Clients: OpenCode 1.18.3 → Bifrost gateway → LM Studio

Happy to open a PR if the z.regexes.html5Email direction looks right to you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions