#Building with AI agents
Rudder Virt exposes a Model Context Protocol (MCP) server so AI agents and other automation can create and maintain modules without using the web editor. An agent connected to the server can list and read modules, write module details, manifests, and rubrics, run and promote builds, review grades, and see and drive a VM's graphical console. It can also read this documentation, so it can learn the aileron manifest format before writing one.
The MCP server is an alternative entry point to the same logic the web app uses. Everything an agent does is subject to the same authorization and audit rules as the UI.
#Getting an API key
The MCP server uses the same API key as the rest of the Rudder Virt API. Generate one in the web app:
- Open your organization's admin area and go to APIs.
- Select Generate New API Key and copy the key. It is shown only once.
Each user has one active key. Generating a new key invalidates the previous one, and Invalidate API key revokes it entirely. The key acts as you: every call is authorized against your roles at the time of the call. You must be an admin (teacher or admin role) of an organization to author or build its modules, and the organization must be permitted to create modules.
#Connecting a client
Authenticate with the API key as a bearer token. Replace YOUR_API_KEY with the key you generated.
Claude Code: add the server from your terminal.
claude mcp add --transport http ruddervirt https://your-ruddervirt-host/mcp --header "Authorization: Bearer YOUR_API_KEY"
Other MCP clients take a JSON configuration like this (consult your client's docs for where it lives):
{
"mcpServers": {
"ruddervirt": {
"type": "http",
"url": "https://your-ruddervirt-host/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
To check connectivity without a full client, send a tools/list request with curl:
curl -sS https://your-ruddervirt-host/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
A missing or invalid key returns 401. Both request headers (Content-Type and Accept) are required by the transport.
#What the agent can do
Tools are grouped by task.
Discovery
list_organizations: list the organizations you can author in, with your role and whether each may create modules. You belong to several, so call this first to get theorganizationIdthat every other module tool needs.
Authoring
list_modules,get_module: browse an organization's modules and read one in full (details, briefing, rubric, manifest, tags, access level, builds).list_base_builds: list the builds the organization may reference as asource.build_refbase image (its ownBase Image-tagged modules, maintainer-shared modules, and consumer-sharedBase Imagemodules, promoted to its zone), with thebuildNameandvmNamesyou need to write the ref.create_module: create a module, or patch it in place if a module with the same name already exists in the organization.organizationIdandnameidentify the module and everything else is optional: only the fields you pass are written, so changing one setting leaves the rubric, manifest and briefings alone. Creating a new module does requiredescription,briefingandaileronManifest. Tags are additive here; useset_module_tagwithvalue=nullto remove one.update_module_details,update_manifest,update_rubric,set_module_tag: edit the parts of an existing module.
Build and preview lifecycle
launch_build: start a real build of the module's current manifest in the organization's deployment zone.list_builds: list builds and the current stable pointer.get_build: get one build's status and its provisioner logs — how the agent watches a build in progress and works out why a failed build failed. Logs are tailed to the most recentlimitentries (200 by default). Also reportsawaitingHandbuild: the VMs parked on ahandbuildprovisioner, with the step name and the author's instructions. A build whose logs stop without failing is usually waiting there.create_preview: launch a preview clone so you can test and grade it before promoting. PassbuildIdto preview a specific succeeded build — one active preview per module, so asking for a preview of a different build fails rather than returning the wrong image. OmitbuildIdto preview the module's stable build for the organization's zone, which re-points an existing preview instead of failing. Returns aconsoleUrlyou can open to watch the VM yourself, and apreviewBuildIdsaying which build the preview actually runs. A preview is free — no virt, no VM Slot — but it lasts one hour (expiresAtsays when it ends) and an organization may only have one live at a time, across every module.get_preview: read the current preview clone, which build it points at, when it expires, and itsconsoleUrl. Null means there is no live preview — including one that has passed its hour and been reaped.reprovision_preview: reset the preview to a clean unsolved state, optionally onto a newer build as you iterate. AtargetBuildIdmust be in the same deployment zone as the preview. This is the right way to start over. It rotates to a fresh clone of the build image and sequences the old one's teardown, and it resets the one-hour deadline.destroy_preview: tear the preview down, freeing the organization's one live preview so a different module can be previewed. Not needed for cleanup, since a preview expires on its own within the hour. Teardown is asynchronous, so the clone keeps running briefly after this returns and aconsolePathcaptured earlier still points at it. If you only want a clean VM, reprovision instead of destroying and recreating.continue_build: release a build paused by ahandbuildprovisioner, once the hand work is done. Takes the manifestvmNamethatawaitingHandbuildreports.promote_stable_build: promote a succeeded build to stable.
Grading review
grade_preview(owner/maintainer only): autograde the module's preview clone, so an agent can test a rubric it wrote without you clicking Grade in the browser. Asynchronous; one grade in flight per preview. Grading drives the VM's serial console, so this refuses while a VM is still booting and names the power state it is in. That is a wait-and-retry answer rather than a failure, and it records no grade. A preview finishes provisioning well before its VMs finish booting.get_preview_grades(owner/maintainer only — the breakdown is effectively the answer key): the preview's grade history with the full per-rubric-component breakdown: the score each component produced, the commands the grader ran, and their output. This is what tells an agent why a component scored, so it can fix a rubric that measures the wrong thing. A row withfailed: trueis an infrastructure failure rather than a score. Read theerrorin itsgradeResultDataand retry instead of tuning the rubric against the zero.get_module_grade_summary: read the module's maximum grade (from its rubric) and aggregate grade statistics.
Console
console_screenshot: take a picture of a VM's graphical console and get it back as an image. This is the answer to "the build logs just stop": a GUI installer waiting on a dialog, an ISO boot menu nobody answered, a VM sitting at an unexpected login screen. PassbuildIdto look at a VM in that build, or omit it to look at the module's preview clone. The capture waits for the screen to stop changing first (settleMs, default 750ms); a screen that never settles — a spinner, a blinking cursor — is captured anyway and reported withsettled: false, which is normal.frameHashis a coarse digest of the screen: poll on it rather than re-reading images, because an unchanged value means nothing has moved.console_type: type a string into the console. The text uses the same token vocabulary as a manifest's boot command, so"Administrator<tab>hunter2<enter>"and"<leftCtrlOn>a<leftCtrlOff>"mean what you expect. An unknown<token>is refused by name rather than typed literally.console_send_keys: send key chords and named keys, e.g.["ctrl+alt+f2", "<wait5>", "<enter>"].console_click: click, double-click, move, drag or scroll at a coordinate. Coordinates are full-resolution console pixels; the screenshot reports the size it was rendered at, which may be smaller.
Every input tool returns a screenshot of the result in the same call, so acting and seeing what you did costs one round trip. Set screenshot: false to skip the image.
Documentation
list_docs,read_doc: list and read these documentation pages as markdown. The same pages are also offered as MCP resources underruddervirt-docs:///<slug>.
Classroom and student management are not exposed. Use the web app for those.
#Driving the console
The console tools reach the same screen a person sees in the browser, over the same connection to the deployment zone's console gateway, under the same permissions. Two consoles are reachable: a VM in a build (pass buildId) and the organization's preview clone (omit it). Student VMs are not exposed.
There is no session to open or close. Each call connects, does what you asked, captures the screen, and disconnects. Three consequences worth planning around:
- A whole gesture must fit in one call. Modifiers held with
<leftCtrlOn>are released by their matching<leftCtrlOff>, and released unconditionally when the call ends, so nothing can be left stuck down in the guest. You cannot hold a key across two calls. - Do not wait inside a call. Waits in a keystroke string are capped. To watch a long installer, send the keystrokes and then poll
console_screenshot, comparingframeHashbetween calls. - A build VM only has a console while it is interactive, roughly between its boot command and the end of provisioning. Outside that window the tools say so rather than hanging.
A typical debugging loop when a build stops making progress:
get_buildto see the provisioner logs and confirm they have stopped.console_screenshotwithbuildIdto see the actual screen.console_typeorconsole_clickto answer whatever is waiting, checking the returned screenshot each time.console_screenshoton a poll until the build starts logging again.
Sending input is recorded in the audit log, against the module for a build VM and against the namespace for a preview. The keystrokes themselves are never recorded, so a password typed into an installer does not end up in the log. Screenshots are not audited — an agent polling an installer would write hundreds of rows and bury the entries that matter.
#Handbuild steps
A handbuild provisioner deliberately stops the build and waits for a person to do something the automation cannot: run a vendor installer, click through a wizard, accept a licence. The build is neither failing nor progressing, and its logs simply stop — which is exactly what a hung build looks like.
get_build reports these in awaitingHandbuild, naming the VM, the step, and whatever instructions the manifest author wrote. The loop is:
get_build—awaitingHandbuildis non-empty, so the build is parked rather than stuck.console_screenshotwith thatbuildIdandvmNameto see the screen.console_typeandconsole_clickto do the work described in the step's instructions.continue_buildwith the samevmNameto let the build carry on.- Poll
get_buildagain — a build may pause more than once.
Do not leave a build parked. It blocks indefinitely, and if the organization adopted the module rather than owning it, nobody else is going to finish it.
One thing to know before you connect: an API key that can run a build can also drive that build's console. This is not new — the build page has always handed a full-input console URL to any organization admin who can operate the build — but these tools make it reachable from automation, so treat an API key as console access.
#Learning the manifest format
A module is one or more virtual machines described by an aileron manifest. An agent should read the format before authoring one. The server's instructions point agents at the key pages, and they are always reachable through the documentation tools:
read_doc("building/reference"): every manifest field. See also Reference.read_doc("building/concepts"): the primitives (sources, provisioners, networking, files). See also Concepts.read_doc("building/recipes/hello-world"): a minimal worked example. See also Recipes.read_doc("grader/writing-rubrics"): the rubric format. See also Writing rubrics.
#A typical authoring flow
list_organizationsto pick the organization to work in (one that hascanCreateModules), andread_doc("building/reference")plusread_doc("building/recipes/hello-world")to learn the manifest format.list_modulesto see what already exists, orget_moduleto read one you are revising.create_module(orupdate_manifest) to write the manifest, thenupdate_rubricto add grading.launch_build, then pollget_builduntilisTerminalis true. If it failed, the logs it returns say why. If the logs stop without failing, the VM is probably waiting on something graphical:console_screenshotshows you what, andconsole_type/console_clickanswer it.create_previewfrom that build and open theconsoleUrlit returns to see the VM in your browser;reprovision_previewonto a newer build as you iterate on the manifest.console_screenshotwithout abuildIdto see the preview VM for yourself, thengrade_previewto try the rubric against the preview, thenget_preview_gradesto see what each component scored and why. Give the VM time to boot first:grade_previewsays so if it is not ready. Fix the rubric (or the manifest) and repeat, usingreprovision_previewwhenever you need the VM back in its unsolved state.promote_stable_buildonce it is good,destroy_previewto clear the way for the next module's preview, andget_module_grade_summaryto review results once students have run it.
#Trying a module you did not write
An organization that only consumes a module — one shared with it, or a public module — can still preview it, to judge whether it is worth assigning. That loop is shorter, because there is no build to pick and no rubric to tune:
list_modulesand checkhasStableBuildInOrgZone. False means the module has not been promoted into this organization's deployment zone, so there is nothing to run yet.create_previewwith nobuildId. The preview runs the zone's stable build, which is exactly what students get on release; any otherbuildIdis refused for a consumer.- Open the
consoleUrland work through the module. destroy_previewwhen finished, so the organization can preview another module. It costs nothing to leave running either — it expires within the hour on its own.
grade_preview and get_preview_grades are not part of this loop — they are limited to the module's own organization and its maintainers. See Previewing a module for the browser equivalent.