Gyazo MCP Server
by yuiseki
Verified
#!/usr/bin/env node
/**
* This is a MCP server that provides access to Gyazo images.
* It allows you to list available images, read image contents, and fetch the latest image.
* The server uses the Gyazo API to fetch image metadata and content.
* The server provides a single tool for fetching the latest image content and metadata.
* The server is started using stdio transport, which allows it to communicate via standard input/output streams.
* The server requires a Gyazo access token to access the Gyazo API.
* The access token is read from the GYAZO_ACCESS_TOKEN environment variable.
* The server is started by running the script with Node.js.
* The server is implemented using the MCP SDK, which provides a high-level API for building MCP servers.
*/
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";
import dotenv from "dotenv";
dotenv.config();
// Ensure the GYAZO_ACCESS_TOKEN environment variable is set
const GYAZO_ACCESS_TOKEN = process.env.GYAZO_ACCESS_TOKEN;
if (!GYAZO_ACCESS_TOKEN) {
throw new Error("GYAZO_ACCESS_TOKEN environment variable is required");
}
/**
* Type alias for a note object.
*/
type GyazoImage = {
image_id: string;
permalink_url: string;
thumb_url: string;
url: string;
type: string;
created_at: string;
metadata: {
app: string;
title: string;
url: string;
desc: string;
};
ocr?: {
locale: string;
description: string;
};
};
/**
* Type for search API response.
*/
type SearchedGyazoImage = {
image_id: string;
permalink_url: string;
url: string;
access_policy: string | null;
type: string;
thumb_url: string;
created_at: string;
alt_text: string;
};
/**
* Create an MCP server with capabilities for resources (to list/read images)
* and tools (to fetch the latest image).
*/
const server = new Server(
{
name: "gyazo-mcp-server",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
/**
* Handler for listing available images as resources.
* Each note is exposed as a resource with:
* - A gyazo-mcp:// URI
* - Plain text MIME type
* - Human readable name and description
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const endpoint = "https://api.gyazo.com/api/images";
const params = new URLSearchParams();
params.append("access_token", GYAZO_ACCESS_TOKEN);
params.append("page", "1");
params.append("per_page", "10");
const url = `${endpoint}?${params.toString()}`;
const response = await fetch(url);
const gyazoImages: GyazoImage[] = await response.json();
return {
resources: gyazoImages.map((gyazoImage) => ({
uri: `gyazo-mcp:///${gyazoImage.image_id}`,
mimeType: `image/${gyazoImage.type}`,
name: gyazoImage.metadata.title || gyazoImage.image_id,
})),
};
});
const getImageMetadataMarkdown = (gyazoImage: GyazoImage) => {
let imageMetadataMarkdown = "";
if (gyazoImage.metadata.title) {
imageMetadataMarkdown += `### Title:\n${gyazoImage.metadata.title}\n\n`;
}
if (gyazoImage.metadata.desc) {
imageMetadataMarkdown += `### Description:\n${gyazoImage.metadata.desc}\n\n`;
}
if (gyazoImage.metadata.app) {
imageMetadataMarkdown += `### App:\n${gyazoImage.metadata.app}\n\n`;
}
if (gyazoImage.metadata.url) {
imageMetadataMarkdown += `### URL:\n${gyazoImage.metadata.url}\n\n`;
}
if (gyazoImage.ocr?.description) {
imageMetadataMarkdown += `### OCR:\n${gyazoImage.ocr.description}\n\n`;
}
if (gyazoImage.ocr?.locale) {
imageMetadataMarkdown += `### Locale:\n${gyazoImage.ocr.locale}\n\n`;
}
return imageMetadataMarkdown;
};
/**
* Handler for reading the contents of a specific image.
* Takes a gyazo-mcp:// URI and returns the image contents.
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
try {
const url = request.params.uri.toString();
const id = url.replace("gyazo-mcp:///", "");
const endpoint = `https://api.gyazo.com/api/images/${id}`;
const params = new URLSearchParams();
params.append("access_token", GYAZO_ACCESS_TOKEN);
const response = await fetch(`${endpoint}?${params.toString()}`);
const gyazoImage: GyazoImage = await response.json();
if (!gyazoImage) {
throw new Error(`Image ${id} not found`);
}
const imageUrl = gyazoImage.url;
const imageBlob = await fetch(imageUrl).then((res) => res.blob());
const imageBuffer = await imageBlob.arrayBuffer();
const imageBase64 = Buffer.from(imageBuffer).toString("base64");
const imageMetadataMarkdown = getImageMetadataMarkdown(gyazoImage);
return {
contents: [
{
uri: url,
mimeType: `image/${gyazoImage.type}`,
blob: imageBase64,
},
{
uri: url,
mimeType: "text/plain",
text: imageMetadataMarkdown,
},
],
};
} catch (error) {
console.error(error);
if (error instanceof Error) {
throw new Error(`${error.message}`);
} else {
throw new Error("An unknown error occurred");
}
}
});
/**
* Handler for listing available tools.
* This server provides a single tool for fetching image metadata.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "gyazo_latest_image",
description: "Fetch latest image content and metadata from Gyazo",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
const: "gyazo_latest_image",
},
},
required: ["name"],
},
},
{
name: "gyazo_search",
description: "Search through user's saved Gyazo images",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (max length: 200 characters)",
},
page: {
type: "integer",
description: "Page number for pagination",
minimum: 1,
default: 1,
},
per: {
type: "integer",
description: "Number of results per page (max: 100)",
minimum: 1,
maximum: 100,
default: 20,
},
},
required: ["query"],
},
},
],
};
});
/**
* Handler for calling a tool.
* This server provides a single tool for fetching image metadata.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "gyazo_search") {
if (!request.params.arguments || typeof request.params.arguments.query !== "string") {
throw new Error("Invalid search arguments: query is required and must be a string");
}
const endpoint = "https://api.gyazo.com/api/search";
const params = new URLSearchParams();
params.append("access_token", GYAZO_ACCESS_TOKEN);
params.append("query", request.params.arguments.query);
const page = typeof request.params.arguments.page === "number" ? request.params.arguments.page : 1;
const per = typeof request.params.arguments.per === "number" ? request.params.arguments.per : 20;
params.append("page", page.toString());
params.append("per", per.toString());
const response = await fetch(`${endpoint}?${params.toString()}`);
const images: SearchedGyazoImage[] = await response.json();
if (!images || images.length === 0) {
return {
content: [
{
type: "text",
text: "No images found",
}
]
};
}
const contents = await Promise.all(
images.map(async (image) => {
return {
uri: `gyazo-mcp:///${image.image_id}`,
mimeType: `image/${image.type}`,
permalink_url: image.permalink_url,
url: image.url,
thumb_url: image.thumb_url,
created_at: image.created_at,
alt_text: image.alt_text,
};
})
);
return {
content: [
{
type: "text",
text: JSON.stringify(contents, null, 2)
}
]
};
} else if (request.params.name === "gyazo_latest_image") {
const endpoint = "https://api.gyazo.com/api/images";
const params = new URLSearchParams();
params.append("access_token", GYAZO_ACCESS_TOKEN);
params.append("page", "1");
params.append("per_page", "1");
const response = await fetch(`${endpoint}?${params.toString()}`);
const images: GyazoImage[] = await response.json();
if (!images || images.length === 0) {
throw new Error(`Image not found`);
}
const image = images[0];
const imageUrl = image.url;
const imageBlob = await fetch(imageUrl).then((res) => res.blob());
const imageBuffer = await imageBlob.arrayBuffer();
const imageBase64 = Buffer.from(imageBuffer).toString("base64");
return {
content: [
{
type: "image",
data: imageBase64,
mimeType: `image/${image.type}`,
},
{
type: "text",
text: getImageMetadataMarkdown(image),
},
],
};
}
throw new Error(`Tool ${request.params.name} not found`);
});
/**
* Start the server using stdio transport.
* This allows the server to communicate via standard input/output streams.
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});