Skip to main content

Build Custom MCP Tools: Hands-On Tutorial with Code

Β· 9 min read
STOA Team
The STOA Platform Team

Custom MCP tools let you expose any API as an AI-native interface that Claude and other AI agents can discover and invoke automatically. This tutorial walks you through creating, registering, and testing a custom MCP tool using the STOA gateway, from initial YAML definition to live invocation by an AI agent.

What Are Custom MCP Tools?​

The Model Context Protocol (MCP) defines a standard way for AI agents to discover and invoke external tools. Instead of hardcoding API calls or building custom integrations for every service, you define tools as declarative resources that agents can query at runtime.

STOA implements MCP as a Kubernetes-native gateway, meaning your tools are defined as Custom Resource Definitions (CRDs) that live alongside your application manifests. Once registered, these tools become immediately available to any MCP-compatible AI agent connected to your gateway.

By the end of this tutorial, you'll have:

  • A working MCP tool that wraps a REST API
  • Hands-on experience with STOA's Tool CRD
  • A test flow showing an AI agent invoking your tool
  • Understanding of tool discovery, registration, and execution patterns

Prerequisites​

Before you start, ensure you have:

  • STOA gateway running β€” follow the quickstart guide if you haven't deployed yet
  • kubectl access to your Kubernetes cluster (or Docker Compose for local dev)
  • Basic YAML knowledge β€” tools are defined as Kubernetes manifests
  • A REST API to wrap β€” we'll use a public weather API as an example, but you can substitute your own internal API
  • curl or httpie for testing (optional but recommended)

If you're new to MCP concepts, review our MCP gateway overview first.

Step 1: Design Your Tool Specification​

Every MCP tool needs three things: a unique name, input parameters (the schema), and an endpoint to invoke. Let's design a simple weather lookup tool.

Example API: OpenWeatherMap​

We'll wrap the OpenWeatherMap current weather endpoint:

GET https://api.openweathermap.org/data/2.5/weather?q={city}&appid={key}

Tool Design Decisions​

  1. Name: get_current_weather β€” follows MCP naming convention (verb + noun, snake_case)
  2. Input schema: single required parameter city (string)
  3. Output schema: JSON with temp, description, humidity fields
  4. Authentication: API key passed via query parameter (handled by STOA's secret injection)

This design maps cleanly to the Tool CRD structure. For complex APIs with multiple operations, consider creating separate tools for each endpoint (see MCP tools development guide for patterns).

Step 2: Create the Tool CRD Manifest​

STOA uses Kubernetes Custom Resource Definitions to manage tools. Here's the complete manifest for our weather tool:

apiVersion: gostoa.dev/v1alpha1
kind: Tool
metadata:
name: weather-current
namespace: default
labels:
app: weather-service
category: external-api
spec:
displayName: "Get Current Weather"
description: "Fetch current weather conditions for a city using OpenWeatherMap API"

# Input schema (JSON Schema format)
inputSchema:
type: object
required:
- city
properties:
city:
type: string
description: "City name (e.g., 'Paris', 'New York')"
minLength: 2
maxLength: 100

# Backend API configuration
endpoint: "https://api.openweathermap.org/data/2.5/weather"
method: POST

# Authentication and secrets
auth:
type: apiKey
apiKey:
location: query
name: appid
secretRef:
name: openweathermap-creds
key: api-key

# Request transformation
requestTransform:
queryParams:
- name: q
valueFrom: "{{.input.city}}"
- name: units
value: "metric"

# Response transformation
responseTransform:
template: |
{
"temperature": {{.response.main.temp}},
"description": "{{.response.weather[0].description}}",
"humidity": {{.response.main.humidity}},
"city": "{{.response.name}}"
}

Understanding the Manifest​

SectionPurpose
metadataKubernetes identification (name, namespace, labels)
spec.displayNameHuman-readable name shown in MCP tool listings
spec.descriptionHelps AI agents understand when to use this tool
spec.inputSchemaJSON Schema defining required/optional parameters
spec.endpointBackend API URL to invoke
spec.authHow to authenticate (apiKey, bearer, oauth2, mtls)
spec.requestTransformMap MCP input to API query params, headers, or body
spec.responseTransformExtract relevant fields from API response for agent consumption

The responseTransform template uses Go template syntax to shape the API's verbose JSON response into a clean structure the AI agent can easily parse.

Step 3: Store Secrets and Apply the Manifest​

Before applying the Tool CRD, create a Kubernetes Secret for the API key:

kubectl create secret generic openweathermap-creds \
--from-literal=api-key=YOUR_API_KEY_HERE \
-n default

Now apply the tool manifest:

kubectl apply -f weather-tool.yaml

Verify the tool was registered:

kubectl get tools -n default

# Expected output:
# NAME DISPLAY NAME ENDPOINT
# weather-current Get Current Weather https://api.openweathermap.org/...

Check the tool's status:

kubectl describe tool weather-current -n default

The Status section shows:

  • Registered: tool is active and discoverable
  • Endpoint health: STOA validates the backend is reachable
  • Last invocation: timestamp of most recent use (initially empty)

If the status shows Pending or errors, check STOA gateway logs:

kubectl logs -n stoa-system deployment/stoa-gateway --tail=50 | grep weather-current

Step 4: Test Tool Discovery​

MCP agents discover tools via the GET /mcp/v1/tools endpoint. Let's verify your tool appears in the listing:

curl -s https://mcp.gostoa.dev/mcp/v1/tools | jq '.tools[] | select(.name == "get_current_weather")'

Expected response:

{
"name": "get_current_weather",
"description": "Fetch current weather conditions for a city using OpenWeatherMap API",
"inputSchema": {
"type": "object",
"required": ["city"],
"properties": {
"city": {
"type": "string",
"description": "City name (e.g., 'Paris', 'New York')"
}
}
}
}

This is the exact payload an MCP client (like Claude) receives when it queries available tools. Notice how the CRD fields map directly to the MCP tool schema.

For more details on the discovery protocol, see our MCP protocol architecture deep dive.

Step 5: Invoke the Tool​

Now test the tool invocation flow. MCP tools are invoked via POST /mcp/v1/tools/{name}/invoke:

curl -X POST https://mcp.gostoa.dev/mcp/v1/tools/get_current_weather/invoke \
-H "Content-Type: application/json" \
-d '{
"arguments": {
"city": "Paris"
}
}' | jq

Expected response:

{
"result": {
"temperature": 12.5,
"description": "scattered clouds",
"humidity": 67,
"city": "Paris"
},
"isError": false
}

What Just Happened?​

  1. Agent sent: {"city": "Paris"} to STOA's MCP endpoint
  2. STOA transformed: input to GET https://api.openweathermap.org/data/2.5/weather?q=Paris&units=metric&appid=SECRET
  3. Backend responded: with verbose JSON including dozens of fields
  4. STOA transformed: response using the responseTransform template, extracting only the 4 fields defined
  5. Agent received: clean, schema-validated JSON ready for natural language generation

This transformation layer is crucial β€” it lets you expose complex enterprise APIs with messy schemas as clean, AI-friendly tools. See our guide on converting REST APIs to MCP tools for more transformation patterns.

What You've Built​

You now have a production-ready custom MCP tool that:

  • Lives in version control β€” the Tool CRD is a YAML file you can commit, review, and track alongside your app code
  • Integrates with Kubernetes RBAC β€” use namespaces and role bindings to control which teams can register tools
  • Handles secrets securely β€” API keys never appear in manifests or logs
  • Transforms data automatically β€” agents get clean responses without knowing the backend API's quirks
  • Scales with your infrastructure β€” STOA handles rate limiting, retries, and observability (metrics for every tool invocation)

To connect an AI agent like Claude to your tool, configure the MCP client with your gateway URL (https://mcp.gostoa.dev). The agent will automatically discover get_current_weather and can invoke it whenever a user asks about weather conditions.

Advanced Patterns​

Multi-Step Tools​

For complex workflows (e.g., "create user, then send email"), use ToolSets to group related tools:

apiVersion: gostoa.dev/v1alpha1
kind: ToolSet
metadata:
name: user-onboarding
spec:
tools:
- toolRef:
name: create-user
- toolRef:
name: send-welcome-email
executionOrder: sequential

Dynamic Parameters​

Use templates in requestTransform to derive parameters from context:

requestTransform:
headers:
- name: X-Tenant-ID
valueFrom: "{{.context.tenant}}"
- name: X-Request-ID
valueFrom: "{{.context.requestId}}"

Response Validation​

Add JSON Schema validation to catch malformed backend responses:

spec:
responseSchema:
type: object
required: [temperature, city]
properties:
temperature:
type: number
minimum: -100
maximum: 100

For more advanced patterns, see the MCP tools reference documentation.

Troubleshooting Common Issues​

SymptomCauseFix
Tool not appearing in listingsNamespace mismatch or RBAC issueCheck kubectl get tools -A and namespace in curl
401 Unauthorized on invokeSecret not mounted or wrong key nameVerify secret exists: kubectl get secret openweathermap-creds -o yaml
Invocation times outBackend unreachable or slowAdd spec.timeout: 30s to Tool manifest
Empty or malformed responseTransform template errorCheck gateway logs for template parse errors
Tool registered but not invokableBackend API changedUpdate endpoint or responseTransform

FAQ​

Can I wrap internal APIs behind firewalls?​

Yes. STOA runs inside your Kubernetes cluster, so tools can reference internal service names (e.g., http://user-service.prod.svc.cluster.local). For APIs outside the cluster, ensure the STOA gateway pod has network access. Use mTLS authentication for secure internal communication.

How do I version tools when APIs change?​

Use the tool name as a version identifier (e.g., weather-v1, weather-v2). Agents can specify which version to invoke. Alternatively, update the existing tool and rely on STOA's schema validation to catch breaking changes. For production systems, consider blue/green deployments where both versions run simultaneously during migration.

What authentication methods are supported?​

STOA supports API key (query, header, or cookie), Bearer token, OAuth 2.0 client credentials, and mutual TLS (mTLS). For OAuth, STOA can handle token refresh automatically using a TokenSource CRD. See the MCP getting started guide for authentication examples.

Next Steps​

Now that you've built your first custom MCP tool, explore these resources:

For questions or to share what you've built, join the discussion on GitHub Discussions or open an issue if you hit a snag. The STOA community is here to help you bridge the gap between traditional APIs and the AI-native future.