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_URLMANDATE_OS_AGENT_TOKEN
Optional defaults:
MANDATE_OS_MCP_DEFAULT_MANDATE_IDMANDATE_OS_MCP_DEFAULT_SOURCEMANDATE_OS_MCP_SERVER_NAMEMANDATE_OS_MCP_SERVER_VERSION
The API key behind MANDATE_OS_AGENT_TOKEN should usually include:
control-plane:readfor workspace and mandate contextmandates:readandreceipts:readfor mandate and receipt lookupssimulate:writefor 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/repoThat command:
- updates
/absolute/path/to/your/repo/.codex/config.tomlwith[features].codex_hooks = trueand a project-scopedmandateosMCP entry - updates
/absolute/path/to/your/repo/.codex/hooks.jsonwith a MandateOSPreToolUsehook forBash - adds
.codex/config.tomland.codex/hooks.jsonto.git/info/excludewhen 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.jsonrelease-platform.jsondocs-content.jsonfinance-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/repoUseful install flags:
--no-project-mcpto skip workspace.codex/config.tomlMCP registration--no-project-hooksto skip workspace.codex/hooks.json--no-hooks-featureto skip enabling[features].codex_hooks--no-git-excludeto skip.git/info/excludeupdates--rules-files /a.json,/b.jsonto override the bundled starter rules--unmatched-permission allow|ask|denyto 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.tomland.codex/hooks.jsonfrom the active repository - current Codex hooks only emit
PreToolUseandPostToolUseforBash, so generic MCP-side interception is not available yet - Codex currently parses
permissionDecision: "ask"but does not enforce it, so MandateOS maps bothaskanddenyinto a blocking Codex hook response until nativeaskenforcement 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/repoThat command:
- updates
~/.cursor/mcp.jsonwith a globalmandateosMCP entry - updates
/absolute/path/to/your/repo/.cursor/mcp.jsonwith a workspace override - updates
/absolute/path/to/your/repo/.cursor/hooks.jsonwith MandateOSbeforeShellExecutionandbeforeMCPExecutionhooks
The default installer uses all bundled starter rule files:
local-workspace.jsonrelease-platform.jsondocs-content.jsonfinance-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/repoUseful install flags:
--no-user-mcpto skip~/.cursor/mcp.json--no-project-mcpto skip workspace.cursor/mcp.json--no-project-hooksto skip workspace.cursor/hooks.json--rules-files /a.json,/b.jsonto override the bundled starter rules--unmatched-permission allow|ask|denyto 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 tomandateos_execute_enforced_action cursor-agent --printdid not invokebeforeShellExecution, 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/repoThat command:
- updates
~/.claude.jsonwith a local-scopedmandateosMCP entry for that workspace - updates
/absolute/path/to/your/repo/.claude/settings.local.jsonwith MandateOSPreToolUsehooks forBashandmcp__.* - adds
.claude/settings.local.jsonto.git/info/excludewhen the workspace is a Git repository
The default installer uses all bundled starter rule files:
local-workspace.jsonrelease-platform.jsondocs-content.jsonfinance-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/repoUseful install flags:
--no-local-mcpto skip the workspace entry inside~/.claude.json--no-local-hooksto skip workspace.claude/settings.local.json--no-git-excludeto skip.git/info/excludeupdates--rules-files /a.json,/b.jsonto override the bundled starter rules--unmatched-permission allow|ask|denyto 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
mandateosMCP entry was loaded from~/.claude.json - the local
PreToolUsehooks were loaded from.claude/settings.local.json - direct
gh issue edit ... --add-label ...was blocked and redirected tomandateos_execute_enforced_action
Tool surface
Generic workflow tools:
mandateos_get_contextmandateos_get_policy_catalogmandateos_issue_mandatemandateos_evaluate_actionsmandateos_issue_execution_grantmandateos_verify_mandatemandateos_verify_receiptmandateos_verify_execution_grant
Enforced adapter tools:
mandateos_execute_enforced_action- legacy aliases remain available for compatibility, including
mandateos_execute_github_issue_labelandmandateos_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
beforeShellExecutionas the main bypass blocker for direct shell-based side effects - Cursor
beforeMCPExecutionas the main bypass blocker for side-effecting tools exposed by other MCP servers - Codex
PreToolUseas 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:
- Register this MandateOS MCP server.
- Allow all
mandateos_*tools. - Use
beforeShellExecutionto intercept direct provider or mutation commands such asgh,curl,kubectl,terraform,aws,gcloud,npm publish, orgit push, then call MandateOS to decide whether they are allowed. - Use
beforeMCPExecutionto intercept side-effecting tools from non-MandateOS MCP servers, then call MandateOS to decide whether they are allowed. - Keep provider credentials out of the agent process whenever possible.
- For generic workflows, require a fresh MandateOS receipt before allowing the side effect.
- For enforced adapters, deny the direct path and force the agent onto
mandateos_execute_enforced_actionor a supported legacy alias.
The practical hook flow is:
- The host calls
beforeShellExecution,beforeMCPExecution, orPreToolUse. - The hook normalizes the attempted command or tool call into a MandateOS action shape.
- The hook calls MandateOS, usually through
@mandate-os/sdkor a direct API request. - The hook maps the MandateOS decision back to the host's local allow or block mechanism.
For example:
gh issue edit --add-label bugbecomes agithub.issue.labelaction proposalkubectl apply -f prod.yamlbecomes a deployment or cluster mutation action proposalterraform applybecomes an infrastructure mutation action proposal- an unknown but side-effecting command can fall back to a coarse action like
shell.command.executeand 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_URLMANDATE_OS_AGENT_TOKENMANDATE_OS_MCP_DEFAULT_MANDATE_IDMANDATE_OS_MCP_DEFAULT_SOURCE(optional)MANDATE_OS_HOST_GATEWAY_UNMATCHED_PERMISSIONwithaskby defaultMANDATE_OS_HOST_GATEWAY_RULES_FILESfor a comma-separated list of starter or custom bundle filesMANDATE_OS_HOST_GATEWAY_RULES_JSONorMANDATE_OS_HOST_GATEWAY_RULES_FILEfor custom domain rules
Included starter bundles:
dist/packages/mandate-os-mcp/rules/starter-bundles/local-workspace.jsondist/packages/mandate-os-mcp/rules/starter-bundles/release-platform.jsondist/packages/mandate-os-mcp/rules/starter-bundles/docs-content.jsondist/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:buildregister the built stdio command in your MCP-capable host:
node /absolute/path/to/dist/packages/mandate-os-mcp/index.jswith the MandateOS env vars above injected into the server process.