SteelFrame MCP servers & reference oracle

Drive the real mainframe engine from any MCP client — Claude Desktop, Claude Code, Cursor, a CI harness, or any remote-native connector. Two things you can do with it: write and run COBOL/JCL on the live engine (the developer loop), and use SteelFrame as a reference oracle for a modernization comparison — it returns files, joblogs, screens one keystroke at a time and scheduler cycles, and your harness does the comparing. Two servers, one engine: a persistent lane whose work lands in the shared store, and a private / ephemeral lane with the same tool surface that runs RAM-only with zero retention — one-shot sandboxes for a single answer, RAM-only workspaces for multi-step work including the whole oracle chain.

Tested. The oracle surface's acceptance gate (test/tw_mcp_oracle_battle.mjs) drives every oracle tool through both doors against a scratch engine with the CardDemo estate — workspaces, seeding, run_batch joblogs, screen-by-screen CICS with a pinned clock, CA-7 DEMAND cycles — and the three MCP battles (tw_mcp_stdio_battle, tw_mcp_http_battle, tw_mcp_private_battle) cover both auth modes (incl. auto-re-logon across a real engine restart), scope gating, JSON-RPC conformance, ephemeral isolation/cleanup on the abend path and the injection/traversal sanitizers. An adversarial QA pass on the MCP doors (solo Opus, 2026-09-01) returned PUSH-READY with zero confirmed correctness or security bugs. Real counts and dates are in notes/MCP-ORACLE-BUILD-LEDGER.md and notes/MCP-TEST-FINDINGS.md. Compile diagnostics, abends, screens and joblogs are the engine's genuine output, never fabricated.

What it is

One engine, one route registry, two projections: every REST route that opts in becomes exactly one MCP tool (name, description and input schema are generated from the route — nothing is hand-written per tool, so the catalog cannot drift from the dispatcher). What you do with the tools falls into two uses:

Use 1 · Developer loop both lanes

You write COBOL/JCL in your MCP client and compile, run and inspect it on the REAL engine: check_source → compile_program → run_program, the raw JES2 tools, WTOR replies, datasets, SPUFI, the analysis suite. On the persistent lane everything lands in the shared store — log into the SteelFrame web as the same userid and your jobs are in SDSF, your members in DSLIST, your load modules in <USERID>.LOADLIB.

Use 2 · Reference oracle both lanes

You are modernizing an estate and need the mainframe's actual behaviour to compare against. SteelFrame returns what the mainframe holds and does: source + copybooks + JCL + maps + CSD slice, records decoded through copybooks beside their bytes, a batch run's whole joblog and output datasets, CICS screens one keystroke at a time, CA-7 / Control-M cycles — all on an isolated copy of the estate (a disk workspace on the persistent lane, a RAM-only workspace on the private lane). It never judges equivalence and never converts code; your harness replays the same inputs against the modernized system and compares on its side. Details below.

Which lane? Persistent vs private / ephemeral

Both lanes expose the same engine and the same tool surface. The difference is one thing only: where the bytes live. Persistent work lands in the shared store and is web-visible; private work is RAM-only and zero-retention — a single-use sandbox per one-shot call, or a leased RAM-only workspace that survives across calls so a whole equivalence scenario runs inside it and then vanishes.

Persistent default Private / ephemeral lane=ephemeral
What it is for The developer loop on the real engine, and the whole reference-oracle workflow (workspaces, seeding, batch runs, CICS sessions, scheduler cycles, state dumps) — when the result should exist afterwards. The same two uses when nothing may be retained: a one-shot answer ("does this compile?", "what does this deck do?"), or the whole oracle chain — find/export → seed → run_batch → cics_send screen by screen → sched_wait_cycle → export_state — inside a RAM-only workspace.
Where it runs The shared engine, as your userid; or a disk workspace (a child engine on a copy of the estate under the data root, visible in the region grid). A single-use sandbox per one-shot call (destroyed before the response returns), or a RAM-only workspace: a leased sandbox-class engine whose data root and scratch live on the RAM-backed base, never under the shared data root, never in /api/instances; destroyed by destroy_workspace, its TTL, or the parent's exit.
Retention Persistent and web-visible: SDSF, DSLIST, LOADLIB, run records, bundles, state dumps (with TTL/quota). Full audit as you. Nothing. Nothing written to the shared engine, nothing visible in the web, nothing after the session. Parent-side audit keeps sizes, return codes and workspace ids only, never content. RAM-backed (/dev/shm) on hosted Linux; results and workspace views carry an honest ramBacked flag.
How results come back Inline, and as persisted artifacts you fetch later (get_run, get_bundle, get_state, read_spool). One-shots: inline only. RAM workspace: inline plus the same artifacts — but they live inside the workspace: fetch them before destroy_workspace; the bundle tar.gz download (a REST stream) is not reachable — page with get_bundle_part.
Tool count The full catalog (the live count is on the button in the tool reference). The full catalog plus five: the one-shots (check_source, compile_inline, run_inline, compile_and_run, jcl_inline) first, then the RAM-workspace lifecycle, then every persistent tool with workspace required.
The one rule workspace:<id> is optional — without it a tool runs on the live estate (except sched_reset / cics_define, workspace-only). create_workspace first, then workspace:<id> on every stateful tool; without it the call is refused in-band (403) — nothing ever runs on the live estate from this lane. A disk workspace id is invisible here (404).
Known limit wait_job caps at 300 s (call it again on timedOut:true); run_batch waits up to 3600 s. A WTOR-ing one-shot times out (no operator to reply); the one-shot caps are engine env (ES_EPH_TIMEOUT_S, ES_EPH_MAX_KB, ES_EPH_MAX). A seed:"clone" RAM workspace copies the whole estate into RAM (budget for it; seed:"fresh" + load_dataset is the light path); disk + RAM workspaces share the per-user / plane caps and the TTL (≤ 86400 s).
Scope The developer scope set (and the harness additions for the oracle — see authentication). ephemeral.exec to create the sandbox/workspace; inside a workspace each routed tool is checked against your own scopes, so an oracle harness token carries the same scopes as on the persistent lane plus ephemeral.exec.
Pick it when You want the result to exist afterwards, or you want it in the web. You need a zero-retention guarantee — for a throwaway answer or for a whole equivalence run.

Setup

Two doors

DoorWhat
Door A — stdio bridge bin/steelframe-mcp.mjs (zero-dep, Node 18+): the standard local MCP client configuration. It fetches its tool catalog from the engine (GET /api/mcp/tools) so it never hardcodes a tool; a workspace:<id> argument is routed into that workspace's child engine for you.
Door B — remote POST /mcp on the engine (JSON-RPC over POST, bearer auth): for remote-native MCP clients and CI harnesses. Append ?lane=ephemeral for the private lane. Same scopes/audit as the REST API by construction.

Client config snippet

Claude Desktop / Claude Code (mcpServers), Cursor (mcp.json). Point args at your checkout's bridge:

{
  "mcpServers": {
    "steelframe": {
      "command": "node",
      "args": ["/path/to/mainframe/bin/steelframe-mcp.mjs"],
      "env": {
        "SF_URL": "https://steelframe.cobolstack.com",
        "SF_USER": "IBMUSER",
        "SF_PASSWORD": "LIONS123"
      }
    },
    "steelframe-private": {
      "command": "node",
      "args": ["/path/to/mainframe/bin/steelframe-mcp.mjs"],
      "env": {
        "SF_URL": "https://steelframe.cobolstack.com",
        "SF_TOKEN": "sf_...",
        "SF_LANE": "ephemeral"
      }
    }
  }
}

SF_URL is your SteelFrame URL: https://steelframe.cobolstack.com for the hosted estate (the default above). Self-host only: use http://localhost:3270 if you run the engine from your own checkout — there is nothing listening on localhost:3270 otherwise, and the bridge will report connection refused.

Remote client (connector-style): for the hosted estate the door is https://steelframe.cobolstack.com/mcp, header Authorization: Bearer sf_... (or a session token). For a self-hosted plane use https://<plane>/mcp. No MCP client at all? Every tool is an ordinary REST route — the binding (method + path) is shown on each tool in the reference and the same routes are documented on /api.html; e.g. POST /api/mcp/compile with { "program": "HELLO", "lines": [...] } or POST /api/run/batch with { "member": "CARDDEMO.JCL(POSTTRAN)" }.

Connecting from another computer (remote, no local checkout)

On a different machine — no repo, no node, no bridge process — register the hosted door as an HTTP transport straight from Claude Code. This needs only Claude Code + internet:

claude mcp add -s user --transport http steelframe \
  https://steelframe.cobolstack.com/mcp \
  --header "Authorization: Bearer sf_..."

-s user makes the server available in every folder (drop it for project scope). --transport http selects the remote door; --header carries your bearer token. The sf_... value is a persistent scoped API key — mint one under Authentication below. (Flag names verified against Claude Code's claude mcp add --help; if yours differs, check that help for your CLI version.) Contrast with Door A — stdio bridge above, which runs bin/steelframe-mcp.mjs locally and therefore needs the repo checkout + node.

Authentication — two ways, pick either

1 · API token CI / harness / long-lived

Mint with POST /api/tokens (admin, or self-service clamped to your own scopes) and set SF_TOKEN. The developer scope set:

datasets.write, jobs.write, db2.exec,
system.read, cics.read
(+ ephemeral.exec for the private lane)

A modernization harness using the oracle adds:

cics.exec        # headless CICS sessions, scripts, LINK
sched.admin      # DEMAND / ORDER / define / import
workspace.write  # create / reset / destroy workspaces
(db2.exec already covers DB2 table dumps)

2 · Userid + password no API key

Set SF_USER + SF_PASSWORD. The bridge performs POST /api/logon at startup and automatically re-logons on any 401 — session tokens live in engine memory and die on a restart, and the bridge rides through transparently.

The password stays in your client config/env; the engine's logon audit records the userid only. A session token carries the ordinary session scope set (which includes cics.exec); a RACF SPECIAL user gets *.

Security posture (honest). Every request in both modes carries a valid bearer token — scope checks, audit and RACF revocation apply identically. The password never leaves the client and never appears in bridge stderr or any tool result (QA-scanned). The bridge refuses plain http:// to non-localhost engines unless SF_ALLOW_HTTP=1 (hosted planes terminate TLS at the gateway). No admin / operator / security tool is reachable from MCP regardless of the token — those verbs are deliberately excluded from the catalog. Seeding tools (load_dataset, stage_gdg, sched_import, sched_define_job) carry the MCP destructive hint and refuse system HLQs; cics_define and sched_reset are workspace-only (refused on the live estate). On a plane shared by mutually-untrusting developers, enable PROTECTALL + security.enforce (documented opt-in) so the shared store isn't cross-user readable/destructible.

Use 1 — the developer loop

The persistent lane exposes the whole spine — compile-check, upload, compile+link, run, the raw JES2 job tools, WTOR, and the analysis suite. The private lane collapses this into five one-shot composites. Highlights:

ToolWhat it does
check_sourceinline compile-check (real cobc + DB2 precompiler / CICS translator auto-detect), diagnostics with line numbers, no artifacts
upload_sourcebatch-write members: kind cbl→<HLQ>.SOURCE, cpy→<HLQ>.COPYLIB, jcl→<HLQ>.JCL (allocate-on-first-use)
compile_programONE call: source → real EXEC IGYWCL deck → JES2 → {jobid, maxcc, per-step RCs, diagnostics} + listing excerpt on failure + the generated JCL; load module → <HLQ>.LOADLIB. parm:"SQL"/"CICS" for the precompiler/translator; copybooks ride via SYSLIB. loadlib + syslib install the module into an estate's library instead
run_programGO deck with STEPLIB <HLQ>.LOADLIB, DDs from a JSON spec ({lines} instream / {dsn,disp} / {sysout} / {dummy}), returns RC + SYSOUT
JES2 spinesubmit_job / wait_job / get_job / list_jobs / read_spool (tail-guards to the last 500 lines, full:true overrides; wait_job caps at 300s — on timedOut:true just call it again)
list_replies / reply_wtorsee and answer a blocked batch step's WTOR
cancel/purge/hold/release_jobqueue actions (destructive ones carry MCP destructive hints)
datasets · analysis · querylist/search/allocate, members and raw records (hex, binary-safe), vsam_info; inventory_scan, analysis_model/graph/impact/metrics, explain_program, bms_preview; sql (SPUFI), whoami, system_info

Worked example — compile → run → read logs

Against Door B (POST /mcp, JSON-RPC 2.0). Step 1 compiles; the response carries maxcc, per-step RCs, parsed diagnostics and the load-module dsname:

  1. compile_program
    curl -s https://<plane>/mcp \
      -H "Authorization: Bearer sf_..." \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc":"2.0","id":1,"method":"tools/call",
        "params":{
          "name":"compile_program",
          "arguments":{
            "program":"HELLO",
            "lines":[
              "       IDENTIFICATION DIVISION.",
              "       PROGRAM-ID. HELLO.",
              "       PROCEDURE DIVISION.",
              "           DISPLAY \"HELLO FROM MCP\".",
              "           STOP RUN."
            ]
          }
        }
      }'
    # → { "jobid":"JOB00021", "ok":true, "maxcc":0,
    #       "steps":[{"name":"COB","pgm":"IGYCRCTL","rc":0}, ...],
    #       "diagnostics":[], "loadModule":"IBMUSER.LOADLIB(HELLO)",
    #       "jcl":"//IBMUSERC JOB ..." }
  2. run_program — GO deck with STEPLIB <HLQ>.LOADLIB:
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
         "params":{"name":"run_program",
           "arguments":{"program":"HELLO",
             "dds":{"SYSOUT":{"sysout":"*"}}}}}'
    # → { "ok":true, "maxcc":0, "sysout":"HELLO FROM MCP" }
  3. read_spool — pull the JES2 log for the compile job (tail-guarded):
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
         "params":{"name":"read_spool",
           "arguments":{"jobid":"JOB00021"}}}'
    # → { "content":"...JES2 job log...", "truncated":false }
    #   (pass "full":true or "tail":<n> for the whole spool)

A compile error comes back honestly — e.g. { "ok":false, "maxcc":12, "diagnostics":[{"line":7, "code":"IGYPS2121-S","message":"'WS-BOGUS' IS NOT DEFINED"}] } — the line number and symbol are parsed from the genuine cobc listing. A program that abends surfaces { "ok":false, "abend":"S0C7", "maxcc":"S0C7" }.

The same loop on the private lane

Point the client at SF_LANE=ephemeral (or POST /mcp?lane=ephemeral). One shot: call compile_and_run with the source, copybooks and data DDs inline — the verdict, diagnostics, RC, SYSOUT and requested output datasets come back in the one response and the sandbox is gone (jcl_inline is the generalization lever for any one-shot batch deck). Multi-step: create_workspace (a RAM-only workspace, seed fresh), then the very same upload_source → compile_program → run_program → read_spool with workspace:<id>, then destroy_workspace — identical results, nothing retained.

Use 2 — SteelFrame as a REFERENCE ORACLE for modernization

What it is, honestly. SteelFrame returns what the mainframe holds and does — files, decoded data, joblogs, screens one at a time, scheduler cycles. It never judges equivalence (no "same/different" verdict, no score) and it never converts code (no generated Java/SQL/anything). Your harness replays the same inputs against your modernized code and compares on its side — including deciding which facts (dates, task numbers, JOBIDs) are expected to differ. Every oracle tool is a route on the same registry (GET /api/mcp/tools), audited as your userid.

The workflow

find + export→ create_workspace→ seed (load_dataset / stage_gdg)→ run_batch → joblog + outputs→ cics_open_session → cics_send … (screen by screen)→ sched_demand → sched_wait_cycle→ export_state (after)→ compare on YOUR side→ destroy_workspace
  1. Find and export. search_source (content / regex / kind-classified members) and find_program (source, transitive copybooks, load modules, the JCL steps that run it, the CSD slice, mapsets, the scheduler jobs that run those decks) tell you what the estate knows. export_bundle hands it over in one call — programs + copybooks + JCL + procs + BMS (source, parsed model, physical map) + CSD slice + schedule + datasets as hex and copybook-decoded rows; text parts inline, dataset parts paged by get_bundle_part, or the whole thing as GET /api/export/bundles/{id}/download (tar.gz). Nothing is converted.
  2. Isolate. create_workspace {seed:'clone'} boots a child engine on a copy-on-write clone of the live estate (or snapshot:<id> for a reproducible CI baseline, or fresh). Then pass workspace:<id> on any tool call to run it there as yourself; the live estate never changes. reset_workspace re-baselines it (optional jobNumber so JOBIDs line up across re-runs); destroy_workspace when done.
  3. Seed. load_dataset puts records in (rows + copybook, or hex with the charset stated): PS replace/append, KSDS replace/append/upsert, ESDS; allocate-on-demand. stage_gdg rolls a new generation through a real IEBGENER DISP=(NEW,CATLG) step. define_cluster for a new VSAM cluster; compile_program {loadlib, syslib} installs a program into the estate's LOADLIB. A mapset is installed by running an inline //S1 EXEC PGM=DFHBMSCP step through run_batch (//SYSIN DD = the BMS source, //SYSPUNCH DD = the symbolic copybook target). Load the same rows into your candidate system.
  4. Batch. run_batch {member:'CARDDEMO.JCL(POSTTRAN)'} (or inline lines) submits, waits, and returns the run record: step RCs / abend, the whole joblog keyed STEP.DD, the output datasets (auto-derived from the deck's DDs) as hex + decoded rows, GDG (+1) resolved after the run, optional DB2 tables, an optional identical dump taken before the run (verbatim — no diff), WTORs auto-answered. clock:{fixed:'2022-07-18 10:20:30'} pins the COBOL runtime clock for that job. Persisted as a steelframe-run/1 record (get_run).
  5. Online, screen by screen. cics_open_session gives you a headless terminal (the same engine terminal the web and TN3270E use) and its initial screen. cics_send sends one AID per call with fields addressed by BMS field name (or row/col) and returns every screen the engine painted during the step plus the final screen as steelframe-screen/1: named fields + attributes, cursor, keyboard state, 24×80 text (raw:true adds the TN3270E data stream). It settles on the engine's keyboard lock → unlock; task.alive:true means a CONVERSE/RECEIVE is waiting for your next send; 409 while the keyboard is still locked (re-read with cics_screen or type ahead with queue:true). cics_run_script runs a whole step list in one call and returns the transcript + a state dump. Both take clock:{fixed:…} to pin EIBDATE/EIBTIME, ASKTIME/FORMATTIME and FUNCTION CURRENT-DATE for that terminal's tasks only, so a re-run paints the same screens. cics_link LINKs a program headlessly with a COMMAREA (hex, or rows + copybook) and returns the COMMAREA back.
  6. Scheduler (CA-7 / Control-M). sched_import takes a real LJOB report or DEFTABLE XML; sched_define_job adds one job with triggers/conditions from JSON; sched_jobs returns the dependency graph as data. sched_demand {job} (CA-7) or sched_order {folder} (Control-M) starts a cycle and returns its id; sched_wait_cycle {id} waits for it to end and returns the cycle log (the order the jobs ran, the triggers that fired), every job's state / jobid / maxcc / steps and joblog, and optional output datasets. sched_ca7_command speaks CA-7's own syntax (RUN,JOB=, LJOB, LQ, LPRRN, POST, …).
  7. Pull the after-state and compare on your side. export_state {datasets, db2, spool} is a pure dump of what the mainframe holds right now (hex + rows, charset stated, sha256 per artifact), persisted for get_state. Take one before and one after, or one from the oracle and one from your candidate, and diff them in your harness. decode_records / encode_records convert between record images and JSON rows through a copybook whenever you need to cross the representation boundary yourself.

What comes back

GroupToolsWhat you get back
find + export search_source · find_program · export_bundle · get_bundle · get_bundle_part · list_bundles · delete_bundle source text inline; datasets as parts paged (hex + rows); the manifest carries a sha256 per part
data both ways decode_records · encode_records · export_state · get_state · list_states · delete_state {charset, hex[], rows[], layout, sha256}; charset is stated every time — PS/PDS = EBCDIC at rest, KSDS/ESDS = the program's record image; numerics as exact scaled-decimal strings ("-123.45"), OCCURS expanded, FILLER omitted
seed load_dataset · stage_gdg · define_cluster · compile_program{loadlib,syslib} counts written / deleted / updated, duplicates reported, sha256 after the load, the absolute GnnnnV00 name + joblog for a GDG roll
batch run_batch · run_capture · list_runs · get_run · delete_run steelframe-run/1: steps[], joblog{STEP.DD}, outputs{dsn:{charset,total,sha256,hex,rows,layout}}, db2, before, wtorsAnswered
online, screen by screen cics_open_session · cics_send · cics_screen · cics_close_session · cics_list_sessions · cics_run_script · cics_link · cics_csd steelframe-screen/1: named fields + attributes, cursor, keyboard, 24×80 text, raw:true = the TN3270E data stream; task{transid,num,alive}; LINK returns the COMMAREA hex (+ rows) and containers
scheduler sched_import · sched_define_job · sched_jobs · sched_demand · sched_order · sched_ca7_command · sched_wait_cycle · sched_status · sched_symbol / options / condition / calendar / job_action / cancel / newday / alerts the order the oracle ran the jobs, verbatim, with every job's RC and joblog
isolation create_workspace · reset_workspace · destroy_workspace · list_workspaces · get_workspace (+ workspace-only: cics_define, sched_reset) a child engine on a copy of the estate; the live estate never changes
Facts are not verdicts. CURDATE/CURTIME on a screen, EIBTASKN, JOBIDs and the timestamps in JESMSGLG all come back exactly as composed — decide on your side what to ignore, or pin what can be pinned (clock:{fixed} on run_batch, cics_open_session, cics_run_script; jobNumber on a workspace so JOBIDs line up). Two CardDemo facts the gate pins, as examples of the kind of thing the oracle will faithfully report rather than smooth over: POSTTRAN needs the AWS.M2.CARDDEMO.DALYREJS GDG base to exist, and a DEMAND cycle that runs CLOSEFIL leaves the CICS files closed until OPENFIL runs (signon then answers "Unable to verify the User").

Worked oracle example — CardDemo, Door B

One harness run, JSON-RPC over POST /mcp (every call below is the params of a tools/call). Result shapes are the routes' documented returns, abbreviated.

  1. create_workspace — an isolated copy of the estate:
    {"name":"create_workspace",
     "arguments":{"seed":"clone","ttlSeconds":3600,"jobNumber":1000,"name":"posting-v1"}}
    # → { "workspace":"W1", "seed":"clone", "status":"READY", "jobNumber":1000,
    #       "expires":"..." }        # from here on: "workspace":"W1" on every call
  2. load_dataset — seed the daily transaction file from rows + copybook:
    {"name":"load_dataset",
     "arguments":{"workspace":"W1",
       "dsn":"AWS.M2.CARDDEMO.DALYTRAN.PS",
       "copybook":"CARDDEMO.COPYLIB(CVTRA06Y)",
       "mode":"replace",
       "rows":[{"DALYTRAN-ID":"...","DALYTRAN-AMT":"125.00", "...":"..."}]}}
    # → { "ok":true, "dsorg":"PS", "charset":"ebcdic", "given":N, "written":N,
    #       "total":N, "sha256":"..." }   # load the same rows into your candidate
  3. run_batch — the posting job, with decoded outputs and a pinned clock:
    {"name":"run_batch",
     "arguments":{"workspace":"W1",
       "member":"CARDDEMO.JCL(POSTTRAN)",
       "copybooks":{"AWS.M2.CARDDEMO.TRANSACT.VSAM.KSDS":"CARDDEMO.COPYLIB(CVTRA05Y)",
                    "AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS":"CARDDEMO.COPYLIB(CVACT01Y)"},
       "records":"rows",
       "clock":{"fixed":"2022-07-18 10:20:30"}}}
    # → { "runId":"...", "jobid":"JOB01000", "maxcc":0, "abend":null,
    #       "steps":[{"name":"STEP15","pgm":"CBTRN02C","rc":0}],
    #       "joblog":{"JESMSGLG":{...},"JESYSMSG":{...},"STEP15.SYSPRINT":{"text":"..."}},
    #       "outputs":{"AWS.M2.CARDDEMO.TRANSACT.VSAM.KSDS":{"charset":"program",
    #                   "total":N,"sha256":"...","rows":[...]}, ...},
    #       "sha256":"..." }         # run YOUR posting job on the same seed; compare rows + RCs
  4. cics_open_session + cics_send — the signon transaction, one AID at a time:
    {"name":"cics_open_session",
     "arguments":{"workspace":"W1","clock":{"fixed":"2022-07-18 10:20:30"}}}
    # → { "session":"S1", "termid":"...", "keyboard":"unlocked",
    #       "screen":{ "format":"steelframe-screen/1", "fields":[...], "text":[24 lines] } }
    
    {"name":"cics_send",
     "arguments":{"workspace":"W1","id":"S1","aid":"ENTER","text":"CC00"}}
    # → { "step":1, "screens":[...every screen painted...], "screen":{...COSGN00...},
    #       "task":{"transid":"CC00","alive":false}, "settled":true }
    
    {"name":"cics_send",
     "arguments":{"workspace":"W1","id":"S1","aid":"ENTER",
       "fields":[{"name":"USERID","text":"USER0001"},{"name":"PASSWD","text":"PASSWORD"}],
       "expect":{"text":"MAIN MENU"}}}
    # → { "screen":{ "fields":[{"name":"CURDATE","text":"07/18/22"}, ...] },
    #       "expectMet":true, ... }   # drive YOUR UI with the same inputs; compare fields
    …then cics_close_session {"id":"S1"}. Or the same steps as one cics_run_script {"steps":[...], "state":"auto"} → the transcript plus a dump of every CSD FILE dataset.
  5. sched_demand → sched_wait_cycle — the CA-7 chain, in the order the oracle ran it:
    {"name":"sched_demand","arguments":{"workspace":"W1","job":"POSTTRAN"}}
    # → { "id":"CYC00004", "jobs":[...] }
    
    {"name":"sched_wait_cycle",
     "arguments":{"workspace":"W1","id":"CYC00004","joblogs":"all"}}
    # → { "cycle":"CYC00004", "ended":true, "result":"OK",
    #       "jobs":[{"name":"POSTTRAN","state":"ENDED-OK","jobid":"JOB01001","maxcc":0,
    #                "steps":[...],"joblog":{...}},
    #               {"name":"WAITSTEP","state":"ENDED-OK", ...}],
    #       "runId":"..." }          # compare job order, RCs and outputs with your orchestration
  6. export_state — the after-state, hashed, to diff on your side:
    {"name":"export_state",
     "arguments":{"workspace":"W1","name":"after-posting","records":"rows",
       "datasets":[{"dsn":"AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS","copybook":"CARDDEMO.COPYLIB(CVACT01Y)"},
                   {"dsn":"AWS.M2.CARDDEMO.TRANSACT.VSAM.KSDS","copybook":"CARDDEMO.COPYLIB(CVTRA05Y)"}]}}
    # → { "id":"...", "sha256":"...",
    #       "artifacts":[{"kind":"dataset","name":"AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS",
    #                     "charset":"program","count":N,"sha256":"..."}, ...],
    #       "datasets":{"AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS":{"rows":[...]}, ...} }
  7. destroy_workspace {"id":"W1"} — nothing on the live estate changed.

Live tool reference

Fetched live from GET /api/mcp/tools — this list is always current and is never baked into this page. It is exactly what an MCP client receives from tools/list: each tool shows its description, the read/write/destructive annotation, the required scope, the bound HTTP method/path, and the full input schema (parameters, types, required fields). The "instructions" box is the text the client receives from initialize. Click a tool to expand.