@mandate-os/mcp
v0.1.3
Published
MandateOS MCP server and installer CLIs for Cursor and Claude Code.
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
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 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:
release-platform.jsondocs-content.jsonfinance-support.json
You can inspect what is installed with:
npx --yes --package @mandate-os/mcp 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 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:
release-platform.jsondocs-content.jsonfinance-support.json
You can inspect what is installed with:
npx --yes --package @mandate-os/mcp 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.
For hosts like Cursor, the most useful hooks are:
beforeShellExecutionas the main bypass blocker for direct shell-based side effectsbeforeMCPExecutionas the main bypass blocker for side-effecting tools exposed by other MCP serversbeforeSubmitPromptas a soft reminder to start with MandateOS, not as the primary enforcement pointafterShellExecutionandafterMCPExecutionfor audit and reconciliationsessionStartfor injecting session defaults such as workspace context, source, or mandate id
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:
- Cursor calls
beforeShellExecutionorbeforeMCPExecution. - 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 Cursor's
allow,deny, orask.
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/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.
