prerequisite
JSON
The text format every MCP message is written in: objects, arrays, strings, numbers, booleans, null, and nesting.
Before this
Nothing beyond first-year college math. This is a starting page.
Why you need this
Every message an MCP host and server exchange is a piece of JSON text: the request, the result, the error, the tool's description, and the schema of its arguments. If you can read JSON fluently, you can read the protocol directly, which is the fastest way to debug it. Everything else in this cluster assumes you can.
The idea
JSON (JavaScript Object Notation) is a way to write structured data as plain text. It came from JavaScript, but every mainstream language can read and write it. A program turns its in-memory data into JSON text to send it somewhere (this is called serializing), and the receiver turns the text back into data (parsing).
A JSON document is exactly one value. There are six kinds of value:
| Kind | Looks like | Notes |
|---|---|---|
| string | "Oslo" |
Always double quotes. Single quotes are not JSON. |
| number | 11, -3.5, 1.25e3 |
One number type. No NaN, no infinity, no leading +. |
| boolean | true, false |
Lower case. |
| null | null |
"There is a value here, and it is nothing." |
| array | ["chem", "week-3"] |
An ordered list of values, separated by commas. |
| object | {"name": "Ana"} |
A set of key/value pairs. Keys are always strings. |
Arrays and objects can hold any values, including other arrays and objects. That is nesting, and it is how JSON describes anything bigger than a single fact. An object inside an object inside an array is normal.
Objects and keys
An object is written with curly braces. Each entry is a key in double quotes, a colon, and a value. Entries are separated by commas, and the last entry has no trailing comma. The order of keys carries no meaning: {"a": 1, "b": 2} and {"b": 2, "a": 1} are the same object. The JSON standard says keys should be unique but does not forbid duplicates, and parsers disagree about what to do with them, so never send them.
Strings and escaping
A string sits between double quotes, so a double quote inside it needs a backslash escape: \". The same goes for the backslash itself (\\) and for control characters such as a newline (\n) or tab (\t). Any Unicode character can be written directly, or as \u followed by four hex digits.
So the title Lab report "draft" is written in JSON as:
"Lab report \"draft\""
This matters for MCP more than it seems: on the stdio transport each message must sit on one line, so a newline inside a string has to travel as the two characters \n, never as a real line break.
Numbers: one type, not two
Python and many other languages separate whole numbers (int) from numbers with a fractional part (float). JSON does not. 1250, 1250.0, and 1.25e3 (scientific notation for ) are three spellings of the same number. The receiving language decides how to store it. Python's parser keeps 1250 as an int and turns the other two into a float, but a JavaScript program stores all three as the same floating-point value.
Two practical consequences. First, do not rely on how a number is spelled to carry meaning: a schema that wants a whole number checks the value, not the decimal point. Second, very large integers can lose precision in readers that use floating point, which can only represent every whole number exactly up to (about 9 quadrillion). Long numeric IDs are safer sent as strings.
Null versus a missing key
These two objects say different things:
{"name": "Ana", "email": null}
{"name": "Ana"}
The first says "this person has an email field, and it is empty". The second says nothing about email at all. Some programs treat them the same; many do not. A server that checks "was an email provided?" by looking for the key will see the first object as providing one. When you design a message, decide which one you mean and write it down.
Worked example
Here is a note as a server might return it. Save it as note.json.
{
"id": "n-42",
"title": "Lab report \"draft\"",
"pinned": false,
"words": 1250,
"rating": 4.5,
"tags": ["chem", "week-3"],
"author": {"name": "Ana", "email": null},
"attachments": []
}
Read it by hand first. Each answer gives the path you followed, one key or index at a time. Array indexes start at 0.
| Question | Path | Answer |
|---|---|---|
| How many top-level keys? | count them | 8 |
| What is the title, as a person would read it? | title |
Lab report "draft" (the backslashes are only escapes) |
| What is the second tag? | tags, then index 1 |
"week-3" |
| How many attachments? | attachments |
0. It is an empty array, not a missing key. |
| Who wrote it? | author, then name |
"Ana" |
| Does the author have an email? | author, then email |
The key is present, and its value is null. |
| When is it due? | due |
There is no due key. That is different from "due": null. |
Now the same reading in Python. json.load parses a file, json.loads parses a string, and json.dumps turns Python data back into JSON text. JSON objects become Python dicts, arrays become lists, true/false become True/False, and null becomes None.
import json
with open("note.json", encoding="utf-8") as f:
note = json.load(f)
print(type(note).__name__, len(note), "keys")
print(note["title"])
print(note["tags"][1], len(note["attachments"]))
print(note["author"]["name"])
print(note["author"]["email"] is None, "email" in note["author"])
print("due" in note, note.get("due", "no due date"))
print(json.loads("[1250, 1250.0, 1.25e3]"))
print(json.dumps(note["author"]))
Run it from the folder that holds both files with python read_note.py. It prints:
dict 8 keys
Lab report "draft"
week-3 0
Ana
True True
False no due date
[1250, 1250.0, 1250.0]
{"name": "Ana", "email": null}
Line five shows null versus missing in code: the email is None, and the key is still there. Line six shows the safe way to read a key that might be absent: note["due"] would raise a KeyError, while note.get("due", default) returns the default. Line seven shows one JSON number type becoming two Python types.
In a server's life
JSON is underneath stage 1, speak the protocol: every MCP message is one JSON object. It shows up again in stage 2, design the surface, because a tool's arguments arrive as a JSON object and its structured result leaves as one, and in stage 6, maintain it, where adding, removing, or retyping a key in that JSON is exactly what can break a client. The next two layers, JSON-RPC and JSON Schema, are both written in JSON.
Common mistakes
- Single quotes or a trailing comma.
{'a': 1,}is valid Python but not JSON. The parser on the other side rejects the whole message, and an MCP server answers with a parse error instead of doing anything. - Printing a Python dict instead of serializing it.
print(data)writes{'ok': True, 'x': None}, which is not JSON. Always usejson.dumps. - A raw newline inside a string on stdio. A pretty-printed message (one that spans several lines) splits into fragments and the receiver sees broken JSON. Send one compact line per message;
json.dumpswithoutindentdoes this. - Treating
nulland a missing key as the same. A client sends"units": null, the server's code checks only whether the key exists, and it then tries to useNoneas a unit. - Large IDs as numbers. A 19-digit ID survives a Python round trip but comes back with its last digits changed after passing through a JavaScript client.
Cost
JSON is text, so it is larger than a binary format: every key name is repeated in every object, and numbers are spelled out digit by digit. For MCP this matters in one place more than any other: tool definitions and tool results are fed to a language model, which pays for every character it reads (see How language models use tools). Parsing is fast, roughly linear in the length of the text, and is never the slow part of a request that calls a network service.
Going further
- JSON Schema, for describing which JSON values a program will accept.
- JSON-RPC, the request and response shapes built out of JSON objects.
- JSON Lines (one JSON value per line), the framing the stdio transport uses.
- The JSON grammar itself, published as RFC 8259, which fits on a few pages.
Leads to
- prerequisiteHow language models use toolsWhat actually happens when a chat model 'calls a tool': tool descriptions in the context, structured output, and the host doing the real work.
- prerequisiteJSON-RPCThe request, response, notification, and error shapes MCP borrows: ids, methods, params, results, and error codes.
- prerequisiteJSON SchemaHow a schema describes what valid JSON looks like, so a model knows what arguments a tool takes and a server can reject bad ones.