1- import fs from 'fs' ;
2- import yaml from 'js-yaml' ;
31import path from 'path' ;
4- import { z } from 'zod' ;
5- import { zodToJsonSchema } from 'zod-to-json-schema' ;
62import { fileURLToPath } from 'url' ;
73import * as healthService from './healthService.mjs' ;
84import * as issueService from './issueService.mjs' ;
95import * as labelService from './labelService.mjs' ;
106import * as pullRequestService from './pullRequestService.mjs' ;
7+ import { initialize , listTools , callTool } from '../../toolService.mjs' ;
118
129const __filename = fileURLToPath ( import . meta. url ) ;
1310const __dirname = path . dirname ( __filename ) ;
1411const openApiFilePath = path . join ( __dirname , '../openapi.yaml' ) ;
1512
16- // Internal cache for parsed tool definitions to avoid re-parsing OpenAPI on every call.
17- let toolMapping = null ;
18- // Internal cache for tools formatted for the MCP 'tools/list' response, including JSON schemas.
19- let allToolsForListing = null ;
20-
21- /**
22- * Maps snake_case tool names (from OpenAPI operationId) to their corresponding
23- * service handler functions. This explicit mapping ensures clarity and allows
24- * for easy lookup of the correct function to execute a tool.
25- */
2613const serviceMapping = {
2714 add_labels : issueService . addLabels ,
2815 checkout_pull_request : pullRequestService . checkoutPullRequest ,
@@ -35,290 +22,7 @@ const serviceMapping = {
3522 remove_labels : issueService . removeLabels
3623} ;
3724
38- /**
39- * Dynamically constructs a Zod schema for a tool's input arguments based on its
40- * OpenAPI operation definition. This schema is used for robust runtime validation
41- * of incoming tool call arguments.
42- * @param {object } openApiDocument - The OpenAPI document object.
43- * @param {object } operation - The OpenAPI operation object.
44- * @returns {z.ZodObject } A Zod object schema representing the tool's input.
45- */
46- function buildZodSchema ( openApiDocument , operation ) {
47- const shape = { } ;
48-
49- // Process parameters defined in the OpenAPI operation (path, query, header, etc.).
50- if ( operation . parameters ) {
51- for ( const param of operation . parameters ) {
52- let schema ;
53- switch ( param . schema . type ) {
54- case 'integer' :
55- schema = z . number ( ) . int ( ) ;
56- break ;
57- case 'string' :
58- schema = z . string ( ) ;
59- break ;
60- case 'boolean' :
61- schema = z . boolean ( ) ;
62- break ;
63- default :
64- // Fallback for unsupported or unknown schema types.
65- schema = z . any ( ) ;
66- }
67- // Mark schema as optional if not explicitly required.
68- if ( ! param . required ) {
69- schema = schema . optional ( ) ;
70- }
71- // Add description for better Zod schema introspection.
72- shape [ param . name ] = schema . describe ( param . description ) ;
73- }
74- }
75-
76- // Process request body properties, typically for POST/PUT operations.
77- if ( operation . requestBody ?. content ?. [ 'application/json' ] ?. schema ) {
78- let requestBodySchema = operation . requestBody . content [ 'application/json' ] . schema ;
79- if ( requestBodySchema . $ref ) {
80- requestBodySchema = resolveRef ( openApiDocument , requestBodySchema . $ref ) ;
81- }
82-
83- if ( requestBodySchema . properties ) {
84- const { properties, required = [ ] } = requestBodySchema ;
85- for ( const [ propName , propSchema ] of Object . entries ( properties ) ) {
86- let schema ;
87- switch ( propSchema . type ) {
88- case 'string' :
89- schema = z . string ( ) ;
90- break ;
91- case 'array' :
92- schema = z . array ( z . string ( ) ) ;
93- break ;
94- default :
95- schema = z . any ( ) ;
96- }
97- if ( ! required . includes ( propName ) ) {
98- schema = schema . optional ( ) ;
99- }
100- shape [ propName ] = schema . describe ( propSchema . description ) ;
101- }
102- }
103- }
104- return z . object ( shape ) ;
105- }
106-
107- /**
108- * Recursively resolves JSON references ($ref) within the OpenAPI document.
109- * This is crucial for building complete Zod schemas from potentially fragmented
110- * OpenAPI definitions (e.g., schemas defined in 'components').
111- * @param {object } doc - The full OpenAPI document.
112- * @param {string } ref - The JSON reference string (e.g., '#/components/schemas/MySchema').
113- * @returns {object } The resolved schema object.
114- */
115- function resolveRef ( doc , ref ) {
116- // Remove '#/' prefix and split the path into components.
117- const parts = ref . substring ( 2 ) . split ( '/' ) ;
118- // Traverse the document object to find the referenced schema.
119- return parts . reduce ( ( acc , part ) => acc [ part ] , doc ) ;
120- }
121-
122- /**
123- * Recursively builds a Zod schema from an OpenAPI schema object, handling
124- * nested structures and JSON references. This is used for output schemas.
125- * @param {object } doc - The full OpenAPI document for reference resolution.
126- * @param {object } schema - The OpenAPI schema object (or a resolved reference).
127- * @returns {z.ZodType } A Zod schema representing the OpenAPI schema.
128- */
129- function buildZodSchemaFromResponse ( doc , schema ) {
130- if ( schema . $ref ) {
131- return buildZodSchemaFromResponse ( doc , resolveRef ( doc , schema . $ref ) ) ;
132- }
133-
134- let zodSchema ;
135- if ( schema . type === 'object' ) {
136- const shape = { } ;
137- if ( schema . properties ) {
138- for ( const [ propName , propSchema ] of Object . entries ( schema . properties ) ) {
139- shape [ propName ] = buildZodSchemaFromResponse ( doc , propSchema ) ;
140- }
141- }
142- zodSchema = z . object ( shape ) ;
143- } else if ( schema . type === 'array' ) {
144- zodSchema = z . array ( buildZodSchemaFromResponse ( doc , schema . items ) ) ;
145- } else if ( schema . type === 'string' ) {
146- zodSchema = z . string ( ) ;
147- } else if ( schema . type === 'integer' ) {
148- zodSchema = z . number ( ) . int ( ) ;
149- } else if ( schema . type === 'boolean' ) {
150- zodSchema = z . boolean ( ) ;
151- } else {
152- zodSchema = z . any ( ) ;
153- }
154-
155- if ( schema . description ) {
156- zodSchema = zodSchema . describe ( schema . description ) ;
157- }
158-
159- return zodSchema ;
160- }
161-
162- /**
163- * Constructs a Zod schema for a tool's output based on its OpenAPI operation's
164- * successful response (200 or 201). This schema is used to describe the expected
165- * output structure to clients.
166- * @param {object } doc - The full OpenAPI document for reference resolution.
167- * @param {object } operation - The OpenAPI operation object.
168- * @returns {z.ZodType|null } A Zod schema for the output, or null if no schema is defined.
169- */
170- function buildOutputZodSchema ( doc , operation ) {
171- const response = operation . responses ?. [ '200' ] || operation . responses ?. [ '201' ] ;
172- const schema = response ?. content ?. [ 'application/json' ] ?. schema ;
173-
174- if ( schema ) {
175- return buildZodSchemaFromResponse ( doc , schema ) ;
176- }
177-
178- if ( response ?. content ?. [ 'text/plain' ] ) {
179- // For text/plain, we need to wrap it in an object for client compatibility
180- return z . object ( { result : z . string ( ) . describe ( response . description || '' ) } ) . required ( ) ;
181- }
182-
183- // If no schema is found, return null to indicate its absence.
184- return null ;
185- }
186-
187- /**
188- * Initializes the internal tool mapping and the list of tools for client discovery.
189- * This function is designed to be called lazily on the first request to avoid
190- * unnecessary parsing at startup.
191- */
192- function initializeToolMapping ( ) {
193- // Prevent re-initialization if already done.
194- if ( toolMapping ) {
195- return ;
196- }
197-
198- toolMapping = { } ;
199- allToolsForListing = [ ] ;
200-
201- // Load and parse the OpenAPI specification.
202- const openApiDocument = yaml . load ( fs . readFileSync ( openApiFilePath , 'utf8' ) ) ;
203-
204- // Iterate through all paths and operations defined in the OpenAPI document.
205- for ( const pathItem of Object . values ( openApiDocument . paths ) ) {
206- for ( const operation of Object . values ( pathItem ) ) {
207- // Only process operations that have an operationId, as these are considered tools.
208- if ( operation . operationId ) {
209- const toolName = operation . operationId ;
210-
211- // Build Zod schema for input arguments and convert to JSON Schema for client discovery.
212- const inputZodSchema = buildZodSchema ( openApiDocument , operation ) ;
213- const inputJsonSchema = zodToJsonSchema ( inputZodSchema ) ;
214-
215- // Build Zod schema for output and convert to JSON Schema for client discovery.
216- const outputZodSchema = buildOutputZodSchema ( openApiDocument , operation ) ;
217- let outputJsonSchema = null ;
218- if ( outputZodSchema ) {
219- outputJsonSchema = zodToJsonSchema ( outputZodSchema ) ;
220- }
221-
222- // Extract argument names in order for positional argument mapping to service handlers.
223- // This is crucial because Zod validation returns an object, but service handlers
224- // might expect positional arguments. The order is derived from OpenAPI parameters.
225- const argNames = ( operation . parameters || [ ] ) . map ( p => p . name ) ;
226- if ( operation . requestBody ?. content ?. [ 'application/json' ] ?. schema ?. properties ) {
227- argNames . push ( ...Object . keys ( operation . requestBody . content [ 'application/json' ] . schema . properties ) ) ;
228- }
229-
230- // Store the internal tool definition for execution.
231- const tool = {
232- name : toolName ,
233- title : operation . summary || toolName ,
234- description : operation . description || operation . summary ,
235- zodSchema : inputZodSchema ,
236- argNames,
237- handler : serviceMapping [ toolName ]
238- } ;
239- toolMapping [ toolName ] = tool ;
240-
241- // Store the client-facing tool definition for 'tools/list' response.
242- const toolForListing = {
243- name : tool . name ,
244- title : tool . title ,
245- description : tool . description ,
246- inputSchema : inputJsonSchema
247- } ;
248- if ( outputJsonSchema !== null ) {
249- toolForListing . outputSchema = outputJsonSchema ;
250- }
251- if ( operation [ 'x-annotations' ] !== null ) {
252- toolForListing . annotations = operation [ 'x-annotations' ] ;
253- }
254- allToolsForListing . push ( toolForListing ) ;
255- }
256- }
257- }
258- }
259-
260- /**
261- * Provides a paginated list of available tools, formatted for MCP client discovery.
262- * @param {object } [options] - Pagination options.
263- * @param {number } [options.cursor=0] - The starting index for the list.
264- * @param {number } [options.limit] - The maximum number of tools to return. If not provided, all tools are returned.
265- * @returns {object } An object containing the list of tools and a nextCursor for pagination.
266- */
267- function listTools ( { cursor = 0 , limit } = { } ) {
268- initializeToolMapping ( ) ;
269-
270- // If no limit is specified, return all tools without pagination.
271- if ( ! limit ) {
272- return {
273- tools : allToolsForListing ,
274- nextCursor : null
275- } ;
276- }
277-
278- // Apply pagination based on cursor and limit.
279- const start = cursor ;
280- const end = start + limit ;
281- const toolsSlice = allToolsForListing . slice ( start , end ) ;
282- const nextCursor = end < allToolsForListing . length ? String ( end ) : null ; // Specs do not accept numbers
283-
284- return {
285- tools : toolsSlice ,
286- nextCursor
287- } ;
288- }
289-
290- /**
291- * Executes a specific tool with the given arguments.
292- * This function performs input validation and maps arguments to the service handler.
293- * @param {string } toolName - The name of the tool to call (snake_case).
294- * @param {object } args - The arguments provided by the client for the tool call.
295- * @returns {Promise<any> } The result of the tool execution.
296- * @throws {Error } If the tool is not found, not implemented, or arguments are invalid.
297- */
298- async function callTool ( toolName , args ) {
299- initializeToolMapping ( ) ;
300- const tool = toolMapping [ toolName ] ;
301-
302- // Ensure the tool exists and has a registered handler.
303- if ( ! tool || ! tool . handler ) {
304- throw new Error ( `Tool "${ toolName } " not found or not implemented.` ) ;
305- }
306-
307- // Validate incoming arguments against the tool's Zod schema.
308- // This will throw an error if validation fails, which is caught by the MCP server.
309- const validatedArgs = tool . zodSchema . parse ( args ) ;
310-
311- // Special handling for 'list_pull_requests' as its handler expects a single object argument.
312- // This is a pragmatic solution for an inconsistent service handler signature.
313- if ( toolName === 'list_pull_requests' ) {
314- return tool . handler ( validatedArgs ) ;
315- }
316-
317- // For other tools, map validated arguments to positional arguments for the handler.
318- // The order is determined by the 'argNames' extracted from OpenAPI.
319- const handlerArgs = tool . argNames . map ( name => validatedArgs [ name ] ) ;
320- return tool . handler ( ...handlerArgs ) ;
321- }
25+ initialize ( serviceMapping , openApiFilePath ) ;
32226
32327export {
32428 listTools ,
0 commit comments