The API token read every GET endpoint. It now reads only the scopes its user allows
(search, models, dashboard, activity, collections, tags, categories, authors, images,
model files, imports, everything else), set beside the MCP tools in a sectioned Token
permissions dialog. Existing and new tokens get every scope, so integrations such as
Homarr and holm keep working; a refused request names the scope it needs.
Thinking comes out of max_tokens, so even low effort could use up a description's budget and cut the answer off. Each effort now adds room on top of the answer, and the description budget allows for URLs and escaped HTML.
The zip writer stores entry names as UTF-8 but did not set the UTF-8 flag
(general purpose bit 11), so readers decoded them as CP437 or Latin-1.
Any model, category or file name with a non-ASCII character came out
garbled, including when the zip was imported back into Thingport.
The AI description check started as soon as the model was created, before
its pictures were attached and its description's images stored locally.
Storing them rewrites the description, so a suggestion that came back
after that was dropped and logged as unchanged. Imports now start the
check when they finish; uploads still start it straight away.
Some MakerWorld listings have no print profiles, only the STL/CAD files
behind "Download STL/CAD files". The backend now downloads those as the
page's own zip and unpacks every model file onto the model, and a zip
Grab passes on is unpacked the same way instead of being stored whole.
Every-profile and queued imports take the raw files instead of failing.
Testers who want changes before a release can point at :dev. :latest and
the version tags still only come from releases on main. Dev images carry
RELEASE_CHANNEL=dev so the update checker compares them with develop.
- Reject triangle indices outside the vertex list instead of stalling the queue
- Leave degenerate triangles out of body bounding boxes
- Skip the quality fetch without a print id and never show a previous print's result
- Disable the per-check switches while a save is in flight
Follow-up to the review of the MCP endpoint, after merging develop.
Backend
- Instance-wide MCP access an admin sets (Off / Read only / Read and
change, default Read and change), at GET/POST /settings/mcp and logged
as mcp_access_changed. Off turns every token away at /api/mcp; Read
only offers and answers only the tools that read, refuses turning a
write tool on, and keeps a token's earlier write choices so they come
back with Read and change. GET /profile/api-token carries mcp_access
and only the tools it offers.
- Model links come from the public URL an admin saved, not the
PUBLIC_URL env var it's only seeded from.
- An import refuses a category that isn't the user's own, for POST
/import and import_model alike.
- add_to_collection inserts and logs only the models it actually adds.
- get_import_status refuses a task the web app started instead of
failing on its result.
- The migration is now 20261030120000_api_token_mcp_tools, after every
migration on develop and on open branches.
- Tests for each access level, the category check, a web app task id
and the logged count.
Frontend
- The token's MCP tools move off the profile into a Token permissions
dialog behind a cog beside the token, with MCP as its first section
so the REST API's permissions can join it later.
- Each tool is a row: a small CSS tile picturing what it does (moving
only while its row is hovered or focused, still for reduced motion,
greyed when denied), its name and description, and a Deny / Allow
Segmented control. Tools are grouped by whether they read or change
the library.
- Admin Auth page: an AI assistants (MCP) section to set the access.
- The connect command gets a copy button and is hidden while MCP is off.
- Regenerating says the token's tools go back to the read-only ones.
- Save errors go through toasts.
- Spanish and French strings for every MCP key, and the new keys in all
four languages.
Docs
- mcp.md and api-token.md: the Token permissions dialog, the admin
setting, the regenerate reset, and what an assistant reading scraped
descriptions could be talked into.
Closes#37. The first time a model is opened, each of its STL, OBJ and 3MF
files is checked in the render worker for holes, non-manifold edges, flipped
or inside-out faces, loose fragments, missing 3MF parts, damaged files and an
unlikely size. The rules follow ADMesh and Blender's 3D Print Toolbox.
The model page shows a scan overlay while it runs, then a tick or a
severity-coloured warning beside 3D Preview that opens a report with an
animated tile per problem. Results are saved per plate and rechecked only
when QUALITY_CHECK_VERSION changes.
Admins can switch checks off as a whole or per problem under Rendering;
severity and switched-off problems apply when results are read, so neither
needs a recheck. Checks are on by default.
A real library of 488 models has 4,306 tags: the whole list came to 63 KB,
more of an assistant's context than one answer should take. It now
returns the most used 200 by default, with how many models have each,
and takes a query to narrow them.
import_model imports a link through the same checks, log and
notification as + Add > Import, optionally into a collection, and hands
an import still running after 25 seconds over to get_import_status.
create_collection and add_to_collection make and fill collections. None
deletes or overwrites anything.
They're off for every token until its user turns them on. import_model
skips the captcha, as Thingport Grab does: an assistant has nowhere to
show one, and the user chose to give it the tool.
POST /import's checks and its log and notification, creating a
collection and adding models to one were written in the routes. They're
now services the routes call, so other callers run the same checks and
leave the same trail.
Collections sort by newest, name or most models; tags gain a newest sort. Both pages get select mode with merge, bookmark and delete. Merging asks which one to keep and previews the result: the kept one's new model count, what is deleted, a bookmark moving over and syncs that stop. Built-in collections sit select mode out.
An AI assistant connects over Streamable HTTP (stateless) with the user's
API token and gets read-only tools: search_models, list_models,
get_model, list_collections, get_collection, list_tags,
list_categories, get_author and library_stats. Each runs as the token's
user through the same services as the REST API.
Every MCP message is a POST, which requireAuth refuses to an API token,
so /api/mcp checks the token itself and is mounted before the routers
that apply requireAuth to every path. The token stays read-only over
REST; over MCP it only reaches the tools its user ticked, stored per
token in User.apiTokenMcpTools. Existing tokens get the read-only tools;
a tool that writes would be off for every token until its user turns it
on.
get_model reads through printOutById rather than GET /print/:id, so an
assistant looking a model up doesn't count as a view or land in
Browsing History.
GET /prints and GET /tags built their query from the request itself. The
same listing is now a function of a user and plain filters, so other
callers list exactly what the library page lists for the same filters.
Gallery and description images, plate thumbnails and renders now live in an
Images folder next to the model's files (Print.assetsPath), and supporting and
prepared files sit beside the plates, named through the storage template. They
move with the model when the storage structure changes, and the storage browser
lists them by kind.
Models from before keep working: reads fall back to the old shared folders, and
the first start runs the storage move to bring their files over, then clears
what's left in those folders.
Appearance gains a render colour that tints generated renders, the dashboard lists, tag tiles and the 3D preview.
It follows the accent until picked apart, and offers the accents plus an untinted light grey. A graphite accent
joins the palette, light grey in the dark theme so it stays visible. The hover slideshow on model cards can be
turned off, and the language picker moves to the top.
The dashboard's own Thumbnails option is gone: its lists and the tag tiles now lead with pictures or renders as
the model cards do, tags from a mosaic of the models' renders. The one-colour dashboard icon is no longer made.
Retry thumbnail adoption for a cached render, reset card and model-page
render state per print, scope the card-preview choice to the signed-in
account and put it back when saving fails.
Model cards can lead with the model's own pictures (default) or with our render, a choice saved per user in Appearance. The server draws each model once in grey shading on transparent, from the first file it can draw, simplifying oversized meshes and viewing flat models from above; the browser tints it with the accent, so every theme and accent shows at once. The model page's 3D preview offers the accent palette and opens in the chosen accent.
Appearance gains an accent picker: Thingport green plus colors from provider brands (Thingiverse, Printables, Thangs, MyMiniFactory, Cults3D). The choice is stored per user beside the theme and recolors the app, the logo and its seasonal overlays at once.
A select toggle above each model grid (models, collections, tags, search,
author) switches clicks from opening a card to selecting it. The selected
models can then be added to or removed from a collection, favourited or
unfavourited, downloaded as one zip, deleted, or sent to AI categorization
or description improvement.
The AI actions start a single library run limited to the selected models,
so they get the run's progress bar and one summary notification instead
of one provider call and notification per model.
Closes#91
Cults3D API requires each model's owner's own API key. Move credential checks
into the lookup loop to detect unavailable keys earlier, and restructure the
selection query to prefer models from owners with configured API keys.
Imports Cults3D models, anyone's likes and the user's own printlists, and
syncs them. Metadata comes from Cults3D's GraphQL API with each user's own
nickname and API key. The API never hands out files, and the site's pages are
behind a Cloudflare check only a browser passes, so Thingport Grab sends the
browser's _session_id and cf_clearance cookies with its User-Agent; with
them the server orders a free model for nothing, as its Download button
does, and follows the download page to the signed file link (CloudFront, or
Cults3D's Scaleway bucket for older models). Paid models download once
bought. A Cloudflare block defers a sync until the next visit with Grab.
- Hide API keys from error messages by stripping query strings
- Validate that category responses contain entries before using them
- Distinguish between unreadable and malformed cookie values with proper types
Each provider gets a Manage button that opens its categories as chips, by name, instead of a field of ids. The names come from a catalog checked in with the app, refreshed by backend/scripts/syncProviderCategories.ts. Stored values keep their shape.
Imports Thangs models, likes and collections, and syncs them. Metadata is
public; files need the user's own sign-in, which Thingport Grab sends and
Thingport renews from the refresh token. Paid models download once owned;
print-only ones are refused before any download. Free accounts' daily
download limit stops a batch and defers the rest of a sync to its next check.
If the plate database update fails after moving a file, the file's
location no longer matches the stored path. Wrap the update in a
try-catch to restore the file to its original location on failure,
so the stored path still finds it.
Profile files often come named alike, so the files on a model read the
same. A profile whose file clashes with another profile's is now named
after its profile, or by its title alone when that already holds the
file's name; profiles with the same title keep the site's name.
Each profile's own picture now becomes its file's thumbnail on Snapmaker
Space, MakerOnline, Creality Cloud and NexPrint too, not just MakerWorld.
Earlier profiles are renamed before the new file is placed, and a preview
render that loses its file to a rename retries from where it went.