@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.
- package/LICENSE +52 -0
- package/README.md +144 -0
- package/dist/cli.cjs +129 -0
- package/dist/cli.d.cts +43 -0
- package/dist/cli.d.mts +43 -0
- package/dist/cli.mjs +125 -0
- package/dist/commands-BlRAon0l.cjs +83 -0
- package/dist/commands-c927CRZc.mjs +83 -0
- package/dist/index.cjs +8 -0
- package/dist/index.d.cts +150 -0
- package/dist/index.d.mts +150 -0
- package/dist/index.mjs +3 -0
- package/dist/stdio-CuIrQe3m.mjs +891 -0
- package/dist/stdio-D7OcvsZd.cjs +957 -0
- package/package.json +59 -0
|
@@ -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 };
|