Skip to main content
Invoke external Model Context Protocol (MCP) tools over HTTP to extend Conductor with third-party capabilities. The tools operation allows ensembles to call external MCP servers (like GitHub MCP, Brave Search MCP, or custom MCP servers) via HTTP. This enables integration with any MCP-compatible service without requiring subprocess execution.

Overview

With Cloudflare’s announcement of MCP support (November 2024), Conductor implements HTTP-only MCP transport for secure, scalable tool integration on the edge. This approach:
  • Works in Cloudflare Workers (no Node.js required)
  • Scales horizontally with zero configuration
  • Provides built-in auth (bearer tokens, OAuth)
  • Supports HMAC signature verification
  • Integrates seamlessly with existing infrastructure

Basic Usage

ensemble: github-pr-analyzer

agents:
  - name: get-pr
    operation: tools
    config:
      mcp: github              # MCP server name
      tool: get_pull_request   # Tool to invoke
      timeout: 10000

inputs:
  owner: anthropics
  repo: anthropic-sdk-typescript
  pull_number: 123

outputs:
  pr_data: ${get-pr.output}

Configuration

Agent Config

config:
  mcp: string          # MCP server name from conductor.config.ts
  tool: string         # Tool name to invoke
  timeout: number      # Request timeout in ms (default: 30000)
  cacheDiscovery: boolean   # Cache tool discovery (default: false)
  cacheTTL: number     # Discovery cache TTL in seconds (default: 300)

Input

The agent input becomes the tool’s arguments:
inputs:
  owner: anthropics
  repo: anthropic-sdk-typescript
  pull_number: 123
Becomes:
{
  "name": "get_pull_request",
  "arguments": {
    "owner": "anthropics",
    "repo": "anthropic-sdk-typescript",
    "pull_number": 123
  }
}

Output

{
  tool: string         // Tool name that was invoked
  server: string       // MCP server name
  content: unknown     // Tool output (format varies by tool)
  duration: number     // Execution time in ms
  isError: boolean     // Whether tool returned an error
}

Setup MCP Servers

1. Configure MCP Servers

Create conductor.config.ts:
import type { ConductorConfig } from '@ensemble-edge/conductor'

const config: ConductorConfig = {
  mcpServers: {
    // GitHub MCP Server
    github: {
      url: 'https://github-mcp.example.com',
      auth: {
        type: 'bearer',
        token: process.env.GITHUB_MCP_TOKEN
      },
      timeout: 15000
    },

    // Brave Search MCP Server
    brave: {
      url: 'https://brave-search-mcp.example.com',
      auth: {
        type: 'bearer',
        token: process.env.BRAVE_API_KEY
      }
    },

    // Custom MCP Server with OAuth
    custom: {
      url: 'https://custom-mcp.example.com',
      auth: {
        type: 'oauth',
        clientId: process.env.OAUTH_CLIENT_ID,
        clientSecret: process.env.OAUTH_CLIENT_SECRET,
        tokenUrl: 'https://auth.example.com/oauth/token'
      },
      timeout: 20000
    },

    // Public MCP Server (no auth)
    public: {
      url: 'https://public-mcp.example.com'
    }
  }
}

export default config

2. Set Environment Variables

# .dev.vars (for local development)
GITHUB_MCP_TOKEN=ghp_xxx
BRAVE_API_KEY=BSAxxx
OAUTH_CLIENT_ID=client_xxx
OAUTH_CLIENT_SECRET=secret_xxx
# wrangler.toml (for production)
[vars]
# Public variables (non-sensitive)

[[env.production.vars]]
# Production secrets via wrangler secret
# Set secrets in production
wrangler secret put GITHUB_MCP_TOKEN
wrangler secret put BRAVE_API_KEY
wrangler secret put OAUTH_CLIENT_SECRET

MCP Server Examples

GitHub MCP

Interact with GitHub repositories, pull requests, and issues:
ensemble: github-pr-review

agents:
  # Get pull request data
  - name: get-pr
    operation: tools
    config:
      mcp: github
      tool: get_pull_request

  # List PR files
  - name: list-files
    operation: tools
    config:
      mcp: github
      tool: list_pull_request_files

  # Get file contents
  - name: get-file
    operation: tools
    config:
      mcp: github
      tool: get_file_contents

  # Analyze with AI
  - name: analyze
    operation: think
    config:
      provider: anthropic
      model: claude-sonnet-4
      prompt: |
        Pull Request: ${get-pr.output}
        Files Changed: ${list-files.output}
        File Contents: ${get-file.output}

        Review this PR and provide feedback.

inputs:
  owner: anthropics
  repo: anthropic-sdk-typescript
  pull_number: 123
  file_path: src/index.ts

outputs:
  review: ${analyze.output}
  pr_data: ${get-pr.output}

Brave Search MCP

Web search with Brave Search API:
ensemble: web-research

agents:
  # Web search
  - name: search
    operation: tools
    config:
      mcp: brave
      tool: web_search

  # Summarize results with AI
  - name: summarize
    operation: think
    config:
      provider: anthropic
      model: claude-sonnet-4
      prompt: |
        Search Results:
        ${search.output}

        Question: ${input.question}

        Provide a comprehensive answer with citations.

inputs:
  query: "Model Context Protocol specification"
  count: 10

outputs:
  answer: ${summarize.output}
  sources: ${search.output}

Custom MCP Server

Call your own MCP server:
ensemble: data-processor

agents:
  # Call custom tool
  - name: process
    operation: tools
    config:
      mcp: custom
      tool: process_data
      timeout: 30000

inputs:
  data: ${input.raw_data}
  format: json

outputs:
  processed: ${process.output}

Common Patterns

Sequential Tool Calls

ensemble: github-analysis

agents:
  # Step 1: Get repository
  - name: get-repo
    operation: tools
    config:
      mcp: github
      tool: get_repo

  # Step 2: List issues
  - name: list-issues
    operation: tools
    config:
      mcp: github
      tool: list_issues

  # Step 3: Analyze issues
  - name: analyze
    operation: think
    config:
      provider: anthropic
      model: claude-sonnet-4
      prompt: |
        Repository: ${get-repo.output}
        Issues: ${list-issues.output}

        Analyze the project's issue trends.

inputs:
  owner: anthropics
  repo: anthropic-sdk-typescript
  state: open

outputs:
  analysis: ${analyze.output}
  issue_count: ${list-issues.output.length}

Parallel Tool Calls

ensemble: multi-source-search

agents:
  # These run in parallel
  - name: search-brave
    operation: tools
    config:
      mcp: brave
      tool: web_search

  - name: search-github
    operation: tools
    config:
      mcp: github
      tool: search_code

  # Combine results
  - name: synthesize
    operation: think
    config:
      provider: anthropic
      model: claude-sonnet-4
      prompt: |
        Brave Results: ${search-brave.output}
        GitHub Results: ${search-github.output}

        Question: ${input.question}

        Synthesize a comprehensive answer.

inputs:
  question: "How to implement MCP servers"
  brave_query: "MCP server implementation guide"
  github_query: "mcp server"
  github_repo: "anthropics/model-context-protocol"

outputs:
  answer: ${synthesize.output}
  sources:
    brave: ${search-brave.output}
    github: ${search-github.output}

Tool with Fallback

agents:
  # Try primary tool
  - name: search-primary
    operation: tools
    config:
      mcp: brave
      tool: web_search

  # Fallback if primary fails
  - name: search-fallback
    condition: ${search-primary.failed}
    operation: api
    config:
      url: https://api.duckduckgo.com/
      method: GET
      params:
        q: ${input.query}

outputs:
  results: ${search-primary.output || search-fallback.output}
  source: ${search-primary.executed ? 'brave' : 'duckduckgo'}

Caching Tool Discovery

agents:
  - name: get-pr
    operation: tools
    config:
      mcp: github
      tool: get_pull_request
      cacheDiscovery: true    # Cache tool list from server
      cacheTTL: 3600          # Cache for 1 hour
This is useful when:
  • MCP server’s tool list doesn’t change often
  • You want to reduce latency
  • You’re making many tool calls to the same server

Authentication

Bearer Token

// conductor.config.ts
mcpServers: {
  github: {
    url: 'https://github-mcp.example.com',
    auth: {
      type: 'bearer',
      token: process.env.GITHUB_MCP_TOKEN
    }
  }
}
HTTP Request:
POST /tools/get_pull_request
Authorization: Bearer ghp_xxx
Content-Type: application/json

{
  "name": "get_pull_request",
  "arguments": { "owner": "...", "repo": "...", "pull_number": 123 }
}

OAuth

// conductor.config.ts
mcpServers: {
  custom: {
    url: 'https://custom-mcp.example.com',
    auth: {
      type: 'oauth',
      clientId: process.env.OAUTH_CLIENT_ID,
      clientSecret: process.env.OAUTH_CLIENT_SECRET,
      tokenUrl: 'https://auth.example.com/oauth/token'
    }
  }
}
OAuth flow:
  1. Conductor requests token from tokenUrl using client credentials
  2. Token is cached and reused for subsequent requests
  3. Token is automatically refreshed when expired

HMAC Signature

For MCP servers that require signature verification:
mcpServers: {
  secure: {
    url: 'https://secure-mcp.example.com',
    auth: {
      type: 'bearer',
      token: process.env.MCP_TOKEN
    },
    secret: process.env.MCP_SECRET  // For signing requests
  }
}
Request includes:
POST /tools/my_tool
Authorization: Bearer token_xxx
X-Conductor-Signature: sha256=abc123...
X-Conductor-Timestamp: 1705315200

Error Handling

Tool Invocation Errors

agents:
  - name: risky-tool
    operation: tools
    config:
      mcp: external
      tool: flaky_api

  - name: handle-error
    condition: ${risky-tool.failed}
    operation: code
    config:
      script: scripts/handle-tool-error
// scripts/handle-tool-error.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function handleToolError(context: AgentExecutionContext) {
  return {
    error: true,
    message: 'Tool failed',
    fallback_value: null
  }
}

outputs:
  result: ${risky-tool.output || handle-error.output}
  success: ${risky-tool.success}

Check Tool Errors

Tools can return isError: true even if the HTTP request succeeds:
agents:
  - name: call-tool
    operation: tools
    config:
      mcp: github
      tool: get_file_contents

  - name: check-result
    operation: code
    config:
      script: scripts/check-tool-result
    input:
      tool_output: ${call-tool.output}
// scripts/check-tool-result.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function checkToolResult(context: AgentExecutionContext) {
  const { tool_output } = context.input

  if (tool_output.isError) {
    throw new Error('Tool returned error: ' + tool_output.content)
  }

  return tool_output.content
}

Timeout Handling

agents:
  - name: slow-tool
    operation: tools
    config:
      mcp: external
      tool: slow_operation
      timeout: 15000    # 15 second timeout

  - name: use-cache
    condition: ${slow-tool.failed}
    operation: storage
    config:
      type: kv
      action: get
      key: cached-result

Testing

Mock MCP Tools

import { describe, it, expect } from 'vitest'
import { TestConductor } from '@ensemble-edge/conductor/testing'

describe('github-pr-analyzer', () => {
  it('should analyze pull request', async () => {
    const conductor = await TestConductor.create({
      mocks: {
        tools: {
          'github:get_pull_request': {
            number: 123,
            title: 'Add new feature',
            state: 'open',
            user: { login: 'octocat' }
          }
        }
      }
    })

    const result = await conductor.executeEnsemble('github-pr-analyzer', {
      owner: 'anthropics',
      repo: 'anthropic-sdk-typescript',
      pull_number: 123
    })

    expect(result).toBeSuccessful()
    expect(result.output.pr_data.number).toBe(123)
  })
})

Integration Testing

// Test against real MCP server
describe('github MCP integration', () => {
  it('should call real GitHub MCP', async () => {
    const conductor = await TestConductor.create({
      projectPath: './conductor'
    })

    const result = await conductor.executeEnsemble('github-pr-analyzer', {
      owner: 'anthropics',
      repo: 'anthropic-sdk-typescript',
      pull_number: 1
    })

    expect(result).toBeSuccessful()
    expect(result.output.pr_data).toHaveProperty('number')
  })
})

Best Practices

1. Set Appropriate Timeouts

# Short timeout for fast tools
- name: cache-lookup
  operation: tools
  config:
    mcp: cache-server
    tool: get
    timeout: 2000    # 2 seconds

# Long timeout for slow tools
- name: ai-analysis
  operation: tools
  config:
    mcp: ai-server
    tool: analyze
    timeout: 60000   # 60 seconds

2. Cache Tool Discovery

# Enable caching for stable tool lists
- name: frequent-tool
  operation: tools
  config:
    mcp: github
    tool: get_repo
    cacheDiscovery: true
    cacheTTL: 3600  # 1 hour

3. Handle Tool Failures

# Always check for failures
- name: tool-call
  operation: tools
  config:
    mcp: external
    tool: api_call

- name: verify
  operation: code
  config:
    script: scripts/verify-tool-call
  input:
    tool_call_failed: ${tool-call.failed}
    tool_output: ${tool-call.output}
// scripts/verify-tool-call.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function verifyToolCall(context: AgentExecutionContext) {
  const { tool_call_failed, tool_output } = context.input

  if (tool_call_failed || tool_output.isError) {
    throw new Error('Tool failed')
  }

  return tool_output.content
}

4. Use Typed Inputs

# Define clear input schema
inputs:
  owner:
    type: string
    required: true
    description: GitHub repository owner
  repo:
    type: string
    required: true
    description: GitHub repository name
  pull_number:
    type: number
    required: true
    description: Pull request number

5. Document Tool Usage

ensemble: github-pr-analyzer
description: Analyzes GitHub pull requests using MCP

agents:
  - name: get-pr
    description: Fetch pull request data from GitHub
    operation: tools
    config:
      mcp: github
      tool: get_pull_request

Limitations

HTTP Only: Conductor only supports HTTP transport for MCP. Stdio/subprocess MCP servers are not supported in Cloudflare Workers. No Streaming: The current implementation doesn’t support streaming responses from MCP servers. Tool Discovery: Tool discovery is performed on-demand. Use cacheDiscovery: true to cache the tool list. OAuth Refresh: OAuth token refresh is automatic but requires the MCP server to support token refresh.

Next Steps