diff --git a/docs/research/README.md b/docs/research/README.md new file mode 100644 index 00000000..a0ccde93 --- /dev/null +++ b/docs/research/README.md @@ -0,0 +1,120 @@ +# MCP Component Research - Quick Summary + +**Research Date:** October 2025 (Revised after feedback) +**Full Report:** [mcp-component-analysis.md](./mcp-component-analysis.md) + +## Question + +Should Chartifact implement a Model Context Protocol (MCP) component for creating and editing interactive documents? + +## Answer + +**YES - Implement a lightweight transactional builder with MCP wrapper (1-2 days effort).** + +## Revised Analysis + +Initial recommendation was "No" based on overestimated complexity (5-7 weeks). After feedback, revised to lightweight approach: + +### Key Insights + +1. **Time estimate was way off**: 1-2 days for simple builder + MCP wrapper, not 5-7 weeks +2. **Multi-turn conversations benefit**: Token accumulation across many editing turns makes transactional operations valuable +3. **LLMs miss schema details**: Structured operations help prevent common mistakes +4. **Editor completion easier with builder**: Builder provides foundation for editor, not vice versa + +## Key Findings + +### 1. Multi-Turn Token Accumulation Matters +- Single document: 741 lines (~5,000 tokens) +- 20 editing turns: 100,000+ tokens (exhausts context) +- Transactional operations: Send only changes, not full document each time + +### 2. Schema Clarity vs. Practice +- Schema is clear in theory +- LLMs can miss details in practice +- Structured builder prevents violations +- Sensible defaults help starting point + +### 3. Lightweight Implementation +- **Effort:** 1-2 days, not 5-7 weeks +- **Code:** 350-600 lines, not 2,500-4,000 +- **Maintenance:** Minimal - maps to schema + +### 4. Editor Foundation +- Builder simplifies editor completion +- Ready-made operations (add, delete, update) +- Shared validation logic +- Easier undo/redo implementation + +## Benefits vs. Costs + +### Benefits (Revised) + +| Benefit | Value | Reason | +|---------|-------|--------| +| Token efficiency | High | Multi-turn conversations stay in context | +| Schema validation | High | Structured operations prevent mistakes | +| Editor foundation | High | Builder simplifies editor completion | +| Sensible defaults | High | Start with working document, not blank | +| MCP tool access | Medium | Standardized protocol for external tools | + +### Costs (Revised) + +| Cost | Effort | Details | +|------|--------|---------| +| Transactional builder | 1 day | Core operations, validation | +| MCP wrapper | 0.5 day | Thin shim over builder | +| File integration | 0.5 day | Load/save operations | +| **Total** | **1-2 days** | Focused development | + +**Verdict: Benefits outweigh costs** with lightweight approach. + +## Recommendation + +### Implement Lightweight Transactional Builder + MCP Wrapper + +**Why the change from original "No"?** + +1. **Complexity overestimated**: 1-2 days, not 5-7 weeks +2. **Multi-turn conversations**: Token accumulation is real issue +3. **LLMs miss schema**: Structured operations help in practice +4. **Editor needs foundation**: Builder makes editor easier to complete + +### Implementation Plan + +**Day 1: Transactional Builder** +- Create immutable document builder class +- Core operations: create, addGroup, addElement, delete* +- Type-safe API with validation +- Sensible defaults included + +**Day 1-2: MCP Wrapper** +- Thin shim exposing builder via MCP protocol +- Tool definitions with JSON schemas +- Simple state management + +**Day 2: File Integration** +- Load/save .idoc.json files +- Basic error handling + +**Next: Refactor Editor** +- Use builder as foundation +- Simpler implementation +- Ready-made operations + +## Conclusion + +**Revised recommendation: YES, implement lightweight version.** + +Original analysis overestimated complexity and underestimated benefits: +- **Complexity**: 1-2 days, not 5-7 weeks +- **Multi-turn value**: Token efficiency matters +- **Practical LLM behavior**: Structured operations help +- **Editor synergy**: Builder provides foundation + +**Next step:** Implement simple transactional builder with MCP wrapper. + +--- + +**Status:** Research revised based on feedback, ready for implementation +**See:** [Full updated analysis](./mcp-component-analysis.md) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md new file mode 100644 index 00000000..968af9dd --- /dev/null +++ b/docs/research/builder-api-sketch.md @@ -0,0 +1,1091 @@ +# Chartifact Document Builder API Sketch + +**Date:** October 2025 (Revised) +**Status:** API Design Proposal - Svelte Version + +## Overview + +This document sketches out the API for a lightweight transactional builder for Chartifact interactive documents. The builder provides: + +1. **Immutable operations** - Each operation returns a new builder instance +2. **Type safety** - TypeScript types prevent schema violations +3. **Sensible defaults** - Documents start with working structure +4. **Chainable API** - Fluent interface for ergonomic usage +5. **Validation** - Errors caught at operation time, not render time +6. **Minimal surface area** - Generic operations only, no type-specific helpers to reduce maintenance overhead + +## Core Builder Class + +```typescript +import { + InteractiveDocument, + ElementGroup, + PageElement, + InteractiveElement, + DataLoader, + Variable, + PageStyle +} from '@microsoft/chartifact-schema'; + +/** + * Immutable builder for creating and modifying Chartifact documents. + * Each operation returns a new builder instance with the updated document. + */ +class ChartifactBuilder { + private readonly doc: InteractiveDocument; + + constructor(initial?: Partial) { + this.doc = this.createDefault(initial); + } + + /** + * Get the current document as a plain object + */ + toJSON(): InteractiveDocument { + return JSON.parse(JSON.stringify(this.doc)); + } + + /** + * Get the current document as a JSON string + */ + toString(): string { + return JSON.stringify(this.doc, null, 2); + } + + /** + * Create a new builder from an existing document + */ + static fromDocument(doc: InteractiveDocument): ChartifactBuilder { + return new ChartifactBuilder(doc); + } + + /** + * Create a new builder from a JSON string + */ + static fromJSON(json: string): ChartifactBuilder { + return new ChartifactBuilder(JSON.parse(json)); + } + + // Note: File I/O operations (fromFile, toFile) are intentionally omitted. + // These should be handled by a dedicated I/O MCP server or external utilities. + // To save: write builder.toString() or builder.toJSON() to file + // To load: read file and use ChartifactBuilder.fromJSON() or fromDocument() + + // Private helper to create default document structure + private createDefault(partial?: Partial): InteractiveDocument { + return { + title: partial?.title || "New Document", + groups: partial?.groups || [{ + groupId: 'main', + elements: ['# Welcome\n\nStart building your interactive document.'] + }], + dataLoaders: partial?.dataLoaders || [], + variables: partial?.variables || [], + style: partial?.style, + resources: partial?.resources, + notes: partial?.notes, + ...partial + }; + } + + // Private helper for immutable updates + private clone(updates: Partial): ChartifactBuilder { + return new ChartifactBuilder({ + ...this.doc, + ...updates + }); + } +} +``` + +## Document-Level Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Set the document title + */ + setTitle(title: string): ChartifactBuilder { + return this.clone({ title }); + } + + /** + * Set or update the CSS styles + */ + setCSS(css: string | string[]): ChartifactBuilder { + return this.clone({ + style: { + ...this.doc.style, + css + } + }); + } + + /** + * Add CSS to existing styles + */ + addCSS(css: string): ChartifactBuilder { + const existingCSS = this.doc.style?.css || ''; + const newCSS = Array.isArray(existingCSS) + ? [...existingCSS, css] + : [existingCSS, css]; + return this.setCSS(newCSS); + } + + /** + * Set Google Fonts configuration + */ + setGoogleFonts(config: GoogleFontsSpec): ChartifactBuilder { + return this.clone({ + style: { + ...this.doc.style, + css: this.doc.style?.css || '', + googleFonts: config + } + }); + } + + /** + * Add a note to the document + */ + addNote(note: string): ChartifactBuilder { + return this.clone({ + notes: [...(this.doc.notes || []), note] + }); + } + + /** + * Clear all notes + */ + clearNotes(): ChartifactBuilder { + return this.clone({ notes: [] }); + } +} +``` + +## Group Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add a new group to the document with optional initial elements. + * This is the preferred way to create groups with content in bulk. + * + * @param groupId - Unique identifier for the group + * @param elements - Array of elements to initialize the group with (default: empty array) + */ + addGroup(groupId: string, elements: PageElement[] = []): ChartifactBuilder { + // Validate groupId doesn't already exist + if (this.doc.groups.some(g => g.groupId === groupId)) { + throw new Error(`Group '${groupId}' already exists`); + } + + return this.clone({ + groups: [ + ...this.doc.groups, + { groupId, elements } + ] + }); + } + + /** + * Insert a group at a specific index + */ + insertGroup(index: number, groupId: string, elements: PageElement[] = []): ChartifactBuilder { + if (this.doc.groups.some(g => g.groupId === groupId)) { + throw new Error(`Group '${groupId}' already exists`); + } + + const groups = [...this.doc.groups]; + groups.splice(index, 0, { groupId, elements }); + + return this.clone({ groups }); + } + + /** + * Remove a group by ID + */ + deleteGroup(groupId: string): ChartifactBuilder { + const groups = this.doc.groups.filter(g => g.groupId !== groupId); + + if (groups.length === this.doc.groups.length) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Rename a group + */ + renameGroup(oldGroupId: string, newGroupId: string): ChartifactBuilder { + if (this.doc.groups.some(g => g.groupId === newGroupId)) { + throw new Error(`Group '${newGroupId}' already exists`); + } + + const groups = this.doc.groups.map(g => + g.groupId === oldGroupId + ? { ...g, groupId: newGroupId } + : g + ); + + if (groups.every(g => g.groupId !== newGroupId)) { + throw new Error(`Group '${oldGroupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Move a group to a new position + */ + moveGroup(groupId: string, toIndex: number): ChartifactBuilder { + const fromIndex = this.doc.groups.findIndex(g => g.groupId === groupId); + + if (fromIndex === -1) { + throw new Error(`Group '${groupId}' not found`); + } + + const groups = [...this.doc.groups]; + const [group] = groups.splice(fromIndex, 1); + groups.splice(toIndex, 0, group); + + return this.clone({ groups }); + } +} +``` + +## Element Operations + +**Philosophy: Keep it svelte.** We provide only generic element operations. +Type-specific helpers (addMarkdown, addChart, etc.) are intentionally omitted to minimize maintenance overhead. +Users should construct element objects directly and use the generic operations. + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add an element to a group + */ + addElement(groupId: string, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [...g.elements, element] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Add multiple elements to a group + */ + addElements(groupId: string, elements: PageElement[]): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [...g.elements, ...elements] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Insert an element at a specific index in a group + */ + insertElement(groupId: string, index: number, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + const elements = [...g.elements]; + elements.splice(index, 0, element); + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Remove an element by index from a group + */ + deleteElement(groupId: string, elementIndex: number): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + if (elementIndex < 0 || elementIndex >= g.elements.length) { + throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); + } + const elements = g.elements.filter((_, i) => i !== elementIndex); + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Update an element at a specific index + */ + updateElement(groupId: string, elementIndex: number, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + if (elementIndex < 0 || elementIndex >= g.elements.length) { + throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); + } + const elements = [...g.elements]; + elements[elementIndex] = element; + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Clear all elements from a group + */ + clearElements(groupId: string): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Replace all elements in a group with a new array. + * This is the preferred way to update group content in bulk rather than + * tracking individual element additions/deletions. + * + * @param groupId - The group to update + * @param elements - New array of elements to replace existing content + */ + setGroupElements(groupId: string, elements: PageElement[]): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } +} + +// Example usage with generic operations: +// Individual element operations (available but typically too granular): +// For markdown (string): +builder.addElement('main', '# Hello World') + +// For a chart: +builder.addElement('main', { type: 'chart', chartKey: 'myChart' }) + +// For a checkbox: +builder.addElement('main', { type: 'checkbox', variableId: 'showDetails', label: 'Show Details' }) + +// For a slider: +builder.addElement('main', { type: 'slider', variableId: 'year', min: 2020, max: 2024, step: 1 }) + +// Bulk operations (RECOMMENDED - preferred for group content management): +// Create group with initial elements +builder.addGroup('dashboard', [ + '# Sales Dashboard', + '## Key Metrics', + { type: 'chart', chartKey: 'revenue' } +]) + +// Replace entire group content +builder.setGroupElements('dashboard', [ + '# Updated Dashboard', + { type: 'chart', chartKey: 'newChart' } +]) +``` + +## Data & Variable Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add a variable to the document + */ + addVariable(variable: Variable): ChartifactBuilder { + // Validate variable ID doesn't already exist + if (this.doc.variables?.some(v => v.variableId === variable.variableId)) { + throw new Error(`Variable '${variable.variableId}' already exists`); + } + + return this.clone({ + variables: [...(this.doc.variables || []), variable] + }); + } + + /** + * Update an existing variable + */ + updateVariable(variableId: string, updates: Partial): ChartifactBuilder { + const variables = (this.doc.variables || []).map(v => + v.variableId === variableId + ? { ...v, ...updates } + : v + ); + + if (variables === this.doc.variables) { + throw new Error(`Variable '${variableId}' not found`); + } + + return this.clone({ variables }); + } + + /** + * Remove a variable + */ + deleteVariable(variableId: string): ChartifactBuilder { + const variables = (this.doc.variables || []).filter( + v => v.variableId !== variableId + ); + + if (variables.length === this.doc.variables?.length) { + throw new Error(`Variable '${variableId}' not found`); + } + + return this.clone({ variables }); + } + + /** + * Add a data loader + */ + addDataLoader(dataLoader: DataLoader): ChartifactBuilder { + // Validate dataSourceName doesn't already exist + const name = 'dataSourceName' in dataLoader ? dataLoader.dataSourceName : undefined; + if (name && this.doc.dataLoaders?.some(dl => 'dataSourceName' in dl && dl.dataSourceName === name)) { + throw new Error(`Data loader '${name}' already exists`); + } + + return this.clone({ + dataLoaders: [...(this.doc.dataLoaders || []), dataLoader] + }); + } + + /** + * Remove a data loader + */ + deleteDataLoader(dataSourceName: string): ChartifactBuilder { + const dataLoaders = (this.doc.dataLoaders || []).filter( + dl => !('dataSourceName' in dl) || dl.dataSourceName !== dataSourceName + ); + + if (dataLoaders.length === this.doc.dataLoaders?.length) { + throw new Error(`Data loader '${dataSourceName}' not found`); + } + + return this.clone({ dataLoaders }); + } +} +``` + +## Resources Operations + +**Note:** Renamed from "Chart Resources" to just "Resources" for generality. +Resources can contain charts and potentially other types in the future. + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add or update a resource in the resources section. + * This includes charts (stored in resources.charts) and potentially other resource types in the future. + * + * @param resourceType - The type of resource (e.g., 'charts') + * @param resourceKey - The key/name for this resource + * @param spec - The resource specification (e.g., Vega/Vega-Lite spec for charts) + */ + setResource(resourceType: string, resourceKey: string, spec: object): ChartifactBuilder { + const resources = { + ...(this.doc.resources?.[resourceType] || {}), + [resourceKey]: spec + }; + + return this.clone({ + resources: { + ...this.doc.resources, + [resourceType]: resources + } + }); + } + + /** + * Remove a resource from the resources section + * + * @param resourceType - The type of resource (e.g., 'charts') + * @param resourceKey - The key/name for this resource + */ + deleteResource(resourceType: string, resourceKey: string): ChartifactBuilder { + if (!this.doc.resources?.[resourceType]?.[resourceKey]) { + throw new Error(`Resource '${resourceKey}' not found in '${resourceType}'`); + } + + const { [resourceKey]: _, ...resources } = this.doc.resources[resourceType]; + + return this.clone({ + resources: { + ...this.doc.resources, + [resourceType]: resources + } + }); + } +} + +// Convenience methods for charts (most common resource type) +// Example usage: +// builder.setResource('charts', 'myChart', vegaSpec) +// builder.deleteResource('charts', 'myChart') +``` + +## Usage Examples + +### Example 1: Create a Dashboard with Bulk Group Operations (Recommended) + +```typescript +// Preferred approach: Use bulk operations for group content +const builder = new ChartifactBuilder() + .setTitle('Sales Dashboard') + .setCSS(` + body { font-family: sans-serif; } + .container { max-width: 1200px; margin: 0 auto; } + `) + .addDataLoader({ + type: 'inline', + dataSourceName: 'sales', + content: [ + { month: 'Jan', revenue: 10000, profit: 2000 }, + { month: 'Feb', revenue: 12000, profit: 2400 }, + { month: 'Mar', revenue: 15000, profit: 3000 } + ] + }) + .setResource('charts', 'revenueChart', { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + data: { name: 'sales' }, + mark: 'bar', + encoding: { + x: { field: 'month', type: 'ordinal' }, + y: { field: 'revenue', type: 'quantitative' } + } + }) + // Create group with all elements at once (recommended pattern) + .addGroup('main', [ + '# Sales Dashboard', + '\n\nView key metrics below.\n', + { type: 'chart', chartKey: 'revenueChart' } + ]); + +// Export to JSON string for saving (use external I/O utility) +const json = builder.toString(); +// Or get as object: const doc = builder.toJSON(); +``` + +### Example 2: Add Interactive Controls + +```typescript +const builder = ChartifactBuilder.fromDocument(existingDoc) + .addVariable({ + variableId: 'yearFilter', + type: 'number', + initialValue: 2024 + }) + .addVariable({ + variableId: 'showDetails', + type: 'boolean', + initialValue: false + }) + // Create group with all control elements at once (recommended pattern) + .addGroup('controls', [ + '## Settings', + { + type: 'slider', + variableId: 'yearFilter', + min: 2020, + max: 2024, + step: 1, + label: 'Select Year' + }, + { + type: 'checkbox', + variableId: 'showDetails', + label: 'Show detailed view' + } + ]); +``` + +### Example 3: Modify Existing Document + +```typescript +// Load from JSON string (external I/O handled separately) +const jsonString = readFileSync('./document.idoc.json', 'utf-8'); +const builder = ChartifactBuilder.fromJSON(jsonString); + +// Get current document to inspect +const doc = builder.toJSON(); +const mainElements = doc.groups.find(g => g.groupId === 'main')?.elements || []; + +// Make modifications - replace first element with new content +const updated = builder + .setTitle('Updated Title') + .setGroupElements('main', ['# New Header', ...mainElements.slice(1)]) + .addGroup('footer', ['Built with Chartifact']) + .addNote('Updated on ' + new Date().toISOString()); + +// Export (external I/O handled separately) +const updatedJson = updated.toString(); +// writeFileSync('./document.idoc.json', updatedJson); +``` + +### Example 4: Chaining Operations + +```typescript +const doc = new ChartifactBuilder({ title: 'My Report' }) + .addDataLoader({ + type: 'inline', + dataSourceName: 'kpis', + content: [ + { name: 'Revenue', value: 1000000 }, + { name: 'Growth', value: 15 } + ] + }) + .setResource('charts', 'trend', { /* vega spec */ }) + .setCSS('.header { text-align: center; }') + // Create all groups with their content at once (recommended pattern) + .addGroup('header', [ + '# Annual Report 2024', + 'Executive Summary' + ]) + .addGroup('metrics', [ + { + type: 'tabulator', + dataSourceName: 'kpis', + editable: false + } + ]) + .addGroup('charts', [ + { type: 'chart', chartKey: 'trend' } + ]) + .toJSON(); +``` + +## Reading State Strategy + +**Strategy:** Since the builder wraps a simple JSON object, the LLM can access the document directly when needed. + +- **Primary access:** Use `toJSON()` to get the complete document +- **No individual getters:** Avoid maintenance overhead of getter methods for every property +- **Direct inspection:** The LLM can read any part of the returned JSON object as needed + +**Usage:** +```typescript +const doc = builder.toJSON(); +// Access any property directly from the JSON: +const title = doc.title; +const groups = doc.groups; +const firstGroup = doc.groups[0]; +const css = doc.style?.css; +const variables = doc.variables; +// etc. +``` + +**MCP Usage:** MCP tools call `toJSON()` after operations to return the updated document. The LLM can then inspect any part of the document it needs. + +## Validation & Error Handling + +The builder includes validation at each operation: + +```typescript +// Validation examples +try { + builder + .addGroup('main', []) // Error: 'main' already exists + .deleteGroup('nonexistent') // Error: Group not found + .deleteElement('main', 999) // Error: Index out of bounds + .addVariable({ /* duplicate variableId */ }); // Error: Variable exists +} catch (error) { + console.error('Builder operation failed:', error.message); +} +``` + +## MCP Tool Mapping + +Each builder method maps naturally to an MCP tool. Note the svelte approach - generic operations only, no individual getters: + +| MCP Tool | Builder Method | Parameters | +|----------|---------------|------------| +| `create_document` | `new ChartifactBuilder()` | `{ title?, groups?, ... }` | +| `from_json` | `ChartifactBuilder.fromJSON()` | `{ json }` | +| `to_json` | `.toJSON()` | `{}` | +| `set_title` | `.setTitle()` | `{ title }` | +| `set_css` | `.setCSS()` | `{ css }` | +| `add_css` | `.addCSS()` | `{ css }` | +| `add_group` | `.addGroup()` | `{ groupId, elements? }` | +| `insert_group` | `.insertGroup()` | `{ index, groupId, elements? }` | +| `delete_group` | `.deleteGroup()` | `{ groupId }` | +| `rename_group` | `.renameGroup()` | `{ oldGroupId, newGroupId }` | +| `move_group` | `.moveGroup()` | `{ groupId, toIndex }` | +| `add_element` | `.addElement()` | `{ groupId, element }` | +| `add_elements` | `.addElements()` | `{ groupId, elements }` | +| `insert_element` | `.insertElement()` | `{ groupId, index, element }` | +| `delete_element` | `.deleteElement()` | `{ groupId, elementIndex }` | +| `update_element` | `.updateElement()` | `{ groupId, elementIndex, element }` | +| `clear_elements` | `.clearElements()` | `{ groupId }` | +| `set_group_elements` | `.setGroupElements()` | `{ groupId, elements }` | +| `add_variable` | `.addVariable()` | `{ variable }` | +| `update_variable` | `.updateVariable()` | `{ variableId, updates }` | +| `delete_variable` | `.deleteVariable()` | `{ variableId }` | +| `add_data_loader` | `.addDataLoader()` | `{ dataLoader }` | +| `delete_data_loader` | `.deleteDataLoader()` | `{ dataSourceName }` | +| `set_resource` | `.setResource()` | `{ resourceType, resourceKey, spec }` | +| `delete_resource` | `.deleteResource()` | `{ resourceType, resourceKey }` | +| `add_note` | `.addNote()` | `{ note }` | +| `clear_notes` | `.clearNotes()` | `{}` | + +**Reading state:** MCP tools call `toJSON()` to get the complete document. The LLM can then inspect any part of the JSON directly. + +**Note on I/O:** File operations (save, load) are handled by a dedicated I/O MCP server, not the builder. + +## Transactional API for MCP + +**Question:** Can MCP take advantage of chainability? + +**Answer:** MCP tools operate one at a time, so direct chainability isn't available. However, we can provide a transactional API that accepts an array of operations to apply atomically. + +### Transaction Types + +```typescript +type Transaction = + | { op: 'setTitle', title: string } + | { op: 'setCSS', css: string | string[] } + | { op: 'addCSS', css: string } + | { op: 'setGoogleFonts', config: GoogleFontsSpec } + | { op: 'addNote', note: string } + | { op: 'clearNotes' } + | { op: 'addGroup', groupId: string, elements?: PageElement[] } + | { op: 'insertGroup', index: number, groupId: string, elements?: PageElement[] } + | { op: 'deleteGroup', groupId: string } + | { op: 'renameGroup', oldGroupId: string, newGroupId: string } + | { op: 'moveGroup', groupId: string, toIndex: number } + | { op: 'addElement', groupId: string, element: PageElement } + | { op: 'addElements', groupId: string, elements: PageElement[] } + | { op: 'insertElement', groupId: string, index: number, element: PageElement } + | { op: 'deleteElement', groupId: string, elementIndex: number } + | { op: 'updateElement', groupId: string, elementIndex: number, element: PageElement } + | { op: 'clearElements', groupId: string } + | { op: 'setGroupElements', groupId: string, elements: PageElement[] } + | { op: 'addVariable', variable: Variable } + | { op: 'updateVariable', variableId: string, updates: Partial } + | { op: 'deleteVariable', variableId: string } + | { op: 'addDataLoader', dataLoader: DataLoader } + | { op: 'deleteDataLoader', dataSourceName: string } + | { op: 'setResource', resourceType: string, resourceKey: string, spec: object } + | { op: 'deleteResource', resourceType: string, resourceKey: string }; +``` + +### Apply Transactions Method + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Apply an array of transactions atomically. + * This is useful for MCP where chainability isn't available, + * allowing multiple operations to be applied in a single call. + * + * @param transactions - Array of transaction objects to apply + * @returns New builder instance with all transactions applied + */ + applyTransactions(transactions: Transaction[]): ChartifactBuilder { + let builder: ChartifactBuilder = this; + + for (const tx of transactions) { + switch (tx.op) { + case 'setTitle': + builder = builder.setTitle(tx.title); + break; + case 'setCSS': + builder = builder.setCSS(tx.css); + break; + case 'addCSS': + builder = builder.addCSS(tx.css); + break; + case 'setGoogleFonts': + builder = builder.setGoogleFonts(tx.config); + break; + case 'addNote': + builder = builder.addNote(tx.note); + break; + case 'clearNotes': + builder = builder.clearNotes(); + break; + case 'addGroup': + builder = builder.addGroup(tx.groupId, tx.elements); + break; + case 'insertGroup': + builder = builder.insertGroup(tx.index, tx.groupId, tx.elements); + break; + case 'deleteGroup': + builder = builder.deleteGroup(tx.groupId); + break; + case 'renameGroup': + builder = builder.renameGroup(tx.oldGroupId, tx.newGroupId); + break; + case 'moveGroup': + builder = builder.moveGroup(tx.groupId, tx.toIndex); + break; + case 'addElement': + builder = builder.addElement(tx.groupId, tx.element); + break; + case 'addElements': + builder = builder.addElements(tx.groupId, tx.elements); + break; + case 'insertElement': + builder = builder.insertElement(tx.groupId, tx.index, tx.element); + break; + case 'deleteElement': + builder = builder.deleteElement(tx.groupId, tx.elementIndex); + break; + case 'updateElement': + builder = builder.updateElement(tx.groupId, tx.elementIndex, tx.element); + break; + case 'clearElements': + builder = builder.clearElements(tx.groupId); + break; + case 'setGroupElements': + builder = builder.setGroupElements(tx.groupId, tx.elements); + break; + case 'addVariable': + builder = builder.addVariable(tx.variable); + break; + case 'updateVariable': + builder = builder.updateVariable(tx.variableId, tx.updates); + break; + case 'deleteVariable': + builder = builder.deleteVariable(tx.variableId); + break; + case 'addDataLoader': + builder = builder.addDataLoader(tx.dataLoader); + break; + case 'deleteDataLoader': + builder = builder.deleteDataLoader(tx.dataSourceName); + break; + case 'setResource': + builder = builder.setResource(tx.resourceType, tx.resourceKey, tx.spec); + break; + case 'deleteResource': + builder = builder.deleteResource(tx.resourceType, tx.resourceKey); + break; + } + } + + return builder; + } +} +``` + +### Transactional Usage Example + +```typescript +// Individual element operations (available but typically too granular): +// builder.setTitle('Dashboard').addGroup('main').addElement('main', '# Hello') + +// Transactions support individual operations: +const updated = builder.applyTransactions([ + { op: 'setTitle', title: 'Dashboard' }, + { op: 'addGroup', groupId: 'main', elements: [] }, + { op: 'addElement', groupId: 'main', element: '# Hello' }, + { op: 'setResource', resourceType: 'charts', resourceKey: 'myChart', spec: { /* ... */ } }, + { op: 'addElement', groupId: 'main', element: { type: 'chart', chartKey: 'myChart' } } +]); + +// RECOMMENDED: Use bulk element operations for group content management +const updatedBulk = builder.applyTransactions([ + { op: 'setTitle', title: 'Sales Dashboard' }, + { + op: 'addGroup', + groupId: 'header', + elements: [ + '# Sales Dashboard', + '## Q4 2024 Performance' + ] + }, + { + op: 'addGroup', + groupId: 'charts', + elements: [ + { type: 'chart', chartKey: 'revenue' }, + { type: 'chart', chartKey: 'profit' } + ] + } +]); + +// Update entire group content at once (typical pattern) +const replaced = builder.applyTransactions([ + { + op: 'setGroupElements', + groupId: 'dashboard', + elements: [ + '# Updated Dashboard', + '## New Content', + { type: 'chart', chartKey: 'newChart' }, + { type: 'tabulator', dataSourceName: 'metrics' } + ] + } +]); + +const doc = updated.toJSON(); +``` + +### MCP Tool for Transactions + +```typescript +// MCP tool definition +{ + name: "apply_transactions", + description: "Apply multiple document operations atomically", + inputSchema: { + type: "object", + properties: { + transactions: { + type: "array", + items: { + // Transaction union type schema + } + } + } + } +} +``` + +**Benefits of Transactional API:** +- **Atomic operations** - All transactions succeed or fail together +- **Efficient** - Single MCP call instead of multiple round-trips +- **Flexible** - LLM can batch multiple edits +- **Compatible with chainable API** - Both patterns coexist + +**MCP Usage:** The LLM can either call individual tools (one operation at a time) or use `apply_transactions` (multiple operations at once). + +## Implementation Notes + +### Svelte Philosophy +**Keep maintenance overhead minimal:** +- Generic operations only (no type-specific helpers like `addMarkdown`, `addChart`, etc.) +- Users construct element objects directly +- Reduces API surface area +- Less code to maintain as schema evolves + +### Immutability +All operations return new builder instances. This enables: +- Undo/redo by keeping history +- Safe concurrent operations +- Easier testing and debugging + +### Type Safety +TypeScript types ensure: +- Valid element types +- Required properties present +- Correct data structures +- IDE autocomplete support + +### Reading State Strategy +- **No individual getters** - Avoids maintenance overhead +- **Use `toJSON()` for all reads** - Returns complete document as plain JSON +- **LLM inspects JSON directly** - Can access any property from the returned object +- **Simple and flexible** - No need to add getters as schema evolves + +### Bulk Operations Pattern (Recommended) +- **Group-level management** - Prefer `addGroup(id, elements[])` and `setGroupElements(id, elements[])` over individual element operations +- **Less granular tracking** - Individual element add/delete/update operations available but typically too granular +- **Simpler mental model** - Think of groups as content units rather than managing elements individually +- **Better for MCP** - Fewer transactions needed, more atomic updates +- **Typical workflow**: Create group with initial content, then use `setGroupElements()` to replace content when needed + +### Performance +- Shallow copying for immutability +- JSON serialization only when needed +- Validation is O(n) or better +- No deep cloning unless necessary + +### I/O Separation +- Builder focuses on document manipulation +- File I/O handled by dedicated utilities or MCP server +- Cleaner separation of concerns +- Easier to test builder in isolation + +### Extension Points +The builder can be extended: +- Custom validators +- Plugin operations (if needed) +- Lifecycle hooks (if needed) + +## Next Steps + +1. **Implement core builder** (~1 day) + - Document operations + - Group operations + - Element operations (generic only) + - Variable operations + - Data loader operations + - Resources operations + - Validation + - Single `toJSON()` method for reading state + +2. **Create MCP wrapper** (~0.5-1 day) + - Tool definitions for each operation + - Parameter schemas (auto-generated from TypeScript) + - Error mapping + - State management (builder instance per session) + - Return updated document via `toJSON()` after each operation + +3. **Testing & Documentation** (ongoing) + - Unit tests for each operation + - Integration examples + - API documentation + - Usage patterns + +**Note:** File I/O intentionally omitted - handled by separate I/O MCP server or external utilities. + +Total estimated effort: **1-2 days** diff --git a/docs/research/mcp-component-analysis.md b/docs/research/mcp-component-analysis.md new file mode 100644 index 00000000..89f57373 --- /dev/null +++ b/docs/research/mcp-component-analysis.md @@ -0,0 +1,563 @@ +# MCP Component Research Report for Chartifact + +**Date:** October 2025 +**Author:** Research Analysis +**Status:** Revised Recommendation - Implement Lightweight Version + +## Executive Summary + +This report evaluates whether Chartifact should implement a Model Context Protocol (MCP) component for creating and editing interactive documents. After thorough analysis and feedback, **we recommend implementing a lightweight transactional builder with an MCP wrapper**. + +**Revised Analysis:** While the JSON format is well-suited for LLM-based editing, a simple transactional builder (1-2 days effort) would provide value by: +1. Offering structured operations that help LLMs avoid schema mistakes +2. Supporting multi-turn conversations where token accumulation matters +3. Serving as a foundation for completing the editor package +4. Enabling a thin MCP wrapper for standardized tool access + +## Table of Contents + +1. [Background](#background) +2. [What is MCP?](#what-is-mcp) +3. [Current State Analysis](#current-state-analysis) +4. [MCP Implementation Requirements](#mcp-implementation-requirements) +5. [Use Case Analysis](#use-case-analysis) +6. [Cost-Benefit Analysis](#cost-benefit-analysis) +7. [Comparison to Similar Projects](#comparison-to-similar-projects) +8. [Recommendation](#recommendation) +9. [Future Considerations](#future-considerations) + +## Background + +The problem statement raised the question: + +> "It is unclear if there is a need for an mcp component to create/edit documents since the json format can be mutated by an LLM in context. Note that the editor package (although incomplete) sends delta documents instead of transactions." + +This research investigates whether an MCP server would provide value for Chartifact document manipulation. + +## What is MCP? + +Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables standardized integration between LLM applications and external data sources/tools. MCP servers expose: + +1. **Resources**: Files, database records, API responses +2. **Tools**: Functions that can be called by the LLM +3. **Prompts**: Templated messages for common tasks + +MCP provides a standardized way for AI models to securely access and manipulate external systems. + +## Current State Analysis + +### Document Size Analysis + +Analysis of example documents in `packages/web-deploy/json/`: + +| Document Type | Lines | Approximate Tokens | +|--------------|-------|-------------------| +| Simple (grocery-list) | 230 | ~700-1,000 | +| Medium (activity-rings) | 500 | ~1,500-2,500 | +| Complex (habit-tracker) | 741 | ~2,200-3,700 | + +**Key Finding:** Even the largest Chartifact documents are under 1,000 lines and ~5,000 tokens. Modern LLMs (GPT-4, Claude) have 100K+ token context windows, so **all documents fit entirely in context with significant room for conversation**. + +**Multi-Turn Consideration:** However, in a conversation with many editing turns (e.g., creating a "masterpiece" iteratively), token usage accumulates: +- 10 editing turns × 5,000 tokens per full document = 50,000 tokens +- 20 turns would exceed typical context windows +- A transactional approach (sending only changes) reduces this significantly + +### Current Editor Implementation + +Examined code in `packages/editor/src/editor.tsx`: + +```typescript +const sendEditToApp = (newPage: InteractiveDocument) => { + const pageMessage: EditorPageMessage = { + type: 'editorPage', + page: newPage, + sender: 'editor' + }; + postMessageTarget.postMessage(pageMessage, '*'); +}; +``` + +**Key Findings:** + +1. **No Delta/Patch System:** Despite the problem statement mentioning "delta documents," the code shows the editor sends **entire documents** via `EditorPageMessage` +2. **Simple Operations:** Only basic operations implemented: `deleteElement`, `deleteGroup` +3. **No Transactions:** No transaction IDs, optimistic locking, or conflict resolution +4. **PostMessage Architecture:** Uses browser postMessage API between editor and host + +### VSCode Extension + +From `packages/vscode/src/web/command-edit.ts`: + +- Reads `.idoc.json` and `.idoc.md` files directly +- Sends complete documents to editor webview +- No sophisticated transaction system +- Uses standard file system operations + +### JSON Format Characteristics + +The `InteractiveDocument` schema (see `docs/schema/idoc_v1.d.ts`): + +- Well-structured, predictable schema +- Clear hierarchy: document → groups → elements +- Strongly typed with TypeScript definitions +- JSON Schema available for validation +- Designed to be human-readable and LLM-friendly + +**Schema Clarity in Practice:** While the schema is clear, real-world usage shows LLMs can miss schema details, especially in complex documents. A transactional builder with well-defined operations (e.g., `addGroup`, `addElement`) would provide: +- Type-safe operations that prevent schema violations +- Starting point with sensible defaults +- Clearer API surface than raw JSON manipulation +- Better error messages when operations fail + +## MCP Implementation Requirements + +### Original (Over-Engineered) Estimate + +Initial analysis suggested a comprehensive implementation requiring 2,500-4,000 lines of code over 5-7 weeks. This included full transaction systems, conflict resolution, file watching, and extensive tooling. + +### Revised (Lightweight) Approach + +**Realistic Implementation: 1-2 focused days** + +The key insight is to start minimal and iterate: + +#### 1. Transactional Builder (~200-300 lines) + +A simple builder class with core operations: +- `create()` - Initialize document with sensible defaults +- `addGroup(id, props?)` - Add group to document +- `addElement(groupId, element)` - Add element to group +- `deleteGroup(groupId)` - Remove group +- `deleteElement(groupId, elementIndex)` - Remove element +- `setTitle(title)` - Update document title +- `setCss(css)` - Update layout CSS +- `toJSON()` - Export current state + +Benefits: +- Type-safe operations +- Validation at each step +- Immutable updates (returns new state) +- Small, focused API surface + +#### 2. MCP Wrapper (~100-200 lines) + +A thin shim that exposes the builder via MCP protocol: +- Tool definitions for each builder method +- Parameter schemas (auto-generated from TypeScript) +- Error handling and validation +- State management (one document per session) + +The MCP server scaffolding is straightforward: +- Use existing MCP SDK +- Map tool calls to builder methods +- Return results in MCP format + +#### 3. File Integration (~50-100 lines) + +Basic file operations: +- Load from `.idoc.json` +- Save to `.idoc.json` +- Simple error handling + +**Total Realistic Implementation:** 350-600 lines of focused code + +**Timeline:** 1-2 days with proper focus, not 5-7 weeks + +**Maintenance:** Minimal - builder operations map 1:1 to schema, MCP wrapper is thin + +## Use Case Analysis + +### Scenario 1: Single Document Creation + +**WITHOUT Builder:** +``` +LLM: [Generates complete JSON document] +Result: Document created in 1 step, ~500 tokens +``` + +**WITH Builder:** +``` +LLM: [Calls create_document with defaults] + [Calls addGroup, addElement as needed] +Result: Document created in 3-5 operations, clearer structure +``` + +**Analysis:** Builder provides sensible defaults and prevents schema violations. Slight overhead but better guardrails. + +### Scenario 2: Multi-Turn Editing Session (Critical Case) + +**WITHOUT Builder (Full Document Each Time):** +``` +Turn 1: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +Turn 2: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +Turn 3: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +... +Turn 20: Context exhausted at 100K tokens +``` + +**WITH Builder (Transactional Operations):** +``` +Turn 1: LLM reads doc (5K tokens) → calls addElement (50 tokens) +Turn 2: Calls updateTitle (30 tokens) +Turn 3: Calls deleteGroup (20 tokens) +... +Turn 20: Still have 40K tokens of context remaining +``` + +**Analysis:** This is where the builder shines. Multi-turn conversations benefit significantly from transactional operations vs. sending full documents repeatedly. + +### Scenario 3: Schema Mistakes + +**WITHOUT Builder:** +``` +LLM: [Generates JSON with subtle schema violation] + [e.g., wrong property name, missing required field] +System: [Fails at render time or silently breaks] +User: [Has to debug and fix manually] +``` + +**WITH Builder:** +``` +LLM: [Calls builder.addElement with invalid params] +Builder: [Validation fails immediately] + [Returns clear error: "element must have 'type' property"] +LLM: [Corrects and retries] +``` + +**Analysis:** Builder catches errors at operation time, not render time. Better DX and prevents broken documents. +LLM: [Reads full document] + [Plans multiple changes] + [Calls update_element multiple times] + [Calls add_variable] + [Calls update_css] +Result: Changes applied in 5+ tool calls, each a network round-trip +``` + +**Analysis:** MCP is actually SLOWER for multi-step edits due to multiple round-trips. Without MCP, LLM can apply all changes in one operation. + +### Scenario 4: Collaborative Editing (Future) + +**WITHOUT MCP:** +``` +User A: [Makes edit, saves file] +User B: [Makes edit to same file] +Result: Last write wins, potential data loss +``` + +**WITH MCP:** +``` +User A: [Calls update_element] +MCP: [Applies with transaction ID] +User B: [Calls update_element on same element] +MCP: [Detects conflict, resolves or rejects] +Result: Conflict handled gracefully +``` + +**Analysis:** MCP provides value for collaborative editing. However, this is not a current requirement, and the editor package doesn't support this scenario yet. + +## Cost-Benefit Analysis + +### Revised Implementation Costs + +| Category | Estimated Effort | Details | +|----------|-----------------|---------| +| Transactional Builder | 1 day | Core operations, type-safe API, validation | +| MCP Wrapper | 0.5 days | Thin shim over builder, tool definitions | +| File Integration | 0.5 days | Load/save, basic error handling | +| **Total Initial** | **1-2 days** | Focused development time | +| Testing | Included | Test builder operations as developed | +| Documentation | Minimal | API is self-documenting, add examples | +| Maintenance | Low | Builder maps to schema, MCP is thin wrapper | + +### Operational Benefits + +- **Reduced token usage in multi-turn conversations**: Transactional operations vs. full document resends +- **Schema validation**: Type-safe operations prevent common mistakes +- **Editor foundation**: Builder can be reused by editor package +- **Sensible defaults**: Starting with working skeleton vs. blank document +- **Clear API**: Well-defined operations vs. raw JSON manipulation +- **Future-proof**: Easy to extend with new operations + +### Benefits (Revised) + +| Benefit | Value | Notes | +|---------|-------|-------| +| Structured operations | **High** | Helps LLMs avoid schema mistakes | +| Token efficiency | **Medium** | Matters in multi-turn editing sessions | +| Editor foundation | **High** | Builder simplifies editor completion | +| Type safety | **Medium** | Runtime validation catches errors early | +| Sensible defaults | **High** | Better starting point than blank slate | +| External tool integration | **Medium** | MCP provides standardized access | + +### Verdict + +**Benefits outweigh costs** with the lightweight approach. 1-2 days of work provides significant value, especially for: +1. Multi-turn editing conversations +2. Completing the editor package +3. Preventing schema violations +4. Standardized tool access via MCP + +## Comparison to Similar Projects + +### Projects WITH MCP Servers + +**Jupyter Notebooks:** +- Complex structure (code cells + output + metadata) +- Need to execute code server-side +- State management between cells +- Multiple kernel types +- Justification: Complexity requires abstraction + +**Database Systems:** +- Large data sets don't fit in context +- Need optimized query execution +- ACID transactions required +- Security and access control +- Justification: Scale requires protocol + +### Projects WITHOUT MCP Servers + +**Markdown Editors:** +- LLMs edit markdown directly +- Full file fits in context +- No special protocol needed +- Works well in practice + +**Configuration Files (JSON/YAML):** +- Tools edit directly via file system +- Schema validation via JSON Schema +- No MCP servers exist +- Chartifact is similar to this category + +**Conclusion:** Projects with simple, structured formats (like Chartifact) don't typically use MCP for editing. The protocol adds unnecessary complexity. + +## Recommendation + +### Revised Recommendation: IMPLEMENT LIGHTWEIGHT TRANSACTIONAL BUILDER WITH MCP WRAPPER + +**Rationale:** + +1. **Multi-Turn Conversations Benefit from Transactions** + - Editing a complex document across 20+ turns accumulates significant tokens + - Transactional operations send only changes, not full documents + - Reduces token usage and context window pressure + - Enables longer, more iterative editing sessions + +2. **LLMs Can Miss Schema Details** + - While the schema is clear, LLMs sometimes violate it in practice + - Transactional builder provides type-safe operations + - Validation at each step catches errors early + - Sensible defaults help LLMs start with working documents + +3. **Editor Completion is Easier WITH Transactional Builder** + - **Previous assumption was backwards**: The editor would be simpler to complete if built on a transactional foundation + - Builder provides ready-made operations (add, delete, update) + - Editor UI can directly call builder methods + - Shared validation and state management logic + - Undo/redo becomes easier with transaction history + +4. **Minimal Implementation Burden** + - **1-2 days**, not 5-7 weeks (original estimate was way off) + - ~350-600 lines of focused code + - Transactional builder is small API surface + - MCP wrapper is thin shim over builder + - Low maintenance - builder maps directly to schema + +5. **Multiple Benefits from Small Investment** + - Token efficiency in conversations + - Schema validation and error prevention + - Foundation for editor package + - Standardized MCP tool access + - Starting point with defaults vs. blank slate + +### Implementation Approach + +#### Step 1: Build Transactional Builder (Day 1) + +Create a simple, immutable document builder: + +```typescript +class ChartifactBuilder { + private doc: InteractiveDocument; + + constructor(initial?: Partial) { + this.doc = this.createDefault(initial); + } + + private createDefault(partial?: Partial): InteractiveDocument { + // Return document with sensible defaults + return { + title: partial?.title || "New Document", + layout: { css: partial?.layout?.css || "" }, + groups: partial?.groups || [], + variables: partial?.variables || [], + dataLoaders: partial?.dataLoaders || [], + ...partial + }; + } + + addGroup(groupId: string, elements: Element[] = []): ChartifactBuilder { + // Immutable operation, returns new builder + } + + addElement(groupId: string, element: Element): ChartifactBuilder { + // Find group, add element, return new builder + } + + deleteGroup(groupId: string): ChartifactBuilder { + // Remove group, return new builder + } + + toJSON(): InteractiveDocument { + return this.doc; + } +} +``` + +Benefits: +- Immutable operations (no mutation bugs) +- Type-safe (TypeScript catches errors) +- Chainable API (fluent interface) +- Sensible defaults included + +#### Step 2: Add MCP Wrapper (Day 1-2) + +Expose builder via MCP protocol: + +```typescript +// Define MCP tools +const tools = [ + { + name: "create_document", + description: "Create new document with optional properties", + inputSchema: { /* JSON schema */ } + }, + { + name: "add_group", + description: "Add a group to the document", + inputSchema: { /* JSON schema */ } + }, + // ... more tools +]; + +// Handle tool calls +async function handleToolCall(name: string, args: any) { + switch (name) { + case "create_document": + builder = new ChartifactBuilder(args); + return builder.toJSON(); + case "add_group": + builder = builder.addGroup(args.groupId, args.elements); + return builder.toJSON(); + // ... more handlers + } +} +``` + +#### Step 3: File Integration (Day 2) + +Add simple file operations: +- Load from filesystem +- Save to filesystem +- Basic error handling + +#### Step 4: Use in Editor Package + +Refactor editor to use builder: +- Replace manual document manipulation with builder calls +- Implement undo/redo using builder's immutable operations +- Add more editing operations easily + +Total effort: **1-2 focused days** + +## Future Considerations + +### When to Reconsider MCP + +Implement MCP if any of these conditions occur: + +#### Trigger 1: Documents Exceed Context Windows +- **Threshold:** Documents regularly exceed 50K tokens +- **Likelihood:** Low (current max is ~5K tokens) +- **Timeline:** Not expected in near future + +#### Trigger 2: Collaborative Editing Requirement +- **Threshold:** Users request real-time multi-user editing +- **Likelihood:** Medium (useful for teams) +- **Timeline:** Could emerge with product growth + +#### Trigger 3: External Tool Ecosystem +- **Threshold:** 3+ external tools need document manipulation +- **Likelihood:** Low (currently only VSCode + web viewer) +- **Timeline:** Depends on adoption + +#### Trigger 4: Editor Implements Transactions +- **Threshold:** Editor package has working transaction system +- **Likelihood:** High (natural evolution) +- **Timeline:** Could be next phase of editor development + +### Phased Approach Recommendation + +**Phase 1 (Current): Direct JSON Editing** ✅ +- LLMs edit JSON directly +- VSCode extension reads/writes files +- Web viewer uses postMessage +- Status: Working well, no issues + +**Phase 2 (If Needed): Transaction Library** +- Create JSON patch/transaction library +- Implement in editor package +- Share between VSCode and web viewer +- No MCP server yet +- Trigger: Need for undo/redo or collaborative editing + +**Phase 3 (If Needed): MCP Server** +- Wrap transaction library in MCP protocol +- Expose as standardized tools +- Deploy for external integrations +- Trigger: External tools need integration + +## Conclusion + +After comprehensive analysis and feedback review, **we recommend implementing a lightweight transactional builder with an MCP wrapper**. + +### Key Insights from Feedback + +1. **Time estimate was way off**: A simple transactional builder + MCP wrapper is 1-2 days, not 5-7 weeks +2. **Multi-turn conversations matter**: Token accumulation across many editing turns makes transactional operations valuable +3. **LLMs miss schema details**: While the schema is clear, a structured API helps prevent mistakes +4. **Editor completion is easier with builder**: The builder provides a foundation for the editor, not the other way around + +### Revised Assessment + +The lightweight approach provides significant value with minimal cost: + +**Benefits:** +- Reduces token usage in multi-turn editing conversations +- Provides type-safe operations that prevent schema violations +- Serves as foundation for completing editor package +- Offers sensible defaults for starting documents +- Enables standardized MCP tool access + +**Costs:** +- 1-2 days of focused development +- ~350-600 lines of maintainable code +- Minimal ongoing maintenance + +**Verdict:** Benefits clearly outweigh costs with the lightweight approach. + +### Next Steps + +1. Implement transactional builder (Day 1) +2. Add MCP wrapper over builder (Day 1-2) +3. Basic file integration (Day 2) +4. Refactor editor to use builder foundation +5. Document usage patterns + +This provides immediate value while keeping implementation simple and maintainable. + +--- + +**Review Status:** Ready for review +**Next Steps:** Share with team for feedback and decision