From cfe3e4d70736edebb30dac3c567564baca01bd36 Mon Sep 17 00:00:00 2001 From: Shivam Kaushik Date: Thu, 18 Jun 2026 18:40:13 +0530 Subject: [PATCH 1/5] docs(mcp-preflight): update description and clarify upload rules --- skills/imagekit-sdk-reference/SKILL.md | 272 +++++++++++++++++++++++++ skills/mcp-preflight/SKILL.md | 32 ++- 2 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 skills/imagekit-sdk-reference/SKILL.md diff --git a/skills/imagekit-sdk-reference/SKILL.md b/skills/imagekit-sdk-reference/SKILL.md new file mode 100644 index 0000000..446211c --- /dev/null +++ b/skills/imagekit-sdk-reference/SKILL.md @@ -0,0 +1,272 @@ +--- +name: imagekit-sdk-reference +description: "MANDATORY: Read this before writing any ImageKit SDK code or calling mcp_imagekit_api_* tools. Provides complete TypeScript method signatures, parameters, return types, error handling, and examples for the @imagekit/nodejs SDK." +--- + +# ImageKit TypeScript SDK Reference + +Read this skill before calling any `mcp_imagekit_api_*` tool or writing TypeScript code against the ImageKit SDK. It contains exact method signatures, parameter types, return shapes, and error handling patterns for `@imagekit/nodejs`. + +**Rules:** +1. Use exact parameter names — the SDK is strict about camelCase +2. `assets.list()` returns `(File | Folder)[]` — always narrow the type before accessing file-specific properties +3. Always wrap calls in try/catch for `ImageKit.APIError` +4. Use `skip`/`limit` for pagination (max 1000 per request) +5. **Never upload files via `mcp_imagekit_api_execute`** — use the `upload-files` skill instead +6. Upload in SDK code is only valid when the `file` param is a **URL string** — never pass local file paths, Buffers, or streams through MCP + +--- + +## Types + +### File +```typescript +{ + fileId: string; name: string; filePath: string; type: 'file' | 'file-version'; + url: string; thumbnail: string; isPrivateFile: boolean; isPublished: boolean; + // Media + fileType: string; // 'image' | 'non-image' + mime: string; size: number; width: number; height: number; hasAlpha: boolean; + // Video-only + duration: number; videoCodec: string; audioCodec: string; bitRate: number; + // Tags & metadata + tags: string[] | null; + AITags: Array<{ name: string; confidence: number; source: string }> | null; + customMetadata: Record; description: string; + customCoordinates: string | null; + embeddedMetadata: Record; + // Versioning + versionInfo: { id: string; name: string }; + createdAt: string; updatedAt: string; +} +``` + +### Folder +```typescript +{ + folderId: string; folderPath: string; name: string; type: 'folder'; + customMetadata: Record; + createdAt: string; updatedAt: string; +} +``` + +### CustomMetadataField +```typescript +{ + id: string; name: string; label: string; + schema: { type: 'Text' | 'Number' | 'Date' | 'Boolean' | 'SingleSelect' | 'MultiSelect'; /* ... */ }; +} +``` + +--- + +## File Operations + +### Upload a file (URL only via MCP) +```typescript +// ⚠️ Via mcp_imagekit_api_execute, ONLY URL-based uploads work. +// For local files, use the upload-files skill (CLI script) instead. +const file = await client.files.upload({ + file: 'https://example.com/img.jpg', // URL string ONLY in MCP context + fileName: 'img.jpg', + folder: '/uploads', + tags: ['tag1'], + customMetadata: { key: 'value' }, + // Key optional params: + // useUniqueFileName: true, // default true — appends random suffix + // isPrivateFile: false, + // overwriteFile: false, // replace existing file at same path + // overwriteTags: false, + // overwriteCustomMetadata: false, + // extensions: [{ name: 'google-auto-tagging', maxTags: 5 }], + // transformation: { pre: 'w-200' }, + // webhookUrl: 'https://...', +}); +// Returns: File object +``` + +### Get file details +```typescript +const file = await client.files.get(fileId); // Returns: File +``` + +### List / search assets +```typescript +const result = await client.assets.list({ + searchQuery: 'name = "img.jpg"', // Lucene-like syntax + path: '/uploads', + fileType: 'image', // 'image' | 'non-image' | 'all' + type: 'file', // 'file' | 'folder' | 'file-version' | 'all' + sort: 'ASC_NAME', + skip: 0, + limit: 100, +}); +// Returns: (File | Folder)[] — a flat array, NOT { files, folders } +``` + +**⚠️ CRITICAL: Type narrowing is required.** Even with `type: 'file'`, the TypeScript return type is `(File | Folder)[]`. You MUST narrow before accessing file-only properties like `filePath`, `fileType`, `size`, `url`: + +```typescript +// ✅ CORRECT — narrow the type first +const result = await client.assets.list({ type: 'file', limit: 10 }); +const files = result.filter((item): item is File => item.type === 'file'); +files.forEach(f => console.log(f.name, f.filePath, f.size, f.url)); // No TS error + +// ❌ WRONG — result.forEach(f => f.filePath) → TS error: 'filePath' not on Folder +``` + +**Shared properties** (safe on both File and Folder): `name`, `type`, `customMetadata`, `createdAt`, `updatedAt` + +**File-only properties** (require narrowing): `fileId`, `filePath`, `fileType`, `mime`, `size`, `width`, `height`, `url`, `thumbnail`, `tags`, `AITags`, `description`, `isPrivateFile`, `isPublished`, `customCoordinates`, `embeddedMetadata`, `versionInfo`, `duration`, `videoCodec`, `audioCodec`, `bitRate`, `hasAlpha` + +**Folder-only properties**: `folderId`, `folderPath` + +### Update / delete file +```typescript +await client.files.update(fileId, { tags: ['newTag'], customMetadata: { key: 'value' } }); // Returns: File +await client.files.delete(fileId); // Returns: void +``` + +### Copy / move / rename file +```typescript +await client.files.copy({ sourceFilePath: '/a/img.jpg', destinationPath: '/b/', includeFileVersions: false }); +await client.files.move({ sourceFilePath: '/a/img.jpg', destinationPath: '/b/' }); +await client.files.rename({ filePath: '/a/img.jpg', newFileName: 'new.jpg', purgeCache: false }); +``` + +--- + +## Bulk Operations + +```typescript +await client.files.bulk.delete({ fileIds: ['id1', 'id2'] }); // { successfullyDeletedFileIds } +await client.files.bulk.addTags({ fileIds: ['id1'], tags: ['promo'] }); // max 50 files +await client.files.bulk.removeTags({ fileIds: ['id1'], tags: ['old'] }); +await client.files.bulk.removeAITags({ fileIds: ['id1'], AITags: ['cat'] }); +``` + +--- + +## Folder Operations + +```typescript +await client.folders.create({ folderName: 'myfolder', parentFolderPath: '/' }); +await client.folders.delete({ folderPath: '/myfolder' }); + +// Copy / move / rename — async operations, return { jobId } +const { jobId } = await client.folders.copy({ sourceFolderPath: '/a', destinationPath: '/b/' }); +const { jobId } = await client.folders.move({ sourceFolderPath: '/a', destinationPath: '/b/' }); +const { jobId } = await client.folders.rename({ folderPath: '/a', newFolderName: 'renamed' }); + +// Check job status +const job = await client.folders.job.get(jobId); +// job.status: 'Pending' | 'Completed' +``` + +--- + +## File Versions + +```typescript +const versions = await client.files.versions.list(fileId); // Returns: File[] +const version = await client.files.versions.get(versionId, { fileId }); // Returns: File +await client.files.versions.restore(versionId, { fileId }); // Returns: File +await client.files.versions.delete(versionId, { fileId }); // Returns: void +``` + +--- + +## File Metadata + +```typescript +const metadata = await client.files.metadata.getFromURL({ url: 'https://ik.imagekit.io/x/img.jpg' }); +// Returns EXIF/IPTC metadata: { height, width, exif, iptc, xmp, ... } +``` + +--- + +## Cache Invalidation + +```typescript +const purge = await client.cache.invalidation.create({ url: 'https://ik.imagekit.io/x/img.jpg' }); +const status = await client.cache.invalidation.get(purge.requestId); +// status.status: 'Pending' | 'Completed' +``` + +--- + +## URL Building + +```typescript +const url = client.helper.buildSrc({ + urlEndpoint: 'https://ik.imagekit.io/your_id', + src: '/path/img.jpg', + transformation: [{ width: 400, height: 300, format: 'webp', quality: 80 }], + signed: true, + expiresIn: 3600, +}); +// Returns: string +``` + +--- + +## Custom Metadata Fields + +```typescript +// Define schema fields for your media library +const field = await client.customMetadataFields.create({ + name: 'brand', label: 'Brand Name', + schema: { type: 'Text', defaultValue: '', isValueRequired: false }, +}); +const fields = await client.customMetadataFields.list(); // Returns: CustomMetadataField[] +await client.customMetadataFields.update(field.id, { label: 'Updated Label' }); +await client.customMetadataFields.delete(field.id); +``` + +--- + +## Account Usage + +```typescript +const usage = await client.accounts.usage.get({ startDate: '2025-01-01', endDate: '2025-01-31' }); +// { bandwidthBytes, mediaLibraryStorageBytes, extensionUnitsCount, videoProcessingUnitsCount } +``` + +--- + +## Webhooks + +```typescript +// Verified parse (requires webhookSecret on client) +const event = client.webhooks.unwrap(rawBody, { headers: req.headers }); +// event.type: 'FILE.CREATE' | 'FILE.UPDATE' | 'FILE.DELETE' +// | 'UPLOAD.POST_TRANSFORM.SUCCESS' | 'UPLOAD.POST_TRANSFORM.ERROR' +// | 'VIDEO_TRANSFORMATION.ACCEPTED' | 'VIDEO_TRANSFORMATION.READY' | 'VIDEO_TRANSFORMATION.ERROR' + +// Unverified parse +const event = client.webhooks.unsafeUnwrap(rawBody); +``` + +--- + +## Pagination & Error Handling + +```typescript +// Offset-based pagination — skip / limit (max 1000). No cursor support. +for (let skip = 0; ; skip += 100) { + const page = await client.assets.list({ skip, limit: 100 }); + if (!page.length) break; + const files = page.filter((item): item is File => item.type === 'file'); +} + +// Error handling +import ImageKit from '@imagekit/nodejs'; +try { await client.files.get('bad-id'); } +catch (err) { + if (err instanceof ImageKit.APIError) { + err.status; // 400 | 401 | 403 | 404 | 409 | 422 | 429 | 5xx + err.message; err.error; err.headers; + } +} +// Auto-retries: connection errors, 408/409/429/5xx — up to 2× with exponential backoff +``` diff --git a/skills/mcp-preflight/SKILL.md b/skills/mcp-preflight/SKILL.md index 9695855..275bda8 100644 --- a/skills/mcp-preflight/SKILL.md +++ b/skills/mcp-preflight/SKILL.md @@ -1,6 +1,6 @@ --- name: mcp-preflight -description: "MANDATORY PRE-STEP: You MUST read this skill BEFORE calling ANY ImageKit MCP tool (mcp_imagekit_api_* or mcp_imagekit_devtools_*). This skill tells you which MCP server owns which capability, how to route requests, and critical rules like never using mcp_imagekit_api_upload_files. Covers: file management, folder ops, cache purge, metadata, search_docs, transformation_builder, upload routing." +description: "MANDATORY PRE-STEP: You MUST read this skill BEFORE calling ANY ImageKit MCP tool (mcp_imagekit_api_* or mcp_imagekit_devtools_*). This skill tells you which MCP server owns which capability, how to route requests, and critical rules like never uploading files via mcp_imagekit_api_execute. Covers: file management, folder ops, cache purge, metadata, search_docs, transformation_builder, upload routing." --- # MCP Preflight @@ -15,7 +15,14 @@ You have access to two ImageKit MCP servers. Each serves a different purpose. Ca Prefix: `mcp_imagekit_api_*` -Handles all CRUD operations on your ImageKit media library: +Two tools only: + +| Tool | Purpose | +|------|----------| +| `search_doc` | Local search that only searches TypeScript SDK code — use to find method signatures and types | +| `execute` | LLM writes TypeScript code and the server executes it. **All operations go through this tool.** | + +All CRUD operations are performed by writing TypeScript code that runs via `execute`: - List, search, get details of files - Delete, move, copy, rename files @@ -42,21 +49,26 @@ Two tools only — no auth required: | Need | Route To | |------|----------| -| List, delete, move, copy files | `mcp_imagekit_api_*` | -| Folder operations | `mcp_imagekit_api_*` | -| File metadata, tags, custom fields | `mcp_imagekit_api_*` | -| Bulk operations | `mcp_imagekit_api_*` | -| Cache purge | `mcp_imagekit_api_*` | -| URL endpoints, origins | `mcp_imagekit_api_*` | -| **Upload files** | **DO NOT use MCP** — use `skills/upload-files/resources/upload.py` | +| List, delete, move, copy files | `mcp_imagekit_api_execute` (write TS code) | +| Folder operations | `mcp_imagekit_api_execute` (write TS code) | +| File metadata, tags, custom fields | `mcp_imagekit_api_execute` (write TS code) | +| Bulk operations | `mcp_imagekit_api_execute` (write TS code) | +| Cache purge | `mcp_imagekit_api_execute` (write TS code) | +| URL endpoints, origins | `mcp_imagekit_api_execute` (write TS code) | +| Find SDK method signatures/types | `mcp_imagekit_api_search_doc` | +| **Upload files** | **DO NOT use `mcp_imagekit_api_execute`** for local files — use `skills/upload-files/resources/upload.py`. URL-based uploads are OK via execute. | | How to do something in ImageKit | `mcp_imagekit_devtools_search_docs` | | Build a transformation URL | `mcp_imagekit_devtools_transformation_builder` | | Find SDK usage or API parameters | `mcp_imagekit_devtools_search_docs` | | Verify feature exists or find limits | `mcp_imagekit_devtools_search_docs` | +## Before Calling imagekit_api + +**Use the `imagekit-sdk-reference` skill** before constructing any TypeScript code that calls `mcp_imagekit_api_*` tools. That skill provides complete SDK method signatures, parameters, return types, error handling patterns, and examples to ensure correct API calls and proper data handling. + ## Critical Rules -1. **NEVER use `mcp_imagekit_api_upload_files`** — it cannot handle local file bytes. Always use the upload CLI script instead. +1. **NEVER upload local files via `mcp_imagekit_api_execute`** — it cannot handle file bytes, streams, or Buffers. Use the upload CLI script for local files. URL-based uploads via execute are fine. 2. **ALWAYS call `search_docs` before writing any ImageKit SDK code** — do not rely on training data for method signatures or parameters. 3. **Do NOT read library source code to figure out usage** — `search_docs` returns official docs and working examples. Only read source code as a last resort when docs fail. 4. **Use `transformation_builder` instead of hand-crafting transformation URLs** — it knows correct parameter syntax and ordering. From bbbc7dd4f30409f2fde220fd6e9c6dc1122b1494 Mon Sep 17 00:00:00 2001 From: Shivam Kaushik Date: Thu, 18 Jun 2026 18:43:35 +0530 Subject: [PATCH 2/5] chore(marketplace): update version and description for imagekit-tools and imagekit-plugin chore(mcp): fix API URL in mcp configuration docs(README): add imagekit-sdk-reference skill to documentation chore(skills): include imagekit-sdk-reference in skills list --- .claude-plugin/marketplace.json | 6 +++--- .claude-plugin/plugin.json | 2 +- .mcp.json | 2 +- README.md | 1 + skills.sh.json | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b95da66..6dd372c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,6 +1,6 @@ { "name": "imagekit-tools", - "version": "1.0.0", + "version": "1.1.0", "owner": { "name": "ImageKit", "email": "developer@imagekit.io" @@ -13,8 +13,8 @@ "name": "imagekit-plugin", "displayName": "ImageKit Plugin", "source": "./", - "description": "Provides MCP server integrations for ImageKit public API for docs search, transformations, and resource management", - "version": "1.0.0", + "description": "Provides MCP server integrations for ImageKit public API for docs search, transformations, SDK reference, and resource management", + "version": "1.1.0", "author": { "name": "ImageKit", "email": "developer@imagekit.io" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0c62f16..dd53b3d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "imagekit-plugin", "displayName": "ImageKit Plugin", "description": "Provides MCP server integrations for ImageKit public API for docs search, transformations, and resource management", - "version": "1.0.0", + "version": "1.1.0", "author": { "name": "ImageKit", "email": "developer@imagekit.io" diff --git a/.mcp.json b/.mcp.json index 92e5535..dfd5cf7 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,7 +4,7 @@ "url": "https://devtools-mcp.imagekit.io/mcp" }, "imagekit_api": { - "url": "https://api-mcp.imagekit.in/mcp" + "url": "https://api-mcp.imagekit.io/mcp" } } } \ No newline at end of file diff --git a/README.md b/README.md index 8973c28..c276bd9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Reusable AI agent skills for [ImageKit.io](https://imagekit.io) — install them | Skill | Description | |-------|-------------| | **mcp-preflight** | Mandatory routing guide — tells the agent which MCP server to call for what, before every ImageKit tool invocation | +| **imagekit-sdk-reference** | TypeScript SDK reference — method signatures, types (File, Folder), error handling, and examples for `@imagekit/nodejs` | | **search-docs** | Search ImageKit documentation with optimized queries and source selection | | **transformation-builder** | Build ImageKit image/video transformations — AI editing, background removal, resize, crop, overlays, and more | | **upload-files** | Upload files to ImageKit media library with folder paths, tags, and metadata | diff --git a/skills.sh.json b/skills.sh.json index bf74946..514949f 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -6,7 +6,8 @@ "title": "General", "description": "Routing and documentation skills that apply before any ImageKit MCP call.", "skills": [ - "mcp-preflight" + "mcp-preflight", + "imagekit-sdk-reference" ] }, { From 780d2effd0f55c3638d71fc329f5233043bf0e6b Mon Sep 17 00:00:00 2001 From: Shivam Kaushik Date: Wed, 24 Jun 2026 19:28:29 +0530 Subject: [PATCH 3/5] Add search-assets and imagekit-integrations skills; document Deno File type gotchas - Add search-assets skill: searchQuery filter syntax, operators, and field reference for client.assets.list() - Add imagekit-integrations skill: index of ImageKit SDKs, plugins, and integrations - Register both new skills under the General grouping in skills.sh.json - imagekit-sdk-reference: add TypeScript Gotchas section explaining why .filter() with `item is File` fails in MCP/Deno (global File shadows SDK File); recommend for...of + if narrowing; document nullable props, .find() undefined, and webhook discrimination - mcp-preflight: clarify execute tool description and upload routing wording --- skills.sh.json | 4 +- skills/imagekit-integrations/SKILL.md | 142 +++++++++++++++++++++++++ skills/imagekit-sdk-reference/SKILL.md | 130 ++++++++++++++++++++-- skills/mcp-preflight/SKILL.md | 4 +- skills/search-assets/SKILL.md | 127 ++++++++++++++++++++++ 5 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 skills/imagekit-integrations/SKILL.md create mode 100644 skills/search-assets/SKILL.md diff --git a/skills.sh.json b/skills.sh.json index 514949f..3b2c07f 100644 --- a/skills.sh.json +++ b/skills.sh.json @@ -7,7 +7,9 @@ "description": "Routing and documentation skills that apply before any ImageKit MCP call.", "skills": [ "mcp-preflight", - "imagekit-sdk-reference" + "imagekit-sdk-reference", + "imagekit-integrations", + "search-assets" ] }, { diff --git a/skills/imagekit-integrations/SKILL.md b/skills/imagekit-integrations/SKILL.md new file mode 100644 index 0000000..7380315 --- /dev/null +++ b/skills/imagekit-integrations/SKILL.md @@ -0,0 +1,142 @@ +--- +name: imagekit-integrations +description: "Index of ImageKit SDKs, plugins, and integrations — front-end, back-end, mobile, CMS, external storage, video player, media library & upload widgets, AI/automation, and URL generation. Use to find the right integration for a technology and what it covers, then query search_docs for implementation details." +--- + +# ImageKit Integrations + +ImageKit ships SDKs (front-end, back-end, mobile), no-code plugins (CMS, e-commerce), external-storage connectors, a video player, embeddable widgets, AI/automation tooling, and direct URL-generation helpers. This skill is an index of all of them and what each covers. + +## When to use this skill + +- You're integrating ImageKit into an app and need to know **which SDK, plugin, player, or widget exists** for your stack. +- You want a quick sense of **what a given integration can do** before committing. +- You need to route a "how do I integrate ImageKit with X" question to the right place. + +## How to use this skill + +1. Skim the sections below — they're grouped by integration type (SDKs, plugins, storage, player, widgets, automation, URL generation). Find the one that matches your stack or use case and read its summary to confirm it fits. +2. For implementation details, **query [`mcp_imagekit_devtools_search_docs`](https://imagekit.io/docs/build-with-ai)** with the `imagekit_guides` and `imagekit_sdk` sources — e.g. *"generate signed URLs in Node.js"* or *"responsive srcset in React"*. This returns only the relevant snippets and is far cheaper and faster than loading a whole page. +3. Only fetch a full doc page (append `.md` to its URL for plain text) when you genuinely need the **entire** setup guide end-to-end. Prefer `search_docs` for everything else. + +## SDKs + +### Front-End + +For web apps that generate optimized URLs and upload from the browser. Covers: + +- Loading optimized images & videos with automatic format/quality +- Resize, crop, and chained transformations (multiple ops in one request) +- Text, image, video, and subtitle overlays +- Responsive images (`srcset` / `sizes`), lazy loading, and low-quality placeholders (LQIP) +- Client-side upload with tagging and metadata + +All front-end SDKs wrap the [JavaScript SDK](https://imagekit.io/docs/integration/javascript) — drop down to it directly when you need full control. + +| Tech | Guide | +|------|-------| +| [JavaScript](https://imagekit.io/docs/integration/javascript) | Vanilla / browser | +| [React](https://imagekit.io/docs/integration/react) | React components & hooks | +| [Next.js](https://imagekit.io/docs/integration/nextjs) | App & Pages router | +| [Vue.js](https://imagekit.io/docs/integration/vuejs) | Vue components | +| [Angular](https://imagekit.io/docs/integration/angular) | Angular components | +| [Astro](https://imagekit.io/docs/integration/astro) | Astro components | +| [React Native](https://imagekit.io/docs/integration/react-native) | Mobile web/native | + +### Back-End + +For servers that build URLs, secure delivery, and manage the media library. Covers: + +- URL construction with transformations +- **Signed URLs** for secure, time-limited delivery of private media +- Generating auth params (`token`, `expire`, `signature`) so the client can upload securely +- File upload, tagging, and metadata operations +- Developer-friendly wrappers over the media management APIs (list, search, move, delete, versions, etc.) + +| Tech | Guide | +|------|-------| +| [Node.js](https://github.com/imagekit-developer/imagekit-nodejs) | JS/TS runtime | +| [Python](https://imagekit.io/docs/integration/python) | Python SDK | +| [Java](https://imagekit.io/docs/integration/java) | Java SDK | +| [PHP](https://imagekit.io/docs/integration/php) | PHP SDK | +| [Ruby](https://imagekit.io/docs/integration/ruby) | Ruby (+ Rails, CarrierWave) | +| [.NET](https://github.com/imagekit-developer/imagekit-dotnet) | .NET SDK | +| [Go](https://github.com/imagekit-developer/imagekit-go) | Go SDK | + +### Mobile + +Native SDKs for iOS and Android. Covers: + +- URL construction for optimized images & videos, including adaptive bitrate (ABR) video streaming +- Media loading — the Android SDK integrates with Glide, Picasso, Coil, and Fresco +- Responsive image loading +- Upload with an upload policy (network type, retry behavior, etc.) +- Pre-upload preprocessing to control file size and dimensions + +The iOS SDK currently trails Android on features. + +| Tech | Guide | +|------|-------| +| [Android](https://github.com/imagekit-developer/imagekit-android) | Glide/Picasso/Coil/Fresco | +| [iOS](https://github.com/imagekit-developer/imagekit-ios) | Swift SDK | + +## No-code Plugins & Storage + +### CMS & E-commerce + +Native plugins that route media through ImageKit from your existing platform — automatic optimization, responsive delivery, and CDN caching without writing integration code. + +| Platform | Guide | +|----------|-------| +| [WordPress](https://imagekit.io/docs/integration/wordpress) | WP plugin | +| [Shopify](https://imagekit.io/docs/integration/shopify) | Shopify app | +| [Magento](https://imagekit.io/docs/integration/magento) | Magento extension | +| [Strapi](https://imagekit.io/docs/integration/strapi) | Strapi provider | +| [Contentful](https://imagekit.io/docs/integration/contentful) | Contentful app | +| [Sanity](https://imagekit.io/docs/integration/sanity) | Sanity plugin | +| [Netlify](https://imagekit.io/docs/integration/netlify) | Netlify integration | + +### External Storage (origins) + +Connect your existing storage as an ImageKit origin so it optimizes and delivers files already there — no re-uploading or migration needed. + +| Provider | Guide | +|----------|-------| +| [AWS S3](https://imagekit.io/docs/integration/aws-s3) | S3 bucket origin | +| [Google Cloud Storage](https://imagekit.io/docs/integration/google-cloud-storage) | GCS bucket origin | +| [Azure Blob Storage](https://imagekit.io/docs/integration/azure-blob-storage) | Azure container origin | +| [Firebase Storage](https://imagekit.io/docs/integration/firebase-storage) | Firebase origin | +| [DigitalOcean Spaces](https://imagekit.io/docs/integration/digitalocean-spaces) | Spaces origin | +| [Web server / proxy](https://imagekit.io/docs/integration/web-server) | Any HTTP origin | + +## Player & Widgets + +### Video Player SDK + +A drop-in [video player](https://imagekit.io/docs/video-player/overview) built on Video.js with ImageKit-powered features. Covers: + +- Adaptive bitrate streaming and seek-preview thumbnails +- AI-generated [subtitles & chapters](https://imagekit.io/docs/video-player/subtitles-and-chapters) (speech-to-text in 50+ languages, word-level highlighting) +- [Shoppable videos](https://imagekit.io/docs/video-player/shoppable-videos) — overlay product hotspots on playback +- [Playlists & recommendations](https://imagekit.io/docs/video-player/playlist-and-recommendations) +- Floating/sticky player and responsive layouts + +Ask `search_docs` things like *"embed ImageKit video player in React"*, *"auto-generate chapters in the video player"*, or *"add shoppable product hotspots to a video"*. + +### Embeddable Widgets + +- **[Embeddable Media Library widget](https://imagekit.io/docs/dam/embeddable-media-library-widget)** — drop ImageKit's media library picker into your own app so users browse and select assets. Query e.g. *"embed media library widget and handle the selected asset callback"*. +- **[Upload widget](https://github.com/imagekit-samples/uppy-uploader)** — [Uppy](https://uppy.io/)-based widget to upload from local files, URLs, Dropbox, Google Drive, Instagram, and more straight to your media library. + +## AI & Automation + +- **[Build with AI](https://imagekit.io/docs/build-with-ai)** — agent skills + two MCP servers (DevTools for docs/transformations, API for managing your media library) for Claude, Cursor, Windsurf, VS Code Copilot, and Codex. +- **[n8n](https://imagekit.io/docs/integration/n8n)** — workflow automation. + +## URL generation + +Beyond the SDKs, ImageKit exposes [URL endpoint functions](https://imagekit.io/docs/url-endpoint-functions) for building and signing delivery URLs directly. Useful when you only need URL construction without pulling in a full SDK. Query e.g. *"sign a delivery URL with an expiry"* or *"build a transformation URL with overlays"*. + +--- + +Query `mcp_imagekit_devtools_search_docs` for implementation details on any of the above integrations, players, widgets, and URL functions. For example: *"generate signed URLs in Node.js"*, *"responsive srcset in React"*, or *"embed the video player with auto subtitles"*. diff --git a/skills/imagekit-sdk-reference/SKILL.md b/skills/imagekit-sdk-reference/SKILL.md index 446211c..2be30e8 100644 --- a/skills/imagekit-sdk-reference/SKILL.md +++ b/skills/imagekit-sdk-reference/SKILL.md @@ -9,11 +9,110 @@ Read this skill before calling any `mcp_imagekit_api_*` tool or writing TypeScri **Rules:** 1. Use exact parameter names — the SDK is strict about camelCase -2. `assets.list()` returns `(File | Folder)[]` — always narrow the type before accessing file-specific properties +2. `assets.list()` returns `(File | Folder)[]` — use `for...of` + `if (item.type === 'file')` to narrow (preferred in MCP/Deno context). Do NOT use `.filter()` — the type predicate `item is File` collides with Deno's global `File` type (see Gotchas). 3. Always wrap calls in try/catch for `ImageKit.APIError` 4. Use `skip`/`limit` for pagination (max 1000 per request) 5. **Never upload files via `mcp_imagekit_api_execute`** — use the `upload-files` skill instead 6. Upload in SDK code is only valid when the `file` param is a **URL string** — never pass local file paths, Buffers, or streams through MCP +7. Nullable properties (`tags`, `AITags`, `customCoordinates`) require optional chaining (`?.`) or null checks +8. `.find()` returns `T | undefined` — always check for `undefined` before accessing properties + +--- + +## TypeScript Gotchas + +> **Key insight:** In the MCP execution context (Deno runtime), **always use `for...of` + `if` blocks** for type narrowing. The type predicate approach (`item is File`) fails because Deno's global `File` (the Blob-based Web API) shadows the ImageKit SDK's `File` type. + +| Pattern | Problem | Fix | +|---------|---------|-----| +| `.filter(i => i.type === 'file')` | Does NOT narrow the union type | Use `for...of` + `if` | +| `.filter((i): i is File => ...)` | Global `File` shadows SDK `File` in Deno | Use `for...of` + `if` | +| `.find(i => ...)` | Returns `T \| undefined` | Check for `undefined` + narrow type | +| `.filter().map()` | `.map()` inherits un-narrowed type | Use `for...of` + `if` + push | +| `file.tags` | `string[] \| null` | Use `?.` or null check | +| `file.AITags` | `Array \| null` | Use `?.` or null check | +| `event.file` | Webhook event is a discriminated union | Narrow by `event.type` first | +| `for...of` + `if` block | — | **Always works — use this** | + +### Why `.filter()` fails in MCP context (two separate issues) + +**Issue 1: `.filter()` without a type predicate does not narrow.** +TypeScript's `.filter()` returns the same array type unless the callback has a type predicate. A boolean callback does NOT narrow the output type. + +```typescript +// ❌ DOES NOT COMPILE — no narrowing: +const files = result.filter((item) => item.type === 'file'); +files[0].fileId; // TS ERROR: 'fileId' does not exist on type 'File | Folder' +``` + +**Issue 2: Type predicate `item is File` collides with global `File` in Deno.** +In the MCP execution environment (Deno), the global `File` type (Web API Blob-based) shadows the SDK's `File` type. TypeScript resolves `File` in the predicate to the global, causing: + +```typescript +// ❌ DOES NOT COMPILE in Deno/MCP — global File ≠ SDK File: +const files = result.filter((item): item is File => item.type === 'file'); +// Error: Type 'File' is not assignable to type 'import("@imagekit/nodejs/...").File' +// Types of property 'type' are incompatible. +// Type 'string' is not assignable to type '"file" | "file-version"' +``` + +**✅ RECOMMENDED: Always use `for...of` + `if` in MCP code:** +```typescript +// ✅ ALWAYS WORKS — control flow narrowing, no type name needed: +const result = await client.assets.list({ type: 'file', limit: 10 }); +const files = []; +for (const item of result) { + if (item.type === 'file') { + files.push({ + name: item.name, + fileId: item.fileId, + filePath: item.filePath, + size: item.size, + url: item.url + }); + } +} +return files; +``` + +### `.find()` returns `T | undefined` + +```typescript +const item = assets.find((i) => i.name === 'hero.jpg'); +// Type: (File | Folder) | undefined + +item.fileId; // ❌ Two errors: possibly undefined AND possibly Folder + +// ✅ Fix: +if (item && item.type === 'file') { + item.fileId; // works +} +``` + +### Nullable properties on File + +```typescript +const file = await client.files.get(fileId); + +file.tags.length; // ❌ tags is string[] | null +file.tags?.length; // ✅ optional chaining + +file.AITags.map(...); // ❌ AITags is Array | null +file.AITags?.map(...); // ✅ +``` + +### Webhook event discrimination + +```typescript +const event = client.webhooks.unwrap(rawBody, { headers }); + +event.file.url; // ❌ not all event types have .file + +// ✅ Narrow by event type +if (event.type === 'FILE.CREATE') { + event.file.url; // safe +} +``` --- @@ -104,15 +203,28 @@ const result = await client.assets.list({ // Returns: (File | Folder)[] — a flat array, NOT { files, folders } ``` -**⚠️ CRITICAL: Type narrowing is required.** Even with `type: 'file'`, the TypeScript return type is `(File | Folder)[]`. You MUST narrow before accessing file-only properties like `filePath`, `fileType`, `size`, `url`: +**⚠️ CRITICAL: Type narrowing is required.** Even with `type: 'file'`, the TypeScript return type is `(File | Folder)[]`. You MUST narrow before accessing file-only properties like `filePath`, `fileType`, `size`, `url`. + +**In MCP/Deno context, always use `for...of` + `if`** — do NOT use `.filter()` with `item is File` (the global `File` type shadows the SDK's): ```typescript -// ✅ CORRECT — narrow the type first -const result = await client.assets.list({ type: 'file', limit: 10 }); +// ❌ DOES NOT COMPILE — .filter() without predicate does not narrow: +const files = result.filter((item) => item.type === 'file'); +files[0].fileId; // TS ERROR: 'fileId' does not exist on type 'File | Folder' + +// ❌ DOES NOT COMPILE in Deno — global File ≠ SDK File: const files = result.filter((item): item is File => item.type === 'file'); -files.forEach(f => console.log(f.name, f.filePath, f.size, f.url)); // No TS error +// Error: Type 'File' is not assignable to type 'import("@imagekit/nodejs/...").File' -// ❌ WRONG — result.forEach(f => f.filePath) → TS error: 'filePath' not on Folder +// ✅ CORRECT — for...of + if (always works in MCP/Deno): +const result = await client.assets.list({ type: 'file', limit: 10 }); +const files = []; +for (const item of result) { + if (item.type === 'file') { + files.push({ name: item.name, fileId: item.fileId, filePath: item.filePath, size: item.size, url: item.url }); + } +} +return files; ``` **Shared properties** (safe on both File and Folder): `name`, `type`, `customMetadata`, `createdAt`, `updatedAt` @@ -256,7 +368,11 @@ const event = client.webhooks.unsafeUnwrap(rawBody); for (let skip = 0; ; skip += 100) { const page = await client.assets.list({ skip, limit: 100 }); if (!page.length) break; - const files = page.filter((item): item is File => item.type === 'file'); + for (const item of page) { + if (item.type === 'file') { + // item is narrowed to File here + } + } } // Error handling diff --git a/skills/mcp-preflight/SKILL.md b/skills/mcp-preflight/SKILL.md index 275bda8..3341bd8 100644 --- a/skills/mcp-preflight/SKILL.md +++ b/skills/mcp-preflight/SKILL.md @@ -20,7 +20,7 @@ Two tools only: | Tool | Purpose | |------|----------| | `search_doc` | Local search that only searches TypeScript SDK code — use to find method signatures and types | -| `execute` | LLM writes TypeScript code and the server executes it. **All operations go through this tool.** | +| `execute` | Executes TypeScript code against the ImageKit SDK to perform CRUD operations | All CRUD operations are performed by writing TypeScript code that runs via `execute`: @@ -56,7 +56,7 @@ Two tools only — no auth required: | Cache purge | `mcp_imagekit_api_execute` (write TS code) | | URL endpoints, origins | `mcp_imagekit_api_execute` (write TS code) | | Find SDK method signatures/types | `mcp_imagekit_api_search_doc` | -| **Upload files** | **DO NOT use `mcp_imagekit_api_execute`** for local files — use `skills/upload-files/resources/upload.py`. URL-based uploads are OK via execute. | +| **Upload files** | **DO NOT use `mcp_imagekit_api_execute`** for local files — use `skills/upload-files/resources/upload.py`. URL-based uploads are OK via `mcp_imagekit_api_execute`. | | How to do something in ImageKit | `mcp_imagekit_devtools_search_docs` | | Build a transformation URL | `mcp_imagekit_devtools_transformation_builder` | | Find SDK usage or API parameters | `mcp_imagekit_devtools_search_docs` | diff --git a/skills/search-assets/SKILL.md b/skills/search-assets/SKILL.md new file mode 100644 index 0000000..62d63bc --- /dev/null +++ b/skills/search-assets/SKILL.md @@ -0,0 +1,127 @@ +--- +name: search-assets +description: "Read when constructing an ImageKit searchQuery for client.assets.list() — advanced search filter syntax, operators, field reference, and examples. Use when searching/filtering files or folders by name, tags, date, size, format, path, or custom/embedded metadata." +--- + +# ImageKit Search Queries (`searchQuery`) + +`client.assets.list({ searchQuery })` takes a Lucene-like filter string. A good query is the difference between one precise API call and paging through everything. This skill is the cheatsheet for building that string. + +## Syntax + +- **Operators:** `=` `:` `<` `<=` `>` `>=` `IN` `NOT IN` `NOT =` `HAS` `EXISTS` `NOT EXISTS` +- **Combine:** `AND`, `OR`. **Group:** `( ... )` +- **Quoting:** string values in `"double quotes"`; numbers and booleans (`true`/`false`) unquoted; `customMetadata`/`embeddedMetadata` field names must be quoted, e.g. `"customMetadata.brand"`. +- `:` = begins-with / within-subfolders. `=` = exact. `HAS` = case-insensitive full-text (tokenized). + +## Field reference + +| Field | Operators | Value notes | +|-------|-----------|-------------| +| `name` | `=` `:` `IN` `NOT =` `NOT IN` `HAS` | String. `=` exact (case-sensitive), `:` begins-with, `HAS` case-insensitive full-text | +| `tags` | `IN` `NOT IN` `HAS` `EXISTS` `NOT EXISTS` | Array. Matches both `tags` and `AITags` | +| `type` | `=` `IN` `NOT =` `NOT IN` | `"file"` \| `"file-version"` \| `"folder"` | +| `id` | `=` `IN` `NOT =` `NOT IN` | fileId or folderId string | +| `createdAt` / `updatedAt` | `=` `<` `<=` `>` `>=` `IN` `NOT =` `NOT IN` | ISO 8601 (`"2020-01-01"`, `"2020-01-01T12:12:12"`) or relative (`"1h"` `"2d"` `"3w"` `"4m"` `"1y"`) | +| `width` / `height` | `=` `<` `<=` `>` `>=` `IN` `NOT =` `NOT IN` | Numeric px. Images only | +| `size` | `=` `<` `<=` `>` `>=` `IN` `NOT =` `NOT IN` | Bytes (`1024`) or string (`"1mb"`, `"10kb"`) | +| `format` | `=` `IN` | `"jpg"` `"png"` `"webp"` `"gif"` `"svg"` `"avif"` `"pdf"` `"mp4"` … | +| `private` / `published` / `transparency` | `=` | Boolean, unquoted: `true` / `false` | +| `createdBy` | `=` `IN` `NOT =` `NOT IN` | Uploader email string | +| `path` | `=` `:` `IN` `NOT =` `NOT IN` | `=` exact folder only; `:` folder + subfolders | +| `"customMetadata."` | type-dependent (`=` `:` `IN` `HAS` `<` `>` `EXISTS` …) | Quote the field name. Operators follow the field's schema type | +| `"embeddedMetadata."` | type-dependent | Quote the field name. `Keywords` uses `IN`/`EXISTS`; `DateTimeOriginal` uses date ops; `LocationTaken` supports geo (`"40,100 5km"`) | + +## Query examples + +```text +name = "red-dress.jpg" exact name (case-sensitive) +name : "red-dress" name begins with red-dress +name HAS "red dress" full-text: contains red AND dress (any case) +id = "64fb1c2d8a7b4c1234567890" single file/folder by id + +createdAt > "7d" uploaded in last 7 days +createdAt < 2020-01-01 uploaded before Jan 1 2020 (00:00 UTC) +updatedAt >= "2024-06-01T00:00:00" modified on/after a timestamp +createdAt > "7d" AND size > "2mb" recent AND large + +size <= "1mb" 1MB or smaller +width > 500 AND height > 500 at least 500x500 (images) +format = "png" PNG files +format IN ["jpg", "webp"] JPG or WebP + +tags IN ["sale", "summer"] has sale OR summer (tags or AITags) +tags NOT IN ["draft"] excludes draft +tags HAS "sale" full-text token match (sales, summer-sale…) +tags EXISTS has at least one tag +tags NOT EXISTS untagged files + +type = "file" files only +private = true private files +published = false drafts / unpublished +transparency = true images with an alpha layer + +path = "/sales-banner/" exactly this folder, no subfolders +path : "/sales-banner/" this folder AND its subfolders +format = "png" AND path : "/sales-banner/" PNGs under a folder tree + +"customMetadata.category" IN ["clothing", "accessories"] +"customMetadata.rating" > 4.3 +"customMetadata.active" = true +"customMetadata.description" HAS "red" +"embeddedMetadata.DateTimeOriginal" > "1y" +"embeddedMetadata.LocationTaken" = "40,100 5km" + +(size < "1mb" AND width > 500) OR (tags IN ["summer-sale", "banner"]) +``` + +## TypeScript usage + +```typescript +// Recent + large files. assets.list returns (File | Folder)[] — narrow before +// reading file-only props (see imagekit-sdk-reference for why for...of + if). +const result = await client.assets.list({ + searchQuery: 'createdAt >= "7d" AND size > "2mb"', + limit: 100, +}); +const files = []; +for (const item of result) { + if (item.type === 'file') { + files.push({ name: item.name, fileId: item.fileId, size: item.size }); + } +} +``` + +```typescript +// Full-text search — always pair HAS with DESC_RELEVANCE sorting. +const result = await client.assets.list({ + searchQuery: 'name HAS "red dress"', + sort: 'DESC_RELEVANCE', + limit: 20, +}); +``` + +```typescript +// Paginate a search with skip/limit (max 1000 per page). +for (let skip = 0; ; skip += 100) { + const page = await client.assets.list({ + searchQuery: 'tags IN ["sale", "summer"]', + skip, + limit: 100, + }); + if (!page.length) break; + for (const item of page) { + if (item.type === 'file') { + // item narrowed to File here + } + } +} +``` + +## Gotchas + +- `name` `=`/`:` are **case-sensitive**; use `HAS` for case-insensitive matching. +- `tags` queries search **both** `tags` and `AITags`. +- `HAS` tokenizes on spaces/punctuation; the last token matches as a prefix (`"red"` matches `redwoods`). Pair with `sort: 'DESC_RELEVANCE'`. +- Booleans (`private`, `published`, `transparency`) are unquoted; everything string-valued is quoted. +- `width`/`height`/`transparency` apply to images only. From bf37156274346e135ff27dc0fa82f8d3e3230ece Mon Sep 17 00:00:00 2001 From: Shivam Kaushik Date: Wed, 24 Jun 2026 19:29:04 +0530 Subject: [PATCH 4/5] chore(agents): remove AGENTS.md file and its content --- AGENTS.md | 54 ------------------------------------------------------ 1 file changed, 54 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 65a1a43..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,54 +0,0 @@ -# AGENTS.md - -## What Is This Repo? - -This is a [skills.sh](https://skills.sh) skills repository for ImageKit. It contains reusable AI agent skills that teach coding agents how to use ImageKit's MCP servers, APIs, and tools correctly. - -Skills are installed by users via: -```bash -npx skills add shivamik/skills -``` - -## `skills.sh.json` - -The `skills.sh.json` file at the repo root controls how skills are displayed and categorized on the [skills.sh page](https://skills.sh/shivamik/skills). It groups skills into labeled sections. - -### Structure - -```json -{ - "$schema": "https://skills.sh/schemas/skills.sh.schema.json", - "notGrouped": "bottom", - "groupings": [ - { - "title": "Category Name", - "description": "Short description of this category.", - "skills": ["skill-folder-name", "another-skill"] - } - ] -} -``` - -- `title` — Category heading shown on the page -- `description` — One-line summary of what skills in this group do -- `skills` — Array of skill folder names (must match folder names under `skills/`) -- `notGrouped` — Where ungrouped skills appear: `"top"` or `"bottom"` - -### Categories - -Use these categories when grouping skills: - -| Category | Purpose | -|----------|---------| -| **General** | Skills that apply broadly — routing, preflight checks, documentation lookup | -| **MCP Tools** | Skills tied to specific MCP tool usage — transformation builder, upload, AI tasks | - -Any skill not listed in a grouping appears in the "ungrouped" section (position controlled by `notGrouped`). - -### Updating `skills.sh.json` - -When adding or removing skills: - -1. Add the skill's folder name to the appropriate `skills` array in `skills.sh.json` -2. If no existing category fits, create a new grouping -3. **Always ask the user to review the generated categories before committing** — auto-generated groupings may not match their intended organization \ No newline at end of file From 4fc8f2c05b2cf6d80800a2a68024569a345e2508 Mon Sep 17 00:00:00 2001 From: Shivam Kaushik Date: Wed, 24 Jun 2026 19:46:04 +0530 Subject: [PATCH 5/5] docs(search-assets): update description for clarity and emphasis on mandatory reading --- skills/mcp-preflight/SKILL.md | 13 +++++++++++++ skills/search-assets/SKILL.md | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/skills/mcp-preflight/SKILL.md b/skills/mcp-preflight/SKILL.md index 3341bd8..dab0285 100644 --- a/skills/mcp-preflight/SKILL.md +++ b/skills/mcp-preflight/SKILL.md @@ -61,6 +61,18 @@ Two tools only — no auth required: | Build a transformation URL | `mcp_imagekit_devtools_transformation_builder` | | Find SDK usage or API parameters | `mcp_imagekit_devtools_search_docs` | | Verify feature exists or find limits | `mcp_imagekit_devtools_search_docs` | +| Search / filter / list assets | `mcp_imagekit_api_execute` (write TS code) — read `search-assets` skill first | +| Integrate ImageKit into a framework/SDK/CMS | read `imagekit-integrations` skill first | + +## Searching / Filtering Assets + +When listing assets with `client.assets.list`, **always pass a `searchQuery`** to filter on the server instead of fetching everything and filtering in code. A precise `searchQuery` trims the output to exactly what you need, saving tokens and API calls. + +**Read the `search-assets` skill** before constructing any `searchQuery` — it is the cheatsheet for the Lucene-like filter syntax, operators, and field reference. + +## Integration Use Cases + +When the task is to integrate ImageKit into a specific technology (front-end, back-end, mobile, CMS, external storage, upload widgets, URL generation, etc.), **read the `imagekit-integrations` skill** to find the right SDK/plugin and what it covers before writing code. ## Before Calling imagekit_api @@ -72,3 +84,4 @@ Two tools only — no auth required: 2. **ALWAYS call `search_docs` before writing any ImageKit SDK code** — do not rely on training data for method signatures or parameters. 3. **Do NOT read library source code to figure out usage** — `search_docs` returns official docs and working examples. Only read source code as a last resort when docs fail. 4. **Use `transformation_builder` instead of hand-crafting transformation URLs** — it knows correct parameter syntax and ordering. +5. **ALWAYS pass a `searchQuery` to `client.assets.list`** to filter assets server-side — read the `search-assets` skill first. Prefer search queries over fetching-and-filtering; they trim the output. diff --git a/skills/search-assets/SKILL.md b/skills/search-assets/SKILL.md index 62d63bc..80412d1 100644 --- a/skills/search-assets/SKILL.md +++ b/skills/search-assets/SKILL.md @@ -1,6 +1,6 @@ --- name: search-assets -description: "Read when constructing an ImageKit searchQuery for client.assets.list() — advanced search filter syntax, operators, field reference, and examples. Use when searching/filtering files or folders by name, tags, date, size, format, path, or custom/embedded metadata." +description: "MANDATORY: Read this whenever calling client.assets.list() with a searchQuery, or when searching/filtering/listing ImageKit files or folders. Covers the Lucene-like searchQuery filter syntax, operators, field reference, and examples for filtering by name, tags, date, size, format, path, or custom/embedded metadata." --- # ImageKit Search Queries (`searchQuery`)