Skip to main content
Glama
jordanburke

joplin-mcp-server

read_multinote

Retrieve and read the complete content of multiple notes simultaneously using note IDs from the Joplin MCP Server for efficient data access.

Instructions

Read the full content of multiple notes at once

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
note_idsYesArray of note IDs to read

Implementation Reference

  • The ReadMultiNote class extending BaseTool. The `call` method implements the core tool logic: validates note IDs, fetches each note and its parent notebook from Joplin API, formats metadata (title, location, todo status, timestamps), note body in Markdown, handles not found/errors, and returns a structured summary.
    class ReadMultiNote extends BaseTool {
      async call(noteIds: string[]): Promise<string> {
        if (!noteIds || !Array.isArray(noteIds) || noteIds.length === 0) {
          return 'Please provide an array of note IDs. Example: read_multinote note_ids=["id1", "id2", "id3"]'
        }
    
        // Validate that all IDs look like valid note IDs
        const invalidIds = noteIds.filter((id) => !id || id.length < 10 || !id.match(/[a-f0-9]/i))
        if (invalidIds.length > 0) {
          return `Error: Some IDs do not appear to be valid note IDs: ${invalidIds.join(", ")}\n\nNote IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse search_notes to find notes and their IDs.`
        }
    
        const resultLines: string[] = []
        const notFound: string[] = []
        const errors: string[] = []
        const successful: string[] = []
    
        // Add a header
        resultLines.push(`# Reading ${noteIds.length} notes\n`)
    
        // Process each note ID
        for (let i = 0; i < noteIds.length; i++) {
          const noteId = noteIds[i]
          resultLines.push(`## Note ${i + 1} of ${noteIds.length} (ID: ${noteId})\n`)
    
          try {
            // Get the note details with all relevant fields
            const note = await this.apiClient.get<JoplinNote>(`/notes/${noteId}`, {
              query: {
                fields: "id,title,body,parent_id,created_time,updated_time,is_todo,todo_completed,todo_due",
              },
            })
    
            // Validate note response
            if (!note || typeof note !== "object" || !note.id) {
              errors.push(noteId)
              resultLines.push(`Error: Unexpected response format from Joplin API when fetching note ${noteId}\n`)
              continue
            }
    
            successful.push(noteId)
    
            // Get the notebook info to show where this note is located
            let notebookInfo = "Unknown notebook"
            if (note.parent_id) {
              try {
                const notebook = await this.apiClient.get<JoplinFolder>(`/folders/${note.parent_id}`, {
                  query: { fields: "id,title" },
                })
                if (notebook && notebook.title) {
                  notebookInfo = `"${notebook.title}" (notebook_id: "${note.parent_id}")`
                }
              } catch (err: unknown) {
                process.stderr.write(`Error fetching notebook info for note ${noteId}: ${err}\n`)
                // Continue even if we can't get the notebook info
              }
            }
    
            // Add note metadata
            resultLines.push(`### Note: "${note.title}"`)
            resultLines.push(`Notebook: ${notebookInfo}`)
    
            // Add todo status if applicable
            if (note.is_todo) {
              const status = note.todo_completed ? "Completed" : "Not completed"
              resultLines.push(`Status: ${status}`)
    
              if (note.todo_due) {
                const dueDate = this.formatDate(note.todo_due)
                resultLines.push(`Due: ${dueDate}`)
              }
            }
    
            // Add timestamps
            const createdDate = this.formatDate(note.created_time)
            const updatedDate = this.formatDate(note.updated_time)
            resultLines.push(`Created: ${createdDate}`)
            resultLines.push(`Updated: ${updatedDate}`)
    
            // Add a separator before the note content
            resultLines.push("\n---\n")
    
            // Add the note body
            if (note.body) {
              resultLines.push(note.body)
            } else {
              resultLines.push("(This note has no content)")
            }
    
            // Add a separator after the note
            resultLines.push("\n---\n")
          } catch (error: any) {
            process.stderr.write(`Error reading note ${noteId}: ${error}\n`)
            if (error.response && error.response.status === 404) {
              notFound.push(noteId)
              resultLines.push(`Note with ID "${noteId}" not found.\n`)
            } else {
              errors.push(noteId)
              resultLines.push(`Error reading note: ${error.message || "Unknown error"}\n`)
            }
          }
        }
    
        // Add a summary at the end
        resultLines.push("# Summary")
        resultLines.push(`Total notes requested: ${noteIds.length}`)
        resultLines.push(`Successfully retrieved: ${successful.length}`)
    
        if (notFound.length > 0) {
          resultLines.push(`Notes not found: ${notFound.length}`)
          resultLines.push(`IDs not found: ${notFound.join(", ")}`)
        }
    
        if (errors.length > 0) {
          resultLines.push(`Errors encountered: ${errors.length}`)
          resultLines.push(`IDs with errors: ${errors.join(", ")}`)
        }
    
        return resultLines.join("\n")
      }
    }
    
    export default ReadMultiNote
  • Zod input schema for read_multinote tool: requires `note_ids` as array of strings.
    parameters: z.object({
      note_ids: z.array(z.string()).describe("Array of note IDs to read"),
    }),
  • Registration of the read_multinote tool in the FastMCP server using server.addTool, including name, description, schema, and execute handler delegating to manager.
    server.addTool({
      name: "read_multinote",
      description: "Read the full content of multiple notes at once",
      parameters: z.object({
        note_ids: z.array(z.string()).describe("Array of note IDs to read"),
      }),
      execute: async (args) => {
        return await manager.readMultiNote(args.note_ids)
      },
    })
  • JSON input schema for read_multinote tool in stdio MCP server listTools response.
    inputSchema: {
      type: "object",
      properties: {
        note_ids: { type: "array", items: { type: "string" }, description: "Array of note IDs to read" },
      },
      required: ["note_ids"],
    },
  • src/index.ts:245-248 (registration)
    Tool dispatch/registration in stdio server CallToolRequest handler switch statement, delegating to manager.readMultiNote.
    case "read_multinote": {
      const multiResult = await manager.readMultiNote(args.note_ids as string[])
      return { content: [{ type: "text", text: multiResult }], isError: false }
    }
Behavior2/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 states the tool reads content but doesn't specify permissions needed, rate limits, error handling, or what happens if some note IDs are invalid. This leaves significant gaps for a tool that interacts with multiple resources.

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 a single, efficient sentence that directly states the tool's function without unnecessary words. It is front-loaded with the core action and resource, making it easy to parse quickly.

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

Completeness2/5

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

Given the lack of annotations and output schema, the description is insufficient for a tool that reads multiple notes. It doesn't explain the return format, how results are structured, or error scenarios, leaving the agent with incomplete operational context.

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?

The schema description coverage is 100%, with the parameter 'note_ids' fully documented in the schema. The description adds no additional semantic context beyond implying multiple notes, which is already covered. This meets the baseline for high schema coverage.

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?

The description clearly states the action ('read the full content') and resource ('multiple notes at once'), making the purpose immediately understandable. It distinguishes from the sibling 'read_note' by specifying multiple notes, though it doesn't explicitly contrast with other tools like 'search_notes' or 'list_notebooks'.

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

Usage Guidelines2/5

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

No guidance is provided on when to use this tool versus alternatives. The description doesn't mention prerequisites, when to choose this over 'read_note' for single notes, or how it relates to 'search_notes' for finding notes. Usage is implied by the name but not explicitly stated.

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

Related 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/jordanburke/joplin-mcp-server'

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