Skip to main content
Glama

search_in_document

Find specific text within Dynalist documents, returning matching nodes with optional parent context and children for comprehensive search results.

Instructions

Search for text in a Dynalist document. Returns matching nodes with optional parent context and children. WARNING: Many matches with parents/children can return many words.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlNoDynalist URL
file_idNoDocument ID (alternative to URL)
queryYesText to search for (case-insensitive)
search_notesNoAlso search in notes
parent_levelsNoHow many parent levels to include (0 = none, 1 = direct parent, 2+ = ancestors)
include_childrenNoInclude direct children (level 1) of each match
bypass_warningNoONLY use after receiving a size warning. Do NOT set true on first request.

Implementation Reference

  • Main handler function: parses input, fetches document, filters nodes matching query in content or notes, adds optional parent/children context, checks result size with warning, returns JSON array of matches.
    async ({ url, file_id, query, search_notes, parent_levels, include_children, bypass_warning }) => {
      let documentId = file_id;
    
      if (url) {
        const parsed = parseDynalistUrl(url);
        documentId = parsed.documentId;
      }
    
      if (!documentId) {
        return {
          content: [{ type: "text", text: "Error: Either 'url' or 'file_id' must be provided" }],
          isError: true,
        };
      }
    
      const doc = await client.readDocument(documentId);
      const nodeMap = buildNodeMap(doc.nodes);
      const queryLower = query.toLowerCase();
    
      const matches = doc.nodes
        .filter((node) => {
          const contentMatch = node.content?.toLowerCase().includes(queryLower);
          const noteMatch = search_notes && node.note?.toLowerCase().includes(queryLower);
          return contentMatch || noteMatch;
        })
        .map((node) => {
          const result: {
            id: string;
            content: string;
            note?: string;
            url: string;
            parents?: { id: string; content: string }[];
            children?: { id: string; content: string }[];
          } = {
            id: node.id,
            content: node.content,
            note: node.note || undefined,
            url: buildDynalistUrl(documentId!, node.id),
          };
    
          // Add parents if requested
          if (parent_levels > 0) {
            const parents = getAncestors(doc.nodes, node.id, parent_levels);
            if (parents.length > 0) {
              result.parents = parents;
            }
          }
    
          // Add children if requested
          if (include_children && node.children && node.children.length > 0) {
            result.children = node.children
              .map(childId => {
                const childNode = nodeMap.get(childId);
                return childNode ? { id: childNode.id, content: childNode.content } : null;
              })
              .filter((c): c is { id: string; content: string } => c !== null);
          }
    
          return result;
        });
    
      const resultText = matches.length > 0
        ? JSON.stringify(matches, null, 2)
        : `No matches found for "${query}"`;
    
      // Check content size
      const sizeCheck = checkContentSize(resultText, bypass_warning || false, [
        "Use a more specific query to reduce matches",
        "Use parent_levels: 0 to exclude parent context",
        "Use include_children: false to exclude children",
      ]);
    
      if (sizeCheck) {
        return {
          content: [{ type: "text", text: sizeCheck.warning }],
        };
      }
    
      return {
        content: [
          {
            type: "text",
            text: resultText,
          },
        ],
      };
    }
  • Zod input schema defining parameters for the search_in_document tool.
    {
      url: z.string().optional().describe("Dynalist URL"),
      file_id: z.string().optional().describe("Document ID (alternative to URL)"),
      query: z.string().describe("Text to search for (case-insensitive)"),
      search_notes: z.boolean().optional().default(true).describe("Also search in notes"),
      parent_levels: z.number().optional().default(1).describe("How many parent levels to include (0 = none, 1 = direct parent, 2+ = ancestors)"),
      include_children: z.boolean().optional().default(false).describe("Include direct children (level 1) of each match"),
      bypass_warning: z.boolean().optional().default(false).describe("ONLY use after receiving a size warning. Do NOT set true on first request."),
    },
  • MCP server.tool registration call for search_in_document, including name, description, schema, and handler function.
    server.tool(
      "search_in_document",
      "Search for text in a Dynalist document. Returns matching nodes with optional parent context and children. WARNING: Many matches with parents/children can return many words.",
      {
        url: z.string().optional().describe("Dynalist URL"),
        file_id: z.string().optional().describe("Document ID (alternative to URL)"),
        query: z.string().describe("Text to search for (case-insensitive)"),
        search_notes: z.boolean().optional().default(true).describe("Also search in notes"),
        parent_levels: z.number().optional().default(1).describe("How many parent levels to include (0 = none, 1 = direct parent, 2+ = ancestors)"),
        include_children: z.boolean().optional().default(false).describe("Include direct children (level 1) of each match"),
        bypass_warning: z.boolean().optional().default(false).describe("ONLY use after receiving a size warning. Do NOT set true on first request."),
      },
      async ({ url, file_id, query, search_notes, parent_levels, include_children, bypass_warning }) => {
        let documentId = file_id;
    
        if (url) {
          const parsed = parseDynalistUrl(url);
          documentId = parsed.documentId;
        }
    
        if (!documentId) {
          return {
            content: [{ type: "text", text: "Error: Either 'url' or 'file_id' must be provided" }],
            isError: true,
          };
        }
    
        const doc = await client.readDocument(documentId);
        const nodeMap = buildNodeMap(doc.nodes);
        const queryLower = query.toLowerCase();
    
        const matches = doc.nodes
          .filter((node) => {
            const contentMatch = node.content?.toLowerCase().includes(queryLower);
            const noteMatch = search_notes && node.note?.toLowerCase().includes(queryLower);
            return contentMatch || noteMatch;
          })
          .map((node) => {
            const result: {
              id: string;
              content: string;
              note?: string;
              url: string;
              parents?: { id: string; content: string }[];
              children?: { id: string; content: string }[];
            } = {
              id: node.id,
              content: node.content,
              note: node.note || undefined,
              url: buildDynalistUrl(documentId!, node.id),
            };
    
            // Add parents if requested
            if (parent_levels > 0) {
              const parents = getAncestors(doc.nodes, node.id, parent_levels);
              if (parents.length > 0) {
                result.parents = parents;
              }
            }
    
            // Add children if requested
            if (include_children && node.children && node.children.length > 0) {
              result.children = node.children
                .map(childId => {
                  const childNode = nodeMap.get(childId);
                  return childNode ? { id: childNode.id, content: childNode.content } : null;
                })
                .filter((c): c is { id: string; content: string } => c !== null);
            }
    
            return result;
          });
    
        const resultText = matches.length > 0
          ? JSON.stringify(matches, null, 2)
          : `No matches found for "${query}"`;
    
        // Check content size
        const sizeCheck = checkContentSize(resultText, bypass_warning || false, [
          "Use a more specific query to reduce matches",
          "Use parent_levels: 0 to exclude parent context",
          "Use include_children: false to exclude children",
        ]);
    
        if (sizeCheck) {
          return {
            content: [{ type: "text", text: sizeCheck.warning }],
          };
        }
    
        return {
          content: [
            {
              type: "text",
              text: resultText,
            },
          ],
        };
      }
    );
  • Helper function to retrieve ancestor (parent) nodes for providing context around search matches.
    function getAncestors(
      nodes: import("../dynalist-client.js").DynalistNode[],
      nodeId: string,
      levels: number
    ): { id: string; content: string }[] {
      if (levels <= 0) return [];
    
      const ancestors: { id: string; content: string }[] = [];
      let currentId = nodeId;
    
      for (let i = 0; i < levels; i++) {
        const parentInfo = findNodeParent(nodes, currentId);
        if (!parentInfo) break; // Reached root or node not found
    
        const parentNode = nodes.find(n => n.id === parentInfo.parentId);
        if (!parentNode) break;
    
        ancestors.push({ id: parentNode.id, content: parentNode.content });
        currentId = parentNode.id;
      }
    
      return ancestors;
    }
  • Helper to check result size and issue warnings or block large outputs, used before returning search results.
    function checkContentSize(
      content: string,
      bypassWarning: boolean,
      recommendations: string[]
    ): { warning: string; canBypass: boolean } | null {
      const tokenCount = estimateTokens(content);
    
      // If bypass was used preemptively (result is small), warn against this practice
      if (bypassWarning && tokenCount <= 5000) {
        return {
          warning: `⚠️ INCORRECT USAGE: You used bypass_warning: true preemptively.\n\n` +
            `The bypass_warning option should ONLY be used AFTER receiving a size warning, ` +
            `not on the first request. Please repeat the request WITHOUT bypass_warning to get the result.\n\n` +
            `This ensures you're aware of large results before they fill your context.`,
          canBypass: false,
        };
      }
    
      if (tokenCount <= 5000 || bypassWarning) {
        return null; // OK to return content
      }
    
      const canBypass = tokenCount <= 24500;
    
      let warning = `⚠️ LARGE RESULT WARNING\n`;
      warning += `This query would return ~${tokenCount.toLocaleString()} tokens which may fill your context.\n\n`;
      warning += `Recommendations:\n`;
      for (const rec of recommendations) {
        warning += `- ${rec}\n`;
      }
    
      if (canBypass) {
        warning += `\nTo receive the full result anyway (~${tokenCount.toLocaleString()} tokens), repeat the SAME request with bypass_warning: true`;
      } else {
        warning += `\n❌ Result too large (>${(24500).toLocaleString()} tokens). Please reduce the scope using the recommendations above.`;
      }
    
      return { warning, canBypass };
    }

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/cristip73/dynalist-mcp'

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