{"slug": "mcp-server-on-edge-compute-167-lines-of-python", "title": "MCP Server on Edge Compute 167 Lines of Python", "summary": "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.", "body_md": "The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 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`\n\nendpoint and call them via `tools/call`\n\n. Any HTTP service that implements that contract becomes a tool provider the agent can use.\n\nThis walkthrough deploys a working MCP server to Telnyx Edge Compute. The whole server is 167 lines of Python in `function/func.py`\n\n, uses Python's standard library for the HTTP layer, and exposes four Telnyx APIs (`send_sms`\n\n, `search_numbers`\n\n, `run_inference`\n\n, `list_phone_numbers`\n\n) as MCP tools. There is no Express, no FastAPI, no SDK, just `urllib`\n\n, `json`\n\n, `os`\n\n, and an ASGI handler.\n\nThe canonical code example lives in the Telnyx code examples repo:\n\nhttps://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-mcp-server-deploy-python\n\n## What This Example Builds\n\nA single ASGI function deployed to Telnyx Edge Compute that exposes four MCP tools:\n\n| Tool | Telnyx API | Purpose |\n|---|---|---|\n`send_sms` | `POST /v2/messages` | Send an SMS message to an E.164 number |\n`search_numbers` | `GET /v2/available_phone_numbers` | Search available phone numbers by country and area code |\n`run_inference` | `POST /v2/ai/chat/completions` | Run LLM inference via Telnyx AI (OpenAI-compatible) |\n`list_phone_numbers` | `GET /v2/phone_numbers` | List phone numbers on the account |\n\nThe function exposes four HTTP endpoints:\n\n| Method | Path | Purpose |\n|---|---|---|\n`GET` , `POST` | `/mcp/tools/list` | Return the tool catalog |\n`POST` | `/mcp/tools/call` | Execute a tool by name with arguments |\n`GET` | `/health` | Health check (tool count, API key presence) |\n`GET` | `/` | Service info (name, tool list, endpoint paths) |\n\nOnce 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.\n\n## Why Edge Compute\n\nEdge 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:\n\n**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.\n\nThe 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.\n\n## Products Used\n\n```\ntelnyx_products: [Edge Compute, Messaging, Numbers, AI Inference]\n```\n\nTelnyx Edge Compute runs the ASGI function. Messaging powers `send_sms`\n\n. Numbers powers `search_numbers`\n\nand `list_phone_numbers`\n\n. AI Inference powers `run_inference`\n\nvia the OpenAI-compatible chat completions endpoint.\n\n## Architecture\n\n```\n        AI Agent (Claude / Cursor / MCP client)\n                    │\n                    │ HTTPS\n                    ▼\n        ┌──────────────────────┐\n        │  Edge Compute         │  function/func.py\n        │  ASGI handler         │  167 lines, stdlib only\n        │                       │\n        │  GET  /mcp/tools/list │  → returns TOOLS array\n        │  POST /mcp/tools/call │  → _execute_tool(name, args)\n        │  GET  /health         │\n        └──────────┬────────────┘\n                   │ Bearer TELNYX_API_KEY\n                   │ (injected as secret at deploy time)\n                   ▼\n        https://api.telnyx.com/v2\n        ├── /messages            (Messaging)\n        ├── /available_phone_numbers (Numbers)\n        ├── /phone_numbers       (Numbers)\n        └── /ai/chat/completions (AI Inference)\n```\n\nThe 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`\n\nenvironment variable when the function boots.\n\n## The Code\n\nThe full function is 167 lines in `function/func.py`\n\n. Three pieces matter.\n\n### The tool catalog\n\nThe `TOOLS`\n\narray is the contract the agent reads during `tools/list`\n\n. Each entry has a `name`\n\n, a human-readable `description`\n\n, and a JSON Schema `inputSchema`\n\n. The schema is what the agent uses to construct valid arguments:\n\n```\nTOOLS = [\n    {\n        \"name\": \"send_sms\",\n        \"description\": \"Send an SMS message via Telnyx\",\n        \"inputSchema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"to\": {\"type\": \"string\", \"description\": \"Destination phone number in E.164 format\"},\n                \"from_number\": {\"type\": \"string\", \"description\": \"Your Telnyx phone number in E.164\"},\n                \"text\": {\"type\": \"string\", \"description\": \"Message body\"},\n            },\n            \"required\": [\"to\", \"from_number\", \"text\"],\n        },\n    },\n    {\n        \"name\": \"search_numbers\",\n        \"description\": \"Search for available phone numbers to purchase\",\n        \"inputSchema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"country_code\": {\"type\": \"string\", \"description\": \"ISO 3166-1 alpha-2 country code\", \"default\": \"US\"},\n                \"area_code\": {\"type\": \"string\", \"description\": \"Area code to search in\"},\n                \"limit\": {\"type\": \"integer\", \"description\": \"Max results\", \"default\": 5},\n            },\n        },\n    },\n    {\n        \"name\": \"run_inference\",\n        \"description\": \"Run LLM inference via Telnyx AI (OpenAI-compatible)\",\n        \"inputSchema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"prompt\": {\"type\": \"string\", \"description\": \"User prompt\"},\n                \"model\": {\"type\": \"string\", \"description\": \"Model name\", \"default\": \"moonshotai/Kimi-K2.6\"},\n                \"max_tokens\": {\"type\": \"integer\", \"default\": 256},\n            },\n            \"required\": [\"prompt\"],\n        },\n    },\n    {\n        \"name\": \"list_phone_numbers\",\n        \"description\": \"List phone numbers on your Telnyx account\",\n        \"inputSchema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"page_size\": {\"type\": \"integer\", \"default\": 10},\n            },\n        },\n    },\n]\n```\n\nThe agent reads this array, picks the right tool for the user's intent, fills in the arguments, and POSTs to `/mcp/tools/call`\n\n.\n\n### The tool executor\n\n`_execute_tool`\n\nis a flat dispatcher. The MCP layer extracts `name`\n\nand `arguments`\n\nfrom 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:\n\n``` python\ndef _execute_tool(self, name, args):\n    if name == \"send_sms\":\n        return self._telnyx_request(\"POST\", \"/messages\", {\n            \"from\": args[\"from_number\"], \"to\": args[\"to\"], \"text\": args[\"text\"],\n        })\n    elif name == \"search_numbers\":\n        cc = args.get(\"country_code\", \"US\")\n        limit = args.get(\"limit\", 5)\n        path = f\"/available_phone_numbers?filter[country_code]={cc}&filter[limit]={limit}\"\n        if args.get(\"area_code\"):\n            path += f\"&filter[national_destination_code]={args['area_code']}\"\n        return self._telnyx_request(\"GET\", path)\n    elif name == \"run_inference\":\n        return self._telnyx_request(\"POST\", \"/ai/chat/completions\", {\n            \"model\": args.get(\"model\", \"moonshotai/Kimi-K2.6\"),\n            \"messages\": [{\"role\": \"user\", \"content\": args[\"prompt\"]}],\n            \"max_tokens\": args.get(\"max_tokens\", 256),\n        })\n    elif name == \"list_phone_numbers\":\n        size = args.get(\"page_size\", 10)\n        return self._telnyx_request(\"GET\", f\"/phone_numbers?page[size]={size}\")\n    return {\"error\": f\"Unknown tool: {name}\"}\n```\n\nAdding a new tool means appending to `TOOLS`\n\nand adding one more branch here. The two stay in sync because both reference the same string names.\n\n### The ASGI handler\n\nThe whole HTTP surface is one method. Edge Compute calls `handle(scope, receive, send)`\n\non every request, the ASGI contract. The method routes on `path`\n\nand `method`\n\n, reads the JSON body for `tools/call`\n\n, and returns a JSON response:\n\n``` python\nasync def handle(self, scope, receive, send):\n    if scope.get(\"type\") != HTTP_SCOPE_TYPE:\n        await self._respond(send, 400, {\"error\": \"not http\"})\n        return\n\n    path = scope.get(\"path\", \"/\")\n    method = scope.get(\"method\", \"GET\")\n\n    if path == \"/mcp/tools/list\" and method in (\"GET\", \"POST\"):\n        await self._respond(send, 200, {\"tools\": TOOLS})\n        return\n\n    if path == \"/mcp/tools/call\" and method == \"POST\":\n        body = await self._read_body(receive)\n        try:\n            req_data = json.loads(body)\n        except json.JSONDecodeError:\n            await self._respond(send, 400, {\"error\": \"invalid json\"})\n            return\n        tool_name = req_data.get(\"params\", {}).get(\"name\", req_data.get(\"name\", \"\"))\n        tool_args = req_data.get(\"params\", {}).get(\"arguments\", req_data.get(\"arguments\", {}))\n        result = self._execute_tool(tool_name, tool_args)\n        await self._respond(send, 200, {\n            \"content\": [{\"type\": \"text\", \"text\": json.dumps(result, indent=2)}],\n            \"isError\": \"error\" in result,\n        })\n        return\n\n    if path == \"/health\" and method == \"GET\":\n        await self._respond(send, 200, {\n            \"status\": \"ok\", \"tools\": len(TOOLS), \"has_api_key\": bool(self.api_key),\n        })\n        return\n\n    if path == \"/\" and method == \"GET\":\n        await self._respond(send, 200, {\n            \"name\": \"telnyx-mcp\",\n            \"description\": \"Telnyx MCP Server, send SMS, search numbers, run inference\",\n            \"tools\": [t[\"name\"] for t in TOOLS],\n            \"endpoints\": {\"list_tools\": \"/mcp/tools/list\", \"call_tool\": \"/mcp/tools/call\", \"health\": \"/health\"},\n        })\n        return\n\n    await self._respond(send, 404, {\"error\": \"not found\"})\n```\n\nThe handler accepts both `params`\n\nand a flat shape for the tool name and arguments, so it works with MCP clients that send either convention.\n\n## Understanding MCP\n\nMCP is an HTTP contract. There are two methods any server must support:\n\n, return a JSON array of tool definitions. Each tool has a`tools/list`\n\n`name`\n\n,`description`\n\n, and JSON Schema describing its arguments. The agent reads this to know what it can call., accept a JSON body with`tools/call`\n\n`name`\n\nand`arguments`\n\n, execute the tool, and return the result as a`content`\n\narray with a`type`\n\nof`text`\n\nand a`text`\n\nfield containing the JSON-stringified result.\n\nThat'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.\n\nThe Telnyx implementation accepts both `{\"name\": \"send_sms\", \"arguments\": {...}}`\n\n(flat) and `{\"params\": {\"name\": \"send_sms\", \"arguments\": {...}}}`\n\n(nested) shapes, because different MCP clients use different conventions.\n\n## Deploy It\n\n```\ngit clone https://github.com/team-telnyx/telnyx-code-examples.git\ncd telnyx-code-examples/edge-mcp-server-deploy-python\ntelnyx-edge auth login\ntelnyx-edge secrets add TELNYX_API_KEY <your-api-key>\ntelnyx-edge ship\n```\n\n`telnyx-edge ship`\n\npackages `func.toml`\n\nplus the `function/`\n\ndirectory and pushes them to Telnyx Edge Compute. The deploy returns a URL like `https://mcp-telnyx-tools-<id>.telnyxcompute.com`\n\n. The `TELNYX_API_KEY`\n\nis stored as an Edge Compute secret and injected into the function as an environment variable on each invocation, the agent never sees the key.\n\nVerify the deploy:\n\n```\ncurl https://mcp-telnyx-tools-<id>.telnyxcompute.com/health\n# {\"status\":\"ok\",\"tools\":4,\"has_api_key\":true}\n\ncurl https://mcp-telnyx-tools-<id>.telnyxcompute.com/mcp/tools/list\n# {\"tools\":[{\"name\":\"send_sms\",\"description\":\"...\",\"inputSchema\":{...}}, ...]}\n\ncurl -X POST https://mcp-telnyx-tools-<id>.telnyxcompute.com/mcp/tools/call \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"list_phone_numbers\", \"arguments\": {\"page_size\": 5}}'\n# {\"content\":[{\"type\":\"text\",\"text\":\"{...phone numbers...}\"}],\"isError\":false}\n```\n\nPoint your MCP-compatible agent at the deployed URL and it will discover the four tools and call them as part of its reasoning loop.\n\n## Connecting AI Agents\n\nMCP 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.\n\nFor 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`\n\n, learns the four tools, and uses them whenever the user's request maps to send SMS, search numbers, run inference, or list numbers.\n\n## Going to Production\n\nThis example ships with sensible defaults for a working demo. For production:\n\n**Authentication at the MCP layer**, the example has none. Add Bearer token validation on`/mcp/tools/list`\n\nand`/mcp/tools/call`\n\nso 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`\n\nwith a per-IP or per-token rate limiter.**Tool-level access control**, agents that should only send SMS, not run inference, should not see`run_inference`\n\nin the tool list. Filter`TOOLS`\n\nper-agent based on the authenticated token.**Logging and tracing**, add structured logs for each`tools/call`\n\ninvocation so you can audit what agents did. The function already has a logger; pipe it somewhere searchable.**Timeout tuning**,`run_inference`\n\ndefaults to 256 tokens, which returns in under a second. For longer completions, raise`max_tokens`\n\nand the`urlopen`\n\ntimeout accordingly.**Local development**, the`func.py`\n\nis plain ASGI, so it runs under any ASGI server (`uvicorn function.func:app`\n\nwith a tiny`app = MCPServer()`\n\nwrapper). Iterate locally before shipping to the edge.", "url": "https://wpnews.pro/news/mcp-server-on-edge-compute-167-lines-of-python", "canonical_source": "https://lowlatencyclub.ai/blog/posts/edge-mcp-server-deploy-python", "published_at": "2026-07-10 12:27:26+00:00", "updated_at": "2026-07-10 12:35:12.866505+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Telnyx", "Claude", "Cursor", "Model Context Protocol", "Edge Compute"], "alternates": {"html": "https://wpnews.pro/news/mcp-server-on-edge-compute-167-lines-of-python", "markdown": "https://wpnews.pro/news/mcp-server-on-edge-compute-167-lines-of-python.md", "text": "https://wpnews.pro/news/mcp-server-on-edge-compute-167-lines-of-python.txt", "jsonld": "https://wpnews.pro/news/mcp-server-on-edge-compute-167-lines-of-python.jsonld"}}