InfiniWorkflow exposes its node graph to an AI agent (Claude, or any MCP-compatible client) over an MCP server. This page covers connecting an agent, launching one, and what it can actually do.
The MCP server now starts itself the first time it's needed - either from the "Launch" button below, or the chatbox's Generative AI dialog - so -mcp at startup is optional (useful only if you want it running before you open this page, e.g. for Claude Desktop). If starting it fails (for example fastmcp isn't installed), the error shows up right where you triggered it.
The in-app chatbox has no separate "Workflow" or "Plugin" mode anymore - it's a single free-form conversation, backed by this same MCP server, that decides on its own which tools a message calls for (build/edit a graph, generate a plugin, drive AI Studio, inspect state, control playback, ...). It keeps real multi-turn history too: each conversation is its own saved thread with its own persistent claude session, switchable from the sidebar, so context isn't lost between messages or across an InfiniWorkflow restart.
Opens a real terminal window running the command below, in the InfiniWorkflow project directory, starting the MCP server first if it isn't already running. The command is remembered (in a cookie) for next time.
Register the server once per machine (or per project, with --scope local):
claude mcp add --transport sse infiniworkflow http://127.0.0.1:8000/sse
After that, any claude session started in this project picks up the connection automatically - including one launched from the button above. No special prefix is needed - just describe what you want in plain language and Claude calls the right tools on its own.
Add this to Claude Desktop's MCP server settings:
{
"mcpServers": {
"infiniworkflow": {
"type": "sse",
"url": "http://127.0.0.1:8000/sse"
}
}
}
Install AnythingLLM Desktop, then go to Settings > Extensions > MCP and add:
{
"mcpServers": {
"infiniworkflow": {
"type": "sse",
"url": "http://127.0.0.1:8000/sse"
}
}
}
Then, in Workspace Settings > Agent Configuration, paste a system prompt telling the agent how to map requests to tools (see the manual's MCP section for a starting point) - unlike Claude, AnythingLLM's agent doesn't already know the tool names, so it needs to be told.
@agent - that's AnythingLLM's own convention for switching a message into agent/tool-use mode, not anything specific to InfiniWorkflow. Plain chat messages (no @agent) never call tools, no matter what you ask. Example: @agent add a canny node connected to the movie reader.
Once connected, try asking things like this (copy one into your agent's chat - prefix with @agent if you're using AnythingLLM, no prefix needed for Claude):
Also kept in sync in the manual's MCP Server section. A node can be addressed by its uuid or by its human-readable name shown on the canvas; if a name matches more than one node, tools that take a node name will report the ambiguity instead of guessing.
| Tool | What it does |
|---|---|
| Discovery / read-only | |
get_tool_catalog() | Returns every available node type (identifier, description, inputs, outputs) - use this to look up the correct tool_type before calling add_node. |
get_demo_examples() | Returns example prompt/workflow pairs from the built-in demos, useful as few-shot reference. |
get_recent_catalog_items(months_back) | Lists catalog entries added/modified in the last N months - useful for discovering newly added node types. |
get_workflow_json() | Returns the current graph (nodes, edges, uuids) as JSON. |
get_workflow_path() | Returns the current workflow's file path/root on disk. |
get_unique_node_name(base_name) | Returns a name guaranteed not to collide with an existing node, e.g. "Camera" → "Camera1". |
validate_add_node(tool_type) | Checks a tool identifier's mandatory-input requirements before calling add_node. |
get_node_image(node_name, port, is_input) | Returns the current image for a node's port as a real image the agent can look at. |
get_node_resolution(node_name, port) | Returns width/height/channels/depth of a node's output port (C++ nodes only). |
get_node_custom_values(node_names) | Returns every set parameter for the given nodes (or all nodes if omitted), keyed by each input's own uuid. |
get_playback_state(node_name) | Returns the current frame/playing state of a playable node. |
get_existing_groups(group_name) | Lists group/macro definitions available to insert into the graph. |
get_project_resolution() | Returns the project's current canvas resolution. |
get_cuda_devices() | Lists available CUDA device names on this machine. |
get_package_info() | Lists installed/available Python packages the workflow's tools depend on. |
get_performance_counters() | Returns per-node performance/timing counters for the running workflow. |
get_node_inputs(node_name) | Richer read than get_workflow_json's per-node inputs - includes live supervise/validation state (whether each input currently satisfies what the node needs to run). |
get_node_io_customization(node_name) | Returns a node's exposed-input/output customization state (per-port uuid, current label, hidden flags) - used for group/macro port customization. |
get_valid_functions(node_names) | Given selected node(s), lists which operations/conditions are valid for them - use to ground add_trigger's operation/condition arguments instead of guessing. |
get_published_outputs() | Read-only: lists which node outputs are currently published. |
get_node_value(node_name, port) | Reads a Python node's current output value directly (number, string, list, small matrix) - Python nodes only, no equivalent for cv.* nodes. |
search_tool_catalog(query, max_results) | Keyword-relevance search over the full catalog (identifier/title/description/tags) - use instead of get_tool_catalog when you have a specific capability in mind, e.g. search_tool_catalog("detect edges"). |
explain_workflow() | Returns the current graph in data-flow (topological) order with each node's catalog title/description, set parameters, and named connections - a ready-made basis for summarizing/explaining a workflow, instead of cross-referencing get_workflow_json and get_tool_catalog yourself. |
| Graph editing | |
add_node(tool_type, node_name, x, y) | Adds a node to the canvas, e.g. add_node(tool_type="cv.canny", node_name="Edge Detect"). x/y are optional; omit to auto-place. |
propose_pipeline(tool_types, node_names, parameters, auto_build, clear_canvas, connect_from, connect_from_port, start_x, start_y) | Builds a linear node chain from explicit tool_type identifiers you've already decided on (e.g. via search_tool_catalog/get_tool_catalog) - no natural-language guessing happens inside this tool itself, since picking the right node for a plain-language step is a judgment call, not a keyword-matching one. Read-only by default (auto_build=False) - pass auto_build=True to actually add and wire the nodes left-to-right. parameters (optional, one dict per tool_type, {} for none) applies set_parameter to each node right after it's added, e.g. so the first node's source is already set. clear_canvas=True wipes the graph first (new pipeline from scratch); connect_from="Existing Node" wires the new chain onto an existing node's output instead (extend/fix an existing pipeline) - use one or the other, not both. |
batch_update(add_vertices, add_edges, remove_vertices, remove_edges) | Builds/tears down many nodes and edges in one round-trip - use instead of N separate add_node/connect_ports/remove_node calls when constructing or restructuring a large graph. add_edges entries may reference a node_name defined earlier in the same add_vertices list. |
remove_node(node_name) | Removes a node from the graph. |
rename_node(node_name, new_name) | Renames a node. |
set_node_enabled(node_name, enabled) | Enables or disables a node. |
connect_nodes(from_node, from_port, to_node, to_port) | Wires an output port to an input port, e.g. connect_nodes("Camera", 0, "Edge Detect", 0). Same as connect_ports. |
remove_connection(from_node, from_port, to_node, to_port) | Removes an existing connection. |
set_parameter(node_name, parameter, value) | Sets an input's value, e.g. set_parameter("Edge Detect", "threshold1", "100"). Parameter names come from get_tool_catalog's inputs list. |
add_node_input(node_name) / remove_node_input(node_name) | Adds/removes a dynamic input port on nodes that support variable input counts (Python nodes only). |
add_node_output(node_name) / remove_node_output(node_name) | Adds/removes a dynamic output port on nodes that support variable output counts (Python nodes only). |
set_node_io_customization(node_name, input_names, output_names) | Renames/relabels a node's exposed inputs and/or outputs, keyed by input/output identifier, e.g. {"threshold1": {"name": "Sensitivity"}}. |
update_node_positions(positions) | Repositions one or more nodes, e.g. update_node_positions({"Canny": [400, 200]}). |
add_trigger(sink_node, input_port, source_node, operation, condition, value) | Adds a conditional trigger on an input, firing when another node's value satisfies the given condition. |
remove_all_triggers(sink_node, input_port) | Clears all triggers on a node's input port. |
merge_cpu_threads(node_names) / split_cpu_threads(node_names) | Groups the given nodes onto one CPU thread, or splits them back apart. |
set_node_buffer_size(node_name, buffer_size) | Tunes a node's frame buffer depth - perf-tuning lever for playback-heavy graphs. |
set_node_gpu_device(node_names, gpu_device) | Reassigns the GPU device index for one or more nodes. |
set_project_resolution(width, height, resize_spatial) | Sets the project's canvas resolution. |
flush_undo_redo() | Clears the undo/redo history - use after a batch of agent-driven edits the user shouldn't undo back through. |
reset_canvas() | Clears the entire workflow, starting fresh. Cannot be undone. |
| Viewer / playback / UI | |
open_viewer(node_name, port) | Opens the image/video viewer on a node's output. |
zoom_to_node(node_name) | Pans/zooms the canvas to bring an off-screen node into view. |
toggle_playback(node_name) | Starts or stops playback on a specific playable node, returning the resulting play state. |
set_playing(play) | Explicitly starts or stops playback on every playable node - use for a known end state rather than toggle_playback's per-node flip. |
jump_to_frame(frame, node_name) | Scrubs to a specific frame. Omit node_name to apply to every playable node. |
advance_frame(node_name) / rewind_frame(node_name) | Steps one frame forward/back. Omit node_name to apply to every playable node. |
jump_to_first_frame(node_name) / jump_to_last_frame(node_name) | Jumps to the start/end of playback. Omit node_name to apply to every playable node. |
show_message(text, duration) | Shows a status message/speech bubble in the UI - use this to narrate what the agent is doing. |
| Workflow files | |
import_workflow(filename) | Loads a workflow file from the demos folder, replacing the current graph. Cannot be undone. |
save_workflow() | Saves the current graph to disk (same path the app's own Save action uses). |
save_as_workflow(filename, root, folder) | Saves the current graph to a NEW file, leaving the currently-open workflow untouched - a fork/branch rather than an overwrite. |
| AI Studio / Video Studio pipeline (see full section below) | |
run_ai_studio_pipeline(movie_path, root, folder, seed_prompt, track_direction, generate_variations, stop_after, export_name, base_model, epochs, train_split_pct, force_retrack, use_batch) | Runs extract → (split if >1024 frames) → seed → SAM2 track → optional variations → optional train as one background pipeline. Returns a pipeline_id immediately. seed_prompt takes a points list (a point you picked, e.g. after looking at the frame with get_node_image), or {"auto": true, "prompt": "welding pool"} for real text-guided matching - a bare {"auto": true} with no prompt isn't a real seeding mode (it's just SAM's most-confident guess) and is treated the same as giving no seed_prompt at all: the pipeline stops at stage="awaiting_seed", and the live progress dialog's click-to-seed UI takes over from there. A points/auto+prompt attempt that fails also falls back to the same click-to-seed UI rather than hard-failing. base_model is "Small"/"Medium" (default)/"Large", matching AI Labeling Studio's own dropdown - any other value is rejected by the training script. force_retrack=True clears stale per-frame tracking output from a prior run on the same clip first, since SAM2 tracking otherwise silently skips frames that already have cached output. use_batch (default False) picks the training mechanism for stop_after="train" - see the training callout below. |
get_ai_studio_pipeline_status(pipeline_id) | Polls a pipeline's current stage/progress/message. Once training starts (non-batch, the default), keep polling this in the same session until stage is "done"/"failed" - the process no longer exits. |
cancel_ai_studio_pipeline(pipeline_id) | Requests cancellation at the next stage boundary. During non-batch training this actually terminates the training subprocess and reports stage="failed"; has no effect once use_batch=True training has actually started (the process exits shortly after regardless). |
run_ai_studio_finetune(root, folder, export_name, base_model, epochs, train_split_pct, generate_variations, use_batch) | Trains directly on an already-labeled frames folder, skipping extract/seed/track. Same base_model/use_batch choices as above. folder must be the SOURCE frames folder that already contains real tracked labels (e.g. "welding_clip/.videos/welding_clip") - the training subprocess discovers images/masks by walking that folder's own parent for sibling .videos/.variations entries, not from export_name. |
get_ai_studio_pipeline_result(export_name) | Reads training results (loss/accuracy CSV/PNG, exported ONNX) straight off disk. The exported *.onnx is always under export_name; the .results CSV/PNG metrics instead land under the SOURCE folder's top-level segment (e.g. "welding_clip", not export_name) - call this again with that name if the first call shows the onnx but no metrics. |
| Plugin generation (see full section below) | |
get_plugin_sdk_context() | Returns the plugin SDK headers (plugin_api.h/plugin_sdk.h) plus 4 full reference example plugins - everything needed to write a new C++ plugin node's 4 files yourself. Call this first; it supplies grounding, it does not generate code for you. |
get_plugin_catalog_schema() | Returns the catalog-JSON contract for a plugin's <plugin_name>.json, including the critical rule that the inputs array order is a positional contract with plugin_impl.cpp's update(), not just documentation. |
build_and_install_plugin(plugin_name, files, overwrite) | Writes your drafted plugin_impl.h/plugin_impl.cpp/<plugin_name>.json/CMakeLists.txt to sdk/gen_ai/<plugin_name>/ and runs the CMake configure+build. Returns {"success": true, "log": "..."} or a build failure log to fix and retry (pass overwrite=True on a retry - the first attempt's directory needs clearing). |
activate_plugin(plugin_name) | Hot-registers a just-built plugin with the running engine so it's usable in this session without an app restart. Call once, right after a successful build_and_install_plugin, then confirm with add_node. |
preview_plugin_in_ui(plugin_name, files) | Shows a plugin's drafted files in the app's own code-viewer dialog - the same one the chatbox uses - so a human can read the code and click Download or Compile themselves, instead of the calling model building it autonomously. UI-only; doesn't build or validate anything itself. |
These five tools let an agent drive the whole "segment this movie and train a model" workflow that AI Labeling Studio / Video Studio otherwise require many manual clicks for. They call the exact same underlying functions the UI itself uses - nothing about AI Labeling Studio or Video Studio changes because this exists, and anything the pipeline produces (extracted frames, keyframe masks, tracked labels) shows up exactly the same as if a human had done it, if you open that folder in AI Studio afterward.
seed_prompt={"frame": N, "points": [[x,y]]} - look at the frame first with get_node_image to pick a good point), or a plain-language description (seed_prompt={"frame": N, "auto": true, "prompt": "welding pool"}). There's no way to guess "what to segment" from a single sentence about the whole video, so one of these is required - a bare {"auto": true} with no prompt isn't reliable enough to run on its own (it's just SAM's single most-confident guess) and is treated the same as no seed_prompt at all: the pipeline stops and waits, and the app's own progress dialog offers a click-to-seed UI to finish the job by hand (click one or more points, preview the resulting mask, then confirm). A points/prompt attempt that fails falls back to the same click-to-seed UI rather than just failing outright.stop_after="train" and run_ai_studio_finetune both train via the same non-batch, in-GUI mechanism AI Labeling Studio's own "Start Fine-Tuning" button uses - InfiniWorkflow (and this MCP server) stays running the whole time, so just keep polling get_ai_studio_pipeline_status until stage is "done"/"failed", then call get_ai_studio_pipeline_result in the same session. On success the node also registers a brand-new inference tool in the catalog, named after export_name - the status message names it directly, so search for that name (or check Available Tools' search_tool_catalog) to actually use the trained model in a graph.
use_batch=True exits the app instead. Pass this to use the legacy path (same as the app's own "EXIT and train" button): InfiniWorkflow - including this MCP server - closes about a second later, on purpose, to free the GPU/CPU for a fully detached training process. Your agent's connection will drop; that's expected, not a failure. Reconnect once InfiniWorkflow has restarted and call get_ai_studio_pipeline_result(export_name). Only use this if the default non-batch path doesn't work for some reason.
run_ai_studio_pipeline(
movie_path="welding_clip.mp4",
root="demos",
seed_prompt={"frame": 0, "points": [[420, 260]]},
stop_after="track"
)
# -> {"pipeline_id": "..."} poll with get_ai_studio_pipeline_status(pipeline_id)
# once stage == "done", the video is fully labeled - open it in AI Labeling
# Studio to check it, or go straight to training:
run_ai_studio_finetune(
root="demos",
folder="welding_clip/.videos/welding_clip",
export_name="welding_v1",
epochs=30
)
# -> {"pipeline_id": "..."} - InfiniWorkflow stays open; keep polling
# get_ai_studio_pipeline_status(pipeline_id) until stage is "done"/"failed",
# then read the results (onnx under "welding_v1", metrics under "welding_clip"):
get_ai_studio_pipeline_result(export_name="welding_v1")
get_ai_studio_pipeline_result(export_name="welding_clip")
run_ai_studio_pipeline(
movie_path="welding_clip.mp4",
root="demos",
seed_prompt={"frame": 0, "auto": true, "prompt": "welding pool"},
stop_after="track"
)
# if this description-based match fails, the pipeline falls back to
# stage="awaiting_seed" automatically - same as giving no seed_prompt at all
# (see below) - rather than just failing outright.
Omitting seed_prompt - or giving one that doesn't pan out - stops the pipeline right after extraction so you (or the agent, after looking at a frame) can decide where to seed. In the app itself, the progress dialog offers a click-to-seed UI for exactly this case instead of needing a second tool call at all.
run_ai_studio_pipeline(movie_path="welding_clip.mp4", root="demos")
# -> stage: "awaiting_seed", frames_folder: "welding_clip/.videos/welding_clip"
# look at a frame, then resume with the same movie_path/root/folder plus a seed:
run_ai_studio_pipeline(
movie_path="welding_clip.mp4",
root="demos",
seed_prompt={"frame": 0, "points": [[420, 260]]}
)
Only one AI Studio pipeline runs at a time; run_ai_studio_pipeline/run_ai_studio_finetune return an error immediately if one is already active. Check with get_ai_studio_pipeline_status, or cancel_ai_studio_pipeline if you need to free the slot before it finishes on its own.
Write a brand-new compiled C++ node from a natural-language description. The design is deliberate: rather than the MCP server calling another LLM internally, these tools hand you (the calling model) the SDK headers and reference examples, and you write the plugin's 4 files yourself - you get to see and fix real compiler errors directly, instead of a second model doing it opaquely inside the tool. This is exactly what the in-app chatbox does too, since it's itself just a Claude session wired to this same MCP server (see Status & Launch) - there's no separate generator anymore.
setup/update/superviseInputs/superviseValue/execute/isCached/flushCache/teardown/destroy) and 4 full reference plugins (Canny2, boxblur, abandoned_object, topple_detection).<plugin_name>.json.plugin_impl.h, plugin_impl.cpp, <plugin_name>.json, CMakeLists.txt (base CMakeLists.txt on abandoned_object's or topple_detection's - not Canny2's or boxblur's, which hard-fail CMake on Windows (they ship a companion .sln/.vcxproj instead, which this flow can't use). Its POST_BUILD step, which copies the compiled binary (renamed to .plugin, via a PREFIX/SUFFIX target property) and catalog JSON into Extensions/, is what actually makes the node show up.overwrite=True), or hand it to the human with preview_plugin_in_ui(plugin_name, files), which opens the same code-viewer dialog the chatbox uses so they can read the code and click Compile themselves (that button posts to the same underlying build endpoint).build_and_install_plugin; the dialog's own Compile button does this automatically.add_node(tool_type="<plugin_name>.plugin", ...) - confirm it's actually usable in a graph.abandoned_object or topple_detection. Two of the four reference examples (Canny2, boxblur) ship a CMakeLists.txt that starts with if (WIN32) message(FATAL_ERROR "Use visual studio project instead!") endif() - it's meant to be used only via their companion .sln/.vcxproj on Windows, and deliberately refuses to configure under CMake there. Both build paths above (build_and_install_plugin and the dialog's Compile button) always run CMake (there's no .sln path for a generated plugin), so copying Canny2/boxblur's CMakeLists.txt fails the build outright on Windows. abandoned_object/topple_detection's CMakeLists.txt has no such guard and is genuinely cross-platform.
This is the kind of request the flow is built for - describe the node in plain language, and the four calls above turn that into a real, running node:
Create a plugin that takes a source image2D and a detection matrix2D (class ID, x, y, w, h, confidence per row). The output is any detected objects that stay stationary for some time. Have inputs of time and an event trigger to clear.
Which the calling model turns into a real build, e.g.:
context = get_plugin_sdk_context()
schema = get_plugin_catalog_schema()
# Using context + schema, write the 4 files yourself. abandoned_object's
# reference plugin is the closest starting point (source + detections in,
# stationary-tracking logic, preview out) - copy its per-track centroid
# matching, not its trackId assignment (that example hands out a new
# trackId to every detection every frame, so stationaryFrames_ never
# actually accumulates - match against previousCentroids_ by nearest
# distance first, the way topple_detection does, or "stay stationary for
# some time" can never trigger).
files = {
"plugin_impl.h": "...", # your PluginApi subclass header
"plugin_impl.cpp": "...", # your PluginApi subclass implementation
"StationaryObjectDetector.json": "...", # identifier MUST be "StationaryObjectDetector.plugin"
"CMakeLists.txt": "...", # base on abandoned_object's/topple_detection's, rename the project
}
result = build_and_install_plugin(plugin_name="StationaryObjectDetector", files=files)
# -> {"success": true, "log": "..."} or a build_failed log to fix and retry
# with overwrite=True
activate_plugin(plugin_name="StationaryObjectDetector")
# -> {"success": true, "identifier": "StationaryObjectDetector.plugin"}
add_node(tool_type="StationaryObjectDetector.plugin", node_name="Stationary Object Detector")
# confirms the node is actually usable in this session - open_viewer() on
# its "preview" output to see it live in the front end
To let a human review/download/compile the same draft instead of building it autonomously, swap the last three calls for one:
preview_plugin_in_ui(plugin_name="StationaryObjectDetector", files=files) # -> opens the "Generated Plugin Files" dialog in any connected browser tab - # same viewer the chatbox uses. The human reads the code, # clicks Download to save the 4 files, or clicks Compile to build it right # there (posts to the same /compile_plugin_to_disk endpoint, then hot- # registers with the engine the same way activate_plugin does).