diff --git a/src/app.ts b/src/app.ts index 238c696..db33b98 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,6 +26,10 @@ export type Photo = { author?: Author } +export type PhotoWithData = Photo & { + base64: string +} + export type Shop = { id: string name?: string @@ -192,3 +196,44 @@ const arrayBufferToJSON = (arrayBuffer: ArrayBuffer) => { return arrayBuffer } } + +export const getShopPhotosWithData = async ( + shopId: string, + options: Options +): Promise => { + const shop = await getShop(shopId, options) + + const photos = await Promise.all( + shop.photos?.map(async (photo): Promise => { + try { + const buffer = await getContentFromKVAsset( + `shops/${shopId}/${photo.name}`, + { + namespace: options.c.env + ? options.c.env.__STATIC_CONTENT + : undefined, + } + ) + const base64 = arrayBufferToBase64(buffer) + return { + ...photo, + base64, + } + } catch (e) { + console.error(`Failed to load image ${photo.name}:`, e) + return null + } + }) || [] + ) + + return photos.filter(Boolean) +} + +export const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer): string => { + const bytes = new Uint8Array(arrayBuffer) + let binary = '' + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]) + } + return btoa(binary) +} diff --git a/src/mcp.ts b/src/mcp.ts index f8e5f72..21b2859 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -5,7 +5,7 @@ import type { Context } from 'hono' import { Hono } from 'hono' import { z } from 'zod' import type { Env } from './app' -import { getShop, listShopsWithPager } from './app' +import { getShop, getShopPhotosWithData, listShopsWithPager } from './app' export const getMcpServer = async (c: Context) => { const server = new McpServer({ @@ -49,6 +49,21 @@ export const getMcpServer = async (c: Context) => { } } ) + server.tool( + 'get_photos_with_data', + 'Get ramen photos with base64 data', + { shopId: z.string() }, + async ({ shopId }) => { + const photos = await getShopPhotosWithData(shopId, { c }) + return { + content: photos.map((photo) => ({ + type: 'image', + data: photo.base64, + mimeType: 'image/jpeg', + })), + } + } + ) return server } diff --git a/test/app.test.ts b/test/app.test.ts index a345d86..953c480 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -6,6 +6,7 @@ import { findIndexFromId, getAuthor, listShopsWithPager, + getShopPhotosWithData, BASE_URL, } from '@/app' @@ -103,3 +104,20 @@ describe('getAuthor', () => { expect(author.url).toBe('https://github.com/yusukebe') }) }) + +describe('getShopPhotosWithData', () => { + it('Should return photos with base64 data', async () => { + const photos = await getShopPhotosWithData('yoshimuraya', options) + expect(photos).not.toBeFalsy() + expect(photos.length).toBe(1) + expect(photos[0].name).toBe('yoshimuraya-001.jpg') + expect(photos[0].url).toBe( + `${BASE_URL}images/yoshimuraya/yoshimuraya-001.jpg` + ) + expect(photos[0].base64).toBeDefined() + expect(typeof photos[0].base64).toBe('string') + expect(photos[0].width).toBe(1200) + expect(photos[0].height).toBe(900) + expect(photos[0].authorId).toBe('yusukebe') + }) +}) diff --git a/test/mcp.test.ts b/test/mcp.test.ts index 565b967..278b1df 100644 --- a/test/mcp.test.ts +++ b/test/mcp.test.ts @@ -171,6 +171,42 @@ describe('Test /mcp', () => { expect(shop).toHaveProperty('photos') expect(Array.isArray(shop.photos)).toBe(true) }) + + it('Should execute get_photos_with_data tool and return image data', async () => { + const res = await app.request('/mcp', { + method: 'POST', + body: JSON.stringify({ + jsonrpc: '2.0', + id: 5, + method: 'tools/call', + params: { + name: 'get_photos_with_data', + arguments: { + shopId: 'yoshimuraya', + }, + }, + }), + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }, + }) + + const messages = await parseSSEJSONResponse(res) + const result = messages.find((m) => m.id === 5) + + expect(res.status).toBe(200) + expect(result).toHaveProperty('result') + expect(result.result).toHaveProperty('content') + expect(Array.isArray(result.result.content)).toBe(true) + expect(result.result.content.length).toBe(1) + + const content = result.result.content[0] + expect(content).toHaveProperty('type', 'image') + expect(content).toHaveProperty('data') + expect(content).toHaveProperty('mimeType', 'image/jpeg') + expect(typeof content.data).toBe('string') + }) }) export async function parseSSEJSONResponse(res: Response) {