@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.
- package/LICENSE +21 -0
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +165 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/filter.d.ts +4 -0
- package/dist/cli/commands/filter.d.ts.map +1 -0
- package/dist/cli/commands/filter.js +102 -0
- package/dist/cli/commands/filter.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +3 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +49 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +88 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/plugins.d.ts +3 -0
- package/dist/cli/commands/plugins.d.ts.map +1 -0
- package/dist/cli/commands/plugins.js +66 -0
- package/dist/cli/commands/plugins.js.map +1 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +45 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/save.d.ts +3 -0
- package/dist/cli/commands/save.d.ts.map +1 -0
- package/dist/cli/commands/save.js +54 -0
- package/dist/cli/commands/save.js.map +1 -0
- package/dist/cli/commands/text.d.ts +3 -0
- package/dist/cli/commands/text.d.ts.map +1 -0
- package/dist/cli/commands/text.js +143 -0
- package/dist/cli/commands/text.js.map +1 -0
- package/dist/cli/commands/transform.d.ts +3 -0
- package/dist/cli/commands/transform.d.ts.map +1 -0
- package/dist/cli/commands/transform.js +59 -0
- package/dist/cli/commands/transform.js.map +1 -0
- package/dist/cli/commands/upload.d.ts +3 -0
- package/dist/cli/commands/upload.d.ts.map +1 -0
- package/dist/cli/commands/upload.js +59 -0
- package/dist/cli/commands/upload.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +139 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +23 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +189 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/client.d.ts +63 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +318 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +75 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/logger.d.ts +12 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +26 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/pipeline-runner.d.ts +64 -0
- package/dist/core/pipeline-runner.d.ts.map +1 -0
- package/dist/core/pipeline-runner.js +109 -0
- package/dist/core/pipeline-runner.js.map +1 -0
- package/dist/core/types.d.ts +392 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +5 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +731 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/providers/ai/index.d.ts +3 -0
- package/dist/providers/ai/index.d.ts.map +1 -0
- package/dist/providers/ai/index.js +2 -0
- package/dist/providers/ai/index.js.map +1 -0
- package/dist/providers/ai/openai.d.ts +44 -0
- package/dist/providers/ai/openai.d.ts.map +1 -0
- package/dist/providers/ai/openai.js +133 -0
- package/dist/providers/ai/openai.js.map +1 -0
- package/dist/providers/save/FsSaveProvider.d.ts +20 -0
- package/dist/providers/save/FsSaveProvider.d.ts.map +1 -0
- package/dist/providers/save/FsSaveProvider.js +34 -0
- package/dist/providers/save/FsSaveProvider.js.map +1 -0
- package/dist/providers/save/S3SaveProvider.d.ts +26 -0
- package/dist/providers/save/S3SaveProvider.d.ts.map +1 -0
- package/dist/providers/save/S3SaveProvider.js +42 -0
- package/dist/providers/save/S3SaveProvider.js.map +1 -0
- package/dist/providers/store/fs.d.ts +20 -0
- package/dist/providers/store/fs.d.ts.map +1 -0
- package/dist/providers/store/fs.js +42 -0
- package/dist/providers/store/fs.js.map +1 -0
- package/dist/providers/store/index.d.ts +3 -0
- package/dist/providers/store/index.d.ts.map +1 -0
- package/dist/providers/store/index.js +3 -0
- package/dist/providers/store/index.js.map +1 -0
- package/dist/providers/store/s3.d.ts +62 -0
- package/dist/providers/store/s3.d.ts.map +1 -0
- package/dist/providers/store/s3.js +92 -0
- package/dist/providers/store/s3.js.map +1 -0
- package/dist/providers/svg/index.d.ts +2 -0
- package/dist/providers/svg/index.d.ts.map +1 -0
- package/dist/providers/svg/index.js +2 -0
- package/dist/providers/svg/index.js.map +1 -0
- package/dist/providers/svg/shapes.d.ts +18 -0
- package/dist/providers/svg/shapes.d.ts.map +1 -0
- package/dist/providers/svg/shapes.js +161 -0
- package/dist/providers/svg/shapes.js.map +1 -0
- package/dist/providers/transform/index.d.ts +2 -0
- package/dist/providers/transform/index.d.ts.map +1 -0
- package/dist/providers/transform/index.js +2 -0
- package/dist/providers/transform/index.js.map +1 -0
- package/dist/providers/transform/presets.d.ts +44 -0
- package/dist/providers/transform/presets.d.ts.map +1 -0
- package/dist/providers/transform/presets.js +205 -0
- package/dist/providers/transform/presets.js.map +1 -0
- package/dist/providers/transform/sharp.d.ts +64 -0
- package/dist/providers/transform/sharp.d.ts.map +1 -0
- package/dist/providers/transform/sharp.js +732 -0
- package/dist/providers/transform/sharp.js.map +1 -0
- package/dist/providers/transform/text.d.ts +38 -0
- package/dist/providers/transform/text.d.ts.map +1 -0
- package/dist/providers/transform/text.js +116 -0
- package/dist/providers/transform/text.js.map +1 -0
- 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
|