Skip to main content
Glama
tan-yong-sheng

TriliumNext Notes' MCP Server

resolve_note_id

Convert note or folder names into their corresponding IDs for use with other tools in the TriliumNext Notes system, enabling accurate identification when users reference notes by title rather than ID.

Instructions

Resolves a note/folder name to its actual note ID for use with other tools. You MUST call this function when users provide note names instead of note IDs (e.g., 'wqd7006', 'My Project') UNLESS the user explicitly provides a note ID. Simple title-based search with user choice when multiple matches found.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
noteNameYesName or title of the note to find (e.g., 'wqd7006', 'My Project Folder')
exactMatchNoWhether to require exact title match. RECOMMENDED: Use false (default) for best user experience - fuzzy search finds partial matches and handles typos, while still prioritizing exact matches when found. Only set to true when user explicitly requests exact matching.
maxResultsNoMaximum number of results to return in topMatches array (default: 10)
autoSelectNoWhen multiple matches found: true = auto-select best match (current behavior), false = stop and ask user to choose from alternatives (default: false for better user experience)

Implementation Reference

  • Core handler function that implements the resolve_note_id tool logic: performs title-based search, handles multiple matches with prioritization (exact, folders, recent), auto-selects or requires user choice.
    export async function handleResolveNoteId(
      args: ResolveNoteOperation,
      axiosInstance: any
    ): Promise<ResolveNoteResponse> {
      const { noteName, exactMatch = false, maxResults = 3, autoSelect = false } = args;
    
      // Verbose logging
      logVerboseInput("resolve_note_id", args);
    
      if (!noteName?.trim()) {
        throw new Error("Note name must be provided");
      }
    
      // Simple title-based search criteria
      const searchCriteria: any[] = [{
        property: "title",
        type: "noteProperty",
        op: exactMatch ? "=" : "contains",
        value: noteName.trim()
      }];
    
      // Build search query from simple criteria
      const searchParams: SearchOperation = {
        searchCriteria
      };
    
      logVerboseInput("resolve_note_id", searchParams);
    
      // Search for notes matching the criteria
      const query = buildSearchQuery(searchParams);
      const params = new URLSearchParams();
      params.append("search", query);
      params.append("fastSearch", "false");
      params.append("includeArchivedNotes", "true");
    
      const response = await axiosInstance.get(`/notes?${params.toString()}`);
      let searchResults = response.data.results || [];
    
      const totalMatches = searchResults.length;
    
      if (searchResults.length === 0) {
        // Enhanced fallback suggestions when no matches found
        let nextSteps = "No notes found matching the search criteria.";
    
        // Primary fallback: suggest broader search using search_notes
        nextSteps += ` Consider using search_notes for broader results: search_notes(text: "${noteName.trim()}") to find notes containing "${noteName.trim()}" in title or content.`;
    
        return {
          noteId: null,
          title: null,
          found: false,
          matches: 0,
          nextSteps
        };
      }
    
      // Store original results for top matches
      const originalResults = [...searchResults];
    
      // Check if user choice is required (multiple matches and autoSelect is false)
      if (totalMatches > 1 && !autoSelect) {
        // Simple prioritization: exact matches → folders → most recent
        const topMatches = originalResults
          .sort((a: any, b: any) => {
            // First priority: exact title matches
            const aExact = a.title && a.title.toLowerCase() === noteName.toLowerCase();
            const bExact = b.title && b.title.toLowerCase() === noteName.toLowerCase();
            if (aExact && !bExact) return -1;
            if (!aExact && bExact) return 1;
    
            // Second priority: book type (folders)
            if (a.type === 'book' && b.type !== 'book') return -1;
            if (a.type !== 'book' && b.type === 'book') return 1;
    
            // Final priority: most recent
            return new Date(b.dateModified).getTime() - new Date(a.dateModified).getTime();
          })
          .slice(0, maxResults)
          .map((note: any) => ({
            noteId: note.noteId,
            title: note.title,
            type: note.type,
            dateModified: note.dateModified
          }));
    
        return {
          noteId: null,
          title: null,
          found: true,
          matches: totalMatches,
          requiresUserChoice: true,
          topMatches
        };
      }
    
      // Apply simple prioritization for selecting the best match
      if (searchResults.length > 1) {
        searchResults.sort((a: any, b: any) => {
          // First priority: exact title matches (if not exactMatch mode, prefer exact matches)
          if (!exactMatch) {
            const aExact = a.title && a.title.toLowerCase() === noteName.toLowerCase();
            const bExact = b.title && b.title.toLowerCase() === noteName.toLowerCase();
            if (aExact && !bExact) return -1;
            if (!aExact && bExact) return 1;
          }
    
          // Second priority: book type (folders)
          if (a.type === 'book' && b.type !== 'book') return -1;
          if (a.type !== 'book' && b.type === 'book') return 1;
    
          // Final priority: most recent
          return new Date(b.dateModified).getTime() - new Date(a.dateModified).getTime();
        });
      }
    
      // Return the first match (or best match if multiple)
      const selectedNote = searchResults[0];
    
      // Prepare top N matches for display (from original results, with simple prioritization)
      const topMatches = originalResults
        .sort((a: any, b: any) => {
          // Simple prioritization: exact matches → folders → most recent
          const aExact = a.title && a.title.toLowerCase() === noteName.toLowerCase();
          const bExact = b.title && b.title.toLowerCase() === noteName.toLowerCase();
          if (aExact && !bExact) return -1;
          if (!aExact && bExact) return 1;
    
          if (a.type === 'book' && b.type !== 'book') return -1;
          if (a.type !== 'book' && b.type === 'book') return 1;
    
          return new Date(b.dateModified).getTime() - new Date(a.dateModified).getTime();
        })
        .slice(0, maxResults)
        .map((note: any) => ({
          noteId: note.noteId,
          title: note.title,
          type: note.type,
          dateModified: note.dateModified
        }));
    
      return {
        noteId: selectedNote.noteId,
        title: selectedNote.title,
        found: true,
        matches: totalMatches,
        topMatches
      };
    }
  • Tool request handler for resolve_note_id: validates READ permission, calls core handleResolveNoteId, formats responses including user choice prompts for multiple matches.
    export async function handleResolveNoteRequest(args: any, permissionChecker: any, axiosInstance: any): Promise<any> {
      // Permission check
      if (!permissionChecker.hasPermission("READ")) {
        throw new Error("READ permission required for resolve_note_id operation");
      }
    
      const resolveOperation: ResolveNoteOperation = {
        noteName: args.noteName,
        exactMatch: args.exactMatch,
        maxResults: args.maxResults,
        autoSelect: args.autoSelect
      };
    
      const response = await handleResolveNoteId(resolveOperation, axiosInstance);
    
      // Enhanced response formatting for multiple matches
      if (response.requiresUserChoice && response.topMatches) {
        const choiceList = response.topMatches
          .map((match, index) => `${index + 1}. ${match.title} (ID: ${match.noteId}, Type: ${match.type}, Modified: ${match.dateModified})`)
          .join('\n');
    
        return {
          content: [
            {
              type: "text",
              text: `Found ${response.matches} matches for "${resolveOperation.noteName}". Please choose:\n\n${choiceList}\n\nTo select: Use the note ID directly, or specify autoSelect=true for automatic selection, or refine your search criteria.`
            }
          ]
        };
      }
    
      // Standard response for single match or auto-selected
      if (response.found && response.noteId) {
        return {
          content: [
            {
              type: "text",
              text: `✅ Resolved "${resolveOperation.noteName}" to Note ID: ${response.noteId} (Title: "${response.title}", Matches: ${response.matches})`
            }
          ]
        };
      }
    
      // No matches found
      return {
        content: [
          {
            type: "text",
            text: `❌ No notes found matching "${resolveOperation.noteName}". ${response.nextSteps || "Try a different search term or check spelling."}`
          }
        ]
      };
    }
  • Input schema and description for the resolve_note_id tool, including parameters like noteName, exactMatch, maxResults, autoSelect.
      name: "resolve_note_id",
      description: "Resolves a note/folder name to its actual note ID for use with other tools. You MUST call this function when users provide note names instead of note IDs (e.g., 'wqd7006', 'My Project') UNLESS the user explicitly provides a note ID. Simple title-based search with user choice when multiple matches found.",
      inputSchema: {
        type: "object",
        properties: {
          noteName: {
            type: "string",
            description: "Name or title of the note to find (e.g., 'wqd7006', 'My Project Folder')",
          },
          exactMatch: {
            type: "boolean",
            description: "Whether to require exact title match. RECOMMENDED: Use false (default) for best user experience - fuzzy search finds partial matches and handles typos, while still prioritizing exact matches when found. Only set to true when user explicitly requests exact matching.",
            default: false
          },
          maxResults: {
            type: "number",
            description: "Maximum number of results to return in topMatches array (default: 10)",
            default: 10,
            minimum: 1,
            maximum: 10
          },
          autoSelect: {
            type: "boolean",
            description: "When multiple matches found: true = auto-select best match (current behavior), false = stop and ask user to choose from alternatives (default: false for better user experience)",
            default: false
          }
        },
        required: ["noteName"],
      },
    },
  • src/index.ts:112-113 (registration)
    MCP server router registration: dispatches resolve_note_id tool calls to the handleResolveNoteRequest function.
    case "resolve_note_id":
      return await handleResolveNoteRequest(request.params.arguments, this, this.axiosInstance);
  • TypeScript interfaces defining input (ResolveNoteOperation) and output (ResolveNoteResponse) for the resolve_note_id handler.
    export interface ResolveNoteOperation {
      noteName: string;
      exactMatch?: boolean;
      maxResults?: number;
      autoSelect?: boolean;
    }
    
    export interface ResolveNoteResponse {
      noteId: string | null;
      title: string | null;
      found: boolean;
      matches: number;
      requiresUserChoice?: boolean;
      topMatches?: Array<{
        noteId: string;
        title: string;
        type: string;
        dateModified: string;
      }>;
      nextSteps?: string;
    }
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes key behaviors: 'Simple title-based search with user choice when multiple matches found' and mentions fuzzy search capabilities in the parameter context. However, it doesn't address potential failure modes or error conditions.

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?

The description is efficiently structured in two sentences: the first states the core purpose, the second provides usage rules and behavioral context. Every element serves a clear purpose with no wasted words.

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?

For a lookup tool with no annotations and no output schema, the description provides good context about purpose, usage rules, and basic behavior. However, without an output schema, it doesn't describe what format the resolved ID will be returned in or what happens when no matches are found.

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 description coverage is 100%, so the schema already documents all parameters thoroughly. The description doesn't add significant parameter semantics beyond what's in the schema, though it reinforces the noteName parameter's purpose. Baseline 3 is appropriate when schema coverage is complete.

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 the specific purpose: 'Resolves a note/folder name to its actual note ID for use with other tools.' It distinguishes this from sibling tools like get_note or search_notes by emphasizing the ID resolution function rather than content retrieval or general search.

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?

Explicit guidance is provided: 'You MUST call this function when users provide note names instead of note IDs... UNLESS the user explicitly provides a note ID.' This creates clear when-to-use rules and distinguishes it from tools that work directly with IDs.

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/tan-yong-sheng/triliumnext-mcp'

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