commit
1f32abf1cf
6 changed files with 353 additions and 0 deletions
@ -0,0 +1,21 @@
|
||||
MIT License |
||||
|
||||
Copyright (c) 2026 |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
||||
@ -0,0 +1,109 @@
|
||||
# local-image-mcp |
||||
|
||||
Minimal Model Context Protocol server for local image inspection. |
||||
|
||||
`local-image-mcp` exposes two tools for debugging workflows: |
||||
|
||||
- `list_local_images`: scans a local directory and returns supported image files |
||||
- `load_local_image`: loads a local image file and returns it as MCP `image` content |
||||
|
||||
Loaded images are also exposed as MCP resources through `debug-image://...` URIs, which lets compatible clients reopen the same image resource later in the session. |
||||
|
||||
## Supported formats |
||||
|
||||
- PNG |
||||
- JPEG / JPG |
||||
- WEBP |
||||
- GIF |
||||
- BMP |
||||
- SVG |
||||
|
||||
## Installation |
||||
|
||||
### From source |
||||
|
||||
```bash |
||||
npm install |
||||
``` |
||||
|
||||
### Run manually |
||||
|
||||
```bash |
||||
node src/index.mjs |
||||
``` |
||||
|
||||
## MCP configuration example |
||||
|
||||
Add the server to your MCP client configuration: |
||||
|
||||
```json |
||||
{ |
||||
"servers": { |
||||
"local-images": { |
||||
"type": "stdio", |
||||
"command": "node", |
||||
"args": [ |
||||
"/absolute/path/to/local-image-mcp/src/index.mjs" |
||||
] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## Tools |
||||
|
||||
### `list_local_images` |
||||
|
||||
Lists supported images in a directory. |
||||
|
||||
Input: |
||||
|
||||
```json |
||||
{ |
||||
"dirPath": "/absolute/path/to/images" |
||||
} |
||||
``` |
||||
|
||||
### `load_local_image` |
||||
|
||||
Loads an image file and returns it as MCP image content. |
||||
|
||||
Input: |
||||
|
||||
```json |
||||
{ |
||||
"filePath": "/absolute/path/to/image.png" |
||||
} |
||||
``` |
||||
|
||||
Response content includes: |
||||
|
||||
- a text confirmation |
||||
- an `image` item with base64 data and MIME type |
||||
|
||||
## Resources |
||||
|
||||
Every successfully loaded image is cached for the current server session and published as a resource: |
||||
|
||||
- URI format: `debug-image://<encoded-absolute-path>` |
||||
|
||||
This is useful when an MCP client supports resource browsing or reopening previously loaded assets. |
||||
|
||||
## Scripts |
||||
|
||||
- `npm run check` — syntax check for the server entrypoint |
||||
|
||||
## Publishing notes |
||||
|
||||
Before publishing: |
||||
|
||||
1. update `version` in [package.json](package.json) |
||||
2. set the `author` field if needed |
||||
3. optionally add repository metadata and homepage fields |
||||
4. publish with your preferred npm workflow |
||||
|
||||
## Use cases |
||||
|
||||
- inspect screenshots produced by test runs |
||||
- review rendered UI snapshots from local tools |
||||
- attach visual artifacts to debugging sessions in MCP-compatible clients |
||||
|
After Width: | Height: | Size: 4.6 MiB |
@ -0,0 +1,25 @@
|
||||
{ |
||||
"name": "local-image-mcp", |
||||
"version": "0.1.0", |
||||
"description": "MCP server that lists and loads local debug images as image content and resources.", |
||||
"type": "module", |
||||
"main": "src/index.mjs", |
||||
"bin": { |
||||
"local-image-mcp": "src/index.mjs" |
||||
}, |
||||
"scripts": { |
||||
"check": "node --check src/index.mjs" |
||||
}, |
||||
"keywords": [ |
||||
"mcp", |
||||
"model-context-protocol", |
||||
"images", |
||||
"debugging", |
||||
"copilot" |
||||
], |
||||
"author": "", |
||||
"license": "MIT", |
||||
"dependencies": { |
||||
"@modelcontextprotocol/sdk": "^1.17.4" |
||||
} |
||||
} |
||||
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
import { promises as fs } from "node:fs"; |
||||
import path from "node:path"; |
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; |
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; |
||||
import { |
||||
CallToolRequestSchema, |
||||
ListResourcesRequestSchema, |
||||
ListToolsRequestSchema, |
||||
ReadResourceRequestSchema, |
||||
} from "@modelcontextprotocol/sdk/types.js"; |
||||
|
||||
const imageCache = new Map(); |
||||
|
||||
const TOOLS = [ |
||||
{ |
||||
name: "list_local_images", |
||||
description: "List image files inside a local directory for debugging.", |
||||
inputSchema: { |
||||
type: "object", |
||||
properties: { |
||||
dirPath: { |
||||
type: "string", |
||||
description: "Absolute path of the directory to scan", |
||||
}, |
||||
}, |
||||
required: ["dirPath"], |
||||
}, |
||||
}, |
||||
{ |
||||
name: "load_local_image", |
||||
description: "Load a local image file and return it as MCP image content so the agent can inspect it.", |
||||
inputSchema: { |
||||
type: "object", |
||||
properties: { |
||||
filePath: { |
||||
type: "string", |
||||
description: "Absolute path of the image file to load", |
||||
}, |
||||
}, |
||||
required: ["filePath"], |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
function inferMimeType(filePath) { |
||||
const ext = path.extname(filePath).toLowerCase(); |
||||
switch (ext) { |
||||
case ".png": |
||||
return "image/png"; |
||||
case ".jpg": |
||||
case ".jpeg": |
||||
return "image/jpeg"; |
||||
case ".webp": |
||||
return "image/webp"; |
||||
case ".gif": |
||||
return "image/gif"; |
||||
case ".bmp": |
||||
return "image/bmp"; |
||||
case ".svg": |
||||
return "image/svg+xml"; |
||||
default: |
||||
throw new Error(`Unsupported image extension: ${ext || "<none>"}`); |
||||
} |
||||
} |
||||
|
||||
async function listLocalImages(dirPath) { |
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true }); |
||||
const files = entries |
||||
.filter((entry) => entry.isFile()) |
||||
.map((entry) => path.join(dirPath, entry.name)) |
||||
.filter((filePath) => { |
||||
try { |
||||
inferMimeType(filePath); |
||||
return true; |
||||
} catch { |
||||
return false; |
||||
} |
||||
}) |
||||
.sort(); |
||||
|
||||
return { |
||||
content: [ |
||||
{ |
||||
type: "text", |
||||
text: files.length |
||||
? files.join("\n") |
||||
: `No supported images found in ${dirPath}`, |
||||
}, |
||||
], |
||||
isError: false, |
||||
}; |
||||
} |
||||
|
||||
async function loadLocalImage(filePath) { |
||||
const resolvedPath = path.resolve(filePath); |
||||
const mimeType = inferMimeType(resolvedPath); |
||||
const bytes = await fs.readFile(resolvedPath); |
||||
const base64 = bytes.toString("base64"); |
||||
|
||||
imageCache.set(resolvedPath, { mimeType, base64 }); |
||||
server.notification({ method: "notifications/resources/list_changed" }); |
||||
|
||||
return { |
||||
content: [ |
||||
{ |
||||
type: "text", |
||||
text: `Loaded image ${resolvedPath}`, |
||||
}, |
||||
{ |
||||
type: "image", |
||||
data: base64, |
||||
mimeType, |
||||
}, |
||||
], |
||||
isError: false, |
||||
}; |
||||
} |
||||
|
||||
const server = new Server( |
||||
{ |
||||
name: "local-image-debug-server", |
||||
version: "0.1.0", |
||||
}, |
||||
{ |
||||
capabilities: { |
||||
tools: {}, |
||||
resources: {}, |
||||
}, |
||||
} |
||||
); |
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); |
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => { |
||||
const name = request.params.name; |
||||
const args = request.params.arguments ?? {}; |
||||
|
||||
try { |
||||
switch (name) { |
||||
case "list_local_images": |
||||
return await listLocalImages(args.dirPath); |
||||
case "load_local_image": |
||||
return await loadLocalImage(args.filePath); |
||||
default: |
||||
return { |
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }], |
||||
isError: true, |
||||
}; |
||||
} |
||||
} catch (error) { |
||||
return { |
||||
content: [ |
||||
{ |
||||
type: "text", |
||||
text: `Tool failed: ${error instanceof Error ? error.message : String(error)}`, |
||||
}, |
||||
], |
||||
isError: true, |
||||
}; |
||||
} |
||||
}); |
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ |
||||
resources: Array.from(imageCache.entries()).map(([filePath, image]) => ({ |
||||
uri: `debug-image://${encodeURIComponent(filePath)}`, |
||||
name: path.basename(filePath), |
||||
mimeType: image.mimeType, |
||||
})), |
||||
})); |
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { |
||||
const uri = request.params.uri.toString(); |
||||
if (!uri.startsWith("debug-image://")) { |
||||
throw new Error(`Unsupported resource URI: ${uri}`); |
||||
} |
||||
|
||||
const filePath = decodeURIComponent(uri.replace("debug-image://", "")); |
||||
const cached = imageCache.get(filePath); |
||||
if (!cached) { |
||||
throw new Error(`Resource not found: ${filePath}`); |
||||
} |
||||
|
||||
return { |
||||
contents: [ |
||||
{ |
||||
uri, |
||||
mimeType: cached.mimeType, |
||||
blob: cached.base64, |
||||
}, |
||||
], |
||||
}; |
||||
}); |
||||
|
||||
const transport = new StdioServerTransport(); |
||||
await server.connect(transport); |
||||
Loading…
Reference in new issue