Skip to main content
Glama

backlog_search

Search tasks, epics, and resources in your backlog using keywords or natural language queries. Filter results by type, status, or parent item to find relevant work items quickly.

Instructions

Search across all backlog content — tasks, epics, and resources. Returns relevance-ranked results with match context. Use this for discovery; use backlog_list for filtering by status/type.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch query. Supports keywords, phrases, and natural language. Fuzzy matching and semantic similarity are applied automatically.
typesNoFilter results by type. Default: all types. Example: ["task", "epic"] to exclude resources.
statusNoFilter tasks/epics by status. Default: all statuses. Example: ["open", "in_progress"] for active work only.
parent_idNoScope search to items under a specific parent. Example: "EPIC-0001"
sortNoSort mode. "relevant" (default) ranks by search relevance. "recent" ranks by last updated.
limitNoMax results to return. Default: 20, max: 100.
include_contentNoInclude full description/content in results. Default: false (returns snippets only). Set true when you need the full text.
include_scoresNoInclude relevance scores in results. Default: false.

Implementation Reference

  • Main tool handler that processes backlog_search requests. Validates input, calls storage.searchUnified(), formats results, and returns JSON response with relevance-ranked search results across tasks, epics, and resources.
    async ({ query, types, status, parent_id, sort, limit, include_content, include_scores }) => {
      if (!query.trim()) {
        return { content: [{ type: 'text', text: JSON.stringify({ error: 'Query must not be empty' }) }], isError: true };
      }
    
      const results = await storage.searchUnified(query, {
        types: types as SearchableType[] | undefined,
        status,
        parent_id,
        sort: sort ?? 'relevant',
        limit: limit ?? 20,
      });
    
      const searchMode = storage.isHybridSearchActive() ? 'hybrid' : 'bm25';
    
      const formattedResults = results.map(r => {
        const isResource = r.type === 'resource';
    
        if (isResource) {
          const resource = r.item as Resource;
          const result: Record<string, unknown> = {
            id: resource.id,
            title: resource.title,
            type: 'resource',
            path: resource.path,
          };
          if (r.snippet) {
            result.snippet = r.snippet.text;
            result.matched_fields = r.snippet.matched_fields;
          }
          if (include_scores) result.score = Math.round(r.score * 1000) / 1000;
          if (include_content) result.content = resource.content;
          return result;
        }
    
        // Task or Epic
        const task = r.item as Entity;
        const result: Record<string, unknown> = {
          id: task.id,
          title: task.title,
          type: r.type,
          status: task.status,
        };
        const parentId = task.parent_id ?? task.epic_id;
        if (parentId) result.parent_id = parentId;
        if (r.snippet) {
          result.snippet = r.snippet.text;
          result.matched_fields = r.snippet.matched_fields;
        }
        if (include_scores) result.score = Math.round(r.score * 1000) / 1000;
        if (include_content) result.description = task.description;
        return result;
      });
    
      const response = {
        results: formattedResults,
        total: formattedResults.length,
        query,
        search_mode: searchMode,
      };
    
      return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
    }
  • Tool registration function that registers backlog_search with the MCP server, including tool name, description, and input schema using Zod validation.
    export function registerBacklogSearchTool(server: McpServer) {
      server.registerTool(
        'backlog_search',
        {
          description: 'Search across all backlog content — tasks, epics, and resources. Returns relevance-ranked results with match context. Use this for discovery; use backlog_list for filtering by status/type.',
          inputSchema: z.object({
            query: z.string().describe('Search query. Supports keywords, phrases, and natural language. Fuzzy matching and semantic similarity are applied automatically.'),
            types: z.array(z.enum(['task', 'epic', 'resource'])).optional().describe('Filter results by type. Default: all types. Example: ["task", "epic"] to exclude resources.'),
            status: z.array(z.enum(STATUSES)).optional().describe('Filter tasks/epics by status. Default: all statuses. Example: ["open", "in_progress"] for active work only.'),
            parent_id: z.string().optional().describe('Scope search to items under a specific parent. Example: "EPIC-0001"'),
            sort: z.enum(['relevant', 'recent']).optional().describe('Sort mode. "relevant" (default) ranks by search relevance. "recent" ranks by last updated.'),
            limit: z.number().min(1).max(100).optional().describe('Max results to return. Default: 20, max: 100.'),
            include_content: z.boolean().optional().describe('Include full description/content in results. Default: false (returns snippets only). Set true when you need the full text.'),
            include_scores: z.boolean().optional().describe('Include relevance scores in results. Default: false.'),
          }),
        },
        async ({ query, types, status, parent_id, sort, limit, include_content, include_scores }) => {
          if (!query.trim()) {
            return { content: [{ type: 'text', text: JSON.stringify({ error: 'Query must not be empty' }) }], isError: true };
          }
    
          const results = await storage.searchUnified(query, {
            types: types as SearchableType[] | undefined,
            status,
            parent_id,
            sort: sort ?? 'relevant',
            limit: limit ?? 20,
          });
    
          const searchMode = storage.isHybridSearchActive() ? 'hybrid' : 'bm25';
    
          const formattedResults = results.map(r => {
            const isResource = r.type === 'resource';
    
            if (isResource) {
              const resource = r.item as Resource;
              const result: Record<string, unknown> = {
                id: resource.id,
                title: resource.title,
                type: 'resource',
                path: resource.path,
              };
              if (r.snippet) {
                result.snippet = r.snippet.text;
                result.matched_fields = r.snippet.matched_fields;
              }
              if (include_scores) result.score = Math.round(r.score * 1000) / 1000;
              if (include_content) result.content = resource.content;
              return result;
            }
    
            // Task or Epic
            const task = r.item as Entity;
            const result: Record<string, unknown> = {
              id: task.id,
              title: task.title,
              type: r.type,
              status: task.status,
            };
            const parentId = task.parent_id ?? task.epic_id;
            if (parentId) result.parent_id = parentId;
            if (r.snippet) {
              result.snippet = r.snippet.text;
              result.matched_fields = r.snippet.matched_fields;
            }
            if (include_scores) result.score = Math.round(r.score * 1000) / 1000;
            if (include_content) result.description = task.description;
            return result;
          });
    
          const response = {
            results: formattedResults,
            total: formattedResults.length,
            query,
            search_mode: searchMode,
          };
    
          return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
        }
      );
  • Tool registry that imports and calls registerBacklogSearchTool as part of the overall tool registration process.
    import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { registerBacklogListTool } from './backlog-list.js';
    import { registerBacklogGetTool } from './backlog-get.js';
    import { registerBacklogCreateTool } from './backlog-create.js';
    import { registerBacklogUpdateTool } from './backlog-update.js';
    import { registerBacklogDeleteTool } from './backlog-delete.js';
    import { registerBacklogSearchTool } from './backlog-search.js';
    import { registerBacklogContextTool } from './backlog-context.js';
    
    export function registerTools(server: McpServer) {
      registerBacklogListTool(server);
      registerBacklogGetTool(server);
      registerBacklogCreateTool(server);
      registerBacklogUpdateTool(server);
      registerBacklogDeleteTool(server);
      registerBacklogSearchTool(server);
      registerBacklogContextTool(server);
    }
  • Core search method searchUnified that serves as the canonical entry point for all search operations, called by both MCP tools and HTTP endpoints. Delegates to OramaSearchService.searchAll with proper filtering.
    /**
     * Canonical search method — the single entry point for all search operations.
     * Both MCP tools (backlog_search) and HTTP endpoints (GET /search) MUST call
     * this method. This ensures MCP and UI always get identical results from the
     * same code path. (ADR-0073: MCP-first unified search architecture)
     *
     * Returns UnifiedSearchResult[] with item, score, type, and server-side snippet.
     * Supports searching tasks, epics, and resources.
     */
    async searchUnified(query: string, options?: {
      types?: SearchableType[];
      limit?: number;
      sort?: 'relevant' | 'recent';
      /** Filter by status (tasks/epics only) */
      status?: Status[];
      /** Scope to parent (epic/folder) */
      parent_id?: string;
    }): Promise<UnifiedSearchResult[]> {
      await this.ensureSearchReady();
    
      const results = await this.search.searchAll(query, {
        docTypes: options?.types,
        limit: options?.limit ?? 20,
        sort: options?.sort,
        filters: {
          status: options?.status,
          parent_id: options?.parent_id,
        },
      });
    
      return results.map(r => ({
        item: r.item,
        score: r.score,
        type: r.type,
        snippet: r.snippet,
      }));
    }
  • Underlying searchAll implementation that executes hybrid BM25+vector search with linear fusion, handles type filtering, and generates match snippets for results.
    /**
     * Search all document types with optional type filtering.
     * Returns results sorted by relevance across all types.
     *
     * This is the canonical search method — both MCP tools and HTTP endpoints
     * should call this (via BacklogService.searchUnified). (ADR-0073)
     *
     * ADR-0080: Uses native sortBy for "recent" mode instead of JS post-sort.
     * ADR-0081: Uses independent retrievers + linear fusion for relevance mode.
     */
    async searchAll(query: string, options?: SearchOptions): Promise<Array<{ id: string; score: number; type: SearchableType; item: Entity | Resource; snippet: SearchSnippet }>> {
      if (!this.db || !query.trim()) return [];
    
      const limit = options?.limit ?? 20;
      const sortMode = options?.sort ?? 'relevant';
      const where = buildWhereClause(options?.filters, options?.docTypes);
    
      const { hits } = await this._fusedSearch({
        query,
        limit,
        boost: options?.boost ?? { id: 10, title: 3 },
        where,
        ...(sortMode === 'recent' ? { sortBy: { property: 'updated_at', order: 'DESC' as const } } : {}),
      });
    
      return hits
        .map(h => {
          const task = this.taskCache.get(h.id);
          const resource = this.resourceCache.get(h.id);
          const item = task || resource;
          if (!item) return null;
          const isResource = !task;
          const docType = (isResource ? 'resource' : (item as Entity).type || 'task') as SearchableType;
          const snippet = isResource
            ? generateResourceSnippet(item as Resource, query)
            : generateTaskSnippet(item as Entity, query);
          return { id: h.id, score: h.score, type: docType, item, snippet };
        })
        .filter((h): h is NonNullable<typeof h> => h !== null);
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gkoreli/backlog-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server