|
| 1 | +import { invariant } from '@epic-web/invariant' |
| 2 | +import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js' |
| 3 | +import { z } from 'zod' |
| 4 | +import { createEntryInputSchema, createTagInputSchema } from './db/schema.ts' |
| 5 | +import { type EpicMeMCP } from './index.ts' |
| 6 | + |
| 7 | +export async function initializeTools(agent: EpicMeMCP) { |
| 8 | + // Entry Tools |
| 9 | + agent.server.tool( |
| 10 | + 'create_entry', |
| 11 | + 'Create a new journal entry', |
| 12 | + createEntryInputSchema, |
| 13 | + async (entry) => { |
| 14 | + const createdEntry = await agent.db.createEntry(entry) |
| 15 | + if (entry.tags) { |
| 16 | + for (const tagId of entry.tags) { |
| 17 | + await agent.db.addTagToEntry({ |
| 18 | + entryId: createdEntry.id, |
| 19 | + tagId, |
| 20 | + }) |
| 21 | + } |
| 22 | + } |
| 23 | + return createReply( |
| 24 | + `Entry "${createdEntry.title}" created successfully with ID "${createdEntry.id}"`, |
| 25 | + ) |
| 26 | + }, |
| 27 | + ) |
| 28 | + |
| 29 | + agent.server.tool( |
| 30 | + 'get_entry', |
| 31 | + 'Get a journal entry by ID', |
| 32 | + { |
| 33 | + id: z.number().describe('The ID of the entry'), |
| 34 | + }, |
| 35 | + async ({ id }) => { |
| 36 | + const entry = await agent.db.getEntry(id) |
| 37 | + invariant(entry, `Entry with ID "${id}" not found`) |
| 38 | + return createReply(entry) |
| 39 | + }, |
| 40 | + ) |
| 41 | + |
| 42 | + agent.server.tool( |
| 43 | + 'list_entries', |
| 44 | + 'List all journal entries', |
| 45 | + { |
| 46 | + tagIds: z |
| 47 | + .array(z.number()) |
| 48 | + .optional() |
| 49 | + .describe('Optional array of tag IDs to filter entries by'), |
| 50 | + }, |
| 51 | + async ({ tagIds }) => { |
| 52 | + const entries = await agent.db.listEntries(tagIds) |
| 53 | + return createReply(entries) |
| 54 | + }, |
| 55 | + ) |
| 56 | + |
| 57 | + agent.server.tool( |
| 58 | + 'update_entry', |
| 59 | + 'Update a journal entry. Fields that are not provided (or set to undefined) will not be updated. Fields that are set to null or any other value will be updated.', |
| 60 | + { |
| 61 | + id: z.number(), |
| 62 | + title: z.string().optional().describe('The title of the entry'), |
| 63 | + content: z.string().optional().describe('The content of the entry'), |
| 64 | + mood: z |
| 65 | + .string() |
| 66 | + .nullable() |
| 67 | + .optional() |
| 68 | + .describe( |
| 69 | + 'The mood of the entry (for example: "happy", "sad", "anxious", "excited")', |
| 70 | + ), |
| 71 | + location: z |
| 72 | + .string() |
| 73 | + .nullable() |
| 74 | + .optional() |
| 75 | + .describe( |
| 76 | + 'The location of the entry (for example: "home", "work", "school", "park")', |
| 77 | + ), |
| 78 | + weather: z |
| 79 | + .string() |
| 80 | + .nullable() |
| 81 | + .optional() |
| 82 | + .describe( |
| 83 | + 'The weather of the entry (for example: "sunny", "cloudy", "rainy", "snowy")', |
| 84 | + ), |
| 85 | + isPrivate: z |
| 86 | + .number() |
| 87 | + .optional() |
| 88 | + .describe('Whether the entry is private (1 for private, 0 for public)'), |
| 89 | + isFavorite: z |
| 90 | + .number() |
| 91 | + .optional() |
| 92 | + .describe( |
| 93 | + 'Whether the entry is a favorite (1 for favorite, 0 for not favorite)', |
| 94 | + ), |
| 95 | + }, |
| 96 | + async ({ id, ...updates }) => { |
| 97 | + const existingEntry = await agent.db.getEntry(id) |
| 98 | + invariant(existingEntry, `Entry with ID "${id}" not found`) |
| 99 | + const updatedEntry = await agent.db.updateEntry(id, updates) |
| 100 | + return createReply( |
| 101 | + `Entry "${updatedEntry.title}" (ID: ${id}) updated successfully`, |
| 102 | + ) |
| 103 | + }, |
| 104 | + ) |
| 105 | + |
| 106 | + agent.server.tool( |
| 107 | + 'delete_entry', |
| 108 | + 'Delete a journal entry', |
| 109 | + { |
| 110 | + id: z.number().describe('The ID of the entry'), |
| 111 | + }, |
| 112 | + async ({ id }) => { |
| 113 | + const existingEntry = await agent.db.getEntry(id) |
| 114 | + invariant(existingEntry, `Entry with ID "${id}" not found`) |
| 115 | + await agent.db.deleteEntry(id) |
| 116 | + return createReply( |
| 117 | + `Entry "${existingEntry.title}" (ID: ${id}) deleted successfully`, |
| 118 | + ) |
| 119 | + }, |
| 120 | + ) |
| 121 | + |
| 122 | + // Tag Tools |
| 123 | + agent.server.tool( |
| 124 | + 'create_tag', |
| 125 | + 'Create a new tag', |
| 126 | + createTagInputSchema, |
| 127 | + async (tag) => { |
| 128 | + const createdTag = await agent.db.createTag(tag) |
| 129 | + return createReply( |
| 130 | + `Tag "${createdTag.name}" created successfully with ID "${createdTag.id}"`, |
| 131 | + ) |
| 132 | + }, |
| 133 | + ) |
| 134 | + |
| 135 | + agent.server.tool( |
| 136 | + 'get_tag', |
| 137 | + 'Get a tag by ID', |
| 138 | + { |
| 139 | + id: z.number().describe('The ID of the tag'), |
| 140 | + }, |
| 141 | + async ({ id }) => { |
| 142 | + const tag = await agent.db.getTag(id) |
| 143 | + invariant(tag, `Tag ID "${id}" not found`) |
| 144 | + return createReply(tag) |
| 145 | + }, |
| 146 | + ) |
| 147 | + |
| 148 | + agent.server.tool('list_tags', 'List all tags', async () => { |
| 149 | + const tags = await agent.db.listTags() |
| 150 | + return createReply(tags) |
| 151 | + }) |
| 152 | + |
| 153 | + agent.server.tool( |
| 154 | + 'update_tag', |
| 155 | + 'Update a tag', |
| 156 | + { |
| 157 | + id: z.number(), |
| 158 | + ...Object.fromEntries( |
| 159 | + Object.entries(createTagInputSchema).map(([key, value]) => [ |
| 160 | + key, |
| 161 | + value.nullable().optional(), |
| 162 | + ]), |
| 163 | + ), |
| 164 | + }, |
| 165 | + async ({ id, ...updates }) => { |
| 166 | + const updatedTag = await agent.db.updateTag(id, updates) |
| 167 | + return createReply( |
| 168 | + `Tag "${updatedTag.name}" (ID: ${id}) updated successfully`, |
| 169 | + ) |
| 170 | + }, |
| 171 | + ) |
| 172 | + |
| 173 | + agent.server.tool( |
| 174 | + 'delete_tag', |
| 175 | + 'Delete a tag', |
| 176 | + { |
| 177 | + id: z.number().describe('The ID of the tag'), |
| 178 | + }, |
| 179 | + async ({ id }) => { |
| 180 | + const existingTag = await agent.db.getTag(id) |
| 181 | + invariant(existingTag, `Tag ID "${id}" not found`) |
| 182 | + await agent.db.deleteTag(id) |
| 183 | + return createReply( |
| 184 | + `Tag "${existingTag.name}" (ID: ${id}) deleted successfully`, |
| 185 | + ) |
| 186 | + }, |
| 187 | + ) |
| 188 | + |
| 189 | + // Entry Tag Tools |
| 190 | + agent.server.tool( |
| 191 | + 'add_tag_to_entry', |
| 192 | + 'Add a tag to an entry', |
| 193 | + { |
| 194 | + entryId: z.number().describe('The ID of the entry'), |
| 195 | + tagId: z.number().describe('The ID of the tag'), |
| 196 | + }, |
| 197 | + async ({ entryId, tagId }) => { |
| 198 | + const tag = await agent.db.getTag(tagId) |
| 199 | + const entry = await agent.db.getEntry(entryId) |
| 200 | + invariant(tag, `Tag ${tagId} not found`) |
| 201 | + invariant(entry, `Entry with ID "${entryId}" not found`) |
| 202 | + const entryTag = await agent.db.addTagToEntry({ |
| 203 | + entryId, |
| 204 | + tagId, |
| 205 | + }) |
| 206 | + return createReply( |
| 207 | + `Tag "${tag.name}" (ID: ${entryTag.tagId}) added to entry "${entry.title}" (ID: ${entryTag.entryId}) successfully`, |
| 208 | + ) |
| 209 | + }, |
| 210 | + ) |
| 211 | +} |
| 212 | + |
| 213 | +function createReply(text: any): CallToolResult { |
| 214 | + if (typeof text === 'string') { |
| 215 | + return { content: [{ type: 'text', text }] } |
| 216 | + } else { |
| 217 | + return { |
| 218 | + content: [{ type: 'text', text: JSON.stringify(text) }], |
| 219 | + } |
| 220 | + } |
| 221 | +} |
0 commit comments