@agimon-ai/imagine-mcp 0.2.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.
@@ -0,0 +1,891 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { constants } from "node:fs";
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { basename, extname, resolve } from "node:path";
7
+ import fetch from "node-fetch";
8
+ import { createApi } from "unsplash-js";
9
+ import { z } from "zod";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import express from "express";
12
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+
15
+ //#region src/utils.ts
16
+ /**
17
+ * Normalizes a file path by handling escaped characters and spaces
18
+ *
19
+ * This function handles cases like 'a\ name.png' by converting them to 'a name.png'
20
+ */
21
+ function normalizeFilePath(filePath) {
22
+ let normalizedPath = filePath.replace(/\\+ /g, " ");
23
+ normalizedPath = normalizedPath.replace(/\\+'/g, "'").replace(/\\+"/g, "\"").replace(/\\+`/g, "`").replace(/\\+\(/g, "(").replace(/\\+\)/g, ")").replace(/\\+\[/g, "[").replace(/\\+\]/g, "]").replace(/\\+\{/g, "{").replace(/\\+\}/g, "}");
24
+ return normalizedPath;
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/services/ImageReader.ts
29
+ const DEFAULT_MIME_TYPE = "application/octet-stream";
30
+ const MIME_BY_EXTENSION = {
31
+ ".png": "image/png",
32
+ ".jpg": "image/jpeg",
33
+ ".jpeg": "image/jpeg",
34
+ ".gif": "image/gif",
35
+ ".webp": "image/webp",
36
+ ".bmp": "image/bmp",
37
+ ".svg": "image/svg+xml",
38
+ ".heic": "image/heic",
39
+ ".heif": "image/heif"
40
+ };
41
+ var ImageReader = class {
42
+ async readImage(source, options = {}) {
43
+ const imageSource = await this.resolveImageSource(source);
44
+ const sha256 = createHash("sha256").update(imageSource.data).digest("hex");
45
+ const result = {
46
+ source: imageSource.source,
47
+ sourceType: imageSource.sourceType,
48
+ mimeType: imageSource.mimeType,
49
+ sizeBytes: imageSource.sizeBytes,
50
+ sha256
51
+ };
52
+ if (imageSource.sourceType !== "data-uri") result.fileName = basename(imageSource.source);
53
+ if (options.includeBase64) result.base64 = imageSource.data.toString("base64");
54
+ return result;
55
+ }
56
+ async resolveImageSource(source) {
57
+ const trimmed = source.trim();
58
+ if (!trimmed) throw new McpError(ErrorCode.InvalidParams, "Image source cannot be empty.");
59
+ if (trimmed.startsWith("data:")) return this.resolveDataUri(trimmed);
60
+ if (/^https?:\/\//i.test(trimmed)) return this.resolveRemoteImage(trimmed);
61
+ return this.resolveFile(trimmed);
62
+ }
63
+ async resolveFile(filePath) {
64
+ const normalizedPath = resolve(normalizeFilePath(filePath));
65
+ try {
66
+ await access(normalizedPath, constants.F_OK);
67
+ } catch (error) {
68
+ throw new McpError(ErrorCode.InvalidParams, `Image file not found at path: ${filePath}`);
69
+ }
70
+ let data;
71
+ try {
72
+ data = await readFile(normalizedPath);
73
+ } catch (error) {
74
+ throw new McpError(ErrorCode.InternalError, `Failed to read image file: ${error instanceof Error ? error.message : String(error)}`);
75
+ }
76
+ return {
77
+ source: normalizedPath,
78
+ sourceType: "file",
79
+ mimeType: MIME_BY_EXTENSION[extname(normalizedPath).toLowerCase()] || DEFAULT_MIME_TYPE,
80
+ sizeBytes: data.length,
81
+ data
82
+ };
83
+ }
84
+ async resolveRemoteImage(url) {
85
+ try {
86
+ const response = await fetch(url, { redirect: "follow" });
87
+ if (!response.ok) throw new McpError(ErrorCode.InvalidParams, `Failed to fetch image from URL: ${url} (${response.status} ${response.statusText})`);
88
+ const arrayBuffer = await response.arrayBuffer();
89
+ const data = Buffer.from(arrayBuffer);
90
+ const contentType = response.headers.get("content-type")?.split(";")[0];
91
+ const fileName = basename(new URL(url).pathname || "image");
92
+ return {
93
+ source: url,
94
+ sourceType: "url",
95
+ mimeType: contentType ?? MIME_BY_EXTENSION[extname(fileName)] ?? DEFAULT_MIME_TYPE,
96
+ sizeBytes: data.length,
97
+ data
98
+ };
99
+ } catch (error) {
100
+ if (error instanceof McpError) throw error;
101
+ throw new McpError(ErrorCode.InternalError, `Failed to download image from URL: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ }
104
+ resolveDataUri(dataUri) {
105
+ const commaIndex = dataUri.indexOf(",");
106
+ if (commaIndex === -1 || commaIndex === dataUri.length - 1) throw new McpError(ErrorCode.InvalidParams, "Invalid data URI format. Expected `data:[<mime>];base64,<payload>`.");
107
+ const metadata = dataUri.slice(5, commaIndex).toLowerCase();
108
+ const payload = dataUri.slice(commaIndex + 1);
109
+ const isBase64 = metadata.includes("base64");
110
+ const declaredMimeType = metadata.split(";")[0] || DEFAULT_MIME_TYPE;
111
+ try {
112
+ const data = isBase64 ? Buffer.from(payload, "base64") : Buffer.from(decodeURIComponent(payload));
113
+ return {
114
+ source: dataUri,
115
+ sourceType: "data-uri",
116
+ mimeType: declaredMimeType || DEFAULT_MIME_TYPE,
117
+ sizeBytes: data.length,
118
+ data
119
+ };
120
+ } catch (error) {
121
+ throw new McpError(ErrorCode.InvalidParams, `Failed to decode data URI payload: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ }
124
+ };
125
+
126
+ //#endregion
127
+ //#region src/tools/ReadImageTool.ts
128
+ var ReadImageTool = class ReadImageTool {
129
+ static TOOL_NAME = "read-image";
130
+ service = new ImageReader();
131
+ getDefinition() {
132
+ return {
133
+ name: ReadImageTool.TOOL_NAME,
134
+ description: "Read an image from a local path, URL, or data URI and return metadata.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ source: {
139
+ type: "string",
140
+ minLength: 1,
141
+ description: "Image source as a local path, HTTP(S) URL, or data URI."
142
+ },
143
+ includeBase64: {
144
+ type: "boolean",
145
+ description: "Whether to include base64-encoded output.",
146
+ default: false
147
+ }
148
+ },
149
+ required: ["source"],
150
+ additionalProperties: false
151
+ }
152
+ };
153
+ }
154
+ async execute(input) {
155
+ try {
156
+ if (!input || typeof input !== "object") throw new McpError(ErrorCode.InvalidParams, "Tool input must be an object.");
157
+ const payload = input;
158
+ const source = payload.source;
159
+ const includeBase64 = payload.includeBase64;
160
+ if (!source || typeof source !== "string" || source.trim().length === 0) throw new McpError(ErrorCode.InvalidParams, "source must be a non-empty string.");
161
+ if (includeBase64 !== void 0 && typeof includeBase64 !== "boolean") throw new McpError(ErrorCode.InvalidParams, "includeBase64 must be a boolean.");
162
+ const options = { includeBase64 };
163
+ const result = await this.service.readImage(source, options);
164
+ return { content: [{
165
+ type: "text",
166
+ text: JSON.stringify(result, null, 2)
167
+ }] };
168
+ } catch (error) {
169
+ return {
170
+ content: [{
171
+ type: "text",
172
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
173
+ }],
174
+ isError: true
175
+ };
176
+ }
177
+ }
178
+ };
179
+
180
+ //#endregion
181
+ //#region src/imagineEnvSchema.ts
182
+ /**
183
+ * Environment variable schema for imagine-mcp
184
+ *
185
+ * Following Agiflow naming conventions:
186
+ * - Service credentials use provider-specific prefixes
187
+ * - Upload service selection uses UPLOAD_SERVICE
188
+ */
189
+ const envSchema = z.object({
190
+ UPLOAD_SERVICE: z.enum([
191
+ "s3",
192
+ "cloudflare",
193
+ "gcloud"
194
+ ]).optional().default("s3"),
195
+ S3_BUCKET: z.string().optional(),
196
+ AWS_ACCESS_KEY_ID: z.string().optional(),
197
+ AWS_SECRET_ACCESS_KEY: z.string().optional(),
198
+ S3_REGION: z.string().optional().default("us-east-1"),
199
+ S3_ENDPOINT: z.string().url("S3_ENDPOINT must be a valid URL").optional(),
200
+ CLOUDFLARE_R2_BUCKET: z.string().optional(),
201
+ CLOUDFLARE_R2_ACCESS_KEY_ID: z.string().optional(),
202
+ CLOUDFLARE_R2_SECRET_ACCESS_KEY: z.string().optional(),
203
+ CLOUDFLARE_R2_REGION: z.string().optional().default("auto"),
204
+ CLOUDFLARE_R2_ENDPOINT: z.string().url("CLOUDFLARE_R2_ENDPOINT must be a valid URL").optional(),
205
+ GCLOUD_BUCKET: z.string().optional(),
206
+ GCLOUD_PROJECT_ID: z.string().optional(),
207
+ GCLOUD_CREDENTIALS_PATH: z.string().optional(),
208
+ UNSPLASH_ACCESS_KEY: z.string().optional()
209
+ }).refine((data) => {
210
+ if (data.AWS_ACCESS_KEY_ID || data.AWS_SECRET_ACCESS_KEY) return data.AWS_ACCESS_KEY_ID && data.AWS_SECRET_ACCESS_KEY;
211
+ return true;
212
+ }, {
213
+ message: "Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be provided together",
214
+ path: ["AWS_ACCESS_KEY_ID"]
215
+ });
216
+
217
+ //#endregion
218
+ //#region src/config.ts
219
+ /**
220
+ * Centralized configuration service for imagine-mcp
221
+ *
222
+ * Validates environment variables at startup and provides
223
+ * type-safe access to configuration throughout the application.
224
+ */
225
+ var ConfigService = class ConfigService {
226
+ static instance;
227
+ config;
228
+ constructor() {
229
+ try {
230
+ this.config = envSchema.parse(process.env);
231
+ } catch (error) {
232
+ if (error instanceof z.ZodError) {
233
+ const errorMessage = error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
234
+ throw new McpError(ErrorCode.InternalError, `Environment configuration validation failed: ${errorMessage}`);
235
+ }
236
+ throw error;
237
+ }
238
+ }
239
+ /**
240
+ * Get the singleton instance of ConfigService
241
+ */
242
+ static getInstance() {
243
+ if (!ConfigService.instance) ConfigService.instance = new ConfigService();
244
+ return ConfigService.instance;
245
+ }
246
+ /**
247
+ * Get the validated configuration
248
+ */
249
+ getConfig() {
250
+ return this.config;
251
+ }
252
+ /**
253
+ * Get S3 configuration
254
+ */
255
+ getS3Config() {
256
+ return {
257
+ bucket: this.config.S3_BUCKET,
258
+ accessKeyId: this.config.AWS_ACCESS_KEY_ID,
259
+ secretAccessKey: this.config.AWS_SECRET_ACCESS_KEY,
260
+ region: this.config.S3_REGION,
261
+ endpoint: this.config.S3_ENDPOINT
262
+ };
263
+ }
264
+ /**
265
+ * Get Cloudflare R2 configuration
266
+ */
267
+ getCloudflareConfig() {
268
+ return {
269
+ bucket: this.config.CLOUDFLARE_R2_BUCKET,
270
+ accessKeyId: this.config.CLOUDFLARE_R2_ACCESS_KEY_ID,
271
+ secretAccessKey: this.config.CLOUDFLARE_R2_SECRET_ACCESS_KEY,
272
+ region: this.config.CLOUDFLARE_R2_REGION,
273
+ endpoint: this.config.CLOUDFLARE_R2_ENDPOINT
274
+ };
275
+ }
276
+ /**
277
+ * Get Google Cloud Storage configuration
278
+ */
279
+ getGCloudConfig() {
280
+ return {
281
+ bucket: this.config.GCLOUD_BUCKET,
282
+ projectId: this.config.GCLOUD_PROJECT_ID,
283
+ credentialsPath: this.config.GCLOUD_CREDENTIALS_PATH
284
+ };
285
+ }
286
+ /**
287
+ * Get the default upload service
288
+ */
289
+ getUploadService() {
290
+ return this.config.UPLOAD_SERVICE;
291
+ }
292
+ /**
293
+ * Get Unsplash API configuration
294
+ */
295
+ getUnsplashConfig() {
296
+ return { accessKey: this.config.UNSPLASH_ACCESS_KEY };
297
+ }
298
+ /**
299
+ * Validate that required credentials exist for a specific service
300
+ */
301
+ validateServiceConfig(service) {
302
+ switch (service) {
303
+ case "s3":
304
+ if (!this.config.S3_BUCKET) throw new McpError(ErrorCode.InvalidParams, "S3_BUCKET is required for S3 upload service");
305
+ break;
306
+ case "cloudflare":
307
+ if (!this.config.CLOUDFLARE_R2_BUCKET) throw new McpError(ErrorCode.InvalidParams, "CLOUDFLARE_R2_BUCKET is required for Cloudflare upload service");
308
+ if (!this.config.CLOUDFLARE_R2_ACCESS_KEY_ID || !this.config.CLOUDFLARE_R2_SECRET_ACCESS_KEY) throw new McpError(ErrorCode.InvalidParams, "CLOUDFLARE_R2_ACCESS_KEY_ID and CLOUDFLARE_R2_SECRET_ACCESS_KEY are required for Cloudflare upload service");
309
+ if (!this.config.CLOUDFLARE_R2_ENDPOINT) throw new McpError(ErrorCode.InvalidParams, "CLOUDFLARE_R2_ENDPOINT is required for Cloudflare upload service");
310
+ break;
311
+ case "gcloud":
312
+ if (!this.config.GCLOUD_BUCKET) throw new McpError(ErrorCode.InvalidParams, "GCLOUD_BUCKET is required for Google Cloud upload service");
313
+ if (!this.config.GCLOUD_PROJECT_ID) throw new McpError(ErrorCode.InvalidParams, "GCLOUD_PROJECT_ID is required for Google Cloud upload service");
314
+ break;
315
+ }
316
+ }
317
+ };
318
+ const config = ConfigService.getInstance();
319
+
320
+ //#endregion
321
+ //#region src/services/UnsplashService.ts
322
+ /**
323
+ * UnsplashService
324
+ *
325
+ * DESIGN PATTERNS:
326
+ * - Service pattern for business logic encapsulation
327
+ * - Single responsibility principle
328
+ *
329
+ * CODING STANDARDS:
330
+ * - Use async/await for asynchronous operations
331
+ * - Throw descriptive errors for error cases
332
+ * - Keep methods focused and well-named
333
+ * - Document complex logic with comments
334
+ *
335
+ * AVOID:
336
+ * - Mixing concerns (keep focused on single domain)
337
+ * - Direct tool implementation (services should be tool-agnostic)
338
+ */
339
+ var UnsplashService = class {
340
+ api;
341
+ constructor(accessKey) {
342
+ const unsplashConfig = config.getUnsplashConfig();
343
+ const apiKey = accessKey || unsplashConfig.accessKey;
344
+ if (!apiKey) throw new Error("Unsplash API key is required. Set UNSPLASH_ACCESS_KEY environment variable.");
345
+ this.api = createApi({ accessKey: apiKey });
346
+ }
347
+ async searchImages(params) {
348
+ try {
349
+ const result = await this.api.search.getPhotos({
350
+ query: params.query,
351
+ page: params.page || 1,
352
+ perPage: params.perPage || 10,
353
+ orientation: params.orientation,
354
+ color: params.color
355
+ });
356
+ if (result.errors) throw new Error(`Unsplash API error: ${result.errors.join(", ")}`);
357
+ if (!result.response) throw new Error("No response from Unsplash API");
358
+ return result.response.results.map((photo) => ({
359
+ id: photo.id,
360
+ description: photo.description,
361
+ alt_description: photo.alt_description,
362
+ urls: {
363
+ raw: photo.urls.raw,
364
+ full: photo.urls.full,
365
+ regular: photo.urls.regular,
366
+ small: photo.urls.small,
367
+ thumb: photo.urls.thumb
368
+ },
369
+ links: {
370
+ html: photo.links.html,
371
+ download: photo.links.download
372
+ },
373
+ user: {
374
+ name: photo.user.name,
375
+ username: photo.user.username,
376
+ portfolio_url: photo.user.portfolio_url
377
+ },
378
+ width: photo.width,
379
+ height: photo.height,
380
+ color: photo.color || "#000000",
381
+ likes: photo.likes
382
+ }));
383
+ } catch (error) {
384
+ throw new Error(`Failed to search Unsplash images: ${error instanceof Error ? error.message : "Unknown error"}`);
385
+ }
386
+ }
387
+ };
388
+
389
+ //#endregion
390
+ //#region src/tools/UnsplashSearchTool.ts
391
+ var UnsplashSearchTool = class UnsplashSearchTool {
392
+ static TOOL_NAME = "unsplash_search";
393
+ service;
394
+ accessKey;
395
+ constructor(accessKey) {
396
+ this.accessKey = accessKey;
397
+ this.service = null;
398
+ }
399
+ getService() {
400
+ if (!this.service) this.service = new UnsplashService(this.accessKey);
401
+ return this.service;
402
+ }
403
+ getDefinition() {
404
+ return {
405
+ name: UnsplashSearchTool.TOOL_NAME,
406
+ description: "Search for stock images from Unsplash by keyword and return image URLs with metadata",
407
+ inputSchema: {
408
+ type: "object",
409
+ properties: {
410
+ query: {
411
+ type: "string",
412
+ description: "Search query (e.g., \"sunset\", \"technology\", \"nature\")"
413
+ },
414
+ perPage: {
415
+ type: "number",
416
+ description: "Number of results per page (1-30, default: 10)",
417
+ minimum: 1,
418
+ maximum: 30
419
+ },
420
+ page: {
421
+ type: "number",
422
+ description: "Page number for pagination (default: 1)",
423
+ minimum: 1
424
+ },
425
+ orientation: {
426
+ type: "string",
427
+ enum: [
428
+ "landscape",
429
+ "portrait",
430
+ "squarish"
431
+ ],
432
+ description: "Filter by photo orientation"
433
+ },
434
+ color: {
435
+ type: "string",
436
+ enum: [
437
+ "black_and_white",
438
+ "black",
439
+ "white",
440
+ "yellow",
441
+ "orange",
442
+ "red",
443
+ "purple",
444
+ "magenta",
445
+ "green",
446
+ "teal",
447
+ "blue"
448
+ ],
449
+ description: "Filter by photo color"
450
+ }
451
+ },
452
+ required: ["query"],
453
+ additionalProperties: false
454
+ }
455
+ };
456
+ }
457
+ async execute(input) {
458
+ try {
459
+ const images = await this.getService().searchImages({
460
+ query: input.query,
461
+ perPage: input.perPage,
462
+ page: input.page,
463
+ orientation: input.orientation,
464
+ color: input.color
465
+ });
466
+ if (images.length === 0) return { content: [{
467
+ type: "text",
468
+ text: `No images found for query: "${input.query}"`
469
+ }] };
470
+ const formattedResults = images.map((img, index) => {
471
+ return `
472
+ 📷 **Image ${index + 1}**
473
+ - **ID**: ${img.id}
474
+ - **Description**: ${img.alt_description || img.description || "No description"}
475
+ - **Photographer**: ${img.user.name} (@${img.user.username})
476
+ - **Dimensions**: ${img.width}x${img.height}px
477
+ - **Color**: ${img.color}
478
+ - **Likes**: ${img.likes}
479
+
480
+ **URLs**:
481
+ - Full: ${img.urls.full}
482
+ - Regular: ${img.urls.regular}
483
+ - Small: ${img.urls.small}
484
+ - Thumbnail: ${img.urls.thumb}
485
+
486
+ **Links**:
487
+ - View on Unsplash: ${img.links.html}
488
+ - Download: ${img.links.download}
489
+ ${img.user.portfolio_url ? `- Photographer Portfolio: ${img.user.portfolio_url}` : ""}
490
+ `.trim();
491
+ }).join("\n\n" + "-".repeat(80) + "\n\n");
492
+ return { content: [{
493
+ type: "text",
494
+ text: `Found ${images.length} image(s) for "${input.query}":\n\n${formattedResults}`
495
+ }] };
496
+ } catch (error) {
497
+ return {
498
+ content: [{
499
+ type: "text",
500
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
501
+ }],
502
+ isError: true
503
+ };
504
+ }
505
+ }
506
+ };
507
+
508
+ //#endregion
509
+ //#region src/server/index.ts
510
+ /**
511
+ * MCP Server Setup
512
+ *
513
+ * DESIGN PATTERNS:
514
+ * - Factory pattern for server creation
515
+ * - Tool registration pattern
516
+ *
517
+ * CODING STANDARDS:
518
+ * - Register all tools, resources, and prompts here
519
+ * - Keep server setup modular and extensible
520
+ * - Import tools from ../tools/ and register them in the handlers
521
+ */
522
+ function createServer() {
523
+ const server = new Server({
524
+ name: "imagine-mcp",
525
+ version: "0.1.0"
526
+ }, { capabilities: { tools: {} } });
527
+ const unsplashSearchTool = new UnsplashSearchTool();
528
+ const readImageTool = new ReadImageTool();
529
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [unsplashSearchTool.getDefinition(), readImageTool.getDefinition()] }));
530
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
531
+ const { name, arguments: args } = request.params;
532
+ if (name === UnsplashSearchTool.TOOL_NAME) return await unsplashSearchTool.execute(args);
533
+ if (name === ReadImageTool.TOOL_NAME) return await readImageTool.execute(args);
534
+ return {
535
+ content: [{
536
+ type: "text",
537
+ text: `Unknown tool: ${name}`
538
+ }],
539
+ isError: true
540
+ };
541
+ });
542
+ return server;
543
+ }
544
+
545
+ //#endregion
546
+ //#region src/transports/http.ts
547
+ /**
548
+ * HTTP Transport Handler
549
+ *
550
+ * DESIGN PATTERNS:
551
+ * - Transport handler pattern implementing TransportHandler interface
552
+ * - Session management for stateful connections
553
+ * - Streamable HTTP protocol (2025-03-26) with resumability support
554
+ * - Factory pattern for creating MCP server instances per session
555
+ *
556
+ * CODING STANDARDS:
557
+ * - Use async/await for all asynchronous operations
558
+ * - Implement proper session lifecycle management
559
+ * - Handle errors gracefully with appropriate HTTP status codes
560
+ * - Provide health check endpoint for monitoring
561
+ * - Clean up resources on shutdown
562
+ *
563
+ * AVOID:
564
+ * - Sharing MCP server instances across sessions (use factory pattern)
565
+ * - Forgetting to clean up sessions on disconnect
566
+ * - Missing error handling for request processing
567
+ * - Hardcoded configuration (use TransportConfig)
568
+ */
569
+ /**
570
+ * HTTP session manager
571
+ */
572
+ var HttpFullSessionManager = class {
573
+ sessions = /* @__PURE__ */ new Map();
574
+ getSession(sessionId) {
575
+ return this.sessions.get(sessionId);
576
+ }
577
+ setSession(sessionId, transport, server) {
578
+ this.sessions.set(sessionId, {
579
+ transport,
580
+ server
581
+ });
582
+ }
583
+ deleteSession(sessionId) {
584
+ const session = this.sessions.get(sessionId);
585
+ if (session) session.server.close();
586
+ this.sessions.delete(sessionId);
587
+ }
588
+ hasSession(sessionId) {
589
+ return this.sessions.has(sessionId);
590
+ }
591
+ clear() {
592
+ for (const session of this.sessions.values()) session.server.close();
593
+ this.sessions.clear();
594
+ }
595
+ };
596
+ /**
597
+ * HTTP transport handler using Streamable HTTP (protocol version 2025-03-26)
598
+ * Provides stateful session management with resumability support
599
+ */
600
+ var HttpTransportHandler = class {
601
+ serverFactory;
602
+ app;
603
+ server = null;
604
+ sessionManager;
605
+ config;
606
+ constructor(serverFactory, config$1) {
607
+ this.serverFactory = typeof serverFactory === "function" ? serverFactory : () => serverFactory;
608
+ this.app = express();
609
+ this.sessionManager = new HttpFullSessionManager();
610
+ this.config = {
611
+ mode: config$1.mode,
612
+ port: config$1.port ?? 3e3,
613
+ host: config$1.host ?? "localhost"
614
+ };
615
+ this.setupMiddleware();
616
+ this.setupRoutes();
617
+ }
618
+ setupMiddleware() {
619
+ this.app.use(express.json());
620
+ }
621
+ setupRoutes() {
622
+ this.app.post("/mcp", async (req, res) => {
623
+ await this.handlePostRequest(req, res);
624
+ });
625
+ this.app.get("/mcp", async (req, res) => {
626
+ await this.handleGetRequest(req, res);
627
+ });
628
+ this.app.delete("/mcp", async (req, res) => {
629
+ await this.handleDeleteRequest(req, res);
630
+ });
631
+ this.app.get("/health", (_req, res) => {
632
+ res.json({
633
+ status: "ok",
634
+ transport: "http"
635
+ });
636
+ });
637
+ }
638
+ async handlePostRequest(req, res) {
639
+ const sessionId = req.headers["mcp-session-id"];
640
+ let transport;
641
+ if (sessionId && this.sessionManager.hasSession(sessionId)) transport = this.sessionManager.getSession(sessionId).transport;
642
+ else if (!sessionId && isInitializeRequest(req.body)) {
643
+ const mcpServer = this.serverFactory();
644
+ transport = new StreamableHTTPServerTransport({
645
+ sessionIdGenerator: () => randomUUID(),
646
+ enableJsonResponse: true,
647
+ onsessioninitialized: (sessionId$1) => {
648
+ this.sessionManager.setSession(sessionId$1, transport, mcpServer);
649
+ }
650
+ });
651
+ transport.onclose = () => {
652
+ if (transport.sessionId) this.sessionManager.deleteSession(transport.sessionId);
653
+ };
654
+ await mcpServer.connect(transport);
655
+ } else {
656
+ res.status(400).json({
657
+ jsonrpc: "2.0",
658
+ error: {
659
+ code: -32e3,
660
+ message: "Bad Request: No valid session ID provided"
661
+ },
662
+ id: null
663
+ });
664
+ return;
665
+ }
666
+ await transport.handleRequest(req, res, req.body);
667
+ }
668
+ async handleGetRequest(req, res) {
669
+ const sessionId = req.headers["mcp-session-id"];
670
+ if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
671
+ res.status(400).send("Invalid or missing session ID");
672
+ return;
673
+ }
674
+ await this.sessionManager.getSession(sessionId).transport.handleRequest(req, res);
675
+ }
676
+ async handleDeleteRequest(req, res) {
677
+ const sessionId = req.headers["mcp-session-id"];
678
+ if (!sessionId || !this.sessionManager.hasSession(sessionId)) {
679
+ res.status(400).send("Invalid or missing session ID");
680
+ return;
681
+ }
682
+ await this.sessionManager.getSession(sessionId).transport.handleRequest(req, res);
683
+ this.sessionManager.deleteSession(sessionId);
684
+ }
685
+ async start() {
686
+ return new Promise((resolve$1, reject) => {
687
+ try {
688
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
689
+ process.stderr.write(`imagine-mcp MCP server started on http://${this.config.host}:${this.config.port}/mcp\n`);
690
+ process.stderr.write(`Health check: http://${this.config.host}:${this.config.port}/health\n`);
691
+ resolve$1();
692
+ });
693
+ this.server.on("error", (error) => {
694
+ reject(error);
695
+ });
696
+ } catch (error) {
697
+ reject(error);
698
+ }
699
+ });
700
+ }
701
+ async stop() {
702
+ return new Promise((resolve$1, reject) => {
703
+ if (this.server) {
704
+ this.sessionManager.clear();
705
+ this.server.close((err) => {
706
+ if (err) reject(err);
707
+ else {
708
+ this.server = null;
709
+ resolve$1();
710
+ }
711
+ });
712
+ } else resolve$1();
713
+ });
714
+ }
715
+ getPort() {
716
+ return this.config.port;
717
+ }
718
+ getHost() {
719
+ return this.config.host;
720
+ }
721
+ };
722
+
723
+ //#endregion
724
+ //#region src/transports/sse.ts
725
+ /**
726
+ * Session manager for SSE transports
727
+ */
728
+ var SseSessionManager = class {
729
+ sessions = /* @__PURE__ */ new Map();
730
+ getSession(sessionId) {
731
+ return this.sessions.get(sessionId)?.transport;
732
+ }
733
+ setSession(sessionId, transport, server) {
734
+ this.sessions.set(sessionId, {
735
+ transport,
736
+ server
737
+ });
738
+ }
739
+ deleteSession(sessionId) {
740
+ const session = this.sessions.get(sessionId);
741
+ if (session) session.server.close();
742
+ this.sessions.delete(sessionId);
743
+ }
744
+ hasSession(sessionId) {
745
+ return this.sessions.has(sessionId);
746
+ }
747
+ clear() {
748
+ for (const session of this.sessions.values()) session.server.close();
749
+ this.sessions.clear();
750
+ }
751
+ };
752
+ /**
753
+ * SSE (Server-Sent Events) transport handler
754
+ * Legacy transport for backwards compatibility (protocol version 2024-11-05)
755
+ * Uses separate endpoints: /sse for SSE stream (GET) and /messages for client messages (POST)
756
+ */
757
+ var SseTransportHandler = class {
758
+ serverFactory;
759
+ app;
760
+ server = null;
761
+ sessionManager;
762
+ config;
763
+ constructor(serverFactory, config$1) {
764
+ this.serverFactory = typeof serverFactory === "function" ? serverFactory : () => serverFactory;
765
+ this.app = express();
766
+ this.sessionManager = new SseSessionManager();
767
+ this.config = {
768
+ mode: config$1.mode,
769
+ port: config$1.port ?? 3e3,
770
+ host: config$1.host ?? "localhost"
771
+ };
772
+ this.setupMiddleware();
773
+ this.setupRoutes();
774
+ }
775
+ setupMiddleware() {
776
+ this.app.use(express.json());
777
+ }
778
+ setupRoutes() {
779
+ this.app.get("/sse", async (req, res) => {
780
+ await this.handleSseConnection(req, res);
781
+ });
782
+ this.app.post("/messages", async (req, res) => {
783
+ await this.handlePostMessage(req, res);
784
+ });
785
+ this.app.get("/health", (_req, res) => {
786
+ res.json({
787
+ status: "ok",
788
+ transport: "sse"
789
+ });
790
+ });
791
+ }
792
+ async handleSseConnection(_req, res) {
793
+ try {
794
+ const mcpServer = this.serverFactory();
795
+ const transport = new SSEServerTransport("/messages", res);
796
+ this.sessionManager.setSession(transport.sessionId, transport, mcpServer);
797
+ res.on("close", () => {
798
+ this.sessionManager.deleteSession(transport.sessionId);
799
+ });
800
+ await mcpServer.connect(transport);
801
+ process.stderr.write(`SSE session established: ${transport.sessionId}\n`);
802
+ } catch (error) {
803
+ process.stderr.write(`Error handling SSE connection: ${error instanceof Error ? error.message : String(error)}\n`);
804
+ if (!res.headersSent) res.status(500).send("Internal Server Error");
805
+ }
806
+ }
807
+ async handlePostMessage(req, res) {
808
+ const sessionId = req.query.sessionId;
809
+ if (!sessionId) {
810
+ res.status(400).send("Missing sessionId query parameter");
811
+ return;
812
+ }
813
+ const transport = this.sessionManager.getSession(sessionId);
814
+ if (!transport) {
815
+ res.status(404).send("No transport found for sessionId");
816
+ return;
817
+ }
818
+ try {
819
+ await transport.handlePostMessage(req, res, req.body);
820
+ } catch (error) {
821
+ process.stderr.write(`Error handling post message: ${error instanceof Error ? error.message : String(error)}\n`);
822
+ if (!res.headersSent) res.status(500).send("Internal Server Error");
823
+ }
824
+ }
825
+ async start() {
826
+ return new Promise((resolve$1, reject) => {
827
+ try {
828
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
829
+ process.stderr.write(`imagine-mcp MCP server started with SSE transport on http://${this.config.host}:${this.config.port}\n`);
830
+ process.stderr.write(`SSE endpoint: http://${this.config.host}:${this.config.port}/sse\n`);
831
+ process.stderr.write(`Messages endpoint: http://${this.config.host}:${this.config.port}/messages\n`);
832
+ process.stderr.write(`Health check: http://${this.config.host}:${this.config.port}/health\n`);
833
+ resolve$1();
834
+ });
835
+ this.server.on("error", (error) => {
836
+ reject(error);
837
+ });
838
+ } catch (error) {
839
+ reject(error);
840
+ }
841
+ });
842
+ }
843
+ async stop() {
844
+ return new Promise((resolve$1, reject) => {
845
+ if (this.server) {
846
+ this.sessionManager.clear();
847
+ this.server.close((err) => {
848
+ if (err) reject(err);
849
+ else {
850
+ this.server = null;
851
+ resolve$1();
852
+ }
853
+ });
854
+ } else resolve$1();
855
+ });
856
+ }
857
+ getPort() {
858
+ return this.config.port;
859
+ }
860
+ getHost() {
861
+ return this.config.host;
862
+ }
863
+ };
864
+
865
+ //#endregion
866
+ //#region src/transports/stdio.ts
867
+ /**
868
+ * Stdio transport handler for MCP server
869
+ * Used for command-line and direct integrations
870
+ */
871
+ var StdioTransportHandler = class {
872
+ server;
873
+ transport = null;
874
+ constructor(server) {
875
+ this.server = server;
876
+ }
877
+ async start() {
878
+ this.transport = new StdioServerTransport();
879
+ await this.server.connect(this.transport);
880
+ process.stderr.write("imagine-mcp MCP server started on stdio\n");
881
+ }
882
+ async stop() {
883
+ if (this.transport) {
884
+ await this.transport.close();
885
+ this.transport = null;
886
+ }
887
+ }
888
+ };
889
+
890
+ //#endregion
891
+ export { UnsplashSearchTool as a, createServer as i, SseTransportHandler as n, ReadImageTool as o, HttpTransportHandler as r, StdioTransportHandler as t };