Skip to content

v1.9.1 - Server logs without debug mode, per-server startup timeouts

Choose a tag to compare

@pacnpal pacnpal released this 05 Nov 17:06
· 18 commits to main since this release

⚠️ BREAKING CHANGES

Read this before upgrading. Each item says what changed, who it affects, and what to
do instead. If nothing here describes your setup, the upgrade needs no action.

In 1.9.1

1. last_error is set while a server is still retrying (#222).

  • What changed: last_error used to stay null until a server ran out of start
    attempts. Now it carries the latest failed attempt's reason during the retry wait
    (attempt 1/5 failed: setup exited with code 100), and it clears once the server is
    running.
  • Affects you if: a script, dashboard or alert treats a non-null last_error as "this
    server has failed."
  • Replace with: check state == "failed" for a server that has given up. Use
    last_error for the reason.

In 1.9.0

2. System pip is gone from the image (#213).

  • What changed: the container no longer has pip. Its bundled copies of msgpack,
    urllib3 and setuptools carried five HIGH CVEs that no pip release has fixed yet.

  • Affects you if: you run pip inside the container, or a setup script or
    post-clone script calls pip.

  • Replace with uv, which is already installed:

    • Run a Python tool: uvx <tool>
    • Install into a virtualenv: uv venv && uv pip install <pkg>
    • Install into the system Python the way pip did: uv pip install --system <pkg>

    A word-for-word swap of pip for uv pip fails with an error saying no virtual
    environment was found. Add --system or create a venv first.

3. An open GET stream to a group no longer keeps its members awake (#217).

  • What changed: groups now keep a session per client, and a GET to a group is that
    session's long-lived stream. It no longer counts as activity on the members.
  • Affects you if: you held a GET open to a group to stop its members from idling out.
  • Replace with: set the member's idle timeout to 0 (never idle) on its settings, or
    raise the global default in Settings → Idle shutdown (idle_timeout_s).

4. Upstream OAuth checks the iss parameter (#215).

  • What changed: MCP SDK v2 enforces RFC 9207. If a provider says it sends iss on the
    authorization callback and then doesn't, sign-in is refused.
  • Affects you if: a remote server's OAuth sign-in worked on 1.8.x and fails after
    upgrading.
  • Replace with: there's no setting to turn this off, because the provider is breaking
    its own published metadata. Report it to the provider. If the API also accepts a static
    token, put it in an Authorization header and mark it secret in the meantime.

Carried over from 1.8.x, if you're upgrading from 1.7.x

5. Glama in the catalog needs an API key (1.8.0, #172).

  • What changed: Glama's API now requires a key.
  • Affects you if: you browse or install from Glama in the catalog.
  • Replace with: set MCPE_GLAMA_API_KEY. Without it, Glama disappears from the source
    list.

6. Catalog errors return 503 instead of 502 (1.8.0, #172).

  • What changed: Cloudflare replaces any origin 502 with its own page, which hid the
    real error.
  • Affects you if: you monitor or script against /api/catalog and check for 502.
  • Replace with: check for 503.

7. Server detail no longer returns a stored OpenAPI document by default (1.8.1, #191).

  • What changed: GET /api/servers/{id} leaves out openapi_document, because
    multi-megabyte specs were riding along on every poll.
  • Affects you if: you read a pasted OpenAPI document over the API.
  • Replace with: add ?include_document=true. Over the control MCP, pass
    include_document to get_server.

v1.9.1 - Server logs without debug mode, per-server startup timeouts

When a server failed to start, the container log said nothing unless debug mode was on.
Now starts, failures and the reasons for them show up at INFO. A setup script killed
partway through an apt-get used to leave dpkg broken for every attempt after it. That
gets repaired before the next run. And a server whose setup takes longer than two minutes
can now have its own startup timeout without raising it for everything.

If you're upgrading from 1.8.x, the 1.9.0 notes are further down. From 1.7.x, read the
1.8.x notes too.


Server logs without debug mode (#222)

With debug mode off, nothing was attached to the control plane's loggers. INFO lines went
nowhere, and warnings reached stderr through Python's fallback handler with no timestamp,
level or source. Now one handler writes mcpelevator's own log lines to the container log,
in the format time LEVEL source: message, at MCPE_LOG_LEVEL (default INFO).

  • What gets logged: each server's start, failed attempts, its final failure (with the
    hint and the last lines of the server's own output), a running server crashing, idle
    stops and stops. A server refused a start is logged once per distinct error, so a retry
    loop doesn't flood the log.
  • Credentials are scrubbed. A failure report quotes the server's own output, which can
    hold a credential, so every line goes through the same scrubber debug mode uses.
  • Libraries keep their own levels. Only mcpelevator's app loggers are affected.
  • Debug mode still takes over when it's on, and removes this handler so nothing prints
    twice.
  • Three old messages changed format. Reconcile errors, post-reconcile hook errors and
    stop failures used to print as [mcpelevator] reconcile error: … and so on. They now go
    through the logger like everything else, as ERROR app.supervisor.supervisor: reconcile failed: …. The startup banners and a server's own clone and build lines keep their
    [mcpelevator] prefix.

MCPE_LOG_LEVEL=WARNING keeps only failures. An unknown level falls back to INFO.

last_error during retries

While start attempts are failing, a server's last_error now carries the latest one, like
attempt 1/5 failed: setup exited with code 100, instead of staying null until every
attempt is spent. It clears once the server is running. See breaking change 1 if you read
this field from a script.


Repairing an interrupted dpkg (#222)

Stopping a server mid-setup kills the script. That covers a disable, a restart, or the
setup's own timeout. If it was in the middle of a package install, dpkg is left
interrupted, and from then on every apt-get refuses with "dpkg was interrupted, you must
manually run 'dpkg --configure -a'." Every later attempt failed the same way until someone
fixed it by hand.

Now, before each setup and checkout script, mcpelevator checks whether dpkg reports an
interrupted run. If it does and the script runs as root, it runs dpkg --configure -a
first and says so in the server's log. That's the repair apt itself asks for, and it
doesn't remove anything.


Per-server startup timeouts (#222)

A server can now set its own Startup timeout and, for a repository server, its own
Checkout & build timeout. Both are in the server form, in seconds, and the second one
only shows when the server has a repository. Left blank, they use the instance defaults,
MCPE_START_TIMEOUT_S and MCPE_REPO_BUILD_TIMEOUT_S. Each one takes 1 second up to
86,400 (one day).

Raise the startup timeout for a setup that installs system packages or downloads a browser,
like playwright install --with-deps. On a cold container, that can run past the
two-minute default. A change applies from the next start and never restarts a running
server. Cloning a server copies its timeouts.

Over the API and the control MCP, these are start_timeout_s and repo_build_timeout_s.
Sending null puts the server back on the instance default.


Dependencies

  • Frontend: layerchart 2.9.2 (#218), svelte-check 4.7.8 (#221).
  • Image: the ghcr.io/astral-sh/uv image 0.13.7 (#220).
  • Docs: the README shows the Docker image build status.

Upgrade notes for 1.9.1

These are in addition to the breaking changes at the top.

  • No migration step. The new start_timeout_s and repo_build_timeout_s columns
    arrive through the existing forward-only ADD COLUMN path and default to null, so every
    server keeps using the instance defaults.
  • The container log is busier. Server starts, stops and idle stops now show up at
    INFO. Set MCPE_LOG_LEVEL=WARNING to keep only failures.
  • Log alerts on the old supervisor text need updating. If you match
    [mcpelevator] reconcile error, post-reconcile hook error or stop failed, match the
    new reconcile failed, post-reconcile hook failed and stop failed lines from
    app.supervisor.supervisor instead.
  • New environment variable: MCPE_LOG_LEVEL (default INFO; DEBUG, INFO,
    WARNING or ERROR).

Full Changelog: v1.9.0...v1.9.1


v1.9.0 - FastMCP 4, server requests for every client, and secret env values

mcpelevator now runs on FastMCP 4.1.4 and MCP SDK v2. Clients on the newer sessionless
protocol (2026-07-28) couldn't be sent a server's roots, sampling or elicitation
requests at all. Now they can, on every path: stdio, remote, groups, tools, resources and
prompts. Groups also keep a real session per client now, so older handshake clients get
those requests through a group too.

Any env var or header can be marked secret. It's encrypted at rest and never shown again.
YAML OpenAPI documents that people write by hand now load, and the form previews the base
URL a document points at before you save.

If you're upgrading from 1.7.x, the 1.8.1 and 1.8.0 notes are further down.


FastMCP 4 and MCP SDK v2 (#215)

FastMCP 4 sits on MCP SDK v2 and httpx2. Most of this was a port of imports and the few
places the bridge and OAuth code reached into client internals. Behavior stays the same
except where the new protocol leaves no choice.

  • Caller headers still stay put. FastMCP 4 moved forward_incoming_headers from the
    transport to ProxyClient and turned it on by default. The old override would have
    quietly stopped applying and sent callers' bearer tokens on to remote upstreams and
    member bridges. It's now passed to ProxyClient in both the bridge and the group hub.
  • Upstream OAuth runs on httpx2. The SDK's provider is an httpx2.Auth now. The
    callback's RFC 9207 iss is passed through, and the SDK rejects a missing iss from a
    provider that says it sends one (breaking change 4 above).
  • The openapi runner's API client is httpx2.AsyncClient.
  • Tool schemas are validated. SDK v2 checks that tool input schemas are objects.

Server requests reach every client (#216, #217, #143)

An MCP server can ask its client for things in the middle of a call. It can ask for
roots/list (which directories the client is working in), sampling/createMessage
(generate text with the client's model) and elicitation/create (ask the user). A client
on the 2026-07-28 protocol has no back-channel, so before this release those requests
had nowhere to go. Roots came back empty and the rest were refused. Since the FastMCP 4
upgrade, these clients would also have lost what remote upstreams and group members sent.

Now the bridge hands the server's request back to the client as an
InputRequiredResult. The client answers and sends its request again, and the bridge
passes the answer to the server, which has been waiting the whole time.

  • It covers tool calls, resource reads and prompts, on remote servers, stdio servers
    with Client requests on, and groups.
  • Answers are type-checked against what was asked before they reach the server.
  • It has limits. A client that's asked something and doesn't come back within 10
    minutes is treated like one that cancelled. At most 256 requests per server or group
    wait like this at once, and one call can have at most 16 open. Past either limit, the
    server's request is refused and the call carries on without it.
  • Logs and progress follow the live request. If a log line can't be delivered to the
    caller, it lands in the bridge log instead of being dropped.

Groups keep a session per client

Before this, groups served stateless HTTP. A handshake-era client going through a group
could never be asked anything, because its reply had nowhere to land. Groups now serve
stateful HTTP. A member starting, stopping or idling no longer rebuilds the group. Its
membership changes in place, so sessions stay open and see the change right away.

  • List changes are announced. When a group's members change, clients get
    list_changed for tools, resources and prompts. Handshake-era clients get it on their
    session's stream, and 2026-07-28 clients get it on subscriptions/listen if they
    opened one.
  • Open streams don't keep members awake. A GET stream or a listen request stays open
    as long as the client is connected, so neither one holds a member awake (breaking change
    3 above).
  • A waiting request survives a membership change, unless the member it reached is
    gone. While it waits, the members it reached stay awake.

Secret env values and headers (#205)

Click the lock next to any env var (or header, for remote and openapi servers) to make
it secret. The value is encrypted in the database with a key in <data_dir>/env.key
(0600) and is never shown again. The form, the API and the control MCP all read
<hidden>. Leave the field blank to keep the stored value, or type a new one to replace
it.

Someone with edit rights could otherwise point the server somewhere that prints the
secret back. So some changes need the secret values typed in again:

  • Changing what the server runs: runner, command, args, image options, working directory,
    setup script, repository or OpenAPI document.
  • Unlocking a secret.
  • On a local server, adding, changing or removing any other env value. Things like
    NODE_OPTIONS, LD_PRELOAD and PYTHONSTARTUP change what runs just as much as the
    command does, and no list of them would be complete.
  • On a remote or openapi server, changing the Host header. Other headers only travel to
    the upstream, so they can change freely.

Resending a secret's unchanged value keeps the stored copy, so the server doesn't restart
for nothing.


OpenAPI fixes (#205)

  • Hand-written YAML loads. In YAML, an unquoted 200: becomes a number, version: 1.0
    and openapi: 3.0 become floats, and 2024-01-01 becomes a date. JSON can't produce any
    of those, so only YAML specs broke. Keys, dates and the two version fields are now
    turned back into strings.
  • YAML can't blow up the control plane. The size check now measures the document with
    aliases and << merges fully expanded, before PyYAML builds anything. It's held to the
    same 32 MiB limit as JSON, and recursive aliases are refused. Server variables are
    filled in one pass, and the resulting URL is capped at 8 KiB. An info.title that isn't
    a string is refused.
  • Base URL preview. The form shows the base URL a document's servers entry names,
    under the base URL field, before you save. Coolify's spec, for example, shows
    app.coolify.io, which tells you to set your own instance's URL.
    POST /api/openapi/preview does the same over the API and never sends your headers.
  • The form says how to authenticate. It notes that the document can be JSON or YAML,
    and that the API's credential goes under Headers (like
    Authorization: Bearer <token>).

.mcpb exports (#205, #207)

  • Every manifest is validated against the official MCPB 0.2 schema before it's served.
    One that fails is refused instead of being handed to a client that would reject it.
  • Secrets aren't embedded. Each secret value becomes a sensitive prompt that the
    client asks for at install.
  • The manifest lists the server's tools from its last run, under their upstream names.
    An absolute command path pins the bundle's platform.
  • No stale tool lists (#206). Right after an edit, the cached tool list still described
    the old command until the server restarted. The list is now stamped with the config it
    was probed under, and an export only uses one that matches.

Security

  • SvelteKit 3 (#171, #204). Picks up the fixes for
    GHSA-27mw-82g8-phjj, an XSS through
    an untrusted cache-control header in prerendered pages, and
    GHSA-pfhg-q3qv-9q6r, an open
    redirect through enhanced form actions. Both are rated low. #171 bumped the version
    without updating the lockfile, which broke npm ci on main. #204 finished the
    migration: config moved into the Vite plugin, $lib became #lib, adapter-static went
    to 4, and runed is overridden to 0.38.
  • Trivy image findings cleared (#213). The base image's system pip is gone. pip ships
    its own copies of its dependencies, and its bundled msgpack, urllib3 and setuptools
    accounted for five HIGH alerts. Even the newest pip still bundles those versions, so
    upgrading it couldn't clear them. Nothing in the image used it, since the app runs from
    a uv venv and servers install with uv or uvx. Breaking change 2 above covers what to use
    instead. npm moved to 11.21.1, and its bundled undici is swapped to 6.29.1 for the three
    CVEs it fixes.

Dependencies

  • Backend: fastmcp 3.4.10 → 4.1.4 (#215), plus lock file maintenance (#212).
  • Frontend: svelte 5.57.3 (#195), jsdom 30 (#199), cookie 2 (#198), and vitest
    5 and @types/node 26.6.6 (folded into #204).
  • CI and image: uv 0.13.6 in CI (#194), astral-sh/setup-uv v10.3.2 (#196) and
    v10.3.3 (#201), dependencies pinned by digest (#200), docker/build-push-action v7.4.0
    (#208), docker/setup-buildx-action 4.4.1 (#209), actions/upload-artifact v7.0.2 and
    actions/download-artifact v8.0.2 (#210). Renovate's npm is held at 11 (#214), because
    npm 12 refuses Tailwind's @tailwindcss/oxide-wasm32-wasi tarball and broke lockfile
    updates.

Upgrade notes for 1.9.0

These are in addition to the breaking changes at the top.

  • No migration step. The new tools_config_hash column arrives through the existing
    forward-only ADD COLUMN path and defaults to blank.
  • Back up <data_dir>/env.key with the database once you mark anything secret. Without
    it, the secret values can't be decrypted, and the server's start error names the ones to
    re-enter.
  • 2026-07-28 clients get server requests back as InputRequiredResult. FastMCP 4 and
    MCP SDK v2 clients default to this protocol and handle it. Handshake-era clients work
    the same as before.
  • Idle timeouts on /s. On a single server's /s endpoint, the control plane can't see
    a request that's waiting for the client to answer, so an idle timeout shorter than 10
    minutes could stop the server mid-wait. If a server uses roots, sampling or elicitation,
    give it an idle timeout of 600 seconds or more, or 0. Groups hold their members awake
    for this on their own.

Full Changelog: v1.8.1...v1.9.0


v1.8.1 - Paste or upload an OpenAPI document

An openapi server no longer needs its spec published at a URL. You can now paste the
document or upload the file, and mcpelevator stores it with the server. This release also
fixes a base URL with no host slipping through validation.

If you're upgrading from 1.7.x, the 1.8.0 notes are further down.


Paste or upload the document (#191)

The openapi runner's form now has a choice between two sources. URL is fetched on
every start, the same as 1.8.0. Paste or upload is stored with the server. Pasted text
or a picked .json, .yaml or .yml file goes into a new openapi_document field, and
command stays blank. Over the API, that's "command": "" plus the JSON or YAML text in
"openapi_document".

  • It's checked on save. The document goes through the bridge's own parser, so a bad
    one gets a 400 when you save instead of a failed start later.
  • It needs a base URL when its servers entry is relative or missing. A stored
    document has no URL of its own to resolve against.
  • It's served as-is. Edit it to pick up a changed API. A digest of the document is part
    of config_hash, so saving a change restarts the bridge.
  • It reaches the bridge on stdin. A real spec runs past the 128 KiB env var that
    carries the launch spec.

Kept out of polled responses

The server detail page polls GET /api/servers/{id} every few seconds, and a
multi-megabyte spec was riding along on every poll. The document now comes back only with
?include_document=true, which only the edit form asks for. A blank command already tells
the detail view one is stored. The control MCP's get_server takes include_document and
passes it through, so an agent can still read a pasted spec before editing it.


Fixes

  • A base URL with no host is rejected (#193, fixes #192). resolve_base_url only
    checked the scheme, so a servers entry like https://, or https://{host} whose
    variable defaults to "", passed. A pasted document saved fine, and then every call
    failed in httpx with nowhere to connect. The check now requires a hostname. A pasted
    document gets a 400 on save, and a fetched one fails its start with one openapi: …
    line. A fetched document mostly couldn't hit this, because urljoin gives a same-scheme
    hostless entry the document's own host. A different scheme, like http://:8080/x from an
    https document, still could.

Upgrade notes for 1.8.1

  • No migration step. The new openapi_document column arrives through the existing
    forward-only ADD COLUMN path and defaults to blank, so existing openapi servers keep
    fetching their URL.
  • GET /api/servers/{id} omits openapi_document unless you pass
    ?include_document=true. Anything that reads a stored document over the API needs the
    parameter.

Full Changelog: v1.8.0...v1.8.1


v1.8.0 - Run servers from source and from OpenAPI documents

This release adds two new ways to get a server running. Some MCP servers are never
published to npm or PyPI, and their README just says "clone, build, run." mcpelevator can
now manage that checkout and build for you. The new openapi runner turns any REST API
with an OpenAPI 3.0 or 3.1 document into an MCP server, one tool per operation.

It also fixes the catalog page failing with Cloudflare 502s. The real cause was the
shared HTTP pool running out of connections, and the /s proxy was leaking slots from it.

If you're upgrading from 1.6.x, read the 1.7.2 notes first. They cover the built-in
mcpelevator server and the Cloudflare Access changes.


Install a server from its source repository (#186)

A local-runner server (npx, uvx, command) can now launch from a git checkout that
mcpelevator manages. Three new fields control it:

  • repo_url is the repository. https://, ssh://, git:// and git@host:path all
    work, and so do GitHub owner/repo shorthand and …/tree/<ref> page URLs. It's cloned
    into <data_dir>/repos/<server id>.
  • repo_ref is a branch, tag or commit. Blank means the default branch. It's fetched
    on every activation. If the fetch fails and an earlier checkout exists, that one starts.
  • post_clone_script holds the install and build steps (npm ci, npm run build,
    uv sync). It runs with /bin/sh -e -c inside the checkout, and only when the
    checked-out commit or the script itself changed since its last good run. A restart
    doesn't rebuild.

The command then runs with the checkout as its working directory. The checkout and build
show up as a new checkout startup phase, with their output in the server's logs. They
run under MCPE_REPO_BUILD_TIMEOUT_S (900 seconds by default) instead of the start
timeout, since a first clone and build can take minutes. Deleting the server deletes its
checkout.

Add → From GitHub

The new From GitHub tab on the Add page takes a repository URL and reads the root
manifests to draft the install, build and start steps. The tab only shows for users with
the local-runner permission, since the result always runs on a local runner.

  • package.json. The install command follows a declared packageManager first, then
    the lockfile. pnpm and Yarn run through Corepack via npx, because Node 25 and later no
    longer ship it and corepack enable failed in the Node 26 image. Yarn installs a
    node_modules tree so a plain node <bin> launch can find its dependencies. A build
    script gets its own step unless prepare already runs it. The start command comes from
    bin, then the start script, then main. A non-JS bin is made executable after the
    build, because a root install never marks the package's own bin.
  • pyproject.toml. uv sync (with --frozen when there's a uv.lock), then
    uv run --no-sync <script>. A legacy Poetry project with no [project] table installs
    with uv pip install . into the checkout's .venv. Both that case and a project with no
    [build-system] get a warning and wait for review, since uv sync won't install their
    entry point.

Whatever it can't work out is left blank with the reason. Every step stays editable in
the review form and later under Source repository on the server's edit page.
GET /api/catalog/repository?url=… returns the draft without creating anything.

The build and the server both run without mcpelevator's own VIRTUAL_ENV and
UV_PROJECT_ENVIRONMENT, so uv installs into the checkout's .venv and never into the
control plane's environment. An explicit server env var still wins.

Private repositories and trust

Put a token in the URL (https://<token>@github.com/…, stored like env and stripped
from the clone log line) or use an SSH key. The image now ships openssh-client, which
--no-install-recommends had been dropping from git's dependencies, so SSH URLs could
never clone in Docker. Mount the key and a known_hosts entry into the container user's
~/.ssh, because git won't prompt to accept an unknown host key.

The post-clone script runs code from that repository as the mcpelevator process user,
with the server's environment. It's gated by the same local-runner permission as setup
scripts. Every activation fetches the ref, so pin a tag or commit when you don't trust the
upstream to stay the way you reviewed it. Only network transports are accepted, never
file:// or ext::.

From an agent

create_server and update_server take the new fields. Two new tools,
get_repository_draft and install_from_repository, do over MCP what From GitHub does in
the UI. install_from_repository returns whatever detection left blank in its notes.


The openapi runner (#187, #190)

Pick openapi as the runner on Add → Manual. Fill in the OpenAPI document URL
(OpenAPI 3.0 or 3.1, JSON or YAML) and, if you need it, an API base URL. Left blank,
the base URL comes from the document's servers entry. Anything the API needs, like an
API key, goes under Headers. mcpelevator fetches the document each time the server
starts and serves every operation as an MCP tool through FastMCP's OpenAPIProvider. It
goes through the same tool policy as every other server, so hide and rename, the REST
mirror, groups, idle shutdown, usage and /s auth all work unchanged.

There's no new schema for it. Over the API, the row reuses the launch fields:

  • command is the document URL.
  • args is [], or [base_url] to override where API calls go.
  • env holds the API's HTTP headers.

A restart re-reads the document. It isn't a local runner, so members without the
local-runner permission can create one, the same as remote.

Credentials stay with the API

  • The caller's request headers are never forwarded to the API. FastMCP's OpenAPITool
    copies them by default, which would have sent along things like a
    Cf-Access-Jwt-Assertion.
  • Configured headers always win over a header parameter with the same name, and that
    parameter is dropped from the tool's arguments.
  • A paths key written as an absolute URL made FastMCP call that URL with your
    credentials attached, even over a base-URL override. A bad or compromised document
    could use that to collect your API key. Paths that don't start with / are now
    refused, and any request whose origin doesn't match the API's base URL is refused
    before it's sent.
  • The document is fetched without your headers. On a 401 or 403 it retries with them, but
    only when the document comes from the API's own origin, and that's only known when you
    set a base-URL override. Otherwise the start fails with a message saying how to allow
    it. Redirects are never followed with credentials.
  • A relative servers URL resolves against the URL that actually served the document
    after redirects, instead of the entry URL.

A bad document fails the start with one openapi: … log line. That covers an unreachable
URL, Swagger 2.0, a parse failure, or no base URL.

From an agent (#190)

The control MCP didn't know about the new runner at first. create_server and
update_server now explain how its launch fields map, and the connect instructions gain a
"serve a REST API from its OpenAPI document" flow that ends in wait_for_server and
points at the openapi: log line. A test now checks that create_server names every
runner, so the next one can't skip the control MCP.


Catalog fixes and a local mirror (#172)

The catalog page failed with Cloudflare's "502 Bad gateway" after about 15 seconds, for
both the MCP Registry and Glama at once. The network was fine. The /s proxy and the
catalog share one httpx client capped at 100 connections, and every open MCP client stream
holds one. Normal use filled the pool. Proxied requests then hung waiting for a slot, and
catalog fetches died on PoolTimeout with an empty message. That empty message is where
"upstream request failed" came from.

The proxy also leaked slots. Its cleanup lived in the body generator's finally, and when
the client was already gone at response start, Starlette raised before it ever iterated
the body. The connection stayed checked out for good, and the stuck in-flight count kept
idle shutdown from ever stopping that server.

  • No connection cap on the shared client, and the proxy's response runs cleanup once
    however it ends.
  • 503 instead of 502. Cloudflare replaces any origin 502 or 504 with its own page, so
    the real error never reached the UI.
  • Errors you can read. A failed fetch names the exception type when its message is
    empty (PoolTimeout, ConnectTimeout, ReadTimeout) and logs a warning with the URL.
  • Stale beats broken. When a live fetch fails, the source serves the last good answer
    it cached.

Local mirror

The registry's docs say it has no uptime guarantee and ask clients to sync about hourly
and keep their own copy. A background task now does that, writing to
<data_dir>/catalog-mirror.db:

  • The registry syncs hourly. After the first full pass it only asks for what changed, so
    status changes like deleted still land.
  • Glama has no changed-since filter, so it does a full pass daily and drops whatever it no
    longer lists.
  • Once a source finishes its first full sync, list, search and detail read the local copy.
    Anything else goes upstream.

Against the live registry, a full sync took 37 seconds for about 49,500 servers and an
incremental one took 0.1 seconds. List and search answer in 4 to 40 ms. The file was
73 MB. It's only a cache, so deleting it just means the next sync rebuilds it.
MCPE_CATALOG_MIRROR=false turns it off.

Glama now needs an API key

Glama's API requires a key now. Set MCPE_GLAMA_API_KEY. Without one, Glama drops out of
the source list, answers 400 like an unknown source, and is never synced. Its data license
also requires a visible credit and a link to each listing's Glama page, so the browse view
shows both when Glama is on.


Security

  • FastMCP 3.4.4 → 3.4.10 (#170). Picks up the fix for
    GHSA-7xgj-8392-5q4r, where a direct
    call could reach a transformed component without its authorization check running. The
    range also brings the 3.4.8 and 3.4.9 security backports, among them OpenAPI path
    parameters refusing . and .. segments, which matters now that the openapi runner
    exists.
  • joserfc 1.7.1 → 1.7.6 (#188). It comes in through authlib. No advisory is attached,
    but the range hardens JWT and JWS handling. Signed JSON with an empty signature list is
    rejected, iss, sub and aud get default claim validation, non-string jti claims
    are refused, and token and payload sizes are checked before they're split or decoded.

Dependencies

  • Backend: authlib 1.7.2 → 1.9.1 and dnspython 2.8.0 → 2.9.0 (#183).
  • Frontend: svelte 5.57.2 (#165), vite 8.3.5 (#166), layerchart 2.9.1 (#181).
  • CI and image: uv 0.13.4 (#164) and 0.13.5 (#177), the ghcr.io/astral-sh/uv image
    0.13.6 (#179), the docker image pinned by digest (#173), github/codeql-action digest
    (#174), actions/setup-node v7 (#184), actions/setup-python v7 (#185),
    docker/login-action 4.6.0 (#175) and 4.7.0 (#182), docker/setup-qemu-action 4.4.0
    (#178), docker/setup-buildx-action 3.12.0 (#176), docker/build-push-action 6.19.2
    (#167) and hadolint/hadolint-action 3.5.0 (#180).

Upgrade notes for 1.8.0

  • No migration step. The new repo_url, repo_ref and post_clone_script columns
    arrive through the existing forward-only ADD COLUMN path and default to blank, so
    existing servers are unchanged.
  • Glama needs a key. If you browse Glama in the catalog, set MCPE_GLAMA_API_KEY or it
    disappears from the source list.
  • Catalog errors are 503 now. Anything that watched those routes for a 502 should look
    for 503.
  • The catalog mirror uses disk. Expect a file in the tens of megabytes at
    <data_dir>/catalog-mirror.db. Set MCPE_CATALOG_MIRROR=false if you'd rather not keep
    one.
  • SSH clones in Docker need a key and a known_hosts entry mounted into the container
    user's ~/.ssh.
  • New environment variables: MCPE_REPO_BUILD_TIMEOUT_S (default 900, must be a
    finite number above zero), MCPE_CATALOG_MIRROR (default true) and
    MCPE_GLAMA_API_KEY.

Full Changelog: v1.7.2...v1.8.0

License

MIT © pacnpal