All files / src/entities/search registry.ts

100% Statements 28/28
100% Branches 8/8
100% Functions 4/4
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 12817x 17x   17x 17x                 17x                           20x     20x 1x     19x     4x     4x     4x       4x                 10x     10x     10x       10x                   5x     5x     5x       5x                                         17x 1x           17x 4x             17x   3x 3x    
import * as z from "zod";
import { BrowseSearchSchema } from "./schema-readonly";
import { ToolRegistry, EnhancedToolDefinition } from "../../types";
import { isActionDenied } from "../../config";
import { gitlab, paths, toQuery } from "../../utils/gitlab-api";
 
/**
 * Search tools registry - 1 read-only CQRS tool
 *
 * browse_search (Query): global, project, group
 *
 * Search is read-only by design - no manage_search tool needed.
 */
export const searchToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinition>([
  // ============================================================================
  // browse_search - CQRS Query Tool (discriminated union schema)
  // TypeScript automatically narrows types in each switch case
  // ============================================================================
  [
    "browse_search",
    {
      name: "browse_search",
      description:
        "Search across GitLab resources globally or within a scope. Actions: global (entire instance), project (within specific project), group (within specific group). Searchable: projects, issues, merge_requests, milestones, users, blobs (code), commits, wiki_blobs, notes.",
      inputSchema: z.toJSONSchema(BrowseSearchSchema),
      gate: { envVar: "USE_SEARCH", defaultValue: true },
      handler: async (args: unknown): Promise<unknown> => {
        const input = BrowseSearchSchema.parse(args);
 
        // Runtime validation: reject denied actions even if they bypass schema filtering
        if (isActionDenied("browse_search", input.action)) {
          throw new Error(`Action '${input.action}' is not allowed for browse_search tool`);
        }
 
        switch (input.action) {
          case "global": {
            // TypeScript knows: input has scope, search, and optional filters
            const { scope, ...params } = input;
 
            // Build query params excluding action (not an API parameter)
            const query = toQuery(params, ["action"]);
 
            // Global search endpoint
            const results = await gitlab.get<unknown[]>("search", {
              query: { ...query, scope },
            });
 
            return {
              scope,
              count: results.length,
              results,
            };
          }
 
          case "project": {
            // TypeScript knows: input has project_id, scope, search, and optional filters
            const { project_id, scope, ref, ...params } = input;
 
            // Build query params excluding action (project_id, scope, ref are already destructured)
            const query = toQuery(params, ["action"]);
 
            // Project-scoped search endpoint
            const results = await gitlab.get<unknown[]>(`${paths.project(project_id)}/search`, {
              query: { ...query, scope, ...(ref && { ref }) },
            });
 
            return {
              project_id,
              scope,
              count: results.length,
              results,
            };
          }
 
          case "group": {
            // TypeScript knows: input has group_id, scope, search, and optional filters
            const { group_id, scope, ...params } = input;
 
            // Build query params excluding action (group_id, scope are already destructured)
            const query = toQuery(params, ["action"]);
 
            // Group-scoped search endpoint
            const results = await gitlab.get<unknown[]>(`${paths.group(group_id)}/search`, {
              query: { ...query, scope },
            });
 
            return {
              group_id,
              scope,
              count: results.length,
              results,
            };
          }
 
          /* istanbul ignore next -- unreachable with Zod discriminatedUnion */
          default:
            throw new Error(`Unknown action: ${(input as { action: string }).action}`);
        }
      },
    },
  ],
]);
 
/**
 * Get read-only tool names from the registry
 * Search is entirely read-only, so all tools are read-only
 */
export function getSearchReadOnlyToolNames(): string[] {
  return ["browse_search"];
}
 
/**
 * Get all tool definitions from the registry
 */
export function getSearchToolDefinitions(): EnhancedToolDefinition[] {
  return Array.from(searchToolRegistry.values());
}
 
/**
 * Get filtered tools based on read-only mode
 * Since search is read-only, this always returns all tools
 */
export function getFilteredSearchTools(readOnlyMode: boolean = false): EnhancedToolDefinition[] {
  // Search is always read-only, so readOnlyMode doesn't affect filtering
  void readOnlyMode;
  return getSearchToolDefinitions();
}