src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { existsSync } from "fs";
import { mkdir, writeFile } from "fs/promises";
import { isAbsolute, join } from "path";
import { configSchematics } from "./configSchematics";
import { McpStdioClient, extractPayload } from "./mcpClient";
import { buildNotebookXml, sanitizeFileName } from "./notebookDump";
import { DocCache, sliceItems, sliceText } from "./pagination";
/**
* OneNote Retrieval tools provider.
*
* Every tool here is a thin, read-only wrapper around a `local-onenote-mcp`
* command. The wrappers exist for one reason: LM Studio truncates large MCP
* tool results with no way to disable it. These native plugin tools instead
* return results in explicit, model-controlled pages:
*
* - text / XML -> offset + max_chars (returns next_offset, has_more)
* - lists -> offset + limit (returns next_offset, has_more)
*
* The plugin always asks the underlying server for the FULL document (a very
* large max_chars) so the server itself never truncates first, then it hands
* the content back to the model one small, complete slice at a time.
*/
interface Cfg {
serverCommand: string;
serverArgs: string[];
timeoutSeconds: number;
requestTimeoutMs: number;
underlyingMaxChars: number;
defaultPageChars: number;
defaultItemLimit: number;
}
const DEFAULT_CFG: Cfg = {
serverCommand: "local-onenote-mcp",
serverArgs: [],
timeoutSeconds: 90,
requestTimeoutMs: 120000,
underlyingMaxChars: 5000000,
defaultPageChars: 8000,
defaultItemLimit: 50,
};
function readConfig(ctl: ToolsProviderController): Cfg {
const cfg = { ...DEFAULT_CFG };
try {
const c = ctl.getPluginConfig(configSchematics);
cfg.serverCommand = c.get("serverCommand") || cfg.serverCommand;
cfg.serverArgs = c.get("serverArgs") ?? cfg.serverArgs;
cfg.timeoutSeconds = c.get("timeoutSeconds") ?? cfg.timeoutSeconds;
cfg.requestTimeoutMs = c.get("requestTimeoutMs") ?? cfg.requestTimeoutMs;
cfg.underlyingMaxChars = c.get("underlyingMaxChars") ?? cfg.underlyingMaxChars;
cfg.defaultPageChars = c.get("defaultPageChars") ?? cfg.defaultPageChars;
cfg.defaultItemLimit = c.get("defaultItemLimit") ?? cfg.defaultItemLimit;
} catch {
// Config not available in this context; fall back to defaults / env.
}
// Environment overrides (handy when running the server from a venv).
if (process.env.LOCAL_ONENOTE_MCP_COMMAND) cfg.serverCommand = process.env.LOCAL_ONENOTE_MCP_COMMAND;
return cfg;
}
// The underlying OneNote MCP server is expensive to start (COM init), so we
// keep ONE long-lived process per connection configuration and reuse it across
// every toolsProvider invocation. If connection settings change, the old
// process is stopped and replaced.
let sharedClient: McpStdioClient | undefined;
let sharedClientKey = "";
// Cache of full documents keyed by tool+identifier, so paging (offset > 0)
// does not re-query OneNote on every turn. Module-level so it survives across
// toolsProvider invocations.
const textCache = new DocCache<string>(24);
function getClient(cfg: Cfg): McpStdioClient {
const key = JSON.stringify([
cfg.serverCommand,
cfg.serverArgs,
cfg.timeoutSeconds,
cfg.requestTimeoutMs,
cfg.underlyingMaxChars,
]);
if (sharedClient && sharedClientKey === key) return sharedClient;
if (sharedClient) sharedClient.stop();
sharedClient = new McpStdioClient({
command: cfg.serverCommand,
args: cfg.serverArgs,
requestTimeoutMs: cfg.requestTimeoutMs,
env: {
// Make the underlying server return full documents so IT never truncates.
LOCAL_ONENOTE_MCP_TIMEOUT: String(cfg.timeoutSeconds),
LOCAL_ONENOTE_MCP_MAX_TEXT_CHARS: String(cfg.underlyingMaxChars),
},
onLog: (line) => {
// Surfaced to the LM Studio developer logs.
// eslint-disable-next-line no-console
console.info(`[onenote-retrieval] ${line}`);
},
});
sharedClientKey = key;
return sharedClient;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const cfg = readConfig(ctl);
const client = getClient(cfg);
/** Call an underlying tool and unwrap its {ok:true,...} envelope. Throws on error. */
async function call(name: string, args: Record<string, unknown>): Promise<any> {
const result = await client.callTool(name, args);
const payload = extractPayload(result);
if (result?.isError) {
const message =
typeof payload === "string" ? payload : payload?.error ?? JSON.stringify(payload);
throw new Error(message);
}
if (payload && typeof payload === "object" && payload.ok === false) {
throw new Error(payload.error || "Unknown OneNote error.");
}
return payload;
}
/** Wrap an implementation so any failure is returned to the model as a clean string. */
function guard<T extends (...a: any[]) => Promise<any>>(fn: T): T {
return (async (...args: any[]) => {
try {
return await fn(...args);
} catch (error: any) {
return `Error: ${error?.message ?? String(error)}`;
}
}) as T;
}
/**
* Fetch a full text document (cached) and return the requested slice.
*
* The cached copy is reused for ALL offsets, including offset 0. This is
* deliberate: every fetch of page XML/text triggers OneNote's GetPageContent,
* which loads (hydrates) the page inside the running OneNote process and is
* not released — so re-reading the same page would needlessly grow OneNote's
* memory and slow it down. Pass forceRefresh to bypass the cache when you
* genuinely need the latest content.
*/
async function pagedText(
cacheKey: string,
offset: number,
maxChars: number,
fetchFull: () => Promise<string>,
extra: Record<string, unknown> = {},
forceRefresh = false,
) {
let full = forceRefresh ? undefined : textCache.get(cacheKey);
if (full === undefined) {
full = await fetchFull();
textCache.set(cacheKey, full);
}
return { ok: true, ...extra, ...sliceText(full, offset, maxChars) };
}
const pageChars = () => cfg.defaultPageChars;
const itemLimit = () => cfg.defaultItemLimit;
const tools: Tool[] = [];
// ---------------------------------------------------------------------------
// Discovery / diagnostics
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_health_check",
description:
"Verify the local OneNote connection and return server info plus notebook/section counts. " +
"Use this first if other OneNote tools are failing.",
parameters: {},
implementation: guard(async () => {
return await call("health_check", {});
}),
}),
);
tools.push(
tool({
name: "onenote_resolve_identifier",
description:
"Resolve a OneNote identifier (object GUID, exact hierarchy path like " +
"'Personal/Quick Notes/My Section', or a unique display name) to a single live object. " +
"Use before other tools when a name might be ambiguous.",
parameters: {
identifier: z.string().describe("GUID, exact path, or unique name."),
item_type: z
.enum(["", "notebook", "section_group", "section", "page"])
.optional()
.describe("Optional filter to disambiguate the object type."),
},
implementation: guard(async ({ identifier, item_type }) => {
return await call("resolve_identifier", { identifier, item_type: item_type ?? "" });
}),
}),
);
tools.push(
tool({
name: "onenote_get_special_locations",
description: "Return OneNote's local special folders: backup, unfiled, and default notebook folder.",
parameters: {},
implementation: guard(async () => {
return await call("get_special_locations", {});
}),
}),
);
// ---------------------------------------------------------------------------
// Hierarchy listing (item pagination)
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_list_notebooks",
description:
"List notebooks known to the local OneNote desktop app. Returns a page of items with " +
"next_offset/has_more for paging.",
parameters: {
offset: z.number().int().min(0).optional().describe("Item index to start from (default 0)."),
limit: z.number().int().min(1).optional().describe("Max items to return this call."),
},
implementation: guard(async ({ offset, limit }) => {
const payload = await call("list_notebooks", {});
const items: any[] = payload.notebooks ?? [];
return { ok: true, ...sliceItems(items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
tools.push(
tool({
name: "onenote_list_sections",
description:
"List sections, optionally restricted to one notebook (by GUID, exact path, or unique name). " +
"Paginated with offset/limit.",
parameters: {
notebook_identifier: z.string().optional().describe("Optional notebook GUID, path, or name."),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ notebook_identifier, include_recycle_bin, offset, limit }) => {
const payload = await call("list_sections", {
notebook_identifier: notebook_identifier ?? "",
include_recycle_bin: include_recycle_bin ?? false,
});
const items: any[] = payload.sections ?? [];
return { ok: true, ...sliceItems(items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
tools.push(
tool({
name: "onenote_list_pages",
description:
"List pages within a section (by GUID, exact path, or unique name). Paginated with offset/limit.",
parameters: {
section_identifier: z.string().describe("Section GUID, exact path, or unique name."),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ section_identifier, include_recycle_bin, offset, limit }) => {
const payload = await call("list_pages", {
section_identifier,
include_recycle_bin: include_recycle_bin ?? false,
});
const items: any[] = payload.pages ?? [];
return {
ok: true,
section: payload.section,
...sliceItems(items, offset ?? 0, limit ?? itemLimit()),
};
}),
}),
);
tools.push(
tool({
name: "onenote_list_hierarchy",
description:
"List live OneNote hierarchy objects starting from an optional identifier. Paginated with offset/limit.",
parameters: {
start_identifier: z.string().optional().describe("Optional starting GUID, path, or name."),
scope: z
.enum(["self", "children", "notebooks", "sections", "pages"])
.optional()
.describe("Traversal depth (default 'pages')."),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ start_identifier, scope, include_recycle_bin, offset, limit }) => {
const payload = await call("list_hierarchy", {
start_identifier: start_identifier ?? "",
scope: scope ?? "pages",
include_xml: false,
include_recycle_bin: include_recycle_bin ?? false,
});
const items: any[] = payload.items ?? [];
return { ok: true, ...sliceItems(items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
// ---------------------------------------------------------------------------
// Page content (text / XML pagination)
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_get_page",
description:
"Read a page's plain text (title + object list included on the first slice). Text is returned in " +
"pages: pass offset=next_offset to continue until has_more is false. This is the main way to read " +
"a full page without truncation. Prefer this over onenote_get_page_xml unless you specifically need " +
"raw OneNote XML.",
parameters: {
page_identifier: z
.string()
.describe("Page GUID (fastest — use the 'id' from list/search results), exact path, or unique name."),
page_info: z
.enum(["basic", "all", "selection", "file_type"])
.optional()
.describe("Detail level (default 'basic'). Avoid 'all' unless you need binary; it is much heavier."),
offset: z.number().int().min(0).optional().describe("Character index to start from (default 0)."),
max_chars: z.number().int().min(1).optional().describe("Max characters to return this call."),
refresh: z
.boolean()
.optional()
.describe("Re-read from OneNote instead of the cached copy (default false). Rarely needed."),
},
implementation: guard(async ({ page_identifier, page_info, offset, max_chars, refresh }) => {
const info = page_info ?? "basic";
const off = offset ?? 0;
const key = `get_page:${page_identifier}:${info}`;
let title: string | undefined;
let objects: unknown;
const page = await pagedText(
key,
off,
max_chars ?? pageChars(),
async () => {
const payload = await call("get_page", {
page_identifier,
page_info: info,
include_xml: false,
max_chars: cfg.underlyingMaxChars,
});
title = payload.title;
objects = payload.objects;
return String(payload.text ?? "");
},
{},
refresh ?? false,
);
// Include lightweight metadata only on the first slice to keep pages small.
if (off === 0) {
return { ...page, title, objects };
}
return page;
}),
}),
);
tools.push(
tool({
name: "onenote_get_page_text",
description:
"Return the plain text of a page, paginated by offset/max_chars. Use next_offset to read the whole " +
"page across several calls.",
parameters: {
page_identifier: z
.string()
.describe("Page GUID (fastest — use the 'id' from list/search results), exact path, or unique name."),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z
.boolean()
.optional()
.describe("Re-read from OneNote instead of the cached copy (default false)."),
},
implementation: guard(async ({ page_identifier, offset, max_chars, refresh }) => {
const off = offset ?? 0;
const key = `get_page_text:${page_identifier}`;
return await pagedText(
key,
off,
max_chars ?? pageChars(),
async () => {
const payload = await call("get_page_text", {
page_identifier,
max_chars: cfg.underlyingMaxChars,
});
return String(payload.text ?? "");
},
{},
refresh ?? false,
);
}),
}),
);
tools.push(
tool({
name: "onenote_get_page_xml",
description:
"Return the raw OneNote XML of a page, paginated by offset/max_chars. Only use this when you need the " +
"raw XML structure (object IDs, formatting, tables) — for reading content prefer onenote_get_page or " +
"onenote_get_page_text, which are cheaper. XML can be very large, so read it in slices using next_offset.",
parameters: {
page_identifier: z
.string()
.describe("Page GUID (fastest — use the 'id' from list/search results), exact path, or unique name."),
page_info: z
.enum(["basic", "all", "selection", "file_type"])
.optional()
.describe("Detail level (default 'basic'). Avoid 'all' unless you need binary; it is much heavier."),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z
.boolean()
.optional()
.describe("Re-read from OneNote instead of the cached copy (default false)."),
},
implementation: guard(async ({ page_identifier, page_info, offset, max_chars, refresh }) => {
const info = page_info ?? "basic";
const off = offset ?? 0;
const key = `get_page_xml:${page_identifier}:${info}`;
return await pagedText(
key,
off,
max_chars ?? pageChars(),
async () => {
const payload = await call("get_page_xml", { page_identifier, page_info: info });
return String(payload.xml ?? "");
},
{},
refresh ?? false,
);
}),
}),
);
tools.push(
tool({
name: "onenote_get_page_objects",
description:
"List a page's content objects (outlines, images, attachments, ink) with their object/callback IDs. " +
"Paginated with offset/limit. Use callback IDs with onenote_get_binary_content.",
parameters: {
page_identifier: z.string(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ page_identifier, offset, limit }) => {
const payload = await call("get_page_objects", { page_identifier });
const items: any[] = payload.objects ?? [];
return { ok: true, ...sliceItems(items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
tools.push(
tool({
name: "onenote_get_binary_content",
description:
"Read binary page content (base64) by callback ID from onenote_get_page_objects. The base64 string is " +
"paginated by offset/max_chars; concatenate slices in order to reconstruct it.",
parameters: {
page_identifier: z.string(),
callback_id: z.string().describe("callback ID from onenote_get_page_objects."),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
},
implementation: guard(async ({ page_identifier, callback_id, offset, max_chars }) => {
const off = offset ?? 0;
const key = `get_binary_content:${page_identifier}:${callback_id}`;
return await pagedText(
key,
off,
max_chars ?? Math.max(pageChars(), 8000),
async () => {
const payload = await call("get_binary_content", { page_identifier, callback_id });
return String(payload.base64 ?? "");
},
{ encoding: "base64" },
);
}),
}),
);
// ---------------------------------------------------------------------------
// Search
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_search_pages",
description:
"Search page text across OneNote using OneNote's own search index (fast). Results are paginated with " +
"offset/limit; each result may include a text snippet. IMPORTANT: prefer scoping the search to a " +
"notebook or section via start_identifier when you know roughly where the content is. Only set " +
"include_unindexed=true as a last resort if the indexed search misses very recently created pages — " +
"it forces OneNote to load every page in scope, which is slow and can make OneNote sluggish.",
parameters: {
query: z.string().describe("Text to search for."),
start_identifier: z
.string()
.optional()
.describe("Recommended scope: notebook/section GUID, path, or name. Empty = search everything."),
max_results: z
.number()
.int()
.min(1)
.optional()
.describe("Max result pages to collect (default 20)."),
include_snippets: z.boolean().optional().describe("Include matching text snippets (default true)."),
include_unindexed: z
.boolean()
.optional()
.describe(
"Last-resort live scan of every page's text instead of the index (default FALSE). Slow; loads all " +
"pages in scope into OneNote. Use only if indexed search misses recent pages.",
),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(
async ({
query,
start_identifier,
max_results,
include_snippets,
include_unindexed,
include_recycle_bin,
offset,
limit,
}) => {
const payload = await call("search_pages", {
query,
start_identifier: start_identifier ?? "",
max_results: max_results ?? 20,
include_snippets: include_snippets ?? true,
include_unindexed: include_unindexed ?? false,
include_recycle_bin: include_recycle_bin ?? false,
});
const items: any[] = payload.pages ?? [];
return {
ok: true,
search_backend: payload.search_backend,
...sliceItems(items, offset ?? 0, limit ?? itemLimit()),
};
},
),
}),
);
tools.push(
tool({
name: "onenote_find_meta",
description:
"Find pages or objects that carry a matching OneNote meta name. Paginated with offset/limit.",
parameters: {
start_identifier: z.string().optional(),
name: z.string().describe("Meta name to match."),
include_unindexed: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ start_identifier, name, include_unindexed, offset, limit }) => {
const payload = await call("find_meta", {
start_identifier: start_identifier ?? "",
name,
include_unindexed: include_unindexed ?? true,
});
const items: any[] = payload.items ?? [];
return { ok: true, ...sliceItems(items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
// ---------------------------------------------------------------------------
// Links / relationships
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_get_hyperlink",
description:
"Return a OneNote client link (onenote:) or a web link for an object. Small result, no pagination.",
parameters: {
object_identifier: z.string().describe("GUID, exact path, or unique name."),
page_content_object_id: z.string().optional().describe("Optional sub-object id within a page."),
web: z.boolean().optional().describe("Return a web (https) link instead of a client link."),
},
implementation: guard(async ({ object_identifier, page_content_object_id, web }) => {
return await call("get_hyperlink", {
object_identifier,
page_content_object_id: page_content_object_id ?? "",
web: web ?? false,
});
}),
}),
);
tools.push(
tool({
name: "onenote_get_parent",
description: "Return the parent object ID of a notebook hierarchy object.",
parameters: {
object_identifier: z.string().describe("GUID, exact path, or unique name."),
},
implementation: guard(async ({ object_identifier }) => {
return await call("get_parent", { object_identifier });
}),
}),
);
// ---------------------------------------------------------------------------
// Creation & structural edits (WRITE)
// ---------------------------------------------------------------------------
const CONTENT_FORMATS = ["plain", "html", "markdown"] as const;
tools.push(
tool({
name: "onenote_open_hierarchy",
description:
"Open (or create) a notebook, section group, or section by path. Existing paths resolve directly; set " +
"create_type to create the object if it does not exist.",
parameters: {
path: z.string().describe("Path or name of the object."),
relative_to_identifier: z
.string()
.optional()
.describe("Optional parent GUID/path/name to interpret 'path' relative to."),
create_type: z
.enum(["none", "notebook", "folder", "section_group", "section"])
.optional()
.describe("What to create if missing (default 'none' = open only)."),
},
implementation: guard(async ({ path, relative_to_identifier, create_type }) => {
return await call("open_hierarchy", {
path,
relative_to_identifier: relative_to_identifier ?? "",
create_type: create_type ?? "none",
});
}),
}),
);
tools.push(
tool({
name: "onenote_create_notebook",
description: "Create a local notebook folder and open it in OneNote.",
parameters: {
name_or_path: z.string().describe("Notebook name, or an absolute folder path."),
base_folder: z.string().optional().describe("Optional parent folder for a name (not a full path)."),
},
implementation: guard(async ({ name_or_path, base_folder }) => {
return await call("create_notebook", { name_or_path, base_folder: base_folder ?? "" });
}),
}),
);
tools.push(
tool({
name: "onenote_create_section",
description: "Create a section under a notebook or section group.",
parameters: {
parent_identifier: z.string().describe("Notebook or section-group GUID, path, or name."),
section_name: z.string(),
},
implementation: guard(async ({ parent_identifier, section_name }) => {
return await call("create_section", { parent_identifier, section_name });
}),
}),
);
tools.push(
tool({
name: "onenote_create_section_group",
description: "Create a section group under a notebook or another section group.",
parameters: {
parent_identifier: z.string().describe("Notebook or section-group GUID, path, or name."),
group_name: z.string(),
},
implementation: guard(async ({ parent_identifier, group_name }) => {
return await call("create_section_group", { parent_identifier, group_name });
}),
}),
);
tools.push(
tool({
name: "onenote_create_page",
description:
"Create a page in a section. content_format accepts plain, html, or markdown (markdown requires the " +
"OneMore add-in for rich rendering).",
parameters: {
section_identifier: z.string().describe("Section GUID, path, or name."),
title: z.string(),
content: z.string().optional(),
content_format: z.enum(CONTENT_FORMATS).optional().describe("Default 'plain'."),
new_page_style: z
.enum(["default", "blank_with_title", "blank_no_title"])
.optional()
.describe("Default 'blank_with_title'."),
},
implementation: guard(async ({ section_identifier, title, content, content_format, new_page_style }) => {
return await call("create_page", {
section_identifier,
title,
content: content ?? "",
content_format: content_format ?? "plain",
new_page_style: new_page_style ?? "blank_with_title",
});
}),
}),
);
tools.push(
tool({
name: "onenote_update_page_title",
description: "Update a page's title.",
parameters: {
page_identifier: z.string().describe("Page GUID, path, or name."),
title: z.string(),
},
implementation: guard(async ({ page_identifier, title }) => {
return await call("update_page_title", { page_identifier, title });
}),
}),
);
tools.push(
tool({
name: "onenote_append_to_page",
description:
"Append a new outline block to a page. content_format accepts plain, html, or markdown. Optional x/y " +
"position the outline (in points).",
parameters: {
page_identifier: z.string().describe("Page GUID, path, or name."),
content: z.string(),
content_format: z.enum(CONTENT_FORMATS).optional().describe("Default 'plain'."),
x: z.number().optional(),
y: z.number().optional(),
},
implementation: guard(async ({ page_identifier, content, content_format, x, y }) => {
return await call("append_to_page", {
page_identifier,
content,
content_format: content_format ?? "plain",
x,
y,
});
}),
}),
);
tools.push(
tool({
name: "onenote_add_image_to_page",
description:
"Add a local image file to a page. Missing width/height are inferred from the image's native size.",
parameters: {
page_identifier: z.string().describe("Page GUID, path, or name."),
image_path: z.string().describe("Absolute path to a local image file (on the machine running OneNote)."),
image_format: z.string().optional().describe("Override format if the file has no extension (e.g. 'png')."),
x: z.number().optional(),
y: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
},
implementation: guard(async ({ page_identifier, image_path, image_format, x, y, width, height }) => {
return await call("add_image_to_page", {
page_identifier,
image_path,
image_format: image_format ?? "",
x,
y,
width,
height,
});
}),
}),
);
tools.push(
tool({
name: "onenote_replace_page_body",
description:
"Delete a page's existing content objects and write new body content. Optionally update the title. " +
"content_format accepts plain, html, or markdown. Destructive — replaces existing content.",
parameters: {
page_identifier: z.string().describe("Page GUID, path, or name."),
content: z.string(),
title: z.string().optional(),
content_format: z.enum(CONTENT_FORMATS).optional().describe("Default 'plain'."),
},
implementation: guard(async ({ page_identifier, content, title, content_format }) => {
return await call("replace_page_body", {
page_identifier,
content,
title,
content_format: content_format ?? "plain",
});
}),
}),
);
tools.push(
tool({
name: "onenote_delete_page_content",
description:
"Delete one deletable content object from a page by its object ID (from onenote_get_page_objects). " +
"Destructive.",
parameters: {
page_identifier: z.string().describe("Page GUID, path, or name."),
object_id: z.string().describe("Object ID of the content element to delete."),
},
implementation: guard(async ({ page_identifier, object_id }) => {
return await call("delete_page_content", { page_identifier, object_id });
}),
}),
);
tools.push(
tool({
name: "onenote_delete_hierarchy",
description:
"Delete a notebook, section group, section, or page. By default it goes to OneNote's recycle bin; set " +
"permanently=true to delete outright. Destructive — use with care.",
parameters: {
object_identifier: z.string().describe("GUID, path, or name of the object to delete."),
permanently: z.boolean().optional().describe("Permanently delete instead of recycle-bin (default false)."),
},
implementation: guard(async ({ object_identifier, permanently }) => {
return await call("delete_hierarchy", { object_identifier, permanently: permanently ?? false });
}),
}),
);
// ---------------------------------------------------------------------------
// Raw low-level control (WRITE)
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_update_page_xml",
description:
"Advanced: submit raw OneNote page XML to UpdatePageContent. The XML must target an existing page. " +
"Use force=true to override newer-content conflicts.",
parameters: {
xml: z.string().describe("Raw OneNote page XML (2013 schema)."),
force: z.boolean().optional(),
},
implementation: guard(async ({ xml, force }) => {
return await call("update_page_xml", { xml, force: force ?? false });
}),
}),
);
tools.push(
tool({
name: "onenote_update_hierarchy_xml",
description: "Advanced: submit raw OneNote hierarchy XML to UpdateHierarchy.",
parameters: {
xml: z.string().describe("Raw OneNote hierarchy XML (2013 schema)."),
},
implementation: guard(async ({ xml }) => {
return await call("update_hierarchy_xml", { xml });
}),
}),
);
// ---------------------------------------------------------------------------
// File & app control
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_publish_object",
description:
"Export a notebook, section, or page to a local file (PDF, Word, XPS, HTML, .one, etc.). target_path is " +
"a path on the machine running OneNote.",
parameters: {
object_identifier: z.string().describe("GUID, path, or name of the object to export."),
target_path: z.string().describe("Output file path on the OneNote machine."),
format: z
.enum(["pdf", "xps", "word", "docx", "doc", "one", "onepkg", "mhtml", "mht", "html", "emf", "one2007"])
.optional()
.describe("Export format (default 'pdf')."),
overwrite: z.boolean().optional(),
},
implementation: guard(async ({ object_identifier, target_path, format, overwrite }) => {
return await call("publish_object", {
object_identifier,
target_path,
format: format ?? "pdf",
overwrite: overwrite ?? false,
});
}),
}),
);
tools.push(
tool({
name: "onenote_navigate_to",
description: "Open/focus a OneNote object in the desktop app.",
parameters: {
object_identifier: z.string().describe("GUID, path, or name."),
page_content_object_id: z.string().optional().describe("Optional sub-object id within a page."),
new_window: z.boolean().optional(),
},
implementation: guard(async ({ object_identifier, page_content_object_id, new_window }) => {
return await call("navigate_to", {
object_identifier,
page_content_object_id: page_content_object_id ?? "",
new_window: new_window ?? false,
});
}),
}),
);
tools.push(
tool({
name: "onenote_navigate_to_url",
description: "Open a OneNote URL (onenote:) in the desktop app.",
parameters: {
url: z.string(),
new_window: z.boolean().optional(),
},
implementation: guard(async ({ url, new_window }) => {
return await call("navigate_to_url", { url, new_window: new_window ?? false });
}),
}),
);
tools.push(
tool({
name: "onenote_sync_hierarchy",
description: "Ask OneNote to sync a notebook hierarchy object.",
parameters: {
object_identifier: z.string().describe("GUID, path, or name."),
},
implementation: guard(async ({ object_identifier }) => {
return await call("sync_hierarchy", { object_identifier });
}),
}),
);
tools.push(
tool({
name: "onenote_close_notebook",
description: "Close a notebook in the desktop OneNote app.",
parameters: {
notebook_identifier: z.string().describe("Notebook GUID, path, or name."),
force: z.boolean().optional(),
},
implementation: guard(async ({ notebook_identifier, force }) => {
return await call("close_notebook", { notebook_identifier, force: force ?? false });
}),
}),
);
tools.push(
tool({
name: "onenote_merge_sections",
description: "Merge one section into another. Destructive — the source section's pages move into the destination.",
parameters: {
source_section_identifier: z.string().describe("Source section GUID, path, or name."),
destination_section_identifier: z.string().describe("Destination section GUID, path, or name."),
},
implementation: guard(async ({ source_section_identifier, destination_section_identifier }) => {
return await call("merge_sections", {
source_section_identifier,
destination_section_identifier,
});
}),
}),
);
tools.push(
tool({
name: "onenote_set_filing_location",
description: "Set OneNote's local filing location for email, web clips, printouts, and similar content.",
parameters: {
filing_location: z
.enum(["email", "contacts", "tasks", "meetings", "web_content", "printouts"])
.describe("Which kind of incoming content."),
filing_location_type: z
.enum(["named_section_new_page", "current_section_new_page", "current_page", "named_page"])
.describe("How it should be filed."),
section_or_page_identifier: z.string().describe("Target section or page GUID, path, or name."),
},
implementation: guard(async ({ filing_location, filing_location_type, section_or_page_identifier }) => {
return await call("set_filing_location", {
filing_location,
filing_location_type,
section_or_page_identifier,
});
}),
}),
);
// ---------------------------------------------------------------------------
// Bulk export
// ---------------------------------------------------------------------------
tools.push(
tool({
name: "onenote_save_all_notebook_pages_xml_to_file",
description:
"Read an entire notebook (by name) and save the raw OneNote XML of every page to a single file, " +
"preserving the section-group / section / page structure. The file is named after the notebook with a " +
".xml extension. NOTE: this reads every page via GetPageContent, which loads them into OneNote and can " +
"make OneNote sluggish for very large notebooks — consider restarting OneNote afterward.",
parameters: {
notebook_name: z.string().describe("Notebook name (or GUID/path)."),
output_dir: z
.string()
.optional()
.describe("Directory to write into (on the OneNote machine). Default: the plugin working directory."),
page_info: z
.enum(["basic", "all", "selection", "file_type"])
.optional()
.describe("Page detail level (default 'basic'). Use 'all' to include binary; much heavier."),
overwrite: z.boolean().optional().describe("Overwrite the file if it already exists (default false)."),
},
implementation: guard(async ({ notebook_name, output_dir, page_info, overwrite }, { status, warn, signal }) => {
const info = page_info ?? "basic";
// Resolve the notebook.
const resolved = await call("resolve_identifier", { identifier: notebook_name, item_type: "notebook" });
const notebook = resolved.item ?? {};
if (!notebook.id) throw new Error(`Could not resolve notebook '${notebook_name}'.`);
// Enumerate the whole notebook down to pages.
status?.(`Reading structure of '${notebook.name ?? notebook_name}'...`);
const listed = await call("list_hierarchy", {
start_identifier: notebook.id,
scope: "pages",
include_xml: false,
include_recycle_bin: false,
});
const items: any[] = listed.items ?? [];
// Build the structured XML, fetching each page's XML by GUID (fast path).
const { xml, stats } = await buildNotebookXml(
{ id: notebook.id, name: notebook.name, path: notebook.path },
items,
async (pageId) => {
const payload = await call("get_page_xml", { page_identifier: pageId, page_info: info });
return String(payload.xml ?? "");
},
{
onProgress: (done, total, name) => status?.(`Reading page ${done}/${total}: ${name}`),
isAborted: () => signal?.aborted ?? false,
},
);
// Resolve the output directory and filename.
let dir = output_dir && output_dir.trim() ? output_dir.trim() : ctl.getWorkingDirectory();
if (!isAbsolute(dir)) dir = join(ctl.getWorkingDirectory(), dir);
await mkdir(dir, { recursive: true });
const fileName = `${sanitizeFileName(String(notebook.name ?? notebook_name))}.xml`;
const filePath = join(dir, fileName);
if (existsSync(filePath) && !overwrite) {
throw new Error(`File already exists: ${filePath}. Pass overwrite=true to replace it.`);
}
await writeFile(filePath, xml, "utf-8");
if (stats.errors > 0) warn?.(`${stats.errors} page(s) could not be read; placeholders were written.`);
return {
ok: true,
file: filePath,
notebook: notebook.name ?? notebook_name,
pages_written: stats.pages,
sections: stats.sections,
section_groups: stats.section_groups,
page_read_errors: stats.errors,
bytes: Buffer.byteLength(xml, "utf-8"),
};
}),
}),
);
return tools;
}