UNPKG

bottlenecks-mcp-server

Version:

Model Context Protocol server for Bottlenecks database - enables AI agents like Claude to interact with bottleneck data

300 lines 12.6 kB
/** * MCP Image Operations Tools * Lets AI agents generate a cover image for a bottleneck. Deliberately kept * separate from create_bottleneck / update_bottleneck — creating or editing a * bottleneck never silently triggers a paid image generation call; an agent * must call this tool on its own. This thin wrapper calls /api/images/generate * over HTTP (same pattern as every other tool in this file set) — that route * is itself generated from the single @supernal/universal-command definition * in lib/images/commands.ts, so this is not a second implementation of the * generation logic, just its MCP-facing transport. */ /** * Generate a cover image for an existing bottleneck. * * Guardrail: the API enforces a cap (default 1) on how many generated images * a single bottleneck can accumulate. Exceeding it requires `confirm: true` * — deliberately NOT defaulted to true, so an agent has to consciously ask * to replace/add another image rather than looping past the cap by accident. */ export function createGenerateBottleneckImageTool(client) { return { type: 'call_tool', name: 'generate_bottleneck_image', description: 'Generate an AI cover image for an existing bottleneck. Each bottleneck is capped at one auto-generated image by default — pass confirm=true to generate an additional one on purpose.', inputSchema: { type: 'object', properties: { bottleneck_id: { type: 'string', description: 'ID of the bottleneck to generate a cover image for', format: 'uuid', }, prompt: { type: 'string', description: 'What the image should depict. Keep it concrete and representative of the bottleneck, not abstract shapes and not a literal photo.', minLength: 10, }, theme: { type: 'string', description: 'Named house style from lib/images/themes.ts, e.g. "flat-editorial" or "linocut". Omit for the base default.', }, style_override: { type: 'string', description: 'Free-form style text layered on top of the theme, for one-off adjustments.', }, confirm: { type: 'boolean', description: "Set true only when you intend to generate beyond this bottleneck's image limit (default cap: 1). Do not set this unless explicitly asked to.", default: false, }, }, required: ['bottleneck_id', 'prompt'], }, handler: async (args) => { try { const hasWritePermission = await client.checkPermissions(['write']); if (!hasWritePermission) { return { content: [ { type: 'text', text: 'Error: Write permission required to generate images. Please check your API key scopes.', }, ], isError: true, }; } const response = await client.request('/api/images/generate', { method: 'POST', body: { prompt: args.prompt, entityType: 'card', entityId: args.bottleneck_id, theme: args.theme, styleOverride: args.style_override, confirm: Boolean(args.confirm), }, requireAuth: true, }); if (!response.success) { const status = response.status; if (status === 409) { return { content: [ { type: 'text', text: `Image limit reached: ${response.error}. Call again with confirm=true if you really want another image for this bottleneck.`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `Error generating image: ${response.error}`, }, ], isError: true, }; } const image = response.data.image; let content = `# Image Generated\n\n`; content += `**URL:** ${image.publicUrl}\n`; content += `**Model:** ${image.model}\n`; content += `**Attached to bottleneck:** \`${args.bottleneck_id}\`\n`; return { content: [{ type: 'text', text: content }], }; } catch (error) { return { content: [ { type: 'text', text: `Error in generate_bottleneck_image: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * Upload or link an already-available image for a bottleneck — the DIY * alternative to generate_bottleneck_image. No per-entity cap: a deliberate * upload isn't the runaway-cost risk that repeated generation calls are. */ export function createUploadBottleneckImageTool(client) { return { type: 'call_tool', name: 'upload_bottleneck_image', description: 'Store an already-available image for a bottleneck — either base64-encoded bytes or an external URL to download. Use this instead of generate_bottleneck_image when a specific image already exists.', inputSchema: { type: 'object', properties: { bottleneck_id: { type: 'string', description: 'ID of the bottleneck this image belongs to', format: 'uuid', }, image_base64: { type: 'string', description: 'Base64-encoded image bytes. Provide this OR image_url, not both.', }, mime_type: { type: 'string', description: 'Content type of image_base64, e.g. "image/png". Required when image_base64 is set.', }, image_url: { type: 'string', description: 'An external URL to download and store. Provide this OR image_base64, not both.', }, alt_text: { type: 'string', description: 'Accessibility alt text for the image.', }, }, required: ['bottleneck_id'], }, handler: async (args) => { try { const hasWritePermission = await client.checkPermissions(['write']); if (!hasWritePermission) { return { content: [ { type: 'text', text: 'Error: Write permission required to upload images. Please check your API key scopes.', }, ], isError: true, }; } const response = await client.request('/api/images/upload', { method: 'POST', body: { entityType: 'card', entityId: args.bottleneck_id, imageBase64: args.image_base64, mimeType: args.mime_type, imageUrl: args.image_url, altText: args.alt_text, }, requireAuth: true, }); if (!response.success) { return { content: [ { type: 'text', text: `Error uploading image: ${response.error}`, }, ], isError: true, }; } const image = response.data.image; let content = `# Image Uploaded\n\n`; content += `**URL:** ${image.publicUrl}\n`; content += `**Source:** ${image.source}\n`; content += `**Attached to bottleneck:** \`${args.bottleneck_id}\`\n`; return { content: [{ type: 'text', text: content }], }; } catch (error) { return { content: [ { type: 'text', text: `Error in upload_bottleneck_image: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * Delete a bottleneck's current image. Completes the CRUD set alongside * generate_bottleneck_image (create/generated) and upload_bottleneck_image * (create/uploaded). */ export function createDeleteBottleneckImageTool(client) { return { type: 'call_tool', name: 'delete_bottleneck_image', description: "Delete a bottleneck's current image from storage and the images table.", inputSchema: { type: 'object', properties: { bottleneck_id: { type: 'string', description: 'ID of the bottleneck whose image should be deleted', format: 'uuid', }, }, required: ['bottleneck_id'], }, handler: async (args) => { try { const hasWritePermission = await client.checkPermissions(['write']); if (!hasWritePermission) { return { content: [ { type: 'text', text: 'Error: Write permission required to delete images. Please check your API key scopes.', }, ], isError: true, }; } const response = await client.request('/api/images/delete', { method: 'POST', body: { entityType: 'card', entityId: args.bottleneck_id, }, requireAuth: true, }); if (!response.success) { return { content: [ { type: 'text', text: `Error deleting image: ${response.error}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `Image deleted for bottleneck \`${args.bottleneck_id}\`.`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in delete_bottleneck_image: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } //# sourceMappingURL=image-operations.js.map