Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions packages/agent-infra/mcp-servers/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@

[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=filesystem&config=eyJjb21tYW5kIjoibnB4IEBhZ2VudC1pbmZyYS9tY3Atc2VydmVyLWNvbW1hbmRzQGxhdGVzdCJ9) [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%253Amcp%252Finstall%253F%257B%2522name%2522%253A%2522commands%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540agent-infra%252Fmcp-server-commands%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%253Amcp%252Finstall%253F%257B%2522name%2522%253A%2522commands%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540agent-infra%252Fmcp-server-commands%2540latest%2522%255D%257D)


A Model Context Protocol (MCP) server that provides execuate arbitrary commands.

![](https://github.com/user-attachments/assets/ee8df75f-04f4-46c8-8b57-0e32e4373c3e)


### Requirements

- Node.js 18 or newer
- VS Code, Cursor, Windsurf, Claude Desktop or any other MCP client


### Getting started

#### Local (Stdio)
Expand Down Expand Up @@ -48,6 +45,7 @@ code --add-mcp '{"name":"commands","command":"npx","args":["@agent-infra/mcp-ser
```

After installation, the Commands MCP server will be available for use with your GitHub Copilot agent in VS Code.

</details>

<details>
Expand All @@ -68,6 +66,7 @@ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, u
}
}
```

</details>

<details>
Expand All @@ -88,6 +87,7 @@ Follow Windsuff MCP [documentation](https://docs.windsurf.com/windsurf/cascade/m
}
}
```

</details>

<details>
Expand All @@ -108,6 +108,7 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
}
}
```

</details>

#### Remote (SSE / Streamable HTTP)
Expand All @@ -120,10 +121,10 @@ npx @agent-infra/mcp-server-commands --port 8089
```

You can use one of the two MCP Server remote endpoint:

- Streamable HTTP(Recommended): `http://127.0.0.1::8089/mcp`
- SSE: `http://127.0.0.1::8089/sse`


And then in MCP client config, set the `url` to the SSE endpoint:

```js
Expand Down Expand Up @@ -195,10 +196,42 @@ const toolResult = await client.callTool({
console.log(toolResult);
```

### Safety policy

Commands MCP includes a server-side safety policy for high-risk command
execution. Commands that match the default destructive patterns are not
executed immediately; the tool returns an `approval_required` result with the
matched rule id and approval request id.

Default approval-required categories include recursive or forceful deletion,
disk or partition mutation, shutdown or restart operations, privileged command
execution, destructive git commands, and remote script execution such as
`curl | sh`.

In-process callers can customize the policy:

```js
const server = createServer({
safety: {
rules: [
{
id: 'allow-known-cleanup',
action: 'allow',
reason: 'This cleanup command is handled by the host approval flow.',
patterns: [String.raw`git\s+clean\s+-fd\s+build`],
},
],
},
});
```

Set `COMMANDS_SAFETY_ENABLED=false` to disable the policy for trusted,
externally sandboxed environments.

### Developement

Access http://127.0.0.1:6274/:

```bash
npm run dev
```
```
222 changes: 222 additions & 0 deletions packages/agent-infra/mcp-servers/commands/src/safety-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { createHash } from 'node:crypto';

type CommandSafetyAction = 'allow' | 'deny' | 'require_approval';

type CommandSafetyRule = {
id: string;
action: CommandSafetyAction;
reason: string;
patterns: string[];
};

type CommandSafetyPolicyConfig = {
enabled?: boolean;
defaultAction?: CommandSafetyAction;
defaultReason?: string;
useDefaultRules?: boolean;
rules?: CommandSafetyRule[];
};

type CommandSafetySubject = {
toolName: string;
command?: string;
interpreter?: string;
script?: string;
cwd?: string;
};

type CommandSafetyDecision = {
action: CommandSafetyAction;
reason?: string;
ruleId?: string;
approvalRequestId?: string;
};

const DEFAULT_COMMAND_SAFETY_RULES: CommandSafetyRule[] = [
{
id: 'destructive-file-removal',
action: 'require_approval',
reason:
'The command appears to recursively or forcefully remove filesystem data.',
patterns: [
String.raw`(?:^|[;&|]\s*)rm\s+(?:-[^\s]*[rRfF][^\s]*|--recursive|--force)`,
String.raw`(?:^|[;&|]\s*)rmdir\s+(?:/s|-[^\s]*r)`,
String.raw`(?:^|[;&|]\s*)del(?:ete)?\s+[\s\S]*(?:/s|/q)`,
String.raw`\bRemove-Item\b[\s\S]*(?:-Recurse|-Force)`,
],
},
{
id: 'disk-or-partition-mutation',
action: 'require_approval',
reason: 'The command appears to modify disks, partitions, or filesystems.',
patterns: [
String.raw`(?:^|[;&|]\s*)(?:format|mkfs(?:\.[a-z0-9]+)?|fdisk|parted|diskpart|diskutil)\b`,
],
},
{
id: 'system-power-action',
action: 'require_approval',
reason:
'The command appears to shut down, restart, or power off the system.',
patterns: [
String.raw`(?:^|[;&|]\s*)(?:shutdown|reboot|halt|poweroff)\b`,
String.raw`\bRestart-Computer\b`,
String.raw`\bStop-Computer\b`,
],
},
{
id: 'privileged-or-recursive-permission-change',
action: 'require_approval',
reason:
'The command appears to request elevated privileges or recursively change permissions.',
patterns: [
String.raw`(?:^|[;&|]\s*)(?:sudo|su)\b`,
String.raw`(?:^|[;&|]\s*)(?:chmod|chown)\s+[\s\S]*(?:-R|--recursive)`,
],
},
{
id: 'destructive-git-operation',
action: 'require_approval',
reason:
'The command appears to destructively modify git working tree state.',
patterns: [
String.raw`(?:^|[;&|]\s*)git\s+reset\s+--hard\b`,
String.raw`(?:^|[;&|]\s*)git\s+clean\s+-[^\s]*f`,
],
},
{
id: 'remote-script-execution',
action: 'require_approval',
reason:
'The command appears to download remote content and pipe it into a shell or evaluator.',
patterns: [
String.raw`\b(?:curl|wget)\b[\s\S]*\|[\s\S]*(?:sh|bash|zsh|fish|powershell|pwsh)\b`,
String.raw`\b(?:Invoke-WebRequest|Invoke-RestMethod|iwr|irm)\b[\s\S]*(?:Invoke-Expression|\biex\b)`,
],
},
];

function resolveSafetyPolicy(policy?: CommandSafetyPolicyConfig): Required<
Pick<CommandSafetyPolicyConfig, 'enabled' | 'defaultAction' | 'rules'>
> & {
defaultReason?: string;
} {
const useDefaultRules = policy?.useDefaultRules ?? true;
return {
enabled: policy?.enabled ?? process.env.COMMANDS_SAFETY_ENABLED !== 'false',
defaultAction: policy?.defaultAction ?? 'allow',
defaultReason: policy?.defaultReason,
rules: [
...(policy?.rules ?? []),
...(useDefaultRules ? DEFAULT_COMMAND_SAFETY_RULES : []),
],
};
}

function evaluateCommandSafety(
subject: CommandSafetySubject,
policy?: CommandSafetyPolicyConfig,
): CommandSafetyDecision {
const resolvedPolicy = resolveSafetyPolicy(policy);

if (!resolvedPolicy.enabled) {
return { action: 'allow' };
}

const executableText = getExecutableText(subject);

for (const rule of resolvedPolicy.rules) {
if (
rule.patterns.some((pattern) => matchesPattern(pattern, executableText))
) {
if (rule.action === 'allow') {
return { action: 'allow' };
}
return decisionForAction(rule.action, subject, rule.reason, rule.id);
}
}

if (resolvedPolicy.defaultAction !== 'allow') {
return decisionForAction(
resolvedPolicy.defaultAction,
subject,
resolvedPolicy.defaultReason ?? 'The command requires explicit approval.',
);
}

return { action: 'allow' };
}

function formatSafetyDecision(decision: CommandSafetyDecision): string {
const status =
decision.action === 'require_approval'
? 'approval_required'
: decision.action;

const lines = [
`Command safety policy: ${status}`,
'No command was executed.',
];

if (decision.ruleId) {
lines.push(`Rule: ${decision.ruleId}`);
}
if (decision.reason) {
lines.push(`Reason: ${decision.reason}`);
}
if (decision.approvalRequestId) {
lines.push(`Approval request: ${decision.approvalRequestId}`);
}

return lines.join('\n');
}

function getExecutableText(subject: CommandSafetySubject): string {
return [subject.command, subject.interpreter, subject.script]
.filter((part): part is string => Boolean(part))
.join('\n');
}

function matchesPattern(pattern: string, value: string): boolean {
return new RegExp(pattern, 'im').test(value);
}

function decisionForAction(
action: Exclude<CommandSafetyAction, 'allow'>,
subject: CommandSafetySubject,
reason?: string,
ruleId?: string,
): CommandSafetyDecision {
return {
action,
reason,
ruleId,
approvalRequestId:
action === 'require_approval'
? createApprovalRequestId(subject, ruleId)
: undefined,
};
}

function createApprovalRequestId(
subject: CommandSafetySubject,
ruleId?: string,
): string {
return createHash('sha256')
.update(JSON.stringify({ ...subject, ruleId }))
.digest('hex')
.slice(0, 16);
}

export {
DEFAULT_COMMAND_SAFETY_RULES,
evaluateCommandSafety,
formatSafetyDecision,
};
export type {
CommandSafetyAction,
CommandSafetyDecision,
CommandSafetyPolicyConfig,
CommandSafetyRule,
CommandSafetySubject,
};
Loading