19 minute read

In Part 1, we built foundations. In Part 2, we designed for resilience. In Part 3, we established governance. Now we complete the picture with advanced patterns that make systems production-ready: composition, security, and testing for failure.


Series Navigation

  1. Part 1: Foundation & Architecture
  2. Part 2: Resilience & Runtime Behavior
  3. Part 3: System Behavior & Policies
  4. Part 4: Advanced Patterns & Production (this article)

Composition, Routing, and Orchestration Are Where Architecture Shows

As agents become more capable, tools stop being called in isolation. They become building blocks.

Higher-level patterns emerge:

  • Composition turns simple tools into reusable workflows
  • Routing chooses tools dynamically based on context
  • Orchestration coordinates multi-step operations

These patterns should be explicit and observable. Hidden orchestration inside prompts or ad-hoc logic tends to collapse under scale.

Tool Composition Pattern

flowchart LR
    A[Input] --> B[Step 1: Search]
    B --> C[Step 2: Extract]
    C --> D[Step 3: Synthesize]
    D --> E[Output]
    
    B -.->|results.urls| C
    C -.->|results.documents| D

Algorithm:

FUNCTION composeWorkflow(steps[], input):
  context = {input: input, results: {}}
  
  FOR EACH step IN steps:
    // Map previous results to inputs using $-references
    stepInput = resolveInputMapping(step.inputMapping, context)
    
    // Execute step
    result = step.tool.execute(stepInput)
    
    // Store result for next steps
    context.results[step.name] = result
  
  RETURN context.results[finalStep]

FUNCTION resolveInputMapping(mapping, context):
  resolved = {}
  
  FOR EACH (key, value) IN mapping:
    IF value.startsWith('$'):
      // Reference: $input.topic or $results.search.urls
      resolved[key] = getFromContext(value, context)
    ELSE:
      // Literal value
      resolved[key] = value
  
  RETURN resolved

// Example workflow definition:
workflow = {
  steps: [
    {name: 'search', tool: searchTool, 
     inputMapping: {query: '$input.topic'}},
    {name: 'extract', tool: extractTool, 
     inputMapping: {urls: '$results.search.urls'}},
    {name: 'synthesize', tool: llmTool, 
     inputMapping: {documents: '$results.extract.documents'}}
  ]
}

Tool Routing Algorithm

// Dynamic tool selection based on health, performance, cost
FUNCTION routeTool(intent, context):
  candidates = findCandidatesByCapability(intent.capability)
  scored = []
  
  FOR EACH tool IN candidates:
    score = 100
    health = metrics.getToolHealth(tool.id)
    
    // Health scoring
    IF health.status == 'unhealthy': score = 0
    IF health.status == 'degraded': score -= 20
    
    // Performance scoring
    IF context.prioritizeSpeed:
      IF health.avgLatency > 1000ms: score -= 30
      IF health.avgLatency < 200ms: score += 20
    
    // Cost scoring
    IF context.minimizeCost:
      score -= tool.metadata.costPerCall * 1000
    
    // Capability match
    featureMatch = countMatches(tool.capabilities, context.requirements)
    score += featureMatch * 30
    
    // Permission check
    IF NOT hasPermissions(tool, context.user): score = 0
    
    scored.APPEND({tool, score})
  
  // Return highest scoring tool
  RETURN max(scored, by=score).tool

Orchestration Pattern

// Multi-step workflow with dependencies and error handling
FUNCTION orchestrateWorkflow(workflow, input):
  results = {}
  executionLog = []
  
  FOR EACH step IN workflow.steps:
    startTime = NOW()
    
    TRY:
      // Resolve dependencies
      dependencies = {}
      FOR EACH dep IN step.dependencies:
        IF NOT results.contains(dep):
          THROW "Dependency unavailable: " + dep
        dependencies[dep] = results[dep]
      
      // Execute step (simple, parallel, or conditional)
      IF step.type == 'parallel':
        result = executeParallel(step.tools, input)
      ELSE IF step.type == 'conditional':
        tool = IF step.condition(input) THEN step.trueBranch ELSE step.falseBranch
        result = tool.execute(input)
      ELSE:
        result = step.tool.execute(input)
      
      results[step.id] = result
      executionLog.APPEND({step: step.id, status: 'success', duration: NOW() - startTime})
      
    CATCH error:
      executionLog.APPEND({step: step.id, status: 'error', error, duration: NOW() - startTime})
      
      // Handle based on error policy
      IF step.optional:
        CONTINUE  // Skip optional steps
      IF step.fallback EXISTS:
        TRY result = step.fallback.execute(input)
        results[step.id] = result
        CONTINUE
      IF workflow.errorHandling == 'continue-on-error':
        CONTINUE
      ELSE:
        RETURN {status: 'failed', results, executionLog, failedAt: step.id}
  
  RETURN {status: 'completed', results, executionLog}

When to Use Each Pattern

Composition:

  • Linear workflows with clear data flow
  • Reusable pipelines (search → extract → summarize)
  • ETL-style processes

Routing:

  • Multiple tools with same capability
  • Runtime decisions based on context
  • Cost/performance tradeoffs

Orchestration:

  • Complex multi-step processes
  • Conditional branches and error recovery
  • Parallel execution with dependencies

Security Is Structural, Not Additive

Tool integration expands the blast radius of mistakes.

Security cannot be bolted on after the fact. It must be structural:

  • Credentials are scoped and rotated
  • Inputs are validated consistently
  • Data sharing is minimized by default
  • Network boundaries are enforced

The most costly security failures in tool systems are rarely novel—they’re architectural.

Security Lifecycle

flowchart TB
    A[Credential Request] --> B{Permission Check}
    B -->|Denied| C[Error: Unauthorized]
    B -->|Granted| D[Retrieve Encrypted Credentials]
    D --> E{Expired?}
    E -->|Yes| F[Rotate Credentials]
    F --> G[Store New Encrypted]
    E -->|No| H[Decrypt]
    G --> H
    H --> I[Return to Tool]
    I --> J[Execute with Sandbox]
    J --> K[Audit Log]

Algorithm

// Credential Management
FUNCTION storeCredentials(toolId, credentials):
  encrypted = encrypt(stringify(credentials))
  
  database.save({
    toolId: toolId,
    encrypted: encrypted,
    createdAt: NOW(),
    expiresAt: NOW() + 90_days
  })
  
  scheduleRotation(toolId, after=90_days)

FUNCTION getCredentials(toolId, userId):
  // Permission check
  IF NOT hasPermission(userId, toolId):
    THROW "Unauthorized access"
  
  record = database.find({toolId: toolId})
  
  IF NOT record.exists:
    THROW "Credentials not found"
  
  // Auto-rotate if expired
  IF record.expiresAt < NOW():
    rotateCredentials(toolId)
    RETURN getCredentials(toolId, userId)  // Recursive call
  
  // Decrypt only when needed
  decrypted = decrypt(record.encrypted)
  RETURN parse(decrypted)

FUNCTION rotateCredentials(toolId):
  auditLog.record({event: 'credential_rotation', toolId, timestamp: NOW()})
  
  tool = registry.getTool(toolId)
  newCredentials = tool.refreshCredentials()
  
  storeCredentials(toolId, newCredentials)

// Input Validation
FUNCTION executeWithValidation(toolId, input, userId):
  tool = registry.getTool(toolId)
  
  // Schema validation
  errors = validateAgainstSchema(input, tool.schema)
  IF errors.isNotEmpty():
    THROW "Invalid input: " + errors.join(', ')
  
  // Sanitize input
  sanitized = sanitizeInput(input, tool.schema)
  
  // PII detection
  IF detectPII(sanitized) AND NOT userConsentedToPII(userId, toolId):
    THROW "PII detected but user has not consented"
  
  // Audit log
  auditLog.record({
    event: 'tool_execution',
    userId, toolId,
    timestamp: NOW(),
    inputHash: hash(sanitized)
  })
  
  // Execute with permissions
  RETURN executeWithSandbox(toolId, sanitized, userId)

FUNCTION sanitizeInput(input, schema):
  sanitized = {}
  
  FOR EACH (key, value) IN input:
    fieldSchema = schema.properties[key]
    
    IF NOT fieldSchema.exists:
      CONTINUE  // Drop unknown fields
    
    IF fieldSchema.type == 'string':
      // Remove dangerous characters, enforce max length
      sanitized[key] = removeDangerousChars(value)
                       .substring(0, fieldSchema.maxLength OR 10000)
    
    ELSE IF fieldSchema.type == 'number':
      num = parseNumber(value)
      IF num.isValid():
        // Enforce min/max bounds
        sanitized[key] = clamp(num, fieldSchema.minimum, fieldSchema.maximum)
    
    ELSE:
      sanitized[key] = value
  
  RETURN sanitized

FUNCTION detectPII(data):
  patterns = {
    email: /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i,
    phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,
    ssn: /\b\d{3}-\d{2}-\d{4}\b/,
    creditCard: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/
  }
  
  dataStr = stringify(data)
  
  FOR EACH (type, pattern) IN patterns:
    IF pattern.matches(dataStr):
      RETURN {detected: true, type: type}
  
  RETURN {detected: false}

// Sandbox Execution
FUNCTION executeWithSandbox(toolId, input, userId):
  tool = registry.getTool(toolId)
  permissions = tool.metadata.security.requiredPermissions
  
  // Network boundary check
  IF 'network.external' IN permissions:
    IF NOT canAccessExternal(userId):
      THROW "User not authorized for external network access"
  
  // Create restricted context
  context = {
    userId: userId,
    permissions: permissions,
    canAccessFileSystem: 'filesystem' IN permissions,
    canAccessNetwork: 'network.external' IN permissions,
    rateLimiter: getRateLimiter(userId),
    logAccess: (resource) => auditLog.record({
      event: 'resource_access',
      userId, resource,
      timestamp: NOW()
    })
  }
  
  RETURN tool.execute(input, context)

Security Layers

1. Authentication & Authorization

  • User authentication (who is using the system)
  • Tool authorization (who can use which tools)
  • Permission scoping (what each tool can access)

2. Data Protection

  • Encryption at rest (credentials, sensitive data)
  • Encryption in transit (API calls, logging)
  • Data minimization (only collect what’s needed)

3. Input Validation

  • Schema enforcement
  • Type checking
  • Size limits
  • Dangerous character filtering

4. Audit & Compliance

  • All tool executions logged
  • PII detection and handling
  • Retention policies
  • Compliance reporting

Testing for Failure Is a Form of Respect

Testing only the happy path assumes the system will be treated gently by reality. It won’t.

Serious MCP systems test for:

  • Tool outages
  • Partial responses
  • Network degradation
  • Expired credentials

Chaos testing is not pessimism. It’s respect for complexity.

Testing Patterns

CRITICAL TEST SCENARIOS:

1. TRANSIENT FAILURES
   Test: Tool fails twice, succeeds third time
   Expected: System retries with exponential backoff, eventually succeeds
   Verifies: Retry logic, backoff calculation

2. CIRCUIT BREAKER
   Test: Tool fails repeatedly past threshold
   Expected: Circuit opens, subsequent calls fail fast
   Verifies: Circuit breaker state transitions, fast failure

3. CREDENTIAL EXPIRATION
   Test: Tool returns 401, credentials refresh, retry succeeds
   Expected: Single refresh attempt, successful retry
   Verifies: Credential rotation, retry after refresh

4. GRACEFUL DEGRADATION
   Test: Primary fails, fallback succeeds
   Expected: System tries fallback, logs usage, returns result
   Verifies: Fallback chain, logging

5. CACHE BEHAVIOR
   Test: Identical requests within TTL window
   Expected: Second request returns cached result
   Verifies: Cache key generation, TTL enforcement

// Chaos Testing Algorithm
FUNCTION injectChaos(config):
  active = true
  failureRate = config.failureRate OR 0.1  // 10%
  latencyInjection = config.latencyInjection OR false
  maxLatency = config.maxLatency OR 5000ms
  
  FOR EACH tool IN registry.getAllTools():
    originalExecute = tool.execute
    
    tool.execute = (input) => {
      IF NOT active:
        RETURN originalExecute(input)
      
      // Inject latency
      IF latencyInjection AND random() < 0.3:
        delay = random() * maxLatency
        WAIT(delay milliseconds)
      
      // Inject failure
      IF random() < failureRate:
        errorType = chooseRandom([TIMEOUT, NETWORK_ERROR, AUTH_ERROR, RATE_LIMIT, SERVER_ERROR])
        THROW createError(errorType)
      
      RETURN originalExecute(input)
    }

FUNCTION chaosTest():
  system = createMCPSystem()
  chaos = injectChaos({failureRate: 0.2, latencyInjection: true})
  
  results = []
  FOR i FROM 1 TO 50:
    TRY:
      result = system.executeTask({task: 'test'})
      results.APPEND({success: true, result})
    CATCH error:
      results.APPEND({success: false, error})
  
  stopChaos()
  
  // Verify graceful degradation
  successCount = count(results where success == true)
  failureCount = count(results where success == false)
  
  ASSERT successCount > 30  // At least 60% success despite 20% injected failure
  ASSERT failureCount < 20  // Less than 40% failures
  
  // System should degrade gracefully, not catastrophically

Key Testing Principles

1. Test failure modes explicitly
   • Don't just test happy paths
   • Inject realistic failures
   • Verify recovery mechanisms

2. Test under load
   • Concurrent requests
   • Rate limit violations
   • Connection pool exhaustion

3. Test state transitions
   • Circuit breaker: CLOSED → OPEN → HALF_OPEN
   • Lazy loading: NOT_LOADED → INITIALIZING → READY
   • Credentials: VALID → EXPIRED → ROTATED

4. Test observability
   • Verify metrics are recorded
   • Verify logs contain correlation IDs
   • Verify alerts fire correctly

5. Test security boundaries
   • Permission checks
   • Input sanitization
   • PII detection
   • Credential encryption

Production Readiness Checklist

Deploy to production only when:

Resilience:

  • Fallback chains tested and documented
  • Circuit breakers configured per tool
  • Timeout values tuned based on profiling
  • Degraded modes verified with chaos testing

Observability:

  • All tool calls instrumented with metrics
  • Correlation IDs propagate through system
  • Health checks return accurate status
  • Dashboards show per-tool performance

Security:

  • Credentials encrypted and rotated
  • Input validation enforced
  • PII detection active
  • Audit logs capturing all executions
  • Permission matrix documented

Operations:

  • Runbooks for common failure scenarios
  • Alerts configured with appropriate thresholds
  • On-call team trained on system architecture
  • Deployment rollback procedure tested

Governance:

  • Tool registry complete with metadata
  • Error handling policies documented
  • Performance SLAs defined
  • Tool deprecation process established

Final Reflection: Building Infrastructure That Lasts

MCP tool integration is not about adding capabilities to agents. It’s about building infrastructure that earns trust over time.

The systems that last are not the ones with the most tools, but the ones with:

  • Clear boundaries — Separation allows independent evolution
  • Honest assumptions — Failure is routine, not exceptional
  • Visible behavior — Observability enables continuous improvement
  • Disciplined evolution — Governance prevents chaos at scale

If you design MCP integration as a system—rather than a shortcut—you give your agents something rare: a foundation that doesn’t crack as they grow.

From Principles to Practice

This series presented patterns and algorithms, but patterns alone don’t build systems. Teams do.

The practices that matter:

  1. Start simple, design for complex — Build clean interfaces even if you only have one tool
  2. Make failure visible — Don’t hide errors; surface them with context
  3. Measure everything — You can’t improve what you don’t observe
  4. Automate governance — Let systems enforce policies humans forget
  5. Test the edges — Happy paths lie; failure modes reveal truth

The Path Forward

MCP is young, and best practices are still emerging. But the principles underlying resilient systems—separation of concerns, graceful degradation, observability, security—are timeless.

As you build, remember: the goal isn’t perfect tools. It’s systems that behave predictably when tools aren’t perfect.

That’s the difference between demos and production. Between experiments and infrastructure. Between hope and trust.


Series Conclusion

Thank you for following this 4-part series on MCP Tool Integration as Systems Thinking. If these patterns resonate, apply them in your systems and share what you learn. The community grows stronger when we build on shared foundations.

Series Links:

  1. Part 1: Foundation & Architecture
  2. Part 2: Resilience & Runtime Behavior
  3. Part 3: System Behavior & Policies
  4. Part 4: Advanced Patterns & Production (this article)