Skip to main content
Glama

Steward

Steward sits between an AI coding agent (Cursor, in this demo) and monday.com. The agent can read one allowlisted board and propose a change. It cannot write to monday until two things are true: the board’s column contract is frozen, and a person approves the write in a browser.

The monday API token never leaves the Worker. Cursor gets a Steward-issued OAuth token that is only valid at Steward. MCP (Model Context Protocol) is how Cursor calls those tools over HTTP.

Live MCP: https://steward.c-rogs.workers.dev/mcp Approvals: https://steward.c-rogs.workers.dev/approvals

This is a hiring demo on cameron.monday.com (a personal sandbox). It does not talk to monday.monday.com. Default MONDAY_API_TOKEN in a work laptop’s shell is usually the company tenant; Steward refuses that.

Why it is useful

Companies are wiring agents into SaaS APIs. monday’s official MCP (https://mcp.monday.com/mcp) exposes 60+ tools, including all_monday_api, which is raw GraphQL. The agent or the host holds a monday credential. Tool annotations such as readOnlyHint are suggestions to the model. They do not grant or deny anything. There is no tenant allowlist, no frozen schema, and no human gate. A prompt that says “just this once” can mutate whatever that token can reach.

Steward is eight tools with real checks in the handlers. Reads are scoped. Writes go through propose, approve, then commit. Status updates send monday’s stable label id (index). Colour names and UI sort order are rejected. monday has no If-Match; Steward snapshots the item at propose time and refuses the commit if it drifted.

If you are hiring for agent-plus-platform work, this is a control plane: scopes, a frozen board contract, and a human gate.

Related MCP server: mcp-monday

How a write works

  1. Cursor connects to /mcp with no monday header. Unauthenticated calls get 401 plus OAuth metadata (RFC 9728). monday JWTs get a named error, not_a_steward_token.

  2. Cursor registers as an OAuth client (dynamic registration) and runs PKCE. You log in with a demo password and grant steward.read, steward.propose, and steward.commit. Steward mints an opaque token stored in KV. The token’s aud is https://steward.c-rogs.workers.dev/mcp (RFC 8707). A stolen Steward token cannot be replayed at api.monday.com.

  3. freeze_contract inspects the Acme board and stores a versioned snapshot of columns in D1. People columns take user ids, not display names. Subitems live on a different board; a parent-board id for a subitem fails closed.

  4. propose_write validates the patch against that contract, dry-runs it, and stores {patch, expected_cas, status: pending}. It does not call monday mutations. The tool result is pending_approval plus the /approvals URL.

  5. You open /approvals, log in, and click Approve (or Approve & apply). CSRF is checked. Approve does not depend on Cursor still being connected.

  6. commit_proposal requires steward.commit, an approved row, a matching contract, a matching CAS token, and an Idempotency-Key. Then the Worker calls change_multiple_column_values. monday’s create_item has no idempotency; Steward keys (client_id, key) in D1. Same key and same payload return the original result. Same key and a different payload return idempotency_conflict.

  7. Every tool call appends a hash-chained audit row (hashed arguments, not raw args). If you hold the tip hash, a middle-row edit is visible. The chain does not prove the Worker never appended a lie, and a D1 admin can recompute it.

Cursor today speaks 2025-era streamable HTTP. That path cannot receive server-to-client elicitation on a stateless Worker, so /approvals is the gate that always works. Clients on the 2026 protocol that declare elicitation still get inputRequired; confirming there still does not mutate monday.

Toggle off Cursor’s monday-mcp and monday-all servers when you try this, or the agent will write around Steward.

Recorded on production (2026-08-31): freeze board 5103212880 → propose Cutover runbook 3194824173 to Done → pending_approval (monday still not Done) → approve on /approvalscommit_proposal with an Idempotency-Key → Status {index: 1} → replay of the same key returned the original result → D1 audit shows propose_write/pending_approval then commit_proposal/committed.

npx wrangler d1 execute steward --remote --command "select tool, decision, substr(this_hash,1,12) as tip from audit order by id desc limit 8"

What it is built on

One Cloudflare Worker (steward, https://steward.c-rogs.workers.dev). Same process is the OAuth authorization server and the MCP resource server. That is a demo shortcut. A production split would keep Steward as the resource server and let Access, GitHub, or another IdP issue JWTs. Keycloak is not in this project.

Piece

Job

@modelcontextprotocol/server v2 createMcpHandler

Streamable HTTP MCP at /mcp. Per-request server factory. Cloudflare has feature-frozen McpAgent; approval state belongs in D1, not in a session that dies when the isolate hibernates.

@cloudflare/workers-oauth-provider

OAuth 2.1, PKCE, dynamic client registration, protected-resource metadata.

Workers KV (OAUTH_KV)

Issued grants.

D1 (steward)

Contracts, proposals, idempotency keys, hash-chained audit. Interface is StewardRepo; src/store/ords.ts throws not_configured so an Oracle ORDS backend can slot in later.

monday GraphQL, API-Version: 2025-10

Reads and the single mutation path. Account slug must be cameron.

HTML /authorize and /approvals

Mock login (password in DEMO_PASSWORD). No GitHub App required to run the demo.

Eight tools: whoami, list_acme, get_item, freeze_contract, diff_contract, propose_write, list_proposals, commit_proposal. tools/list is filtered by scope. Every handler checks scope again. Advertised annotations (readOnlyHint and friends) do not grant or deny anything.

v1 has no Durable Object and no Workers AI. The Worker calls monday GraphQL directly rather than wrapping @mondaydotcomorg/monday-api-mcp.

Run it locally

Token from mcli-demo / keychain monday-api-token-demo (cameron.monday.com). Do not paste the zshenv MONDAY_API_TOKEN; that is monday.monday.com, account 5. Seed overlays .dev.vars for that reason. The Worker queries me.account.slug and refuses anything except cameron.

cp .dev.vars.example .dev.vars
npm install
npx wrangler d1 migrations apply steward --local
npm run seed          # prints BOARD_ALLOWLIST; put it in wrangler.jsonc vars and .dev.vars
npm run dev

Production secrets: wrangler secret put MONDAY_API_TOKEN, DEMO_PASSWORD, COOKIE_ENCRYPTION_KEY. Never commit .dev.vars.

Cursor: merge a steward entry into ~/.cursor/mcp.json with only "url": "https://steward.c-rogs.workers.dev/mcp". No static headers.

Not in this build

monday webhook verification (UI webhooks have no HMAC), triggerUuid idempotency, GraphQL vs webhook drift, a what_breaks tool, a session complexity budget, and VECTOR memory. Next, not now.

Questions after you try it

  1. Why is official monday MCP an unsafe write surface even if every tool has readOnlyHint / destructiveHint?

  2. Steward issues its own tokens and checks aud. Which RFC is that, and what confused-deputy does it stop?

  3. Cursor documents elicitation. Why does /approvals still exist?

  4. monday’s status mutation field is named index. Why is that not display order, and why reject label and colour on the way in?

  5. create_item has no idempotency. Same Idempotency-Key, different payload: what happens?

  1. Annotations are hints to the model. The host or the agent holds a monday token, all_monday_api is a GraphQL escape hatch, there is no tenant allowlist, no frozen schema, and no human gate. Steward’s handlers re-check steward.commit and will not call change_multiple_column_values unless the proposal is approved.

  2. RFC 8707 (resource indicators). Audience is https://steward.c-rogs.workers.dev/mcp. A monday JWT at /mcp is not_a_steward_token. The monday credential never leaves the Worker.

  3. Elicitation is a capability plus a protocol era. 2026-07-28 input_required needs a client that declared elicitation. Cursor often speaks 2025-era streamable HTTP, per-request, and cannot receive elicitation/create. Returning inputRequired on that path throws inside the SDK after the handler returns. Steward stores the proposal first and always offers /approvals. Confirming in elicitation still does not mutate monday; commit_proposal (or Approve & apply) does.

  4. monday documents that index is the label id, not UI sort order and not the colour name. Palettes and strings change when someone edits the board. The frozen contract maps Done to {index: N}. Incoming label is translated then stripped. Colour keys fail closed.

  5. Steward keys (client_id, key) in D1. Same key and same args_hash return the original result (no second monday call). Same key and a different hash return idempotency_conflict. monday will create two items; Steward will not.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/C-Rogs/steward'

If you have feedback or need assistance with the MCP directory API, please join our Discord server