Skip to content

Commit 70f3539

Browse files
committed
#7486 tools list endpoint
1 parent c7288b9 commit 70f3539

6 files changed

Lines changed: 139 additions & 0 deletions

File tree

.github/ISSUE/epic-architect-github-workflow-as-mcp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ We will employ a rapid and agile development approach. The scope and API specifi
3737
- `ticket-get-pr-conversation-history.md`: Implement an endpoint to retrieve the full conversation history for a PR.
3838
- `ticket-manage-repository-labels.md`: Implement endpoints for listing and managing repository labels.
3939

40+
### Phase 3: MCP Refactoring
41+
- **Goal:** Evolve the server from a REST API to a true MCP tool-providing server.
42+
- **Sub-Tasks:**
43+
- `ticket-refactor-to-mcp-tool-server.md`: Implement `tools/list` and `tools/call` endpoints to dynamically expose the server's capabilities.
44+
4045
### Future Scope
4146
- Implementation of the API endpoints.
4247
- Integration of ticket and issue synchronization.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: Refactor to an MCP Tool-Providing Server
3+
labels: enhancement, AI, refactoring
4+
---
5+
6+
GH ticket id: #7386
7+
8+
**Epic:** #7477
9+
**Phase:** 3
10+
**Assignee:** tobiu
11+
**Status:** To Do
12+
13+
## Description
14+
15+
The server currently functions as a standard REST API, which forces the agent to use `curl`. This is inefficient and does not align with the Model Context Protocol (MCP) vision.
16+
17+
This ticket covers the refactoring of the server to become a true MCP-compliant, tool-providing server. It will parse its own `openapi.yaml` to dynamically expose its capabilities as tools for the agent.
18+
19+
## Acceptance Criteria
20+
21+
1. A new `toolService.mjs` is created that can parse the `openapi.yaml` and map API endpoints to their underlying service functions.
22+
2. A new `tools.mjs` route is created.
23+
3. A `GET /tools/list` endpoint is implemented. It returns a JSON array of available tools, derived from the OpenAPI spec.
24+
4. A `POST /tools/call` endpoint is implemented. It receives a `toolName` and `arguments`, executes the correct service function, and returns the result.
25+
5. The existing REST endpoints (e.g., `/pull-requests`, `/labels`) remain functional for now but are considered deprecated for agent use.

ai/mcp/server/github-workflow/app.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import healthRouter from './routes/health.mjs';
77
import issuesRouter from './routes/issues.mjs';
88
import labelsRouter from './routes/labels.mjs';
99
import pullRequestsRouter from './routes/pullRequests.mjs';
10+
import toolsRouter from './routes/tools.mjs';
1011
import errorHandler from './middleware/errorHandler.mjs';
1112
import notFoundHandler from './middleware/notFoundHandler.mjs';
1213
import serverConfig from './config.mjs';
@@ -26,6 +27,7 @@ app.use('/', healthRouter);
2627
app.use('/', issuesRouter);
2728
app.use('/', labelsRouter);
2829
app.use('/', pullRequestsRouter);
30+
app.use('/', toolsRouter);
2931

3032
// 404 + Error handlers
3133
app.use(notFoundHandler);

ai/mcp/server/github-workflow/openapi.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ paths:
6363
/labels:
6464
get:
6565
summary: List Repository Labels
66+
operationId: listLabels
6667
description: Retrieves a list of all available labels in the repository.
6768
tags: [Labels]
6869
responses:
@@ -82,6 +83,7 @@ paths:
8283
/pull-requests:
8384
get:
8485
summary: List Pull Requests
86+
operationId: listPullRequests
8587
description: Retrieves a list of open pull requests from the GitHub repository.
8688
tags: [Pull Requests]
8789
parameters:
@@ -119,6 +121,7 @@ paths:
119121
/pull-requests/{pr_number}/checkout:
120122
post:
121123
summary: Checkout PR Branch
124+
operationId: checkoutPullRequest
122125
description: Checks out the branch for a given pull request locally using `gh pr checkout`.
123126
tags: [Pull Requests]
124127
parameters:
@@ -152,6 +155,7 @@ paths:
152155
/pull-requests/{pr_number}/diff:
153156
get:
154157
summary: Get PR Diff
158+
operationId: getPullRequestDiff
155159
description: Retrieves the diff for a specific pull request using `gh pr diff`.
156160
tags: [Pull Requests]
157161
parameters:
@@ -185,6 +189,7 @@ paths:
185189
/pull-requests/{pr_number}/comments:
186190
post:
187191
summary: Create a PR Comment
192+
operationId: createComment
188193
description: Adds a comment to a specific pull request.
189194
tags: [Pull Requests]
190195
parameters:
@@ -231,6 +236,7 @@ paths:
231236
/pull-requests/{pr_number}/conversation:
232237
get:
233238
summary: Get PR Conversation
239+
operationId: getConversation
234240
description: Retrieves the full conversation for a pull request, including the title, body, and all comments.
235241
tags: [Pull Requests]
236242
parameters:
@@ -252,6 +258,7 @@ paths:
252258
/issues/{issue_number}/labels:
253259
post:
254260
summary: Add labels to an issue or PR
261+
operationId: addLabels
255262
description: Adds one or more labels to a specific issue or pull request.
256263
tags: [Issues]
257264
parameters:
@@ -296,6 +303,7 @@ paths:
296303
$ref: '#/components/schemas/ErrorResponse'
297304
delete:
298305
summary: Remove labels from an issue or PR
306+
operationId: removeLabels
299307
description: Removes one or more labels from a specific issue or pull request.
300308
tags: [Issues]
301309
parameters:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Router} from 'express';
2+
import asyncHandler from '../middleware/asyncHandler.mjs';
3+
import {listTools, callTool} from '../services/toolService.mjs';
4+
5+
const router = Router();
6+
7+
router.get('/tools/list', asyncHandler(async (req, res) => {
8+
const tools = listTools();
9+
res.status(200).json(tools);
10+
}));
11+
12+
router.post('/tools/call', asyncHandler(async (req, res) => {
13+
const {toolName, args} = req.body;
14+
const result = await callTool(toolName, args);
15+
res.status(200).json(result);
16+
}));
17+
18+
export default router;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from 'fs';
2+
import yaml from 'js-yaml';
3+
import path from 'path';
4+
import {fileURLToPath} from 'url';
5+
import * as issueService from './issueService.mjs';
6+
import * as labelService from './labelService.mjs';
7+
import * as pullRequestService from './pullRequestService.mjs';
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
const openApiFilePath = path.join(__dirname, '../openapi.yaml');
12+
13+
let toolMapping = null;
14+
15+
const serviceMapping = {
16+
...issueService,
17+
...labelService,
18+
...pullRequestService
19+
};
20+
21+
/**
22+
* Parses the openapi.yaml file to build a mapping of tool names to service functions.
23+
*/
24+
function initializeToolMapping() {
25+
if (toolMapping) {
26+
return;
27+
}
28+
29+
toolMapping = {};
30+
31+
const openApiDocument = yaml.load(fs.readFileSync(openApiFilePath, 'utf8'));
32+
33+
for (const [path, pathItem] of Object.entries(openApiDocument.paths)) {
34+
for (const [method, operation] of Object.entries(pathItem)) {
35+
if (operation.operationId) {
36+
toolMapping[operation.operationId] = {
37+
path,
38+
method,
39+
description: operation.description,
40+
handler: serviceMapping[operation.operationId]
41+
};
42+
}
43+
}
44+
}
45+
}
46+
47+
/**
48+
* Lists all available tools.
49+
* @returns {object[]} A list of tool definitions.
50+
*/
51+
function listTools() {
52+
initializeToolMapping();
53+
return Object.entries(toolMapping).map(([name, tool]) => ({
54+
name: name,
55+
description: tool.description
56+
}));
57+
}
58+
59+
/**
60+
* Calls a tool by its name with the given arguments.
61+
* @param {string} toolName - The name of the tool to call.
62+
* @param {object} args - The arguments for the tool.
63+
* @returns {Promise<any>} The result of the tool execution.
64+
*/
65+
async function callTool(toolName, args) {
66+
initializeToolMapping();
67+
const tool = toolMapping[toolName];
68+
69+
if (!tool || !tool.handler) {
70+
throw new Error(`Tool "${toolName}" not found or not implemented.`);
71+
}
72+
73+
// This is a simplified argument handling. A real implementation would be more robust.
74+
const argValues = args ? Object.values(args) : [];
75+
return tool.handler(...argValues);
76+
}
77+
78+
export {
79+
listTools,
80+
callTool
81+
};

0 commit comments

Comments
 (0)