@@ -12,9 +12,16 @@ const __filename = fileURLToPath(import.meta.url);
1212const __dirname = path . dirname ( __filename ) ;
1313const openApiFilePath = path . join ( __dirname , '../openapi.yaml' ) ;
1414
15+ // Internal cache for parsed tool definitions to avoid re-parsing OpenAPI on every call.
1516let toolMapping = null ;
17+ // Internal cache for tools formatted for the MCP 'tools/list' response, including JSON schemas.
1618let allToolsForListing = null ;
1719
20+ /**
21+ * Maps snake_case tool names (from OpenAPI operationId) to their corresponding
22+ * service handler functions. This explicit mapping ensures clarity and allows
23+ * for easy lookup of the correct function to execute a tool.
24+ */
1825const serviceMapping = {
1926 add_labels : issueService . addLabels ,
2027 checkout_pull_request : pullRequestService . checkoutPullRequest ,
@@ -26,8 +33,17 @@ const serviceMapping = {
2633 remove_labels : issueService . removeLabels
2734} ;
2835
36+ /**
37+ * Dynamically constructs a Zod schema for a tool's input arguments based on its
38+ * OpenAPI operation definition. This schema is used for robust runtime validation
39+ * of incoming tool call arguments.
40+ * @param {object } operation - The OpenAPI operation object.
41+ * @returns {z.ZodObject } A Zod object schema representing the tool's input.
42+ */
2943function buildZodSchema ( operation ) {
3044 const shape = { } ;
45+
46+ // Process parameters defined in the OpenAPI operation (path, query, header, etc.).
3147 if ( operation . parameters ) {
3248 for ( const param of operation . parameters ) {
3349 let schema ;
@@ -42,15 +58,19 @@ function buildZodSchema(operation) {
4258 schema = z . boolean ( ) ;
4359 break ;
4460 default :
61+ // Fallback for unsupported or unknown schema types.
4562 schema = z . any ( ) ;
4663 }
64+ // Mark schema as optional if not explicitly required.
4765 if ( ! param . required ) {
4866 schema = schema . optional ( ) ;
4967 }
68+ // Add description for better Zod schema introspection.
5069 shape [ param . name ] = schema . describe ( param . description ) ;
5170 }
5271 }
5372
73+ // Process request body properties, typically for POST/PUT operations.
5474 if ( operation . requestBody ?. content ?. [ 'application/json' ] ?. schema ?. properties ) {
5575 const { properties, required = [ ] } = operation . requestBody . content [ 'application/json' ] . schema ;
5676 for ( const [ propName , propSchema ] of Object . entries ( properties ) ) {
@@ -60,30 +80,55 @@ function buildZodSchema(operation) {
6080 schema = z . string ( ) ;
6181 break ;
6282 case 'array' :
63- schema = z . array ( z . string ( ) ) ; // Assuming array of strings for now
83+ // Currently assumes arrays of strings. This is a simplification
84+ // based on current OpenAPI spec usage and may need refinement
85+ // if other array item types are introduced.
86+ schema = z . array ( z . string ( ) ) ;
6487 break ;
6588 default :
89+ // Fallback for unsupported or unknown schema types.
6690 schema = z . any ( ) ;
6791 }
92+ // Mark schema as optional if not explicitly required in the request body.
6893 if ( ! required . includes ( propName ) ) {
6994 schema = schema . optional ( ) ;
7095 }
96+ // Add description for better Zod schema introspection.
7197 shape [ propName ] = schema . describe ( propSchema . description ) ;
7298 }
7399 }
74100 return z . object ( shape ) ;
75101}
76102
103+ /**
104+ * Recursively resolves JSON references ($ref) within the OpenAPI document.
105+ * This is crucial for building complete Zod schemas from potentially fragmented
106+ * OpenAPI definitions (e.g., schemas defined in 'components').
107+ * @param {object } doc - The full OpenAPI document.
108+ * @param {string } ref - The JSON reference string (e.g., '#/components/schemas/MySchema').
109+ * @returns {object } The resolved schema object.
110+ */
77111function resolveRef ( doc , ref ) {
78- const parts = ref . substring ( 2 ) . split ( '/' ) ; // remove '#/' and split
112+ // Remove '#/' prefix and split the path into components.
113+ const parts = ref . substring ( 2 ) . split ( '/' ) ;
114+ // Traverse the document object to find the referenced schema.
79115 return parts . reduce ( ( acc , part ) => acc [ part ] , doc ) ;
80116}
81117
118+ /**
119+ * Recursively builds a Zod schema from an OpenAPI schema object, handling
120+ * nested structures and JSON references. This is used for output schemas.
121+ * @param {object } doc - The full OpenAPI document for reference resolution.
122+ * @param {object } schema - The OpenAPI schema object (or a resolved reference).
123+ * @returns {z.ZodType } A Zod schema representing the OpenAPI schema.
124+ */
82125function buildZodSchemaFromResponse ( doc , schema ) {
126+ // If the schema is a reference, resolve it first.
83127 if ( schema . $ref ) {
84128 return buildZodSchemaFromResponse ( doc , resolveRef ( doc , schema . $ref ) ) ;
85129 }
86130
131+ // Handle different OpenAPI schema types.
87132 if ( schema . type === 'object' ) {
88133 const shape = { } ;
89134 if ( schema . properties ) {
@@ -93,6 +138,7 @@ function buildZodSchemaFromResponse(doc, schema) {
93138 }
94139 return z . object ( shape ) ;
95140 } else if ( schema . type === 'array' ) {
141+ // Recursively build schema for array items.
96142 return z . array ( buildZodSchemaFromResponse ( doc , schema . items ) ) ;
97143 } else if ( schema . type === 'string' ) {
98144 return z . string ( ) ;
@@ -101,53 +147,81 @@ function buildZodSchemaFromResponse(doc, schema) {
101147 } else if ( schema . type === 'boolean' ) {
102148 return z . boolean ( ) ;
103149 } else {
150+ // Fallback for unsupported or unknown schema types.
104151 return z . any ( ) ;
105152 }
106153}
107154
155+ /**
156+ * Constructs a Zod schema for a tool's output based on its OpenAPI operation's
157+ * successful response (200 or 201). This schema is used to describe the expected
158+ * output structure to clients.
159+ * @param {object } doc - The full OpenAPI document for reference resolution.
160+ * @param {object } operation - The OpenAPI operation object.
161+ * @returns {z.ZodType|null } A Zod schema for the output, or null if no schema is defined.
162+ */
108163function buildOutputZodSchema ( doc , operation ) {
164+ // Prioritize 200 OK, then 201 Created responses.
109165 const response = operation . responses ?. [ '200' ] || operation . responses ?. [ '201' ] ;
110166 const schema = response ?. content ?. [ 'application/json' ] ?. schema ;
111167
168+ // If an application/json schema is found, build a Zod schema from it.
112169 if ( schema ) {
113170 return buildZodSchemaFromResponse ( doc , schema ) ;
114171 }
115172
173+ // Special handling for text/plain responses (e.g., diff output).
116174 if ( response ?. content ?. [ 'text/plain' ] ) {
117175 return z . string ( ) ;
118176 }
119177
120178 return null ;
121179}
122180
181+ /**
182+ * Initializes the internal tool mapping and the list of tools for client discovery.
183+ * This function is designed to be called lazily on the first request to avoid
184+ * unnecessary parsing at startup.
185+ */
123186function initializeToolMapping ( ) {
187+ // Prevent re-initialization if already done.
124188 if ( toolMapping ) {
125189 return ;
126190 }
127191
128192 toolMapping = { } ;
129193 allToolsForListing = [ ] ;
130194
195+ // Load and parse the OpenAPI specification.
131196 const openApiDocument = yaml . load ( fs . readFileSync ( openApiFilePath , 'utf8' ) ) ;
132197
198+ // Iterate through all paths and operations defined in the OpenAPI document.
133199 for ( const pathItem of Object . values ( openApiDocument . paths ) ) {
134200 for ( const operation of Object . values ( pathItem ) ) {
201+ // Only process operations that have an operationId, as these are considered tools.
135202 if ( operation . operationId ) {
136203 const toolName = operation . operationId ;
204+
205+ // Build Zod schema for input arguments and convert to JSON Schema for client discovery.
137206 const inputZodSchema = buildZodSchema ( operation ) ;
138207 const inputJsonSchema = zodToJsonSchema ( inputZodSchema ) ;
139208
209+ // Build Zod schema for output and convert to JSON Schema for client discovery.
140210 const outputZodSchema = buildOutputZodSchema ( openApiDocument , operation ) ;
141211 let outputJsonSchema = null ;
142212 if ( outputZodSchema ) {
143213 outputJsonSchema = zodToJsonSchema ( outputZodSchema ) ;
144214 }
145215
216+ // Extract argument names in order for positional argument mapping to service handlers.
217+ // This is crucial because Zod validation returns an object, but service handlers
218+ // might expect positional arguments. The order is derived from OpenAPI parameters.
146219 const argNames = ( operation . parameters || [ ] ) . map ( p => p . name ) ;
147220 if ( operation . requestBody ?. content ?. [ 'application/json' ] ?. schema ?. properties ) {
148221 argNames . push ( ...Object . keys ( operation . requestBody . content [ 'application/json' ] . schema . properties ) ) ;
149222 }
150223
224+ // Store the internal tool definition for execution.
151225 const tool = {
152226 name : toolName ,
153227 title : operation . summary || toolName ,
@@ -158,6 +232,7 @@ function initializeToolMapping() {
158232 } ;
159233 toolMapping [ toolName ] = tool ;
160234
235+ // Store the client-facing tool definition for 'tools/list' response.
161236 allToolsForListing . push ( {
162237 name : tool . name ,
163238 title : tool . title ,
@@ -171,16 +246,25 @@ function initializeToolMapping() {
171246 }
172247}
173248
249+ /**
250+ * Provides a paginated list of available tools, formatted for MCP client discovery.
251+ * @param {object } [options] - Pagination options.
252+ * @param {number } [options.cursor=0] - The starting index for the list.
253+ * @param {number } [options.limit] - The maximum number of tools to return. If not provided, all tools are returned.
254+ * @returns {object } An object containing the list of tools and a nextCursor for pagination.
255+ */
174256function listTools ( { cursor = 0 , limit } = { } ) {
175257 initializeToolMapping ( ) ;
176258
259+ // If no limit is specified, return all tools without pagination.
177260 if ( ! limit ) {
178261 return {
179262 tools : allToolsForListing ,
180263 nextCursor : null
181264 } ;
182265 }
183266
267+ // Apply pagination based on cursor and limit.
184268 const start = cursor ;
185269 const end = start + limit ;
186270 const toolsSlice = allToolsForListing . slice ( start , end ) ;
@@ -192,20 +276,35 @@ function listTools({ cursor = 0, limit } = {}) {
192276 } ;
193277}
194278
279+ /**
280+ * Executes a specific tool with the given arguments.
281+ * This function performs input validation and maps arguments to the service handler.
282+ * @param {string } toolName - The name of the tool to call (snake_case).
283+ * @param {object } args - The arguments provided by the client for the tool call.
284+ * @returns {Promise<any> } The result of the tool execution.
285+ * @throws {Error } If the tool is not found, not implemented, or arguments are invalid.
286+ */
195287async function callTool ( toolName , args ) {
196288 initializeToolMapping ( ) ;
197289 const tool = toolMapping [ toolName ] ;
198290
291+ // Ensure the tool exists and has a registered handler.
199292 if ( ! tool || ! tool . handler ) {
200293 throw new Error ( `Tool "${ toolName } " not found or not implemented.` ) ;
201294 }
202295
296+ // Validate incoming arguments against the tool's Zod schema.
297+ // This will throw an error if validation fails, which is caught by the MCP server.
203298 const validatedArgs = tool . zodSchema . parse ( args ) ;
204299
300+ // Special handling for 'list_pull_requests' as its handler expects a single object argument.
301+ // This is a pragmatic solution for an inconsistent service handler signature.
205302 if ( toolName === 'list_pull_requests' ) {
206303 return tool . handler ( validatedArgs ) ;
207304 }
208305
306+ // For other tools, map validated arguments to positional arguments for the handler.
307+ // The order is determined by the 'argNames' extracted from OpenAPI.
209308 const handlerArgs = tool . argNames . map ( name => validatedArgs [ name ] ) ;
210309 return tool . handler ( ...handlerArgs ) ;
211310}
0 commit comments