Build Your First Custom Tool

Goal: Expose a real API endpoint to the model — the model calls your tool, TengineAI forwards the request to your server with bearer token authentication, and your server returns a result.

By the end you will see the exact headers TengineAI injects, the authenticated request your server receives, and the model's natural language response.

For the meaning of every tool field, see the Custom Tools Reference.


What We're Building

A CRM contact lookup tool. The model calls crm-contacts-get_by_id, TengineAI POSTs to your server with a static bearer token, and your server returns the contact record.


Step 1: Build the Server

A minimal FastAPI endpoint that validates the bearer token and responds to TengineAI tool calls:

# server.py
import os
from fastapi import FastAPI, Header, HTTPException, Request
import uvicorn

app = FastAPI()

BEARER_TOKEN = os.environ["TENGINEAI_API_KEY"]

CONTACTS = {
    "c_001": {"id": "c_001", "name": "Alice Johnson", "email": "alice@acme.com", "tier": "enterprise"},
    "c_002": {"id": "c_002", "name": "Bob Smith", "email": "bob@startup.io", "tier": "pro"},
}


@app.post("/v1/contacts/lookup")
async def lookup_contact(
    request: Request,
    authorization: str = Header(...),
    x_tengine_project_id: str = Header(default=""),
    x_tengine_member_id: str = Header(default=""),
    x_tengine_request_id: str = Header(default=""),
):
    # Validate bearer token
    if authorization != f"Bearer {BEARER_TOKEN}":
        raise HTTPException(status_code=401, detail="Unauthorized")

    body = await request.json()

    print(f"Request ID:  {x_tengine_request_id}")
    print(f"Project ID:  {x_tengine_project_id}")
    print(f"Member ID:   {x_tengine_member_id}")
    print(f"Body:        {body}")

    contact_id = body.get("contact_id")
    contact = CONTACTS.get(contact_id)

    if not contact:
        return {"found": False, "contact_id": contact_id}

    return {"found": True, "contact": contact, "requested_by_member": x_tengine_member_id}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Run it:

pip install fastapi uvicorn
python server.py

TengineAI cannot reach localhost directly — expose it with a tunnel:

ngrok http 8000
# Copy the https:// URL — you'll use it as the Base URL in Step 2

Step 2: Register the Tool in the Console

In your TengineAI Dashboard, open your project and go to Tools → Create Tool.

Basic Information

  • Tool name: crm-contacts-get_by_id
  • Description: Look up a CRM contact by their ID. Returns name, email, and account tier.
  • Requires member context: Off

Request

  • Base URL: your ngrok https:// URL (e.g. https://abc123.ngrok.io)
  • HTTP Method: POST
  • Endpoint Path: /v1/contacts/lookup

Authentication Strategy

  • Auth Strategy: Static Bearer Token
  • Auth Secret: your tengine_... API key

TengineAI will add Authorization: Bearer tengine_... to every outbound request. Your server validates it against the same key.

Advanced — Input Schema

{
  "type": "object",
  "properties": {
    "contact_id": {
      "type": "string",
      "description": "The contact ID to look up (e.g. c_001)"
    }
  },
  "required": ["contact_id"]
}

Click Create Tool, then confirm it appears as enabled in your project's tool list.


Step 3: Call the Tool with the Model

import json
import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

response = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Look up contact c_001 and tell me their account tier.",
        }
    ],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://app.tengine.ai/mcp/",
            "name": "tengineai",
            "authorization_token": os.environ["TENGINEAI_API_KEY"],
        }
    ],
    betas=["mcp-client-2025-04-04"],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "mcp_tool_use":
        print(
            f"[tool_use] server={block.server_name} tool={block.name} id={block.id} "
            f"input={json.dumps(block.input, default=str)}"
        )
    elif block.type == "mcp_tool_result":
        print(
            f"[tool_result] id={block.tool_use_id} is_error={getattr(block, 'is_error', False)}"
        )
        print(block.content)
export ANTHROPIC_API_KEY="sk-ant-..."
export TENGINEAI_API_KEY="tengine_..."
pip install anthropic
python run.py

Expected output:

Alice Johnson is on the Enterprise tier. Her email is alice@acme.com.

Step 4: Inspect What TengineAI Sent

Your server logs will show exactly what TengineAI forwarded:

Request ID:  7f3a9b12-e4c8-4d1a-b290-1f8e3c72aa40
Project ID:  42
Member ID:   (empty — plain API key used)
Body:        {"contact_id": "c_001"}

The Authorization: Bearer my-secret-token-changeme header will also be present — your server checked it before reaching the log statements.

When a member session token (tng_mst_...) is used instead of an API key, Member ID will contain the user's external ID.


Step 5: Simulate a Request Without TengineAI

Test your endpoint directly to confirm the bearer token check works before connecting TengineAI:

curl -X POST "http://localhost:8000/v1/contacts/lookup" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TENGINEAI_API_KEY" \
  -H "X-Tengine-Project-Id: 42" \
  -H "X-Tengine-Member-Id: " \
  -H "X-Tengine-Request-Id: test-local" \
  -d '{"contact_id":"c_001"}'

If your server returns the contact record, the auth check is correct and TengineAI's live requests will pass.


Debugging

Tool not appearing in the model's tool list

  • Confirm the tool is enabled in your project's Tools list
  • Confirm the API key belongs to the same project as the tool
  • If require_member: true, use a member session token (tng_mst_...), not a plain API key

Tool called but server received nothing

  • Confirm base_url is publicly reachable — TengineAI cannot reach localhost directly
  • Confirm the ngrok tunnel is running and the URL in base_url matches the current tunnel address

401 from your server

  • Confirm the Auth Secret in your tool config exactly matches your TENGINEAI_API_KEY
  • Use the curl simulation in Step 5 to test locally before involving TengineAI

Inspect exact headers TengineAI sends

Add a logging middleware to print every inbound header:

from fastapi import Request
import logging

@app.middleware("http")
async def log_requests(request: Request, call_next):
    logging.info(f"Headers: {dict(request.headers)}")
    return await call_next(request)

For the full list of headers TengineAI injects and when each is present, see Identity Headers in the Custom Tools Reference.


Next Steps