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
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | Dynalist URL | |
| file_id | No | Document ID (alternative to URL) | |
| query | Yes | Text to search for (case-insensitive) | |
| search_notes | No | Also search in notes | |
| parent_levels | No | How many parent levels to include (0 = none, 1 = direct parent, 2+ = ancestors) | |
| include_children | No | Include direct children (level 1) of each match | |
| bypass_warning | No | ONLY use after receiving a size warning. Do NOT set true on first request. |
Implementation Reference
- src/tools/index.ts:522-608 (handler)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, }, ], }; }
- src/tools/index.ts:513-521 (schema)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."), },
- src/tools/index.ts:510-609 (registration)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, }, ], }; } );
- src/tools/index.ts:67-89 (helper)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; }
- src/tools/index.ts:23-61 (helper)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 }; }