Skip to content

Commit 7940179

Browse files
committed
[MCP] Support issue comments in create_comment tool #7821
1 parent 6ed71d7 commit 7940179

6 files changed

Lines changed: 173 additions & 136 deletions

File tree

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

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -244,34 +244,30 @@ paths:
244244
schema:
245245
$ref: '#/components/schemas/ErrorResponse'
246246

247-
/pull-requests/{pr_number}/comments:
247+
/comments:
248248
post:
249-
summary: Create a PR Comment
249+
summary: Create a Comment on Issue or PR
250250
operationId: create_comment
251+
x-pass-as-object: true
251252
x-annotations:
252253
readOnlyHint: false
253254
description: |
254-
Adds a final review comment to a pull request, either approving it or requesting changes. This is the primary tool for summarizing the outcome of a PR review.
255-
255+
Adds a comment to a pull request or issue.
256256
The tool automatically adds a header indicating the AI agent source and prefixes the comment with a special icon.
257257
258258
**When to Use:**
259-
This tool is for providing a conclusive, final review on a pull request.
260259
260+
**For Pull Requests:**
261+
This is the primary tool for providing a conclusive, final review.
261262
- **To Approve a PR:** Use this tool to leave a comment indicating your approval (e.g., "LGTM, thanks for the contribution!").
262263
- **To Request Changes:** Use this tool to provide a clear, actionable list of changes required for the PR to be approved. This should be a summary of your findings after a detailed review.
263264
264-
**Note on Inline Comments:**
265-
While this tool is for the final, summary comment, detailed line-by-line feedback should be provided through other means if available. This tool provides the final verdict.
266-
tags: [Pull Requests]
267-
parameters:
268-
- name: pr_number
269-
in: path
270-
required: true
271-
description: The number of the pull request to comment on.
272-
schema:
273-
type: integer
274-
example: 123
265+
**For Issues:**
266+
Use this tool to add general comments, such as providing status updates, asking for clarification, or summarizing discussions.
267+
268+
**Note on Inline Comments (PRs):**
269+
While this tool is for the final, summary comment on PRs, detailed line-by-line feedback should be provided through other means if available. This tool provides the final verdict.
270+
tags: [Pull Requests, Issues]
275271
requestBody:
276272
required: true
277273
content:
@@ -282,6 +278,14 @@ paths:
282278
- body
283279
- agent
284280
properties:
281+
pr_number:
282+
type: integer
283+
description: The number of the pull request (if commenting on a PR).
284+
example: 123
285+
issue_number:
286+
type: integer
287+
description: The number of the issue (if commenting on an issue).
288+
example: 456
285289
body:
286290
type: string
287291
description: The raw, unformatted content of the comment. The tool will handle templating.
@@ -297,8 +301,14 @@ paths:
297301
application/json:
298302
schema:
299303
$ref: '#/components/schemas/SuccessResponse'
304+
'400':
305+
description: Bad Request (missing pr_number or issue_number).
306+
content:
307+
application/json:
308+
schema:
309+
$ref: '#/components/schemas/ErrorResponse'
300310
'404':
301-
description: Pull request not found.
311+
description: Resource not found.
302312
content:
303313
application/json:
304314
schema:

ai/mcp/server/github-workflow/services/IssueService.mjs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ import {exec} from 'child_process';
77
import {promisify} from 'util';
88
import {spawn} from 'child_process';
99
import {GET_ISSUE_AND_LABEL_IDS, GET_ISSUE_PARENT, GET_BLOCKED_BY, FETCH_ISSUES_FOR_SYNC} from './queries/issueQueries.mjs';
10-
import {ADD_LABELS, REMOVE_LABELS, ADD_SUB_ISSUE, REMOVE_SUB_ISSUE, ADD_BLOCKED_BY, REMOVE_BLOCKED_BY, GET_ISSUE_ID} from './queries/mutations.mjs';
10+
import {GET_PULL_REQUEST_ID} from './queries/pullRequestQueries.mjs';
11+
import {ADD_LABELS, REMOVE_LABELS, ADD_SUB_ISSUE, REMOVE_SUB_ISSUE, ADD_BLOCKED_BY, REMOVE_BLOCKED_BY, GET_ISSUE_ID, ADD_COMMENT} from './queries/mutations.mjs';
1112

1213
const execAsync = promisify(exec);
1314

15+
const AGENT_ICONS = {
16+
gemini : '✦',
17+
claude : '❋',
18+
gpt : '●',
19+
default: '◆'
20+
};
21+
1422
/**
1523
* Service for interacting with GitHub issues via the GraphQL API.
1624
* @class Neo.ai.mcp.server.github-workflow.services.IssueService
@@ -44,6 +52,95 @@ class IssueService extends Base {
4452
return this.writePermissions.includes(RepositoryService.viewerPermission);
4553
}
4654

55+
/**
56+
* Extracts the agent type from the agent string for icon selection.
57+
* @param {string} agent - The full agent identifier
58+
* @returns {string} The agent type key for AGENT_ICONS lookup
59+
*/
60+
getAgentType(agent) {
61+
const agentLower = agent.toLowerCase();
62+
63+
if (agentLower.includes('gemini')) return 'gemini';
64+
if (agentLower.includes('claude')) return 'claude';
65+
if (agentLower.includes('gpt')) return 'gpt';
66+
67+
return 'default';
68+
}
69+
70+
/**
71+
* Creates a comment on a specific issue or pull request.
72+
* @param {object} options
73+
* @param {number} [options.issue_number] - The number of the issue.
74+
* @param {number} [options.pr_number] - The number of the pull request.
75+
* @param {string} options.body - The raw content of the comment.
76+
* @param {string} options.agent - The identity of the calling agent.
77+
* @returns {Promise<object>} A promise that resolves to a success message.
78+
*/
79+
async createComment({issue_number, pr_number, body, agent}) {
80+
// Input Validation
81+
if (issue_number && pr_number) {
82+
return {
83+
error: "Bad Request",
84+
message: "Please provide either 'pr_number' or 'issue_number', not both.",
85+
code: "INVALID_ARGUMENTS"
86+
};
87+
}
88+
if (!issue_number && !pr_number) {
89+
return {
90+
error: "Bad Request",
91+
message: "Missing required argument: Please provide 'pr_number' or 'issue_number'.",
92+
code: "MISSING_ARGUMENTS"
93+
};
94+
}
95+
96+
const isPR = !!pr_number;
97+
const number = isPR ? pr_number : issue_number;
98+
const idVariables = {
99+
owner : aiConfig.owner,
100+
repo : aiConfig.repo,
101+
// The PR query uses 'prNumber', the Issue query uses 'number'
102+
[isPR ? 'prNumber' : 'number']: number
103+
};
104+
105+
// Agent Header Formatting
106+
const header = `**Input from ${agent}:**\n\n`;
107+
const agentIcon = AGENT_ICONS[this.getAgentType(agent)];
108+
const headingMatch = body.match(/^(#+\s*)(.*)$/);
109+
let processedBody;
110+
111+
if (headingMatch) {
112+
const headingMarkers = headingMatch[1];
113+
const headingContent = headingMatch[2];
114+
processedBody = `${headingMarkers}${agentIcon} ${headingContent}\n${body.substring(headingMatch[0].length)}`;
115+
} else {
116+
processedBody = `${agentIcon} ${body}`;
117+
}
118+
119+
const finalBody = `${header}${processedBody.split('\n').map(line => `> ${line}`).join('\n')}`;
120+
121+
try {
122+
// Divergent ID Lookup
123+
const query = isPR ? GET_PULL_REQUEST_ID : GET_ISSUE_ID;
124+
const idData = await GraphqlService.query(query, idVariables);
125+
126+
const subjectId = isPR
127+
? idData.repository.pullRequest.id
128+
: idData.repository.issue.id;
129+
130+
// Shared Mutation
131+
await GraphqlService.query(ADD_COMMENT, { subjectId, body: finalBody });
132+
return { message: `Successfully created comment on ${isPR ? 'PR' : 'issue'} #${number}` };
133+
134+
} catch (error) {
135+
logger.error(`Error creating comment on ${isPR ? 'PR' : 'issue'} #${number} via GraphQL:`, error);
136+
return {
137+
error : 'GraphQL API request failed',
138+
message: error.message,
139+
code : 'GRAPHQL_API_ERROR'
140+
};
141+
}
142+
}
143+
47144
/**
48145
* Assigns one or more users to a GitHub issue, or clears all assignees.
49146
* This method first verifies that the user has the required permissions (`WRITE`, `MAINTAIN`, or `ADMIN`)

ai/mcp/server/github-workflow/services/PullRequestService.mjs

Lines changed: 8 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import {exec} from 'child_process';
2-
import {promisify} from 'util';
3-
import Base from '../../../../../src/core/Base.mjs';
4-
import GraphqlService from './GraphqlService.mjs';
5-
import aiConfig from '../config.mjs';
6-
import logger from '../logger.mjs';
7-
import {ADD_COMMENT, FETCH_PULL_REQUESTS, GET_CONVERSATION, GET_PULL_REQUEST_ID, UPDATE_COMMENT} from './queries/pullRequestQueries.mjs';
1+
import {exec} from 'child_process';
2+
import {promisify} from 'util';
3+
import Base from '../../../../../src/core/Base.mjs';
4+
import GraphqlService from './GraphqlService.mjs';
5+
import aiConfig from '../config.mjs';
6+
import logger from '../logger.mjs';
7+
import {FETCH_PULL_REQUESTS, GET_CONVERSATION} from './queries/pullRequestQueries.mjs';
8+
import {UPDATE_COMMENT} from './queries/mutations.mjs';
89

910
const execAsync = promisify(exec);
1011

11-
const AGENT_ICONS = {
12-
gemini : '✦',
13-
claude : '❋',
14-
gpt : '●',
15-
default: '◆'
16-
};
17-
1812
/**
1913
* Service for interacting with GitHub Pull Requests via the `gh` CLI and GraphQL API.
2014
* @class Neo.ai.mcp.server.github-workflow.services.PullRequestService
@@ -88,21 +82,6 @@ class PullRequestService extends Base {
8882
}
8983
}
9084

91-
/**
92-
* Extracts the agent type from the agent string for icon selection.
93-
* @param {string} agent - The full agent identifier (e.g., "Gemini 2.5 Pro", "Claude Sonnet 4.5", "Chat GPT 4")
94-
* @returns {string} The agent type key for AGENT_ICONS lookup
95-
*/
96-
getAgentType(agent) {
97-
const agentLower = agent.toLowerCase();
98-
99-
if (agentLower.includes('gemini')) return 'gemini';
100-
if (agentLower.includes('claude')) return 'claude';
101-
if (agentLower.includes('gpt')) return 'gpt';
102-
103-
return 'default';
104-
}
105-
10685
/**
10786
* Gets the diff for a specific pull request.
10887
* @param {number} prNumber - The number of the pull request.
@@ -122,55 +101,6 @@ class PullRequestService extends Base {
122101
}
123102
}
124103

125-
/**
126-
* Creates a comment on a specific pull request.
127-
* @param {number} prNumber - The number of the pull request.
128-
* @param {string} body - The raw content of the comment. The agent should provide
129-
* any desired markdown formatting (e.g., blockquotes, code blocks).
130-
* This tool will only add the agent header and icon prefix.
131-
* @param {string} agent - The identity of the calling agent (e.g., "Gemini 2.5 Pro", "Claude Sonnet 4.5").
132-
* Adds a formatted header with the appropriate icon.
133-
* @returns {Promise<object>} A promise that resolves to a success message or a structured error.
134-
*/
135-
async createComment(prNumber, body, agent) {
136-
const idVariables = {
137-
owner : aiConfig.owner,
138-
repo : aiConfig.repo,
139-
prNumber
140-
};
141-
142-
const header = `**Input from ${agent}:**\n\n`;
143-
const agentIcon = AGENT_ICONS[this.getAgentType(agent)];
144-
const headingMatch = body.match(/^(#+\s*)(.*)$/);
145-
let processedBody;
146-
147-
if (headingMatch) {
148-
const headingMarkers = headingMatch[1];
149-
const headingContent = headingMatch[2];
150-
processedBody = `${headingMarkers}${agentIcon} ${headingContent}\n${body.substring(headingMatch[0].length)}`;
151-
} else {
152-
processedBody = `${agentIcon} ${body}`;
153-
}
154-
155-
const finalBody = `${header}${processedBody.split('\n').map(line => `> ${line}`).join('\n')}`;
156-
157-
try {
158-
const idData = await GraphqlService.query(GET_PULL_REQUEST_ID, idVariables);
159-
const subjectId = idData.repository.pullRequest.id;
160-
161-
await GraphqlService.query(ADD_COMMENT, { subjectId, body: finalBody });
162-
return { message: `Successfully created comment on PR #${prNumber}` };
163-
164-
} catch (error) {
165-
logger.error(`Error creating comment on PR #${prNumber} via GraphQL:`, error);
166-
return {
167-
error : 'GraphQL API request failed',
168-
message: error.message,
169-
code : 'GRAPHQL_API_ERROR'
170-
};
171-
}
172-
}
173-
174104
/**
175105
* Gets the full conversation for a specific pull request.
176106
* @param {number} prNumber - The number of the pull request.

ai/mcp/server/github-workflow/services/queries/mutations.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,42 @@ export const REMOVE_BLOCKED_BY = `
221221
}
222222
}
223223
`;
224+
225+
/**
226+
* Mutation to add a comment to a subject (issue or PR).
227+
*
228+
* Variables required:
229+
* - $subjectId: ID! - The global ID of the issue or PR
230+
* - $body: String! - The comment body
231+
*/
232+
export const ADD_COMMENT = `
233+
mutation AddComment($subjectId: ID!, $body: String!) {
234+
addComment(input: {subjectId: $subjectId, body: $body}) {
235+
commentEdge {
236+
node {
237+
id
238+
url
239+
}
240+
}
241+
}
242+
}
243+
`;
244+
245+
/**
246+
* Mutation to update an existing comment.
247+
*
248+
* Variables required:
249+
* - $commentId: ID! - The global ID of the comment to update
250+
* - $body: String! - The new comment body
251+
*/
252+
export const UPDATE_COMMENT = `
253+
mutation UpdateComment($commentId: ID!, $body: String!) {
254+
updateIssueComment(input: {id: $commentId, body: $body}) {
255+
issueComment {
256+
id
257+
url
258+
updatedAt
259+
}
260+
}
261+
}
262+
`;

ai/mcp/server/github-workflow/services/queries/pullRequestQueries.mjs

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,45 +31,6 @@ export const FETCH_PULL_REQUESTS = `
3131
}
3232
`;
3333

34-
/**
35-
* Mutation to add a comment to a pull request.
36-
*
37-
* Variables required:
38-
* - $subjectId: ID! - The global ID of the pull request
39-
* - $body: String! - The comment body
40-
*/
41-
export const ADD_COMMENT = `
42-
mutation AddComment($subjectId: ID!, $body: String!) {
43-
addComment(input: {subjectId: $subjectId, body: $body}) {
44-
commentEdge {
45-
node {
46-
id
47-
url
48-
}
49-
}
50-
}
51-
}
52-
`;
53-
54-
/**
55-
* Mutation to update an existing comment.
56-
*
57-
* Variables required:
58-
* - $commentId: ID! - The global ID of the comment to update
59-
* - $body: String! - The new comment body
60-
*/
61-
export const UPDATE_COMMENT = `
62-
mutation UpdateComment($commentId: ID!, $body: String!) {
63-
updateIssueComment(input: {id: $commentId, body: $body}) {
64-
issueComment {
65-
id
66-
url
67-
updatedAt
68-
}
69-
}
70-
}
71-
`;
72-
7334
/**
7435
* Query to get the global ID of a pull request.
7536
*

ai/mcp/server/github-workflow/services/toolService.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const serviceMapping = {
1717
add_labels : IssueService .addLabels .bind(IssueService),
1818
assign_issue : IssueService .assignIssue .bind(IssueService),
1919
checkout_pull_request : PullRequestService.checkoutPullRequest .bind(PullRequestService),
20-
create_comment : PullRequestService.createComment .bind(PullRequestService),
20+
create_comment : IssueService .createComment .bind(IssueService),
2121
create_issue : IssueService .createIssue .bind(IssueService),
2222
get_conversation : PullRequestService.getConversation .bind(PullRequestService),
2323
get_local_issue_by_id : LocalFileService .getIssueById .bind(LocalFileService),

0 commit comments

Comments
 (0)