cd /news/ai-agents/mcp-server-on-edge-compute-167-lines… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-54168] src=lowlatencyclub.ai β†— pub= topic=ai-agents verified=true sentiment=↑ positive

MCP Server on Edge Compute 167 Lines of Python

Telnyx released a 167-line Python MCP server for Edge Compute that exposes four Telnyx APIs as AI agent tools, including SMS, number search, and LLM inference. The server runs on Telnyx's private network, eliminating public internet round trips and reducing latency for agent tool calls.

read9 min views1 publishedJul 10, 2026
MCP Server on Edge Compute 167 Lines of Python
Image: source

The Model Context Protocol (MCP) has become the standard way for AI agents to call external tools. Claude, Cursor, and a growing ecosystem of agent frameworks speak MCP natively, they discover tools via a tools/list

endpoint and call them via tools/call

. Any HTTP service that implements that contract becomes a tool provider the agent can use.

This walkthrough deploys a working MCP server to Telnyx Edge Compute. The whole server is 167 lines of Python in function/func.py

, uses Python's standard library for the HTTP layer, and exposes four Telnyx APIs (send_sms

, search_numbers

, run_inference

, list_phone_numbers

) as MCP tools. There is no Express, no FastAPI, no SDK, just urllib

, json

, os

, and an ASGI handler.

The canonical code example lives in the Telnyx code examples repo:

https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-mcp-server-deploy-python

What This Example Builds #

A single ASGI function deployed to Telnyx Edge Compute that exposes four MCP tools:

Tool Telnyx API Purpose
send_sms POST /v2/messages Send an SMS message to an E.164 number
search_numbers GET /v2/available_phone_numbers Search available phone numbers by country and area code
run_inference POST /v2/ai/chat/completions Run LLM inference via Telnyx AI (OpenAI-compatible)
list_phone_numbers GET /v2/phone_numbers List phone numbers on the account

The function exposes four HTTP endpoints:

Method Path Purpose
GET , POST /mcp/tools/list Return the tool catalog
POST /mcp/tools/call Execute a tool by name with arguments
GET /health Health check (tool count, API key presence)
GET / Service info (name, tool list, endpoint paths)

Once deployed, an AI agent that supports MCP can be pointed at the deployed URL and immediately call Telnyx APIs as part of its reasoning loop.

Why Edge Compute #

Edge Compute runs serverless functions co-located with the Telnyx private network, the same network that handles voice, messaging, and AI inference. For an MCP server that calls Telnyx APIs as tools, that means:

No public internet round trips for the tool execution path. The function runs in the same network as the Telnyx API endpoints.Single API key surface. The Telnyx API key is injected as a function secret at deploy time, never sent over the wire by the AI agent.No cold-start container orchestration. Edge Compute is a function runtime, not a container host.

The MCP server sits one network hop away from every Telnyx API it calls. That is the carrier-edge advantage for AI agents: the same private network that carries SMS traffic carries the tool calls.

Products Used #

telnyx_products: [Edge Compute, Messaging, Numbers, AI Inference]

Telnyx Edge Compute runs the ASGI function. Messaging powers send_sms

. Numbers powers search_numbers

and list_phone_numbers

. AI Inference powers run_inference

via the OpenAI-compatible chat completions endpoint.

Architecture #

        AI Agent (Claude / Cursor / MCP client)
                    β”‚
                    β”‚ HTTPS
                    β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  Edge Compute         β”‚  function/func.py
        β”‚  ASGI handler         β”‚  167 lines, stdlib only
        β”‚                       β”‚
        β”‚  GET  /mcp/tools/list β”‚  β†’ returns TOOLS array
        β”‚  POST /mcp/tools/call β”‚  β†’ _execute_tool(name, args)
        β”‚  GET  /health         β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚ Bearer TELNYX_API_KEY
                   β”‚ (injected as secret at deploy time)
                   β–Ό
        https://api.telnyx.com/v2
        β”œβ”€β”€ /messages            (Messaging)
        β”œβ”€β”€ /available_phone_numbers (Numbers)
        β”œβ”€β”€ /phone_numbers       (Numbers)
        └── /ai/chat/completions (AI Inference)

The agent only knows the deployed URL. It never sees the Telnyx API key. The key is injected by the Edge Compute runtime as the TELNYX_API_KEY

environment variable when the function boots.

The Code #

The full function is 167 lines in function/func.py

. Three pieces matter.

The tool catalog

The TOOLS

array is the contract the agent reads during tools/list

. Each entry has a name

, a human-readable description

, and a JSON Schema inputSchema

. The schema is what the agent uses to construct valid arguments:

TOOLS = [
    {
        "name": "send_sms",
        "description": "Send an SMS message via Telnyx",
        "inputSchema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Destination phone number in E.164 format"},
                "from_number": {"type": "string", "description": "Your Telnyx phone number in E.164"},
                "text": {"type": "string", "description": "Message body"},
            },
            "required": ["to", "from_number", "text"],
        },
    },
    {
        "name": "search_numbers",
        "description": "Search for available phone numbers to purchase",
        "inputSchema": {
            "type": "object",
            "properties": {
                "country_code": {"type": "string", "description": "ISO 3166-1 alpha-2 country code", "default": "US"},
                "area_code": {"type": "string", "description": "Area code to search in"},
                "limit": {"type": "integer", "description": "Max results", "default": 5},
            },
        },
    },
    {
        "name": "run_inference",
        "description": "Run LLM inference via Telnyx AI (OpenAI-compatible)",
        "inputSchema": {
            "type": "object",
            "properties": {
                "prompt": {"type": "string", "description": "User prompt"},
                "model": {"type": "string", "description": "Model name", "default": "moonshotai/Kimi-K2.6"},
                "max_tokens": {"type": "integer", "default": 256},
            },
            "required": ["prompt"],
        },
    },
    {
        "name": "list_phone_numbers",
        "description": "List phone numbers on your Telnyx account",
        "inputSchema": {
            "type": "object",
            "properties": {
                "page_size": {"type": "integer", "default": 10},
            },
        },
    },
]

The agent reads this array, picks the right tool for the user's intent, fills in the arguments, and POSTs to /mcp/tools/call

.

The tool executor

_execute_tool

is a flat dispatcher. The MCP layer extracts name

and arguments

from the request body and hands them to this method, which translates them into Telnyx REST calls. Every tool hits a different endpoint with different body shapes:

def _execute_tool(self, name, args):
    if name == "send_sms":
        return self._telnyx_request("POST", "/messages", {
            "from": args["from_number"], "to": args["to"], "text": args["text"],
        })
    elif name == "search_numbers":
        cc = args.get("country_code", "US")
        limit = args.get("limit", 5)
        path = f"/available_phone_numbers?filter[country_code]={cc}&filter[limit]={limit}"
        if args.get("area_code"):
            path += f"&filter[national_destination_code]={args['area_code']}"
        return self._telnyx_request("GET", path)
    elif name == "run_inference":
        return self._telnyx_request("POST", "/ai/chat/completions", {
            "model": args.get("model", "moonshotai/Kimi-K2.6"),
            "messages": [{"role": "user", "content": args["prompt"]}],
            "max_tokens": args.get("max_tokens", 256),
        })
    elif name == "list_phone_numbers":
        size = args.get("page_size", 10)
        return self._telnyx_request("GET", f"/phone_numbers?page[size]={size}")
    return {"error": f"Unknown tool: {name}"}

Adding a new tool means appending to TOOLS

and adding one more branch here. The two stay in sync because both reference the same string names.

The ASGI handler

The whole HTTP surface is one method. Edge Compute calls handle(scope, receive, send)

on every request, the ASGI contract. The method routes on path

and method

, reads the JSON body for tools/call

, and returns a JSON response:

async def handle(self, scope, receive, send):
    if scope.get("type") != HTTP_SCOPE_TYPE:
        await self._respond(send, 400, {"error": "not http"})
        return

    path = scope.get("path", "/")
    method = scope.get("method", "GET")

    if path == "/mcp/tools/list" and method in ("GET", "POST"):
        await self._respond(send, 200, {"tools": TOOLS})
        return

    if path == "/mcp/tools/call" and method == "POST":
        body = await self._read_body(receive)
        try:
            req_data = json.loads(body)
        except json.JSONDecodeError:
            await self._respond(send, 400, {"error": "invalid json"})
            return
        tool_name = req_data.get("params", {}).get("name", req_data.get("name", ""))
        tool_args = req_data.get("params", {}).get("arguments", req_data.get("arguments", {}))
        result = self._execute_tool(tool_name, tool_args)
        await self._respond(send, 200, {
            "content": [{"type": "text", "text": json.dumps(result, indent=2)}],
            "isError": "error" in result,
        })
        return

    if path == "/health" and method == "GET":
        await self._respond(send, 200, {
            "status": "ok", "tools": len(TOOLS), "has_api_key": bool(self.api_key),
        })
        return

    if path == "/" and method == "GET":
        await self._respond(send, 200, {
            "name": "telnyx-mcp",
            "description": "Telnyx MCP Server, send SMS, search numbers, run inference",
            "tools": [t["name"] for t in TOOLS],
            "endpoints": {"list_tools": "/mcp/tools/list", "call_tool": "/mcp/tools/call", "health": "/health"},
        })
        return

    await self._respond(send, 404, {"error": "not found"})

The handler accepts both params

and a flat shape for the tool name and arguments, so it works with MCP clients that send either convention.

Understanding MCP #

MCP is an HTTP contract. There are two methods any server must support:

, return a JSON array of tool definitions. Each tool has atools/list

name

,description

, and JSON Schema describing its arguments. The agent reads this to know what it can call., accept a JSON body withtools/call

name

andarguments

, execute the tool, and return the result as acontent

array with atype

oftext

and atext

field containing the JSON-stringified result.

That's it. No streaming, no sessions, no authentication at the MCP layer, the transport is plain HTTPS with JSON bodies. Any HTTP client can be an MCP server. Any HTTP server exposing those two endpoints is an MCP server.

The Telnyx implementation accepts both {"name": "send_sms", "arguments": {...}}

(flat) and {"params": {"name": "send_sms", "arguments": {...}}}

(nested) shapes, because different MCP clients use different conventions.

Deploy It #

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/edge-mcp-server-deploy-python
telnyx-edge auth login
telnyx-edge secrets add TELNYX_API_KEY <your-api-key>
telnyx-edge ship

telnyx-edge ship

packages func.toml

plus the function/

directory and pushes them to Telnyx Edge Compute. The deploy returns a URL like https://mcp-telnyx-tools-<id>.telnyxcompute.com

. The TELNYX_API_KEY

is stored as an Edge Compute secret and injected into the function as an environment variable on each invocation, the agent never sees the key.

Verify the deploy:

curl https://mcp-telnyx-tools-<id>.telnyxcompute.com/health

curl https://mcp-telnyx-tools-<id>.telnyxcompute.com/mcp/tools/list

curl -X POST https://mcp-telnyx-tools-<id>.telnyxcompute.com/mcp/tools/call \
  -H "Content-Type: application/json" \
  -d '{"name": "list_phone_numbers", "arguments": {"page_size": 5}}'

Point your MCP-compatible agent at the deployed URL and it will discover the four tools and call them as part of its reasoning loop.

Connecting AI Agents #

MCP support is built into Claude Desktop, Cursor, and a growing number of agent frameworks. Each one has its own way of registering a remote MCP server, the connection details are always the same: the deployed URL plus, if your agent requires authentication, a Bearer token.

For Claude Desktop, the server URL is enough. For Claude Code, Cursor, or other MCP clients, add the URL to your agent's MCP configuration. The first request from the agent hits /mcp/tools/list

, learns the four tools, and uses them whenever the user's request maps to send SMS, search numbers, run inference, or list numbers.

Going to Production #

This example ships with sensible defaults for a working demo. For production:

Authentication at the MCP layer, the example has none. Add Bearer token validation on/mcp/tools/list

and/mcp/tools/call

so only authorized agents can discover and call your tools. The Telnyx API key is already protected (it's a secret, not a request parameter), but the MCP endpoint itself is open.Rate limiting, protect against runaway agents. Wrap_execute_tool

with a per-IP or per-token rate limiter.Tool-level access control, agents that should only send SMS, not run inference, should not seerun_inference

in the tool list. FilterTOOLS

per-agent based on the authenticated token.Logging and tracing, add structured logs for eachtools/call

invocation so you can audit what agents did. The function already has a logger; pipe it somewhere searchable.Timeout tuning,run_inference

defaults to 256 tokens, which returns in under a second. For longer completions, raisemax_tokens

and theurlopen

timeout accordingly.Local development, thefunc.py

is plain ASGI, so it runs under any ASGI server (uvicorn function.func:app

with a tinyapp = MCPServer()

wrapper). Iterate locally before shipping to the edge.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @telnyx 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/mcp-server-on-edge-c…] indexed:0 read:9min 2026-07-10 Β· β€”