Pre-flight validator for the Anthropic Messages API. Catch the structural
mistakes that make messages.create come back with a cryptic 400 — before
you spend the network round-trip (and the time debugging it).
You know these errors:
messages: roles must alternate between "user" and "assistant"
messages.1: each `tool_use` block must have a corresponding `tool_result` ...
messages.0: text content blocks must be non-empty
msglint finds all of them locally, points at the exact block, and tells you
how to fix it. Zero dependencies, no API key, no network.
- ❌ a
systemrole smuggled into themessageslist (it's a top-level param) - ❌ non-alternating roles / a conversation that doesn't start with
user - ❌ a
tool_usewith no matchingtool_resultin the next user turn (dangling) - ❌ a
tool_resultthat references atool_useid that was never declared (orphan) - ❌ duplicate
tool_useids - ❌ empty text / empty content lists (the API rejects them)
- ❌
tool_usein a user turn, ortool_resultin an assistant turn - ❌ malformed image blocks (missing
media_type, badsource.type, …) ⚠️ tool_usenaming a tool that isn't in yourtoolslist⚠️ unrecognized block types
pip install msglint
# or, from source:
git clone https://github.com/oavlloh-wq/msglint && pip install -e msglintfrom msglint import lint, format_report
messages = [
{"role": "user", "content": "weather in Paris?"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": {"city": "Paris"}}
]},
# ...forgot the tool_result here
]
issues = lint(messages, tools=[{"name": "get_weather"}])
if any(i.is_error for i in issues):
print(format_report(issues))
raise SystemExit("payload is not safe to send")Wire it into your client wrapper so a bad payload never leaves the building:
def safe_create(client, **kwargs):
issues = lint(kwargs.get("messages"), system=kwargs.get("system"),
tools=kwargs.get("tools"))
errs = [i for i in issues if i.is_error]
if errs:
raise ValueError("msglint: " + "; ".join(str(e) for e in errs))
return client.messages.create(**kwargs)msglint payload.json # lint a saved request payload
cat payload.json | msglint - # read from stdin
msglint --selftest # offline demo, no API key neededIt accepts either a bare messages list or a full request object
({"messages": [...], "system": ..., "tools": [...]}). Exit code is 1 when
there are errors, so it drops straight into CI or a pre-commit hook.
$ msglint --selftest
### An assistant tool_use with no following tool_result
msglint
[ERROR] dangling-tool-use messages[1].content[0]: tool_use id 'toolu_x' is never answered;
append a user message with a matching tool_result before sending
1 error(s), 0 warning(s)
### A tool_result that references nothing
msglint
[ERROR] orphan-tool-result messages[2].content[0]: tool_result references 'toolu_ghost',
which no preceding assistant tool_use declared
1 error(s), 0 warning(s)
### A system role smuggled into messages
msglint
[ERROR] system-role messages[0].role: 'system' is not a valid message role;
pass it as the top-level `system` parameter instead
1 error(s), 0 warning(s)
lint(messages, *, system=None, tools=None) -> list[Issue]Returns a list of Issue(level, code, path, message). An empty list means the
payload is structurally sound. Pass system/tools for extra checks (e.g. a
system role hiding in messages, or a tool_use naming an undefined tool).
format_report(issues, source=None) -> str renders them for humans.
MIT