Skip to main content
Glama

m9k_search

Read-onlyIdempotent

Find relevant past conversations and indexed content quickly. Apply filters for project, date, source, and sort order. Results prioritize current session and project.

Instructions

Search indexed past conversations. Returns compact results with snippets. Results from the current project and session are boosted by default. Use m9k_context or m9k_full to drill down.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch query (keywords or natural language)
projectNoFilter by project path. Omit for cross-project search.
limitNoMax results
sinceNoISO-8601 date. Only results after this date.
untilNoISO-8601 date. Only results before this date (exclusive).
orderNoSort order: score (relevance), date_asc (oldest first), date_desc (newest first)score
sourceNoFilter by source type. Default: all sources.

Implementation Reference

  • The 'm9k_search' tool registration and handler. registerSearchTools registers 'm9k_search' with input schema (query, project, limit, since, until, order, source) and calls the core search() function, returning JSON-stringified results.
    export function registerSearchTools(server: McpServer, ctx: ToolContext): void {
      server.registerTool(
        'm9k_search',
        {
          description:
            'Search indexed past conversations. Returns compact results with snippets. Results from the current project and session are boosted by default. Use m9k_context or m9k_full to drill down.',
          inputSchema: {
            query: z.string().describe('Search query (keywords or natural language)'),
            project: z
              .string()
              .optional()
              .describe('Filter by project path. Omit for cross-project search.'),
            limit: z.number().int().min(1).max(50).default(10).describe('Max results'),
            since: z.string().optional().describe('ISO-8601 date. Only results after this date.'),
            until: z
              .string()
              .optional()
              .describe('ISO-8601 date. Only results before this date (exclusive).'),
            order: z
              .enum(['score', 'date_asc', 'date_desc'])
              .default('score')
              .describe(
                'Sort order: score (relevance), date_asc (oldest first), date_desc (newest first)',
              ),
            source: z
              .enum(['conversations', 'git', 'files'])
              .optional()
              .describe('Filter by source type. Default: all sources.'),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
        async ({ query, project, limit, since, until, order }) => {
          const currentSession = getStat(ctx.db, 'current_session_id') || undefined;
          const results = await search(
            ctx.db,
            {
              query,
              project,
              currentProject: ctx.currentProject,
              currentSession,
              limit,
              since,
              until,
              order,
            },
            ctx.searchContext,
          );
          return {
            content: [{ type: 'text' as const, text: JSON.stringify(results) }],
          };
        },
      );
  • Registration of 'm9k_search' tool via server.registerTool with its input schema (zod definitions for query, project, limit, since, until, order, source) and annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint).
    export function registerSearchTools(server: McpServer, ctx: ToolContext): void {
      server.registerTool(
        'm9k_search',
        {
          description:
            'Search indexed past conversations. Returns compact results with snippets. Results from the current project and session are boosted by default. Use m9k_context or m9k_full to drill down.',
          inputSchema: {
            query: z.string().describe('Search query (keywords or natural language)'),
            project: z
              .string()
              .optional()
              .describe('Filter by project path. Omit for cross-project search.'),
            limit: z.number().int().min(1).max(50).default(10).describe('Max results'),
            since: z.string().optional().describe('ISO-8601 date. Only results after this date.'),
            until: z
              .string()
              .optional()
              .describe('ISO-8601 date. Only results before this date (exclusive).'),
            order: z
              .enum(['score', 'date_asc', 'date_desc'])
              .default('score')
              .describe(
                'Sort order: score (relevance), date_asc (oldest first), date_desc (newest first)',
              ),
            source: z
              .enum(['conversations', 'git', 'files'])
              .optional()
              .describe('Filter by source type. Default: all sources.'),
          },
          annotations: {
            readOnlyHint: true,
            destructiveHint: false,
            idempotentHint: true,
            openWorldHint: false,
          },
        },
  • Input schema for the m9k_search tool: query (string, required), project (optional string), limit (number, 1-50, default 10), since (optional ISO-8601 string), until (optional ISO-8601 string), order (enum: score/date_asc/date_desc, default score), source (optional enum: conversations/git/files).
    inputSchema: {
      query: z.string().describe('Search query (keywords or natural language)'),
      project: z
        .string()
        .optional()
        .describe('Filter by project path. Omit for cross-project search.'),
      limit: z.number().int().min(1).max(50).default(10).describe('Max results'),
      since: z.string().optional().describe('ISO-8601 date. Only results after this date.'),
      until: z
        .string()
        .optional()
        .describe('ISO-8601 date. Only results before this date (exclusive).'),
      order: z
        .enum(['score', 'date_asc', 'date_desc'])
        .default('score')
        .describe(
          'Sort order: score (relevance), date_asc (oldest first), date_desc (newest first)',
        ),
      source: z
        .enum(['conversations', 'git', 'files'])
        .optional()
        .describe('Filter by source type. Default: all sources.'),
    },
  • Core search() function executed by m9k_search. Performs hybrid search: BM25 (FTS5) + optional text/code vector search (via sqlite-vec), fused via Reciprocal Rank Fusion (RRF). Supports filtering by project/date, auto-fuzzy fallback, cross-encoder reranker, project/session affinity boosts, and date-based sorting.
    export async function search(
      db: Database.Database,
      options: SearchOptions,
      ctx?: SearchContext,
    ): Promise<SearchResult[]> {
      if (!options.query.trim()) return [];
    
      const t0 = performance.now();
      const autoFuzzyThreshold = ctx?.autoFuzzyThreshold ?? 3;
    
      // BM25 search (always available)
      const bm25Results = searchBM25(db, options.query, options.limit * 2);
      const t1 = performance.now();
      logger.debug('search', ` BM25: ${(t1 - t0).toFixed(0)}ms (${bm25Results.length} results)`);
    
      let results: SearchResult[];
    
      // Triple vector search: BM25 + text vectors + code vectors, fused via RRF
      const vecLists: SearchResult[][] = [bm25Results];
    
      if (ctx?.vecTextEnabled && ctx.embedderText) {
        try {
          const t2 = performance.now();
          const embedFn = ctx.embedderText.embedQuery ?? ctx.embedderText.embed;
          const emb = await embedFn.call(ctx.embedderText, options.query);
          const t3 = performance.now();
          logger.debug('search', ` text embed: ${(t3 - t2).toFixed(0)}ms`);
          vecLists.push(searchVectorized(db, emb, options.limit * 2, MAX_VECTOR_DISTANCE, '_text'));
          logger.debug('search', ` text vec search: ${(performance.now() - t3).toFixed(0)}ms`);
        } catch (err) {
          logger.warn('search', 'Text vector search failed — degrading gracefully', err);
        }
      }
    
      if (ctx?.vecCodeEnabled && ctx.embedderCode) {
        try {
          const t4 = performance.now();
          const embedFn = ctx.embedderCode.embedQuery ?? ctx.embedderCode.embed;
          const emb = await embedFn.call(ctx.embedderCode, options.query);
          const t5 = performance.now();
          logger.debug('search', ` code embed: ${(t5 - t4).toFixed(0)}ms`);
          vecLists.push(searchVectorized(db, emb, options.limit * 2, MAX_VECTOR_DISTANCE, '_code'));
          logger.debug('search', ` code vec search: ${(performance.now() - t5).toFixed(0)}ms`);
        } catch (err) {
          logger.warn('search', 'Code vector search failed — degrading gracefully', err);
        }
      }
    
      results = vecLists.length > 1 ? fusionRRF(vecLists) : bm25Results;
      logger.debug('search', ` RRF: ${(performance.now() - t1).toFixed(0)}ms total`);
    
      // Filter by project if specified
      if (options.project) {
        results = results.filter((r) => r.project === options.project);
      }
    
      // Filter by date if specified
      if (options.since) {
        results = results.filter((r) => r.timestamp >= options.since!);
      }
      if (options.until) {
        results = results.filter((r) => r.timestamp < options.until!);
      }
    
      // Auto-fuzzy fallback: if fewer than threshold results, try wildcard search
      if (results.length < autoFuzzyThreshold) {
        const fuzzyResults = searchFuzzy(db, options.query, options.limit * 2);
        // Merge: add fuzzy results that aren't already in exact results
        const existingIds = new Set(results.map((r) => r.chunkId));
        for (const fr of fuzzyResults) {
          if (!existingIds.has(fr.chunkId)) {
            // Apply same filters
            if (options.project && fr.project !== options.project) continue;
            if (options.since && fr.timestamp < options.since) continue;
            if (options.until && fr.timestamp >= options.until) continue;
            results.push(fr);
            existingIds.add(fr.chunkId);
          }
        }
      }
    
      // Reranker: cross-encoder reorders pre-limit results by relevance
      const tRerank0 = performance.now();
      if (ctx?.reranker && results.length > 1) {
        try {
          const documents = results.map((r) => {
            const chunk = db
              .prepare('SELECT user_content, assistant_content FROM conv_chunks WHERE id = ?')
              .get(r.chunkId) as { user_content: string; assistant_content: string } | undefined;
            return {
              id: r.chunkId,
              content: chunk
                ? (chunk.user_content + ' ' + chunk.assistant_content).slice(0, 512)
                : r.snippet,
            };
          });
    
          const reranked = await ctx.reranker.rerank(options.query, documents, options.limit);
          const rerankedMap = new Map(reranked.map((rr, i) => [rr.id, { score: rr.score, rank: i }]));
    
          results = results
            .filter((r) => rerankedMap.has(r.chunkId))
            .sort((a, b) => {
              const ra = rerankedMap.get(a.chunkId)!;
              const rb = rerankedMap.get(b.chunkId)!;
              return ra.rank - rb.rank;
            })
            .map((r) => ({ ...r, score: rerankedMap.get(r.chunkId)!.score }));
        } catch (err) {
          logger.warn('search', 'Reranker failed — keeping original order', err);
        }
      }
      logger.debug('search', ` reranker: ${(performance.now() - tRerank0).toFixed(0)}ms`);
    
      // Project affinity boost: promote current project results when no strict project filter
      if (!options.project && options.currentProject) {
        for (const r of results) {
          if (r.project === options.currentProject) {
            // Handle both positive (RRF/reranker) and negative (raw BM25) scores
            r.score = r.score >= 0 ? r.score * PROJECT_BOOST_FACTOR : r.score / PROJECT_BOOST_FACTOR;
          }
        }
        results.sort((a, b) => b.score - a.score);
      }
    
      // Session affinity boost: promote current session results (weaker than project boost)
      if (options.currentSession) {
        for (const r of results) {
          if (r.sessionId === options.currentSession) {
            r.score = r.score >= 0 ? r.score * SESSION_BOOST_FACTOR : r.score / SESSION_BOOST_FACTOR;
          }
        }
        results.sort((a, b) => b.score - a.score);
      }
    
      // Re-sort if order is date-based (default is score from BM25/RRF/reranker)
      if (options.order === 'date_asc') {
        results.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
      } else if (options.order === 'date_desc') {
        results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
      }
    
      const finalResults = results.slice(0, options.limit);
    
      // Update usage counters (best-effort, don't fail the search)
      try {
        incrementStat(db, 'search_count');
        if (finalResults.length > 0) {
          incrementStat(db, 'hit_count');
          const tokensServed = finalResults.reduce(
            (sum, r) => sum + Math.ceil(r.snippet.length / 4),
            0,
          );
          incrementStat(db, 'tokens_served', tokensServed);
        }
        setStat(db, 'last_search_at', new Date().toISOString());
      } catch {
        // Non-critical — don't fail searches if stats write fails
      }
    
      return finalResults;
    }
  • ToolContext interface used by m9k_search handler, containing db, config, searchContext (embedders, reranker), currentProject, orchestrator, embeddingState, version, and mode.
    export interface ToolContext {
      db: DatabaseType;
      cfg: MelchizedekConfig;
      searchContext: SearchContext;
      currentProject?: string;
      orchestrator: EmbedOrchestrator | null;
      embeddingState: EmbeddingState;
      version: string;
      mode: 'daemon' | 'local';
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Description adds value beyond annotations by revealing result format (compact snippets) and boosting behavior. Consistent with readOnlyHint=true annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences with clear front-loading of purpose, no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 7 parameters, no output schema, and strong annotations, the description provides sufficient context for appropriate tool selection and invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so parameters are already well-defined. Description adds no parameter-level detail beyond the boosting context, so baseline 3 appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it searches indexed past conversations and returns compact results with snippets. Differentiates from siblings by mentioning drilling down with m9k_context or m9k_full.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit guidance: boosting of current project/session, and alternatives for deeper investigation (m9k_context, m9k_full).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/louis49/melchizedek'

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