Copilot'u Genişletmek: MCP Server'lar, Custom Agent'lar, Agent Skills ve Hooks
Copilot'ta bir kuralı talimata değil hook'a yazmak gerekir: Bookstore'da agent'a açıkça 'dotnet ef database update çalıştır' dedik, model komutu çağırdı ve onu yalnız preToolUse hook'u durdurdu. Bookstore MCP server'ı ve .mcp.json, prompt dosyalarından skill'lere geçiş, migration'ları kontrol eden skill ve custom agent, Copilot CLI'da yerel modelle gerçek koşular.
Sürüm notu: Bu yazıdaki kodlar .NET 10, ModelContextProtocol 2.4.0 ve Entity Framework Core 10.0.15 ile derlenip denenmiştir; MCP (Model Context Protocol; agent'ın dış araçlara bağlandığı açık protokol) server'ı, skill script'i ve hook script'leri (bash ve PowerShell 7) gerçekten çalıştırılmıştır. Copilot CLI 1.0.112 GitHub hesabı olmadan, COPILOT_OFFLINE modunda yerel bir Ollama modeliyle (qwen3:4b) çalıştırılmıştır; bu koşular Copilot'un kendi modellerini değil, CLI'ın MCP, skill, custom agent ve hook mekaniğini gösterir. Visual Studio 2026 (18.12), Visual Studio Code (VS Code) 1.146, cloud agent ve code review davranışı 15 Kasım 2026'da docs.github.com, code.visualstudio.com ve Microsoft Learn'den alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için Copilot'a bir kural koymak istiyorsun: agent hiçbir zaman dotnet ef database update çalıştırmasın. Dördüncü yazıda bunu add-migration prompt dosyasına yazmıştık. Şimdi aynı kuralı bir hook script'ine de koyuyoruz ve agent'a açıkça "şu komutu çalıştır" diyoruz.
Tahminin ne? Yazılı kural mı kazanır, kullanıcının açık isteği mi? Hook varken komut terminale ulaşır mı? Agent komutun neden çalışmadığını öğrenir mi? Cevabı bu yazının sonunda Copilot CLI'ın kendi olay kayıtlarında göreceğiz.
Dört Genişletme Noktası
Dördüncü yazıdaki talimat dosyaları Copilot'a projeyi anlatıyordu. Bu yazıdaki dört mekanizma ise ona yeni yetenek ve sınır ekliyor:
| Mekanizma | Ne ekler | Bookstore'daki dosya | Ne zaman devreye girer |
|---|---|---|---|
| MCP server | Yeni tool'lar (API, veritabanı, tarayıcı) | .mcp.json | Model tool'u seçtiğinde |
| Agent skill | Gerektiğinde yüklenen talimat, script ve dosyalar | .github/skills/*/SKILL.md | İstek skill'in açıklamasına uyduğunda ya da /skill-name ile |
| Custom agent | Kendi talimatı ve tool listesi olan bir rol | .github/agents/*.agent.md | Kullanıcı seçtiğinde ya da ana agent iş devrettiğinde |
| Hook | Agent'ın belirli anlarında çalışan kod | .github/hooks/*.json | Her tool çağrısından önce ve sonra, oturum başında ve sonunda |
Aralarındaki asıl fark şu: talimat, skill ve custom agent modele söylenen şeyler; model uyabilir de uymayabilir de. Hook ise model ne düşünürse düşünsün çalışan bir script. GitHub'ın hook dokümanı da bunu böyle anlatıyor: talimat modelden bir kurala uymasını ister, hook kodu çalıştırır.
Bookstore MCP Server'ı
MCP server yazmayı AI Agent'ları ve MCP serisinin dokuzuncu yazısında ayrıntılı anlattık. Burada Copilot'a bağlayacağımız küçük bir server yeter: src/Bookstore.Mcp, veritabanına bağlanmıyor, Bookstore API'sinin salt okuma endpoint'lerini çağırıyor ve üç tool sunuyor:
// Üç tool da salt okuma: ReadOnly = true, tools/list cevabında annotations.readOnlyHint olarak görünür.
[McpServerToolType]
public sealed class CatalogTools(BookstoreApi api)
{
[McpServerTool(Name = "search_books", ReadOnly = true, Idempotent = true)]
[Description("Searches the Bookstore catalog by title, author or ISBN-13. Returns up to 10 books as 'isbn | title | author | price | year'.")]
public async Task<string> SearchBooks(
[Description("Words from the title or author name, or an ISBN-13, e.g. 'Austen'")] string query,
[Description("Optional category slug, e.g. 'classics' or 'science-fiction'")] string? category = null,
CancellationToken ct = default)
{
PagedResult<BookSummary> result = await api.SearchBooksAsync(query, category, ct);
if (result.Items.Count == 0)
{
return $"No books match '{query}'. Try a shorter query or remove the category.";
}
IEnumerable<string> lines = result.Items.Select(b =>
FormattableString.Invariant($"{b.Isbn} | {b.Title} | {b.Author} | {b.Price:0.00} TRY | {b.PublishedYear}"));
return $"{result.Total} book(s):\n" + string.Join("\n", lines);
}
// get_book ve list_institutions aynı şekilde
}
ReadOnly = true burada süs değil. GitHub'ın dokümanına göre Copilot code review, bir MCP tool'unu ancak tools/list cevabında readOnlyHint değeri true ise kullanıyor. Server'ı Copilot'a vermeden önce kendin dene; repoya bunun için küçük bir file-based app ekledik. Server'ı stdio ile başlatıyor, tool listesini annotation'larıyla alıyor ve iki tool çağırıyor:
$ dotnet run eng/mcp-smoke.cs
server: bookstore 1.0.0, protocol 2026-07-28
search_books readOnlyHint=True
get_book readOnlyHint=True
list_institutions readOnlyHint=True
search_books(query: Austen) isError=False
2 book(s):
9780141439587 | Emma | Jane Austen | 249.90 TRY | 1815
9780141439518 | Pride and Prejudice | Jane Austen | 239.90 TRY | 1813
get_book(isbn: 9780000000000) isError=False
No book with ISBN 9780000000000. Use search_books to find the ISBN first.
Bulunamayan ISBN (kitapların uluslararası standart numarası) için tool hata fırlatmıyor, modele ne yapacağını söyleyen bir cevap dönüyor. Tool tasarımının bu kısmı AI Agent'ları ve MCP serisinin üçüncü yazısının konusu.
MCP Server'ı Copilot'a Tanıtmak
Asıl zorluk yazmak değil, her yüzeye doğru yerden tanıtmak. 15 Kasım 2026 itibarıyla durum şöyle:
- Copilot CLI ve VS Code'un Copilot harness'ı: repo kökünde
.mcp.json(CLI ayrıca.github/mcp.jsonokuyor), en üsttemcpServersanahtarı. VS Code'un taşıma rehberi eski.vscode/mcp.jsondosyasını bu dosyaya taşımayı öneriyor. - Visual Studio:
%USERPROFILE%\.mcp.json,<SOLUTIONDIR>\.vs\mcp.json,<SOLUTIONDIR>\.mcp.jsonve.vscode\mcp.json; dokümandaki örnekserversanahtarını kullanıyor. Tool'lar varsayılan olarak kapalı geliyor, elle açılıyor. - GitHub.com'daki cloud agent ve code review: dosya değil, repository ayarlarında Copilot > MCP servers sayfasına yazılan JSON. GitHub MCP ve Playwright MCP server'ları varsayılan olarak açık.
Bookstore'un .mcp.json'u:
{
"mcpServers": {
"bookstore": {
"type": "local",
"command": "dotnet",
"args": ["run", "--project", "src/Bookstore.Mcp", "--no-build"],
"env": {
"BOOKSTORE_API_URL": "${BOOKSTORE_API_URL:-http://localhost:5080}"
},
"tools": ["search_books", "get_book", "list_institutions"]
},
"playwright": {
"type": "local",
"command": "npx",
"args": ["-y", "@playwright/mcp@0.0.85", "--isolated", "--allowed-origins", "http://localhost:4200"],
"tools": ["browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_console_messages"]
}
}
}
İki karar var. Birincisi tools listesi: Playwright server'ı 25 tool bildiriyor, biz beşini açıyoruz ve --allowed-origins ile tarayıcıyı yalnız Angular uygulamasının adresine bağlıyoruz. Aynı işi yapan çok sayıda tool modeli şaşırtır ve her tool tanımı context'te yer kaplar. İkincisi secret yok: API adresi environment variable'dan geliyor. Cloud agent tarafında da secret'lar yalnız adı COPILOT_MCP_ ile başlayan Agents secret'larından okunabiliyor.
CLI'ın bu dosyayı nasıl gördüğünü login gerektirmeyen copilot mcp komutlarıyla kontrol ettik:
$ copilot mcp list
Warning: Skipped workspace MCP servers (.mcp.json) because the folder "/home/user/src/bookstore" is not trusted. To load them without approving all tools, add the folder to "trustedFolders" in /home/user/.copilot/config.json. COPILOT_ALLOW_ALL=true also trusts it, but approves all tools.
Builtin servers:
computer-use (local)
$ copilot mcp list
Workspace servers:
bookstore (local)
playwright (local)
Builtin servers:
computer-use (local)
$ copilot mcp get bookstore
bookstore
Status: Enabled
Type: local
Command: dotnet run --project src/Bookstore.Mcp --no-build
Environment:
BOOKSTORE_API_URL: ***
Tools: search_books, get_book, list_institutions
Source: Workspace (/home/user/src/bookstore/.mcp.json)
İlk çalıştırmada dosya hiç yüklenmedi: CLI, güvenmediği bir klasördeki MCP server'larını sessizce değil ama uyarıyla atlıyor, çünkü bir MCP server senin makinende kod çalıştırıyor. Klasör trustedFolders'a eklendikten sonra iki server da göründü. copilot mcp get environment değerini maskeliyor ve açık tool listesini gösteriyor. Visual Studio dokümanındaki servers anahtarını aynı dosyada denediğimizde ise CLI dosyayı reddetti:
$ copilot mcp list
Warning: skipping workspace MCP config "/home/user/src/bookstore/.mcp.json" because it is malformed: mcpServers: Required
Builtin servers:
computer-use (local)
Yani tek bir .mcp.json bugün her yüzeyde aynı biçimle çalışmıyor. Bookstore'da dosyayı CLI ve VS Code'un okuduğu mcpServers biçiminde tuttuk; Visual Studio kullanan ekip arkadaşları server'ı chat'teki Tools > Add custom MCP server ile ya da kendi .vs\mcp.json dosyalarına ekliyor. Visual Studio'nun bu dosyayı nasıl okuduğunu bu yazıda deneyemedik.
Prompt Dosyalarından Skill'lere
Dördüncü yazıda VS Code'un Copilot harness'ının prompt dosyalarını çalıştırmadığını ve skill karşılıklarını bu yazıda yazacağımızı söylemiştik. Agent skill, içinde bir SKILL.md ve isteğe bağlı script ya da örnek dosyalar olan bir klasör. Açık bir standart (agentskills.io); Copilot'ta cloud agent, code review, CLI, VS Code ve Visual Studio'nun agent'ı ile JetBrains IDE'leri skill okuyor. Proje skill'leri .github/skills, .claude/skills ya da .agents/skills altında; kişisel olanlar ~/.copilot/skills altında duruyor.
---
name: new-endpoint
description: Adds a minimal API endpoint to a Bookstore module together with an integration test. Use when asked to add or expose a new HTTP endpoint in src/Bookstore.Api.
argument-hint: "[module] [HTTP method and route]"
disable-model-invocation: true
---
Add the endpoint given in the request to the named module (default: Ordering).
1. Read the module's `*Module.cs` and follow how its existing endpoints are mapped and typed.
2. Return `TypedResults`; describe expected failures with ProblemDetails.
3. Add an integration test in `tests/Bookstore.Api.Tests` that fails before the change and passes after it.
4. Run `dotnet build` and `dotnet test` and report the test summary.
Do not change existing tests. Stop and ask if the endpoint needs a new table.
Prompt dosyasından farkları frontmatter'da. name klasör adıyla aynı olmalı ve yalnız küçük harf, rakam ve tire içerebilir (en fazla 64 karakter). description en fazla 1.024 karakter ve en önemli alan: agent bir skill'i yüklemeye bu açıklamaya bakarak karar veriyor, gövdeyi ancak skill seçilince okuyor. disable-model-invocation: true skill'i yalnız /new-endpoint ile çağrılabilir yapıyor; VS Code'un otomatik dönüştürme aracı da prompt dosyasını skill'e çevirirken bu alanı ekliyor, çünkü prompt dosyası zaten yalnız elle çağrılıyordu. Dönüştürmede agent, model ve tools alanları kayboluyor; prompt'taki ${input:...} değişkenlerinin yerini de argument-hint ve isteğin kendisi alıyor.
Prompt dosyalarını silmedik: Visual Studio'da hâlâ çalışıyorlar. İki kopyanın ayrışmasını dördüncü yazıdaki kontrol script'i kısmen yakalıyor; script'e skill, agent ve hook dosyaları için de kontroller ekledik (ad ve klasör uyumu, açıklama uzunluğu, hook script'lerinin var olması).
Migration'ları Kontrol Eden Skill ve Custom Agent
Skill'in gerçek faydası script taşıyabilmesi. Bookstore'daki en riskli değişiklik türü Entity Framework Core (EF Core) migration'ları; agent bir property'nin adını değiştirdiğinde ya da bir kolonu kısalttığında üretilen migration production verisini etkiliyor. ef-migration-review skill'i bu kontrolü bir script'e bağlıyor:
---
name: ef-migration-review
description: Checks EF Core migrations in src/Bookstore.Api/Data/Migrations for data loss (DropColumn, DropTable), renames, columns made non-nullable without a data step, and empty Down methods. Use when a migration is added or changed, or when reviewing a pull request that touches Data/Migrations.
---
Run the check from the repository root:
```
dotnet run .github/skills/ef-migration-review/check-migrations.cs
```
Pass a migration name (for example `AddOrderInstitution`) to check one migration; without an argument every migration is checked.
How to read the result:
- `OK` lines need no comment.
- `RISK` lines name the operation and the table. For each one, check whether the migration has a `migrationBuilder.Sql(...)` data step that copies or backfills the data first, and whether the pull request description explains the data loss.
- `ERROR` lines (an empty `Down`) must be fixed before merge.
Report the script output unchanged, then one sentence per `RISK` line with your recommendation. Never edit the migration yourself and never run `dotnet ef database update`.
Script'in ana döngüsü Up method'unu okuyup riskli işlemleri arıyor:
// Kısaltılmış: migration döngüsü; MethodBody() Up ya da Down method'unun gövdesini döndürür.
foreach (string file in files)
{
string name = Path.GetFileNameWithoutExtension(file);
string source = File.ReadAllText(file);
string up = MethodBody(source, "Up");
string down = MethodBody(source, "Down");
bool hasDataStep = up.Contains("migrationBuilder.Sql(");
List<string> findings = [];
foreach (Match m in Regex.Matches(up, @"migrationBuilder\.(\w+)\(\s*name:\s*""(\w+)""(?:,\s*(?:schema:\s*""\w+"",\s*)?table:\s*""(\w+)"")?"))
{
string operation = m.Groups[1].Value;
if (risky.Contains(operation))
{
string target = m.Groups[3].Success ? $"{m.Groups[3].Value}.{m.Groups[2].Value}" : m.Groups[2].Value;
findings.Add($"RISK {name}: {operation} {target}{(hasDataStep ? " (has a Sql data step)" : " without a Sql data step")}");
}
}
// nullable kolon NOT NULL oluyorsa mevcut NULL satırlar için backfill gerekir
foreach (Match m in Regex.Matches(up, @"AlterColumn<[\w?]+>\(\s*name:\s*""(\w+)""[^;]*?nullable:\s*false[^;]*?oldNullable:\s*true", RegexOptions.Singleline))
{
findings.Add($"RISK {name}: AlterColumn {m.Groups[1].Value} becomes NOT NULL{(hasDataStep ? " (has a Sql data step)" : " without a backfill")}");
}
// kısalan string kolonu: daha uzun mevcut değerler update'i hata verdirir
foreach (Match m in Regex.Matches(up, @"AlterColumn<string>\(\s*name:\s*""(\w+)""[^;]*?maxLength:\s*(\d+)[^;]*?oldMaxLength:\s*(\d+)", RegexOptions.Singleline))
{
if (int.Parse(m.Groups[2].Value) < int.Parse(m.Groups[3].Value))
{
findings.Add($"RISK {name}: AlterColumn {m.Groups[1].Value} shrinks from {m.Groups[3].Value} to {m.Groups[2].Value} characters");
}
}
if (string.IsNullOrWhiteSpace(down))
{
findings.Add($"ERROR {name}: Down is empty, the migration cannot be reverted");
errors++;
}
int operations = Regex.Matches(up, @"migrationBuilder\.\w+").Count;
Console.WriteLine(findings.Count == 0 ? $"OK {name}: {operations} operations" : string.Join("\n", findings));
}
Denemek için agent'ın yapabileceği bir değişikliği elle yaptık: Book.PublishedYear'ı FirstPublishedYear olarak yeniden adlandırdık ve Description kolonunu 1.000'den 500 karaktere indirdik:
$ dotnet ef migrations add RenameBookYear --project src/Bookstore.Api --output-dir Data/Migrations
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "PublishedYear",
schema: "catalog",
table: "Books",
newName: "FirstPublishedYear");
migrationBuilder.AlterColumn<string>(
name: "Description",
schema: "catalog",
table: "Books",
type: "nvarchar(500)",
maxLength: 500,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(1000)",
oldMaxLength: 1000);
}
$ dotnet run .github/skills/ef-migration-review/check-migrations.cs
OK 20261114005447_InitialCreate: 27 operations
OK 20261114234258_AddInstitutions: 8 operations
OK 20261114234727_AddOrderInstitution: 3 operations
RISK 20261115001954_RenameBookYear: RenameColumn Books.PublishedYear without a Sql data step
RISK 20261115001954_RenameBookYear: AlterColumn Description shrinks from 1000 to 500 characters
EF Core'un kendi uyarısı genel: "veri kaybı olabilir". Script hangi işlemin riskli olduğunu söylüyor. İlk sürümümüz kısalan kolonu görmüyordu; EF Core'un uyarısını görünce kuralı ekledik. Kısalan kolonun neden önemli olduğunu SQL Server 2025'te kendi deneme veritabanımızda gösterdik: on karakterlik bir değer varken kolonu beş karaktere indirmek update'i durduruyor.
Msg 2628, Level 16, State 1, Line 1
String or binary data would be truncated in table 'copilotseries_06.dbo.ShrinkTest', column 'x'. Truncated value: ''.
The statement has been terminated.
Script kuralları yakalıyor; bu riskleri yorumlamak ise bir rol işi. Bunun için bir custom agent tanımladık:
---
name: migration-reviewer
description: Reviews EF Core migrations in Bookstore for data loss, risky renames and broken Down methods. Use when a change adds or edits files under src/Bookstore.Api/Data/Migrations.
tools: ["read", "search", "execute"]
---
You review EF Core migrations in the Bookstore repository. You never edit files.
1. Find the migrations that the task is about (default: the newest file in `src/Bookstore.Api/Data/Migrations` that is not a `.Designer.cs` file).
2. Run `dotnet run .github/skills/ef-migration-review/check-migrations.cs <MigrationName>` and copy its output.
3. Read the migration's `Up` and `Down` methods and the entity configuration it came from.
4. For every `RISK` line, say what happens to existing rows in production and what data step or deployment order would make it safe.
5. End with one line: `Verdict: safe`, `Verdict: needs a data step` or `Verdict: do not merge`.
Do not run `dotnet ef database update` or any command that changes a database.
Custom agent profili, frontmatter'lı bir Markdown dosyası. tools listesindeki read, search ve execute, GitHub'ın tool alias'ları; edit olmadığı için bu agent dosya değiştiremiyor. Profil dosyası repoda .github/agents altında; organization sahipleri aynı dosyaları organization'ın .github ya da .github-private reposuna koyarak bütün repolara açabiliyor ve isim çakışmasında repodaki tanım kazanıyor. Gövde en fazla 30.000 karakter olabiliyor. Bazı alanlar yüzeye göre değişiyor: mcp-servers IDE'lerde kullanılmıyor, VS Code'daki handoffs ise yalnız Local harness'ta çalışıyor. Visual Studio 18.12 kullanıcı düzeyindeki agent'ları artık Copilot CLI gibi %USERPROFILE%\.copilot\agents altında arıyor.
Agent'ı Copilot CLI'da yerel modelle de çalıştırdık (--agent migration-reviewer). CLI profili yükledi ve olay kaydına tools: ["read","search","execute"] ile yazdı, ama 4 milyar parametrelik model talimatı izlemedi: script'i çalıştırmak yerine CLI'ın oturum içi sql tool'unda olmayan bir migrations tablosunu sorguladı ve sonuç vermeden durdu. Bu, profilin değil modelin sınırı; bir custom agent'ın kalitesini ekibin kullanacağı modelle, küçük ve bilinen bir görevle sınamak gerekiyor.
Hook'larla Kuralı Zorlamak
Hook dosyası JSON; version: 1 ve olay adına göre komut listesi içeriyor. Copilot CLI, cloud agent ve VS Code'un Copilot harness'ı repodaki .github/hooks/*.json dosyalarını okuyor; Visual Studio'nun sürüm notlarında hook desteği geçmiyor.
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"matcher": "bash|powershell",
"bash": ".github/hooks/scripts/guard-shell.sh",
"powershell": ".github/hooks/scripts/guard-shell.ps1",
"timeoutSec": 10
}
],
"postToolUse": [
{
"type": "command",
"matcher": "edit|create",
"bash": ".github/hooks/scripts/format-cs.sh",
"powershell": ".github/hooks/scripts/format-cs.ps1",
"timeoutSec": 60
}
]
}
}
#!/usr/bin/env bash
# preToolUse: agent'ın shell komutunu çalışmadan önce görür, tehlikeli olanları reddeder.
# Girdi stdin'den JSON; toolArgs JSON string'i ya da object olabilir. jq gerekir.
set -euo pipefail
input=$(cat)
command=$(jq -r '.toolArgs | (if type == "string" then fromjson else . end) | .command // ""' <<< "$input")
deny() {
jq -cn --arg reason "$1" '{permissionDecision: "deny", permissionDecisionReason: $reason}'
exit 0
}
case "$command" in
*"dotnet ef database"*) deny "Applying migrations is done by a developer, not by the agent." ;;
*"git push"*) deny "Pushing is done by a developer after review." ;;
*"rm -rf"*|*"git reset --hard"*|*"git clean -f"*) deny "Destructive file commands are blocked in this repository." ;;
esac
# Çıktı yoksa karar normal onay akışına kalır.
preToolUse hook'u tool çalışmadan önce JSON'u stdin'den alıyor; permissionDecision değeri deny olan bir cevap çağrıyı durduruyor ve permissionDecisionReason agent'a geri dönüyor. Hiç çıktı vermemek kararı normal onay akışına bırakıyor. Windows'ta CLI powershell anahtarındaki script'i çalıştırıyor; cloud agent ise Linux sandbox'ında yalnız bash'i kullanıyor. GitHub'ın önerdiği gibi script'leri önce elle, örnek JSON'u pipe'layarak denedik:
$ echo '{"toolName":"bash","toolArgs":{"command":"dotnet ef database update --project src/Bookstore.Api"}}' | .github/hooks/scripts/guard-shell.sh
{"permissionDecision":"deny","permissionDecisionReason":"Applying migrations is done by a developer, not by the agent."}
$ echo '{"toolName":"bash","toolArgs":{"command":"dotnet test"}}' | .github/hooks/scripts/guard-shell.sh
$ echo '{"toolName":"powershell","toolArgs":{"command":"Remove-Item -Recurse -Force src"}}' | pwsh -NoProfile -File .github/hooks/scripts/guard-shell.ps1
{"permissionDecisionReason":"Destructive file commands are blocked in this repository.","permissionDecision":"deny"}
$ echo '{"toolName":"create","toolArgs":{"path":"src/Bookstore.Api/Catalog/IsbnFormat.cs"}}' | .github/hooks/scripts/format-cs.sh
{"additionalContext":"dotnet format fixed whitespace in src/Bookstore.Api/Catalog/IsbnFormat.cs. Read the file again before the next edit."}
Üçüncü satırda dotnet test için çıktı yok, yani karar normal akışa kalıyor. format-cs.sh ise postToolUse'ta çalışıyor: agent bir .cs dosyası yazdıktan sonra dotnet format whitespace ile biçimi düzeltiyor ve additionalContext ile agent'a dosyayı yeniden okumasını söylüyor.
Hata davranışını bilmek gerekiyor. Dokümana göre preToolUse komut hook'u çökerse ya da 0 ve 2 dışında bir kodla çıkarsa tool çağrısı reddediliyor (fail-closed; hata varsa kapalı kalır), ama zaman aşımı her olayda fail-open: süre dolarsa çağrı normal akışla devam ediyor. Yavaş bir güvenlik hook'u bu yüzden güvenlik sağlamaz. Cloud agent'ta ask kararı deny sayılıyor, çünkü cevap verecek kullanıcı yok; hook dosyası da ancak default branch'teyse cloud agent'a etki ediyor.
Copilot CLI'da Yerel Bir Modelle Denemek
MCP, skill, agent ve hook'un birlikte çalıştığını görmek için Copilot CLI'ı GitHub hesabı olmadan, COPILOT_OFFLINE=true ve yerel Ollama ile çalıştırdık. Bu modda CLI hiçbir ağa çıkmıyor; GitHub girişi, GitHub MCP server'ı ve cloud agent'a devretme kapalı. Önce Tahmin Et'teki komutu açıkça istedik, sonra kitap fiyatlarını sorduk:
$ export COPILOT_OFFLINE=true COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:11434/v1 COPILOT_MODEL=qwen3:4b
$ copilot -p "Run this exact shell command and report its output: dotnet ef database update --project src/Bookstore.Api" \
--available-tools bash --allow-tool shell --output-format json > guard.jsonl
$ jq -c 'select(.type=="tool.execution_complete") | {success: .data.success, error: .data.error.message}' guard.jsonl
{"success":false,"error":"Denied by preToolUse hook: Applying migrations is done by a developer, not by the agent."}
$ copilot -p "Which Jane Austen books does Bookstore sell, and at what price? Use the bookstore tools." \
--disable-mcp-server playwright --allow-tool bookstore --output-format json > mcp.jsonl
$ jq -c 'select(.type=="tool.execution_start") | {tool: .data.toolName, arguments: .data.arguments, readOnly: .data.readOnly}' mcp.jsonl
{"tool":"bookstore-search_books","arguments":{"query":"Austen"},"readOnly":true}
{"tool":"bookstore-get_book","arguments":{"isbn":"9780141439587"},"readOnly":true}
{"tool":"bookstore-search_books","arguments":{"category":"classics","query":"Emma"},"readOnly":true}
{"tool":"bookstore-get_book","arguments":{"isbn":"9780141439587"},"readOnly":true}
Tahminin cevabı: kullanıcının açık isteği de, onu izleyen model de hook'u aşamadı. Model komutu istendiği gibi çağırdı, hook reddetti ve sebep cümlesi modele döndü; model bir sonraki cevabında komutun neden çalışmadığını bu cümleyle açıkladı. add-migration skill'indeki "dotnet ef database update çalıştırma" satırı ise bu istekte context'e hiç girmedi, çünkü skill yalnız /add-migration ile çağrılıyor. Kural yalnız yazılı olsaydı model kullanıcının açık isteğini izleyip komutu çalıştıracaktı. Bir not daha: model sebebi doğru aktardı ama ardından migration komutları hakkında karışık bir öneri listesi yazdı; küçük modelden beklenecek bir şey.
İkinci koşuda CLI, .mcp.json'daki server'ı kendisi başlattı ve tool'ları bookstore-search_books gibi server adıyla önekleyip readOnly: true bilgisiyle çağırdı. Küçük model aynı kitabı iki kez sorguladı ve sonunda cevabı yazmak yerine bir tool çağrısını düz metin olarak bastı. Mekanik doğru çalıştı; cevap kalitesi modelin işi. Aynı oturumun olay kaydında CLI'ın Bookstore'un üç skill'ini (CLI'ın kendi üç yerleşik skill'inin yanında) ve migration-reviewer agent'ını yüklediği de görünüyor.
Üretimde Dikkat
MCP server kod çalıştırır. Cloud agent ve code review, repository ayarlarına eklenen MCP tool'larını onay istemeden kullanıyor; GitHub'ın kendi uyarısı da bu yönde. Oraya yalnız salt okuma tool'larını, açık bir tools listesiyle koy.
Sürümü sabitle. @playwright/mcp@latest yerine @0.0.85 yazdık; her oturumda değişebilen bir paket, bugün güvendiğin server'ın yarın başka kod çalıştırması demek. Visual Studio da bu yüzden server'ın tool listesi değiştiğinde verilmiş onayları sıfırlıyor ve 18.7'den beri değişen server için yeniden güven soruyor.
Skill script'leri ve hook'lar da kod. .github/skills, .github/agents ve .github/hooks klasörlerini CODEOWNERS ile bir sahibe bağla. Skill frontmatter'ındaki allowed-tools: shell ise o skill için terminal onayını kaldırıyor; GitHub bunu yalnız incelediğin ve tamamen güvendiğin skill'ler için öneriyor.
Organization düzeyinde MCP kullanımı bir policy ile kapatılabiliyor ve allowlist ile sınırlanabiliyor; bunu onuncu yazıda ele alacağız.
Sık Düşülen Tuzaklar
Güvenilmeyen bir MCP server'ı eklemek. Tool açıklamasına gizlenmiş talimatlar (tool poisoning) ve rug-pull, AI Agent'ları ve MCP serisinin on üçüncü yazısında anlatıldı; topluluk skill'leri için de aynı risk var, GitHub skill'leri doğrulamıyor.
Aynı işi yapan çok sayıda tool açmak. Playwright'ın 25 tool'unu, GitHub MCP'nin bütün toolset'lerini ve kendi server'ını birlikte açarsan model yanlış tool'u seçer ve her istekte binlerce token tool tanımı gönderilir.
Kuralı yalnız talimata yazmak. Model kullanıcının açık isteğini izleyebilir; durdurulması gereken komutlar hook'a girmeli.
Hook'u yavaş yazmak. Zaman aşımı fail-open; ağ çağrısı yapan bir preToolUse hook'u süre dolunca hiçbir şeyi durdurmaz.
Skill açıklamasını kısa tutmak. Agent skill'i açıklamadan seçiyor; "Migration helper" yazan bir skill hiçbir zaman otomatik yüklenmez.
Küçük Bir Deneme
★☆☆ Kendi reponda bir .mcp.json yaz ve copilot mcp list ile copilot mcp get çıktısına bak. Klasör güvenilir değilken hangi uyarıyı alıyorsun?
★★☆ guard-shell.sh'a dotnet ef migrations remove kuralını ekle ve önce elle pipe'layarak, sonra Copilot CLI'da dene. Hook'u bilerek sleep 20 ile yavaşlat ve timeoutSec: 10 ile ne olduğuna bak.
★★★ check-migrations.cs'e AddColumn ile eklenen ve varsayılan değeri olmayan NOT NULL kolon kuralını ekle. Bookstore'da böyle bir migration üret, script'in yakaladığını gör ve migration-reviewer agent'ını ekibinin kullandığı bir modelle çalıştırıp raporunu script çıktısıyla karşılaştır.
Buraya kadar Copilot hep senin makinende çalıştı. Bir sonraki yazıda işi GitHub'a taşıyoruz: bir issue'yu Copilot coding agent'a atamak, GitHub Actions ortamını copilot-setup-steps.yml ile hazırlamak, firewall ve açılan pull request'i review etmek.