esc

Tools for AI apps

An add-on can offer actions to Claude, Cursor, Codex and other MCP apps, through the server Ledge runs: “search my notes”, “add a note”. Nothing reaches it until the person turns AI apps on, lets the app in and turns the add-on's tools on. Needs LedgeKit 0.16.0.

Offering tools

Add "ai-tool" to surfaces in addon.json, set kit.minAPI to 0.16.0, send the list once the add-on is ready, and answer calls in onTool:

AddOnSession.show(tools: [
    AddOnTool("search_notes", title: "Search notes",
              summary: "Finds notes containing every word of the query, newest first, and returns each note's id, title and preview.",
              arguments: [.init("query", summary: "Words to look for.", kind: .text(maximumLength: 200), required: false)]),
    AddOnTool("add_note", title: "Add a note", summary: "Adds a note with the given Markdown text and returns its id.",
              arguments: [.init("text", summary: "The note, in Markdown.", kind: .text(maximumLength: 20_000))]),
])

handlers.onTool = { call in
    switch call.tool {
    case "add_note":
        let id = try await notes.add(call.text("text") ?? "")
        return .json(["id": .string(id)])
    default:
        throw AddOnError("Notes has no tool named \(call.tool).")
    }
}

An app sees each tool as <add-on id>__<name>, like notes__add_note, with your add-on's name in its description. Return .text for words or .json for data; throw AddOnError with a sentence the app can act on to refuse.

Arguments, and why they're checked twice

KindHolds
.text(maximumLength:)Text up to a length
.number(minimum:maximum:whole:)A number, whole if asked, within bounds
.booleanYes or no
.choice([…])One of a list

Arguments come from a model, which may have read them in a web page or a file. Ledge checks every call against the tool's arguments before your add-on sees it: unknown arguments, missing ones, the wrong kind, too long or out of range are refused. Check them again for meaning all the same: an id that becomes a file name must be one you know, and never run or open anything an argument names.

Destructive tools

Mark a tool destructive: true if it deletes, overwrites or sends anything that can't be taken back. Ledge then asks the person in the notch before every call, showing the app, the tool and its arguments; closing the notch or no answer in 30 seconds is no. ledgekit lint warns about a tool whose name sounds destructive but isn't marked.

Limits

WhatLimit
Tools per add-on16
A tool's nameA lowercase letter, then letters, digits or _, up to 32
Title and summaryUp to 60 and 300 characters
ArgumentsUp to 10 per tool
A result64 KB
A call30 seconds, then the app is told it took too long

Testing

A check script can call a tool the way an app would, with its arguments checked as Ledge checks them. The report's tools has each result or refusal:

[{"wait": 1}, {"tool": "add_note", "arguments": {"text": "# Shopping\nOat milk"}}, {"tool": "search_notes", "arguments": {"query": "oat"}}]