<p>Uygulamanızı bir AI ajanına MCAP üzerinden açmak, kelime anlamıyla bir anahtar kısmını eline verip, sadece açması gereken kapıları açacağını ummaktır. Bu güven, bir hata olma potansiyelidir. Bu hafta, çok kiracılı bir Laravel uygulaması üzerinde bir grup MCP aracını yapılandırdım ve süreç esasen şu soruya odaklandı: <em>Bir ajana uygulamayı sürdürmesine nasıl izin veririm, ama başka birinin verilerine erişmesine nasıl engel olurum?</em></p>
<p>Burada MCP araçları hakkında önemli bir detay var: her biri bir uç noktadır. Bir ajan <code>list_events</code>, <code>publish_event</code>, <code>check_in_participant</code> gibi çağrılar yapar ve sunucunuz, çağrıyı yapanın adına kod çalıştırır. Birden fazla kiracı olduğunda, her bir aracın öncelikle iki soruya cevap vermesi gerekir: <strong>bunu yapmaya yetkin misin</strong> ve <strong>bunu *burada* yapmaya yetkin misin</strong>. Yetkilendirme ve kapsam. Biri atlanırsa, karmaşık bir durum yaratmış olursunuz.</p>
<h2>
<a name="the-trap-ambient-scope-doesnt-exist-under-token-auth" href="#the-trap-ambient-scope-doesnt-exist-under-token-auth"></a>
Tuzak: Token auth altında çevresel kapsam mevcut değildir
</h2>
<p>Normal bir web isteğinde, çok kiracılık rahattır. Giriş yapmış bir kullanıcınız, model üzerinde <code>where organization_id = ?</code> kümesiyle global bir kapsam vardır ve çoğunlukla varlığını unuturuz. Her şey düzgün çalışır çünkü oturumda bir "mevcut organizasyon" bulunur.</p>
<p>Ancak MCP araçlarında bu yoktur. Çağıran bir token ile kimlik doğrulaması yapar, oturum yoktur, "mevcut kiracı" bağlamını oluşturan herhangi bir middleware yığına sahip değildir. Eğer bir global <code>OrganizationScope</code>'a dayanıyorsanız ve "mevcut organizasyon" bir yerden okuyorsa, <em>hiçbir şey</em> okur ve varsaydığınız bir sorgu, her kiracının satırını döner. Bu tür bir hata, bir hata fırlatmaz; sessizce sızıntı yapar.</p>
<p>Bu yüzden, benimsediğim kural: <strong>token auth altında, çevresel kapsamdan asla yararlanmayın. Her zaman, kesin olarak filtreleyin, tek bir yerde.</strong></p>
<p>Bu "tek yer", her etkinlik kapsamlı aracın içe aktardığı küçük bir trait'tir:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="kd">trait</span> <span class="nc">ResolvesOrgEvents</span>{
protected function resolveOrgEvent(Authenticatable $user, string $uuid): ?Event
{
if (empty($user->organization_id)) {
return null;
}
<span class="k">return</span> <span class="nc">Event</span><span class="o">::</span><span class="nf">query</span><span class="p">()</span>
<span class="o">-></span><span class="nf">withOrganization</span><span class="p">(</span><span class="nv">$user</span><span class="o">-></span><span class="n">organization_id</span><span class="p">)</span>
<span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1">'uuid'</span><span class="p">,</span> <span class="nv">$uuid</span><span class="p">)</span>
<span class="o">-></span><span class="nf">first</span><span class="p">();</span>
<span class="p">}</span>}
<p>Hiçbir zeka içermeyen bir çözüm — ve bu nokta. Organizasyon filtresi, aktif olmasını umudu içinde beklediğiniz küresel bir kapsam değil; elle uygulanan ve tam olarak bir trait içinde yaşayan bir adlandırılmış sorgu alanı (<code>withOrganization</code>). UUID ile bir etkinlik belirlemek için her ayrıntılı araç bu yoldan geçer. Eğer belirleme <code>null</code> dönerse, araç "organizasyonunuzda bulunamadı" yanıtını verir ve durur. Başka bir kiracıdan gelen bir UUID sorgulayan bir ajan, var olmayan UUID ile aynı yanıtı alır — orak yok, sızıntı yok.</p>
<p>Unutmayın, arama <strong>UUID ile istemektedir, artan otomatik kimlik ile değil</strong>. Kamusal tanıtıcıların tahmin edilemez olması gerekir. Bir ajan (ya da prompt enjekte edilen bir) <code>event/1</code>, <code>event/2</code>, <code>event/3</code> şeklinde sıralamaya girememelidir. Dahili sayısal anahtar, veritabanının dışına çıkmaz.</p>
<h2>
<a name="authorization-one-ability-per-tool-checked-the-same-way-as-the-web-app" href="#authorization-one-ability-per-tool-checked-the-same-way-as-the-web-app"></a>
Yetkilendirme: her araç için tek bir yetenek, web uygulamasıyla aynı şekilde kontrol edildi
</h2>
<p>Kapsam sizi kiracınızda tutar. Yetkilendirme, içinde ne yapabileceğinizi belirler. Her araca tek bir beyan edilmiş yetenek verdim:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="na">#[Name('event_readiness_check')]</span>#[Description(‘Check whether an event is ready to publish. Returns ready=true/false and blocking issues.’)]
#[IsReadOnly]
class EventReadinessCheckTool extends McpKitTool
{
use ResolvesOrgEvents;
<span class="k">protected</span> <span class="k">function</span> <span class="n">ability</span><span class="p">():</span> <span class="kt">string</span>
<span class="p">{</span>
<span class="k">return</span> <span class="s1>'events.view.details'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">):</span> <span class="kt">Response</span>
<span class="p">{</span>
<span class="nv">$user</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">authorizedUser</span><span class="p">(</span><span class="nv">$request</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$user</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">unauthorized</span><span class="p">();</span>
<span class="p">}</span>
<span class="nv">$validated</span> <span class="o">=</span> <span class="nv">$request</span><span class="o">-></span><span class="nf">validate</span><span class="p">([</span>
<span class="s1>'event'</span> <span class="o">=></span> <span class="p">[</span><span class="s1>'required'</span><span class="p">,</span> <span class="s1>'string'</span><span class="p">,</span> <span class="s1>'max:36'</span><span class="p">],</span>
<span class="p">]);</span>
<span class="nv">$event</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">resolveOrgEvent</span><span class="p">(</span><span class="nv">$user</span><span class="p">,</span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'event'</span><span class="p">]);</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$event</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nc">Response</span><span class="o">::</span><span class="nf">error</span><span class="p">(</span><span class="s1>'Event not found in your organization.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// ... read-only work</span>
<span class="p">}</span>}


