diff --git a/src/mcpConnection.ts b/src/mcpConnection.ts index 08ef71f..60a3492 100644 --- a/src/mcpConnection.ts +++ b/src/mcpConnection.ts @@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest } from "fastify"; import { randomUUID } from "node:crypto"; import { McpServer, RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { ToolDef } from "./types"; +import { TextResult, ToolDef } from "./types"; export default class McpSession { private readonly transport: StreamableHTTPServerTransport; @@ -37,7 +37,7 @@ export default class McpSession { await this.mcp.connect(this.transport); } - private registerTools(...tools: ToolDef[]) { + private registerTools(...tools: ToolDef[]) { const toolsToRegister = tools .filter((tool) => !this.registeredTools.find((rtool) => rtool.title === tool.title)); const toolsToUnregister = this.registeredTools @@ -53,13 +53,16 @@ export default class McpSession { ...toolsToRegister .map((tool) => this.mcp.registerTool( tool.title, - tool.config, + { + inputSchema: {}, + ...tool.config, + }, tool.handler, )) ); } - public handleToolListChange(tools: ToolDef[], sendToolListChangedNotification: boolean = true): void { + public handleToolListChange(tools: ToolDef[], sendToolListChangedNotification: boolean = true): void { this.registerTools(...tools); if (sendToolListChangedNotification) this.mcp.sendToolListChanged(); diff --git a/src/mcpTools/gismeteo_weather.ts b/src/mcpTools/gismeteo_weather.ts index aea4151..e9badf8 100644 --- a/src/mcpTools/gismeteo_weather.ts +++ b/src/mcpTools/gismeteo_weather.ts @@ -6,7 +6,8 @@ import { gismeteoLocationCache } from "../utils/gismeteoLocationCache"; export const weatherFindCitiesByNameTool: ToolDef = { title: "weather-find-locations-by-name", config: { - description: "Find location (city, place or airport) id for futher forecast requests by its full or partial name", + description: `Authoritative weather forecast with most actual full forecast results from gismeteo.ru. +Find location (city, place or airport) id for futher forecast requests by its full or partial name.`, inputSchema: { nameStartsWith: z.string().describe("String location name starts with"), language: z.string().default('ru').describe("Language 2-letter code, e.g. ru, en"), @@ -30,9 +31,8 @@ export const weatherFindCitiesByNameTool: ToolDef = { export const weatherGetWeatherByLocationIdTool: ToolDef = { title: "weather-get-weather-by-location-id", config: { - description: `Maximum authority weather forecast source with full forecast results. -All data is based on GisMeteo.ru source. -Get weather data by location id. Use weather-find-cities-by-name to find the id`, + description: `Authoritative weather forecast with most actual full forecast results from gismeteo.ru. +Returns current weather condition and multi-day forecast for a location. Use weather-find-cities-by-name to find the location id`, inputSchema: { locationId: z.string().describe("Location id to get weather data for"), language: z.string().default('ru').describe("Language 2-letter code, e.g. ru, en"), @@ -56,9 +56,8 @@ Get weather data by location id. Use weather-find-cities-by-name to find the id` export const weatherGetWeatherByLocationNameTool: ToolDef = { title: "weather-get-weather-by-location-name", config: { - description: `Maximum authority weather forecast source with full forecast results. -All data is based on GisMeteo.ru source. -Get weather data by location (city, place or airport) name.`, + description: `Authoritative weather forecast with most actual full forecast results from gismeteo.ru. +Returns current weather condition and multi-day forecast for one or more locations`, inputSchema: { locationNameStartsWith: z.string().describe("Location name to get weather data for. All locations starting with this will be returned"), language: z.string().default('ru').describe("Language 2-letter code, e.g. ru, en"), diff --git a/src/mcpTools/listTools.ts b/src/mcpTools/listTools.ts index f964323..3f6fb05 100644 --- a/src/mcpTools/listTools.ts +++ b/src/mcpTools/listTools.ts @@ -18,7 +18,7 @@ export const listToolsTool: ToolDef = { ...tool, config: { ...tool.config, - inputSchema: zodToJsonSchema(z.object(tool.config.inputSchema), { target: "jsonSchema7" }), + inputSchema: zodToJsonSchema(z.object(tool.config.inputSchema ?? {}), { target: "jsonSchema7" }), } })), null, diff --git a/src/server.ts b/src/server.ts index 7b42d33..2963364 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ import Fastify, { type FastifyInstance } from "fastify"; -import { ToolDef } from "./types"; +import { TextResult, ToolDef } from "./types"; import McpSession from "./mcpConnection"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import * as mcpTools from "./mcpTools"; @@ -10,7 +10,7 @@ export class Server { private readonly app: FastifyInstance; private readonly port: number; private readonly sessions: McpSession[] = []; - public readonly tools: Record = {}; + public readonly tools: Record> = {}; constructor(port: number = Number(process.env.PORT ?? 3000)) { this.port = port; @@ -63,7 +63,26 @@ export class Server { for (const tool of tools) { if (!this.getTool(tool.title)) { // console.log(`Registering new tool ${tool.title}`); - this.tools[tool.title] = logged(tool); + const toolLogged = logged(tool); + this.tools[tool.title] = { + ...toolLogged, + handler: async (args) => { + try { + const res = await toolLogged.handler(args); + if (typeof res === 'string') + return { content: [{ type: "text", text: res, }] }; + else + return res; + } + catch (err) { + console.error(err); + return { + isError: true, + content: [{ type: "text", text: (err as Error).message ?? String(err) }], + }; + } + }, + }; } } diff --git a/src/types/index.ts b/src/types/index.ts index 4d22c0e..9f1af4a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,12 +8,12 @@ export type TextResult = { isError?: boolean; }; -export type ToolDef = { +export type ToolDef = { title: string; config: { description: string; - inputSchema: ZodRawShape, + inputSchema?: ZodRawShape, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - handler: (...args: any[]) => Promise; + handler: (...args: any[]) => Promise; }; \ No newline at end of file diff --git a/src/utils/logged.ts b/src/utils/logged.ts index 8cab4d3..1c80191 100644 --- a/src/utils/logged.ts +++ b/src/utils/logged.ts @@ -5,12 +5,9 @@ import { TextResult, ToolDef } from "../types"; export function logged( tool: ToolDef, ): ToolDef { - - console.dir(tool); - return { ...tool, - handler: async (...args: A): Promise => { + handler: async (...args: A): Promise => { const id = randomUUID().slice(0, 8); const t0 = process.hrtime.bigint(); console.log(`[tool>] ${tool.title} #${id} args=${JSON.stringify(args)}`); @@ -19,7 +16,7 @@ export function logged( const ms = Number(process.hrtime.bigint() - t0) / 1e6; console.log( `[tool<] ${tool.title} #${id} ${ms.toFixed(1)}ms` + - `${result.isError ? " ERROR" : ""} result=${JSON.stringify(result).slice(0, 2000)}`, + `${((typeof result !== 'string') && result.isError) ? " ERROR" : ""} result=${JSON.stringify(result).slice(0, 2000)}`, ); return result; } catch (err) {