@rog0x/mcp-image-tools 1.0.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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # mcp-image-tools
2
+
3
+ MCP server providing image analysis tools for AI agents. Metadata inspection only -- no image processing or manipulation.
4
+
5
+ ## Tools
6
+
7
+ ### image_metadata
8
+ Read image metadata from a URL using HTTP headers. Returns content-type, file size, last-modified, etag, and cache info without downloading the full image.
9
+
10
+ ### find_favicons
11
+ Find all favicons for any website. Checks `/favicon.ico`, parses HTML `<link>` tags, and inspects `manifest.json`. Returns all discovered favicons with sizes and types.
12
+
13
+ ### extract_og_image
14
+ Extract Open Graph image, Twitter card image, and Apple touch icon from any URL. Useful for generating link previews.
15
+
16
+ ### generate_placeholder
17
+ Generate placeholder image URLs via the placehold.co API. Supports custom dimensions, colors, text, format, and font. Returns the URL plus ready-to-use HTML and Markdown markup. Can generate multiple sizes at once.
18
+
19
+ ### responsive_images
20
+ Generate `srcset` and `<picture>` element HTML for responsive images. Given a base image URL, produces multiple size variants with proper markup for responsive design, including multi-format `<source>` elements.
21
+
22
+ ## Setup
23
+
24
+ ```bash
25
+ npm install
26
+ npm run build
27
+ ```
28
+
29
+ ## Usage with Claude Desktop
30
+
31
+ Add to your Claude Desktop config:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "image-tools": {
37
+ "command": "node",
38
+ "args": ["path/to/mcp-image-tools/dist/index.js"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const image_meta_js_1 = require("./tools/image-meta.js");
8
+ const favicon_finder_js_1 = require("./tools/favicon-finder.js");
9
+ const og_image_js_1 = require("./tools/og-image.js");
10
+ const placeholder_generator_js_1 = require("./tools/placeholder-generator.js");
11
+ const responsive_images_js_1 = require("./tools/responsive-images.js");
12
+ const server = new index_js_1.Server({
13
+ name: "mcp-image-tools",
14
+ version: "1.0.0",
15
+ }, {
16
+ capabilities: {
17
+ tools: {},
18
+ },
19
+ });
20
+ // List available tools
21
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
22
+ tools: [
23
+ {
24
+ name: "image_metadata",
25
+ description: "Read image metadata from a URL via HTTP headers: content-type, file size, last-modified, etag, cache info. No image downloading required.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ url: {
30
+ type: "string",
31
+ description: "Direct URL to an image file",
32
+ },
33
+ },
34
+ required: ["url"],
35
+ },
36
+ },
37
+ {
38
+ name: "find_favicons",
39
+ description: "Find all favicons for a website. Checks /favicon.ico, parses HTML link tags, and inspects manifest.json. Returns all found favicons with sizes and types.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ url: {
44
+ type: "string",
45
+ description: "Website URL to find favicons for (e.g. https://example.com)",
46
+ },
47
+ },
48
+ required: ["url"],
49
+ },
50
+ },
51
+ {
52
+ name: "extract_og_image",
53
+ description: "Extract Open Graph image, Twitter card image, and Apple touch icon from any URL. Returns all social media preview images found on the page.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ url: {
58
+ type: "string",
59
+ description: "URL of the web page to extract images from",
60
+ },
61
+ },
62
+ required: ["url"],
63
+ },
64
+ },
65
+ {
66
+ name: "generate_placeholder",
67
+ description: "Generate placeholder image URLs via placehold.co. Supports custom size, colors, text, format, and font. Returns URL plus ready-to-use HTML and Markdown markup.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ width: {
72
+ type: "number",
73
+ description: "Image width in pixels (1-4000)",
74
+ },
75
+ height: {
76
+ type: "number",
77
+ description: "Image height in pixels (1-4000). Defaults to same as width.",
78
+ },
79
+ backgroundColor: {
80
+ type: "string",
81
+ description: "Background color hex without # (e.g. 'cccccc'). Default: cccccc",
82
+ },
83
+ textColor: {
84
+ type: "string",
85
+ description: "Text color hex without # (e.g. '333333'). Default: 333333",
86
+ },
87
+ text: {
88
+ type: "string",
89
+ description: "Custom text to display on the image. Default: WIDTHxHEIGHT",
90
+ },
91
+ format: {
92
+ type: "string",
93
+ description: "Image format: png, jpg, jpeg, gif, webp, svg. Default: png",
94
+ enum: ["png", "jpg", "jpeg", "gif", "webp", "svg"],
95
+ },
96
+ font: {
97
+ type: "string",
98
+ description: "Font name (e.g. 'roboto', 'open-sans', 'montserrat')",
99
+ },
100
+ fontSize: {
101
+ type: "number",
102
+ description: "Font size in pixels",
103
+ },
104
+ sizes: {
105
+ type: "array",
106
+ items: {
107
+ type: "object",
108
+ properties: {
109
+ width: { type: "number" },
110
+ height: { type: "number" },
111
+ },
112
+ required: ["width"],
113
+ },
114
+ description: "Generate multiple placeholders at once. If provided, width/height params are ignored.",
115
+ },
116
+ },
117
+ required: ["width"],
118
+ },
119
+ },
120
+ {
121
+ name: "responsive_images",
122
+ description: "Generate srcset and <picture> element HTML for responsive images. Given a base image URL, produces multiple size variants with proper HTML markup for responsive design.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ baseUrl: {
127
+ type: "string",
128
+ description: "Base image URL that accepts width/format query parameters",
129
+ },
130
+ alt: {
131
+ type: "string",
132
+ description: "Alt text for the image",
133
+ },
134
+ widths: {
135
+ type: "array",
136
+ items: { type: "number" },
137
+ description: "Array of widths to generate (default: [320, 640, 768, 1024, 1280, 1536, 1920])",
138
+ },
139
+ sizes: {
140
+ type: "string",
141
+ description: "CSS sizes attribute (default: '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw')",
142
+ },
143
+ className: {
144
+ type: "string",
145
+ description: "CSS class name to add to the element",
146
+ },
147
+ loading: {
148
+ type: "string",
149
+ description: "Loading strategy: 'lazy' or 'eager' (default: lazy)",
150
+ enum: ["lazy", "eager"],
151
+ },
152
+ formats: {
153
+ type: "array",
154
+ items: { type: "string" },
155
+ description: "Image formats for <picture> sources (default: ['webp', auto-detected original format])",
156
+ },
157
+ },
158
+ required: ["baseUrl"],
159
+ },
160
+ },
161
+ ],
162
+ }));
163
+ // Handle tool calls
164
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
165
+ const { name, arguments: args } = request.params;
166
+ try {
167
+ switch (name) {
168
+ case "image_metadata": {
169
+ const result = await (0, image_meta_js_1.getImageMetadata)(args?.url);
170
+ return {
171
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
172
+ };
173
+ }
174
+ case "find_favicons": {
175
+ const result = await (0, favicon_finder_js_1.findFavicons)(args?.url);
176
+ return {
177
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
178
+ };
179
+ }
180
+ case "extract_og_image": {
181
+ const result = await (0, og_image_js_1.extractOgImage)(args?.url);
182
+ return {
183
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
184
+ };
185
+ }
186
+ case "generate_placeholder": {
187
+ const sizes = args?.sizes;
188
+ if (sizes && sizes.length > 0) {
189
+ const results = (0, placeholder_generator_js_1.generatePlaceholderSet)(sizes, {
190
+ backgroundColor: args?.backgroundColor,
191
+ textColor: args?.textColor,
192
+ text: args?.text,
193
+ format: args?.format,
194
+ font: args?.font,
195
+ fontSize: args?.fontSize,
196
+ });
197
+ return {
198
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
199
+ };
200
+ }
201
+ const result = (0, placeholder_generator_js_1.generatePlaceholder)({
202
+ width: args?.width,
203
+ height: args?.height,
204
+ backgroundColor: args?.backgroundColor,
205
+ textColor: args?.textColor,
206
+ text: args?.text,
207
+ format: args?.format,
208
+ font: args?.font,
209
+ fontSize: args?.fontSize,
210
+ });
211
+ return {
212
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
213
+ };
214
+ }
215
+ case "responsive_images": {
216
+ const result = (0, responsive_images_js_1.generateResponsiveImages)({
217
+ baseUrl: args?.baseUrl,
218
+ alt: args?.alt,
219
+ widths: args?.widths,
220
+ sizes: args?.sizes,
221
+ className: args?.className,
222
+ loading: args?.loading,
223
+ formats: args?.formats,
224
+ });
225
+ return {
226
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
227
+ };
228
+ }
229
+ default:
230
+ throw new Error(`Unknown tool: ${name}`);
231
+ }
232
+ }
233
+ catch (error) {
234
+ const message = error instanceof Error ? error.message : String(error);
235
+ return {
236
+ content: [{ type: "text", text: `Error: ${message}` }],
237
+ isError: true,
238
+ };
239
+ }
240
+ });
241
+ // Start server
242
+ async function main() {
243
+ const transport = new stdio_js_1.StdioServerTransport();
244
+ await server.connect(transport);
245
+ console.error("MCP Image Tools server running on stdio");
246
+ }
247
+ main().catch(console.error);
@@ -0,0 +1,12 @@
1
+ export interface FaviconResult {
2
+ domain: string;
3
+ favicons: FaviconEntry[];
4
+ checkedLocations: string[];
5
+ }
6
+ export interface FaviconEntry {
7
+ url: string;
8
+ source: string;
9
+ type: string | null;
10
+ sizes: string | null;
11
+ }
12
+ export declare function findFavicons(websiteUrl: string): Promise<FaviconResult>;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findFavicons = findFavicons;
4
+ async function fetchText(url, timeoutMs = 10000) {
5
+ const controller = new AbortController();
6
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
7
+ try {
8
+ const response = await fetch(url, {
9
+ signal: controller.signal,
10
+ redirect: "follow",
11
+ headers: { "User-Agent": "MCP-Image-Tools/1.0" },
12
+ });
13
+ if (!response.ok)
14
+ return { text: "", ok: false };
15
+ const text = await response.text();
16
+ return { text, ok: true };
17
+ }
18
+ catch {
19
+ return { text: "", ok: false };
20
+ }
21
+ finally {
22
+ clearTimeout(timeout);
23
+ }
24
+ }
25
+ async function checkUrl(url, timeoutMs = 8000) {
26
+ const controller = new AbortController();
27
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
28
+ try {
29
+ const response = await fetch(url, {
30
+ method: "HEAD",
31
+ signal: controller.signal,
32
+ redirect: "follow",
33
+ });
34
+ return response.ok;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ finally {
40
+ clearTimeout(timeout);
41
+ }
42
+ }
43
+ function extractLinkTags(html, baseUrl) {
44
+ const entries = [];
45
+ // Match <link> tags with rel containing "icon" or "apple-touch-icon"
46
+ const linkRegex = /<link\s+[^>]*>/gi;
47
+ let match;
48
+ while ((match = linkRegex.exec(html)) !== null) {
49
+ const tag = match[0];
50
+ const relMatch = tag.match(/rel\s*=\s*["']([^"']+)["']/i);
51
+ if (!relMatch)
52
+ continue;
53
+ const rel = relMatch[1].toLowerCase();
54
+ if (!rel.includes("icon"))
55
+ continue;
56
+ const hrefMatch = tag.match(/href\s*=\s*["']([^"']+)["']/i);
57
+ if (!hrefMatch)
58
+ continue;
59
+ const href = hrefMatch[1];
60
+ const typeMatch = tag.match(/type\s*=\s*["']([^"']+)["']/i);
61
+ const sizesMatch = tag.match(/sizes\s*=\s*["']([^"']+)["']/i);
62
+ let resolvedUrl;
63
+ try {
64
+ resolvedUrl = new URL(href, baseUrl).href;
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ entries.push({
70
+ url: resolvedUrl,
71
+ source: `HTML link[rel="${rel}"]`,
72
+ type: typeMatch ? typeMatch[1] : null,
73
+ sizes: sizesMatch ? sizesMatch[1] : null,
74
+ });
75
+ }
76
+ return entries;
77
+ }
78
+ function extractManifestIcons(manifestJson, baseUrl) {
79
+ const entries = [];
80
+ try {
81
+ const manifest = JSON.parse(manifestJson);
82
+ const icons = manifest.icons;
83
+ if (!Array.isArray(icons))
84
+ return entries;
85
+ for (const icon of icons) {
86
+ if (!icon.src)
87
+ continue;
88
+ let resolvedUrl;
89
+ try {
90
+ resolvedUrl = new URL(icon.src, baseUrl).href;
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ entries.push({
96
+ url: resolvedUrl,
97
+ source: "manifest.json",
98
+ type: icon.type || null,
99
+ sizes: icon.sizes || null,
100
+ });
101
+ }
102
+ }
103
+ catch {
104
+ // Invalid JSON
105
+ }
106
+ return entries;
107
+ }
108
+ async function findFavicons(websiteUrl) {
109
+ const parsed = new URL(websiteUrl);
110
+ const origin = parsed.origin;
111
+ const domain = parsed.hostname;
112
+ const checkedLocations = [];
113
+ const favicons = [];
114
+ const seenUrls = new Set();
115
+ function addFavicon(entry) {
116
+ if (!seenUrls.has(entry.url)) {
117
+ seenUrls.add(entry.url);
118
+ favicons.push(entry);
119
+ }
120
+ }
121
+ // 1. Check /favicon.ico
122
+ const faviconIcoUrl = `${origin}/favicon.ico`;
123
+ checkedLocations.push(faviconIcoUrl);
124
+ const icoExists = await checkUrl(faviconIcoUrl);
125
+ if (icoExists) {
126
+ addFavicon({
127
+ url: faviconIcoUrl,
128
+ source: "default /favicon.ico",
129
+ type: "image/x-icon",
130
+ sizes: null,
131
+ });
132
+ }
133
+ // 2. Parse HTML for link tags
134
+ checkedLocations.push(websiteUrl);
135
+ const { text: html, ok: htmlOk } = await fetchText(websiteUrl);
136
+ if (htmlOk) {
137
+ const htmlFavicons = extractLinkTags(html, websiteUrl);
138
+ for (const f of htmlFavicons) {
139
+ addFavicon(f);
140
+ }
141
+ // 3. Check for manifest.json reference
142
+ const manifestMatch = html.match(/<link\s+[^>]*rel\s*=\s*["']manifest["'][^>]*href\s*=\s*["']([^"']+)["']/i)
143
+ || html.match(/<link\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*rel\s*=\s*["']manifest["']/i);
144
+ if (manifestMatch) {
145
+ let manifestUrl;
146
+ try {
147
+ manifestUrl = new URL(manifestMatch[1], websiteUrl).href;
148
+ }
149
+ catch {
150
+ manifestUrl = "";
151
+ }
152
+ if (manifestUrl) {
153
+ checkedLocations.push(manifestUrl);
154
+ const { text: manifestText, ok: manifestOk } = await fetchText(manifestUrl);
155
+ if (manifestOk) {
156
+ const manifestIcons = extractManifestIcons(manifestText, manifestUrl);
157
+ for (const f of manifestIcons) {
158
+ addFavicon(f);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ // 4. Check common manifest location
165
+ const defaultManifest = `${origin}/manifest.json`;
166
+ if (!checkedLocations.includes(defaultManifest)) {
167
+ checkedLocations.push(defaultManifest);
168
+ const { text: manifestText, ok: manifestOk } = await fetchText(defaultManifest);
169
+ if (manifestOk) {
170
+ const manifestIcons = extractManifestIcons(manifestText, defaultManifest);
171
+ for (const f of manifestIcons) {
172
+ addFavicon(f);
173
+ }
174
+ }
175
+ }
176
+ return { domain, favicons, checkedLocations };
177
+ }
@@ -0,0 +1,14 @@
1
+ export interface ImageMetadata {
2
+ url: string;
3
+ contentType: string | null;
4
+ fileSize: number | null;
5
+ fileSizeFormatted: string | null;
6
+ lastModified: string | null;
7
+ etag: string | null;
8
+ cacheControl: string | null;
9
+ acceptRanges: string | null;
10
+ server: string | null;
11
+ accessTime: string;
12
+ statusCode: number;
13
+ }
14
+ export declare function getImageMetadata(url: string): Promise<ImageMetadata>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getImageMetadata = getImageMetadata;
4
+ function formatBytes(bytes) {
5
+ if (bytes === 0)
6
+ return "0 B";
7
+ const units = ["B", "KB", "MB", "GB"];
8
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
9
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
10
+ }
11
+ async function getImageMetadata(url) {
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), 15000);
14
+ try {
15
+ // Use HEAD request first to avoid downloading the entire image
16
+ let response = await fetch(url, {
17
+ method: "HEAD",
18
+ signal: controller.signal,
19
+ redirect: "follow",
20
+ });
21
+ // Some servers reject HEAD, fall back to GET with range
22
+ if (response.status === 405 || response.status === 403) {
23
+ response = await fetch(url, {
24
+ method: "GET",
25
+ headers: { Range: "bytes=0-0" },
26
+ signal: controller.signal,
27
+ redirect: "follow",
28
+ });
29
+ }
30
+ const headers = response.headers;
31
+ const contentLength = headers.get("content-length");
32
+ const fileSize = contentLength ? parseInt(contentLength, 10) : null;
33
+ return {
34
+ url,
35
+ contentType: headers.get("content-type"),
36
+ fileSize,
37
+ fileSizeFormatted: fileSize !== null ? formatBytes(fileSize) : null,
38
+ lastModified: headers.get("last-modified"),
39
+ etag: headers.get("etag"),
40
+ cacheControl: headers.get("cache-control"),
41
+ acceptRanges: headers.get("accept-ranges"),
42
+ server: headers.get("server"),
43
+ accessTime: new Date().toISOString(),
44
+ statusCode: response.status,
45
+ };
46
+ }
47
+ finally {
48
+ clearTimeout(timeout);
49
+ }
50
+ }
@@ -0,0 +1,18 @@
1
+ export interface OgImageResult {
2
+ url: string;
3
+ ogImage: string | null;
4
+ ogImageAlt: string | null;
5
+ ogImageWidth: string | null;
6
+ ogImageHeight: string | null;
7
+ ogImageType: string | null;
8
+ twitterImage: string | null;
9
+ twitterCard: string | null;
10
+ appleTouchIcon: string | null;
11
+ allImages: ImageEntry[];
12
+ }
13
+ export interface ImageEntry {
14
+ url: string;
15
+ source: string;
16
+ alt: string | null;
17
+ }
18
+ export declare function extractOgImage(url: string): Promise<OgImageResult>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractOgImage = extractOgImage;
4
+ function extractMetaContent(html, property) {
5
+ // Match both property="..." and name="..." attributes
6
+ const patterns = [
7
+ new RegExp(`<meta\\s+[^>]*property\\s*=\\s*["']${escapeRegex(property)}["'][^>]*content\\s*=\\s*["']([^"']+)["']`, "i"),
8
+ new RegExp(`<meta\\s+[^>]*content\\s*=\\s*["']([^"']+)["'][^>]*property\\s*=\\s*["']${escapeRegex(property)}["']`, "i"),
9
+ new RegExp(`<meta\\s+[^>]*name\\s*=\\s*["']${escapeRegex(property)}["'][^>]*content\\s*=\\s*["']([^"']+)["']`, "i"),
10
+ new RegExp(`<meta\\s+[^>]*content\\s*=\\s*["']([^"']+)["'][^>]*name\\s*=\\s*["']${escapeRegex(property)}["']`, "i"),
11
+ ];
12
+ for (const pattern of patterns) {
13
+ const match = html.match(pattern);
14
+ if (match)
15
+ return match[1];
16
+ }
17
+ return null;
18
+ }
19
+ function escapeRegex(str) {
20
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21
+ }
22
+ function extractAppleTouchIcon(html, baseUrl) {
23
+ const match = html.match(/<link\s+[^>]*rel\s*=\s*["']apple-touch-icon["'][^>]*href\s*=\s*["']([^"']+)["']/i)
24
+ || html.match(/<link\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*rel\s*=\s*["']apple-touch-icon["']/i);
25
+ if (match) {
26
+ try {
27
+ return new URL(match[1], baseUrl).href;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ function resolveUrl(href, baseUrl) {
36
+ if (!href)
37
+ return null;
38
+ try {
39
+ return new URL(href, baseUrl).href;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function extractOgImage(url) {
46
+ const controller = new AbortController();
47
+ const timeout = setTimeout(() => controller.abort(), 15000);
48
+ try {
49
+ const response = await fetch(url, {
50
+ signal: controller.signal,
51
+ redirect: "follow",
52
+ headers: { "User-Agent": "MCP-Image-Tools/1.0" },
53
+ });
54
+ if (!response.ok) {
55
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
56
+ }
57
+ const html = await response.text();
58
+ const allImages = [];
59
+ // Extract OG image properties
60
+ const ogImage = resolveUrl(extractMetaContent(html, "og:image"), url);
61
+ const ogImageAlt = extractMetaContent(html, "og:image:alt");
62
+ const ogImageWidth = extractMetaContent(html, "og:image:width");
63
+ const ogImageHeight = extractMetaContent(html, "og:image:height");
64
+ const ogImageType = extractMetaContent(html, "og:image:type");
65
+ if (ogImage) {
66
+ allImages.push({ url: ogImage, source: "og:image", alt: ogImageAlt });
67
+ }
68
+ // Extract Twitter card image
69
+ const twitterImage = resolveUrl(extractMetaContent(html, "twitter:image"), url);
70
+ const twitterCard = extractMetaContent(html, "twitter:card");
71
+ if (twitterImage && twitterImage !== ogImage) {
72
+ allImages.push({ url: twitterImage, source: "twitter:image", alt: null });
73
+ }
74
+ // Extract Apple touch icon
75
+ const appleTouchIcon = extractAppleTouchIcon(html, url);
76
+ if (appleTouchIcon) {
77
+ allImages.push({ url: appleTouchIcon, source: "apple-touch-icon", alt: null });
78
+ }
79
+ return {
80
+ url,
81
+ ogImage,
82
+ ogImageAlt,
83
+ ogImageWidth,
84
+ ogImageHeight,
85
+ ogImageType,
86
+ twitterImage,
87
+ twitterCard,
88
+ appleTouchIcon,
89
+ allImages,
90
+ };
91
+ }
92
+ finally {
93
+ clearTimeout(timeout);
94
+ }
95
+ }