@teamflojo/floimg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli/commands/config.d.ts +3 -0
  3. package/dist/cli/commands/config.d.ts.map +1 -0
  4. package/dist/cli/commands/config.js +165 -0
  5. package/dist/cli/commands/config.js.map +1 -0
  6. package/dist/cli/commands/filter.d.ts +4 -0
  7. package/dist/cli/commands/filter.d.ts.map +1 -0
  8. package/dist/cli/commands/filter.js +102 -0
  9. package/dist/cli/commands/filter.js.map +1 -0
  10. package/dist/cli/commands/generate.d.ts +3 -0
  11. package/dist/cli/commands/generate.d.ts.map +1 -0
  12. package/dist/cli/commands/generate.js +49 -0
  13. package/dist/cli/commands/generate.js.map +1 -0
  14. package/dist/cli/commands/mcp.d.ts +3 -0
  15. package/dist/cli/commands/mcp.d.ts.map +1 -0
  16. package/dist/cli/commands/mcp.js +88 -0
  17. package/dist/cli/commands/mcp.js.map +1 -0
  18. package/dist/cli/commands/plugins.d.ts +3 -0
  19. package/dist/cli/commands/plugins.d.ts.map +1 -0
  20. package/dist/cli/commands/plugins.js +66 -0
  21. package/dist/cli/commands/plugins.js.map +1 -0
  22. package/dist/cli/commands/run.d.ts +3 -0
  23. package/dist/cli/commands/run.d.ts.map +1 -0
  24. package/dist/cli/commands/run.js +45 -0
  25. package/dist/cli/commands/run.js.map +1 -0
  26. package/dist/cli/commands/save.d.ts +3 -0
  27. package/dist/cli/commands/save.d.ts.map +1 -0
  28. package/dist/cli/commands/save.js +54 -0
  29. package/dist/cli/commands/save.js.map +1 -0
  30. package/dist/cli/commands/text.d.ts +3 -0
  31. package/dist/cli/commands/text.d.ts.map +1 -0
  32. package/dist/cli/commands/text.js +143 -0
  33. package/dist/cli/commands/text.js.map +1 -0
  34. package/dist/cli/commands/transform.d.ts +3 -0
  35. package/dist/cli/commands/transform.d.ts.map +1 -0
  36. package/dist/cli/commands/transform.js +59 -0
  37. package/dist/cli/commands/transform.js.map +1 -0
  38. package/dist/cli/commands/upload.d.ts +3 -0
  39. package/dist/cli/commands/upload.d.ts.map +1 -0
  40. package/dist/cli/commands/upload.js +59 -0
  41. package/dist/cli/commands/upload.js.map +1 -0
  42. package/dist/cli/index.d.ts +3 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/index.js +139 -0
  45. package/dist/cli/index.js.map +1 -0
  46. package/dist/config/index.d.ts +10 -0
  47. package/dist/config/index.d.ts.map +1 -0
  48. package/dist/config/index.js +38 -0
  49. package/dist/config/index.js.map +1 -0
  50. package/dist/config/loader.d.ts +23 -0
  51. package/dist/config/loader.d.ts.map +1 -0
  52. package/dist/config/loader.js +189 -0
  53. package/dist/config/loader.js.map +1 -0
  54. package/dist/core/client.d.ts +63 -0
  55. package/dist/core/client.d.ts.map +1 -0
  56. package/dist/core/client.js +318 -0
  57. package/dist/core/client.js.map +1 -0
  58. package/dist/core/errors.d.ts +38 -0
  59. package/dist/core/errors.d.ts.map +1 -0
  60. package/dist/core/errors.js +75 -0
  61. package/dist/core/errors.js.map +1 -0
  62. package/dist/core/logger.d.ts +12 -0
  63. package/dist/core/logger.d.ts.map +1 -0
  64. package/dist/core/logger.js +26 -0
  65. package/dist/core/logger.js.map +1 -0
  66. package/dist/core/pipeline-runner.d.ts +64 -0
  67. package/dist/core/pipeline-runner.d.ts.map +1 -0
  68. package/dist/core/pipeline-runner.js +109 -0
  69. package/dist/core/pipeline-runner.js.map +1 -0
  70. package/dist/core/types.d.ts +392 -0
  71. package/dist/core/types.d.ts.map +1 -0
  72. package/dist/core/types.js +5 -0
  73. package/dist/core/types.js.map +1 -0
  74. package/dist/index.d.ts +18 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +49 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/mcp/server.d.ts +3 -0
  79. package/dist/mcp/server.d.ts.map +1 -0
  80. package/dist/mcp/server.js +731 -0
  81. package/dist/mcp/server.js.map +1 -0
  82. package/dist/providers/ai/index.d.ts +3 -0
  83. package/dist/providers/ai/index.d.ts.map +1 -0
  84. package/dist/providers/ai/index.js +2 -0
  85. package/dist/providers/ai/index.js.map +1 -0
  86. package/dist/providers/ai/openai.d.ts +44 -0
  87. package/dist/providers/ai/openai.d.ts.map +1 -0
  88. package/dist/providers/ai/openai.js +133 -0
  89. package/dist/providers/ai/openai.js.map +1 -0
  90. package/dist/providers/save/FsSaveProvider.d.ts +20 -0
  91. package/dist/providers/save/FsSaveProvider.d.ts.map +1 -0
  92. package/dist/providers/save/FsSaveProvider.js +34 -0
  93. package/dist/providers/save/FsSaveProvider.js.map +1 -0
  94. package/dist/providers/save/S3SaveProvider.d.ts +26 -0
  95. package/dist/providers/save/S3SaveProvider.d.ts.map +1 -0
  96. package/dist/providers/save/S3SaveProvider.js +42 -0
  97. package/dist/providers/save/S3SaveProvider.js.map +1 -0
  98. package/dist/providers/store/fs.d.ts +20 -0
  99. package/dist/providers/store/fs.d.ts.map +1 -0
  100. package/dist/providers/store/fs.js +42 -0
  101. package/dist/providers/store/fs.js.map +1 -0
  102. package/dist/providers/store/index.d.ts +3 -0
  103. package/dist/providers/store/index.d.ts.map +1 -0
  104. package/dist/providers/store/index.js +3 -0
  105. package/dist/providers/store/index.js.map +1 -0
  106. package/dist/providers/store/s3.d.ts +62 -0
  107. package/dist/providers/store/s3.d.ts.map +1 -0
  108. package/dist/providers/store/s3.js +92 -0
  109. package/dist/providers/store/s3.js.map +1 -0
  110. package/dist/providers/svg/index.d.ts +2 -0
  111. package/dist/providers/svg/index.d.ts.map +1 -0
  112. package/dist/providers/svg/index.js +2 -0
  113. package/dist/providers/svg/index.js.map +1 -0
  114. package/dist/providers/svg/shapes.d.ts +18 -0
  115. package/dist/providers/svg/shapes.d.ts.map +1 -0
  116. package/dist/providers/svg/shapes.js +161 -0
  117. package/dist/providers/svg/shapes.js.map +1 -0
  118. package/dist/providers/transform/index.d.ts +2 -0
  119. package/dist/providers/transform/index.d.ts.map +1 -0
  120. package/dist/providers/transform/index.js +2 -0
  121. package/dist/providers/transform/index.js.map +1 -0
  122. package/dist/providers/transform/presets.d.ts +44 -0
  123. package/dist/providers/transform/presets.d.ts.map +1 -0
  124. package/dist/providers/transform/presets.js +205 -0
  125. package/dist/providers/transform/presets.js.map +1 -0
  126. package/dist/providers/transform/sharp.d.ts +64 -0
  127. package/dist/providers/transform/sharp.d.ts.map +1 -0
  128. package/dist/providers/transform/sharp.js +732 -0
  129. package/dist/providers/transform/sharp.js.map +1 -0
  130. package/dist/providers/transform/text.d.ts +38 -0
  131. package/dist/providers/transform/text.d.ts.map +1 -0
  132. package/dist/providers/transform/text.js +116 -0
  133. package/dist/providers/transform/text.js.map +1 -0
  134. package/package.json +84 -0
@@ -0,0 +1,731 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { readFile, mkdir } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+ import createClient from "../index.js";
9
+ import { loadConfig } from "../config/loader.js";
10
+ import { FloimgError } from "../core/errors.js";
11
+ /**
12
+ * floimg MCP Server v0.1.0 - Smart Image Generation & Workflow Orchestration
13
+ *
14
+ * Key improvements:
15
+ * - Session workspace: Images stored with IDs, no byte passing between tools
16
+ * - File path references: Transform/save can reference any image by path or ID
17
+ * - Pipeline support: Multi-step workflows in a single call
18
+ * - Better intent routing: Recognizes AI image requests properly
19
+ */
20
+ // Session workspace for storing images between tool calls
21
+ const SESSION_WORKSPACE = join(process.cwd(), ".floimg", "mcp-session");
22
+ const imageRegistry = new Map();
23
+ // Ensure workspace exists
24
+ async function ensureWorkspace() {
25
+ if (!existsSync(SESSION_WORKSPACE)) {
26
+ await mkdir(SESSION_WORKSPACE, { recursive: true });
27
+ }
28
+ }
29
+ // Generate unique image ID
30
+ function generateImageId() {
31
+ return `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
32
+ }
33
+ // Load image from various sources
34
+ async function loadImage(imageId, imagePath, imageBytes, mime) {
35
+ // Priority 1: imageId (reference to session image)
36
+ if (imageId) {
37
+ const registered = imageRegistry.get(imageId);
38
+ if (!registered) {
39
+ throw new Error(`Image ID not found: ${imageId}. Use generate_image first to create an image.`);
40
+ }
41
+ const bytes = await readFile(registered.path);
42
+ return {
43
+ bytes,
44
+ mime: registered.mime,
45
+ ...registered.metadata,
46
+ };
47
+ }
48
+ // Priority 2: imagePath (reference to file on disk)
49
+ if (imagePath) {
50
+ const bytes = await readFile(imagePath);
51
+ // Detect MIME type from extension if not provided
52
+ const detectedMime = mime || detectMimeFromPath(imagePath);
53
+ return {
54
+ bytes,
55
+ mime: detectedMime,
56
+ };
57
+ }
58
+ // Priority 3: imageBytes (base64 encoded, for external images)
59
+ if (imageBytes && mime) {
60
+ return {
61
+ bytes: Buffer.from(imageBytes, "base64"),
62
+ mime: mime,
63
+ };
64
+ }
65
+ throw new Error("Must provide imageId, imagePath, or imageBytes+mime");
66
+ }
67
+ // Detect MIME type from file path
68
+ function detectMimeFromPath(path) {
69
+ const ext = path.split('.').pop()?.toLowerCase();
70
+ const mimeMap = {
71
+ 'svg': 'image/svg+xml',
72
+ 'png': 'image/png',
73
+ 'jpg': 'image/jpeg',
74
+ 'jpeg': 'image/jpeg',
75
+ 'webp': 'image/webp',
76
+ 'avif': 'image/avif',
77
+ };
78
+ return mimeMap[ext || ''] || 'image/png';
79
+ }
80
+ // Plugin auto-discovery
81
+ async function loadAvailablePlugins(client) {
82
+ const plugins = [];
83
+ console.error('[floimg-mcp] Starting plugin discovery...');
84
+ const potentialPlugins = [
85
+ { name: 'quickchart', module: '@teamflojo/floimg-quickchart' },
86
+ { name: 'd3', module: '@teamflojo/floimg-d3' },
87
+ { name: 'mermaid', module: '@teamflojo/floimg-mermaid' },
88
+ { name: 'qr', module: '@teamflojo/floimg-qr' },
89
+ { name: 'screenshot', module: '@teamflojo/floimg-screenshot' },
90
+ ];
91
+ for (const { name, module } of potentialPlugins) {
92
+ try {
93
+ console.error(`[floimg-mcp] Attempting to load ${module}...`);
94
+ const plugin = await import(module);
95
+ if (!plugin.default) {
96
+ console.error(`[floimg-mcp] ⚠ ${module} has no default export`);
97
+ continue;
98
+ }
99
+ const generator = typeof plugin.default === 'function'
100
+ ? plugin.default()
101
+ : plugin.default;
102
+ client.registerGenerator(generator);
103
+ plugins.push(name);
104
+ console.error(`[floimg-mcp] ✓ Loaded plugin: ${name}`);
105
+ }
106
+ catch (err) {
107
+ const error = err;
108
+ console.error(`[floimg-mcp] ✗ Failed to load ${module}: ${error.message}`);
109
+ }
110
+ }
111
+ console.error(`[floimg-mcp] Plugin discovery complete. Loaded: ${plugins.join(', ') || 'none'}`);
112
+ if (plugins.length === 0) {
113
+ console.error('[floimg-mcp] ⚠ No generator plugins found!');
114
+ console.error('[floimg-mcp] Install with: npm install -g @teamflojo/floimg-quickchart @teamflojo/floimg-mermaid @teamflojo/floimg-qr @teamflojo/floimg-d3 @teamflojo/floimg-screenshot');
115
+ console.error('[floimg-mcp] Only built-in generators (shapes, openai) will be available.');
116
+ }
117
+ return plugins;
118
+ }
119
+ // Smart generator selection based on intent - IMPROVED for v0.4.0
120
+ function selectGenerator(intent, params) {
121
+ const intentLower = intent.toLowerCase();
122
+ // QR codes
123
+ if (intentLower.includes('qr') || intentLower.includes('barcode')) {
124
+ return 'qr';
125
+ }
126
+ // Screenshots
127
+ if (intentLower.includes('screenshot') || intentLower.includes('capture') ||
128
+ intentLower.includes('website') || intentLower.includes('webpage') ||
129
+ intentLower.includes('url') && params.url) {
130
+ return 'screenshot';
131
+ }
132
+ // Diagrams (Mermaid)
133
+ if (intentLower.includes('flowchart') || intentLower.includes('diagram') ||
134
+ intentLower.includes('sequence') || intentLower.includes('gantt') ||
135
+ intentLower.includes('class diagram') || intentLower.includes('entity') ||
136
+ intentLower.includes('state') || intentLower.includes('mindmap')) {
137
+ return 'mermaid';
138
+ }
139
+ // Charts & Data Visualization (check BEFORE AI detection)
140
+ if (intentLower.includes('chart') || intentLower.includes('graph') ||
141
+ intentLower.includes('plot') || intentLower.includes('visualiz')) {
142
+ // D3 for custom/complex visualizations
143
+ if (params.render || params.renderString ||
144
+ intentLower.includes('custom') || intentLower.includes('d3')) {
145
+ return 'd3';
146
+ }
147
+ // QuickChart for standard charts
148
+ return 'quickchart';
149
+ }
150
+ // AI Image Generation - IMPROVED: Better keyword detection
151
+ // Check for scene descriptions, subjects, art styles, etc.
152
+ const aiKeywords = [
153
+ 'photo', 'picture', 'illustration', 'painting', 'drawing',
154
+ 'scene', 'image of', 'portrait', 'landscape', 'artwork',
155
+ 'realistic', 'photorealistic', 'stylized', 'artistic',
156
+ 'dall-e', 'ai image', 'ai generated', 'generate image',
157
+ 'person', 'people', 'animal', 'building', 'nature',
158
+ 'stadium', 'player', 'celebrating', 'sunset', 'dramatic'
159
+ ];
160
+ const hasAIKeyword = aiKeywords.some(keyword => intentLower.includes(keyword));
161
+ const hasPromptParam = params.prompt !== undefined;
162
+ // Route to OpenAI if:
163
+ // 1. Has AI-related keywords OR
164
+ // 2. Has prompt parameter OR
165
+ // 3. Intent describes a scene/subject (more than 5 words)
166
+ const wordCount = intent.trim().split(/\s+/).length;
167
+ const isDescriptiveIntent = wordCount > 5 && !intentLower.includes('gradient') && !intentLower.includes('shape');
168
+ if (hasAIKeyword || hasPromptParam || isDescriptiveIntent) {
169
+ return 'openai';
170
+ }
171
+ // Default to shapes for simple SVG graphics (gradients, basic shapes)
172
+ return 'shapes';
173
+ }
174
+ // Initialize server
175
+ const server = new Server({
176
+ name: "floimg",
177
+ version: "0.1.0",
178
+ }, {
179
+ capabilities: {
180
+ tools: {},
181
+ },
182
+ });
183
+ // Define available tools
184
+ const TOOLS = [
185
+ {
186
+ name: "generate_image",
187
+ description: "Generate any type of image. Routes to the appropriate generator based on intent. " +
188
+ "Supports: AI images (DALL-E), charts (bar, line, pie), diagrams (flowcharts, sequence), " +
189
+ "QR codes, screenshots, data visualizations (D3), and simple shapes/gradients. " +
190
+ "Images are saved to session workspace and assigned an imageId for chaining operations.",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {
194
+ intent: {
195
+ type: "string",
196
+ description: "Brief description to route to the right generator and provide simple defaults. " +
197
+ "For AI images: intent becomes the prompt (e.g., 'golden retriever in field'). " +
198
+ "For QR codes: include the URL (e.g., 'QR code for https://example.com'). " +
199
+ "For charts/diagrams: just routing hint (e.g., 'bar chart', 'flowchart') - must provide params with data.",
200
+ },
201
+ params: {
202
+ type: "object",
203
+ description: "Generator-specific parameters. " +
204
+ "AI images & QR codes: Optional (auto-filled from intent). " +
205
+ "Charts & diagrams: REQUIRED - must provide structured data. " +
206
+ "Examples: " +
207
+ "Charts: {type: 'bar', data: {labels: [...], datasets: [...]}}. " +
208
+ "Diagrams: {code: 'graph TD; A-->B'}. " +
209
+ "AI images: {prompt: '...', size: '1024x1024'} (or omit, uses intent). " +
210
+ "QR codes: {text: 'https://...'} (or omit if URL in intent).",
211
+ default: {},
212
+ },
213
+ saveTo: {
214
+ type: "string",
215
+ description: "Optional: Also save to a destination (filesystem or cloud). " +
216
+ "Examples: './output.png', 's3://bucket/key.png', 'r2://bucket/key.png'. " +
217
+ "Image is always saved to session workspace regardless.",
218
+ },
219
+ },
220
+ required: ["intent"],
221
+ },
222
+ },
223
+ {
224
+ name: "transform_image",
225
+ description: "Transform an image with filters, effects, text, resizing, and more. " +
226
+ "Reference images by: imageId (from generate_image), imagePath (any file), or imageBytes (base64). " +
227
+ "Supports: resize, convert, blur, sharpen, grayscale, modulate, tint, roundCorners, " +
228
+ "addText, addCaption, and preset filters (vintage, vibrant, dramatic, etc.). " +
229
+ "Transformed image is saved to session workspace with new imageId.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ imageId: {
234
+ type: "string",
235
+ description: "ID of image from previous generate_image or transform_image call",
236
+ },
237
+ imagePath: {
238
+ type: "string",
239
+ description: "Path to image file on disk (e.g., './my-image.png', 'generated/abc.png')",
240
+ },
241
+ imageBytes: {
242
+ type: "string",
243
+ description: "Base64-encoded image bytes (for external images not in session)",
244
+ },
245
+ mime: {
246
+ type: "string",
247
+ description: "MIME type (required only if using imageBytes, auto-detected for imagePath/imageId)",
248
+ },
249
+ operation: {
250
+ type: "string",
251
+ description: "Transform operation to apply",
252
+ enum: [
253
+ "convert",
254
+ "resize",
255
+ "composite",
256
+ "optimizeSvg",
257
+ "blur",
258
+ "sharpen",
259
+ "grayscale",
260
+ "negate",
261
+ "normalize",
262
+ "threshold",
263
+ "modulate",
264
+ "tint",
265
+ "extend",
266
+ "extract",
267
+ "roundCorners",
268
+ "addText",
269
+ "addCaption",
270
+ "preset",
271
+ ],
272
+ },
273
+ params: {
274
+ type: "object",
275
+ description: "Parameters for the operation. Examples: " +
276
+ "resize: {width: 800, height: 600}, " +
277
+ "blur: {sigma: 5}, " +
278
+ "modulate: {brightness: 1.2, saturation: 1.3}, " +
279
+ "roundCorners: {radius: 20}, " +
280
+ "addText: {text: 'Hello', x: 100, y: 100, size: 48, color: '#fff', shadow: true}, " +
281
+ "addCaption: {text: 'Caption', position: 'bottom'}, " +
282
+ "preset: {name: 'vintage' | 'vibrant' | 'dramatic' | 'soft'}",
283
+ },
284
+ to: {
285
+ type: "string",
286
+ description: "Target MIME type (for convert operation)",
287
+ },
288
+ saveTo: {
289
+ type: "string",
290
+ description: "Optional: Also save to a destination (filesystem or cloud). " +
291
+ "Image is always saved to session workspace regardless.",
292
+ },
293
+ },
294
+ required: ["operation"],
295
+ },
296
+ },
297
+ {
298
+ name: "save_image",
299
+ description: "Save an image to filesystem or cloud storage (S3, Tigris, R2, etc.). " +
300
+ "Reference images by: imageId (from previous calls), imagePath (any file), or imageBytes (base64). " +
301
+ "Supports smart destination routing: './output.png' → filesystem, 's3://bucket/key' → S3. " +
302
+ "Returns public URL if saving to cloud storage.",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ imageId: {
307
+ type: "string",
308
+ description: "ID of image from previous generate_image or transform_image call",
309
+ },
310
+ imagePath: {
311
+ type: "string",
312
+ description: "Path to image file on disk",
313
+ },
314
+ imageBytes: {
315
+ type: "string",
316
+ description: "Base64-encoded image bytes",
317
+ },
318
+ mime: {
319
+ type: "string",
320
+ description: "MIME type (required only if using imageBytes)",
321
+ },
322
+ destination: {
323
+ type: "string",
324
+ description: "Where to save: './output.png', 's3://bucket/key.png', 'r2://bucket/key.png'",
325
+ },
326
+ provider: {
327
+ type: "string",
328
+ description: "Storage provider: 's3' or 'fs' (auto-detected from destination if not specified)",
329
+ },
330
+ },
331
+ required: ["destination"],
332
+ },
333
+ },
334
+ {
335
+ name: "run_pipeline",
336
+ description: "Execute a multi-step image workflow in a single call. " +
337
+ "Define a series of generate, transform, and save operations. " +
338
+ "Each step automatically receives the output from the previous step. " +
339
+ "Perfect for complex workflows like: generate → resize → add caption → upload to cloud.",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ steps: {
344
+ type: "array",
345
+ description: "Array of steps to execute in order. Each step is an object with one key: " +
346
+ "'generate', 'transform', or 'save'. The value is the parameters for that operation.",
347
+ items: {
348
+ type: "object",
349
+ },
350
+ },
351
+ },
352
+ required: ["steps"],
353
+ },
354
+ },
355
+ ];
356
+ // List tools handler
357
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
358
+ return { tools: TOOLS };
359
+ });
360
+ // Call tool handler
361
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
362
+ const { name, arguments: args } = request.params;
363
+ try {
364
+ await ensureWorkspace();
365
+ // Load configuration (from floimg.config.ts or .floimgrc.json)
366
+ const config = await loadConfig();
367
+ const client = createClient(config);
368
+ // Load available plugins
369
+ const availablePlugins = await loadAvailablePlugins(client);
370
+ console.error(`[floimg-mcp] Available generators: shapes, openai, ${availablePlugins.join(', ')}`);
371
+ switch (name) {
372
+ case "generate_image": {
373
+ const { intent, params = {}, saveTo } = args;
374
+ if (!intent) {
375
+ throw new Error("'intent' parameter is required");
376
+ }
377
+ // Smart generator selection
378
+ const generator = selectGenerator(intent, params);
379
+ console.error(`[floimg-mcp] Intent: "${intent}" → Generator: ${generator}`);
380
+ // Auto-fill params for simple cases to improve UX
381
+ const finalParams = { ...params };
382
+ if (generator === 'openai' && !finalParams.prompt) {
383
+ // For AI images: use intent as prompt
384
+ finalParams.prompt = intent;
385
+ finalParams.size = finalParams.size || '1024x1024';
386
+ console.error(`[floimg-mcp] Auto-filled: prompt="${intent}", size=${finalParams.size}`);
387
+ }
388
+ if (generator === 'qr' && !finalParams.text) {
389
+ // For QR codes: extract URL from intent
390
+ const urlMatch = intent.match(/https?:\/\/[^\s]+/);
391
+ if (urlMatch) {
392
+ finalParams.text = urlMatch[0];
393
+ console.error(`[floimg-mcp] Auto-filled: text="${finalParams.text}"`);
394
+ }
395
+ else {
396
+ throw new Error("Could not extract URL from intent for QR code. " +
397
+ "Please provide params.text explicitly. " +
398
+ "Example: { intent: 'qr code', params: { text: 'https://example.com' } }");
399
+ }
400
+ }
401
+ // For charts and diagrams: params always required (too complex to extract)
402
+ if ((generator === 'quickchart' || generator === 'mermaid' || generator === 'd3') &&
403
+ !finalParams.type && !finalParams.data && !finalParams.code && !finalParams.render) {
404
+ throw new Error(`${generator} requires explicit params. Intent is only for routing. ` +
405
+ `Please provide structured data: ` +
406
+ `${generator === 'quickchart' ? '{ type: "bar", data: {...} }' : ''}` +
407
+ `${generator === 'mermaid' ? '{ code: "graph TD; A-->B" }' : ''}` +
408
+ `${generator === 'd3' ? '{ render: "...", data: [...] }' : ''}`);
409
+ }
410
+ const blob = await client.generate({
411
+ generator,
412
+ params: finalParams,
413
+ });
414
+ // Save to session workspace
415
+ const imageId = generateImageId();
416
+ const ext = getExtension(blob.mime);
417
+ const sessionPath = join(SESSION_WORKSPACE, `${imageId}.${ext}`);
418
+ await client.save(blob, sessionPath);
419
+ // Register in session
420
+ imageRegistry.set(imageId, {
421
+ path: sessionPath,
422
+ mime: blob.mime,
423
+ metadata: {
424
+ width: blob.width,
425
+ height: blob.height,
426
+ source: blob.source,
427
+ },
428
+ });
429
+ console.error(`[floimg-mcp] Saved to session: ${imageId} → ${sessionPath}`);
430
+ // Optionally save to additional destination
431
+ let cloudResult = null;
432
+ if (saveTo) {
433
+ cloudResult = await client.save(blob, saveTo);
434
+ console.error(`[floimg-mcp] Also saved to: ${saveTo}`);
435
+ }
436
+ return {
437
+ content: [
438
+ {
439
+ type: "text",
440
+ text: JSON.stringify({
441
+ success: true,
442
+ imageId,
443
+ generator,
444
+ session: {
445
+ path: sessionPath,
446
+ mime: blob.mime,
447
+ width: blob.width,
448
+ height: blob.height,
449
+ },
450
+ ...(cloudResult && {
451
+ saved: {
452
+ location: cloudResult.location,
453
+ provider: cloudResult.provider,
454
+ size: cloudResult.size,
455
+ },
456
+ }),
457
+ }, null, 2),
458
+ },
459
+ ],
460
+ };
461
+ }
462
+ case "transform_image": {
463
+ const { imageId, imagePath, imageBytes, mime, operation, params = {}, to, saveTo } = args;
464
+ // Load input image
465
+ const inputBlob = await loadImage(imageId, imagePath, imageBytes, mime);
466
+ let resultBlob;
467
+ // Handle special cases that need specific parameters
468
+ if (operation === "convert") {
469
+ if (!to)
470
+ throw new Error("'to' parameter required for convert operation");
471
+ resultBlob = await client.transform({
472
+ blob: inputBlob,
473
+ op: "convert",
474
+ to: to,
475
+ params,
476
+ });
477
+ }
478
+ else if (operation === "resize") {
479
+ const { width, height } = params;
480
+ if (!width && !height)
481
+ throw new Error("'width' or 'height' required in params for resize");
482
+ resultBlob = await client.transform({
483
+ blob: inputBlob,
484
+ op: "resize",
485
+ params: { width, height, ...params },
486
+ });
487
+ }
488
+ else {
489
+ // All other operations use the generic params approach
490
+ resultBlob = await client.transform({
491
+ blob: inputBlob,
492
+ op: operation,
493
+ params,
494
+ });
495
+ }
496
+ // Save to session workspace
497
+ const newImageId = generateImageId();
498
+ const ext = getExtension(resultBlob.mime);
499
+ const sessionPath = join(SESSION_WORKSPACE, `${newImageId}.${ext}`);
500
+ await client.save(resultBlob, sessionPath);
501
+ // Register in session
502
+ imageRegistry.set(newImageId, {
503
+ path: sessionPath,
504
+ mime: resultBlob.mime,
505
+ metadata: {
506
+ width: resultBlob.width,
507
+ height: resultBlob.height,
508
+ source: resultBlob.source,
509
+ },
510
+ });
511
+ console.error(`[floimg-mcp] Transformed and saved: ${newImageId} → ${sessionPath}`);
512
+ // Optionally save to additional destination
513
+ let cloudResult = null;
514
+ if (saveTo) {
515
+ cloudResult = await client.save(resultBlob, saveTo);
516
+ console.error(`[floimg-mcp] Also saved to: ${saveTo}`);
517
+ }
518
+ return {
519
+ content: [
520
+ {
521
+ type: "text",
522
+ text: JSON.stringify({
523
+ success: true,
524
+ imageId: newImageId,
525
+ operation,
526
+ session: {
527
+ path: sessionPath,
528
+ mime: resultBlob.mime,
529
+ width: resultBlob.width,
530
+ height: resultBlob.height,
531
+ },
532
+ ...(cloudResult && {
533
+ saved: {
534
+ location: cloudResult.location,
535
+ provider: cloudResult.provider,
536
+ size: cloudResult.size,
537
+ },
538
+ }),
539
+ }, null, 2),
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ case "save_image": {
545
+ const { imageId, imagePath, imageBytes, mime, destination, provider } = args;
546
+ // Load input image
547
+ const inputBlob = await loadImage(imageId, imagePath, imageBytes, mime);
548
+ const result = await client.save(inputBlob, provider ? { path: destination, provider } : destination);
549
+ console.error(`[floimg-mcp] Saved to: ${destination}`);
550
+ return {
551
+ content: [
552
+ {
553
+ type: "text",
554
+ text: JSON.stringify({
555
+ success: true,
556
+ location: result.location,
557
+ provider: result.provider,
558
+ size: result.size,
559
+ mime: result.mime,
560
+ }, null, 2),
561
+ },
562
+ ],
563
+ };
564
+ }
565
+ case "run_pipeline": {
566
+ const { steps } = args;
567
+ if (!Array.isArray(steps) || steps.length === 0) {
568
+ throw new Error("'steps' must be a non-empty array");
569
+ }
570
+ let currentImageId;
571
+ const results = [];
572
+ for (let i = 0; i < steps.length; i++) {
573
+ const step = steps[i];
574
+ const stepType = Object.keys(step)[0]; // 'generate', 'transform', or 'save'
575
+ const stepParams = step[stepType];
576
+ console.error(`[floimg-mcp] Pipeline step ${i + 1}/${steps.length}: ${stepType}`);
577
+ if (stepType === 'generate') {
578
+ // Generate step
579
+ const { intent, params = {} } = stepParams;
580
+ const generator = selectGenerator(intent, params);
581
+ // Auto-fill params for simple cases (same logic as generate_image tool)
582
+ const finalParams = { ...params };
583
+ if (generator === 'openai' && !finalParams.prompt) {
584
+ finalParams.prompt = intent;
585
+ finalParams.size = finalParams.size || '1024x1024';
586
+ }
587
+ if (generator === 'qr' && !finalParams.text) {
588
+ const urlMatch = intent.match(/https?:\/\/[^\s]+/);
589
+ if (urlMatch)
590
+ finalParams.text = urlMatch[0];
591
+ }
592
+ const blob = await client.generate({ generator, params: finalParams });
593
+ // Save to session
594
+ const imageId = generateImageId();
595
+ const ext = getExtension(blob.mime);
596
+ const sessionPath = join(SESSION_WORKSPACE, `${imageId}.${ext}`);
597
+ await client.save(blob, sessionPath);
598
+ imageRegistry.set(imageId, {
599
+ path: sessionPath,
600
+ mime: blob.mime,
601
+ metadata: { width: blob.width, height: blob.height, source: blob.source },
602
+ });
603
+ currentImageId = imageId;
604
+ results.push({ step: i + 1, type: 'generate', imageId, generator });
605
+ }
606
+ else if (stepType === 'transform') {
607
+ // Transform step - uses current image
608
+ if (!currentImageId) {
609
+ throw new Error(`Pipeline step ${i + 1}: transform requires a previous generate step`);
610
+ }
611
+ const { operation, params = {}, to } = stepParams;
612
+ const inputBlob = await loadImage(currentImageId);
613
+ let resultBlob;
614
+ if (operation === "convert") {
615
+ resultBlob = await client.transform({
616
+ blob: inputBlob,
617
+ op: "convert",
618
+ to: to,
619
+ params,
620
+ });
621
+ }
622
+ else if (operation === "resize") {
623
+ resultBlob = await client.transform({
624
+ blob: inputBlob,
625
+ op: "resize",
626
+ params,
627
+ });
628
+ }
629
+ else {
630
+ resultBlob = await client.transform({
631
+ blob: inputBlob,
632
+ op: operation,
633
+ params,
634
+ });
635
+ }
636
+ // Save to session
637
+ const newImageId = generateImageId();
638
+ const ext = getExtension(resultBlob.mime);
639
+ const sessionPath = join(SESSION_WORKSPACE, `${newImageId}.${ext}`);
640
+ await client.save(resultBlob, sessionPath);
641
+ imageRegistry.set(newImageId, {
642
+ path: sessionPath,
643
+ mime: resultBlob.mime,
644
+ metadata: { width: resultBlob.width, height: resultBlob.height },
645
+ });
646
+ currentImageId = newImageId;
647
+ results.push({ step: i + 1, type: 'transform', operation, imageId: newImageId });
648
+ }
649
+ else if (stepType === 'save') {
650
+ // Save step - saves current image
651
+ if (!currentImageId) {
652
+ throw new Error(`Pipeline step ${i + 1}: save requires a previous generate/transform step`);
653
+ }
654
+ const { destination, provider } = stepParams;
655
+ const inputBlob = await loadImage(currentImageId);
656
+ const result = await client.save(inputBlob, provider ? { path: destination, provider } : destination);
657
+ results.push({
658
+ step: i + 1,
659
+ type: 'save',
660
+ location: result.location,
661
+ provider: result.provider,
662
+ size: result.size,
663
+ });
664
+ }
665
+ else {
666
+ throw new Error(`Unknown pipeline step type: ${stepType}. Use 'generate', 'transform', or 'save'.`);
667
+ }
668
+ }
669
+ return {
670
+ content: [
671
+ {
672
+ type: "text",
673
+ text: JSON.stringify({
674
+ success: true,
675
+ pipeline: {
676
+ totalSteps: steps.length,
677
+ finalImageId: currentImageId,
678
+ results,
679
+ },
680
+ }, null, 2),
681
+ },
682
+ ],
683
+ };
684
+ }
685
+ default:
686
+ throw new Error(`Unknown tool: ${name}`);
687
+ }
688
+ }
689
+ catch (error) {
690
+ const message = error instanceof Error ? error.message : String(error);
691
+ const errorType = error instanceof FloimgError ? error.name : "Error";
692
+ return {
693
+ content: [
694
+ {
695
+ type: "text",
696
+ text: JSON.stringify({
697
+ success: false,
698
+ error: errorType,
699
+ message,
700
+ }, null, 2),
701
+ },
702
+ ],
703
+ isError: true,
704
+ };
705
+ }
706
+ });
707
+ // Helper: Get file extension from MIME type
708
+ function getExtension(mime) {
709
+ const map = {
710
+ 'image/svg+xml': 'svg',
711
+ 'image/png': 'png',
712
+ 'image/jpeg': 'jpg',
713
+ 'image/webp': 'webp',
714
+ 'image/avif': 'avif',
715
+ };
716
+ return map[mime] || 'png';
717
+ }
718
+ // Start server with stdio transport
719
+ async function main() {
720
+ const transport = new StdioServerTransport();
721
+ await server.connect(transport);
722
+ // Log to stderr (stdout is used for MCP communication)
723
+ console.error("floimg MCP server v0.1.0 running on stdio");
724
+ console.error("Session workspace:", SESSION_WORKSPACE);
725
+ console.error("Smart routing enabled - will auto-select best generator based on intent");
726
+ }
727
+ main().catch((error) => {
728
+ console.error("Fatal error:", error);
729
+ process.exit(1);
730
+ });
731
+ //# sourceMappingURL=server.js.map