resolveToolAvailability function

ToolAvailabilityResolution resolveToolAvailability({
  1. required Map<String, ToolCapability> capabilities,
  2. required List<(ToolScope, ToolsConfig)> scopes,
  3. Set<String>? essentialToolIds,
  4. Iterable<String> mcpServerIds = const [],
})

Resolves the scope stack against the host's capabilities.

scopes lists (scope, config) pairs shallow→deep (global, project, session, runtime); the deepest scope mentioning a key wins. Capability absence is the hard floor: such ids stay disabled at ToolScope.builtin even when a scope force-enables them. Present capabilities are on by default and only config can turn them off. Unknown ids are collected in ToolAvailabilityResolution.unknownIds without failing.

essentialToolIds is the active load-mode preset (issue #680): when set, every ENABLED id outside it — static tool ids AND enabled mcp:<server> families — demotes to discoverable (see ToolAvailabilityResolution.discoverableIds). mcpServerIds names the live MCP servers (the manager's connected set): servers no scope declares are enabled by default, so the resolver cannot see them without this — they demote like everyone else.

Implementation

ToolAvailabilityResolution resolveToolAvailability({
  required Map<String, ToolCapability> capabilities,
  required List<(ToolScope, ToolsConfig)> scopes,
  Set<String>? essentialToolIds,
  Iterable<String> mcpServerIds = const [],
}) {
  final (:intent, :intentScope, :unknownIds, :mcpServers) = _collectToolIntents(
    scopes,
  );

  final byId = <String, ResolvedToolAvailability>{
    for (final id in knownToolIds)
      id: _resolveKnownTool(id, capabilities[id], intent, intentScope),
  };

  if (intent['mcp'] == false) {
    for (final server in mcpServers.keys.toList()) {
      mcpServers[server] = false;
    }
  }
  // Load-mode demotion (issue #680): under a preset, an enabled id
  // outside the essential set becomes discoverable — UNLESS a scope
  // explicitly enabled it (`tools: {id: on}` is a standing mount: the
  // user asked for the tool, the preset only curates the default). The
  // essential set is pinned: nothing in the scope stack can demote it,
  // and an explicit `off` still disables it exactly as before.
  final discoverable = essentialToolIds == null
      ? const <String>{}
      : {
          for (final entry in byId.entries)
            if (entry.value.enabled &&
                !essentialToolIds.contains(entry.key) &&
                intent[entry.key] != true)
              entry.key,
          // MCP families demote too (issue #680 — the card lists mcp__*
          // among the discoverable): the heaviest tool schemas are the
          // feature's whole point. The same standing-mount rule applies:
          // an explicit per-server `tools: {mcp:<server>: on}` or an
          // aggregate `tools: {mcp: on}` keeps the family in the schema.
          // Presets never carry mcp ids, and a disabled family (declared
          // off, or the mcp:false kill-switch above) fails the enabled
          // check — disable stays disable, not demotion.
          if (intent['mcp'] != true)
            for (final server in {...mcpServerIds, ...mcpServers.keys})
              if ((mcpServers[server] ?? byId['mcp']?.enabled ?? true) &&
                  mcpServers[server] != true)
                'mcp:$server',
        };
  return ToolAvailabilityResolution(
    byId: byId,
    unknownIds: unknownIds,
    mcpServers: mcpServers,
    discoverableIds: discoverable,
  );
}