Skip to content

About

Build real 3ds Max plugins with Claude, Codex or any coding agent — Python or C++, live inside Max, without writing against the Max SDK. The agent writes, deploys and iterates in your running session over a local MCP endpoint; a compiled host does the SDK work. 3ds Max 2026 today, more next.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

114 Commits

Folders and files

Repository files navigation

M8ven Verified M8ven Verified

3ds Max SDK MCP

Build real 3ds Max plugins with Claude, Codex, or any coding agent — in Python or C++, live inside Max, without writing against the Max SDK.

The agent writes the plugin, deploys it into your running 3ds Max, runs it on your actual scene, and iterates on what actually happened — instead of you hand-writing C++ against the SDK yourself. What you get out is a real, registered 3ds Max plugin class — a geometry object, a modifier, a renderer, a texture — not a script pasted into the listener. What you never touch is the SDK: a pre-compiled host (the Slot) links it once and stays stable, and your behaviour (the Cartridge) rides on top as a hot-swappable Python or C++ payload with no SDK dependency of its own.

Under the hood it is a local Model Context Protocol server served from inside a running 3ds Max — that is how the agent reaches your session — but the point is the plugin you get out, not the protocol.

Not sure this fits what you're doing? Ask your agent. Paste it this README (or point it at your fork) and ask what it could build for you here — that answer costs you nothing and tells you more than a feature list will.

What you make is yours

Every plugin and cartridge you create with this kit belongs to you. You may sell it, give it away, or keep it private, under any licence you choose. We claim no rights in it, take no royalties, and require no credit or mention — including no disclosure that it was written with AI or with this MCP. The examples and barebones are released under MIT-0, so you can copy them into your work without keeping our notice. You may bundle the plugin and Slot binaries with what you ship, by any means — so your customers can run your cartridge without installing anything themselves.

How it works

The server lives inside a running 3ds Max

The plugin loads when 3ds Max starts and stays for the whole session. It serves an MCP endpoint on 127.0.0.1 — your machine only. Nothing is exposed to the network.

That endpoint is how a coding agent reaches your 3ds Max. Claude Code, Codex, anything that speaks the protocol.

The agent does the work, not you

The agent does not hand you a script to paste and run. Point it at your fork and it will:

  1. write the payload
  2. deploy it into the running 3ds Max
  3. call it there, on your actual scene
  4. read back what happened — the return value, the error, the log
  5. fix it, and go round again

You say what the plugin should do. The agent is the one finding out whether it does. Each lap takes seconds, and not one of them restarts 3ds Max.

A 3ds Max plugin is normally one thing. Here it is two

Writing a plugin for 3ds Max normally means C++, the 3ds Max SDK, the exact compiler the host was built with, and a restart of 3ds Max to see each change.

This kit splits that plugin in half:

  1. the Slot — the half 3ds Max deals with: a real compiled plugin holding all the SDK machinery, built once and shipped as a binary
  2. the Cartridge — the half you write: behaviour, and nothing else

The Slot loads your Cartridge and calls it. Your code never touches the 3ds Max SDK.

Why it was split that way

Four things fall out of the split, and they are the whole point:

  1. No SDK, and no compiler at all in the Python lane. The hard requirements live in the Slot, and the Slot is already built.
  2. Seconds, not minutes. Edit the payload, refresh, look. No rebuild. No restart.
  3. A mistake is cheaper. A Python payload raises an exception and tells you. SDK code with a bad pointer takes 3ds Max down with it.
  4. An agent can write one. It is ordinary Python or C++ against a small, written contract — no SDK to learn, no build environment, no host to hold in your head.

The last one is why this exists. You cannot reasonably hand a coding agent the 3ds Max SDK. You can hand it a payload contract.

What you end up with is a real plugin

Not a script somebody has to remember to run. A registered 3ds Max plugin — an entry in the Modifier List, a file type in the Export dialog — that behaves like anything else shipped with the host.

MaxScript 3ds Max Python This kit
Extends 3ds Max with a plugin yes, a scripted plugin no — it drives 3ds Max, it does not extend it yes, through a compiled Slot
Language you write MaxScript Python Python or C++
The host-side code is interpreted interpreted native C++, already built
Needs the 3ds Max SDK no no no
An AI agent writing it weak — a niche language, thin training data strong strong

MaxScript can do a good deal of this, and has for twenty years. The difference is what you are left holding, and who is able to write it.

Fork it, don't clone it

Fork this repository — or gh repo fork --remote — then:

git clone https://github.com/<you>/3dsmax-sdk-mcp

The fork is not a formality; it is where your work lives. Cartridges you create are written into cartridges/ in your working copy, and in a fork that is your code — committed, pushed, backed up, and shareable. In a clone of somebody else's repository it is a pile of untracked files in a directory you cannot push. You also get the two things a fork is actually for: pulling updates as this kit changes, and opening a pull request if you fix something.

Then open your clone in an MCP-capable coding agent and ask it to install. It:

  1. finds your 3ds Max
  2. checks what is missing, and tells you the command that fixes it
  3. places the plugin, verifying the download before it unpacks anything
  4. waits while you start 3ds Max, then checks the endpoint answers
  5. connects itself to it

Then ask it to bootstrap a Cartridge, and you have a working plugin in the host — with a panel and a button — before you have written a line.

(Just looking? Clone it and read. The kit will mention the fork once, when it starts to matter, and then leave you alone.)

Keeping your fork current

Forking a toolkit raises one fair objection — am I now stuck on today's version? No. Point your fork at this one, once:

git remote add upstream https://github.com/nmalex/3dsmax-sdk-mcp

then, whenever you want what has changed here:

git pull upstream main

cartridges/ is shared — it is where contributed Cartridges land, so an update brings other people's work as well as this kit's changes. That is the point rather than a side effect: the library is the reason to pull. Your own Cartridge sits beside theirs and is only ever in the way if two of you picked the same name, which the naming check exists to prevent.

If you have written something worth having, push your branch and open a pull request — see CONTRIBUTING.md.

What you need

To write a Cartridge in Python, you need:

  1. Windows
  2. 3ds Max 2026

That is the whole list. No compiler. No SDK.

To write one in C++, add Visual Studio with this exact component:

Microsoft.VisualStudio.Component.VC.14.38.17.8.x86.x64

You never need the 3ds Max SDK. Not for Python, not for C++. Nothing you write links against it.

3ds Max 2026 only

This release ships one binary, and it was built for 2026. It will not load in any other version.

3ds Max refuses it on purpose — a plugin built against one version of the SDK must not load into another. So you will not get a broken plugin. You will get no plugin.

You will not be left guessing either: check-prereqs tells you before you install.

Read these two first

LAW.md what you may never do. Chiefly LAW-1: a Cartridge calls the facade and never the 3ds Max SDK - and what to do instead when the facade cannot reach something (you file it, and that is the encouraged move, not a defeat).
ESSENTIALS.md the practice: Python rather than MAXScript and why, the 3ds Max SDK documentation to read for awareness, and how to enumerate the facade before concluding anything.

And when you need to know whether the facade reaches something, ask, do not guess:

python surface/max_facade_surface.py <what you are looking for>
python surface/max_facade_surface.py --missing

surface/ is all 5,430 facade entries and every declared gap, generated from the same declaration the facade itself is generated from - so it cannot drift from what actually ships.

The three verbs

install — once per machine:

  1. find 3ds Max
  2. check prerequisites
  3. place the plugin
  4. start 3ds Max
  5. verify the endpoint answers
  6. connect your agent to it

bootstrap — once per Cartridge. A named new Cartridge with a default About rollout and a Hello World button, deployed and proven in the running host.

author — the loop after that: edit the payload, refresh, see it. No restart.

Connecting your agent

The plugin serves MCP over HTTP on 127.0.0.1. A client that speaks HTTP connects straight to it - point it at http://127.0.0.1:3000/mcp.

Some MCP clients only launch a server as a command and talk to it over stdin/stdout. For those, run the stdio bridge (tools/mcp_bridge.py), which speaks that and forwards to the plugin. It needs only Python — no packages — and, like the plugin, talks only to your own machine:

{"mcpServers": {"3dsmax": {"command": "python",
                           "args": ["C:/path/to/3dsmax-sdk-mcp/tools/mcp_bridge.py"]}}}

The bridge lists the tools even before 3ds Max is running, so a client can show them; a call made while 3ds Max is down returns a result that says to start it, rather than dropping the connection. Calls that would discard work still ask you first, through your client.

Cartridge

A Cartridge is a payload — a Python module, or a native DLL. It is not a compiled 3ds Max plugin, and you do not build a Slot. You occupy one.

One Slot per plugin type. One Cartridge per Slot. A Slot is a workbench station, not a home. Two things follow, and both matter for your test scenes:

  1. the entry you see in 3ds Max carries the Slot's name, not your Cartridge's
  2. a saved scene resolves by the Slot — so it loads whichever Cartridge occupies it later

docs/SLOTS.md explains both.

A Cartridge declares its parameters; the Slot keeps them. A value your payload stores in a Python global does not survive a Refresh and is never saved with the scene. describe_params() is how you get one that is saved, keyable in Track View and reachable from a script — see docs/SLOTS.md. It needs 0.4.0-alpha.3 or newer.

Two lanes, one behaviour:

Python payload Native payload
Needs a compiler no yes, the pinned toolset
Edit-to-see loop refresh build, then refresh
Where it goes python/<module>.py native/

Barebones examples

Start here. barebones/ has the smallest working cartridge for every plugin type. Each one registers and appears where that type appears, and says hello in a log line you read back with cartridge_logs — most greet as soon as the automated probe drives them; a few report alive on load and fire their Hello World under a host action (a render, a solve, a panel), which is a property of that plugin kind, not a failure. Fork the one whose type matches what you are building; every example is written to be copied, and each carries a README listing all the SuperClassIDs that slot can bear.

Thirty slots ship today, each with a Python and a C++ lane:

Plugin type Python C++ What it is
Geometry python native a parametric object built as a mesh (GEOMOBJECT_CLASS_ID)
Shape python native a spline shape built from polylines (SHAPE_CLASS_ID)
Light python native an omni light (LIGHT_CLASS_ID)
Camera python native a free camera (CAMERA_CLASS_ID)
Space warp python native a space-warp object + binding (WSM_OBJECT_CLASS_ID)
System python native a node-system plugin (SYSTEM_CLASS_ID)
Custom attributes python native a parameter block for any object (CUST_ATTRIB_CLASS_ID)
Modifier python native reads the mesh, hands it back (OSM_CLASS_ID)
Exporter python native formats text the slot writes (SCENE_EXPORT_CLASS_ID)
Renderer python native a Choose-Renderer plugin (RENDERER_CLASS_ID)
Manipulator python native a viewport gizmo (HELPER_CLASS_ID)
Helper python native a plain helper that draws the lines it returns (HELPER_CLASS_ID)
Controller python native an animation controller (CTRL_FLOAT_CLASS_ID)
UV generator python native a texmap UV coordinate source (UVGEN_CLASS_ID)
XYZ generator python native a texmap XYZ coordinate source (XYZGEN_CLASS_ID)
Texture output python native a texmap output filter (TEXOUTPUT_CLASS_ID)
AA filter kernel python native an anti-aliasing filter (FILTER_KERNEL_CLASS_ID)
Radiosity python native an advanced-lighting plugin (RADIOSITY_CLASS_ID)
Data Channel engine python native a Data Channel modifier engine (DATACHANNELENGINE_SUPER_CLASS_ID)
Render effect python native post-processes a rendered frame (RENDER_EFFECT_CLASS_ID)
IK solver python native solves an IK chain (IK_SOLVER_CLASS_ID)
Object snap python native a snap in the Snaps settings (OSNAP_CLASS_ID)
ParticleFlow operator python native a Particle View operator (HELPER_CLASS_ID)
Utility python native a Utilities-panel tool (UTILITY_CLASS_ID)
Colour picker python native a colour selector (COLPICK_CLASS_ID)
Video Post filter python native a Video Post image filter (FLT_CLASS_ID)
Texture map python native a map for a material's map slot — colour, mono, bump (TEXMAP_CLASS_ID)
Material python native a material the renderer asks to shade every sample (MATERIAL_CLASS_ID)
Shader python native a shading model in the Standard material's Shader list (SHADER_CLASS_ID)
Sampler python native a supersampler in the Standard material's SuperSampling rollout (SAMPLER_CLASS_ID)

manipulator and pfoperator share HELPER_CLASS_ID and are still different slots — a super-class is a property of the class a slot registers, not a bucket that owns slots.

The four material-family slots — texture map, material, shader, sampler — are asked for an answer at every shading sample, so Python never runs per sample. A Python cartridge runs managed: it returns data once (an image, a colour, a tint, a sub-pixel pattern) and the slot evaluates it natively. A native payload exporting MaxMcpGetUnmanaged runs unmanaged: the slot calls it per sample with an opaque handle it reads through the facade's ShadeSample* entries — still no 3ds Max header, no raw pointer. See docs/SLOTS.md.

The full index is barebones/README.md. docs/SUPERCLASS_CENSUS.md is the complete census of every plugin type the SDK declares — which have a slot, which are registerable with a slot on the roadmap, and which can never be a third-party slot and why — and docs/COVERAGE_MAP.md accounts for how each of the 30 is verified.

Limitations

  • 3ds Max 2026 only. Other versions need a binary that does not exist yet.
  • The binaries are unsigned. Windows SmartScreen will warn. Every release publishes the SHA-256 of its asset and the installer verifies what it downloaded against it.
  • Windows only, which is where 3ds Max is.
  • The Slot sources are not published. The plugin and its Slots are delivered as binaries; what is published is everything needed to author a payload against them.

Documentation

The plugin serves its own documentation over MCP once installed — an agent driving it can read every rule over the protocol alone. The copies in docs/ ship beside the binary.

Privacy and security. The plugin collects nothing and tracks nothing - see PRIVACY.md. How the local server is protected, and how to report a vulnerability: SECURITY.md.

The MCP tools the server serves are listed in docs/TOOLS.md, with every argument, and machine-readable in mcp-tools.json. Both are the server's own tools/list answer, regenerated for each release.

Tests. tests/ holds the suites: Pester for the scripts, and Python unittest (test_*.py) for the cartridge payloads and the facade surface index. CI runs them all on every pull request beside PSScriptAnalyzer and the cartridge-structure check (.github/workflows/ci.yml). Run the same three jobs locally with tools/check-all.ps1.

Status

0.9.0-alpha.1. STATUS.md says which parts work today and which do not; ROADMAP.md says what is missing on purpose.

Reporting a bug

Open an issue here. github.com/nmalex/3dsmax-sdk-mcp/issues

This section is written for an agent as much as for a person. If you are driving this repository with Claude, Codex or anything else, it can gather every fact below itself and file the issue for you — the commands are literal, they only read, and none of them touches your scene.

Bug, gap, or feature request? They are three different reports. A bug is a facade entry that misbehaves; a gap is a 3ds Max SDK function the facade does not wrap yet; a feature request is a capability beyond one call. docs/REPORTING.md tells the three apart and gives the exact evidence each needs — start there if it is not plainly a bug. The rest of this section is the deep-dive for a bug.

First, decide which half is broken, because only one of them is yours

A Cartridge bug A Slot bug
What it is your payload — the Python or the native DLL you wrote the compiled plugin that hosts it
Who can fix it you, in seconds, with a refresh only a new release
What a report is worth a question, usually answered by the log the reason this repository has issues at all

A Slot cannot be patched in the field. It is a compiled .dlm/.dlu/.dle, so a fix means a new binary and a 3ds Max restart for everyone. That is the whole reason a Slot bug is worth a careful report and a Cartridge bug usually is not.

The distinction is not a judgement call, and you should not make it by reading code. Ask what the Slot was asked and what it answered — that is what the evidence below is for. A real example: #2 looked like a broken payload, because a payload's panel did not appear. Every payload-side signal was green: the module loaded, its exports were listed, its deform() ran. The Slot was never calling describe_ui at all. Nothing was asking, and no amount of staring at the payload would have shown that.

The evidence, and why each line of it is load-bearing

Gather all five. A report missing the first two usually needs a second round trip before anyone can start, which costs more of your time than collecting them did.

1. Which binary is actually in memory. Not which one you installed — which one is running.

max_capabilities

The MCP tool, and its own description says to trust it over any document. It answers the buildId of the DLL 3ds Max has loaded. A plugin file on disk and a plugin in memory are different claims, and 3ds Max cannot unload a native plugin — so an install that "definitely worked" can be sitting behind a session that started before it.

2. Which Slot version, from the Slot itself. Every Slot's About rollout carries its version and build stamp. Read them off the panel and paste them verbatim. This is how a report survives being read six weeks later: 0.2.2-dev, built 2026-09-06 10:46:41 UTC names one binary and nothing else.

3. What the Slot thinks it is holding.

max_cartridges          — every open instance, its module, and whether its interpreter is up
max_cartridge_refresh   — reloads from disk and reports the exports it found

max_cartridge_refresh is the one that separates "my file is not being read" from "my file is being read and something else is wrong": it names the functions it actually found.

4. The log, which says what happened rather than what was supposed to.

cartridge_logs

Every crossing into Python, every payload print and every raise, timestamped and tagged with the instance that produced it. The reply carries the path of the file it read, so attach that file rather than transcribing lines out of it — and ask the tool for the path rather than assuming one, because it is a detail of the build you are running and not a promise. Set MAXMCP_LOG_VERBOSITY if you need the verbose lines; they are suppressed by default.

5. The machine, as JSON, not as prose.

powershell -NoProfile -ExecutionPolicy Bypass -File tools\find-max.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tools\2026\check-prereqs.ps1

Every installed 3ds Max, and every readiness row with its remedy. Paste the JSON. A tool's own output is evidence; a summary of it is a claim, and the field that turns out to matter is always the one the summary dropped.

What never goes in an issue

Your scene is yours and this repository does not want it. Leave out scene contents and asset paths, anything naming a client or a project, licence or serial numbers, and machine or user names beyond what the tool output above already carries. If a path names your work rather than the kit, replace it — C:\...\<project>\scene.max reads perfectly well.

A minimal repro beats a real scene every time, and for this kit it is usually one primitive: a sphere, one Cartridge, one click.

Say what you expected, separately from what happened

Two headings, never merged into a paragraph. Expected is the promise you were relying on — quote it, and say where you read it: a line in docs/, a comment in the scaffolded payload, a message the tool printed. Half of what makes a report good is naming the promise, because a bug is the gap between a promise and a behaviour, and the maintainer may not know which promise you read.

Actual is what the host did, with the evidence above under it. Include what you already ruled out and how — a discounted theory saves the next person from spending an hour on it.

The template

Copy this. An agent can fill every field of it without asking you anything.

## Environment
- Installed release:            <from tools\2026\release.json, or the tag you installed>
- Build id in memory:           <max_capabilities → buildId>
- Slot version and build stamp: <the About rollout, verbatim>
- 3ds Max:                      <version, from tools\find-max.ps1>
- Lane:                         <python | native>

## Expected
<the promise, quoted, and where it is written>

## Actual
<what the host did>

## Reproduction
1. <numbered, from a fresh scene, ending at the moment it goes wrong>

## Evidence
<max_cartridges, max_cartridge_refresh, cartridge_logs, check-prereqs.ps1 — as output, not prose>

## Already ruled out
<theories tested, and what disproved them>

One report per bug. Two problems in one issue get one answer, and it is usually the wrong one.

If you can also fix it

Say so in the issue and read CONTRIBUTING.md before you branch. Note that a Slot fix is not something a pull request here can carry — the Slot sources are not published, so a Slot issue is fixed upstream and reaches you as a release. A report is not a lesser contribution than a patch for that half of the system; it is the only contribution there is.

Contributing

Read CONTRIBUTING.md first. Branch naming, commit form, the pull-request flow and the release ritual are written there, and they bind everyone including the maintainer.

BACKLOG.md is the work in flight; ROADMAP.md is the direction and what is deliberately absent.

Licence

MIT — see LICENSE. The code you copy from — barebones/ and examples/ — is MIT-0 (barebones/LICENSE, examples/LICENSE): no notice to keep. What you build is yours; see What you make is yours.

About

Build real 3ds Max plugins with Claude, Codex or any coding agent — Python or C++, live inside Max, without writing against the Max SDK. The agent writes, deploys and iterates in your running session over a local MCP endpoint; a compiled host does the SDK work. 3ds Max 2026 today, more next.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages