Connect an agent
Everything an engineer needs to point an MCP client at this range. The gateway speaks Streamable HTTP JSON-RPC at three separately authorized surfaces, and refuses anything a person has not granted. Access is provisioned per organization — the endpoints below answer with a 401 challenge until yours is approved.
1. Endpoints
https://your-range.example/api/public/mcp/controlDiscover Cybrary Forge activities, plan exercises and read authorized results. Read tools never enroll anyone or start a run; run control lives behind explicit scopes.
Start with catalog.read
https://your-range.example/api/public/mcp/runOperate inside a live simulated range run as a bound seat. Every mutation goes through the server-authoritative reducer; hidden objectives, seeds and answer keys are never returned.
Start with runtime.observe
https://your-range.example/api/public/mcp/authorDraft, validate and submit range activities for human review. Publishing is deliberately not available over MCP.
Start with author.draft
1c. Audit evidence on demand
Every gateway call is sealed into your organization's hash chain. Call audit.export on the control surface to pull that record back as a verified evidence bundle: the sealed events, a chain verdict, tool and refusal tallies, and a SHA-256 digest over the bundle itself. Narrow it with runHandle or since, and pass format: "markdown" for a file an auditor can read without tooling. It only ever returns your own organization's events, and never request payloads.
If verified comes back false, the bundle names the sequence number where the chain stops adding up — treat that as an integrity incident, not a bad export.
1d. Preflight before you act
Call policy.preflight on the control surface with { "tool": "run.action" } to ask whether a call would be allowed before you make it. It runs the same ten gates over the same snapshot, charges no quota and executes nothing, then answers with the verdict, the gate that would refuse, the scopes your token is missing, whether a human approval is required, and how much of today's budget is left.
Use it to plan multi-step work: a destructive step that would stall on approval is far cheaper to discover at plan time than halfway through a containment sequence. The tool you name may live on any surface — the report tells you which one to send the real call to.
1b. Live run events
An exercise director can drop new tasking into a run from the control surface, and no amount of polling makes that arrive on time. Open a Server-Sent Events stream on the run surface instead: it pushes injects, solved objectives and lifecycle changes as they happen. It needs the runs.read scope and the same bearer token, and it can only read the run your registration is bound to.
curl -N \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: text/event-stream" \ "https://your-range.example/api/public/mcp/run?run=$RUN_HANDLE"
Each frame is a JSON-RPC notifications/message whose id is the run revision. Connections are capped at five minutes and end with a run.detached frame telling you whether to reconnect; because every frame is derived from the durable run state rather than from server memory, reconnecting loses nothing.
2. Interactive agents: OAuth
Add the surface URL as a remote MCP server in a host such as Claude, ChatGPT or Cursor. An unauthenticated call returns the RFC 9728 challenge below; the host reads the protected-resource metadata, discovers this range's authorization server, and runs OAuth 2.1 with PKCE. A person approves the connection on the consent screen once.
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://your-range.example/.well-known/oauth-protected-resource/api/public/mcp/control"
A fresh connection carries catalog reads and nothing else. Everything past that — creating runs, acting inside one, exporting results — is widened by an owner in the portal, per agent.
3. Unattended agents: signed assertions
A scheduled job has nobody to click consent. An owner mints a service account in the portal and stores the private key; the job signs its own bearer per call. The audience must be the exact surface URL, the assertion must carry a unique jti, and it may live at most 300 seconds.
// Node 18+, using jose. The private JWK came from the portal, once.
import { SignJWT, importJWK } from "jose";
const key = await importJWK(JSON.parse(process.env.RANGE_PRIVATE_JWK), "ES256");
const resource = "https://your-range.example/api/public/mcp/control";
const assertion = await new SignJWT({})
.setProtectedHeader({ alg: "ES256" })
.setIssuer(process.env.RANGE_CLIENT_ID) // svc:...
.setSubject(process.env.RANGE_CLIENT_ID) // same value
.setAudience(resource) // the exact surface URL
.setJti(crypto.randomUUID())
.setIssuedAt()
.setExpirationTime("300s")
.sign(key);
await fetch(resource, {
method: "POST",
headers: {
authorization: `Bearer ${assertion}`,
"content-type": "application/json",
"mcp-protocol-version": "2026-07-28",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "activity.search", arguments: { query: "phishing" } },
}),
});4. Scope catalog
| Scope | Surface | Grants | Risk |
|---|---|---|---|
arena.read | control | Read Arena events you can already see: standings, schedule and (for staff) activity counts. | low |
arena.staff | control | As event staff: announce, freeze the board, move schedule times and create big-screen links. Each needs your approval. | high · approval |
ranges.read | control | Read your Forge Ranges and their latest scenario runs. | low |
anvil.read | control | Ask your Anvil engagements questions and run evidence-quality checks (cited extracts only). | low |
anvil.draft | control | Draft findings from chosen evidence. Drafts are never saved or exported without a human reviewer. | low |
anvil.evidence | control | Show the screenshots saved to your Anvil engagements, as pictures (short-lived links). Evidence can be sensitive, so this is not part of the default access. | medium |
approvals.respond | control | Let this client record your approval of a pending action from inside the assistant. Without it an assistant can only decline; approving happens in the Forge portal or in chat. | high · approval |
catalog.read | control | Read entitled activity metadata and recommendations. | low |
assignments.read | control | Read learner and team assignments. | low |
assignments.write | control | Create learner and team assignments. | medium |
runs.create | control | Create a run and configure it before start. | medium |
runs.read | control | Read run status and metadata. | low |
runs.control | control | Start, pause, resume or end a run. | high |
runs.inject | control | Schedule or trigger instructor injects. | high |
results.read | control | Read scorecards and redacted timelines. | medium |
results.export | control | Export reports and evidence within policy. | high |
agents.connect | control | Bind an agent registration to a run seat. | high |
runtime.observe | run | Use investigation and context tools inside a run. | medium |
runtime.act | run | Take reversible or low-impact simulated response actions. | high |
runtime.respond.high_impact | run | Containment, broad block or irreversible simulated action. | critical · approval |
author.draft | author | Create and edit private activity drafts. | medium |
author.validate | author | Run schema, completeness, fuzz and localization validation. | medium |
author.submit | author | Submit a draft for human review. | high |
tenant.admin | control | Manage identity, connections and policy. | critical · approval |
progress.read | control | Read your own graded progress, scores and credentials. | low |
insights.read | control | Read usage reports you are allowed to see (your own; your company if you administer it). | low |
connectors.read | control | Read integration status for companies you administer (field hints and feed health, never secret values). | low |
connectors.propose | control | Propose integration changes for a company you administer. Each proposal waits for a person's approval on screen. | medium |
5. When a call is refused
Every call passes ten gates before it executes, and every decision — allow or deny — is written to a hash-chained audit trail the organization can read. Denials are structured: the error data carries the gate that stopped you and one of these codes.
| Code | Meaning | What to do |
|---|---|---|
unauthorized | No token, or the token expired. | Re-run the OAuth flow, or mint a fresh assertion. |
invalid_audience | The token was issued for a different surface. | Request a token whose resource is the exact surface URL you call. |
tenant_inactive | An operator or owner stopped the organization. | Ask an owner to re-enable agents in the portal. |
client_revoked | The agent registration or service-account key was revoked. | Register the agent again, or mint a new key. |
client_expired | The registration passed its expiry. | Ask an owner to renew it in the portal. |
insufficient_scope | The tool needs a scope the token does not carry. | Have an owner widen the grant, then request a token with that scope. |
not_entitled | The activity is outside the organization's catalog. | Pick an activity returned by activity.search. |
not_released | The activity is not generally available yet. | Use a released activity, or ask for early-access entitlement. |
rate_limited | Hourly or daily call budget is spent. | Call usage.status on the control surface to read remaining quota and reset times, then back off until the window rolls. |
run_frozen | An instructor paused the run. | Poll run status and resume work when it unfreezes. |
replayed | The same idempotency key already executed. | Read the original result; do not retry with a new key. |
6. What the gateway will never do
- Return a seed, answer key, hidden objective or database identifier — handles only.
- Publish an activity. Drafting and submitting are agent work; publishing is a human action in the portal.
- Execute a containment or other irreversible simulated action without an explicit approval step.
- Cross organizations. A handle minted for one org reads as not found in another.