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.
what it is which lane? setup authentication developer loop reference oracle worked oracle example live tool reference REST API (same routes) language catalog back to the terminal
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.
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:
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.
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.
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. |
| Door | What |
|---|---|
| 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. |
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)" }.
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.
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)
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 *.
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.
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:
| Tool | What it does |
|---|---|
| check_source | inline compile-check (real cobc + DB2 precompiler / CICS translator auto-detect), diagnostics with line numbers, no artifacts |
| upload_source | batch-write members: kind
cbl→<HLQ>.SOURCE,
cpy→<HLQ>.COPYLIB,
jcl→<HLQ>.JCL (allocate-on-first-use) |
| compile_program | ONE 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_program | GO deck with STEPLIB
<HLQ>.LOADLIB, DDs from a JSON spec
({lines} instream / {dsn,disp} /
{sysout} / {dummy}), returns RC + SYSOUT |
| JES2 spine | submit_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_wtor | see and answer a blocked batch step's WTOR |
| cancel/purge/hold/release_job | queue actions (destructive ones carry MCP destructive hints) |
| datasets · analysis · query | list/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 |
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:
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 ..." }<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" }-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" }.
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.
GET /api/mcp/tools), audited as your userid.
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.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.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.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).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.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, …).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.| Group | Tools | What 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 |
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").
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.
{"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{"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{"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{"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.{"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{"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":[...]}, ...} }{"id":"W1"} — nothing on the live
estate changed.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.