GitHub Kanban MCP Server
- src
- handlers
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { IssueArgs, CreateIssueArgs, UpdateIssueArgs, ToolResponse } from '../types.js';
import { execAsync, writeToTempFile, removeTempFile } from '../utils/exec.js';
import { getExistingLabels, createLabel } from './label-handlers.js';
import { getRepoInfoFromGitConfig } from '../utils/repo-info.js';
/**
* リポジトリ情報を取得する
*/
async function getRepoInfo(args: { path: string }): Promise<{ owner: string; repo: string }> {
if (!args.path) {
throw new McpError(
ErrorCode.InvalidParams,
'リポジトリのパスを指定してください。'
);
}
return await getRepoInfoFromGitConfig(args.path);
}
/**
* Issue一覧を取得する
*/
export async function handleListIssues(args: IssueArgs): Promise<ToolResponse> {
const { owner, repo } = await getRepoInfo(args);
const stateFlag = args.state ? `--state ${args.state}` : '';
const labelsFlag = args.labels?.length ? `--label ${args.labels.join(',')}` : '';
const { stdout } = await execAsync(
`gh issue list --repo ${owner}/${repo} ${stateFlag} ${labelsFlag} --json number,title,state,labels,assignees,createdAt,updatedAt`
);
return {
content: [
{
type: 'text',
text: stdout,
},
],
};
}
/**
* 新しいIssueを作成する
*/
export async function handleCreateIssue(args: CreateIssueArgs): Promise<ToolResponse> {
const { owner, repo } = await getRepoInfo(args);
const assigneesFlag = args.assignees?.length ? `--assignee ${args.assignees.join(',')}` : '';
const tempFile = 'issue_body.md';
let bodyFlag = '';
try {
// ラベルの存在確認と作成
if (args.labels?.length) {
const existingLabels = await getExistingLabels(args.path);
for (const label of args.labels) {
if (!existingLabels.includes(label)) {
await createLabel(args.path, label);
}
}
}
const labelsFlag = args.labels?.length ? `--label ${args.labels.join(',')}` : '';
if (args.body) {
const fullPath = await writeToTempFile(args.body, tempFile);
bodyFlag = `--body-file "${fullPath}"`;
}
// タイトルに絵文字を付与(指定がある場合)
const titleWithEmoji = args.emoji ? `${args.emoji} ${args.title}` : args.title;
const { stdout } = await execAsync(
`gh issue create --repo ${owner}/${repo} --title "${titleWithEmoji}" ${bodyFlag} ${labelsFlag} ${assigneesFlag}`
);
// URLから issue number を抽出
const issueUrl = stdout.trim();
const issueNumber = issueUrl.split('/').pop();
// 作成したissueの詳細情報を取得
const { stdout: issueData } = await execAsync(
`gh issue view ${issueNumber} --repo ${owner}/${repo} --json number,title,url`
);
return {
content: [
{
type: 'text',
text: issueData,
},
],
};
} finally {
if (args.body) {
await removeTempFile(tempFile);
}
}
}
/**
* 既存のIssueを更新する
*/
export async function handleUpdateIssue(args: UpdateIssueArgs): Promise<ToolResponse> {
const { owner, repo } = await getRepoInfo(args);
// タイトルが更新される場合は絵文字を付与(指定がある場合)
const titleFlag = args.title ? `--title "${args.emoji ? `${args.emoji} ${args.title}` : args.title}"` : '';
const labelsFlag = args.labels?.length ? `--add-label ${args.labels.join(',')}` : '';
const assigneesFlag = args.assignees?.length ? `--add-assignee ${args.assignees.join(',')}` : '';
const tempFile = 'update_body.md';
let bodyFlag = '';
try {
// 状態の更新を処理
if (args.state) {
const command = args.state === 'closed' ? 'close' : 'reopen';
await execAsync(
`gh issue ${command} ${args.issue_number} --repo ${owner}/${repo}`
);
}
// その他の更新を処理
if (args.title || args.body || args.labels?.length || args.assignees?.length) {
if (args.body) {
const fullPath = await writeToTempFile(args.body, tempFile);
bodyFlag = `--body-file "${fullPath}"`;
}
await execAsync(
`gh issue edit ${args.issue_number} --repo ${owner}/${repo} ${titleFlag} ${bodyFlag} ${labelsFlag} ${assigneesFlag}`
);
}
const { stdout: issueData } = await execAsync(
`gh issue view ${args.issue_number} --repo ${owner}/${repo} --json number,title,state,url`
);
return {
content: [
{
type: 'text',
text: issueData,
},
],
};
} finally {
if (args.body) {
await removeTempFile(tempFile);
}
}
}