Blog

Python Support for Runnable Code Servers

You can now write Runnable Code servers in Python 3.13. Put a schema dict and your functions in main.py, and Gatana deploys it and exposes the functions as MCP tools, the same way it does for Node.js.

· Erik Jonsson Thorén, Founder, Gatana

A white rounded tile with the Python logo, tilted and floating above a blue surface

Python is now a runtime for Runnable Code

Runnable Code servers turn a file with a few functions into MCP tools. Gatana deploys the file, lists the functions as tools in the gateway, and passes each call the caller’s credentials. There is no server code to write and no container to build. Up to now the file had to be JavaScript, running on Node.js 24.

A lot of the code people want to expose is Python, and porting it to JavaScript just to get a tool out of it is a poor use of time. Python 3.13 is now available as a second runtime.

Choosing the runtime

When you create a Runnable Code server, a Runtime field offers Node.js 24 and Python 3.13. With Python selected, the server starts with a sample main.py instead of an index.js. You can switch the runtime later in the server’s Settings tab. The server redeploys, and you then need a package written for the new runtime.

What a Python package looks like

main.py works the way index.js does. A schema dict lists the tools, and the module has a function with the same name for each of them:

import json

from pydantic import BaseModel


class AddInput(BaseModel):
    a: float
    b: float


schema = {
    "whoami": {"description": "returns HTTP headers"},
    "add": {"description": "adds two numbers", "input": AddInput},
}


def whoami(args, credentials):
    return json.dumps(credentials)


def add(args, credentials):
    return str(args["a"] + args["b"])

The input of a tool is a pydantic model. Gatana validates the arguments against it before it calls your function. If you prefer, input can also be a plain JSON Schema dict. Functions may be async def. If a function returns something other than a string, the result is serialized to JSON.

The second argument, credentials, is the object the JavaScript runtime passes. It holds the request headers, which include the OAuth bearer token when the server uses OAuth, the apikeys the caller has stored, and gatanaUserEmail, the Gatana user the call is made for.

Dependencies

If your package has a requirements.txt, it is installed with uv each time the server starts, before main.py is imported. A handful of packages installs in a few seconds. pydantic is already there, so the example above needs no requirements file.

If the server should not download packages at start, put them in the zip. pip install --target vendor -r requirements.txt places them in a vendor folder, and that folder is on the import path.

Environment variables, storage, Tailscale

These work as they do for Node.js. Environment variables from Server Settings are in os.environ. With persistent storage enabled, write to the directory in os.environ["GATANA_DATA_DIR"]. With Tailscale enabled, plain sockets reach your tailnet, so a normal database driver works without proxy settings. Deployment and output logs are in the Deployment tab, as before.

Both runtimes live in one container image, and the runtime setting decides whether Node.js or Python starts. Each server still runs in its own virtual machine on Kata Containers, with the public internet as its only network unless you attach a tailnet.

The fine print

  • Installing requirements.txt has to finish within about a minute, or the deploy is restarted. Keep the list short, or use a vendor folder.
  • Your packages are installed into the same environment as the runtime, which brings mcp, pydantic, starlette and uvicorn. A pin that conflicts with one of these can keep the server from starting. The deployment logs show the error.
  • The gatana CLI commands hosted init, hosted verify and hosted run only handle JavaScript packages for now. hosted upload works for both runtimes.
  • Existing Node.js servers are not affected. The image moved from Alpine to Debian so that Python wheels run, and Node.js servers run as before.

Getting started

Create a server, choose Runnable Code, and set Runtime to Python 3.13. The sample main.py deploys as it is; replace its two functions with yours. The Runnable Code documentation describes the entry file, the credentials object, and Python packages in more detail.

All posts