Stu Mason
Stu Mason

Activity

Issue Resolved

Issue #203 closed: system list_resources returns full Coolify payload by default — same bug class as pre-#158 listApplicationDeployments

Summary

system({ action: 'list_resources' }) (introduced in v2.11.0 / #172) typed as Promise<ResourceListItem[]> where ResourceListItem is { uuid, name, type, status? }, but the underlying Coolify /api/v1/resources endpoint actually returns the full nested resource record (~95 fields per item including build/healthcheck/limits/git/docker-compose config). The TypeScript type is runtime-erased, so the client just casts the bloated response without projecting it. This is the same bug class that PR #158 fixed for listApplicationDeployments (was typed Deployment[], returned { count, deployments } envelope with embedded logs blobs).

Reproduction

Against a Coolify v4 instance with a modest 33 apps/services:

MetricValue
Tool callsystem({ action: 'list_resources' })
HTTP endpointGET /api/v1/resources
Item count33
Response size537,930 bytes (~525 KB)
Avg bytes / item~16,300
Keys per item~95 (vs. 4 promised by ResourceListItem)

Sample of fields actually returned that the type does not declare (truncated):

additional_networks_count, additional_servers, additional_servers_count,
base_directory, build_command, build_pack, compose_parsing_version,
config_hash, created_at, custom_docker_run_options, custom_healthcheck_found,
custom_labels, custom_network_aliases, custom_nginx_configuration, deleted_at,
description, destination, destination_id, destination_type, docker_compose,
docker_compose_custom_build_command, docker_compose_custom_start_command,
docker_compose_domains, docker_compose_location, docker_compose_raw,
docker_registry_image_name, docker_registry_image_tag, dockerfile,
dockerfile_location, dockerfile_target_build, environment_id, fqdn,
git_branch, git_commit_sha, git_full_url, git_repository,
health_check_command, health_check_enabled, health_check_host,
health_check_interval, health_check_method, health_check_path,
health_check_port, health_check_response_text, health_check_retries,
health_check_return_code, health_check_scheme, health_check_start_period,
health_check_timeout, health_check_type, ...

Impact

  • MCP token budget: 525 KB blows past typical MCP client/LLM context limits on a single tool call. The result has to be spilled to disk by the host, after which the LLM cannot meaningfully consume it. Effectively renders list_resources unusable for any agentic workflow on instances with non-trivial resource counts.
  • Type lie: any TypeScript consumer destructuring ResourceListItem fields works (lucky), but anyone relying on the typed surface to reason about response size or field set is wrong-at-runtime.
  • Scales linearly with instance size: 33 resources is small. A larger instance (100+ apps/services) easily exceeds 1.5 MB on this single call.

Proposed fix (mirrors PR #158)

  1. Introduce a true runtime essential projection:

    export interface ResourceListItemEssential {
      uuid: string;
      name: string;
      type: 'server' | 'application' | 'database' | 'service' | string;
      status?: string;
    }
    

    (i.e. what ResourceListItem already claims to be, but now actually enforced at the API boundary in listResources() by mapping over the response.)

  2. Update listResources() to project by default and accept an opt-in:

    async listResources(
      options?: { include_full?: boolean }
    ): Promise<ResourceListItemEssential[] | FullResourceListItem[]>
    
  3. Expose include_full?: boolean on the system tool's list_resources action. Default false (essential projection). Set true to opt back into the raw Coolify response.

  4. Add an integration test that asserts the default response shape contains only the essential keys, guarding against future regressions.

Notes

  • Naming include_full (rather than include_logs) because the heavy data on /resources is not a single field but the full record. Pattern parity with PR #158's include_logs: true opt-in.
  • A separate issue will cover secret-masking on the include_full path — the raw Coolify response contains manual_webhook_secret_* and http_basic_auth_password fields that should be masked by default (analog of the v2.9.0 env_vars masking from #159/#182).
  • Happy to send the PR.