diff --git a/package.json b/package.json index d85fec3..52cad1b 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "license": "MIT", "dependencies": { "@hono/graphql-server": "^0.4.1", + "@hono/mcp": "^0.1.0", "@modelcontextprotocol/sdk": "^1.12.1", - "fetch-to-node": "^2.1.0", "graphql": "^16.6.0", "hono": "^4.7.11", "zod": "^3.25.7" diff --git a/src/mcp.ts b/src/mcp.ts index 21b2859..9e75a35 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,8 +1,8 @@ +import { StreamableHTTPTransport } from '@hono/mcp' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' -import { toFetchResponse, toReqRes } from 'fetch-to-node' import type { Context } from 'hono' import { Hono } from 'hono' +import { HTTPException } from 'hono/http-exception' import { z } from 'zod' import type { Env } from './app' import { getShop, getShopPhotosWithData, listShopsWithPager } from './app' @@ -69,38 +69,20 @@ export const getMcpServer = async (c: Context) => { const app = new Hono() -app.post('/', async (c) => { - const { req, res } = toReqRes(c.req.raw) +app.all('/', async (c) => { const mcpServer = await getMcpServer(c) - const transport: StreamableHTTPServerTransport = - new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }) + const transport = new StreamableHTTPTransport() await mcpServer.connect(transport) - await transport.handleRequest(req, res, await c.req.json()) - res.on('close', () => { - transport.close() - mcpServer.close() - }) - return toFetchResponse(res) + return transport.handleRequest(c) }) -app.on(['GET', 'DELETE'], '/', (c) => { - return c.json( - { - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Method not allowed.', - }, - id: null, - }, - 405 - ) -}) +app.onError((err, c) => { + console.log(err.message) + + if (err instanceof HTTPException && err.res) { + return err.res + } -app.onError((e, c) => { - console.error(e.message) return c.json( { jsonrpc: '2.0', diff --git a/test/mcp.test.ts b/test/mcp.test.ts index 507b36e..81c946b 100644 --- a/test/mcp.test.ts +++ b/test/mcp.test.ts @@ -236,6 +236,37 @@ describe('Test /mcp', () => { expect(content).toHaveProperty('mimeType', 'image/jpeg') expect(typeof content.data).toBe('string') }) + + it('Should throw an error for get_shops tool', async () => { + const res = await app.request( + '/mcp', + { + method: 'POST', + body: JSON.stringify({ + jsonrpc: '2.0', + id: 6, + method: 'tools/call', + params: { + name: 'get_shops', + arguments: {}, + }, + }), + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }, + }, + mockEnv + ) + + const messages = await parseSSEJSONResponse(res) + const result = messages.find((m) => m.id === 6) + + expect(res.status).toBe(200) + expect(result).toHaveProperty('error') + expect(result.error.code).toBe(-32602) + expect(result.error.message).toContain('Invalid arguments for tool get_shops') + }) }) export async function parseSSEJSONResponse(res: Response) { diff --git a/yarn.lock b/yarn.lock index 70603e5..5dfe36a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -378,6 +378,11 @@ dependencies: graphql "^16.5.0" +"@hono/mcp@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@hono/mcp/-/mcp-0.1.0.tgz#be5b1af6d0a271b669cf477cede0e5448bf78117" + integrity sha512-IELNnKF5tjbUm+wthfToZrNxZIjs1RWLiwCx8N+m8xfJcL6hfcpj/21Gsw8HL88lbnCh191mbFwYvXnx856QZg== + "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -1765,11 +1770,6 @@ fdir@^6.4.4: resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz" integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw== -fetch-to-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fetch-to-node/-/fetch-to-node-2.1.0.tgz#e779d699f80aea0b1b34fd057737d151cb1dc19e" - integrity sha512-Wq05j6LE1GrWpT2t1YbCkyFY6xKRJq3hx/oRJdWEJpZlik3g25MmdJS6RFm49iiMJw6zpZuBOrgihOgy2jGyAA== - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"