Add multi-account OAuth, Obsidian integration, product assets, and test tooling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
133
src/tools.ts
133
src/tools.ts
@@ -1,12 +1,13 @@
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
|
||||
import { sendEmail, createDraft } from './smtp.js';
|
||||
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
|
||||
|
||||
const ACCOUNT_PARAM = {
|
||||
account: {
|
||||
type: 'string',
|
||||
enum: ['yahoo', 'fetcherpay', 'garfield', 'sales', 'leads', 'founder'],
|
||||
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com), "fetcherpay" (garfield.heron@fetcherpay.com), "garfield" (garfield@fetcherpay.com), "sales" (sales@fetcherpay.com), "leads" (leads@fetcherpay.com), or "founder" (founder@fetcherpay.com). Defaults to "yahoo".',
|
||||
enum: ['yahoo', 'fetcherpay', 'garfield', 'sales', 'leads', 'founder', 'gmail'],
|
||||
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com), "fetcherpay" (garfield.heron@fetcherpay.com), "garfield" (garfield@fetcherpay.com), "sales" (sales@fetcherpay.com), "leads" (leads@fetcherpay.com), "founder" (founder@fetcherpay.com), or "gmail" (Gmail account). Defaults to "yahoo".',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +28,7 @@ export const tools: Tool[] = [
|
||||
properties: {
|
||||
q: { type: 'string', description: 'Search query (keyword, from:email, subject:text)' },
|
||||
maxResults: { type: 'number', description: 'Max messages to return (default 20)' },
|
||||
folder: { type: 'string', description: 'Folder to search (default INBOX). For Gmail, [Gmail]/All Mail searches everywhere.' },
|
||||
...ACCOUNT_PARAM,
|
||||
},
|
||||
required: ['q'],
|
||||
@@ -39,6 +41,7 @@ export const tools: Tool[] = [
|
||||
type: 'object',
|
||||
properties: {
|
||||
uid: { type: 'number', description: 'Message UID from search results' },
|
||||
folder: { type: 'string', description: 'Folder the message was found in (default INBOX). Use the folder value from search_results.' },
|
||||
...ACCOUNT_PARAM,
|
||||
},
|
||||
required: ['uid'],
|
||||
@@ -80,6 +83,97 @@ export const tools: Tool[] = [
|
||||
required: ['to', 'subject', 'body'],
|
||||
},
|
||||
},
|
||||
|
||||
// ── Obsidian tools ────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'obsidian_search_notes',
|
||||
description:
|
||||
'Search across the Obsidian vault by content, tags, or title. Use when the user references "my notes", "in obsidian", "I wrote about", or needs personal knowledge retrieval. Returns note paths relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Always use these exact relative paths when calling obsidian_read_note or obsidian_append_to_note.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Text to search for in note content or title' },
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by Obsidian tags (all must match)',
|
||||
},
|
||||
limit: { type: 'number', description: 'Max results to return (default 10)' },
|
||||
path_filter: {
|
||||
type: 'string',
|
||||
description: 'Only return notes whose relative vault path contains this string (e.g. "Daily Notes")',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obsidian_read_note',
|
||||
description:
|
||||
'Retrieve the full content of a specific Obsidian note by relative vault path. Use when the user asks to read, open, or show a specific note. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md") or just the note title/filename',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obsidian_append_to_note',
|
||||
description:
|
||||
'Append markdown content to an Obsidian daily note or any specific vault note. Use for logging, journaling, capturing insights from email, or recording action items in your notes. Creates the note if it does not exist. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md")',
|
||||
},
|
||||
content: { type: 'string', description: 'Markdown content to append' },
|
||||
create_if_missing: {
|
||||
type: 'boolean',
|
||||
description: 'Create the note if it does not exist (default true)',
|
||||
},
|
||||
header: {
|
||||
type: 'string',
|
||||
description: 'Optional H2 section header to add before the content',
|
||||
},
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obsidian_update_note',
|
||||
description:
|
||||
'Overwrite the entire content of an Obsidian note with new markdown. Use when you need to replace, rewrite, or structurally edit an existing note rather than append to it. Creates the note if it does not exist.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'File path relative to vault root (e.g. "Daily Notes/2026-04-15.md")',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Full markdown content to write — replaces existing content entirely',
|
||||
},
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'obsidian_sync_status',
|
||||
description:
|
||||
'Check the sync status of the Obsidian vault via Syncthing. Use when the user asks if their notes are synced or up to date.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function acct(args: Record<string, unknown>): Account {
|
||||
@@ -101,11 +195,11 @@ export async function handleToolCall(
|
||||
break;
|
||||
|
||||
case 'search_messages':
|
||||
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, acct(args));
|
||||
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, acct(args), args.folder as string | undefined);
|
||||
break;
|
||||
|
||||
case 'read_message':
|
||||
result = await readMessage(args.uid as number, acct(args));
|
||||
result = await readMessage(args.uid as number, acct(args), args.folder as string | undefined);
|
||||
break;
|
||||
|
||||
case 'list_folders':
|
||||
@@ -120,6 +214,37 @@ export async function handleToolCall(
|
||||
result = await sendEmail(args.to as string, args.subject as string, args.body as string, acct(args));
|
||||
break;
|
||||
|
||||
// ── Obsidian ──────────────────────────────────────────────────────────
|
||||
case 'obsidian_search_notes':
|
||||
result = await searchNotes(
|
||||
args.query as string,
|
||||
args.tags as string[] | undefined,
|
||||
(args.limit as number) ?? 10,
|
||||
args.path_filter as string | undefined
|
||||
);
|
||||
break;
|
||||
|
||||
case 'obsidian_read_note':
|
||||
result = await getNote(args.path as string);
|
||||
break;
|
||||
|
||||
case 'obsidian_append_to_note':
|
||||
result = await appendToNote(
|
||||
args.path as string,
|
||||
args.content as string,
|
||||
(args.create_if_missing as boolean) ?? true,
|
||||
args.header as string | undefined
|
||||
);
|
||||
break;
|
||||
|
||||
case 'obsidian_update_note':
|
||||
result = await updateNote(args.path as string, args.content as string);
|
||||
break;
|
||||
|
||||
case 'obsidian_sync_status':
|
||||
result = await getSyncStatus();
|
||||
break;
|
||||
|
||||
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
||||
case 'yahoo_get_profile':
|
||||
result = await getProfile('yahoo');
|
||||
|
||||
Reference in New Issue
Block a user