@sylphx/image-reader-mcp 0.1.0 → 0.1.1

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/dist/index.js DELETED
@@ -1,624 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/index.ts
4
- import { createRequire } from "node:module";
5
-
6
- // src/handlers/readImage.ts
7
- import fs2 from "node:fs/promises";
8
- import path2 from "node:path";
9
- import exifr from "exifr";
10
- import sharp from "sharp";
11
-
12
- // src/mcp.ts
13
- import { createHash, timingSafeEqual } from "node:crypto";
14
- import { createServer as createHttpServer } from "node:http";
15
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
- import { z } from "zod";
19
- var text = (value) => ({ type: "text", text: value });
20
- var toolError = (message) => ({
21
- content: [text(message)],
22
- isError: true
23
- });
24
-
25
- class ToolBuilder {
26
- #description;
27
- #inputSchema;
28
- constructor(descriptionValue, inputSchema) {
29
- this.#description = descriptionValue;
30
- this.#inputSchema = inputSchema;
31
- }
32
- description(value) {
33
- return new ToolBuilder(value, this.#inputSchema);
34
- }
35
- input(schema) {
36
- return new ToolBuilder(this.#description, schema);
37
- }
38
- handler(handler) {
39
- return {
40
- description: this.#description ?? "",
41
- inputSchema: this.#inputSchema ?? z.object({}),
42
- handler
43
- };
44
- }
45
- }
46
- var tool = () => new ToolBuilder;
47
- var stdio = () => ({ kind: "stdio" });
48
- var http = (config) => ({ kind: "http", ...config });
49
- var isCallToolResult = (result) => typeof result === "object" && result !== null && ("content" in result);
50
- var isContentArray = (result) => Array.isArray(result);
51
- var normalizeToolResult = (result) => {
52
- if (isContentArray(result))
53
- return { content: [...result] };
54
- if (isCallToolResult(result))
55
- return result;
56
- return { content: [result] };
57
- };
58
- var withCors = (res, origin) => {
59
- if (!origin)
60
- return;
61
- res.setHeader("Access-Control-Allow-Origin", origin);
62
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
63
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, MCP-Protocol-Version, Mcp-Session-Id, X-API-Key");
64
- };
65
- var isApiKeyValid = (configuredKey, presented) => {
66
- const provided = Array.isArray(presented) ? presented[0] : presented;
67
- if (typeof provided !== "string" || provided.length === 0)
68
- return false;
69
- const expected = createHash("sha256").update(configuredKey).digest();
70
- const actual = createHash("sha256").update(provided).digest();
71
- return timingSafeEqual(expected, actual);
72
- };
73
- var unauthorized = (res) => {
74
- res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": "X-API-Key" });
75
- res.end(JSON.stringify({
76
- jsonrpc: "2.0",
77
- id: null,
78
- error: { code: -32001, message: "Unauthorized: missing or invalid X-API-Key header" }
79
- }));
80
- };
81
- var ensureStreamableAcceptHeader = (req) => {
82
- const accept = req.headers.accept;
83
- const acceptValues = Array.isArray(accept) ? accept.join(", ") : accept ?? "";
84
- if (acceptValues.includes("application/json") && acceptValues.includes("text/event-stream")) {
85
- return;
86
- }
87
- req.headers.accept = "application/json, text/event-stream";
88
- const rawAcceptIndex = req.rawHeaders.findIndex((value, index) => index % 2 === 0 && value.toLowerCase() === "accept");
89
- if (rawAcceptIndex >= 0) {
90
- req.rawHeaders[rawAcceptIndex + 1] = "application/json, text/event-stream";
91
- return;
92
- }
93
- req.rawHeaders.push("Accept", "application/json, text/event-stream");
94
- };
95
- var handleNonMcpRoute = (req, res, pathname, transportConfig) => {
96
- if (pathname === "/mcp/health" && req.method === "GET") {
97
- res.writeHead(200, { "Content-Type": "application/json" });
98
- res.end(JSON.stringify({ status: "ok" }));
99
- return true;
100
- }
101
- if (pathname !== "/mcp") {
102
- res.writeHead(404, { "Content-Type": "application/json" });
103
- res.end(JSON.stringify({ error: "Not found" }));
104
- return true;
105
- }
106
- if (req.method === "OPTIONS") {
107
- res.writeHead(204);
108
- res.end();
109
- return true;
110
- }
111
- if (transportConfig.apiKey !== undefined && !isApiKeyValid(transportConfig.apiKey, req.headers["x-api-key"])) {
112
- unauthorized(res);
113
- return true;
114
- }
115
- return false;
116
- };
117
- var handleHttpRequest = async (req, res, mcpServer, transportConfig) => {
118
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? transportConfig.hostname}`);
119
- withCors(res, transportConfig.cors);
120
- if (handleNonMcpRoute(req, res, url.pathname, transportConfig))
121
- return;
122
- ensureStreamableAcceptHeader(req);
123
- const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
124
- try {
125
- await mcpServer.connect(transport);
126
- await transport.handleRequest(req, res);
127
- } finally {
128
- await mcpServer.close();
129
- }
130
- };
131
- var startHttpServer = async (serverInfo, transportConfig) => {
132
- const mcpServer = buildMcpServer(serverInfo);
133
- const httpServer = createHttpServer((req, res) => {
134
- handleHttpRequest(req, res, mcpServer, transportConfig);
135
- });
136
- await new Promise((resolve, reject) => {
137
- httpServer.once("error", reject);
138
- httpServer.listen(transportConfig.port, transportConfig.hostname, () => {
139
- httpServer.off("error", reject);
140
- resolve();
141
- });
142
- });
143
- return { mcpServer, httpServer };
144
- };
145
- var buildMcpServer = ({
146
- name,
147
- version,
148
- instructions,
149
- tools
150
- }) => {
151
- const mcpServer = new McpServer({ name, version }, {
152
- ...instructions ? { instructions } : {}
153
- });
154
- for (const [toolName, definition] of Object.entries(tools)) {
155
- mcpServer.registerTool(toolName, {
156
- description: definition.description,
157
- inputSchema: definition.inputSchema
158
- }, async (input, ctx) => normalizeToolResult(await definition.handler({ input, ctx })));
159
- }
160
- return mcpServer;
161
- };
162
- var createServer = (options) => {
163
- let mcpServer;
164
- let httpServer;
165
- return {
166
- async start() {
167
- if (options.transport.kind === "stdio") {
168
- mcpServer = buildMcpServer(options);
169
- await mcpServer.connect(new StdioServerTransport);
170
- return;
171
- }
172
- const started = await startHttpServer(options, options.transport);
173
- mcpServer = started.mcpServer;
174
- httpServer = started.httpServer;
175
- },
176
- async close() {
177
- await mcpServer?.close();
178
- const serverToClose = httpServer;
179
- if (!serverToClose)
180
- return;
181
- await new Promise((resolve, reject) => {
182
- serverToClose.close((error) => {
183
- if (error) {
184
- reject(error);
185
- return;
186
- }
187
- resolve();
188
- });
189
- });
190
- }
191
- };
192
- };
193
-
194
- // src/schemas/readImage.ts
195
- import { z as z2 } from "zod";
196
- var boundingBoxSchema = z2.object({
197
- x: z2.number().describe("Left edge in pixels."),
198
- y: z2.number().describe("Top edge in pixels."),
199
- width: z2.number().describe("Width in pixels."),
200
- height: z2.number().describe("Height in pixels.")
201
- });
202
- var ocrLineSchema = z2.object({
203
- text: z2.string(),
204
- bbox: boundingBoxSchema,
205
- confidence: z2.number().min(0).max(100).optional()
206
- });
207
- var imageDimensionsSchema = z2.object({
208
- width: z2.number().int().positive(),
209
- height: z2.number().int().positive()
210
- });
211
- var readImageArgsSchema = z2.object({
212
- path: z2.string().min(1).describe("Path to the local image file (absolute or relative to cwd)."),
213
- include_metadata: z2.boolean().optional().describe("Include EXIF, XMP, and IPTC metadata when present. Defaults to true."),
214
- include_ocr: z2.boolean().optional().describe("Attempt OCR via the local Tesseract adapter when installed. Defaults to false; gracefully skips when unavailable."),
215
- ocr_languages: z2.array(z2.string().min(1)).optional().describe('OCR language codes for Tesseract (e.g. ["eng"]). Defaults to ["eng"].')
216
- });
217
- var agentMediaTwinSchema = z2.object({
218
- filename: z2.string(),
219
- mime: z2.string(),
220
- dimensions: imageDimensionsSchema,
221
- orientation: z2.number().int().optional(),
222
- color_space: z2.string().optional(),
223
- has_alpha: z2.boolean().optional(),
224
- metadata: z2.record(z2.string(), z2.unknown()).optional(),
225
- ocr: z2.object({
226
- available: z2.boolean(),
227
- skipped_reason: z2.string().optional(),
228
- lines: z2.array(ocrLineSchema)
229
- }).optional(),
230
- trust_warnings: z2.array(z2.string())
231
- });
232
-
233
- // src/utils/errors.ts
234
- class ImageError extends Error {
235
- code;
236
- constructor(code, message, options) {
237
- super(message, options?.cause ? { cause: options.cause } : undefined);
238
- this.code = code;
239
- this.name = "ImageError";
240
- }
241
- }
242
-
243
- // src/utils/metadata.ts
244
- var GPS_FIELD_PATTERN = /^(gps|geo|location|latitude|longitude|altitude|coordinates)/i;
245
- var GPS_NESTED_KEYS = new Set([
246
- "latitude",
247
- "longitude",
248
- "altitude",
249
- "lat",
250
- "lon",
251
- "lng",
252
- "GPSLatitude",
253
- "GPSLongitude",
254
- "GPSAltitude",
255
- "GPSLatitudeRef",
256
- "GPSLongitudeRef",
257
- "GPSAltitudeRef",
258
- "GPSDateStamp",
259
- "GPSTimeStamp",
260
- "GPSProcessingMethod",
261
- "GPSAreaInformation",
262
- "GPSDOP",
263
- "GPSMapDatum",
264
- "GPSDestLatitude",
265
- "GPSDestLongitude",
266
- "GPSDestBearing",
267
- "GPSDestDistance",
268
- "GPSHPositioningError"
269
- ]);
270
- var isGpsKey = (key) => GPS_FIELD_PATTERN.test(key) || GPS_NESTED_KEYS.has(key);
271
- var redactValue = (value) => {
272
- if (Array.isArray(value)) {
273
- return value.map((item) => redactValue(item));
274
- }
275
- if (value !== null && typeof value === "object") {
276
- return redactGpsFields(value).metadata;
277
- }
278
- return "[redacted]";
279
- };
280
- var redactGpsFields = (metadata) => {
281
- let hadGps = false;
282
- const redacted = {};
283
- for (const [key, value] of Object.entries(metadata)) {
284
- if (key.toLowerCase() === "gps" && value !== null && typeof value === "object") {
285
- hadGps = true;
286
- redacted[key] = "[redacted]";
287
- continue;
288
- }
289
- if (isGpsKey(key)) {
290
- hadGps = true;
291
- redacted[key] = redactValue(value);
292
- continue;
293
- }
294
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
295
- const nested = redactGpsFields(value);
296
- if (nested.hadGps)
297
- hadGps = true;
298
- redacted[key] = nested.metadata;
299
- continue;
300
- }
301
- redacted[key] = value;
302
- }
303
- return { metadata: redacted, hadGps };
304
- };
305
- var collectTrustWarnings = (metadata, hadGps) => {
306
- const warnings = [];
307
- if (hadGps) {
308
- warnings.push("GPS coordinates were present in metadata and have been redacted from the response.");
309
- }
310
- const software = metadata["Software"] ?? metadata["software"];
311
- if (typeof software === "string" && /photoshop|gimp|ai|generative|midjourney|stable diffusion/i.test(software)) {
312
- warnings.push(`EXIF Software field suggests possible editing or synthetic origin: "${software}".`);
313
- }
314
- const make = metadata["Make"] ?? metadata["make"];
315
- const model = metadata["Model"] ?? metadata["model"];
316
- if (typeof make === "string" && typeof model === "string" && /unknown|fake|synthetic/i.test(`${make} ${model}`)) {
317
- warnings.push("Camera make/model metadata looks inconsistent or synthetic.");
318
- }
319
- return warnings;
320
- };
321
-
322
- // src/utils/ocr.ts
323
- import { spawnSync } from "node:child_process";
324
- var OCR_HEALTHCHECK_TIMEOUT_MS = 2500;
325
- var OCR_TIMEOUT_MS = 60000;
326
- var parseTesseractTsv = (raw) => {
327
- const lines = raw.split(/\r?\n/).filter((line) => line.length > 0);
328
- if (lines.length <= 1)
329
- return [];
330
- const rows = lines.slice(1).map((line) => line.split("\t"));
331
- const lineMap = new Map;
332
- for (const columns of rows) {
333
- if (columns.length < 12)
334
- continue;
335
- const level = Number.parseInt(columns[0] ?? "", 10);
336
- if (level !== 5)
337
- continue;
338
- const text2 = columns[11]?.trim() ?? "";
339
- if (text2.length === 0)
340
- continue;
341
- const lineNum = Number.parseInt(columns[4] ?? "", 10);
342
- const left = Number.parseInt(columns[6] ?? "", 10);
343
- const top = Number.parseInt(columns[7] ?? "", 10);
344
- const width = Number.parseInt(columns[8] ?? "", 10);
345
- const height = Number.parseInt(columns[9] ?? "", 10);
346
- const conf = Number.parseFloat(columns[10] ?? "");
347
- if (!Number.isFinite(lineNum) || !Number.isFinite(left) || !Number.isFinite(top))
348
- continue;
349
- const bucket = lineMap.get(lineNum) ?? { words: [] };
350
- bucket.words.push({
351
- text: text2,
352
- left,
353
- top,
354
- width: Number.isFinite(width) ? width : 0,
355
- height: Number.isFinite(height) ? height : 0,
356
- conf: Number.isFinite(conf) ? conf : 0
357
- });
358
- lineMap.set(lineNum, bucket);
359
- }
360
- const ocrLines = [];
361
- for (const bucket of lineMap.values()) {
362
- if (bucket.words.length === 0)
363
- continue;
364
- const sorted = [...bucket.words].sort((a, b) => a.left - b.left);
365
- const text2 = sorted.map((word) => word.text).join(" ").trim();
366
- if (text2.length === 0)
367
- continue;
368
- const left = Math.min(...sorted.map((word) => word.left));
369
- const top = Math.min(...sorted.map((word) => word.top));
370
- const right = Math.max(...sorted.map((word) => word.left + word.width));
371
- const bottom = Math.max(...sorted.map((word) => word.top + word.height));
372
- const confidenceValues = sorted.map((word) => word.conf).filter((value) => value >= 0);
373
- const confidence = confidenceValues.length > 0 ? confidenceValues.reduce((sum, value) => sum + value, 0) / confidenceValues.length : undefined;
374
- ocrLines.push({
375
- text: text2,
376
- bbox: {
377
- x: left,
378
- y: top,
379
- width: Math.max(0, right - left),
380
- height: Math.max(0, bottom - top)
381
- },
382
- ...confidence !== undefined ? { confidence } : {}
383
- });
384
- }
385
- return ocrLines.sort((a, b) => a.bbox.y - b.bbox.y || a.bbox.x - b.bbox.x);
386
- };
387
- var isTesseractAvailable = () => {
388
- const result = spawnSync("tesseract", ["--version"], {
389
- timeout: OCR_HEALTHCHECK_TIMEOUT_MS,
390
- windowsHide: true,
391
- stdio: "ignore"
392
- });
393
- return result.status === 0;
394
- };
395
- var runTesseractOcr = (imagePath, languages) => {
396
- if (!isTesseractAvailable()) {
397
- return {
398
- available: false,
399
- skipped_reason: "Tesseract is not installed or not available on PATH.",
400
- lines: []
401
- };
402
- }
403
- const languageArg = languages.length > 0 ? languages.join("+") : "eng";
404
- const result = spawnSync("tesseract", [imagePath, "stdout", "-l", languageArg, "tsv"], {
405
- encoding: "utf8",
406
- timeout: OCR_TIMEOUT_MS,
407
- windowsHide: true,
408
- maxBuffer: 10 * 1024 * 1024
409
- });
410
- if (result.error) {
411
- return {
412
- available: false,
413
- skipped_reason: `Tesseract failed to start: ${result.error.message}`,
414
- lines: []
415
- };
416
- }
417
- if (result.status !== 0) {
418
- const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
419
- return {
420
- available: false,
421
- skipped_reason: stderr.length > 0 ? stderr : `Tesseract exited with status ${String(result.status)}.`,
422
- lines: []
423
- };
424
- }
425
- const stdout = typeof result.stdout === "string" ? result.stdout : "";
426
- return {
427
- available: true,
428
- lines: parseTesseractTsv(stdout)
429
- };
430
- };
431
-
432
- // src/utils/pathUtils.ts
433
- import fs from "node:fs";
434
- import path from "node:path";
435
- var PROJECT_ROOT = process.cwd();
436
- var canonicalize = (p) => {
437
- try {
438
- return fs.realpathSync(p);
439
- } catch (err) {
440
- if (typeof err === "object" && err !== null && "code" in err && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
441
- const parent = path.dirname(p);
442
- if (parent === p)
443
- return p;
444
- return path.join(canonicalize(parent), path.basename(p));
445
- }
446
- throw err;
447
- }
448
- };
449
- var resolvePath = (userPath) => {
450
- if (typeof userPath !== "string") {
451
- throw new ImageError(-32602 /* InvalidParams */, "Path must be a string.");
452
- }
453
- const normalizedUserPath = path.normalize(userPath);
454
- const resolved = path.isAbsolute(normalizedUserPath) ? normalizedUserPath : path.resolve(PROJECT_ROOT, normalizedUserPath);
455
- return canonicalize(resolved);
456
- };
457
-
458
- // src/handlers/readImage.ts
459
- var mimeFromFormat = (format) => {
460
- switch (format) {
461
- case "jpeg":
462
- return "image/jpeg";
463
- case "png":
464
- return "image/png";
465
- case "webp":
466
- return "image/webp";
467
- case "gif":
468
- return "image/gif";
469
- case "tiff":
470
- return "image/tiff";
471
- case "avif":
472
- return "image/avif";
473
- case "heif":
474
- return "image/heif";
475
- default:
476
- return format ? `image/${format}` : "application/octet-stream";
477
- }
478
- };
479
- var readMetadata = async (filePath, includeMetadata) => {
480
- if (!includeMetadata) {
481
- return { trustWarnings: [] };
482
- }
483
- try {
484
- const parsed = await exifr.parse(filePath, {
485
- tiff: true,
486
- xmp: true,
487
- iptc: true,
488
- icc: false,
489
- jfif: false,
490
- ihdr: false,
491
- mergeOutput: true
492
- });
493
- if (!parsed || typeof parsed !== "object" || Object.keys(parsed).length === 0) {
494
- return {
495
- trustWarnings: ["No EXIF, XMP, or IPTC metadata was found in this image."]
496
- };
497
- }
498
- const rawMetadata = parsed;
499
- const { metadata, hadGps } = redactGpsFields(rawMetadata);
500
- const trustWarnings = collectTrustWarnings(rawMetadata, hadGps);
501
- return { metadata, trustWarnings };
502
- } catch {
503
- return {
504
- trustWarnings: ["Metadata extraction failed or metadata is not present in this image."]
505
- };
506
- }
507
- };
508
- var readImage = tool().description("Evidence-first image reader. Returns an Agent Media Twin with filename, mime, dimensions, metadata, optional OCR lines with bounding boxes, and trust warnings. No generative LLM is used.").input(readImageArgsSchema).handler(async ({ input }) => {
509
- let resolvedPath;
510
- try {
511
- resolvedPath = resolvePath(input.path);
512
- } catch (error) {
513
- if (error instanceof ImageError) {
514
- return toolError(error.message);
515
- }
516
- throw error;
517
- }
518
- try {
519
- await fs2.access(resolvedPath);
520
- } catch (error) {
521
- const message = error instanceof Error ? error.message : "File not found.";
522
- return toolError(`Unable to read image at '${input.path}': ${message}`);
523
- }
524
- try {
525
- const image = sharp(resolvedPath, { failOn: "none" });
526
- const metadata = await image.metadata();
527
- const includeMetadata = input.include_metadata ?? true;
528
- const includeOcr = input.include_ocr ?? false;
529
- const ocrLanguages = input.ocr_languages ?? ["eng"];
530
- const { metadata: extractedMetadata, trustWarnings } = await readMetadata(resolvedPath, includeMetadata);
531
- const twin = {
532
- filename: path2.basename(resolvedPath),
533
- mime: mimeFromFormat(metadata.format),
534
- dimensions: {
535
- width: metadata.width ?? 0,
536
- height: metadata.height ?? 0
537
- },
538
- trust_warnings: [...trustWarnings]
539
- };
540
- if (metadata.orientation !== undefined) {
541
- twin.orientation = metadata.orientation;
542
- }
543
- if (metadata.space !== undefined) {
544
- twin.color_space = metadata.space;
545
- }
546
- if (metadata.hasAlpha !== undefined) {
547
- twin.has_alpha = metadata.hasAlpha;
548
- }
549
- if (extractedMetadata !== undefined) {
550
- twin.metadata = extractedMetadata;
551
- }
552
- if (twin.dimensions.width <= 0 || twin.dimensions.height <= 0) {
553
- throw new ImageError(-32600 /* InvalidRequest */, `Unable to determine image dimensions for '${input.path}'.`);
554
- }
555
- if (includeOcr) {
556
- const ocr = runTesseractOcr(resolvedPath, ocrLanguages);
557
- twin.ocr = {
558
- available: ocr.available,
559
- lines: ocr.lines,
560
- ...ocr.skipped_reason !== undefined ? { skipped_reason: ocr.skipped_reason } : {}
561
- };
562
- }
563
- return text(JSON.stringify(twin, null, 2));
564
- } catch (error) {
565
- if (error instanceof ImageError) {
566
- return toolError(error.message);
567
- }
568
- const message = error instanceof Error ? error.message : "Unknown image read failure.";
569
- return toolError(`Failed to read image '${input.path}': ${message}`);
570
- }
571
- });
572
-
573
- // src/index.ts
574
- var require2 = createRequire(import.meta.url);
575
- var packageJson = require2("../package.json");
576
- var transportType = process.env["MCP_TRANSPORT"] ?? "stdio";
577
- var httpPort = Number.parseInt(process.env["MCP_HTTP_PORT"] ?? "8080", 10);
578
- var httpHost = process.env["MCP_HTTP_HOST"] ?? "127.0.0.1";
579
- var apiKey = process.env["MCP_API_KEY"];
580
- var corsOrigin = process.env["MCP_CORS_ORIGIN"];
581
- var isLoopbackHost = (host) => host === "localhost" || host === "::1" || host === "127.0.0.1" || host.startsWith("127.");
582
- function createTransport() {
583
- if (transportType === "http") {
584
- return http({
585
- port: httpPort,
586
- hostname: httpHost,
587
- ...corsOrigin ? { cors: corsOrigin } : {},
588
- ...apiKey ? { apiKey } : {}
589
- });
590
- }
591
- return stdio();
592
- }
593
- var server = createServer({
594
- name: "image-reader-mcp",
595
- version: packageJson.version,
596
- instructions: "Evidence-first image reader MCP server. Use read_image to extract measurable facts — dimensions, metadata, optional OCR text with bounding boxes, and trust warnings — without generative LLM.",
597
- tools: {
598
- read_image: readImage
599
- },
600
- transport: createTransport()
601
- });
602
- async function main() {
603
- await server.start();
604
- if (transportType === "http") {
605
- console.log(`[Image Reader MCP] Server running on http://${httpHost}:${httpPort}/mcp`);
606
- console.log(`[Image Reader MCP] Health check: http://${httpHost}:${httpPort}/mcp/health`);
607
- if (apiKey) {
608
- console.log("[Image Reader MCP] API key authentication enabled (X-API-Key header)");
609
- } else if (!isLoopbackHost(httpHost)) {
610
- console.warn(`[Image Reader MCP] WARNING: bound to non-loopback host ${httpHost} with no API key. ` + "Any client that can reach this port can read every image this process can access. " + "Set MCP_API_KEY to require an X-API-Key header, or bind MCP_HTTP_HOST=127.0.0.1.");
611
- }
612
- if (corsOrigin) {
613
- console.log(`[Image Reader MCP] CORS allowed origin: ${corsOrigin}`);
614
- }
615
- console.log("[Image Reader MCP] Project root:", process.cwd());
616
- } else if (process.env["DEBUG_MCP"]) {
617
- console.error("[Image Reader MCP] Server running on stdio");
618
- console.error("[Image Reader MCP] Project root:", process.cwd());
619
- }
620
- }
621
- main().catch((error) => {
622
- console.error("[Image Reader MCP] Server error:", error);
623
- process.exit(1);
624
- });