basecamp_update_message
Modify existing Basecamp messages by updating subjects, replacing content, or appending/prepending text using HTML formatting rules and partial operations to optimize token usage.
Instructions
Update a message. Use partial content operations when possible to save on token usage.
HTML rules for content:
Allowed tags: div, span, h1, br, strong, em, strike, a (with an href attribute), pre, ol, ul, li, blockquote, bc-attachment (with sgid attribute).
Try to be semantic despite the limitations of tags. Use double to create paragraph spacing
To mention people:
To consume less tokens, existing tags can be rewritten by dropping any attributes/inner content and just leave the "sgid" and "caption" attributes, without loosing any information
You can highlight parts of the content in this format ... valid colors are:
red: 255, 229, 229
yellow: 250, 247, 133
green: 228, 248, 226
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| bucket_id | Yes | Basecamp resource identifier | |
| message_id | Yes | ||
| subject | No | New message subject | |
| message_type_id | No | Optional message type/category ID | |
| content | No | If provided, replaces entire HTML content. Cannot be used with content_append, content_prepend, or search_replace. | |
| content_append | No | Text to append to the end of current content. Cannot be used with content. | |
| content_prepend | No | Text to prepend to the beginning of current content. Cannot be used with content. | |
| search_replace | No | Array of search-replace operations to apply to current content. Cannot be used with content. |
Implementation Reference
- src/tools/messages.ts:301-367 (handler)The handler function implementing the core logic for updating a Basecamp message. Supports partial updates via content operations (append, prepend, search-replace), fetches current content if needed, applies changes, and calls the Basecamp API.async (params) => { try { // Validate at least one operation is provided validateContentOperations(params, ["subject", "message_type_id"]); const client = await initializeBasecampClient(); let finalContent: string | undefined; // Check if we need to fetch current content for partial operations const hasPartialOps = params.content_append || params.content_prepend || params.search_replace; if (hasPartialOps || params.content !== undefined) { // Fetch current message if needed for partial operations if (hasPartialOps) { const currentResponse = await client.messages.get({ params: { bucketId: params.bucket_id, messageId: params.message_id, }, }); if (currentResponse.status !== 200 || !currentResponse.body) { throw new Error( `Failed to fetch current message for partial update: ${currentResponse.status}`, ); } const currentContent = currentResponse.body.content || ""; finalContent = applyContentOperations(currentContent, params); } else { // Full content replacement finalContent = params.content; } } const response = await client.messages.update({ params: { bucketId: params.bucket_id, messageId: params.message_id }, body: { ...(params.subject ? { subject: params.subject } : {}), ...(finalContent !== undefined ? { content: finalContent } : {}), ...(params.message_type_id ? { category_id: params.message_type_id } : {}), }, }); if (response.status !== 200 || !response.body) { throw new Error(`Failed to update message`); } return { content: [ { type: "text", text: `Message updated successfully!\n\nID: ${response.body.id}\nSubject: ${response.body.title}`, }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } },
- src/tools/messages.ts:280-293 (schema)The input schema for the basecamp_update_message tool, defining parameters like bucket_id, message_id, subject, message_type_id, and content operations using Zod.inputSchema: { bucket_id: BasecampIdSchema, message_id: BasecampIdSchema, subject: z .string() .min(1) .max(500) .optional() .describe("New message subject"), message_type_id: BasecampIdSchema.optional().describe( "Optional message type/category ID", ), ...ContentOperationFields, },
- Key helper function that applies partial content operations (full replace, append, prepend, search-replace) to existing message content, enabling efficient updates without full content resubmission.export function applyContentOperations( currentContent: string, operations: ContentOperationParams, ): string | undefined { const hasPartialOps = operations.content_append || operations.content_prepend || operations.search_replace; // Validate mutual exclusivity if (operations.content && hasPartialOps) { throw new Error( "Cannot use 'content' with partial operations (content_append, content_prepend, search_replace). Use either full replacement or partial operations, not both.", ); } // If full content replacement, return it directly if (operations.content !== undefined) { return operations.content; } // If no operations at all, return undefined (no changes) if (!hasPartialOps) { return undefined; } // Apply partial operations let finalContent = currentContent; // Apply search-replace operations first if (operations.search_replace) { for (const operation of operations.search_replace) { // Check if the search string exists in the content if (!finalContent.includes(operation.find)) { throw new Error( `Search string not found: "${operation.find}". The content does not contain this text.`, ); } finalContent = finalContent.replaceAll(operation.find, operation.replace); } } // Apply prepend if (operations.content_prepend) { finalContent = operations.content_prepend + finalContent; } // Apply append if (operations.content_append) { finalContent = finalContent + operations.content_append; } return finalContent; }
- Helper function to validate that at least one content operation or additional field (like subject) is provided for the update.export function validateContentOperations( operations: ContentOperationParams, additionalFields: string[] = [], ): void { const hasContentOp = operations.content || operations.content_append || operations.content_prepend || operations.search_replace; const hasAdditionalFields = additionalFields.some( (field) => (operations as Record<string, unknown>)[field] !== undefined, ); if (!hasContentOp && !hasAdditionalFields) { const fieldsStr = [ "content", "partial operations", ...additionalFields, ].join(", "); throw new Error(`At least one field (${fieldsStr}) must be provided`); } }
- src/utils/contentOperations.ts:26-58 (schema)Shared Zod schema fields for content operations, spread into the tool's inputSchema to support partial updates.export const ContentOperationFields = { content: z .string() .optional() .describe( `If provided, replaces entire HTML content. Cannot be used with content_append, content_prepend, or search_replace.`, ), content_append: z .string() .optional() .describe( "Text to append to the end of current content. Cannot be used with content.", ), content_prepend: z .string() .optional() .describe( "Text to prepend to the beginning of current content. Cannot be used with content.", ), search_replace: z .array( z.object({ find: z.string().describe("Text to search for"), replace: z .string() .describe("Text to replace ALL the occurrences with"), }), ) .optional() .describe( "Array of search-replace operations to apply to current content. Cannot be used with content.", ), };