Self-hosting

The server is one Python service and one Redis-compatible Key Value store (Valkey). You can run it on your own machine or on a host such as Render.

Requirements#

Part Version
Python 3.13
uv A current release
Valkey or Redis One node, no cluster
An Apple Developer account Only for push notifications

Run it locally#

Terminal
git clone https://github.com/Aayush9029/mcp-server.git
cd mcp-server
uv sync
cp .env.example .env
valkey-server --save "" &
uv run mcp-client-server

The server listens on http://localhost:8000. Check it:

Terminal
curl http://localhost:8000/health
JSON
{ "status": "ok", "store": "ok" }

Make an account with POST /v1/accounts and a key with POST /v1/keys, or point the app at your server. Then see Quickstart, with your own URL.

Settings#

All configuration is environment variables. .env.example lists them. An empty value is the same as no value.

Product#

Variable Default Meaning
APP_NAME MCP Client The name in the pages, the MCP server info, and the server instructions
SUPPORT_EMAIL support@example.com The contact address in the pages
PUBLIC_BASE_URL http://localhost:8000 The public URL of the server. Its origin can call /mcp from a browser.
APP_STORE_URL https://apps.apple.com/app/id6821470281 The link to the app in the pages

Store#

Variable Default Meaning
REDIS_URL redis://localhost:6379/0 The URL of the store
REDIS_KEY_PREFIX mc The prefix of each key in the store
REDIS_MAX_CONNECTIONS 20 The size of the connection pool of each worker

Use a store that keeps its data on disk and does not evict keys (maxmemory-policy noeviction).

Push notifications#

Push is off until the key, APNS_KEY_ID, APNS_TEAM_ID, and APNS_BUNDLE_ID are all there. The server then logs one line at the start that says that push is off, and each route still works.

Variable Meaning
APNS_KEY_P8 The APNs authentication key (.p8) as text. \n is accepted for a new line.
APNS_KEY_PATH The path of the .p8 file. Use this or APNS_KEY_P8.
APNS_KEY_ID The key ID of that key
APNS_TEAM_ID The team ID of the Apple Developer account
APNS_BUNDLE_ID The bundle ID of the app
APNS_WATCH_BUNDLE_ID The bundle ID of the watch app. The default is APNS_BUNDLE_ID.

Each device says in its registration if its tokens are for the production or the sandbox environment of APNs.

App integrity#

See App integrity. Each check is off until its variable has a value.

Variable Default Meaning
APP_ATTEST_APP_IDS none The app IDs (TEAMID.bundle.identifier) that can make accounts, with commas between them
APP_ATTEST_ENVIRONMENT production production or development
APP_TOTP_SECRET none The base32 secret of the app code

Limits and access#

Variable Default Meaning
RATE_LIMIT_PER_MINUTE 600 Requests for each credential in one minute. 0 turns the limit off.
RATE_LIMIT_ACCOUNTS_PER_HOUR 20 New accounts, and challenges, for each IP address in one hour. 0 turns the limit off.
MCP_ALLOWED_ORIGINS none More browser origins that can call /mcp, with commas between them

Process#

Variable Default Meaning
HOST 0.0.0.0 The address that the server listens on
PORT 8000 The port
WEB_CONCURRENCY 1 The number of worker processes
LOG_LEVEL INFO DEBUG, INFO, WARNING, ERROR, or CRITICAL

Deploy on Render#

The repository has a render.yaml Blueprint. It makes one web service and one Key Value store.

  1. In Render, make a new Blueprint from your fork of the repository.
  2. Give the values that Render asks for: PUBLIC_BASE_URL, SUPPORT_EMAIL, and the APNS_ values.
  3. Wait for the health check on /health.

A push to the main branch deploys a new version.

Operation#

  • The server keeps no state in memory. Several workers, and several instances, can serve the same account.
  • The server starts also when the store is not there yet. Until the store answers, /health gives 503 and each API request gives 503 with the code unavailable. No restart is necessary after that.
  • The server deletes old data by itself. See Overview.
  • If the server runs behind a proxy that is not Render, the limit for each IP address uses the address of the proxy. Put a rate limit in your proxy in that case.

Delete an account for a person#

A person who lost the device cannot delete the account in the app. Run this inside the service environment (on Render, as a one-off job of the web service), with the account ID or the ID of one of its keys from the support request:

Terminal
uv run mcp-client-server admin delete-account --key-id key_01jb2k3v9d

It deletes the account with all its data, as DELETE /v1/account does, and prints only the account ID. The exit code is 1 if there is no such account, and 2 if the store does not answer. The data goes in small steps; if the command stops before the end, run it again, or wait for the maintenance loop of the server.

Develop#

Terminal
uv run pytest
TEST_STORE=fakeredis uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run pyright

uv run pytest starts its own Valkey if valkey-server is installed. TEST_STORE=fakeredis needs no store.

This page as Markdown