• 26.08.2026 03:38:23
  • Admin Admin

Spring AI ve Spring MCP üzerinde modelin çağırdığı araçları yetkilendirme, şema, gözlemlenebilirlik ve yük testiyle üretime hazırlayın. Model context protocol sınırlarında veri sızıntısı ve gecikmeyi ölçülebilir biçimde yönetin.

Spring MCP ile Java Backend Geliştirmede Güvenli Araç Tasarımı

Spring MCP ve model context protocol için tehdit modelini kurun

Spring AI ile bir aracı modele açmadan önce onu normal bir REST endpoint'i gibi değil, model tarafından üretilen parametrelerle çağrılan ayrı bir güven sınırı olarak ele alın. model context protocol istemcisi, modele araç tanımı ve sonuçlarını taşır; ancak modelin ürettiği tenantId, kullanıcıId veya SQL filtresi yetki kanıtı değildir. İlk envanterde her araç için veri sınıfı, yan etki, maksimum çağrı oranı ve gerekli kullanıcı yetkisini yazın. Örneğin readInvoice aracı PII döndürüyorsa araç sonucunda yalnızca gerekli alanları döndürün; tam Hibernate ORM entity'sini serileştirmeyin.

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

Starter seçimi yalnızca transport sağlar; kimlik doğrulama politikasını sağlamaz. Spring Boot uygulamasında MCP endpoint'ini mevcut Spring Security filter chain arkasına alın ve anonim erişimi kapatın. Ağ sınırında ayrıca reverse proxy ile istek gövdesi limiti uygulayın. Araç çağrıları uzun JSON argümanları taşıyabildiği için, örneğin Nginx tarafında client_max_body_size 64k; uygulama tarafında ise DTO doğrulaması kullanmak, büyük payload ile heap baskısı oluşturulmasını engeller.

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
  return http
      .csrf(csrf -> csrf.disable())
      .authorizeHttpRequests(auth -> auth
          .requestMatchers("/mcp/**").hasAuthority("SCOPE_ai_tools")
          .anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 -> oauth2.jwt())
      .build();
}

Bu yapı, spring framework katmanındaki HTTP kimliğini korur; fakat araç içindeki kaynak yetkisini yine doğrulamanız gerekir. Yaygın hata, modelin gönderdiği accountId değerini SecurityContext'teki kullanıcıyla eşleştirmeden repository sorgusuna vermektir. Bu hata doğru JWT ile oturum açmış bir kullanıcının, model yönlendirmesi veya prompt injection sonucu başka hesabın kaydını istemesine izin verir.

Spring AI araçlarında yetkiyi argümandan değil kimlikten türetin

spring ai aracının imzasını küçük, doğrulanabilir bir komut olarak tasarlayın. Aşağıdaki örnekte hesap kimliği input'tan alınmaz; JWT subject değerinden türetilir. amount değeri Bean Validation ile sınırlandırılır ve servis katmanı sahiplik kontrolünü sorgunun parçası yapar. Böylece yalnızca uygulama kodunda yapılan bir if kontrolüne değil, SQL WHERE koşuluna da güvenirsiniz.

public record RefundRequest(
    @NotBlank String invoiceNumber,
    @DecimalMin("0.01") @DecimalMax("500.00") BigDecimal amount) {}

@Component
class BillingTools {
  private final RefundService refundService;

  BillingTools(RefundService refundService) {
    this.refundService = refundService;
  }

  @Tool(description = "Refunds an invoice owned by the signed-in customer. Maximum amount is 500.")
  RefundResult requestRefund(@Valid RefundRequest request) {
    JwtAuthenticationToken auth = (JwtAuthenticationToken)
        SecurityContextHolder.getContext().getAuthentication();
    UUID customerId = UUID.fromString(auth.getToken().getSubject());
    return refundService.request(customerId, request);
  }
}

spring data jpa tarafında sahiplik denetimini iki ayrı SELECT ile yapmak yerine tek koşullu güncelleme kullanın. İlk SELECT sonrası entity durumunun değişmesi, iki çağrı arasındaki zamanda invoice sahibinin değişmesi gibi TOCTOU penceresi yaratır. Aşağıdaki sorguda güncellenen satır sayısı 0 ise aynı dış hata kodunu döndürün; böylece model veya kullanıcı invoice numarasının var olup olmadığını çıkaramaz.

@Modifying
@Query("""
  update Invoice i
     set i.refundRequested = true,
         i.refundAmount = :amount
   where i.invoiceNumber = :invoiceNumber
     and i.customerId = :customerId
     and i.status = 'PAID'
     and i.refundRequested = false
""")
int markRefundRequested(UUID customerId, String invoiceNumber, BigDecimal amount);

java backend geliştirme ekiplerinde özellikle dikkat edilmesi gereken incelik şudur: @Tool metodunun açıklaması erişim kontrolü değildir. Model açıklamadaki 'signed-in customer' kuralını ihlal edebilir veya araç parametresini yanlış oluşturabilir. Yetkiyi RefundService içinde tekrar zorunlu tutun. Bu sayede aynı servis REST controller, batch job veya java microservices içindeki mesaj tüketicisi tarafından çağrılsa bile politika değişmez.

Hibernate ORM sonuçlarını araç sözleşmesine sızdırmayın

Araç dönüş tipini JPA entity yerine sürümlenmiş bir DTO yapın. Hibernate ORM proxy'leri ve çift yönlü ilişkiler Jackson serileştirmesinde beklenmedik lazy load tetikleyebilir. Bir Invoice entity'sinin customer ve lineItems ilişkilerini döndürmek, tek araç çağrısında yüzlerce ek sorgu üretebilir ve model bağlamına gereksiz PII ekleyebilir. Projection ile yalnızca modelin karar vermesi için gereken alanları çekin.

public interface InvoiceSummary {
  String getInvoiceNumber();
  BigDecimal getOutstandingAmount();
  LocalDate getDueDate();
}

interface InvoiceRepository extends JpaRepository<Invoice, UUID> {
  @Query("""
    select i.invoiceNumber as invoiceNumber,
           i.outstandingAmount as outstandingAmount,
           i.dueDate as dueDate
      from Invoice i
     where i.customerId = :customerId
       and i.invoiceNumber = :invoiceNumber
  """)
  Optional<InvoiceSummary> findSummary(UUID customerId, String invoiceNumber);
}

public record InvoiceToolResult(
    String invoiceNumber, BigDecimal outstandingAmount, LocalDate dueDate) {}

Araç sözleşmesinde hata sonucunu da şemalı döndürün. RuntimeException mesajını modele iletmek; JDBC URL, tablo adı veya üçüncü taraf hata metni sızdırabilir. Örneğin {"status":"NOT_FOUND_OR_FORBIDDEN"} ve {"status":"CONFLICT","retryAfterSeconds":30} gibi sabit kodlar kullanın. correlationId değerini araç yanıtına ekleyin, fakat tenantId ve e-posta gibi yüksek kardinaliteli veya kişisel alanları metric etiketi yapmayın.

  • Başarı yanıtı için maksimum 10 kayıt ve maksimum 8 KB JSON bütçesi koyun.
  • Para değerlerini BigDecimal olarak işlemeye devam edin, araç JSON'unda float hesaplaması yaptırmayın.
  • Enum değerlerini @JsonValue ile sabitleyin; Java enum adını yeniden adlandırmak araç sözleşmesini sessizce kırabilir.

Bu disiplin java fullstack eğitimi yapan ekiplerde frontend DTO tasarımından tanıdık gelebilir; MCP'de fark, tüketicinin derlenmiş bir istemci değil olasılıksal bir model olmasıdır. Bu nedenle 'opsiyonel' alanın yokluğu ile null değeri için açık davranış tanımlayın ve modelin eski şemayı göndermesine karşı @JsonIgnoreProperties(ignoreUnknown = false) ile sözleşme kırılmasını test ortamında görünür hale getirin.

Spring Boot eğitimi kapsamında MCP araç gecikmesini JFR ile ölçün

Araç gecikmesini optimize etmeden önce JDK Flight Recorder ile örnek alın. Üretime yakın bir pod üzerinde sınırlı süreli kayıt için jcmd <pid> JFR.start name=mcp settings=profile duration=5m filename=/tmp/mcp.jfr komutunu kullanın. JDK Mission Control'da jdk.ExecutionSample, jdk.SocketRead, jdk.JavaMonitorEnter ve allocation flame graph görünümlerini inceleyin. Aynı yükte araç başına p50, p95 ve p99 ölçün; yalnızca ortalama süre, nadir fakat model oturumunu bloke eden sorguları gizler.

Micrometer ile araç adı bazında zamanlayıcı ekleyin. invoiceNumber veya customerId etiketi eklemeyin: her yeni değer Prometheus'ta yeni time series oluşturur. Aşağıdaki kod, yalnızca sabit tool etiketiyle histogram üretir. Prometheus'ta histogram_quantile(0.95, sum(rate(ai_tool_seconds_bucket[5m])) by (le,tool)) sorgusuyla p95'i izleyin.

class RefundService {
  private final Timer timer;

  RefundService(MeterRegistry registry) {
    this.timer = Timer.builder("ai_tool_seconds")
        .tag("tool", "request_refund")
        .publishPercentileHistogram()
        .register(registry);
  }

  RefundResult request(UUID customerId, RefundRequest request) {
    return timer.record(() -> doRequest(customerId, request));
  }
}

Örnek bir yük testinde k6 ile 40 sanal kullanıcı, 10 dakika boyunca 20 farklı invoice üzerinde çağrı yaptıktan sonra JFR'da sürenin önemli kısmının entity graph serileştirmesinde ve 120 satırlık lineItems lazy load'unda geçtiğini varsayalım. Önce p95 840 ms ve çağrı başına 123 SQL sorgusu ölçülür. Yukarıdaki projection sorgusuna geçip araç sonucunu 10 kayıtla sınırladıktan sonra aynı k6 senaryosunda p95 145 ms ve çağrı başına 1 sorgu hedeflenir. Bu bir evrensel sayı değildir; aynı veri dağılımı, JVM seçenekleri ve bağlantı havuzu ile önce-sonra karşılaştırması yapılmasının nedenidir.

export BASE_URL=https://staging.example.internal
k6 run --vus 40 --duration 10m mcp-tool-load.js
# Test betiğinde her iterasyonda aynı auth scope ile
# en fazla 10 farklı invoiceNumber kullanın; rastgele sınırsız ID üretmeyin.

Spring framework araç sözleşmesini geriye uyumlu test edin

Model context protocol araç şemasını CI girdisi yapın. Araç adını veya zorunlu parametreyi değiştirmek, REST endpoint sürümlemesindeki kadar kırıcıdır; modelin önceki oturumdan taşıdığı araç çağrısı birkaç dakika sonra gelebilir. MCP Inspector ile staging ortamında araç listesini, input schema'yı ve hata cevaplarını elle doğrulayın; ardından JSON snapshot dosyasını pull request içinde diff edin. Inspector'ı yerel olarak npx @modelcontextprotocol/inspector komutuyla başlatabilirsiniz.

jq -S '.tools[] | {name, inputSchema}' build/mcp-tools.json   > build/mcp-tools.normalized.json
diff -u src/test/resources/mcp-tools.snapshot.json   build/mcp-tools.normalized.json

CI kuralı şu ayrımı yapmalıdır: yeni opsiyonel alan eklemek kabul edilebilir, mevcut alanı zorunlu yapmak veya tool adını değiştirmek onay gerektirir. Java programlama eğitimi veya bir java kursu sırasında genellikle DTO değişiminin yalnızca controller testleriyle korunduğu görülür; burada araç metadata'sını da test etmek gerekir. spring boot eğitimi içeriğinde bu testi Testcontainers PostgreSQL ile birleştirip gerçek sorgunun boş sonuç, yetkisiz kaynak ve çakışan durumlarını da çalıştırın.

@Test
void refundToolDoesNotRevealOtherCustomersInvoice() {
  RefundResult result = service.request(customerA,
      new RefundRequest(invoiceOfCustomerB, new BigDecimal("10.00")));

  assertThat(result.status()).isEqualTo(RefundStatus.NOT_FOUND_OR_FORBIDDEN);
  assertThat(result.message()).doesNotContain(invoiceOfCustomerB);
}

Bu yaklaşım spring framework kullanan ekiplerin mevcut contract-test pratiğini MCP'ye taşır. java eğitimi, java programlama eğitimi ve java kursu materyallerinde araç metodu eklemek kolay gösterilebilir; üretimde zor olan kısım, modelin serbest metin üretimini dar şemaya, sabit yetkiye ve ölçülebilir geri uyumluluğa bağlamaktır.

Sık Sorulan Sorular

Spring MCP aracında kullanıcı yetkisi nasıl doğrulanır?

Araç argümanındaki kullanıcı veya tenant kimliğine güvenmeyin. Spring Security SecurityContext içindeki JWT subject veya authority bilgisini alın, repository sorgusuna customerId = :authenticatedCustomerId koşulu ekleyin ve 0 satır sonucunu tek bir NOT_FOUND_OR_FORBIDDEN koduna eşleyin.

Spring AI ve model context protocol araçlarında p95 gecikme nasıl ölçülür?

Micrometer Timer ile sabit tool etiketi taşıyan histogram yayınlayın, Prometheus histogram_quantile sorgusuyla p95'i hesaplayın ve aynı yük senaryosunda jcmd ile 5 dakikalık JFR kaydı alın. Değişiklikten önce ve sonra aynı VU sayısı, aynı veri kümesi ve aynı connection pool ayarlarıyla SQL sorgu sayısını da karşılaştırın.

Spring Data JPA ile MCP aracına entity döndürmek neden risklidir?

Hibernate ORM entity'si Jackson serileştirmesi sırasında lazy ilişki yükleyebilir, çift yönlü ilişki döngüsü oluşturabilir ve DTO'da olmaması gereken alanları dışarı verebilir. Interface projection veya constructor projection kullanın, dönüşü explicit record'a dönüştürün ve araç sonucu için kayıt ile byte üst sınırı koyun.

Java microservices içinde Spring MCP araç sözleşmesi nasıl test edilir?

MCP Inspector ile staging şemasını kontrol edin, tools listesinden üretilen normalize JSON'u jq -S ile snapshot'a karşı diff edin. Ayrıca Testcontainers ile yetkisiz kaynak, mevcut olmayan kaynak ve durum çakışması testlerini gerçek PostgreSQL üzerinde çalıştırın; yalnızca mock repository kullanmak SQL koşullarındaki yetki hatalarını yakalamaz.

AI / LLM Discovery

Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt