MCP (Model Context Protocol) AI agent external tools-пен қалай сөйлесетінін анықтайды. Бұл Copilot-ты өз workflows арқылы кеңейтудің easy way.
Бұл post үшін мен locally run болатын MCP server жаздым. Example TypeScript-те, .NET support preview ішінде бар, ал келесі samples-ты C#-та жазуды жоспарлап отырмын. Ол екі Git branches салыстырады және GitHub Copilot ішінде early code review іске қосуға көмектеседі.
Неге бұл маңызды
- Өз commands, rules және data sources қосыңыз.
- Employers already expect ететін AI-ready skills құрыңыз.
- Familiar Copilot interface ішінен жұмыс істеңіз.
Clone the repo
GitHub - sigmade/Git-MCP-Server
MCP server core Git operations gitUtils.ts file ішінде өңделеді.
import { execSync } from 'child_process';
export interface MergeInfo {
sourceBranch: string;
targetBranch: string;
filesChanged: string[];
insertions: number;
deletions: number;
commits: number;
summary: string;
}
export class GitUtils {
static executeGit(command: string, repoPath: string): string {
try {
return execSync(command, { cwd: repoPath, encoding: 'utf8' }).toString().trim();
} catch (error) {
throw new Error(`Git error: ${(error as Error).message}`);
}
}
static getCurrentBranch(repoPath: string): string {
return this.executeGit('git branch --show-current', repoPath);
}
static getMainBranch(repoPath: string): string {
try {
this.executeGit('git show-ref --verify refs/heads/master', repoPath);
return 'master';
} catch {
return 'main';
}
}
static getMergeInfo(repoPath: string, fromBranch: string, toBranch: string): MergeInfo {
const filesOutput = this.executeGit(
`git diff --name-only ${fromBranch}..${toBranch}`,
repoPath
);
const filesChanged = filesOutput ? filesOutput.split('\n').filter(f => f.trim()) : [];
const statsOutput = this.executeGit(
`git diff --numstat ${fromBranch}..${toBranch}`,
repoPath
);
let insertions = 0;
let deletions = 0;
if (statsOutput) {
statsOutput.split('\n').forEach(line => {
const parts = line.split('\t');
if (parts.length >= 2) {
insertions += parseInt(parts[0]) || 0;
deletions += parseInt(parts[1]) || 0;
}
});
}
const commitsOutput = this.executeGit(
`git rev-list --count ${fromBranch}..${toBranch}`,
repoPath
);
const commits = parseInt(commitsOutput) || 0;
const summary = commits === 0
? 'No new commits for merge'
: `${commits} commits, ${filesChanged.length} files, +${insertions}/-${deletions} lines`;
return {
sourceBranch: fromBranch,
targetBranch: toBranch,
filesChanged,
insertions,
deletions,
commits,
summary,
};
}
static getQuickStats(repoPath: string, baseBranch: string, currentBranch: string) {
if (baseBranch === currentBranch) {
return {
message: 'Already on the base branch',
needsMerge: false,
};
}
try {
const ahead = this.executeGit(
`git rev-list --count ${baseBranch}..${currentBranch}`,
repoPath
);
const behind = this.executeGit(
`git rev-list --count ${currentBranch}..${baseBranch}`,
repoPath
);
const aheadCount = parseInt(ahead) || 0;
const behindCount = parseInt(behind) || 0;
let message = '';
if (aheadCount === 0 && behindCount === 0) {
message = 'Branches are synchronized';
} else if (aheadCount > 0 && behindCount === 0) {
message = `Ahead by ${aheadCount} commits`;
} else if (aheadCount === 0 && behindCount > 0) {
message = `Behind by ${behindCount} commits`;
} else {
message = `Ahead by ${aheadCount}, behind by ${behindCount} commits`;
}
return {
message,
aheadBy: aheadCount,
behindBy: behindCount,
needsMerge: aheadCount > 0 || behindCount > 0,
};
} catch (error) {
return {
message: 'Failed to determine status',
error: (error as Error).message,
};
}
}
static getFileDiff(repoPath: string, filename: string, fromBranch: string, toBranch: string): string {
try {
return this.executeGit(
`git diff ${fromBranch}..${toBranch} -- "${filename}"`,
repoPath
);
} catch (error) {
throw new Error(`Failed to get diff for file ${filename}: ${(error as Error).message}`);
}
}
}
MCP server entry point және tool handlers index.ts ішінде орналасқан.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs';
import { GitUtils } from './gitUtils.js';
class SimpleMergeReviewMCP {
private server: Server;
constructor() {
this.server = new Server({
name: 'simple-merge-review',
version: '1.0.0',
});
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'show_merge_diff',
description: 'Show changes between branches before merge',
inputSchema: {
type: 'object',
properties: {
repoPath: {
type: 'string',
description: 'Path to the git repository',
},
fromBranch: {
type: 'string',
description: 'Branch to merge from (default: main)',
default: 'main',
},
toBranch: {
type: 'string',
description: 'Branch to merge into (default: current)',
},
},
required: ['repoPath'],
},
},
{
name: 'quick_merge_summary',
description: 'Quick summary of changes for merge',
inputSchema: {
type: 'object',
properties: {
repoPath: {
type: 'string',
description: 'Path to the git repository',
},
branch: {
type: 'string',
description: 'Branch to analyze (default: current)',
},
},
required: ['repoPath'],
},
},
{
name: 'show_file_diff',
description: 'Show specific changes in a file between branches',
inputSchema: {
type: 'object',
properties: {
repoPath: {
type: 'string',
description: 'Path to the git repository',
},
filename: {
type: 'string',
description: 'Path to the file relative to the repository root',
},
fromBranch: {
type: 'string',
description: 'Branch to compare from (default: main/master)',
},
toBranch: {
type: 'string',
description: 'Branch to compare to (default: current)',
},
},
required: ['repoPath', 'filename'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'show_merge_diff':
return await this.showMergeDiff(args);
case 'quick_merge_summary':
return await this.quickMergeSummary(args);
case 'show_file_diff':
return await this.showFileDiff(args);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
throw new McpError(ErrorCode.InternalError, `Error: ${(error as Error).message}`);
}
});
}
private async showMergeDiff(args: any) {
const { repoPath, fromBranch = 'main', toBranch } = args;
if (!fs.existsSync(repoPath)) {
throw new Error(`Repository not found: ${repoPath}`);
}
const currentBranch = toBranch || GitUtils.getCurrentBranch(repoPath);
const mergeInfo = GitUtils.getMergeInfo(repoPath, fromBranch, currentBranch);
return {
content: [
{
type: 'text',
text: JSON.stringify(mergeInfo, null, 2),
},
],
};
}
private async quickMergeSummary(args: any) {
const { repoPath, branch } = args;
if (!fs.existsSync(repoPath)) {
throw new Error(`Repository not found: ${repoPath}`);
}
const currentBranch = branch || GitUtils.getCurrentBranch(repoPath);
const mainBranch = GitUtils.getMainBranch(repoPath);
// Simple summary of changes
const summary = {
currentBranch,
baseBranch: mainBranch,
...GitUtils.getQuickStats(repoPath, mainBranch, currentBranch),
};
return {
content: [
{
type: 'text',
text: JSON.stringify(summary, null, 2),
},
],
};
}
private async showFileDiff(args: any) {
const { repoPath, filename, fromBranch, toBranch } = args;
if (!fs.existsSync(repoPath)) {
throw new Error(`Repository not found: ${repoPath}`);
}
// Determine branches
const sourceBranch = fromBranch || GitUtils.getMainBranch(repoPath);
const targetBranch = toBranch || GitUtils.getCurrentBranch(repoPath);
// Get diff for the specific file
const diffOutput = GitUtils.getFileDiff(repoPath, filename, sourceBranch, targetBranch);
// Parse diff for more readable output
const parsedDiff = this.parseFileDiff(diffOutput);
return {
content: [
{
type: 'text',
text: JSON.stringify({
filename,
fromBranch: sourceBranch,
toBranch: targetBranch,
...parsedDiff,
rawDiff: diffOutput.split('\n').slice(0, 100), // Limit the number of lines
}, null, 2),
},
],
};
}
private parseFileDiff(diffOutput: string): { hasChanges: boolean; additions: string[]; deletions: string[]; summary: string } {
const lines = diffOutput.split('\n');
const additions: string[] = [];
const deletions: string[] = [];
let addedLines = 0;
let deletedLines = 0;
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions.push(line.substring(1)); // Remove the + sign
addedLines++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions.push(line.substring(1)); // Remove the - sign
deletedLines++;
}
}
const hasChanges = addedLines > 0 || deletedLines > 0;
const summary = hasChanges
? `+${addedLines} lines, -${deletedLines} lines`
: 'No changes in the file';
return {
hasChanges,
additions: additions.slice(0, 50), // Limit output
deletions: deletions.slice(0, 50),
summary,
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Simple Merge Review MCP running');
}
}
// Start
const server = new SimpleMergeReviewMCP();
server.run().catch(console.error);
Енді project build жасап, run ету керек.
npm install
npm run build
npm run start
Бұл build/index.js files жасайды.

C:\\Users\\YourUserName\\AppData\\Roaming\\Code\\User ішіне mcp.json қосып, server-ге point етіңіз.
index.js file path көрсету керек.

VS Code ішінде екі branches бар project ашыңыз. Менің жағдайда олар master және new-feature.
Кейін Copilot сіздің MCP-ді detected еткеніне көз жеткізіңіз. Ол үшін Agent mode-қа switch жасап, Tools button басыңыз және list ішінен MCP табыңыз.

Prompt жіберіңіз:
A PR from new-feature to master will be created. Do an early code review
Көріп тұрғанымыздай, Copilot біздің MCP functions таныды және chat ішінде олармен interaction log көрсетеді.

Соңында code changes бойынша review аламыз.

MCP кез келген developer-ге scripts, APIs немесе internal services-ті AI agent-ке қосып, Copilot ішінде daily work automation үшін қолдануға мүмкіндік береді.