3ds Max MCP — Agent Guide
Tool Profile Routing
- Full/core: Operational tools such as
query_sceneandcreate_objectare advertised directly; call the matching tool by name. - Progressive: If the advertised surface contains only
list_toolsets,describe_toolset, andcall_tool, never call an operational name as a top-level MCP tool. Choose the relevant capability withlist_toolsets, load only that group withdescribe_toolset, then invoke the selected operation throughcall_tool(name=..., arguments=...). - Do not describe every toolset up front. Load only the group needed for the current request; if the exact operational tool and arguments are already known,
call_toolcan dispatch it directly.
Principles:
- Match the user's request. Do not run setup, discovery, or scene analysis by habit.
- Do not call
get_bridge_statusorget_session_contextas a session preamble. - Prefer a dedicated MCP tool over raw MAXScript when a tool clearly matches the task.
- Do not render unless the user explicitly asks. Viewport capture is fine when visual proof is useful.
- Multiple Max instances:
list_max_instances,select_max_instance(pid),get_selected_max_instance, andrelease_max_instanceare available in every profile. The first successful native connection stays bound to that Max. Starting or claiming another Max only changes the default for unbound clients. If the selected Max closes, explicitly select another or release it; clients never silently switch.MCP_MAX_PIDorMCP_MAX_PIPEpins the startup target (MCP_MAX_PIPEtakes precedence). Release also clears startup pinning.
Tool Choice
Scene reads — use query_scene(action=...):
overview|filter|class|property|selection|deltaget_instances/get_dependencies— instancing and reference graphresolve_node_refs— turn a name, handle, or absolute JSON-Pointer hierarchy path into a canonical handle/name/path identity; multiple selectors are cross-checkedscene_qa(action="scan")— deterministic naming, transform, hierarchy/group, and timeline checks only; it never analyzes meshes, UVs, topology, normals, skinning, or visual qualityget_session_context— bridge + capabilities + overview + selection (on demand only)
Object/material/plugin inspection:
inspect_object,inspect_properties,get_material_slots,get_materials,get_material_libraryanalyze_node_orientation— pivot, bbox, local axes, world matrix before rig/vehicle/camera transformsintrospect_class,introspect_instance,introspect_osl,discover_plugin_classes,map_class_relationships— unfamiliar plugin APIs and exact param names- Arnold materials such as
ai_standard_surfacemay not appear in class discovery; inspect withinspect_plugin_classorintrospect_osl
Lighting:
lighting_capabilities→create_lights→inspect_lights→edit_lights. Choose a supported renderer route, shape and explicit output unit. A finite bulb isarea/sphere; an HDRI dome isenvironment. Never infer integer enum meanings.- Distances accept scene/mm/cm/m/in/ft. Area emitters take a complete size and an aim point or direction. RGB values are linear in the rendering color space; Kelvin is explicit. EXR/HDR inputs get no extra gamma; preserve the renderer's input primaries conversion and exposure. Existing sky maps can be bound directly.
- For other plugin settings, use
inspect_plugin_class/inspect_plugin_instancewithschema_version=2, a query or exact fields. Follow returned map references and pass schema/state tokens toplugin_patch. Named enums use{"enum":"name"}from the returned choices. Shared or animated resources require deliberate handling.
Mutation:
- Use object, modifier, material, controller, organization, and viewport tools when they match.
- Use
scene_patchfor a preflighted batch of rename, relative transform, visibility/freeze/render flags, or parenting edits that must commit as one native undo step. Pass the mutation-onlyexpected_scene_seqfromresolve_node_refswhen stale targeting matters; selection and sub-object selection do not invalidate it. - Use
scene_qa(action="fix")only for its explicit deterministic naming fixes; preview withdry_run=truewhen the caller has not already approved the repair. - Verify after meaningful edits with
query_scene(action=delta), re-inspection, or viewport capture.
Debugging:
walk_references— trace dependencies from a live objectwatch_scene— track user actions during an interactive sessionexecute_maxscript— fallback only when no dedicated tool existsexecute_python(code)— embedded Python fallback withpymxs; assign a JSON-compatibleresultto return a value alongside stdout/stderr. Requires bridge safe mode off. Calls have fresh variables and one undo step for undoable scene edits; uncaught errors roll those edits back and return a traceback. File I/O and other non-undoable effects persist.
Scene Organization
Layers — manage_layers:
- Actions:
list,create,delete,set_current,set_properties,add_objects,select_objects - Properties: hidden, frozen, renderable, color, boxMode, castShadows, rcvShadows, xRayMtl, backCull, rename, parent
Groups — manage_groups:
- Actions:
list,create,ungroup,open,close,attach,detach
Named Selection Sets — manage_selection_sets:
- Actions:
list,create,delete,select,replace
Tool Reference
Scene reads
query_scene resolve_node_refs scene_qa get_hierarchy get_instances get_dependencies
Atomic scene edits
scene_patch— accepts NodeRefs (handle,name, or JSON-Pointerpath), validates every operation before editing, rejects hierarchy/name conflicts, supports mutation-only stale-sequence guards and dry-run, and rolls back the whole native hold on apply failure; activity-only selection events remain observable without blocking writes- Node handles are stable only within the current loaded scene/session. Cross-check a cached handle with
nameand/orpath, and refresh after a scene reset/load.
Objects
get_object_properties analyze_node_orientation set_object_property create_object delete_objects transform_object select_objects set_visibility clone_objects set_parent batch_rename_objects
Modifiers
add_modifier remove_modifier set_modifier_state set_modifier_property collapse_modifier_stack make_modifier_unique
Modeling
curve_model— named local-plane curves, rounded profiles, tangent arcs, sweeps and resampled quad lofts with saved numeric controls.previewchecks locally;createsaves the recipe on one editable node;read/updateuse a model token to preserve placement and modifiers. Read curve-construction.md for recipes, supported operations and limits.inspect_curve/edit_curve— world knot/handle readback, sampled curve QA, labeled AGENT VIEWPORT capture and image targeting; atomic base edits guarded bycurve_token. Re-inspect after topology changes. See curve-construction.md.create_mesh— explicit world-space vertices and 1-based polygon faces, preserving quads/n-gons. Compute curved panels, lofted sections, and furniture geometry in Python, then send the arrays to Max.loft_mesh— matched cross sections become a quad cage with optional caps or closed path. Coordinates can use arithmetic expressions of named parameters, such aswidthorbackrest_curvature. The definition persists on the mesh in the.maxfile.readreturns compact controls (include_definition=truefor sections);updatechanges parameters while preserving placement, subdivision, and cage IDs. Manual cage edits or instanced bases block parameter updates. Keep profile ordering consistent; no automatic resampling.inspect_mesh— base-cage vertex/edge/face IDs, centers, normals, mesh token, and optional labeled viewport capture. Omit the target for the one selected mesh. Filter by IDs, current selection, world bounds/proximity, face normal, open borders, or sharp edges.pick_component— normalized image x/y plus the capture'sexpected_viewfinds editable vertex/edge/face candidates. Defaults to the hit node; pass name/handle for silhouettes. It favors correspondence to the visible surface within screen tolerance. Inspect ambiguity and surface evidence, then pass the returnedmesh_tokentomesh_edit(expected_mesh=...). Subdivision can separate the visible surface from its cage; proximity does not prove visibility.mesh_edit— batch select/move/scale/extrude/inset/bevel/chamfer/connect/bridge/delete/cap/relax in one undo step. Pass the inspectedexpected_meshtoken when reusing IDs. Omitted component filters use the current sub-object selection; useselection:{all:true}explicitly for the whole mesh. World-space moves/scales preserve modifiers above an Editable Poly base;convert=trueexplicitly collapses a non-poly stack. Shared geometry changes in every instance.geometry_qa— evaluated mesh boundaries, non-manifold edges, inconsistent winding, degenerate/duplicate triangles, components and isolated vertices. Samples use world coordinates and evaluated snapshot IDs; re-inspect the base cage near a sample before editing. No intersection, thickness or outward-normal certification. Open seams and separate components may be intentional.boolean_operation— Boolean modifier (BooleanMod): apply union/subtract/intersect/merge/attach operands, list/retune/rename/extract them. Inlinecuttersbuild scratch primitives in the same call ({name, shape: box|cylinder|sphere, size, pos (bbox center), rot, operation?}) — consumed on apply, zero scene litter;repeat{count, axis, spacing} arrays every cutter (vent_1..N) for vents/ribs/window grids. Recipes: hole = Z-axis cylinder cutter overshooting both faces (rot to orient); slot = box cutter; panel line = cutter +operation_option="imprint".draw_spline— spline shapes from world-space point lists; base-knot editing preserves modifiers above the spline (including CrossSection, Surface, and Sweep), holes via add_spline, renderable thicknessedit_vertices— Editable_Poly verts in world space: get (filtered), move (soft falloff), set, conform to a spline or ray-projected onto geometry- Curved-form recipe:
draw_splinethe reference profile → Lathe/Extrude/Bevel_Profile/Sweep viaadd_modifier→ refine withset_knotsoredit_vertices conform
Materials
- Create + assign:
assign_material,create_material_from_textures,smart_import,palette_laydown - Share an existing material:
assign_material(names=[...], source_name="ExistingObject")(orsource_handle). Shares the full material/maps in one undo step; omit creation arguments. - Edit:
set_material_property,set_material_properties - Inspect:
get_material_slots,get_materials,get_material_library - Scratch libraries:
backup_material_librarysavescurrentMaterialLibrary/meditMaterialsto.mat - Multi/Sub:
set_sub_material - Textures:
create_texture_map,set_texture_map_properties - Dual pipeline:
create_shell_material,replace_material,batch_replace_materials - OSL:
write_osl_shader
Material notes
create_material_from_texturesandsmart_importdefault to OpenPBR. Passmaterial_classfor Physical, Arnold, Redshift, V-Ray, CoronaPhysicalMtl, MaterialX, Octane, etc. (see tool tripbackhint.renderers).palette_laydownhonors it for both single-image previews and grouped PBR sets.create_shell_materialwraps two scene materials inShell_Material(render slot 0, export/viewport slot 1), or builds fromtexture_folderwithrender_material_class/export_material_class. Shell is a container, not a renderer.
Viewport
agent_viewport(action="open")reserves a shaded floating AGENT VIEWPORT. After opening, navigation and capture default to it.start_minimized=trueparks it initially;minimize/restorepark it between inspections. Captures require a visible on-screen panel and explicitly refuse while minimized.statusreportscapture_ready;releasecloses only the owned panel. Initial opening may briefly activate the panel before restoring user focus.agent_viewportalso frames hierarchies, orbits (yaw/pitch degrees), pans (view-plane scene units), zooms (factor<1 closer), and picks surfaces (normalized image x/y, top-left origin). Pass the single capture'sview_tokenasexpected_viewfor picking; then inspect the hit node near its world point before editing base-cage components. View/scene changes invalidate the token; mesh IDs still requireexpected_mesh.- Interactive preview:
agent_viewport(action="render", mode="activeshade"|"vray_ipr"|"vray_vfb"|"corona_vfb"|"shaded"). ActiveShade uses the assigned ActiveShade renderer (renderer_source="production"uses production if compatible). V-Ray previews enable progressive IPR and denoising.vray_vfbandcorona_vfblock the VFB to the agent view; Corona uses the current production renderer and preserves its denoising settings. Start only when rendering is requested. Existing renders elsewhere are refused. Wait forsession_state="running", then useaction="capture"or"stop_capture"(save image, then stop). Optionalcrop=[x,y,width,height]trims VFB pixels. Return to shaded before component targeting or minimizing. Captures do not certify convergence or completed denoising. - Picking supports visible thick splines as well as geometry and returns a world surface normal. Use
draw_spline(action="get")for spline knots. On thin panels, narrow face inspection by proximity and normal to separate the front from the back; frame the part before capturing labels. - Aim/frame:
set_viewport— world-spaceeye+target, named elevations, orframe_names; no camera node is created - Fast:
capture_viewport.capture_method="auto"prefers the direct Nitrous buffer with window capture if unavailable;"window"requests Windows client capture. Both exclude overlapping windows. The agent panel must still redraw and remain open/non-minimized. Window capture requires the updated bridge and retains the view token and capture labels. - Multi-angle grid:
capture_multi_view(frame_rootframes a hierarchy);capture_method="window"applies window capture to every tile. In the agent panel it does not hide other scene nodes; arbitrary-object isolation is not yet supported there. The legacy active-view route also temporarily isolates the hierarchy. source="agent"requires the agent panel;source="active"explicitly targets the user's active view. The defaultautouses the agent panel once opened, and fails if that owned panel becomes unavailable instead of redirecting into the user's viewport. Release and reopen after a scene load or layout replacement.inspect_mesh(capture=true)uses the agent panel when open, drawing component labels into the saved image without adding overlays to the user's viewport.- Fullscreen:
capture_screen(requiresenabled=True) - Frame-buffer window crop:
capture_screen(enabled=True, target="vray_vfb"|"corona_vfb"|"fstorm_vfb"); optionalcrop=[x,y,width,height]trims physical client pixels before resizing. Window captures exclude overlaps without activating/uncovering the target; keep it open and non-minimized. Windows may briefly show a capture border. Recheck dimensions after resizing. Requires the updated bridge; capture failures never return desktop pixels. FStorm targets RT and has no owned agent preview mode.agent_viewport(action="capture"|"stop_capture")automatically uses the same VFB route, or acceptscapture_method="window"for its own panel. Capture does not start rendering or certify convergence. - Blocked production render recovery:
render_automations(action="cancel_capture", job_id=...)saves unobscured VFB window pixels and requests cancellation for the production job you armed and started.capture_target="screen"supports an explicit desktop crop for another renderer. Configure progressive sampling and its denoiser before starting; recovery cannot change blocked render settings. Cancellation is cooperative; check the done-signal separately and treat the image as partial.
Cosmos assets
cosmos_searchfinds models, materials and HDRIs compatible with the selected Max instance and renderer.cosmos_downloadcaches an asset without importing it; ready means completed, queued/downloading means call again to wait.cosmos_importdownloads if needed, then imports through the renderer and returns asset-scoped nodes/materials/maps with file checks. Selection is preserved.- Use returned node refs with existing transform, layer and instance tools. Materials/HDRIs may create editor resources instead of scene nodes.
- Sign in through Cosmos when requested. If an import returns unknown/unverified, inspect before retrying; repeating a completed model import creates another instance.
External .max files (no scene load)
inspect_max_file,search_max_files,merge_from_file,batch_file_info
Plugin discovery
discover_plugin_surface,get_plugin_manifest,refresh_plugin_manifestinspect_plugin_class,inspect_plugin_constructor,inspect_plugin_instance- MCP resources:
resource://3dsmax-mcp/plugins/{name}/manifest|guide|recipes|gotchas
tyFlow
- Create:
create_tyflow,create_tyflow_preset - Inspect:
get_tyflow_info(include_operator_propertiesfor deep readback) - Edit:
modify_tyflow_operator,set_tyflow_shape,set_tyflow_physx,add_tyflow_collision - Simulate:
reset_tyflow_simulation,get_tyflow_particle_count,get_tyflow_particles
For tyFlow graph work (event/operator topology, wiring, transactional edits, operator
discovery, per-event census), read tyflow-graphs.md completely before
acting. It covers get_tyflow_graph, tyflow_apply_patch, the wiring ledger and its
staleness rules, harvest_tyflow_manifest / list_tyflow_operators, tyflow_event_census,
and capture_tyflow_editor for foreign flows.
Forest Pack
scatter_forest_pack— surfaces + source geometry; auto footprint per variant
Controllers & wiring
assign_controller,inspect_controller,inspect_track_view,set_controller_props,add_controller_targetlist_wireable_params,wire_params,get_wired_params,unwire_params
Procedural graph systems
For Data Channel or Max Creation Graph work, read procedural-graphs.md completely before acting. It contains the dedicated tool workflows, agentic compile/verify loop, safety gates, validation rules, and runtime pitfalls.
Scene management
manage_scene(hold/fetch/reset/save/info)get_state_sets,get_camera_sequence
When to Use execute_maxscript
Almost never. Only when there is genuinely no dedicated tool:
- Unsupported controller operations, render/environment settings, custom one-off scripted operations
Do not use for: anything a dedicated tool already does — properties, objects, materials, selection, batch ops, inspection.
Passing code through execute_maxscript (serialization gotchas)
The code string is delivered as a JSON value, so it is un-escaped once before MAXScript ever parses it. A generic parse error (BAD_PARAM) with no line number almost always means the string was corrupted in transit — not that your logic is wrong. if/then/else, chained and, not, and for loops all parse fine on their own; the failures are escaping artifacts. Confirmed causes and fixes:
- Backslashes in string literals break the literal.
"C:\Users\...\textures\"arrives with single backslashes, so MAXScript reads\",\U, etc. as escapes — the trailing\"eats the closing quote and the string never terminates. Use forward slashes in path literals ("C:/Users/.../textures/"— Max accepts them on Windows), or derive paths from runtime values (getFilenamePath/getFilenameFile) instead of hardcoding. \n/\tinside"..."become real control chars and corrupt the literal the same way. Don't embed escapes in strings you send; build output without them.- Keep the whole script on one line, statements separated by
;. Multi-line code through the transport is unreliable;;is not. - Debug tell: on a
BAD_PARAMparse error, shrink to a known-good core —try ( local n=0; for x in (getClassInstances C) do (...); n ) catch (getCurrentException() as string)— and add pieces back. The piece that reintroduces a\or\nin a literal is the culprit.
MCP Tool Pitfalls
set_modifier_property:name+modifier_index(1-based) for one modifier;modifier_class+namesfor batch. Inspect withinspect_properties(target="modifier")first.smart_import: defaultlod_filter="lod0". Shared maps match on asset id; variant meshes in a bundle folder withTextures/share one material key — omitname_patternfor all variants.palette_laydown:sample_mode="random_per_subfolder"for large per-subfolder asset libraries;overflow_mode="palette_then_library"when more than 24 picks.scatter_forest_pack: needs non-zerowidthlist/heightlistper geometry item. Hide source meshes after scatter.get_material_slots: preferslot_scope="map"unless you need every param (slot_scope="all"+include_values:trueis huge on Arnold/Physical).create_object: defaultpos_mode="ground"—posis bottom-center contact, not bbox center. Tripback includesbbox,placement,groundContact.- Box:
width=X,length=Y,height=Z. boolean_operation: non-live operands are consumed — scene node deleted, geometry captured; the operand keeps its node name inside the modifier (rename cutters before applying).live=truekeeps the node (hidden) for later transform tweaks at extra eval cost. Never consume a node other tools still reference by name. Prefer inlinecuttersover scene-node cutters for cuts — pre-named, atomic, no litter on failure.draw_spline: all coordinates world-space; bezierin_vec/out_vecare absolute handle positions, not directions. Edits preserve a SplineShape base beneath modifiers. Bare parametric shapes auto-convert; a parametric base with modifiers requiresconvert=trueto explicitly collapse. Conversion is reported asconverted_to_splineshape.edit_vertices: edits the Editable_Poly base beneath the stack (cage editing — TurboSmooth above stays live); non-poly bases needconvert=true(collapses) orcollapse_modifier_stack.conformto geometry is ray-based —skippedverts had no hit alongaxis; unsigned tokens (z) cast both ways, signed (-z) one way.snapshotAsMeshevaluates the stack and returns a temporary world-space TriMesh. Read its vertices incoordsys worldwithout applyingnode.objectTransformagain; delete the temporary mesh afterward. Base-cage poly vertices still need the node's object transform.list_wireable_paramspaths include[#Parameters]levels — pass through towire_paramsas-is.create_shell_material:mcp_findMaterialByNameusessceneMaterials—getClassInstances Materialis invalid (Material is not a MAXClass).material_classmust be the material's own class name, never a shortened token:PhysicalMaterial, notPhysical—Physicalis the Physical Camera. A non-material class name now returnsBAD_PARAMwithhint.didYouMean.getHandleByAnimformats as values like12345P; quote it as a string when building JSON, or the result is invalid JSON.- MCP tripback is a structured
ToolEnvelopedict (ok/result/error/hint), not a JSON string. Error envelopes may includehint.suggested_tools; tool-authored hints win over auto-hints. - Success JSON payloads may include
message; classify raw structured errors byerror,code, orstatus=error|failed, not bymessagealone. - Never issue mutating native tool calls concurrently: pre-guard bridges interleaved
theHoldtransactions via nested message pumps (0xC0000005, then persistent corruption — phantom successes, wrong handles, bad class resolution). The main-thread executor now defers work items that arrive mid-item, but keep agent-side mutations sequential regardless. USER_BUSYmeans Max has an open undo operation. The native write was rejected before mutation; continue read-only planning and retry after that operation finishes. Do not bypass it with MAXScript or repeated immediate writes.
Keyframes (keyframe_tracks)
action=timeline— targetless read/set offrame_rate,current_frame, andrange_start/range_end; omit setters for a read-only query.action=list— read-only inspection; passfrom_time/to_timeforloopGaps. ParentnumKeysis often 0 — keys live on Bezier Float sub-controllers.delete_keys/move_keys/scale_keys— deterministic key-time edits. They requiretime/timesor bothfrom_timeandto_time; retimes reject destination collisions. Usetime_offset, ortime_scalewith optionalpivot_time.resample/bake— native sampled keys over a requiredfrom_time/to_timewindow.sample_stepdefaults to one frame;bakereplaces keys in-window by default, whileresamplepreserves existing keys unlessreplace_keys=true. List, constraint, expression, script, and motion-capture controllers are intentionally skipped.normalize_tangents— normalizes bounded Bezier keys to smooth tangents by default; passkey_type,in_type, orout_typeto choose another deterministic tangent style.action=loop— copies evaluated pose fromfrom_timetoto_timeparent-first; use for parented reflection rigs (e.g.Plane001→ children). Defaults: frames 1→100.action=matchwithorder=hierarchy— same parent-first copy asloopwhen closing endpoints on rigged hierarchies.- Prefer
value/moveon keyed tracks overtransform_objectfor animated objects —transform_objectrewrites keys at the current slider frame. tracksaccepts exact tokens only:all,position/pos,rotation/rot,scale/scl,transform/tm— not substring matches.
MAXScript Pitfalls
- No parens with keyword args:
Box width:10notBox() width:10 - Wrap in try/catch:
try (...) catch (ex) (ex) NoisevsNoisemodifier: texture map vs modifier(getDir #temp)is Max temp, not OS temp- .NET strings: convert to MAXScript strings before string methods
- Controller/wire paths: normalize display tokens like
[#Z Position]to[#z_position] - TCP fallback is opt-in; prefer the native bridge, and if Max viewport interaction stutters while fallback is running, stop the fallback and use the native bridge path.
OSL
- Use
write_osl_shaderfor file I/O and compilation - Use
introspect_oslbefore wiring — notintrospect_classon OSLMap (massive output) - Shader function name must match
shader_name; use unique names (cache reuse) - OSLMap lowercases param names
MAXScript Reference (bundled)
Read the relevant reference file before writing unfamiliar MAXScript:
| File | Covers |
|---|---|
maxscript-core-syntax.md |
Variables, scope, types, operators, control flow |
maxscript-common-patterns.md |
Undo/animate blocks, callbacks, file I/O |
maxscript-3dsmax-objects.md |
Nodes, transforms, hierarchy, properties |
maxscript-mesh-poly-ops.md |
Sub-object mesh/poly ops |
maxscript-materials-textures.md |
Materials, texmaps, PBR |
maxscript-animation-controllers.md |
Controllers, constraints, wire params |
maxscript-rendering-cameras.md |
Render settings, cameras, environment |
maxscript-splines-shapes.md |
Splines and shapes |
maxscript-scripted-plugins.md |
Scripted geometry, modifiers, utilities |
maxscript-ui-rollouts.md |
Rollout UIs and dialogs |
Unwrap UVW
- Open the editor:
$Box001.modifiers[#Unwrap_UVW].edit()— not theOpenUnwrapUImacro alone