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, workspace-only; the CICS definition tools cics_ceda / cics_cemt / cics_define run live, decided by RACF for your userid). 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

Live on steelframe.cobolstack.com: /mcp (server steelframe, both lanes), /mcp/vscode (steelframe-vscode, scope vscode.drive), /mcp/screen (steelframe-screen, scope screen.drive), /mcp/git (steelframe-git, scope connectors.use) and /mcp/zos (steelframe-zos, scope zos.remote). One API key can carry all five scopes (a Custom key); each door is a separate MCP server in your client. The sections below cover each one.

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": "YOURUSERID",
        "SF_PASSWORD": "YOURPASSWORD"
      }
    },
    "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_USER / SF_PASSWORD are YOUR OWN SteelFrame credentials — no shared account is published. Get a user ID with Get User ID on the logon screen (you choose your own password at first sign-on), then put that userid and password here.

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)" }. Zowe, or your own app reading JES jobs and spool? SteelFrame also answers the z/OSMF jobs and data set REST interface at https://steelframe.cobolstack.com/zosmf/ (HTTP Basic with your userid and password, or a Bearer API key) — see z/OSMF-compatible REST on /api.html.

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

Get a key yourself: log on to the web terminal, open the assistant panel → Credits → API keys, or open your account page → API keys. The key is shown once with ready-made setup snippets (the claude mcp add line above, MCP JSON, mcp-remote, this bridge, curl); pick a preset — Read-only (free tools only), Developer (the write → compile → run loop), Full (everything your user id can do in the terminal) — and set an expiry, a per-key daily spend cap and an IP allow-list. Keys are never wider than your own rights; administrator scopes (sched.admin, workspace.write, act-as) still come from POST /api/tokens (admin). Set SF_TOKEN to the key. 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). Administrator-scoped tools are reachable only with a token an administrator minted explicitly with that scope (POST /api/tokens; never in an API-key preset or a self-service key) or a RACF SPECIAL user's own logon (*), and only on /mcp: sched.admin — sched_import, sched_define_job, sched_ca7_command, sched_order, sched_demand, sched_symbol, sched_options, sched_condition, sched_job_action, sched_cancel, sched_newday, sched_calendar, sched_reset; cics.admin — no tool of its own: it makes cics_ceda / cics_cemt / cics_define act as a CICS administrator (any resource, no group); workspace.write — create_workspace, reset_workspace, destroy_workspace. Never reachable from any MCP door, whatever the token: security / RACF administration and browsing, API-token management, listener, instance, snapshot, application deploy, IMS and Endevor administration, TSO commands, account administration, and connecting a Git account or a mainframe. Seeding tools (load_dataset, stage_gdg, sched_import, sched_define_job) carry the MCP destructive hint and refuse system HLQs; sched_reset is workspace-only. The CICS definition tools (cics_ceda, cics_cemt, cics_define) run on the live estate for any key holding cics.exec, because the engine hands the key's user to the one RACF authority (class SFCSDGRP) that also decides for that user at a 3270 CEDA / CEMT screen: an ordinary user acts only in its own CSD groups (its personal group, named after its userid, plus the groups the security administrator granted), GROUP(g) is required on DEFINE, system-owned entries and other users' groups are refused NOT AUTHORIZED - reason and audited (cics.ceda, door mcp) - so a key never does more than its user can on the 3270, and no scope widens (cics.admin is never on a self-service key). 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; asynchronous — on status:"CREATING" poll get_workspace until RUNNING), 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', name:'…'} boots a child engine on a copy of the live estate (datasets, CSD, scheduler store; the parent's spool is left behind unless includeSpool:true) — or snapshot:<id> for a reproducible CI baseline, or fresh. Creation is asynchronous: the workspace is registered at once with status:"CREATING" (phase, progress) while the copy and the child boot run in the background, and the call answers as soon as it is ready or after waitSeconds (default 20, max 45 — under a 60 s MCP client timeout). On CREATING, poll get_workspace every 10–15 s until status:"RUNNING"; tool calls with workspace:<id> are refused (409 NOT READY) until then. Never call create_workspace again meanwhile: the same name returns the in-flight workspace (reused:true), a different name counts against your cap (429 naming the one still creating). A failed create shows status:"FAILED" with the error (destroy and recreate). Then pass workspace:<id> on any tool call to run it there as yourself; the live estate never changes. reset_workspace re-baselines it the same asynchronous way (optional jobNumber so JOBIDs line up across re-runs); destroy_workspace when done. On a CREATING one it cancels: the estate copy is stopped at once (a queued copy leaves the queue, a running copy process is killed), no child engine boots and the partial directory is removed; the row shows CANCELLING (or DESTROYING for a running one) and counts toward your cap only until that is done — the call waits for it (up to waitSeconds, default 15) and answers settled:true when the row is gone, else poll get_workspace until 404. A create/destroy loop therefore never queues copies ahead of other users. Inside a workspace every tool is checked against the scopes of the token making that call (not the one that created the workspace): no datasets.write → no write_member inside either; the one extra granted inside is cics.admin to a cics.exec holder, so cics_ceda / cics_cemt / cics_define act as a CICS administrator in your own throwaway copy.
  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
CICS definitions (CEDA / CEMT) cics_ceda · cics_cemt · cics_define · cics_csd cics_ceda {"command":"DEFINE LIBRARY(RABILIB) GROUP(RABIAPP) DSNAME01(C826RHA.LOADLIB)"} — ONE CEDA command line (DEFINE / ALTER / DELETE / INSTALL / COPY / DISPLAY / VIEW / CHECK / EXPAND / ADD / REMOVE / LIST; every kind including LIBRARY with DSNAME01..16, RANKING, CRITICAL) through the CEDA transaction's own parser, executor and RACF authority; { ok, lines }. cics_cemt {"command":"SET LIBRARY(RABILIB) DISABLED"} — ONE CEMT line (INQUIRE open; SET / PERFORM / DISCARD authorized; SET FILE also passes CASFILE). cics_define = the JSON form of DEFINE (+install) of one resource, group required for an ordinary user, answering only the entry it defined. A refused or failed CEDA / CEMT command is an MCP error result whose content is still { ok:false, lines } — the E NOT AUTHORIZED - … line plus YOUR CSD GROUPS: …; over REST the same routes answer 200 with ok:false (the 3270 result-list contract the web panel and SDSF rely on). Read-only CEDA verbs need cics.read, everything else cics.exec (which implies cics.read: whoever may run CICS work may look at it); an administrator needs cics.admin only to touch system-owned entries. The AI assistant's direct mode ("via MCP") has the same four tools, acting as the signed-in user. The student flow: DEFINE LIBRARY → PROGRAM → MAPSET → FILE → TRANSACTION (all GROUP(me)) → CHECK GROUP(me) → INSTALL GROUP(me) → run it with cics_open_session / cics_send.
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: sched_reset; inside a workspace cics_ceda / cics_cemt / cics_define act as an administrator) a child engine on a copy of the estate; the live estate never changes. Creation is asynchronous: CREATING → RUNNING | FAILED, polled with get_workspace; name is an idempotency key
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. Which door: with a self-service key (Developer preset) run this chain on POST /mcp?lane=ephemeral — a RAM-only workspace needs only ephemeral.exec, which the preset carries; create_workspace on the persistent /mcp needs the administrator scope workspace.write and answers 403 KEY_SCOPE to a Developer key. The same calls, the same results; nothing survives the workspace. A ready-made project that does exactly this, with Claude Code skills and the CardDemo POSTTRAN job ported and tested end to end, is the modernization playbook kit.

  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.

Pricing (SteelFrame credit)

MCP and API-key calls draw on the same prepaid SteelFrame credit as the AI assistant. Working by hand in the web terminal, Studio and VS Code is never charged.

what the agent does on /mcpclassprice
reads, lists, status, job output, analysisreadfree
writes: members, data sets, records, SQL, job control, CEDA / CEMT changes (cics_ceda DEFINE / ALTER / DELETE / INSTALL / COPY / ADD / REMOVE, cics_cemt SET / PERFORM / DISCARD, cics_define; the read-only verbs DISPLAY / VIEW / CHECK / EXPAND / LIST and CEMT INQUIRE are reads, and a refused command is not charged), exportswrite$0.03 per call
the headless 3270: cics_send (one AID key, the screens it painted)screen$0.01 per key — cics_open_session, cics_screen and cics_close_session are free; cics_run_script is a run
compile_program, check_sourcecompile$0.03
run_program, submit_job, run_batch, run_capture, cics_link, cics_run_scriptrun$0.03 + $0.99/core-hour after the first 10 s of CPU
compile_and_run (private lane)compile + run$0.06 + core time
create_workspace, reset_workspace, destroy_workspaceworkspaceunmetered

Failed and refused calls are free, an identical retry within 60 s is free when nothing of yours changed in between, and every billable result ends with its cost and your remaining balance (_meta['io.steelframe/billing']; each tool's class and price is in its _meta['io.steelframe/price'] in tools/list). At $0 a call answers CREDIT_REQUIRED (do not retry; top up). Each key has its own rolling 24-hour spending cap (DAILY_CAP). The other doors' prices are in their sections below: the same $0.03 write / compile and run rule, and $0.01 for a screen action on /mcp/screen.

The VS Code MCP — your agent drives your own VS Code

A second server, steelframe-vscode at POST /mcp/vscode (same transport and keys as /mcp), lets your agent (Claude Code, Cursor, Claude Desktop, any MCP client) drive your own SteelFrame VS Code seat (code-server + Zowe Explorer, logged on as you) — exactly the tools the AI assistant panel uses there. Every action happens visibly in your VS Code tab: text is typed at human speed, saves go through Zowe and are verified against the mainframe, Java builds and runs stream into the Output panel SteelFrame AI: Java, Python runs and tests into SteelFrame AI: Python, SQL into SteelFrame AI: SQL, and the page shows an amber ring with Your AI agent is working in VS Code (MCP).

Setup (your account page shows these with your key filled in):

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

# .mcp.json / Cursor / Windsurf
{ "mcpServers": { "steelframe-vscode": { "type": "http",
    "url": "https://steelframe.cobolstack.com/mcp/vscode",
    "headers": { "Authorization": "Bearer sf_…" } } } }

Tools and prices (same credit, caps, loop guard and retry rule as /mcp; ledger kind mcp):

toolclassprice
vscode_status, vscode_start, vscode_screen, vscode_open_member, vscode_zowe_filter, vscode_refresh, vscode_job_output, vscode_java_open, vscode_python_open, vscode_dotnet_open, vscode_node_open, vscode_lang_open, vscode_db_schema, vscode_db_queryreadfree (vscode_open_member with create:true is a write)
vscode_type, vscode_set_text, vscode_insert_lines, vscode_save, vscode_java_write, vscode_python_write, vscode_dotnet_write, vscode_node_write, vscode_lang_writewrite$0.03
vscode_run_commandreadfree (the save / undo / redo commands are writes, $0.03)
vscode_db_exec, vscode_db_import, vscode_db_import_vsamwrite$0.03 (a SQL script in one transaction; a DB2 table or a VSAM / sequential data set imported with its parity check)
vscode_java_buildcompile$0.03 (a build that ran and found compile errors counts, like a compile with RC 8)
vscode_python_install, vscode_python_typecheckcompile$0.03 (offline pip install from the seat's wheelhouse; a basedpyright type check)
vscode_lang_build, vscode_lang_installcompile$0.03 (cmake / make / gcc / g++; go build + go vet; bash -n + shellcheck; php -l + composer validate; luac -p; Maven (Kotlin); cargo build; PowerShell parser + PSScriptAnalyzer; go mod tidy, composer update or cargo fetch from the seat's offline sets; a build that ran and found errors counts)
vscode_dotnet_build, vscode_node_build, vscode_node_installcompile$0.03 (dotnet build with packages only from the seat's offline NuGet folder; tsc or ng build; an offline, script-less npm install of what the seat's toolchain lacks; a build that ran and found errors counts)
vscode_java_run, vscode_java_test, vscode_python_run, vscode_python_test, vscode_dotnet_run, vscode_dotnet_test, vscode_node_run, vscode_node_test, vscode_lang_run, vscode_lang_testrun$0.03 + $0.99/core-hour beyond the first 10 s of the run time the seat measured
vscode_submit_jobrun$0.03 + the JES job's core time beyond 10 s, settled when the job ends
vscode_java_serve, vscode_dotnet_serve, vscode_node_serve (coming soon)run$0.03 + $0.99/core-hour beyond the first 10 s of the CPU time the app used, settled when it stops
vscode_app_list, vscode_app_logs, vscode_app_stop (coming soon)readfree

Failed or refused calls are free; the same failing call five times in ten minutes is refused with LOOP_GUARD. The free identical-retry rule (60 s) applies only to tools whose arguments name the action (vscode_java_write, vscode_python_write, the Java build/run/test and the Python run/test/install/type check of a project, the .NET / Node write, build, run, test and install, vscode_open_member); the tools that act on the active editor (vscode_type, vscode_set_text, vscode_insert_lines, vscode_save, vscode_run_command, vscode_submit_job) and vscode_db_exec are charged every time. The same routes exist as REST (POST /api/vscode/<tool>, GET /api/vscode/status) for scripts.

The Git MCP — your agent uses your own GitHub / GitLab

A third server, steelframe-git at POST /mcp/git (same transport and keys as /mcp; only where the operator switched the Connectors on), lets your agent use the GitHub and GitLab accounts you connected in the Connectors tab and the repositories you linked there — to your own data sets (a repository folder ↔ one of your PDSs), your VS Code seat and your seat PostgreSQL database.

claude mcp add -s user --transport http steelframe-git https://steelframe.cobolstack.com/mcp/git \
  --header "Authorization: Bearer sf_…"
toolclassprice
git_accounts, git_repos, git_status, git_diff, git_log, git_branches, git_seat, git_pg_migrationsreadfree
git_link, git_unlink, git_create_repo, git_branch_create, git_branch_switch, git_pull, git_push, git_sync, git_resolve, git_seat_clone, git_pg_migrate, git_pg_exportwrite$0.03

Refused and failed calls are free; an identical retry within 60 s is free when nothing of yours changed in between. The AI assistant panel has the same operations as git_* tools; there, everything that publishes or replaces mainframe text waits for your click on Allow.

The z/OS MCP — your agent works on your own IBM mainframe

A fourth server, steelframe-zos at POST /mcp/zos (same transport and keys as /mcp; only where the operator switched Mainframes on), lets your agent work on your own real IBM mainframe — your company's z/OS, an IBM Z Xplore account, a test system — that you connected in the Mainframes tab (z/OSMF, directly or through a Zowe API ML gateway).

claude mcp add -s user --transport http steelframe-zos https://steelframe.cobolstack.com/mcp/zos \
  --header "Authorization: Bearer sf_…"
toolclassprice
zos_connections, zos_list_datasets, zos_list_members, zos_read_member, zos_list_jobs, zos_job_status, zos_list_spool, zos_read_spoolreadfree
zos_write_member, zos_submit_job, zos_cancel_job, zos_purge_job, zos_copy_to_steelframe, zos_copy_to_mainframewrite$0.03

Refused and failed calls are free; an identical retry within 60 s is free when nothing of yours changed in between. The AI assistant panel has the same operations as zos_* tools, behind the connection's AI chat switches; every save, copy and job waits for your click on Allow unless you turned that off.

The screen MCP — your agent drives your own open SteelFrame page

A fifth server, steelframe-screen at POST /mcp/screen (same transport and keys as /mcp; only where the operator switched it on), lets your agent drive your own open SteelFrame page the way the AI assistant does: the 3270 terminal, the Postgres tab and the tab bar — visibly, with the amber ring, the cursor and typing you can watch.

claude mcp add -s user --transport http steelframe-screen https://steelframe.cobolstack.com/mcp/screen \
  --header "Authorization: Bearer sf_…"
toolclassprice
screen_read, wait_for, read_member, job_output, show_tab, say, pg_tables, pg_open_table, pg_query, pg_explain, pg_export_csvreadfree
type_field, press_key, select_row, editor_set_textscreen$0.01
app_open, app_read (coming soon, with the web apps)readfree (your running web app, in a headless browser that reaches only your own app addresses)
app_click, app_type (coming soon, with the web apps)screen$0.01 (never into a password field)
pg_execwrite$0.03

Refused and failed calls are free; a screen action twice is two actions (no retry waiver).

Where the operator enabled it (it is on here), Anthropic's own Claude Code extension can also run inside your own VS Code seat (you install it and sign in with your own Claude account or your own API key; it is Anthropic's software under Anthropic's terms, billed to your Claude account — SteelFrame credit is not used for it): it then gets these servers with your own rights, and reaches only Anthropic's hosts on the internet (Anthropic's published addresses, and only TLS connections that name the allowed host).

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.