/mcp in the web process, and AI clients such as Claude, Claude Code and Cursor call its tools over HTTPS. This guide builds a server with two read-only tools that query a Sevalla PostgreSQL database over an internal connection, requires a bearer token on every request, and runs in stateless HTTP mode, so it works on as many instances as you need.
The SDK’s MCPServer class is what version 1 of the SDK called FastMCP. To let an AI agent manage your Sevalla services instead, connect it to the Sevalla MCP server.
At a glance
Environment variables
DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD, which the server doesn’t read.
Before you begin
- A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See Git providers.
- Python 3.14 and uv 0.13 or later on your computer.
Create the project
Create the project with uv and add the SDK, the PostgreSQL driver with its connection pool, and Uvicorn:uv init writes pyproject.toml, a .python-version with 3.14, a .gitignore that keeps out .venv, a main.py and a Git repository. uv add installs the packages into .venv and pins every version in uv.lock:
main.py with the server:
- Streamable HTTP on
/mcp.mcp.streamable_http_app()returns a Starlette app that answers MCP at/mcp, and Uvicorn serves it on the port in thePORTenvironment variable, on all interfaces. The app answers clients on the2026-07-28protocol revision and on the older revisions that open with aninitializerequest. - Stateless HTTP. For clients on the older revisions, the SDK keeps a session in the memory of one process by default.
stateless_http=Trueanswers each request on its own instead, so any instance can serve any request. See Run more than one instance. - No localhost-only check. By default the app accepts only requests addressed to
localhostand answers every otherHostwith421 Misdirected Request. Turning that check off lets it answer on its Sevalla domain and on custom domains; the token protects it instead. - A bearer token.
RequireTokencompares theAuthorizationheader withBearerand theMCP_TOKENenvironment variable withhmac.compare_digest, in constant time, and answers401to anything else. The server refuses to start withoutMCP_TOKEN, so it never runs open by mistake./healthstays public for the readiness probe. - Read-only database access.
DB_URLis the variable Sevalla adds when you connect the database. The lifespan opens the pool when the server starts and closes it when it stops. Every connection starts withdefault_transaction_read_only=on, and the queries pass the model’s arguments as parameters (%s), never as SQL text. The type hints andFieldconstraints become the tools’ input schemas, and theread_only_hintannotation tells clients the tools change nothing; clients use it to decide whether to ask you before a call. - Errors the model reads.
ToolErrorreturns its message to the model as a failed tool result,Error executing tool get_order_status: Order 1003 not found, so the model can try again with another number. Any other exception returns onlyError executing tool get_order_status. - Shutdown. Sevalla sends
SIGTERMto each instance it replaces during a deployment. Uvicorn stops accepting connections, finishes the requests in progress, and the lifespan closes the pool.
DB_URL at a local PostgreSQL database that has these tables, for example MCP_TOKEN=local-token DB_URL=postgres://localhost:5432/storefront uv run main.py, then open http://localhost:8000/health: the response is ok.
Prepare the project for Sevalla
The project already has what Ingot, Sevalla’s builder, needs:uv init already created the repository:
Deploy to Sevalla
These steps build with Ingot. To build from your own Dockerfile instead, see Deploy with a Dockerfile. Generate the token that clients send, and keep it in your password manager:Create the database
orders-db as the Display name, choose a Data center and a Size, then click Create database. See Create a database for every field.Create the tables
schema.sql and click Run all.Create the application
Connect the database
orders-db as the Service, turn on Add environment variables to the application, and click Add internal connection. Sevalla adds DB_URL and the other connection variables to the application. See Connect an application.Add the token
MCP_TOKEN as the key and the token you generated as the value. Under Availability, clear Available during buildtime, so the token reaches only the running server, then click Add environment variables.Deploy
Turn on auto-deploy
Verify the deployment
-
On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Python 3.14 from
.python-version, uv fromuv.lock, andpython main.pyas the start command.Build log -
Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running. Its runtime logs show
Uvicorn running on http://0.0.0.0:8080, thenTerminating session: Noneafter each MCP request: the SDK closing the one-request session of stateless mode. With the API, Get deployment (getDeploymentV3) returns the status, Get application deployment logs (getApplicationDeploymentLogsV3) searches the build log, and Get application runtime logs (getApplicationRuntimeLogsV3) searches the runtime logs. WithAPP_IDandDEP_IDfrom Deploy with the API: -
Copy the domain under the application’s name, for example
storefront-mcp-x7k2m.sevalla.app. The MCP endpoint is that domain with/mcp. In a terminal, put the token in a variable and call theget_order_statustool with a JSON-RPC request:The response is one Server-Sent Event with the tool’s result, read from the database:Check the health endpoint, and that the same server refuses a request without the token: -
List the tools with the MCP Inspector, the protocol’s own test client:
Without
jq, the output is the full list with each tool’s input schema. To call a tool, use--method tools/call --tool-name search_products --tool-arg query=mug.


Run more than one instance
Sevalla spreads the requests to a web process across its instances, request by request, with no session affinity: two requests from the same client can reach different instances. Withstateless_http=True, any instance answers any request, so the server works with any number of instances.
Without it, the SDK gives each client that opens with initialize a session that lives in the memory of the instance that created it, and every later request that reaches another instance fails with 404 and {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Session not found"}}. In a test with 2 instances, 29 of 50 requests failed this way and 7 of 10 MCP Inspector runs couldn’t complete a tool call; with stateless_http=True, the same server answered 200 of 200 tool calls.
Stateless mode has one cost: on those older protocol revisions, a tool can’t send a request back to the client in the middle of a call, such as asking the user a question with ctx.elicit(). Neither tool in this guide does. Keep anything a tool must remember in the database, not in the process.
A readiness probe makes each new instance wait until it answers before it gets requests, so a deployment switches over only to instances that serve MCP:
Open the web process
Set the instances
2 or more. More than 1 instance needs a valid payment method.Turn on the readiness probe
/health. Keep the Port at the web process’s port, 8080 unless you changed it.Save and deploy
{"scaling_strategy":{"type":"manual","config":{"instanceCount":2}}} to Update process (updateProcessV3), then deploy; the readiness probe is part of Deploy with the API. The Sevalla MCP server’s scale_process tool sets the instance count too.
Connect a client
Each client needs the endpoint URL, such ashttps://storefront-mcp-x7k2m.sevalla.app/mcp, and the token, sent as Authorization: Bearer <token>. Clients connect over the internet to the application’s domain, so keep HTTP traffic on.
Claude Code
Add the server, then check that Claude Code connects:claude mcp list shows storefront: https://storefront-mcp-x7k2m.sevalla.app/mcp (HTTP) - ✔ Connected. Ask Claude Code “Are there any mugs in stock?”: it calls search_products and answers that the ceramic mug is out of stock. claude mcp add stores the server for the current project only; add --scope user to use it in every project.
Claude
In Claude (claude.ai and the desktop app), a custom connector sends a fixed token only through Request headers, which Anthropic offers to a limited set of organizations. If your Add custom connector dialog has a Request headers section:Add the connector
Enter the server and the token
authorization header with the value Bearer followed by your token.Cursor
Add the server to.cursor/mcp.json in your project, or to ~/.cursor/mcp.json for every project. Cursor reads ${env:STOREFRONT_MCP_TOKEN} from the environment it runs in, so the token stays out of the file:
ChatGPT
ChatGPT adds MCP servers as plugins, with OAuth, No authentication, or OAuth or no authentication. It can’t send a fixed bearer token, so it can’t connect to this server as built. Don’t remove the token to connect it: anyone who finds the URL could then call the tools. See OpenAI’s custom MCP server documentation.Other clients
Any MCP client that supports Streamable HTTP and custom headers connects the same way. The MCP Inspector in Verify the deployment is the quickest check that the URL and token work. To change the token, updateMCP_TOKEN on the Environment variables tab, deploy, and update every client.
Deploy with a Dockerfile
Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. Add aDockerfile and a .dockerignore to the root of the repository:
MCP_TOKEN and DB_URL reach the container at runtime.
Dockerfile, so you can keep it in a repository that builds with Ingot, but it adds the .dockerignore patterns to its own: its build plan shows an Ignored row with 6 patterns from .dockerignore. Then switch the application to the Dockerfile build strategy:
Choose the Dockerfile build strategy
Dockerfile and the Docker context ., then click Update build strategy.Deploy


📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, and its Dockerfile tab shows your Dockerfile. When the deployment is Deployed, the curl request in Verify the deployment returns the order again. See Dockerfile.
Deploy changes
With Auto-deploy on, every push to the branch starts a deployment; otherwise, click Deploy now for each change. Each deployment builds the image, then replaces the web process’s instances. After you add a package withuv add, commit pyproject.toml and uv.lock together. Clients see a tool you add the next time they list the tools, for example when they reconnect. See Deployments.
Add a custom domain
To serve the MCP endpoint from your own domain, such asmcp.example.com, add the domain on the application’s Domains tab and create the DNS records that Sevalla shows. The server accepts any host, so no code change is needed; update the URL in each client. Custom domains need a web process larger than H1, so scale the web process first. See Custom domains.
Troubleshoot
Deploy with the API
These steps make the same deployment as Deploy to Sevalla through the Sevalla API, as commands an agent or a script runs in order. Each step names the API operation, the CLI command and the Sevalla MCP server tool that do the same. What the sequence needs:- An API key with the Company developer role, or custom capabilities that create, read and update applications and databases, in the
SEVALLA_API_KEYenvironment variable. A call the key isn’t allowed to make fails with403and names the missing permission. - The project pushed to GitHub, from a Git account connected to Sevalla. If Sevalla can’t read the repository, Create application fails with
400andCannot access repository <url>: Repository not found or you don't have access., followed by where a person grants access. List Git providers (listGitProvidersV3) returns theconnect_urlthat connects an account. curl,jq,opensslandpsql, run from the project folder, which holdsschema.sql.
Set the variables
MCP_TOKEN; List environment variables (listEnvironmentVariablesV3) returns it again later, while the Sevalla MCP server’s tools never show variable values.Pick the location
listClustersV3) returns each location with its region in location. Pick one by its region from Locations, for example eu-frankfurt-1 for Frankfurt, Germany:sevalla resources clusters. MCP: get_platform_options.Find the database size
listDatabaseResourceTypesV3) returns the database sizes; db1 is the smallest.sevalla resources db-types. MCP: get_platform_options.Find the process size
listProcessResourceTypesV3) returns the process sizes; h1 is H1.sevalla resources process-types. MCP: get_platform_options.Create the database
createDatabaseV3) starts creating orders-db:sevalla databases create --name orders-db --type postgresql --db-version 18 --cluster $CLUSTER_ID --resource-type $DB_SIZE_ID --db-name storefront --db-user storefront --db-password $DB_PASSWORD. MCP: create_database, which generates the password itself and never returns it; once the application exists, its connect_to_application_id and env_var_name: "DB_URL" also make the connection and write DB_URL as postgresql://app_user:..., which psycopg accepts like postgres://.Open the database to your computer
getDatabaseV3) reports ready, Toggle external connection (toggleDatabaseExternalConnectionV3) gives the database a public hostname, so you can create the tables from your computer:External connection toggle started. CLI: sevalla databases connections external-toggle $DB_ID. MCP: sevalla_api_create with toggleDatabaseExternalConnectionV3.Create the tables
getDatabaseV3) is ready again with an external_hostname, then run schema.sql. The hostname can refuse connections for a minute after it appears, so psql retries; -1 runs the file in one transaction, so a failed attempt leaves nothing behind:psql prints CREATE TABLE twice, INSERT 0 3 and INSERT 0 2, after server closed the connection unexpectedly for each attempt that came too early. CLI: sevalla databases get $DB_ID. MCP: get_database.Close the database again
toggleDatabaseExternalConnectionV3):sevalla databases connections external-toggle $DB_ID. MCP: sevalla_api_create with toggleDatabaseExternalConnectionV3.Create the application
createApplicationV3) creates the application with a web process, without deploying it:auto_deploy is false. CLI: sevalla apps create --name storefront-mcp --cluster $CLUSTER_ID --source privateGit --git-type github --repository https://github.com/acme/storefront-mcp --branch main. MCP: create_application with deploy: false (it deploys by default).Find the web process
listProcessesV3) returns the web process:sevalla apps processes list --app-id $APP_ID. MCP: get_application.Set the size and the readiness probe
updateProcessV3) sets the web process to H1 and adds the readiness probe on /health. Send timeoutSeconds: without it, the API sets the probe’s timeout to 120 seconds, where the dashboard uses 1."resource_type_name":"h1" and the probe. CLI: sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID sets the size; the CLI has no option for probes. MCP: sevalla_api_update with updateProcessV3 and the same body.Connect the database
createApplicationInternalConnectionV3) lets the application reach orders-db:sevalla apps internal-connections create $APP_ID --target-id $DB_ID --target-type database. MCP: sevalla_api_create with createApplicationInternalConnectionV3.Add the environment variables
createEnvironmentVariableV3), once per variable, adds the six connection variables the dashboard adds, with the address from Get database, and MCP_TOKEN for runtime only:sevalla apps env-vars create $APP_ID --key DB_URL --value <url>, one variable per command; CLI 2.1.0 makes every variable available during build and runtime and has no option to change that, so add MCP_TOKEN with the API or MCP. MCP: set_environment_variables, with runtime and buildtime per variable.Turn on auto-deploy
updateApplicationV3) deploys every push to main from now on:true. CLI: sevalla apps update $APP_ID --data '{"auto_deploy":true}'. MCP: sevalla_api_update with updateApplicationV3.Deploy
triggerDeploymentV3) builds the latest commit on main with Ingot:sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.Wait for the deployment
getDeploymentV3) until status is success, failed, cancelled or skipped:success. On failed, read the build log with Get application deployment logs (getApplicationDeploymentLogsV3): curl -s "$API/applications/$APP_ID/deployments/$DEP_ID/logs?order=desc&limit=50" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'. CLI: sevalla apps deployments get $DEP_ID --app-id $APP_ID and sevalla apps deployments logs $DEP_ID --app-id $APP_ID. MCP: get_deployment and get_deployment_logs.Run the checks
listApplicationDomainsV3) returns the domain; then call the server:ok, then the order, as in Verify the deployment:sevalla apps domains list --app-id $APP_ID. MCP: get_application.{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."} to Update application (updateApplicationV3), then run the deploy and wait steps again. CLI: sevalla apps update $APP_ID --build-type dockerfile --dockerfile-path Dockerfile. MCP: sevalla_api_update with updateApplicationV3.