Skip to main content

MCP Integration

Integrate Shadow Executor with Model Context Protocol (MCP) servers.

Overview

MCP is Anthropic's standard protocol for connecting AI models to external tools and data sources. Shadow Executor provides middleware that wraps MCP tool handlers to enforce policies before execution.

Installation

npm install @shadow-executor/sdk @modelcontextprotocol/sdk

Basic Usage

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { wrapToolHandler } from '@shadow-executor/sdk/mcp';

// Create MCP server
const server = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);

// Original tool handler
async function deleteDatabase(args: { instance_id: string }) {
// Delete database logic
return { success: true };
}

// Wrap with Shadow Executor
const protectedDeleteDatabase = wrapToolHandler(
deleteDatabase,
{
toolName: 'aws_rds_delete_db_instance',
policyPath: './shadow-exec.policy.yaml',
logPath: '~/.shadow-exec/audit.ndjson',
logSecret: process.env.SHADOW_EXEC_LOG_SECRET,
enableIPIDetection: true,
}
);

// Register wrapped tool
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'aws_rds_delete_db_instance',
description: 'Delete an RDS database instance',
inputSchema: {
type: 'object',
properties: {
instance_id: { type: 'string' },
},
required: ['instance_id'],
},
},
],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'aws_rds_delete_db_instance') {
return await protectedDeleteDatabase(request.params.arguments);
}
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);

Wrapping All Tools

Use wrapAllToolHandlers to protect multiple tools at once:

import { wrapAllToolHandlers } from '@shadow-executor/sdk/mcp';

const tools = {
aws_rds_delete_db_instance: async (args) => { /* ... */ },
aws_s3_delete_object: async (args) => { /* ... */ },
aws_iam_attach_user_policy: async (args) => { /* ... */ },
};

const protectedTools = wrapAllToolHandlers(tools, {
policyPath: './shadow-exec.policy.yaml',
logPath: '~/.shadow-exec/audit.ndjson',
logSecret: process.env.SHADOW_EXEC_LOG_SECRET,
enableIPIDetection: true,
});

// Use protectedTools in your MCP server

Tool Name Conversion

Shadow Executor automatically converts MCP tool names to AgentAction format:

MCP Tool NameServiceOperation
aws_rds_delete_db_instancerdsDeleteDBInstance
aws_s3_delete_objects3DeleteObject
aws_iam_attach_user_policyiamAttachUserPolicy
aws_ec2_terminate_instancesec2TerminateInstances

Conversion rules:

  • Tool name format: aws_{service}_{operation_snake_case}
  • Service: Extracted as-is (lowercase)
  • Operation: Converted to PascalCase with acronym preservation
    • delete_db_instanceDeleteDBInstance (DB preserved)
    • put_bucket_aclPutBucketAcl (ACL preserved)

Custom tool names: Provide a custom convertToAgentAction function in config:

wrapToolHandler(handler, {
toolName: 'custom_tool_name',
policyPath: './shadow-exec.policy.yaml',
convertToAgentAction: (toolName, args) => ({
service: 'my-service',
operation: 'MyOperation',
parameters: args,
timestamp: new Date().toISOString(),
}),
});

Resource Tag Extraction

Shadow Executor extracts resource_tags from tool parameters automatically:

// Tool call with tags in parameters
{
instance_id: 'prod-db-01',
tags: {
Environment: 'production',
Tier: 'critical'
}
}

// Converted to AgentAction
{
service: 'rds',
operation: 'DeleteDBInstance',
resource: 'prod-db-01',
resource_tags: {
Environment: 'production',
Tier: 'critical'
}
}

Tag parameter names: tags, resource_tags, Tags, or ResourceTags

Error Handling

When a policy blocks an action, BlockedActionError is thrown:

import { BlockedActionError } from '@shadow-executor/sdk/mcp';

try {
await protectedDeleteDatabase({ instance_id: 'prod-db-01' });
} catch (error) {
if (error instanceof BlockedActionError) {
console.error('Action blocked by policy:', error.decision.reason);
console.error('Matched rule:', error.decision.matched_rule_id);
// error.decision contains full PolicyDecision object
} else {
throw error;
}
}

Configuration Options

interface ShadowExecutorConfig {
/** Path to policy YAML file */
policyPath: string;

/** Path to audit log file (default: ~/.shadow-exec/audit.ndjson) */
logPath?: string;

/** HMAC secret for audit log signing (required) */
logSecret: string;

/** Enable IPI detection (default: false) */
enableIPIDetection?: boolean;

/** Agent ID for tracking (default: 'mcp-agent') */
agentId?: string;

/** Custom tool name to AgentAction converter */
convertToAgentAction?: (toolName: string, args: unknown) => AgentAction;

/** Approval timeout in minutes (default: 30) */
approvalTimeoutMinutes?: number;
}

Policy Example

version: "1.0"
name: "MCP Server Protection Policy"
rules:
- id: MCP-001
name: Block production RDS deletion
severity: CRITICAL
action: BLOCK
match:
service: rds
operation: DeleteDBInstance
resource_tags:
Environment: production

- id: MCP-002
name: Require approval for IAM changes
severity: HIGH
action: REQUIRE_APPROVAL
match:
service: iam
operation: [AttachUserPolicy, PutUserPolicy]

- id: MCP-003
name: Block high IPI score operations
severity: CRITICAL
action: BLOCK
match:
operation: "Delete*"
ipi_score: ">= 0.7"

Testing

Test your MCP integration with the Shadow Executor demo:

import { wrapToolHandler } from '@shadow-executor/sdk/mcp';

// Mock tool for testing
const mockDeleteDB = async (args: { instance_id: string }) => {
console.log(`Deleting database: ${args.instance_id}`);
return { success: true };
};

// Wrap with policy
const protectedMockDeleteDB = wrapToolHandler(mockDeleteDB, {
toolName: 'aws_rds_delete_db_instance',
policyPath: './test-policy.yaml',
logSecret: 'test-secret',
});

// Test blocked action
try {
await protectedMockDeleteDB({
instance_id: 'prod-customer-data',
tags: { Environment: 'production' }
});
} catch (error) {
console.log('Action blocked as expected:', error.message);
}

Full Example

See examples/mcp/demo.mjs in the repository for a complete working example.

Next Steps