The MCP server and the HTTP API run the same code. A request means the same thing on either one, and both return the same plans and links. Neither generates files: a response ends in a studio link, and the model is generated and exported in the browser that opens it.
The project request
A request describes the model in physical terms. Only requestVersion and area are required. Every other field falls back to the studio's default, and unknown fields are rejected. The MCP tools assume requestVersion: 1 when it is left out; the HTTP routes require it. The JSON Schema is embedded in the OpenAPI document and is also served as the MCP resource topostack://schema/project-request-v1.
{
"requestVersion": 1,
"area": {
"center": {
"lat": 39.09,
"lon": -120.04
},
"widthKm": 30
},
"placeLabel": "Lake Tahoe, California and Nevada",
"output": "layered",
"widthMm": 400,
"heightMm": 300,
"materialThicknessMm": 3,
"verticalExaggeration": 2,
"details": {
"roads": false,
"boundaries": true
},
"title": "LAKE TAHOE\n39.09° N 120.04° W",
"laser": {
"kerfMm": 0.15,
"workAreaWidthMm": 400,
"workAreaHeightMm": 400
},
"markers": [
{
"lat": 38.9533,
"lon": -119.9442,
"name": "South Lake Tahoe",
"symbol": "star"
}
]
} | Field | Range or options | Default | Notes |
|---|---|---|---|
requestVersion | 1 | Required on HTTP routes | |
area | { center: { lat, lon }, widthKm } or { bounds: { west, south, east, north } } | See the notes below. | |
placeLabel | Up to 240 characters | Custom coordinates | Shown in the studio, for example a label from place search |
name | Up to 120 characters | The part of placeLabel before the first comma | Also names the exported files |
output | layered, flat | layered | A stack of cut sheets, or contour lines engraved on one sheet |
widthMm, heightMm | 20–10,000 | 300 × 200 | The finished model |
shape | rectangle, circle | rectangle | |
units | metric, imperial | metric | For engraved labels and the scale bar; files are always in millimeters |
materialThicknessMm | 0.5–25 | 3 | Layered. Thicker material gives fewer, coarser layers. |
verticalExaggeration | 1–10 | 2 | Layered. It is refitted so the stack is a whole number of sheets. |
contourCount | Whole number, 4–40 | 12 | Flat. Engraved contour lines across the elevation range. |
details | water, waterDepth, roads, trails, roadLabels, boundaries, coordinateGrid, elevationLabels, northArrow, scaleBar (true or false) | On: water, water depth, roads, trails, elevation labels, north arrow, scale bar. Off: road labels, boundaries, coordinate grid. | Keys left out keep their default |
title | Up to 3 lines of 40 characters, separated by \n | None | An empty string removes the title |
laser.kerfMm | 0–1 | 0.15 | Kerf compensation for the cut lines |
laser.workAreaWidthMm, laser.workAreaHeightMm | 0, or 20–10,000 | 0 (unlimited) | A model larger than the bed is split into pieces with alignment tabs |
markers | Up to 20 of { lat, lon, name?, symbol? } | None | Names up to 60 characters. Symbols: pin (default), circle, triangle, star, cross. |
Notes on the area:
widthKmis the ground distance from west to east, from 0.1 to 2,000 km.- With
bounds, the whole box stays in view, and the crop is fitted to the model's proportions. - Latitudes must lie within ±85.0511°, the limit of Web Mercator.
- An area cannot cross the antimeridian: west must be less than east.
Notes on text fields:
- Control characters, and invisible characters such as zero-width or bidirectional marks, are removed.
- Runs of whitespace are collapsed.
Fonts, line styles, custom graphics, GPS tracks and depth charts are not part of the request. The person sets them in the studio after opening the link. For what each setting does, see the studio settings reference.
Studio links
A studio link has the form https://topostack.app/studio?generate=1#p=1.<design>. The whole design is compressed into the part after #, which browsers never send to a server. Opening the link does three things:
- It loads the design as one undoable change, so Undo brings back the project that was open before.
- It starts generating straight away.
- It removes the design from the address bar, so a refresh does not generate again.
The same request always gives the same link. Links are limited to 8,000 characters. A request normally fits easily; a complete project with a lot of custom data may not, and is then refused with a message saying so.
HTTP routes
The OpenAPI 3.1 document at https://topostack.app/v1/openapi.json describes every route. POST bodies are JSON (content-type: application/json) of at most 128,000 bytes.
| Route | Takes | Returns |
|---|---|---|
POST /v1/projects/resolve | A project request | The complete studio project the request expands to, its studioUrl, and attribution |
POST /v1/projects/plan | A project request | The same plan as the plan_model tool: the project summary, the sheet count, the stack height, fitted exaggeration, scale, elevation range, coverage, notes, studioUrl and attribution |
POST /v1/projects/link | A project request, or { "project": … } holding a complete studio project | url and length |
GET /v1/coverage | ?bbox=west,south,east,north or ?lat=…&lon=…&widthKm=… | High-resolution terrain and lake surveys covering the area, with attribution |
GET /v1/geocode | ?q= (2–160 characters) and optional limit (1–8) | Place search results |
GET /v1/openapi.json | The OpenAPI document |
For example, to plan a model:
curl -s -X POST https://topostack.app/v1/projects/plan \
-H 'content-type: application/json' \
-d '{"requestVersion":1,"area":{"center":{"lat":46.8523,"lon":-121.7603},"widthKm":20},"placeLabel":"Mount Rainier"}' To make a link for a project saved from the studio, rather than for a request:
curl -s -X POST https://topostack.app/v1/projects/link \
-H 'content-type: application/json' \
-d @- <<'EOF'
{ "project": <the project from a saved TopoStack project file> }
EOF Errors
Errors are JSON with an error message. A 422 for a request also lists each invalid field:
{
"error": "The project request is invalid.",
"errors": [
{
"path": "materialThicknessMm",
"message": "Must be between 0.5 and 25."
}
]
} | Status | When |
|---|---|
400 | The body is not valid JSON, or the coverage query is malformed |
405 | The wrong method for the route |
413 | The body is over 128,000 bytes, or the design does not fit in an 8,000-character link |
415 | The body is not sent as application/json |
422 | The request or project is invalid |
429 | A rate limit, below. Retry after the retry-after seconds. On a plan, "The terrain budget for this client is used up" means the terrain tiles, not the request budget, ran out. |
502 | Terrain could not be fetched for a plan, or place search failed. Try again shortly. |
503, 504 | Place search is not configured on this server, or timed out. |
Limits
- No key or account is needed.
- The POST routes share a budget with the MCP server: 120 requests a minute per address, and a shared ceiling of 1,200 a minute.
- A plan reads at most four low-resolution terrain tiles. When those are not cached, they also count against the terrain budget.
- Place search has its own budget of 30 uncached searches a minute per address.
- Responses allow any origin, so browser code on another site can call the API.
How accurate a plan is
A plan samples coarse terrain, which smooths sharp peaks. Lake depth can also add sheets that only full generation counts. Measured against the studio, plans agree within a sheet or two, for example:
- Mount Rainier: 35 planned, 36 generated;
- Lake Tahoe: 9 and 9;
- the Matterhorn: 45 and 45.
The count the studio shows after generating is the one that is cut. Output is decorative, not survey-grade.
A plan's notes are plain sentences worth showing to the person. The first always says the count is an estimate. Others appear when the ground is nearly flat, when a layered stack passes 60 sheets, when the area reaches the sea (the sea is cut flat and the stack is sized from the land), when surveyed lake depth will add sheets below the shoreline, and when the model is larger than the laser bed, so each sheet is split into pieces with alignment tabs.
Data and credit
Every plan, link and coverage response includes an attribution object for the terrain, map and lake data it used. Keep its credit line with anything shown or passed on. The full list of sources and licenses is on the attribution page, and the privacy notice covers what the API receives.