Documentation
Everything you need to connect a backend, expose it as governed natural-language tools, and put your AI agents in front of it — safely.
Core concepts
Seven ideas run through the whole product:
- System — a connected backend (ServiceNow, Workday, SAP, a Dataverse table, or anything that speaks MCP). One image fronts many systems; you turn them on and off. In MCP terms, a remote server is reached over HTTP (how agents reach EAG, and how EAG federates MCP backends); a local server is a process spoken to over stdio (EAG supports it for desktop development).
- Tool — a canonical, plain-language capability an agent calls by intent (e.g.
case.create,hr.timeoff.balance.get). The catalog is stable; the backend behind a tool can change without the agent noticing. - Scope — a permission string (e.g.
hr.read,case.write) a tool requires. A caller only sees tools their scopes grant. - Mapping — how a tool actually runs on a live backend: which table answers it, whose rows, and how columns become fields. Mock systems serve samples with no mapping; a live system needs one.
- Tier & gating — every tool is read, write, or privileged. Policy gates decide confirm-before-write, and can park a write for a human approver instead of executing it.
- Agent — the application acting for the user, identified by the
azpclaim your identity provider puts in the token. A registered agent carries its own ceiling, which can only ever narrow what its user could already do. - Receipt — a signed, tamper-evident record of an outcome, written to a database you own. Verifiable by anyone holding the public key.
The console
Everything below is done from the admin console (served at /admin), no YAML editing required. Eleven tabs:
- Systems — the fleet: toggle systems on/off, expand a row for tools, gating, config, and Connect.
- Tool catalog — the canonical contract; add custom tools here.
- Mappings — bind a tool to a live table.
- Identity probe — "what would this user see?" before they ask.
- Live audit — every call, streaming, with identity + outcome.
- Approvals — writes parked for a human decision; the tab badges the pending count.
- Agents — the agent registry: which applications may call, and the ceiling each one gets.
- Users — console roles, tool-scope grants, and IdP group mappings.
- Onboard a system — generate a new system's manifest.
- Licensing — install a key, see seats and the expiry countdown.
- Settings — change the identity provider after setup, with discovery and validation.
Use the ☀/◐ button in the header to switch light/dark.
/admin/setup, unlocked by a token printed to the container logs. Pick your provider (Entra, Okta, Auth0, Keycloak, or any OIDC issuer), let it read the discovery document, and the wizard writes the config file for you.Add a system (connect a backend)
Connecting a backend is config, not code — the whole flow lives in the console. It has two halves: generate the manifest (what the system is), then connect & enable it (make it live).
1 · Generate the manifest
Open the Onboard a system tab and fill the form:
- Backend id — a short, stable, lowercase id, e.g.
acme-crm. This is how the system is referenced everywhere (audit, mappings, URLs). - Adapter — the driver that speaks this vendor's protocol. Pick from the list:
servicenow,salesforce,workday,successfactors,ukg,bamboohr,zendesk,sap,snowflake,sql,dynamics365,dataverse,graph(Microsoft 365),azuredevops,entra-identity,mcp-proxy(federate a backend that already speaks MCP — no driver code; eight ready-made templates ship — Jira, Jira Service Management, Confluence, GitHub, HubSpot, Notion, Databricks and ServiceNow's own MCP Server — disabled by default, so you can verify the downstream tool names against the vendor's current server before enabling), ormock(canned sample data, no backend). If your backend has no adapter yet, one has to be built first — see the Adapter Development Guide. - Display name — the human label shown on the system card, e.g. "Acme CRM".
- Base URL — optional; the backend's root URL if the adapter needs one.
- Start in mock mode — leave checked to serve sample data immediately (great for wiring up agents before credentials exist); uncheck when you're ready to go live.
Click Generate YAML to preview the manifest, then Save to manifests/. It always saves disabled and references secrets by env-var name only — nothing sensitive is written. The new system now appears on the Systems tab, off, under its business-domain group.
2 · Connect credentials
On the Systems tab, find the system and click Connect…. The credential dialog is tailored to the adapter and lists its auth methods, safest first (e.g. an integration/service user, OAuth client-credentials, or per-user OAuth On-Behalf-Of). Then:
- Choose the auth method from the dropdown.
- Fill the connection parameters it shows (host, username/password, client id/secret, token URL — whatever that method needs).
- Click Test connection — the gateway spins up a throwaway instance of the adapter and runs a real
health()against the backend. Fix anything that fails here before going live. - Click Save & connect. Secret values go to a gitignored runtime store on the writable data volume (mode 0600); the manifest keeps only the secret names. Secrets never enter git, the image, or the browser.
3 · Verify & enable
- Expand the system's row. Under Secrets (env vars) confirm each referenced secret shows a green ✓ (present), not ✗ (missing).
- Click Run conformance to execute the adapter's test kit against the system (identity isolation, error mapping, health) and read pass/fail inline.
- Flip the toggle switch on. Its tools appear to authorized agents on the next
tools/list. (Reversible — toggling off shows a toast with Undo.)
mock off and add a mapping (next sections) when credentials land. And if the backend has no API yet, model it in Dataverse, serve it live through the same catalog, and repoint to the real backend later — all agent traffic switches in seconds .Create a tool & its scope
The base catalog ships 68 canonical tools across fifteen families, guarded by 19 permission scopes (case.*, hr.*, crm.*, erp.*, work.*, files.*, calendar.*, comms.*, data.*, knowledge.*, identity.*, policy.*, directory.*, approval.*, receipt.* — the full list is on the Tool catalog page). When you need a verb the base doesn't cover — an engagement-specific capability — add a custom tool. It runs through the same policy, gating, and audit pipeline, and can never override a base id.
1 · Add the tool
In Tool catalog, open Add a custom tool and set each field:
- id — dotted and namespaced by domain, e.g.
crm.lead.get. Stable forever once used; agents call it by name. (On the wire, dots become underscores:crm_lead_get.) - tier —
read,write, orprivileged. This drives gating automatically: write tools require a policy/confirm; privileged tools ship blocked until a security sign-off. Choose the least privilege that fits. - classification —
internal,public, orpii. Marks results for audit and redaction; usepiifor anything personal. - scopes — the permission string(s) a caller must carry to see and call the tool, comma-separated, e.g.
crm.read. This is where a scope is defined — you invent the scope name here, then grant it to users (step 3). A tool and its scope must exist before it can be mapped to a system. - description — write it for the model. The agent chooses tools by matching the user's words against this text, so state plainly what it does, what it returns, and any limits. This is the single highest-leverage field for good routing.
- input schema — optional JSON Schema for the tool's arguments, e.g.
The gateway validates every call's args against this and shows it to the agent.{ "type": "object", "properties": { "leadId": { "type": "string" } }, "required": ["leadId"] }
Click Add tool. It appears in the catalog immediately (a custom badge distinguishes it) and is written to a git-trackable file — no code change.
2 · Grant the scope
A tool with a required scope is hidden from anyone who lacks it. On the Users tab, add the scope (e.g. crm.read) to the users or roles who should have it. Use Identity probe to confirm the right people now see the tool and others don't.
3 · Make it do something
A new tool has no backend behind it yet. A mock system will serve it sample data right away; a live system needs a mapping — that's the next section.
Map a tool to live data
A tool's catalog entry says what it is; the mapping says what it does on a real backend — which table answers it, whose rows it returns, and how columns become the tool's output fields. Mock systems serve samples with no mapping; a live system needs one per tool.
1 · Pick the target
In Mappings, choose the system (the live backend) and the tool you're binding. The form below configures how that one tool runs on that one system.
2 · Which table answers it?
Pick the binding mode, then the entity set (the table/collection, e.g. crm_cases):
- one — return a single record (a get-by-id).
- list — return several records (a search/list).
- create — write a new record.
- update — change an existing record. The row is found by identity scope plus your filter, never addressed by an id the agent supplies, so an agent cannot reach a row it could not already read. No match refuses as
not_found; more than one refuses asinvalid_args. - aggregate — group + sum (advanced; configured as JSON).
- handoff — don't call the backend; send the user to a self-service URL instead.
3 · Whose rows?
This is the identity boundary — it decides whether a caller can see only their own data or more:
- Only the caller's — scoped by column — rows are filtered by a lookup column that holds the owner, e.g.
_crm_owner_value. The most common, safest choice. - The caller's own identity row (keyed) — the caller is the record (e.g. their own HR profile).
- Org-wide (explicitly unscoped) — every row, no per-user filter. An opt-in you should choose deliberately (reference data, service-account reads).
4 · Draw the lines (columns → fields)
Map each output field ← table column. The column boxes are dropdowns fed by the system's live table metadata, so you pick real columns, not guesses. Add a row per field. Then, as needed:
- Filter by tool input — an OData-style template using the tool's arguments, e.g.
sn_number eq '{caseId}'. Substitutions are escaped for you. - Result key / order by / max rows — for list mode, name the array, sort it (e.g.
createdon desc), and cap it. - Decode picklists — a choices map turns numeric codes into labels, e.g.
{ "crm_state": { "0": "Active", "1": "Resolved" } }.
5 · Mode extras
- create — set attribute new rows to the caller (a lookup property + its entity set) so records are owned by the signed-in user, plus any defaults JSON (e.g.
{ "crm_state": 0 }). - update — supply only the fields the tool may change; anything you omit is left untouched, so a partial write can never blank a real record.
- handoff — set the message shown to the user and a link label (e.g. "Open CRM").
- Advanced (JSON) — drop to raw binding JSON for anything the form doesn't cover (aggregate is configured here).
6 · Save
Click Save mapping. It's validated server-side (missing columns, bad templates are caught), written to the manifest with a preserved-comment edit, and applied on the very next call — no restart. Test it with Identity probe and watch it run in Live audit.
Govern by tier
Open a system's row and set its gating: write = require-policy / require-approval / off; privileged = block / require-approval / off. Loosening gating prompts for an audited reason — security sign-off is an event, not a checkbox.
Before you hand tools to agents, use Identity probe: type any scope set (or pick a persona) and see the exact tools that identity would be served — allowed, gated, or hidden — with the reason. Governance you can demonstrate, not assert. Everything runs through Live audit: user, tool, system, outcome, latency.
Approve a write
Set a system's write gating to require-approval and writes above the line stop being executed. The agent's call is parked: the gateway returns a typed approval_pending result carrying an approval id, and a named human decides.
1 · Choose who approves
Per system, name the approver as one of:
- A group — anyone holding that IdP group may decide.
- A named person — one specific approver.
- The requester's manager — read live from your directory at request time and then frozen, so a reorg mid-approval can't move the decision.
Nobody can approve their own request, whichever rule you pick.
2 · Decide in the console
Pending items land on the Approvals tab, which badges the count. Each shows the requester, the agent, the tool, and the exact arguments. Approve it, or deny it with a comment — a denial comes back to the agent as a typed gated result carrying your reason, so the agent can tell the user why rather than retrying blindly.
3 · The agent retries
Approval unlocks a single-use grant bound to the tool, a hash of the exact arguments, and the requester. The agent re-runs the identical call under its own fresh token and the grant is consumed. Nothing is replayed on your behalf.
Agents can also check on their own requests: approval.status.get polls one id, approval.list returns the caller's outstanding requests. If nobody ever decides, the grant expires and nothing happened.
Signed receipts
An audit log proves what EAG recorded. A receipt proves what happened, to someone who doesn't trust EAG. Every governed outcome is signed with Ed25519 and carries the requester, the agent, the approver where there was one, the tool, and a SHA-256 of the exact arguments.
Receipts are written to a store you own — a file store out of the box, and a database landing zone (Dataverse, Azure SQL, Snowflake) when you configure one. The write is queued and never blocks a tool call. Alter a row after the fact and verification fails against the public key, which is published at /.well-known/signing-key so anyone can check a receipt without asking you for access.
Two things get certified, and the difference is deliberate:
- Governed outcomes — a write that passed a human approver. The gateway saw every step, so these score highest on trust.
- Agent decisions — any connected agent may call
receipt.certifyto record the decision it just made: its intent, the tools it used, the conclusion it reached. The content is self-reported, but the gateway stamps the requester from the verified token, so the who is never self-reported. These score lower, and visibly so.
Agents with the right scope can read the ledger back through the same catalog — receipt.list and receipt.get — so "show me what this agent did last week" is a tool call, not a support ticket.
The agent registry
EAG knows who the person is with certainty. The registry adds which agent acted for them — and lets you give an agent less than its human, so a read-only research agent cannot write even when the person running it can.
Where agent identity comes from
Every OAuth token carries azp, the application that obtained it, asserted by your identity provider. Registration is mostly a mapping — azp → a named agent with a policy — so there is no new credential scheme to run. Each caller resolves to one of three trust levels:
- attested — the token's
azpmatched a registered agent. This is the only level that grants a ceiling. - declared — only the MCP handshake's self-reported client name matched. Recorded in the audit trail, grants nothing.
- unregistered — neither matched.
Because azp identifies an app registration rather than an instance, the recommended posture is one app registration per agent.
The rule that makes this safe
* means "everything its user has", not "everything". Turning the registry on can therefore never widen anyone's access — only narrow it.What to do on the Agents tab
- Register an agent — give it a name and the
azpfrom your IdP, then a scope ceiling. Audit records and receipts start naming it. - Set the unregistered policy —
openlets unknown callers through as before (the default, so enabling the registry changes nothing until you're ready),restrictlets reads flow but forces writes through approval,denyrefuses them outright. - Pull the kill switch — disable an agent and it stops for every user, at once, without touching a system or a person.
Like users and systems, the registry is a git-trackable YAML file (agents.yaml) that the console writes for you. A malformed registry falls back to today's behaviour and logs loudly — it never fails into a more permissive state.
Users & roles
Your identity provider proves who a caller is — Entra, Okta, Auth0, Keycloak, or any OIDC issuer; the Users tab decides what they may do: a console role (admin / read-only) and granted tool scopes. Add a user by email or directory object id, pick a role, choose their scopes from the live catalog, and save. Unlisted users fall back to the configured default (usually: nothing until granted).
At scale: map groups, not people
Listing every employee doesn't scale, and it duplicates a directory you already maintain. Instead map IdP group claims to scope sets on the same tab: everyone in EAG-Sales gets the CRM scopes, everyone in EAG-Finance gets the ERP ones. Joiners, movers and leavers are then handled entirely in your IdP, and the Users list becomes a short file of deliberate exceptions.
- Entra — enable group claims on the app registration and match by group object id; the claim arrives as a bare GUID, so the name you type beside it is a label for humans.
- Okta and Keycloak — send group names, matched case-insensitively.
Licensing
When it ships, every install will start with a trial — every feature unlocked. A signed key then sets your tier, verified offline (air-gap friendly, no phone-home). In the Licensing tab (or click the plan badge in the header): paste the key, Verify & install. It hot-applies and persists.
The header badge and Licensing tab show a days-remaining countdown (amber at 14 days, red at 5). Renew and install the new key before it lapses, or the gateway locks and tool calls refuse until a valid key is installed. Mock and demo systems never count against live-system caps or the adapter allowlist. (A locked gateway — trial over, no key — refuses all tool calls, mock included, until a key is installed.)
Deploy
EAG ships as one container image, config per customer. Run it on Docker, Azure Container Apps, App Service, on-prem, or fully air-gapped. Point it at your identity provider, mount a writable data volume, and connect your systems from the console. Your systems never leave your cloud.
docker run -d -p 3000:3000 --env-file .env \
-v eag-data:/app/data your-registry/eag-gateway:latest
Console at /admin, MCP endpoint at /mcp. Register that one endpoint in Copilot Studio, Azure AI Foundry, Amazon Bedrock, Claude, or any MCP client, and it fronts your whole governed fleet.
Large catalogs: the compact surface
By default /mcp lists the caller's concrete tools, which is the right shape for most clients and the recommendation up to roughly eighty tools. For hosts whose context a large catalog would swamp, add ?tools=compact to the endpoint URL and the same catalog is served as three meta-tools instead — search_tools, get_tool_schema, invoke_tool — so the agent discovers what it needs and loads only those schemas.
invoke_tool funnels through the same gateway path as a direct call, so every gate — scopes, tier policy, approval, rate limits — applies identically, and the audit trail records the real tool id rather than the wrapper. Search can never surface a tool the caller's identity hides. It is opt-in per client, by URL, so one agent can use it without affecting any other.Get help
Questions, setup help, or a license request — email [email protected]. We read every message.