Skip to content

The .mmap format

A map is a graph: blocks placed on an infinite canvas, joined by links. mmap.v1 is the one portable way to describe that graph. The clipboard uses it, an assistant connected over MCP uses it, and an export produces it — there is a single import path, so a map built by a script and a map built by pasting arrive the same way.

You need this page if you are generating maps from somewhere else. You do not need it to use the app, and you do not need it to connect an assistant — that already knows the format.

Shape

{
  "version": "mmap.v1",
  "kind": "fragment",
  "nodes": [],
  "edges": []
}

version must be exactly "mmap.v1". Anything else is refused.

kind is "fragment" or "board". A fragment is a batch of blocks being added to a map — which is what you almost always want. A board is a whole map, and it is what an export produces and a restore consumes.

Blocks

A node carries what the block says and where it goes: a title, an optional short description, an optional Markdown body, and a position. Give every node a ref — your own name for it, unique within the document — so that links can point at it.

Position is optional, and leaving it out is usually the right choice: with no position, the map reads order from the order you wrote the nodes in instead, and lays them out itself. If you do send one, do not send everything at the same coordinates — that is not "let the map arrange it", it is "throw away the ordering". Say what you mean, one way or the other; the map needs to know what you meant by first, second, and underneath.

Colour is not set directly. A node may declare a role — what it is in the map — and the map turns that into a colour under whatever theme its owner chose. That is why two maps built from the same document can look different and both look deliberate.

Links

An edge names each end exactly once, in one of two ways:

  • source and target, naming a ref in the same document — a link between

two blocks you are adding now.

  • sourceId and targetId, naming blocks already on the map — which is

how a map too big for one call is built branch by branch. Those ids come from reading the map first, and they are checked against it before anything is written.

A link may carry a label, a kind and a weight. Weight is the strength you are declaring, and it becomes how thick the line is drawn.

What happens on import

In order, and all of it before a single row is written:

  1. The version is checked.
  2. The document is validated. Fields we do not know are dropped, not passed

through.

  1. Every link is checked to name each end exactly once, and any block id it

names is checked against the map you are writing to.

  1. Caps are enforced: 500 blocks and 1000 links per call.
  2. Roles become colours under the map's own theme. A colour that cannot be

rendered is replaced rather than refused.

  1. Placement is resolved. A block whose spot is clear keeps it; one that would

land on top of something is moved to the nearest free slot. This never refuses the call — the reply tells you which blocks moved.

  1. The map is resolved and checked to be yours.
  2. Everything is inserted in one transaction. All of it lands, or none of it

does.

  1. You get back a map from each ref to the real id it was given, so you can

refer to what you just created.

Steps 5 and 6 are worth reading twice if a generated map has ever come out looking assembled rather than deliberate. Say what you mean — a sensible position, a role — and let the map's own settings do the rest.

The full schema

Every field a node or a link may carry, sent to the apply_map tool over MCP as its document argument.

A node:

  • ref (string, required) — your own name for this node, unique in the

document, so a link can point at it.

  • title (string, required)
  • metaDescription (string or null, optional) — one line.
  • content (string or null, optional) — the body, in Markdown.
  • parent (string, optional) — the ref of another node in this document, or

the id of a block already on the board. Drawn as a solid, structural link — a tree needs no edges array at all when every node names its parent this way.

  • position ({ x, y }, optional) — the order to seat blocks in; the map's

own arrangement decides the actual pixels. Omit it and let position come from the order you wrote nodes in instead.

  • size ({ width, height, maxHeight? }, optional) — omit it and the map

sizes the block from its role.

  • role (one of the map's block roles, optional) — what the block is; the

whole branch under it inherits the role's colour, so set it on a branch head rather than on every node.

  • color, bgColor (string or null, optional) — leave these out and let the

role and the map's theme decide.

  • blockVariant ("default" | "parent" | "portal" | "image",

default "default") — "parent" draws it as a heading for the cluster under it; "portal" links to another map and needs linkedBoardId; "image" is a picture block and needs imageId — but no tool here can set imageId to anything meaningful, since none uploads a picture, so this value only matters when you are round-tripping a document you read from get_board.

  • styleInherited (boolean, default true)
  • isActive, isTextExpanded (boolean, optional)
  • linkedBoardId (uuid or null, optional) — only inside a whole-board

document, never a fragment.

  • imageId (uuid or null, optional) — only ever a value read out of

get_board; no tool uploads a picture.

A link, naming each end exactly once:

  • ref (string, required)
  • source / target (string, optional) — a ref in this same document.
  • sourceId / targetId (uuid, optional) — a block already on the board.

Give each end either the form or the …Id form, never both, never neither.

  • label (string or null, optional)
  • kind ("default" | "goal", default "default") — "default" is

solid and structural (source contains target); "goal" is a dashed cross-reference. Reach for "goal" unless you are deliberately adding a level to the tree.

  • color (string or null, optional)
  • direction ("none" | "start" | "end" | "both", default "end")
  • weight (number, 0–10, optional) — the declared strength; becomes the

line's thickness.

A document also carries deletedNodeIds / deletedEdgeIds (arrays of uuid, optional) — ids on the board to remove before the rest of the document is inserted, in the same transaction.

A worked example: two blocks and a connection by id

Adding a new block to an existing map, and connecting it to a block that is already there:

{
  "version": "mmap.v1",
  "kind": "fragment",
  "nodes": [
    { "ref": "risk1", "title": "Vendor contract expires before renewal", "role": "risk" },
    { "ref": "task1", "title": "Renew the vendor contract", "parent": "risk1" }
  ],
  "edges": [
    { "ref": "e1", "source": "task1", "targetId": "3f9a1c2e-…", "kind": "goal", "label": "mitigates" }
  ]
}

risk1 and task1 are created in this call and wired to each other by their document-local refs, with task1 hanging under risk1 through parent. The new edge e1 then reaches out to 3f9a1c2e-…, a block id read from an earlier get_board call — the "attach to what is already on the map" form, which is how a map too large for one call is built branch by branch.

Building versus editing

Sending a document is for building: adding a batch of new, related blocks and links at once. It is insert-only.

For editing something that already exists — one field on one block, one field on one link, or a single new connection between two blocks that both already exist — use the single-item operations instead. They take a real id and touch exactly the one thing you name. Never delete a cluster and rebuild it to change one title.

Caps, and what they are for

500 blocks and 1000 links per call. They bound one call, not one map — a large map is built in several calls, which is what the "attach to a block already on the map" form of a link exists for.

What this format does not do

mmap.v1 is open JSON and there is no other export target: no direct Obsidian vault, no Notion database, no proprietary sync format, and none is planned. The format being open is the answer to that — anything that can read JSON can read a map, so a converter for a specific tool is something you can write yourself against the shape above rather than something you have to wait for us to build.

Where the authoritative rules live

A connected assistant can ask for the full writing guide at any time with the list_docs and get_doc tools (MCP page) — overview, blocks, links, layout, style, constraints, and a worked example. That corpus is the rulebook this page summarises, and it is the one that is kept in step with the importer.