import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { getOctokit, runGit } from './helpers/github.js'
import { CreateIssuePrompt } from './helpers/prompt.js'
const server = new McpServer({
name: 'github-issue-from-diff',
version: '0.1.0',
title: 'Create GitHub Issue from Diff by Gustavo Detoni',
})
server.registerTool(
'git_diff',
{
title: 'Get Git diff',
description:
'Retorna um diff do repositório atual. Use para entender alterações locais.',
inputSchema: {
mode: z.enum(['working', 'staged', 'range']).default('working'),
base: z.string().optional(),
head: z.string().optional(),
pathSpec: z.array(z.string()).optional(),
maxBytes: z
.number()
.int()
.positive()
.max(5 * 1024 * 1024)
.optional(),
},
},
async ({ mode = 'working', base, head, pathSpec, maxBytes = 200_000 }) => {
let args: string[] = ['diff', '-U3']
if (mode === 'staged') {
args = ['diff', '--staged', '-U3']
}
if (mode === 'range' && (!base || !head)) {
throw new Error('For mode=range informe base e head')
}
if (mode === 'range') {
args = ['diff', `${base}...${head}`, '-U3']
}
if (pathSpec && pathSpec.length) {
args.push('--', ...pathSpec)
}
const nameStatus = await runGit(
mode === 'range'
? ['diff', '--name-status', `${base}...${head}`]
: mode === 'staged'
? ['diff', '--staged', '--name-status']
: ['diff', '--name-status']
)
const diff = await runGit(args)
const truncated =
diff.length > maxBytes
? diff.slice(0, maxBytes) + '\n\n[...truncated...]'
: diff
const files = nameStatus
.split('\n')
.filter(Boolean)
.map((line) => {
const [status, ...rest] = line.split(/\s+/)
return { status, file: rest.join(' ') }
})
return {
content: [
{
type: 'text',
text: `Changed files:\n${files
.map((f) => `${f.status}\t${f.file}`)
.join('\n')}`,
},
{ type: 'text', text: '\n--- DIFF START ---\n' + truncated },
],
}
}
)
server.registerPrompt(
'draft_issue_from_diff',
{
title: 'Draft GitHub issue from git diff',
description: 'Converte um diff em um rascunho de issue',
argsSchema: {
diff: z.string(),
repoSlug: z.string().describe('owner/repo'),
guidance: z.string().optional(),
},
},
({ diff, repoSlug, guidance }) => CreateIssuePrompt(diff, repoSlug, guidance)
)
server.registerTool(
'create_github_issue',
{
title: 'Create GitHub Issue',
description: 'Cria uma issue no GitHub usando Octokit',
inputSchema: {
owner: z.string().default(process.env.DEFAULT_OWNER || ''),
repo: z.string().default(process.env.DEFAULT_REPO || ''),
title: z.string(),
body: z.string().default(''),
labels: z.array(z.string()).default([]),
assignees: z
.array(z.string())
.default(
process.env.DEFAULT_ASSIGNEE ? [process.env.DEFAULT_ASSIGNEE] : []
),
},
},
async ({ owner, repo, title, body, labels, assignees }) => {
const octokit = getOctokit()
const res = await octokit.request('POST /repos/{owner}/{repo}/issues', {
owner,
repo,
title,
body,
labels,
assignees,
})
const url = res.data.html_url
return {
content: [{ type: 'text', text: `Issue criada: #${url}` }],
}
}
)
const transport = new StdioServerTransport()
await server.connect(transport)