Skip to content

fix: bound tool response sizes; make list_applications search actually filter - #147

Open
schahal wants to merge 1 commit into
argoproj-labs:mainfrom
schahal:feat/bound-tool-response-sizes
Open

fix: bound tool response sizes; make list_applications search actually filter#147
schahal wants to merge 1 commit into
argoproj-labs:mainfrom
schahal:feat/bound-tool-response-sizes

Conversation

@schahal

@schahal schahal commented Aug 5, 2026

Copy link
Copy Markdown

Fixes #59

Problem

Validated against a large real-world ArgoCD instance (hundreds of applications, dozens of projects, ArgoCD v3.x) running v0.8.0 of this server:

  • A bare list_applications() returns ~3M chars (~770k tokens) — it exceeds most model context windows outright.
  • search is silently ignored: it is forwarded as a query param that ArgoCD's ApplicationQuery does not define, so the gRPC gateway drops it and the whole fleet comes back while the caller believes the response is bounded. search:"<exact-app-name>" with no limit still returned every application. This is worse than not offering the parameter, since callers cannot tell their filter did nothing.
  • Each list item carries the app's full inline Helm values twice (spec.source.helm.values + status.sync.comparedTo.source.helm.values) — even limit=1 cost ~11.4k chars.
  • get_application ≈ 117k chars, dominated by status.history (54k) + status.operationState (29k).
  • list_clusters ≈ 227k chars, of which info.apiVersions is ~95%.

Changes

list_applications

  • search is now a real filter: client-side case-insensitive partial match on name (ArgoCD has no server-side equivalent), applied before pagination; metadata.totalItems reflects the filtered count. The bogus query param is no longer sent.
  • Deep-strip list items: sources reduced to identifying fields (repoURL/path/chart/targetRevision/ref/name — inline Helm values dropped), status.sync reduced to status/revision(s) (dropping comparedTo, the second Helm-values copy). Multi-source apps (spec.sources) are now represented too.
  • Default limit of 50 — an unqualified call can no longer return the fleet. metadata.{totalItems,hasMore} support paging, and limit=1 + totalItems remains the cheap way to count.
  • New server-side filters ArgoCD actually supports: projects (repeated), selector (labels), repo.
  • New detail: "name" level (name/namespace/project/sync/health only) for fleet-wide sweeps in a single call.

get_application — drops metadata.managedFields, status.history, full status.operationState (collapsed to phase/message/timestamps/retryCount), and status.sync.comparedTo by default; includeHistory / includeOperationState opt back in. spec is untouched.

list_clusters — drops connection config and info.apiVersions; keeps name, server, labels/annotations, namespaces, connection state, app count, server version.

All tools — a response-size guard in addJsonOutputTool: responses over MCP_MAX_RESPONSE_CHARS (default 100,000 chars ≈ 25k tokens; 0 disables) are replaced with an error naming the tool's narrowing parameters (e.g. get_resources → pass specific resourceRefs). No tool, present or future, can flood the client context. Documented in the README.

HttpClient — array query-param support (projects=a&projects=b, matching gRPC-gateway repeated-field encoding).

Measured impact

Call Before After
list_applications() bare ~770k tokens 50 summarized apps, ~10–15k tokens
list_applications(search:"x") no limit ~770k tokens (no-op filter) matching apps only
list_applications(detail:"name"), full fleet via paging ~15–20k tokens total
get_application ~29k tokens ~5k tokens (opt-in for history/operation detail)
list_clusters ~57k tokens ~2k tokens
any pathological response flood/overflow bounded error with narrowing hint

End to end: an AI-agent session that exercised these tools against the same instance dropped from ~615k tokens of context to ~35k (~94%) after switching to an image built from this branch.

Behavior changes to note

  • An unqualified list_applications now returns at most 50 items (previously: everything). metadata.totalItems/hasMore signal the rest.
  • metadata.totalItems counts applications after filtering — for a fleet count, call unfiltered with limit=1.
  • get_application no longer returns history/full operation state unless asked (opt back in via includeHistory / includeOperationState).
  • Oversized responses (>100k chars by default) now return an error with narrowing guidance instead of the payload; set MCP_MAX_RESPONSE_CHARS=0 for the old pass-through behavior.

Test plan

  • 11 new tests: search filtering (client-side, never forwarded, empty-match), server-side filter forwarding (incl. repeated projects), default limit, Helm-values/comparedTo/managedFields stripping, detail:"name" shape, get_application strip + opt-ins, list_clusters strip, size guard (trip + hint + 0 disables)
  • pnpm lint, pnpm build, pnpm test — 33/33 pass

🤖 Generated with Claude Code

…y filter

An unbounded list_applications on an instance with hundreds of applications returns ~3M chars
(~771k tokens) — beyond most context windows outright. Three compounding
causes, all fixed here:

list_applications:
- `search` was forwarded as a query param ArgoCD's ApplicationQuery does
  not have; the gRPC gateway silently drops it and returns the whole
  fleet while the caller believes the response is bounded. It is now a
  client-side case-insensitive partial match on name, applied before
  pagination, with metadata.totalItems reflecting the filtered count.
- Items carried the full inline Helm values twice (spec.source.helm and
  again inside status.sync.comparedTo). Sources are now reduced to their
  identifying fields and status.sync to its verdict.
- No default limit: an unqualified call now returns at most 50 apps
  (metadata.hasMore/totalItems support paging).
- New server-side filters ArgoCD actually supports: projects (repeated),
  selector, repo; and a detail:"name" level for fleet-wide sweeps.

get_application: drops metadata.managedFields, status.history, the full
operationState (collapsed to phase/message/timestamps), and
status.sync.comparedTo by default; includeHistory/includeOperationState
opt back in. spec is untouched. ~116k chars -> ~20k on a real app.

list_clusters: drops connection config and info.apiVersions (95% of a
real ~227k-char payload) keeping name/server/connection state/app count/
server version.

All tools: responses larger than MCP_MAX_RESPONSE_CHARS (default
100,000 chars, 0 disables) are replaced with an error naming the tool's
narrowing parameters, so no tool can flood the client context.

Ref: argoproj-labs#59

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Satbir Chahal <schahal@nextdoor.com>
@schahal

schahal commented Aug 5, 2026

Copy link
Copy Markdown
Author

Token usage for a simple prompt Are there any ArgoCD parameter overrides set on the <redacted> application in the <redacted> ArgoCD instance?

w/ v0.8.0 docker image

614.9K / 1M context used

w/ this branch docker image

35.2K / 1M context used

@schahal

schahal commented Aug 6, 2026

Copy link
Copy Markdown
Author

@leoluz looking at this repo history, if I'm not mistaken you may be only active maintainer? Possible to take a review/approve/merge?

I have my current ArgoCD-MCP server image built off this change and is working like a charm.

Comment thread src/server/server.ts
const text = JSON.stringify(result);
if (this.maxResponseChars > 0 && text.length > this.maxResponseChars) {
return {
isError: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this response guard called on every tool call?

For write operations like Sync, run_resource_action ect..
In the case they somehow exceed the limit, it would get the isError = true even though the operation succeeded. This may make the LLM behave incorrectly if it tries to retry the tool call.

Maybe we we only do this for read operation?

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.

Feature Request: Optimize list_applications Token Usage

2 participants