Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 180 additions & 82 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"homepage": "https://github.com/JesusMaster/github-see-mcp-server#readme",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"@modelcontextprotocol/sdk": "^1.23.0",
"axios": "^1.9.0",
"cors": "^2.8.5",
"dompurify": "^3.2.6",
Expand All @@ -49,7 +49,7 @@
"http-terminator": "^3.2.0",
"jsdom": "^26.1.0",
"raw-body": "^3.0.0",
"zod": "^3.24.4"
"zod": "^3.25.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
Expand Down
1 change: 0 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ function findAndLoadToken(): string | undefined {
logger.info('GitHub token loaded successfully.');
return token;
}

logger.warn('WARNING: No GitHub token found. API requests may be rate limited or fail.');
return undefined;
}
Expand Down
172 changes: 94 additions & 78 deletions src/features/issues/issues.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,152 +3,168 @@ import { z } from 'zod';
import Issues from "#features/issues/issues.service";

export function registerIssueTools(server: McpServer, issuesInstance: Issues) {
server.tool(


server.registerTool(
'get_issue',
'Gets the contents of an issue within a repository',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
description: 'Gets the contents of an issue within a repository',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
}
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.getIssues(args);
let info = await issuesInstance.getIssues(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'get_issue_comments',
'Get comments for a GitHub issue',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
description: 'Get comments for a GitHub issue',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.getComments(args);
let info = await issuesInstance.getComments(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'create_issue',
'Create a new issue in a GitHub repository',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
title: z.string().describe('Issue title (string, required)'),
body: z.string().optional().describe('Issue body (string, optional)'),
assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'),
labels: z.array(z.string()).optional().describe('Labels to apply to this issue (string[], optional)'),
milestone: z.number().optional().describe('ID of the milestone to associate this issue with (number, optional)'),
description: 'Create a new issue in a GitHub repository',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
title: z.string().describe('Issue title (string, required)'),
body: z.string().optional().describe('Issue body (string, optional)'),
assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'),
labels: z.array(z.string()).optional().describe('Labels to apply to this issue (string[], optional)'),
milestone: z.number().optional().describe('ID of the milestone to associate this issue with (number, optional)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.createIssue(args);
let info = await issuesInstance.createIssue(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'add_issue_comment',
'Add a comment to an issue',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
comment: z.string().describe('Comment text (string, required)'),
description: 'Add a comment to an issue',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
comment: z.string().describe('Comment text (string, required)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.addComment(args);
let info = await issuesInstance.addComment(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'list_issues',
'List and filter repository issues',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
state: z.enum(['open', 'closed','all']).optional().describe("Filter by state ('open', 'closed', 'all') (string, optional)"),
labels: z.array(z.string()).optional().describe('Labels to filter by (string[], optional)'),
sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"),
direction: z.enum(['asc', 'desc']).optional().describe("Sort direction ('asc', 'desc') (string, optional)"),
since: z.string().optional().describe('Filter by date (ISO 8601 timestamp) (string, optional)'),
page: z.number().optional().describe('Page number (number, optional)'),
per_page: z.number().optional().describe('Results per page (number, optional)'),
description: 'List and filter repository issues',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
state: z.enum(['open', 'closed', 'all']).optional().describe("Filter by state ('open', 'closed', 'all') (string, optional)"),
labels: z.array(z.string()).optional().describe('Labels to filter by (string[], optional)'),
sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"),
direction: z.enum(['asc', 'desc']).optional().describe("Sort direction ('asc', 'desc') (string, optional)"),
since: z.string().optional().describe('Filter by date (ISO 8601 timestamp) (string, optional)'),
page: z.number().optional().describe('Page number (number, optional)'),
per_page: z.number().optional().describe('Results per page (number, optional)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.listIssues(args);
let info = await issuesInstance.listIssues(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
}
catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'update_issue',
'Update an issue in a GitHub repository',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
title: z.string().optional().describe('New issue title (string, optional)'),
body: z.string().optional().describe('New issue body (string, optional)'),
assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'),
state: z.enum(['open', 'closed']).optional().describe("New issue state ('open', 'closed') (string, optional)"),
milestone: z.number().optional().describe('New milestone ID (number, optional)'),
labels: z.array(z.string()).optional().describe('New labels (string[], optional)'),
description: 'Update an issue in a GitHub repository',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
issueNumber: z.number().describe('Issue number (number, required)'),
title: z.string().optional().describe('New issue title (string, optional)'),
body: z.string().optional().describe('New issue body (string, optional)'),
assignees: z.array(z.string()).optional().describe('Usernames to assign to this issue (string[], optional)'),
state: z.enum(['open', 'closed']).optional().describe("New issue state ('open', 'closed') (string, optional)"),
milestone: z.number().optional().describe('New milestone ID (number, optional)'),
labels: z.array(z.string()).optional().describe('New labels (string[], optional)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.updateIssue(args);
let info = await issuesInstance.updateIssue(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
server.tool(

server.registerTool(
'search_issues',
'Search for issues and pull requests',
{
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
q: z.string().describe('Search query (string, required)'),
sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"),
order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"),
page: z.number().optional().describe('Page number (number, optional)'),
per_page: z.number().optional().describe('Results per page (number, optional)'),
fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'),
description: 'Search for issues and pull requests',
inputSchema: {
owner: z.string().describe('Repository owner (string, required)'),
repo: z.string().describe('Repository name (string, required)'),
q: z.string().describe('Search query (string, required)'),
sort: z.enum(['created', 'updated', 'comments']).optional().describe("Sort by ('created', 'updated', 'comments') (string, optional)"),
order: z.enum(['asc', 'desc']).optional().describe("Sort order ('asc', 'desc') (string, optional)"),
page: z.number().optional().describe('Page number (number, optional)'),
per_page: z.number().optional().describe('Results per page (number, optional)'),
fields: z.array(z.string()).optional().describe('Fields to return (string[], optional)'),
},
},
async (args) => {
async (args, req: any) => {
try {
let info = await issuesInstance.searchIssues(args);
let info = await issuesInstance.searchIssues(args, req.requestInfo.headers.github_token);
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
} catch (error: any) {
return { content: [{ type: 'text', text: `Error : ${error.message}` }], isError: true };
}
}
);
}
}
30 changes: 15 additions & 15 deletions src/features/issues/issues.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,32 @@ export interface SearchIssuesOptions { owner: string; repo: string; q: string; s

class Issues extends GitHubClient {

async getIssues(options: GetIssuesOptions) {
async getIssues(options: GetIssuesOptions, token: string) {
const { owner, repo, issueNumber } = options;
return this.get(`repos/${owner}/${repo}/issues/${issueNumber}`);
return this.get(`repos/${owner}/${repo}/issues/${issueNumber}`, {}, token);
}

async getComments(options: GetCommentsOptions) {
async getComments(options: GetCommentsOptions, token: string) {
const { owner, repo, issueNumber } = options;
return this.get(`repos/${owner}/${repo}/issues/${issueNumber}/comments`);
return this.get(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, {}, token);
}

async createIssue(options: CreateIssueOptions) {
async createIssue(options: CreateIssueOptions, token: string) {
const { owner, repo, ...payload } = options;
if (payload.title) payload.title = sanitize(payload.title);
if (payload.body) payload.body = sanitize(payload.body);
return this.post(`repos/${owner}/${repo}/issues`, payload);
return this.post(`repos/${owner}/${repo}/issues`, payload, token);
}

async addComment(options: AddCommentOptions) {
async addComment(options: AddCommentOptions, token: string) {
const { owner, repo, issueNumber, comment } = options;
const payload = { body: sanitize(comment) };
return this.post(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, payload);
return this.post(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, payload, token);
}

async listIssues(options: ListIssuesOptions) {
async listIssues(options: ListIssuesOptions, token: string) {
const { owner, repo, fields, ...params } = options;
const results = await this.get(`repos/${owner}/${repo}/issues`, { per_page: 5, ...params });
const results = await this.get(`repos/${owner}/${repo}/issues`, { per_page: 5, ...params }, token);

if (fields?.length) {
return (results as any[]).map((item: any) => {
Expand All @@ -54,17 +54,17 @@ class Issues extends GitHubClient {
return results;
}

async updateIssue(options: UpdateIssueOptions) {
async updateIssue(options: UpdateIssueOptions, token: string) {
const { owner, repo, issueNumber, ...payload } = options;
if (payload.title) payload.title = sanitize(payload.title);
if (payload.body) payload.body = sanitize(payload.body);
return this.patch(`repos/${owner}/${repo}/issues/${issueNumber}`, payload);
return this.patch(`repos/${owner}/${repo}/issues/${issueNumber}`, payload, token);
}

async searchIssues(options: SearchIssuesOptions) {
async searchIssues(options: SearchIssuesOptions, token: string) {
const { owner, repo, fields, q, ...params } = options;
const payload = { q: sanitize(q), per_page: 5, ...params };
const results: any = await this.get('search/issues', payload);
const results: any = await this.get('search/issues', payload, token);

if (fields?.length && results.items) {
results.items = results.items.map((item: any) => {
Expand All @@ -81,4 +81,4 @@ class Issues extends GitHubClient {
}
}

export default Issues;
export default Issues;
Loading
Loading