ulams for AI agents (MCP)
ulams mcp serves the CLI’s commands as Model Context Protocol tools. Tools,
the CLI and the reference come from one registry, so they cannot drift apart
(ADR 0076). The server speaks the 2026-07-28 spec (stateless,
no sessions) through the official TypeScript SDK v2.
Run it
Section titled “Run it”ulams login --url http://coffee.localhost --demo admin # once; the MCP client config then holds no secretulams mcp # stdio: the default, for local clientsulams mcp --http --port 8787 # Streamable HTTP on http://127.0.0.1:8787/mcp- stdio uses the credentials of the current profile (
--profile) orULAMS_URL+ULAMS_TOKEN. Logs go to stderr; stdout carries only the protocol. - HTTP binds loopback. Every request must send
Authorization: Bearer <ulams token>; the server forwards it to the API per request and stores nothing (401 withWWW-Authenticateotherwise).--host 0.0.0.0needs--allow-remote. File access is disabled in this mode.
Which tools
Section titled “Which tools”The default toolset core keeps the list short: whoami, courses, lessons, topics_create_richtext, topics_create_quiz,
users, access_grant, reports_metrics, plus three meta tools that reach everything else:
| Tool | Use |
|---|---|
commands_search |
find a command by keyword (all toolsets) |
commands_describe |
the input JSON Schema and examples of one command |
commands_run |
run any exposed command by id, with the same confirmation rules |
--toolsets core,courses,users,settings adds the generated tools of those nouns (a toolset is the first word of the command:
courses, lessons, topics, users, groups, settings, reports, builder, living, …; ulams schema shows the
toolset of every command as mcp.toolset). --toolsets all exposes every command that is not local to the CLI. The
meta tools search and run all commands the server’s modes allow, whatever the toolsets, so a small server can still reach
everything. --read-only exposes read commands only; --no-destructive hides destructive ones (deletes, replaces).
| Server started with | Tools |
|---|---|
ulams mcp |
23: the core set and the three meta tools |
--read-only |
14 |
--toolsets core,courses,users |
43 |
--toolsets core,builder,living |
88 |
--toolsets all |
535 (258 with --read-only, 476 with --no-destructive) |
These counts are for CLI 0.1.0 and grow with the API. Clients that cap the number of tools work best with the default set.
login, logout, mcp, api, apply, schema, describe, completion, config and profiles are never tools: they are
local to the CLI or need a file system. Commands that read or write local files (topics_create_scorm, uploads, exports)
work over stdio only.
Every tool carries annotations from its kind: readOnlyHint for reads, destructiveHint for deletes and replaces,
idempotentHint, openWorldHint: false. Results are the CLI envelope as structuredContent (and compact JSON text, at most
25 kB; longer lists are truncated with a TRUNCATED warning, narrow them with per_page or fields).
The course builder and Living Course
Section titled “The course builder and Living Course”--toolsets core,builder,living adds the builder (builder_start, builder_interview_answer, builder_outline_approve,
builder_apply, builder_chat, …) and the Living Course tools (living_proposals_list, living_proposals_accept,
living_proposals_apply, …). A model can build a course from a Markdown file in a few calls, reviewing every stage:
Build courses from the command line. Tools that start AI runs take wait and timeout_seconds
(default wait capped at 55 s); a wait that runs out returns status: "running" with a handle for operations_wait.
Safety
Section titled “Safety”- Every write tool accepts
dry_run: true: the plan, and for updates a diff, with no change. - A destructive tool called without
confirmmakes no change. It returns an error resultCONFIRMATION_REQUIREDwhosedetailshold the plan and a single-useconfirmtoken (valid 5 minutes, bound to the exact input, kept in memory, an HMAC with a key made at server start). The model shows the plan to the user, then calls again with the same input and the token. A changed input, a second use or an expired token is refused with a new plan. --unsafe-yesdisables the confirmation; use it only in a throw-away sandbox.- The API enforces the user’s permissions (and, with scoped tokens, the token’s scopes), so an MCP client can never do more
than the user behind the token. Prefer a read-only token or
--read-onlyfor exploration. - Each request carries
X-Ulams-Client: mcpand the agent name (ULAMS_AGENT), which the agent audit log records for scoped tokens.
The confirmation flow
Section titled “The confirmation flow”A destructive tool, for example courses_delete (toolset courses) or any command through commands_run:
// tools/call commands_run{ "id": "courses.delete", "input": { "id": 111 } }// result, isError: true, structuredContent:{ "ok": false, "command": "courses.delete", "error": { "code": "CONFIRMATION_REQUIRED", "message": "courses.delete is destructive.", "details": { "plan": { "request": { "method": "DELETE", "path": "/api/admin/courses/111" }, "current": { "id": 111, "title": "Tmp", "lessons": [] } }, "confirm": "1791564602299.ebd4ddd4…" } } }// after the user agreed: the same call plus the token{ "id": "courses.delete", "input": { "id": 111 }, "confirm": "1791564602299.ebd4ddd4…" }Security notes
Section titled “Security notes”- Credentials. stdio uses the profile token or
ULAMS_TOKEN; it is held in the process and never written to stdout or to a tool result (results are redacted). Give an agent its own scoped token, not your login:ulams tokens create --name claude --scopes @author --kind agent --agent-name claude-code --expires-in-days 30(scoped tokens). - HTTP binds loopback only.
--hostwith another address needs--allow-remoteand prints a warning: anyone who reaches the port with a valid ulams token can then use it, so put TLS and a firewall in front. The server stores no session and no credential; the bearer token of each request is the credential of that call. File access is off. - Prompt injection. Course content, learner submissions and source documents are untrusted input and can contain text
that addresses the model. Keep
--read-onlyfor agents that read such content, and keep confirmation on for the rest. - Scope the toolsets. A small toolset lists fewer capabilities; the API still refuses what the token may not do.
Connect a client
Section titled “Connect a client”The server is the same ulams mcp process for every client; only the way the client starts it differs.
| Client | Setup |
|---|---|
| Claude Code | claude mcp add ulams -- ulams mcp --profile coffee (guide) |
| Claude Desktop | claude_desktop_config.json, see the guide |
| Any stdio client | command ulams, args ["mcp", "--profile", "coffee"], or env ULAMS_URL and ULAMS_TOKEN instead of a profile |
| Any Streamable HTTP client | URL http://127.0.0.1:8787/mcp with the header Authorization: Bearer <ulams token> |
A stdio client’s JSON configuration (the exact key names differ a little between clients):
{ "mcpServers": { "ulams": { "command": "ulams", "args": ["mcp", "--toolsets", "core,courses", "--read-only"], "env": { "ULAMS_URL": "https://school.example.com", "ULAMS_TOKEN": "ulams_pat_…" } } }}Over HTTP, for Claude Code: claude mcp add --transport http ulams http://127.0.0.1:8787/mcp --header "Authorization: Bearer $ULAMS_TOKEN".
The same call by hand, to see what a client sees:
ulams mcp --http --port 8787 &curl -s http://127.0.0.1:8787/mcp -X POST \ -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \ -H "Authorization: Bearer $ULAMS_TOKEN" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'# event: message# data: {"result":{"content":[{"type":"text","text":"{\"ok\":true,…}"}],"structuredContent":{"ok":true,"command":"whoami",…}},"jsonrpc":"2.0","id":1}Without the header the server answers 401 with WWW-Authenticate: Bearer realm="ulams"; any path other than /mcp is 404.
Rules that make agents reliable
Section titled “Rules that make agents reliable”- Call
whoamifirst. 2. Use--jsonorulams schemafor discovery in the shell,commands_searchandcommands_describein MCP. - Pass whole inputs as JSON (
--input @file.json). 4. List before you act (courses_list) instead of guessing ids. - Use
dry_runbefore writes. 6. Passfieldsto keep results short.
Resources
Section titled “Resources”ulams://courses/{id} is the course outline as Markdown (title, status, lessons, topics) and
ulams://courses/{id}/program.json the full object; resources/list returns the first 100 courses, and
resources/templates/list the two templates. Resources are read with the same credentials and the same API permissions as tools.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and fix |
|---|---|
| The client shows the server as failed or “Connection closed” | run ulams mcp in a terminal: AUTH_REQUIRED means no profile (ulams login) or no ULAMS_URL/ULAMS_TOKEN; the message is on stderr |
command not found: ulams in Claude Desktop |
Desktop does not read your shell PATH: use absolute paths to node and ulams.mjs |
| Garbage or parse errors on the protocol | something printed to stdout; the server prints only protocol on stdout, so check wrappers (shell profiles that echo) |
| A tool is missing | it is in another toolset: add --toolsets, or use commands_search and commands_run; --read-only and --no-destructive hide writes and deletes |
CONFIRMATION_REQUIRED |
by design for destructive tools: show the plan, then call again with details.confirm (5 minutes, single use) |
FORBIDDEN or SCOPE_MISSING |
the user or the token may not do it: whoami shows the scopes; create a token with the scope |
FEATURE_DISABLED |
the instance has the feature off (builder, Living Course, platform): curl <url>/api/meta |
"status": "running" with a handle |
a long run outlived the 55 s wait: call operations_wait with the handle |
TRUNCATED warning |
the result exceeded 25 kB: use per_page, limit or fields |
HTTP 401 |
the request lacks Authorization: Bearer <token> |
Checking an agent against a real model
Section titled “Checking an agent against a real model”yarn workspace ulams eval runs a real model against ulams mcp on a local tenant and checks the result through the API
(it costs money, so it is not part of CI; the budget defaults to 1 USD and stops at 30 tool calls per task). Settings and
reports: front/cli/evals.
See also: the CLI guide, Use ulams from Claude Code.