Skip to main content
Glama

search

Find documents and records in DEVONthink databases using flexible search parameters including query terms, group scoping, record type filters, and comparison modes.

Instructions

Search DEVONthink records. Examples: {"query": "invoice"} or {"query": "project review", "groupPath": "/Meetings", "databaseName": "MyDB"}. Note: groupPath requires databaseName and must be database-relative (e.g., "/Meetings" not "/MyDB/Meetings").

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch query (wildcards * and ? are supported)
groupUuidNoUUID of the group to search within
groupIdNoNumeric ID of the group to search within (requires databaseName)
groupPathNoDatabase-relative path of the group to search within (requires databaseName)
databaseNameNoDatabase name (required for groupId or groupPath scoping)
useCurrentGroupNoScope search to the currently selected group in DEVONthink
recordTypeNoFilter results to a specific record type
comparisonNoComparison mode for the search
excludeSubgroupsNoExclude records from subgroups when scoping to a group
limitNoMaximum number of results to return

Implementation Reference

  • The search tool is defined and implemented here, including its input schema (zod validation) and the run handler which executes JXA script for DEVONthink.
    export const searchTool = defineTool({
      name: "search",
      description:
        'Search DEVONthink records. Examples: {"query": "invoice"} or ' +
        '{"query": "project review", "groupPath": "/Meetings", "databaseName": "MyDB"}. ' +
        "Note: groupPath requires databaseName and must be database-relative " +
        '(e.g., "/Meetings" not "/MyDB/Meetings").',
      schema: z.object({
        query: z.string().describe("Search query (wildcards * and ? are supported)"),
        groupUuid: z
          .string()
          .optional()
          .describe("UUID of the group to search within"),
        groupId: z
          .number()
          .int()
          .nonnegative()
          .optional()
          .describe("Numeric ID of the group to search within (requires databaseName)"),
        groupPath: z
          .string()
          .optional()
          .describe(
            "Database-relative path of the group to search within (requires databaseName)"
          ),
        databaseName: z
          .string()
          .optional()
          .describe("Database name (required for groupId or groupPath scoping)"),
        useCurrentGroup: z
          .boolean()
          .optional()
          .describe("Scope search to the currently selected group in DEVONthink"),
        recordType: z
          .enum(RECORD_TYPE_VALUES)
          .optional()
          .describe("Filter results to a specific record type"),
        comparison: z
          .enum(COMPARISON_VALUES)
          .optional()
          .describe("Comparison mode for the search"),
        excludeSubgroups: z
          .boolean()
          .optional()
          .describe("Exclude records from subgroups when scoping to a group"),
        limit: z
          .number()
          .int()
          .positive()
          .optional()
          .describe("Maximum number of results to return"),
      }),
      run: async (args, executor) => {
        const {
          query,
          groupUuid,
          groupId,
          groupPath,
          databaseName,
          useCurrentGroup,
          recordType,
          comparison,
          excludeSubgroups,
          limit,
        } = args;
    
        const script = `
          ${JXA_APP}
          var query = ${jxaLiteral(query)};
          var groupUuid = ${jxaLiteral(groupUuid ?? null)};
          var groupId = ${jxaLiteral(groupId ?? null)};
          var groupPath = ${jxaLiteral(groupPath ?? null)};
          var dbName = ${jxaLiteral(databaseName ?? null)};
          var useCurrentGroup = ${jxaLiteral(useCurrentGroup ?? false)};
          var recordType = ${jxaLiteral(recordType ?? null)};
          var comparison = ${jxaLiteral(comparison ?? null)};
          var excludeSubgroups = ${jxaLiteral(excludeSubgroups ?? false)};
          var limit = ${jxaLiteral(limit ?? null)};
    
          ${JXA_RESOLVE_DB}
    
          // Resolve the search scope group if specified
          var searchIn = null;
          if (groupUuid) {
            searchIn = app.getRecordWithUuid(groupUuid);
            if (!searchIn || !searchIn.uuid()) throw new Error("Group not found for UUID: " + groupUuid);
          } else if (typeof groupId === "number" && groupId >= 0) {
            if (!db) throw new Error("databaseName is required when using groupId");
            searchIn = db.getRecordAt(groupId);
            if (!searchIn || !searchIn.uuid()) throw new Error("Group not found for ID: " + groupId);
          } else if (groupPath) {
            if (!db) throw new Error("databaseName is required when using groupPath");
            searchIn = app.getRecordAt(groupPath, {in: db});
            if (!searchIn || !searchIn.uuid()) throw new Error("Group not found at path: " + groupPath);
          } else if (useCurrentGroup) {
            searchIn = app.currentGroup();
          }
    
          // Build search options
          var searchOpts = {};
          if (searchIn) searchOpts["in"] = searchIn;
          else if (db) searchOpts["in"] = db.root();
          if (comparison) searchOpts["comparison"] = comparison;
          if (excludeSubgroups) searchOpts["excludeSubgroups"] = true;
    
          // Execute search
          var results = app.search(query, searchOpts);
    
          // Filter by record type if specified
          if (recordType) {
            results = results.filter(function(r) { return r.type() === recordType; });
          }
    
          // Apply limit
          if (limit && limit > 0 && results.length > limit) {
            results = results.slice(0, limit);
          }
    
          // Map to summaries
          var summaries = results.map(function(record) {
            return ${JXA_RECORD_SUMMARY};
          });
    
          JSON.stringify(summaries);
        `;
    
        const result = executor.run(script);
        return JSON.parse(result.stdout);
      },
    });
Behavior3/5

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

No annotations provided, so description carries full burden. It discloses the behavioral constraint that groupPath must be database-relative and requires databaseName. However, it omits safety disclosures (read-only vs destructive), return format details, or pagination behavior expected for a search tool.

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

Conciseness4/5

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

Well-structured with purpose upfront, followed by examples and a constraint note. The examples are verbose but necessary for clarity. No redundant or wasted sentences.

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

Completeness3/5

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

Given the rich schema (100% coverage, 10 parameters), the description adequately covers invocation. However, with no output schema provided, the description should ideally explain what search results are returned (e.g., record metadata, content snippets, UUIDs).

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?

Input schema has 100% description coverage, establishing a baseline of 3. The description adds syntactic examples showing how parameters interact (groupPath with databaseName), but does not add semantic meaning beyond what the schema already provides for individual fields.

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

Purpose4/5

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

States specific verb (Search) and resource (DEVONthink records) clearly. Examples reinforce the querying capability. However, it does not explicitly differentiate from siblings like 'lookup_record' or 'get_record_by_identifier' which also retrieve records.

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

Usage Guidelines3/5

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

Provides concrete JSON examples showing parameter combinations. Includes a specific constraint note that groupPath requires databaseName. However, lacks explicit guidance on when to use this versus 'lookup_record', 'list_group_content', or 'selected_records'.

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/mnott/Devon'

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