Skip to main content
Glama

recall

Query a unified index of facts, decisions, frameworks, lessons, and past conversations to retrieve categorized results. Use it to ask anything about the past.

Instructions

Unified search across ALL memory — facts, decisions, frameworks, lessons, and past conversations. Returns categorized results. Use this as the default 'ask anything about the past' tool.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesWhat to recall (natural language)

Implementation Reference

  • The main handler function for the 'recall' tool. Calls searchMemories with topK=10 and minScore=0.25, then categorizes results into Facts & Decisions, Relevant Frameworks, Lessons, and From Past Conversations. Formats and returns the combined output, with a fallback note if keyword search is used.
    export async function handleRecall(query: string): Promise<string> {
      const results = await searchMemories(query, { topK: 10, minScore: 0.25 });
    
      if (results.length === 0) {
        return `No memories found for "${query}".`;
      }
    
      const frameworks = results.filter((r) => r.category === "framework");
      const lessons = results.filter((r) => r.category === "lesson");
      const journals = results.filter((r) => r.source === "journal");
      const facts = results.filter(
        (r) => r.category !== "framework" && r.category !== "lesson" && r.source !== "journal",
      );
    
      const sections: string[] = [];
    
      if (facts.length > 0) {
        sections.push("## Facts & Decisions\n" + facts.map((r) => formatResult(r)).join("\n\n"));
      }
      if (frameworks.length > 0) {
        sections.push("## Relevant Frameworks\n" + frameworks.map((r) => formatResult(r)).join("\n\n"));
      }
      if (lessons.length > 0) {
        sections.push("## Lessons\n" + lessons.map((r) => formatResult(r)).join("\n\n"));
      }
      if (journals.length > 0) {
        sections.push("## From Past Conversations\n" + journals.map((r) => formatResult(r)).join("\n\n"));
      }
    
      const fallbackNote = isUsingFallback() ? "\n\n---\n*Using keyword search — install Ollama for semantic search.*" : "";
      return sections.join("\n\n---\n\n") + fallbackNote;
    }
    
    function formatResult(r: SearchResult): string {
      const score = (r.score * 100).toFixed(0);
      const date = new Date(r.created_at).toISOString().slice(0, 10);
      const project = r.project ? ` [${r.project}]` : "";
      return `**${score}% match**${project} — ${date}\n${r.content.slice(0, 300)}${r.content.length > 300 ? "..." : ""}`;
    }
  • Helper function formatResult that formats a single SearchResult entry with score percentage, project tag, date, and content preview (truncated to 300 chars).
    function formatResult(r: SearchResult): string {
      const score = (r.score * 100).toFixed(0);
      const date = new Date(r.created_at).toISOString().slice(0, 10);
      const project = r.project ? ` [${r.project}]` : "";
      return `**${score}% match**${project} — ${date}\n${r.content.slice(0, 300)}${r.content.length > 300 ? "..." : ""}`;
    }
  • SearchResult type definition used by the recall tool. Contains id, content, category, project, score, created_at, and source fields.
    export type SearchResult = {
      id: string;
      content: string;
      category: string;
      project: string | null;
      score: number;
      created_at: number;
      source: "memory" | "journal";
    };
    
    const STOPWORDS = new Set([
      "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
      "have", "has", "had", "do", "does", "did", "will", "would", "could",
      "should", "may", "might", "shall", "can", "need", "dare", "ought",
      "to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
      "into", "through", "during", "before", "after", "above", "below",
      "between", "out", "off", "over", "under", "again", "further", "then",
      "once", "here", "there", "when", "where", "why", "how", "all", "each",
      "every", "both", "few", "more", "most", "other", "some", "such", "no",
      "nor", "not", "only", "own", "same", "so", "than", "too", "very",
      "and", "but", "or", "if", "while", "because", "until", "about",
      "what", "which", "who", "whom", "this", "that", "these", "those",
      "i", "me", "my", "we", "our", "you", "your", "he", "him", "his",
      "she", "her", "it", "its", "they", "them", "their",
    ]);
    
    function tokenize(text: string): string[] {
      return text
        .toLowerCase()
        .replace(/[^\w\s]/g, " ")
        .split(/\s+/)
        .filter((w) => w.length > 1 && !STOPWORDS.has(w));
    }
    
    function keywordScore(queryTokens: string[], content: string): number {
      if (queryTokens.length === 0) return 0;
      const contentLower = content.toLowerCase();
      let matched = 0;
      for (const token of queryTokens) {
        if (contentLower.includes(token)) matched++;
      }
      return matched / queryTokens.length;
    }
    
    function keywordSearch(
      query: string,
      options: {
        topK: number;
        category?: string;
        project?: string;
        includeArchived: boolean;
        minScore: number;
      },
    ): SearchResult[] {
      const db = getDb();
      const queryTokens = tokenize(query);
      if (queryTokens.length === 0) return [];
    
      let memorySql = `SELECT id, content, category, project, created_at FROM memories WHERE 1=1`;
      const params: unknown[] = [];
    
      if (!options.includeArchived) {
        memorySql += ` AND archived = 0`;
      }
      if (options.category) {
        memorySql += ` AND category = ?`;
        params.push(options.category);
      }
      if (options.project) {
        memorySql += ` AND project = ?`;
        params.push(options.project);
      }
    
      const memories = db.prepare(memorySql).all(...params) as Array<{
        id: string;
        content: string;
        category: string;
        project: string | null;
        created_at: number;
      }>;
    
      let journalSql = `SELECT id, content, project, created_at FROM journal_entries WHERE 1=1`;
      const journalParams: unknown[] = [];
      if (options.project) {
        journalSql += ` AND project = ?`;
        journalParams.push(options.project);
      }
    
      const journals = db.prepare(journalSql).all(...journalParams) as Array<{
        id: string;
        content: string;
        project: string | null;
        created_at: number;
      }>;
    
      const results: SearchResult[] = [];
    
      for (const mem of memories) {
        const score = keywordScore(queryTokens, mem.content);
        if (score >= options.minScore) {
          results.push({
            id: mem.id,
            content: mem.content,
            category: mem.category,
            project: mem.project,
            score,
            created_at: mem.created_at,
            source: "memory",
          });
        }
      }
    
      for (const entry of journals) {
        const score = keywordScore(queryTokens, entry.content);
        if (score >= options.minScore) {
          results.push({
            id: entry.id,
            content: entry.content,
            category: "journal",
            project: entry.project,
            score,
            created_at: entry.created_at,
            source: "journal",
          });
        }
      }
    
      results.sort((a, b) => b.score - a.score);
    
      const now = Date.now();
      const updateAccess = db.prepare(
        `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`,
      );
      for (const r of results.slice(0, options.topK)) {
        if (r.source === "memory") {
          updateAccess.run(now, r.id);
        }
      }
    
      db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run(
        query,
        Math.min(results.length, options.topK),
        now,
      );
    
      return results.slice(0, options.topK);
    }
    
    export async function searchMemories(
      query: string,
      options: {
        topK?: number;
        category?: string;
        project?: string;
        includeArchived?: boolean;
        minScore?: number;
      } = {},
    ): Promise<SearchResult[]> {
      const {
        topK = 5,
        category,
        project,
        includeArchived = false,
        minScore = 0.3,
      } = options;
    
      // Fall back to keyword search when Ollama is unavailable
      if (isUsingFallback()) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      const db = getDb();
      const queryEmbedding = await embed(query);
    
      if (!queryEmbedding) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      let memorySql = `SELECT id, content, category, project, created_at, embedding FROM memories WHERE embedding IS NOT NULL`;
      const params: unknown[] = [];
    
      if (!includeArchived) {
        memorySql += ` AND archived = 0`;
      }
      if (category) {
        memorySql += ` AND category = ?`;
        params.push(category);
      }
      if (project) {
        memorySql += ` AND project = ?`;
        params.push(project);
      }
    
      const memories = db.prepare(memorySql).all(...params) as Array<{
        id: string;
        content: string;
        category: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      let journalSql = `SELECT id, content, project, created_at, embedding FROM journal_entries WHERE embedding IS NOT NULL`;
      const journalParams: unknown[] = [];
    
      if (project) {
        journalSql += ` AND project = ?`;
        journalParams.push(project);
      }
    
      const journals = db.prepare(journalSql).all(...journalParams) as Array<{
        id: string;
        content: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      const results: SearchResult[] = [];
    
      for (const mem of memories) {
        const memEmbedding = bufferToEmbedding(mem.embedding);
        const score = cosineSimilarity(queryEmbedding, memEmbedding);
        if (score >= minScore) {
          results.push({
            id: mem.id,
            content: mem.content,
            category: mem.category,
            project: mem.project,
            score,
            created_at: mem.created_at,
            source: "memory",
          });
        }
      }
    
      for (const entry of journals) {
        const entryEmbedding = bufferToEmbedding(entry.embedding);
        const score = cosineSimilarity(queryEmbedding, entryEmbedding);
        if (score >= minScore) {
          results.push({
            id: entry.id,
            content: entry.content,
            category: "journal",
            project: entry.project,
            score,
            created_at: entry.created_at,
            source: "journal",
          });
        }
      }
    
      results.sort((a, b) => b.score - a.score);
    
      const updateAccess = db.prepare(
        `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`,
      );
      const now = Date.now();
      for (const r of results.slice(0, topK)) {
        if (r.source === "memory") {
          updateAccess.run(now, r.id);
        }
      }
    
      db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run(
        query,
        Math.min(results.length, topK),
        now,
      );
    
      return results.slice(0, topK);
    }
  • Registration of the 'recall' tool on the MCP server with its description, input schema (query string), and handler that calls handleRecall(query) and returns text content.
    server.tool(
      "recall",
      "Unified search across ALL memory — facts, decisions, frameworks, lessons, and past conversations. Returns categorized results. Use this as the default 'ask anything about the past' tool.",
      {
        query: z.string().describe("What to recall (natural language)"),
      },
      async ({ query }) => {
        try {
          const result = await handleRecall(query);
          return { content: [{ type: "text", text: result }] };
        } catch (err) {
          return {
            content: [{ type: "text", text: `Error recalling: ${err}` }],
            isError: true,
          };
        }
      },
    );
  • The searchMemories function called by the recall handler. Performs semantic search using embeddings (falling back to keyword search if unavailable) across both memories and journal entries tables.
    export async function searchMemories(
      query: string,
      options: {
        topK?: number;
        category?: string;
        project?: string;
        includeArchived?: boolean;
        minScore?: number;
      } = {},
    ): Promise<SearchResult[]> {
      const {
        topK = 5,
        category,
        project,
        includeArchived = false,
        minScore = 0.3,
      } = options;
    
      // Fall back to keyword search when Ollama is unavailable
      if (isUsingFallback()) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      const db = getDb();
      const queryEmbedding = await embed(query);
    
      if (!queryEmbedding) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      let memorySql = `SELECT id, content, category, project, created_at, embedding FROM memories WHERE embedding IS NOT NULL`;
      const params: unknown[] = [];
    
      if (!includeArchived) {
        memorySql += ` AND archived = 0`;
      }
      if (category) {
        memorySql += ` AND category = ?`;
        params.push(category);
      }
      if (project) {
        memorySql += ` AND project = ?`;
        params.push(project);
      }
    
      const memories = db.prepare(memorySql).all(...params) as Array<{
        id: string;
        content: string;
        category: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      let journalSql = `SELECT id, content, project, created_at, embedding FROM journal_entries WHERE embedding IS NOT NULL`;
      const journalParams: unknown[] = [];
    
      if (project) {
        journalSql += ` AND project = ?`;
        journalParams.push(project);
      }
    
      const journals = db.prepare(journalSql).all(...journalParams) as Array<{
        id: string;
        content: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      const results: SearchResult[] = [];
    
      for (const mem of memories) {
        const memEmbedding = bufferToEmbedding(mem.embedding);
        const score = cosineSimilarity(queryEmbedding, memEmbedding);
        if (score >= minScore) {
          results.push({
            id: mem.id,
            content: mem.content,
            category: mem.category,
            project: mem.project,
            score,
            created_at: mem.created_at,
            source: "memory",
          });
        }
      }
    
      for (const entry of journals) {
        const entryEmbedding = bufferToEmbedding(entry.embedding);
        const score = cosineSimilarity(queryEmbedding, entryEmbedding);
        if (score >= minScore) {
          results.push({
            id: entry.id,
            content: entry.content,
            category: "journal",
            project: entry.project,
            score,
            created_at: entry.created_at,
            source: "journal",
          });
        }
      }
    
      results.sort((a, b) => b.score - a.score);
    
      const updateAccess = db.prepare(
        `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`,
      );
      const now = Date.now();
      for (const r of results.slice(0, topK)) {
        if (r.source === "memory") {
          updateAccess.run(now, r.id);
        }
      }
    
      db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run(
        query,
        Math.min(results.length, topK),
        now,
      );
    
      return results.slice(0, topK);
    }
Behavior3/5

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

No annotations are provided, so the description carries the full burden. It states 'Returns categorized results' but does not explicitly confirm it is a read-only operation or disclose any side effects. Given the tool's search nature, the lack of explicit safety cues is a minor gap.

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?

Two sentences with zero waste. Every word provides value: it names the function, scope, output type, and usage recommendation. Extremely efficient.

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

Completeness4/5

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

Given the tool's simplicity (one parameter, no output schema, no annotations), the description is sufficiently complete. It covers what the tool does, what it returns, and when to use it. Minor improvement could mention if it's read-only, but not essential.

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 baseline is 3. The description adds little beyond the schema's 'natural language' for the query parameter, but it does imply the query is open-ended. No additional constraints or format details are needed.

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?

The description clearly states it's a unified search across all memory types (facts, decisions, etc.) and returns categorized results. It distinguishes itself as the default 'ask anything about the past' tool, differentiating from siblings like memory_journal or memory_recent.

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

Usage Guidelines4/5

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

It explicitly says 'Use this as the default' which gives a clear when-to-use signal. However, it does not explicitly state when not to use it or provide specific alternatives for narrower searches (e.g., memory_recent for recent items), leaving some room for ambiguity.

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/DomDemetz/claude-soul'

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