JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 125
  • Score
    100M100P100Q88637F
  • License MIT

MandateOS MCP server and installer CLIs for Codex, Cursor, and Claude Code.

Package Exports

  • @mandate-os/mcp
  • @mandate-os/mcp/hook-gateway

Readme

@mandate-os/mcp

MandateOS MCP server for agent hosts such as Codex, Cursor, and Claude Code.

This package exposes:

  • Generic MandateOS control-plane tools for issuing mandates, evaluating actions, minting grants, and verifying signatures
  • Enforced adapter tools for the MandateOS-owned GitHub execution routes
  • A stdio entrypoint that can be registered directly as an MCP server

Why this exists

This MCP is intentionally broader than the current GitHub adapters.

  • Use the generic MandateOS tools when your agent still performs the side effect itself and you want MandateOS to act as the policy decision point.
  • Use the enforced adapter tools when MandateOS already owns the side effect and should act as both the policy decision point and the enforcement point.

Environment

The MCP server expects:

  • MANDATE_OS_BASE_URL
  • MANDATE_OS_AGENT_TOKEN

Optional defaults:

  • MANDATE_OS_MCP_DEFAULT_MANDATE_ID
  • MANDATE_OS_MCP_DEFAULT_SOURCE
  • MANDATE_OS_MCP_SERVER_NAME
  • MANDATE_OS_MCP_SERVER_VERSION

The API key behind MANDATE_OS_AGENT_TOKEN should usually include:

  • control-plane:read for workspace and mandate context
  • mandates:read and receipts:read for mandate and receipt lookups
  • simulate:write for live runtime evaluation, execution-grant issuance, and enforced GitHub execution routes

If the host only reads control-plane state, keep scopes narrower. If it must run live OpenClaw or enforced GitHub policy flows, simulate:write is required.

One-Command Codex Install

If you want Codex to pick up MandateOS as the default path in a workspace, use the published installer CLI:

MANDATE_OS_BASE_URL=http://localhost:4330 \
MANDATE_OS_AGENT_TOKEN='key_id.secret' \
MANDATE_OS_MCP_DEFAULT_MANDATE_ID='mdt_123' \
npx --yes --package @mandate-os/mcp@latest mandate-os-codex-install install \
  --workspace /absolute/path/to/your/repo

That command:

  • updates /absolute/path/to/your/repo/.codex/config.toml with [features].codex_hooks = true and a project-scoped mandateos MCP entry
  • updates /absolute/path/to/your/repo/.codex/hooks.json with a MandateOS PreToolUse hook for Bash
  • adds .codex/config.toml and .codex/hooks.json to .git/info/exclude when the workspace is a Git repository

When the installer itself is invoked through npx, MandateOS writes package-based runtime commands so the generated Codex config does not depend on one transient npm _npx cache path.

The default installer uses all bundled starter rule files:

  • local-workspace.json
  • release-platform.json
  • docs-content.json
  • finance-support.json

You can inspect what is installed with:

npx --yes --package @mandate-os/mcp@latest mandate-os-codex-install status \
  --workspace /absolute/path/to/your/repo

Useful install flags:

  • --no-project-mcp to skip workspace .codex/config.toml MCP registration
  • --no-project-hooks to skip workspace .codex/hooks.json
  • --no-hooks-feature to skip enabling [features].codex_hooks
  • --no-git-exclude to skip .git/info/exclude updates
  • --rules-files /a.json,/b.json to override the bundled starter rules
  • --unmatched-permission allow|ask|deny to control how unmatched shell actions are handled

The current tested trust boundary for Codex is project-scoped PreToolUse on Bash:

  • Codex discovers project-local .codex/config.toml and .codex/hooks.json from the active repository
  • current Codex hooks only emit PreToolUse and PostToolUse for Bash, so generic MCP-side interception is not available yet
  • Codex currently parses permissionDecision: "ask" but does not enforce it, so MandateOS maps both ask and deny into a blocking Codex hook response until native ask enforcement exists

One-Command Cursor Install

If you want Cursor to pick up MandateOS as the default path in a workspace, use the published installer CLI:

MANDATE_OS_BASE_URL=http://localhost:4330 \
MANDATE_OS_AGENT_TOKEN='key_id.secret' \
MANDATE_OS_MCP_DEFAULT_MANDATE_ID='mdt_123' \
npx --yes --package @mandate-os/mcp@latest mandate-os-cursor-install install \
  --workspace /absolute/path/to/your/repo

That command:

  • updates ~/.cursor/mcp.json with a global mandateos MCP entry
  • updates /absolute/path/to/your/repo/.cursor/mcp.json with a workspace override
  • updates /absolute/path/to/your/repo/.cursor/hooks.json with MandateOS beforeShellExecution and beforeMCPExecution hooks

The default installer uses all bundled starter rule files:

  • local-workspace.json
  • release-platform.json
  • docs-content.json
  • finance-support.json

You can inspect what is installed with:

npx --yes --package @mandate-os/mcp@latest mandate-os-cursor-install status \
  --workspace /absolute/path/to/your/repo

Useful install flags:

  • --no-user-mcp to skip ~/.cursor/mcp.json
  • --no-project-mcp to skip workspace .cursor/mcp.json
  • --no-project-hooks to skip workspace .cursor/hooks.json
  • --rules-files /a.json,/b.json to override the bundled starter rules
  • --unmatched-permission allow|ask|deny to control how unmatched shell or MCP actions are handled

The current tested trust boundary is Cursor desktop. In local testing:

  • Cursor desktop loaded the MandateOS MCP and the MandateOS project hooks
  • direct gh issue edit ... --add-label ... was blocked in the desktop app and redirected to mandateos_execute_enforced_action
  • cursor-agent --print did not invoke beforeShellExecution, so it should not yet be treated as equivalent to the desktop enforcement surface

One-Command Claude Code Install

If you want Claude Code to pick up MandateOS as the default path in a workspace, use the published installer CLI:

MANDATE_OS_BASE_URL=http://localhost:4330 \
MANDATE_OS_AGENT_TOKEN='key_id.secret' \
MANDATE_OS_MCP_DEFAULT_MANDATE_ID='mdt_123' \
npx --yes --package @mandate-os/mcp@latest mandate-os-claude-install install \
  --workspace /absolute/path/to/your/repo

That command:

  • updates ~/.claude.json with a local-scoped mandateos MCP entry for that workspace
  • updates /absolute/path/to/your/repo/.claude/settings.local.json with MandateOS PreToolUse hooks for Bash and mcp__.*
  • adds .claude/settings.local.json to .git/info/exclude when the workspace is a Git repository

The default installer uses all bundled starter rule files:

  • local-workspace.json
  • release-platform.json
  • docs-content.json
  • finance-support.json

You can inspect what is installed with:

npx --yes --package @mandate-os/mcp@latest mandate-os-claude-install status \
  --workspace /absolute/path/to/your/repo

Useful install flags:

  • --no-local-mcp to skip the workspace entry inside ~/.claude.json
  • --no-local-hooks to skip workspace .claude/settings.local.json
  • --no-git-exclude to skip .git/info/exclude updates
  • --rules-files /a.json,/b.json to override the bundled starter rules
  • --unmatched-permission allow|ask|deny to control how unmatched shell or MCP actions are handled

The current tested trust boundary for Claude Code is the Claude Code CLI and local project settings:

  • the local-scoped mandateos MCP entry was loaded from ~/.claude.json
  • the local PreToolUse hooks were loaded from .claude/settings.local.json
  • direct gh issue edit ... --add-label ... was blocked and redirected to mandateos_execute_enforced_action

Tool surface

Generic workflow tools:

  • mandateos_get_context
  • mandateos_get_policy_catalog
  • mandateos_issue_mandate
  • mandateos_evaluate_actions
  • mandateos_issue_execution_grant
  • mandateos_verify_mandate
  • mandateos_verify_receipt
  • mandateos_verify_execution_grant

Enforced adapter tools:

  • mandateos_execute_enforced_action
  • legacy aliases remain available for compatibility, including mandateos_execute_github_issue_label and mandateos_execute_github_pull_request_draft

Hooks and Host Enforcement

MCP makes MandateOS available to the agent. Hooks are what help make MandateOS the default path instead of an optional one.

The important architectural point is:

  • hooks should not replace MandateOS policy
  • hooks should intercept host activity and ask MandateOS whether that activity is allowed

In other words, the hook is the local gate and MandateOS remains the central policy decision point.

The most useful hook surfaces today are:

  • Cursor beforeShellExecution as the main bypass blocker for direct shell-based side effects
  • Cursor beforeMCPExecution as the main bypass blocker for side-effecting tools exposed by other MCP servers
  • Codex PreToolUse as the current Bash-side bypass blocker inside a project-scoped .codex/hooks.json
  • prompt and post-tool hooks as reminders, audit helpers, or review layers rather than the primary enforcement point

If you want MandateOS to sit in front of "anything dangerous", the practical pattern is:

  1. Register this MandateOS MCP server.
  2. Allow all mandateos_* tools.
  3. Use beforeShellExecution to intercept direct provider or mutation commands such as gh, curl, kubectl, terraform, aws, gcloud, npm publish, or git push, then call MandateOS to decide whether they are allowed.
  4. Use beforeMCPExecution to intercept side-effecting tools from non-MandateOS MCP servers, then call MandateOS to decide whether they are allowed.
  5. Keep provider credentials out of the agent process whenever possible.
  6. For generic workflows, require a fresh MandateOS receipt before allowing the side effect.
  7. For enforced adapters, deny the direct path and force the agent onto mandateos_execute_enforced_action or a supported legacy alias.

The practical hook flow is:

  1. The host calls beforeShellExecution, beforeMCPExecution, or PreToolUse.
  2. The hook normalizes the attempted command or tool call into a MandateOS action shape.
  3. The hook calls MandateOS, usually through @mandate-os/sdk or a direct API request.
  4. The hook maps the MandateOS decision back to the host's local allow or block mechanism.

For example:

  • gh issue edit --add-label bug becomes a github.issue.label action proposal
  • kubectl apply -f prod.yaml becomes a deployment or cluster mutation action proposal
  • terraform apply becomes an infrastructure mutation action proposal
  • an unknown but side-effecting command can fall back to a coarse action like shell.command.execute and require approval or deny-by-default

Cursor's hooks docs currently describe beforeShellExecution and beforeMCPExecution as running before any shell command or MCP tool call, and they support failClosed: true, which is the right default for security-sensitive MandateOS hooks:

Example hooks.json:

{
  "version": 1,
  "hooks": {
    "beforeSubmitPrompt": [
      {
        "type": "prompt",
        "prompt": "If this task could change an external system, use MandateOS first. Prefer mandateos_execute_* when available. Otherwise issue or load a mandate and call mandateos_evaluate_actions before continuing.",
        "timeout": 10
      }
    ],
    "beforeShellExecution": [
      {
        "command": "node /absolute/path/to/dist/packages/mandate-os-mcp/hook-gateway.js cursor before-shell",
        "timeout": 5,
        "failClosed": true
      }
    ],
    "beforeMCPExecution": [
      {
        "command": "node /absolute/path/to/dist/packages/mandate-os-mcp/hook-gateway.js cursor before-mcp",
        "timeout": 5,
        "failClosed": true
      }
    ]
  }
}

That built hook gateway reads:

  • MANDATE_OS_BASE_URL
  • MANDATE_OS_AGENT_TOKEN
  • MANDATE_OS_MCP_DEFAULT_MANDATE_ID
  • MANDATE_OS_MCP_DEFAULT_SOURCE (optional)
  • MANDATE_OS_HOST_GATEWAY_UNMATCHED_PERMISSION with ask by default
  • MANDATE_OS_HOST_GATEWAY_RULES_FILES for a comma-separated list of starter or custom bundle files
  • MANDATE_OS_HOST_GATEWAY_RULES_JSON or MANDATE_OS_HOST_GATEWAY_RULES_FILE for custom domain rules

Included starter bundles:

  • dist/packages/mandate-os-mcp/rules/starter-bundles/local-workspace.json
  • dist/packages/mandate-os-mcp/rules/starter-bundles/release-platform.json
  • dist/packages/mandate-os-mcp/rules/starter-bundles/docs-content.json
  • dist/packages/mandate-os-mcp/rules/starter-bundles/finance-support.json

Example:

export MANDATE_OS_HOST_GATEWAY_RULES_FILES="/absolute/path/to/dist/packages/mandate-os-mcp/rules/starter-bundles/release-platform.json,/absolute/path/to/dist/packages/mandate-os-mcp/rules/starter-bundles/docs-content.json"

Example beforeShellExecution logic if you want to customize your own wrapper instead of calling the built helper directly:

import { readFileSync } from 'node:fs';
import { MandateOsAgentClient } from '@mandate-os/sdk';
import { createMandateOsHostGateway, toCursorHookResponse } from '@mandate-os/mcp';

const input = JSON.parse(readFileSync(0, 'utf8'));
const gateway = createMandateOsHostGateway({
  client: new MandateOsAgentClient({
    baseUrl: process.env.MANDATE_OS_BASE_URL,
    bearerToken: process.env.MANDATE_OS_AGENT_TOKEN,
  }),
  defaultMandateId: process.env.MANDATE_OS_MCP_DEFAULT_MANDATE_ID,
  defaultSource: 'cursor.beforeShellExecution',
  hostName: 'cursor',
});

const result = await gateway.evaluateShellCommand({
  host: 'cursor',
  source: 'cursor.beforeShellExecution',
  command: String(input.command || ''),
  cwd: typeof input.cwd === 'string' ? input.cwd : null,
});

console.log(JSON.stringify(toCursorHookResponse(result)));

Example beforeMCPExecution logic:

import { readFileSync } from 'node:fs';
import { MandateOsAgentClient } from '@mandate-os/sdk';
import { createMandateOsHostGateway, toCursorHookResponse } from '@mandate-os/mcp';

const input = JSON.parse(readFileSync(0, 'utf8'));
const gateway = createMandateOsHostGateway({
  client: new MandateOsAgentClient({
    baseUrl: process.env.MANDATE_OS_BASE_URL,
    bearerToken: process.env.MANDATE_OS_AGENT_TOKEN,
  }),
  defaultMandateId: process.env.MANDATE_OS_MCP_DEFAULT_MANDATE_ID,
  defaultSource: 'cursor.beforeMCPExecution',
  hostName: 'cursor',
});

const result = await gateway.evaluateMcpToolCall({
  host: 'cursor',
  source: 'cursor.beforeMCPExecution',
  toolName: String(input.tool_name || ''),
  toolInput: input.tool_input,
  serverCommand: typeof input.command === 'string' ? input.command : null,
  serverUrl: typeof input.url === 'string' ? input.url : null,
});

console.log(JSON.stringify(toCursorHookResponse(result)));

Hooks are still defense-in-depth, not the entire trust boundary.

  • If the agent still has raw GitHub, cloud, payment, or deployment credentials, it may still bypass MandateOS through some other route.
  • The strongest setup is: MandateOS MCP exposed, direct shell or tool bypasses blocked with hooks, and external credentials held only by MandateOS.
  • The ideal long-term setup is a small MandateOS host-gateway helper that hooks call directly, so policy translation and receipt handling stay consistent across Cursor, Codex, and other hosts.

Registering the server

After building:

pnpm mandate-os:mcp:build

register the built stdio command in your MCP-capable host:

node /absolute/path/to/dist/packages/mandate-os-mcp/index.js

with the MandateOS env vars above injected into the server process.