Create an MCP Tool

After deploying your first MCP server, create custom tools that AI clients can discover and invoke. This guide walks you through the process using any Redpanda Connect component.

After reading this page, you will be able to:

  • Create a tool file with the correct structure and MCP metadata

  • Map MCP parameters to component configuration fields using Bloblang

  • Test tools locally before connecting AI clients

Prerequisites

Set up the project

If you don’t have an MCP server project yet, create one:

rpk connect mcp-server init <project-name>
cd <project-name>

This command creates the following structure:

<project-name>/
├── resources/
    ├── caches/        # Cache components
    ├── inputs/        # Input components
    ├── outputs/       # Output components
    └── processors/    # Processor components (most common)

The resources/ directory holds your MCP tool files, organized by component type.

Create the tool file

Create a YAML file in the appropriate directory. For example, to create a processor tool:

touch resources/processors/<tool-name>.yaml

The file name can be anything, but use descriptive names that reflect the tool’s purpose.

Each YAML file must contain exactly one component. The directory in your project determines the component type:

Directory Component type

resources/processors/

Processor

resources/inputs/

Input

resources/outputs/

Output

resources/caches/

Cache

For example, a file in resources/processors/ defines a processor component. Do not mix component types in the same file.

Add the tool structure

An MCP tool wraps a Redpanda Connect component and exposes it to AI clients. Each tool file has three parts:

  • Label: The tool name AI clients see

  • Component configuration: A Redpanda Connect component (processor, input, output, or cache)

  • MCP metadata: The tool’s purpose and input parameters

Here’s an example using the sql_select processor:

label: lookup-customer (1)

sql_select: (2)
  driver: postgres
  dsn: "${DATABASE_URL}"
  table: customers
  columns: ["id", "name", "email", "plan"]
  where: id = ?
  args_mapping: '[this.customer_id]'

meta: (3)
  mcp:
    enabled: true
    description: "Look up a customer by ID and return their profile."
    properties:
      - name: customer_id
        type: string
        description: "The customer's unique identifier"
        required: true
1 The label becomes the tool name that AI clients see and invoke.
2 The sql_select processor queries a PostgreSQL database.
3 The MCP metadata tells AI clients what this tool does and what parameters it accepts.

The following sections show how to structure tools for each component type.

Label naming rules

The label field (tool name) must follow these rules:

  • Lowercase letters, numbers, underscores, and hyphens only (a-z, 0-9, _, -)

  • Cannot start with an underscore

  • No spaces or special characters

Valid examples: get-weather, lookup_customer, send-notification-v2

Component types

Processors transform, filter, or enrich data. Use a processors: array with one or more processors:

label: enrich-order

processors:
  - http:
      url: "https://api.example.com/lookup"
      verb: GET

meta:
  mcp:
    enabled: true
    description: "Enrich order with customer data"

Inputs read data from sources, outputs write data to destinations, and caches store and retrieve data. Define these components directly at the top level. Do not wrap components in input:, output:, or cache: blocks. This syntax is for pipelines, not MCP tools.

Input tool
label: read-events

redpanda:  (1)
  seed_brokers: ["${REDPANDA_BROKERS}"]
  topics: ["events"]
  consumer_group: "mcp-reader"

meta:
  mcp:
    enabled: true
    description: "Read events from Redpanda"
1 The component name (redpanda) is at the top level, not wrapped in input:.
Output tool
label: publish-event

redpanda:
  seed_brokers: ["${REDPANDA_BROKERS}"]
  topic: "processed-events"

meta:
  mcp:
    enabled: true
    description: "Publish event to Redpanda"
Cache tool
label: session-cache

memory:
  default_ttl: 300s

meta:
  mcp:
    enabled: true
    description: "In-memory cache for session data"

Outputs can include a processors: section to transform data before publishing:

Output with processors
label: publish-with-timestamp

processors:
  - mutation: |
      root = this
      root.published_at = now()

redpanda:
  seed_brokers: ["${REDPANDA_BROKERS}"]
  topic: "processed-events"

meta:
  mcp:
    enabled: true
    description: "Add timestamp and publish to Redpanda"

For more examples, see outputs with processors.

MCP metadata fields

The meta.mcp block defines how AI clients discover and interact with your tool. These fields control tool visibility, naming, and input parameters.

Field Required Description

enabled

Yes

Set to true to expose this component as an MCP tool. Set to false to disable without deleting the configuration.

description

Yes

Explains what the tool does and what it returns. AI clients use this to decide when to call the tool.

properties

No

Array of input parameters the tool accepts. See Property fields for the fields in each property.

tags

No

Array of strings for categorizing tools. Use with --tag flag to filter which tools are exposed.

Property fields

Each entry in the properties array defines an input parameter:

Field Required Description

name

Yes

Parameter name.

type

Yes

Data type. Must be one of: string, number, or boolean.

description

Yes

Explains what the parameter is for. Include example values and any constraints.

required

Yes

Set to true if the tool cannot function without this parameter.

Property restrictions by component type

Different component types have different property capabilities when exposed as MCP tools:

Component Type Property Support Details

input

Only supports the count property

AI clients can specify how many messages to read, but you cannot define custom properties.

cache

No custom properties

Properties are hardcoded to key and value for cache operations.

output

Custom properties supported

AI sees properties as an array for batch operations: [{prop1, prop2}, {prop1, prop2}].

processor

Custom properties supported

You can define any properties needed for data processing operations.

Map parameters to component fields

When an AI client calls your tool, the arguments object becomes the message body. You can access these arguments using Bloblang, but the syntax depends on where you’re using it:

  • Inside Bloblang contexts (mutation, mapping, args_mapping): Use this.field_name

  • Inside string fields (URLs, topics, headers): Use interpolation ${! json("field_name") }

In Bloblang contexts

Use this to access message fields directly in processors like mutation, mapping, or in args_mapping fields:

mutation: |
  root.search_query = this.query.lowercase()
  root.max_results = this.limit.or(10)
sql_select:
  table: orders
  where: customer_id = ? AND status = ?
  args_mapping: '[this.customer_id, this.status.or("active")]'

In string fields (interpolation)

Use ${! …​ } interpolation to embed Bloblang expressions inside string values like URLs or topic names:

http:
  url: 'https://api.weather.com/v1/current?city=${! json("city") }&units=${! json("units").or("metric") }'
redpanda:
  seed_brokers: ["${REDPANDA_BROKERS}"]  (1)
  topic: '${! json("topic_name") }'  (2)
1 ${VAR} without ! is environment variable substitution, not Bloblang.
2 ${! …​ } with ! is Bloblang interpolation that accesses message data.
For more on Bloblang syntax, see Bloblang. For interpolation details, see Interpolation.

Provide defaults for optional parameters

Use .or(default) to handle missing optional parameters:

mutation: |
  root.city = this.city  # Required - will error if missing
  root.units = this.units.or("metric")  # Optional with default
  root.limit = this.limit.or(10).number()  # Optional, converted to number

Declare which parameters are required in your meta.mcp.properties:

properties:
  - name: city
    type: string
    description: "City name to look up"
    required: true
  - name: units
    type: string
    description: "Temperature units: 'metric' or 'imperial' (default: metric)"
    required: false

Use secrets and environment variables

Never hardcode credentials, API keys, or connection strings in your tool files. Use environment variable substitution to inject secrets at runtime.

Reference environment variables using ${VARIABLE_NAME} syntax. Redpanda Connect replaces these placeholders when loading the configuration:

http:
  url: "https://api.example.com/data"
  headers:
    Authorization: "Bearer ${API_TOKEN}"

sql_select:
  driver: postgres
  dsn: "${DATABASE_URL}"
  table: customers
${VAR} is environment variable substitution (resolved at startup). $\{! expr } is Bloblang interpolation (resolved at runtime from message data). See Map parameters to component fields for the difference.

Set environment variables before starting the MCP server:

export API_TOKEN="your-secret-token"
export DATABASE_URL="postgres://user:password@localhost:5432/mydb"
rpk connect mcp-server --address localhost:4195

For production deployments, load secrets from a secrets manager or inject them from your deployment system rather than exporting them in a shell.

For naming conventions and security guidelines, see MCP Tool Design.

Test the tool

  1. Navigate to the root of your MCP project directory.

  2. Lint your configuration:

    rpk connect mcp-server lint
  3. Start the MCP server:

    rpk connect mcp-server --address localhost:8080
  4. Test tool calls using curl:

    #!/bin/bash
    # Start SSE connection and capture session ID
    exec 3< <(curl -s -N http://localhost:8080/sse)
    read -r line <&3  # event: endpoint
    read -r line <&3  # data: /sse?sessionid=XXX
    SESSION_ID=$(echo "$line" | sed 's/.*sessionid=//')
    echo "Session ID: $SESSION_ID"
    
    # Initialize the session
    curl -s -X POST "http://localhost:8080/message?sessionid=$SESSION_ID" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
    
    sleep 1
    
    # Call your tool (replace with your tool name and arguments)
    curl -s -X POST "http://localhost:8080/message?sessionid=$SESSION_ID" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lookup-customer","arguments":{"customer_id":"cust_12345"}}}'
    
    # Read SSE responses
    sleep 2
    while read -r -t 1 line <&3; do
      echo "$line"
    done
    
    exec 3<&-
  5. Connect to an AI client and verify the tool appears. For example, with Claude Code:

    claude mcp add local -- npx mcp-remote http://localhost:8080/sse
    claude /mcp
  6. Test end-to-end with realistic prompts to verify the AI client uses your tool correctly.

Control which tools are exposed

By default, rpk connect mcp-server exposes all tools in your project that have meta.mcp.enabled: true. Use tags to selectively expose subsets of tools.

Add tags to your tool’s metadata:

meta:
  mcp:
    enabled: true
    description: "Query production database"
    tags:
      - production
      - database

Start the server with the --tag flag to expose only tools matching specific tags. The flag supports regular expressions:

# Expose only tools tagged "production"
rpk connect mcp-server --tag production

# Expose tools with tags starting with "db-"
rpk connect mcp-server --tag "db-.*"

For guidance on organizing tools by environment, feature, or access level, see Tag strategies.

Complete example

Here’s a complete tool that wraps the http processor to fetch weather data:

label: get-weather

processors:
  # Validate and sanitize input
  - label: validate_city
    mutation: |
      root.city = if this.city.or("").trim() == "" {
        throw("city is required")
      } else {
        this.city.trim().lowercase().re_replace_all("[^a-z\\s\\-]", "")
      }
      root.units = this.units.or("metric")

  # Fetch weather data
  - label: fetch_weather
    try:
      - http:
          url: 'https://wttr.in/${! json("city") }?format=j1'
          verb: GET
          timeout: 10s

      - mutation: |
          root.weather = {
            "location": this.nearest_area.0.areaName.0.value,
            "country": this.nearest_area.0.country.0.value,
            "temperature_c": this.current_condition.0.temp_C,
            "temperature_f": this.current_condition.0.temp_F,
            "condition": this.current_condition.0.weatherDesc.0.value,
            "humidity": this.current_condition.0.humidity,
            "wind_kph": this.current_condition.0.windspeedKmph
          }

  # Handle errors gracefully
  - label: handle_errors
    catch:
      - mutation: |
          root.error = true
          root.message = "Failed to fetch weather: " + error()

meta:
  mcp:
    enabled: true
    description: "Get current weather for a city. Returns temperature, conditions, humidity, and wind speed."
    properties:
      - name: city
        type: string
        description: "City name (e.g., 'London', 'New York', 'Tokyo')"
        required: true
      - name: units
        type: string
        description: "Temperature units: 'metric' or 'imperial' (default: metric)"
        required: false

Next steps