Github-Oauth MCP Server
- src
- tools
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import FormData from 'form-data';
import { createGhostApi } from '../config/config.js';
import { handleGhostApiError } from '../utils/error.js';
import { ImageUploadParams, isImageUploadParams, ImageResponse } from '../types/index.js';
import imageSize from 'image-size';
// 許可される画像フォーマット
const ALLOWED_FORMATS = {
image: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.svg'],
profile_image: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.svg'],
icon: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.svg', '.ico']
};
// 最大ファイルサイズ (2MB)
const MAX_FILE_SIZE = 2 * 1024 * 1024;
// 画像フォーマットのバリデーション
const validateImageFormat = (buffer: Buffer, mimeType: string, purpose: string = 'image'): void => {
// ファイルサイズのチェック
if (buffer.length > MAX_FILE_SIZE) {
throw new McpError(
ErrorCode.InvalidParams,
`File size exceeds maximum limit of ${MAX_FILE_SIZE / 1024 / 1024}MB`
);
}
// MIMEタイプから拡張子を取得
const extension = `.${mimeType.split('/')[1]}`.toLowerCase();
const allowedFormats = ALLOWED_FORMATS[purpose as keyof typeof ALLOWED_FORMATS];
if (!allowedFormats.includes(extension)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid image format for ${purpose}. Allowed formats: ${allowedFormats.join(', ')}`
);
}
// profile_imageとiconの場合は正方形のチェックを追加
if (purpose === 'profile_image' || purpose === 'icon') {
try {
const dimensions = imageSize.imageSize(buffer);
if (!dimensions || dimensions.width !== dimensions.height) {
throw new McpError(
ErrorCode.InvalidParams,
dimensions ?
`${purpose} must be square (current dimensions: ${dimensions.width}x${dimensions.height})` :
`Failed to determine image dimensions`
);
}
} catch (error) {
if (error instanceof McpError) throw error;
throw new McpError(
ErrorCode.InvalidParams,
'Failed to read image dimensions'
);
}
}
};
// Base64からBufferへの変換とMIMEタイプの抽出
const parseBase64Image = (base64Data: string): { buffer: Buffer; mimeType: string } => {
const matches = base64Data.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,(.+)$/);
if (!matches) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid Base64 image data format'
);
}
try {
const [, mimeType, base64Image] = matches;
const buffer = Buffer.from(base64Image, 'base64');
return { buffer, mimeType };
} catch (error) {
throw new McpError(
ErrorCode.InvalidParams,
'Failed to decode Base64 image data'
);
}
};
// 画像アップロード関数
export const uploadImage = async (args: unknown): Promise<{ content: { url: string; ref?: string } }> => {
if (!isImageUploadParams(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid image upload parameters'
);
}
try {
const { file, purpose, ref } = args;
// Base64データをパースしてバッファとMIMEタイプを取得
const { buffer, mimeType } = parseBase64Image(file);
// 画像フォーマットのバリデーション
validateImageFormat(buffer, mimeType, purpose);
// FormDataの構築
const formData = new FormData() as any;
formData.append('file', buffer, {
filename: `image${mimeType.replace('image/', '.')}`,
contentType: mimeType
});
if (purpose) formData.append('purpose', purpose);
if (ref) formData.append('ref', ref);
// Ghost Admin APIクライアントの作成
const api = createGhostApi();
// 画像アップロードリクエストの送信
const response = await api.images.upload({
file: formData,
purpose,
ref
});
return {
content: {
url: response.url,
ref: response.ref
}
};
} catch (error) {
return handleGhostApiError(error);
}
};
// ツールスキーマの定義
export const uploadImageSchema = {
name: 'upload_image',
description: '画像をアップロード',
inputSchema: {
type: 'object',
properties: {
file: {
type: 'string',
description: 'アップロードする画像ファイル(Base64)'
},
purpose: {
type: 'string',
description: '画像の用途',
enum: ['image', 'profile_image', 'icon']
},
ref: {
type: 'string',
description: '画像の参照情報(オプション)'
}
},
required: ['file']
}
};