Skip to content

feat(api): rebuild the agent when the toolsets' tools change - #218

Merged
ciaransweet merged 1 commit into
mainfrom
feat/refresh-tools
Oct 28, 2026
Merged

ciaransweet merged 1 commit into
mainfrom
feat/refresh-tools

Conversation

@ciaransweet

@ciaransweet ciaransweet commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

create_app built the agent once at startup and kept its tool list for the life of the process. ViewCache did the same with the view bundles. A toolset redeployed after the API started reached the model with its old descriptions, and the UI with its old views, until something restarted the API.

This happened in production on dss (ecmwf/dss-agentic-ai-services#157). The knowledge-base toolset gained an announcements section, but the agent still had the old search_knowledge description, so for two days it never searched announcements. Locally the bug can't be seen, because a fresh process always lists the current tools.

What

mcp_agent_api.refresh (new, about 60 lines)

  • MCP_AGENT_API_REFRESH_SECONDS sets the interval: default 1800, 0 for never.
  • fingerprint(built) hashes what the model sees of the tools: their names, descriptions and arguments.
  • keep_fresh(app, build, every) runs the same build as startup on that interval, and swaps app.state.built only when the fingerprint changed. A failed build keeps the current agent; a toolset that's down makes list_tools raise, so it never silently drops that toolset's tools.

create_app

Starts keep_fresh beside the lifespan's yield and cancels it on shutdown.

ViewCache

Re-reads the bundles at most once per interval. A toolset that doesn't answer a re-read keeps the bundles it had.

Behaviour

A turn already running finishes on the agent it started with, because the routes read the agent once per request. Threads carry on across a swap, because the checkpointer is shared. Each refresh costs one connections request plus one tools/list per toolset.

A host with its own lifespan (dss) starts the same task itself. The example is in Mounting the routes into your own application in CONSUMING.md. Behaviour changes by default: existing create_app deployments start refreshing every 30 minutes, and 0 restores the old behaviour.

In dss

dss builds its own app around create_router, so it starts the refresh in its own lifespan. Once it takes this release, packages/dss-agent/src/dss_agent/api.py changes like this:

--- a/packages/dss-agent/src/dss_agent/api.py
+++ b/packages/dss-agent/src/dss_agent/api.py
@@ -51,6 +51,7 @@ questions that only need ``geocoding`` or ``ecmwf-paramdb`` are answered normall
 which a pre-flight rejection would break.
 """
 
+import asyncio
 import logging
 import os
 import secrets
@@ -67,6 +68,7 @@ from fastapi.responses import JSONResponse
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from mcp_agent.main import Checkpointing
 from mcp_agent_api.app import cors_origins
+from mcp_agent_api.refresh import keep_fresh, refresh_seconds
 from mcp_agent_api.routes import Built, create_router
 from mcp_agent_api.ui import config_from_environment, create_ui_router
 from starlette.datastructures import MutableHeaders
@@ -363,10 +365,17 @@ def create_api(
         checkpoint tables.
         """
         async with checkpointing:
-            app.state.built = await (build or _from_settings(config, checkpointing))()
+            builder = build or _from_settings(config, checkpointing)
+            app.state.built = await builder()
+            every = refresh_seconds()
+            refresh = (
+                asyncio.create_task(keep_fresh(app, builder, every)) if every else None
+            )
             try:
                 yield
             finally:
+                if refresh:
+                    refresh.cancel()
                 app.state.built = None
 
     app = FastAPI(title="dss-agent-api", lifespan=lifespan)

MCP_AGENT_API_REFRESH_SECONDS can then be set in the chart's API values; left unset, it refreshes every 30 minutes.

Tests

  • In create_app: an unchanged tool list and a failed build both keep the current agent, and a changed description swaps it.
  • 0 builds the agent once.
  • The interval parsing.
  • The view cache re-reads after the interval, and keeps a bundle the re-read missed.

611 passed. Lint clean.

🤖 Generated with Claude Code

The API built the agent once at startup and kept its tool list, and the
views cache read the bundles once, both for the life of the process. A
toolset redeployed after that reached the model with its old descriptions,
and the UI with its old views, until the API restarted.

Every MCP_AGENT_API_REFRESH_SECONDS (default 1800, 0 for never) the app
builds the agent again and swaps it in when the tools' names, descriptions
or arguments changed. A failed build keeps the current agent. The views
cache re-reads the bundles on the same interval, keeping any a toolset did
not answer for. keep_fresh is public, for a host with its own lifespan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ciaransweet
ciaransweet marked this pull request as ready for review October 28, 2026 10:06
@ciaransweet
ciaransweet requested a review from j08lue October 28, 2026 10:06

@j08lue j08lue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand how this works (create_app), but it looks clean.

Comment thread docs/CONSUMING.md
how you say "not yet", and that surfaces as `503`. An agent rebuilt behind it,
on a model change or a reconnect, is picked up without remounting.

**Your lifespan owns the refresh.** `create_app` rebuilds the agent when the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your lifespan owns the refresh

This cryptic / condensed LLM-speak freaks me out.

@ciaransweet
ciaransweet merged commit e1b5707 into main Oct 28, 2026
4 checks passed
@ciaransweet
ciaransweet deleted the feat/refresh-tools branch October 28, 2026 10:23
ciaransweet pushed a commit that referenced this pull request Oct 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.12.1](mcp-toolsets-runtime-v0.12.0...mcp-toolsets-runtime-v0.12.1)
(2026-10-28)


### Features

* **api:** rebuild the agent when the toolsets' tools change
([#218](#218))
([e1b5707](e1b5707))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: ds-release-bot[bot] <116609932+ds-release-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants