Browse Source

Initial commit

main
Matteo Benedetto 2 weeks ago
commit
1f32abf1cf
  1. 2
      .gitignore
  2. 21
      LICENSE
  3. 109
      README.md
  4. BIN
      assets/logo.png
  5. 25
      package.json
  6. 196
      src/index.mjs

2
.gitignore vendored

@ -0,0 +1,2 @@
node_modules/
package-lock.json

21
LICENSE

@ -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.

109
README.md

@ -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

BIN
assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

25
package.json

@ -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"
}
}

196
src/index.mjs

@ -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…
Cancel
Save