Uzantılar
Makine çevirisi
Bu sayfa İngilizce dokümantasyondan otomatik olarak çevrildi; esas alınması gereken sürüm İngilizce sayfadır. Yanlış görünen bir şey varsa, nasıl bildireceğinizi Çeviriler sayfası açıklar.
Uzantı, tek bir tanımlayıcının arkasında toplanmış, isteğe bağlı olarak etkinleştirilen bir MCP davranışı paketidir.
Sunucuda araç, kaynak ve yeni istek metotları katkısında bulunabilir, tools/call isteğini sarmalayabilir. İstemcide ek tools/call sonuç biçimlerini sahiplenebilir ve satıcıya özgü bildirimleri gözlemleyebilir. Her iki taraf da kendi capabilities.extensions alanı altında duyuru yapar ve bunu istememiş hiç kimse için hiçbir şey değişmez. Sözleşme budur (SEP-2133) ve tek bir altın kuralı var: uzantılar varsayılan olarak kapalıdır.
Bir uzantı kullanma
Örnekleri oluşturma sırasında geçirin:
from mcp.server.apps import Apps
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("demo", extensions=[Apps()])
Bu kadar. Sunucu artık capabilities.extensions altında io.modelcontextprotocol/ui duyurur ve uzantının katkıda bulunduğu her şeyi sunar.
Apps yerleşik başvuru uzantısıdır ve kendi sayfası var: MCP Apps.
Note
Uzantılar oluşturma sırasında sabitlenir. Sonradan çağrılacak bir add_extension yoktur: istemciler bağlıyken bir sunucunun yetenek eşlemesi değişmemelidir.
Yetenek eşlemesi server/discover ile taşınır; bu da bir 2026-07-28 yoludur. Eski nesil initialize el sıkışmasında onu koyacak bir yer yoktur, bu yüzden eski nesil bir istemci uzantıyı görmez. Tasarımınızı buna göre yapın: bir uzantı sunucuyu zenginleştirir; sunucunun kullanılabilir olmasının tek yolu olmamalıdır.
Kendi uzantınızı yazma
Extension'dan alt sınıf türetin ve yalnızca ihtiyaç duyduklarınızı geçersiz kılın. Her metodun bir varsayılanı var.
Tanımlayıcı
from mcp.server.extension import Extension
class Stamps(Extension):
identifier = "com.example/stamps"
Tanımlayıcı, spesifikasyonun _meta anahtar dilbilgisini izleyen bir vendor-prefix/name dizesidir: noktayla ayrılmış etiketler (her biri bir harfle başlar, bir harf veya rakamla biter), bir eğik çizgi, ardından ad. Sınıf tanımlandığında doğrulanır; yani bir yazım hatası sunucunun açılmasını beklemez:
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
Önek olarak denetiminizdeki bir alan adı kullanın. io.modelcontextprotocol/*, MCP projesinin kendisinin belirlediği uzantılar içindir.
Araç katkısında bulunma
İşe yarar en küçük uzantı, bir araç ve bir ayarlar eşlemesidir:
from collections.abc import Sequence
from typing import Any
from mcp import Client
from mcp.server.extension import Extension, ToolBinding
from mcp.server.mcpserver import MCPServer
def stamp(text: str) -> str:
"""Stamp a message with the office seal."""
return f"[stamped] {text}"
class Stamps(Extension):
"""A purely additive extension: one tool, one capability entry."""
identifier = "com.example/stamps"
def settings(self) -> dict[str, Any]:
return {"sealed": True}
def tools(self) -> Sequence[ToolBinding]:
return [ToolBinding(fn=stamp)]
mcp = MCPServer("post-office", extensions=[Stamps()])
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.extensions)
# {'com.example/stamps': {'sealed': True}}
result = await client.call_tool("stamp", {"text": "hello"})
print(result.content)
# [TextContent(text='[stamped] hello')]
tools(),ToolBinding'ler döndürür. Sunucu her birini,mcp.add_tool(...)çağrısını kendiniz yapmışsınız gibi kaydeder: aynı şema üretimi, aynıContextenjeksiyonu, her şey aynı.settings(),capabilities.extensions["com.example/stamps"]konumunda duyurulan değerdir. Uzantıyı ayarsız duyurmak için{}(varsayılan) döndürün.- Uzantı sunucuyu hiçbir zaman almaz. Katkıları veri olarak beyan eder; bunları
MCPServertüketir. Değiştirilecek birself.serveryoktur.
Kanıtı da main(): doğrudan mcp'ye bağlanan bellek içi bir istemci:
from collections.abc import Sequence
from typing import Any
from mcp import Client
from mcp.server.extension import Extension, ToolBinding
from mcp.server.mcpserver import MCPServer
def stamp(text: str) -> str:
"""Stamp a message with the office seal."""
return f"[stamped] {text}"
class Stamps(Extension):
"""A purely additive extension: one tool, one capability entry."""
identifier = "com.example/stamps"
def settings(self) -> dict[str, Any]:
return {"sealed": True}
def tools(self) -> Sequence[ToolBinding]:
return [ToolBinding(fn=stamp)]
mcp = MCPServer("post-office", extensions=[Stamps()])
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.extensions)
# {'com.example/stamps': {'sealed': True}}
result = await client.call_tool("stamp", {"text": "hello"})
print(result.content)
# [TextContent(text='[stamped] hello')]
Kendi metotlarınızı sunma
Bir uzantı yeni istek metotları kaydedebilir: spesifikasyonunkilerin yanında sunulan kendi fiilleri:
from collections.abc import Sequence
from typing import Any, Literal
from pydantic import Field
import mcp.types as types
from mcp import Client
from mcp.client import advertise
from mcp.server.context import ServerRequestContext
from mcp.server.extension import Extension, MethodBinding
from mcp.server.mcpserver import MCPServer, require_client_extension
EXTENSION_ID = "com.example/search"
class SearchParams(types.RequestParams):
query: str
limit: int = Field(default=10, ge=1, le=100)
class SearchResult(types.Result):
items: list[str]
class SearchRequest(types.Request[SearchParams, Literal["com.example/search"]]):
method: Literal["com.example/search"] = "com.example/search"
params: SearchParams
async def search(ctx: ServerRequestContext[Any, Any], params: SearchParams) -> SearchResult:
require_client_extension(ctx, EXTENSION_ID)
return SearchResult(items=[f"{params.query}-{n}" for n in range(params.limit)])
class Search(Extension):
"""An extension that serves its own request method."""
identifier = EXTENSION_ID
def methods(self) -> Sequence[MethodBinding]:
return [
MethodBinding(
"com.example/search",
SearchParams,
search,
protocol_versions=frozenset({"2026-07-28"}),
)
]
mcp = MCPServer("catalog", extensions=[Search()])
async def main() -> None:
async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client:
request = SearchRequest(params=SearchParams(query="mcp", limit=3))
result = await client.session.send_request(request, SearchResult)
print(result.items)
# ['mcp-0', 'mcp-1', 'mcp-2']
SearchParams,RequestParams'tan türer; böylece 2026_metazarfı tek biçimde ayrıştırılır ve işleyiciniz ham bir dict değil, her zaman doğrulanmış parametreler alır. İstemcinin denetlediği şeyi sınırlayın:Field(ge=1, le=100), kodunuz onun için herhangi bir şey ayırmadan önce saçma birlimitdeğerini reddeder.require_client_extension(ctx, EXTENSION_ID)kontrol noktasıdır: uzantıyı beyan etmemiş bir istemci, spesifikasyonun istediği makine tarafından okunabilirrequiredCapabilitiesyüküyle birlikte-32021(gerekli istemci yeteneği eksik) hatasını alır.protocol_versions=frozenset({"2026-07-28"})metodu tek bir protokol sürümüne sabitler. Başka herhangi bir sürümde istemciMETHOD_NOT_FOUNDalır; sanki metot orada hiç yokmuş gibi. O istemci için gerçekten de yoktur.
Metotlar yalnızca ekleme niteliğindedir. SDK bunu çalışma zamanında değil, oluşturma sırasında uygular:
- Spesifikasyonda tanımlı bir metot (
tools/list,completion/complete, ...) için birMethodBinding, bağlama oluşturulurkenValueErrorfırlatır. Çekirdek fiiller sunucuya aittir. - Aynı metodu bağlayan iki uzantı, ikincisi kaydolurken hata fırlatır. Son yazan kazanır yaklaşımı, eklentilerin birbirini bozmasının yoludur; biz bunu yapmayız.
- Boş bir
protocol_versionskümesi de hata fırlatır: hiçbir zaman sunulamayacak bir metot bir yapılandırma değil, bir hatadır.
İstemci tarafı
Aynı dosyadaki main(), istemci tarafının tamamıdır; iki yarısıyla birlikte:
from collections.abc import Sequence
from typing import Any, Literal
from pydantic import Field
import mcp.types as types
from mcp import Client
from mcp.client import advertise
from mcp.server.context import ServerRequestContext
from mcp.server.extension import Extension, MethodBinding
from mcp.server.mcpserver import MCPServer, require_client_extension
EXTENSION_ID = "com.example/search"
class SearchParams(types.RequestParams):
query: str
limit: int = Field(default=10, ge=1, le=100)
class SearchResult(types.Result):
items: list[str]
class SearchRequest(types.Request[SearchParams, Literal["com.example/search"]]):
method: Literal["com.example/search"] = "com.example/search"
params: SearchParams
async def search(ctx: ServerRequestContext[Any, Any], params: SearchParams) -> SearchResult:
require_client_extension(ctx, EXTENSION_ID)
return SearchResult(items=[f"{params.query}-{n}" for n in range(params.limit)])
class Search(Extension):
"""An extension that serves its own request method."""
identifier = EXTENSION_ID
def methods(self) -> Sequence[MethodBinding]:
return [
MethodBinding(
"com.example/search",
SearchParams,
search,
protocol_versions=frozenset({"2026-07-28"}),
)
]
mcp = MCPServer("catalog", extensions=[Search()])
async def main() -> None:
async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client:
request = SearchRequest(params=SearchParams(query="mcp", limit=3))
result = await client.session.send_request(request, SearchResult)
print(result.items)
# ['mcp-0', 'mcp-1', 'mcp-2']
Client(..., extensions=[advertise(EXTENSION_ID)])uzantıyı beyan eder. BeyanlarClientCapabilities.extensionshâline gelir: 2026-07-28 bağlantısında eşleme istek başına_metazarfında taşınır, böylece sunucu onu her istekte görür; eski nesil bir bağlantıdainitializeel sıkışmasıyla taşınır. Sunucu kodu hangisi olduğuyla ilgilenmez:require_client_extension(ctx, ...)vectx.session.check_client_capability(...)her iki yolda da doğru kaynağı okur.- Satıcıya özgü metotlar bir katman aşağıya,
client.session.send_request(...)düzeyine iner;Clientyalnızca spesifikasyon fiilleri için birinci sınıf metotlar kazanır.send_requestherhangi birRequestalt sınıfını kabul eder, bu yüzden satıcıya özgü istek olduğu gibi geçer.
tools/call isteğini yakalama
Yakalayıcı nitelikteki tek kanca. Bir araç çağrısını gözlemlemek, kısa devre yapmak veya veto etmek için intercept_tool_call'u geçersiz kılın:
import logging
from typing import Any
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
from mcp.server.extension import Extension
from mcp.server.mcpserver import MCPServer
from mcp.types import CallToolRequestParams
logger = logging.getLogger(__name__)
class AuditLog(Extension):
"""Observe every tools/call without touching its result."""
identifier = "com.example/audit"
async def intercept_tool_call(
self,
params: CallToolRequestParams,
ctx: ServerRequestContext[Any, Any],
call_next: CallNext,
) -> HandlerResult:
logger.info("tool %r called", params.name)
return await call_next(ctx)
mcp = MCPServer("audited", extensions=[AuditLog()])
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
params, doğrulanmışCallToolRequestParamsnesnesidir: ham JSON'a dokunmadanparams.nameveparams.argumentselinizdedir. Hangi araç çağrısının çalışacağına karar veren de odur:call_nextüzerinden yeniden yazılmış bir bağlam geçirmek, araç çağrısını değil, işleyicininctxüzerinde gözlemlediğini değiştirir. İletim düzeyinde istek yeniden yazımı Middleware sayfasının konusudur.call_next(ctx)zincirin geri kalanını çalıştırır ve işleyicinin sonucunu döndürür. Onu değiştirmeden döndürün (gözlemleme), başka bir şey döndürün (değiştirme) ya da birMCPErrorfırlatın (reddetme). Ne döndürürseniz döndürün, 2026 neslininserverInfokimlik damgası dâhil, herhangi bir işleyici sonucu gibi serileştirilir; bu yüzden kısa devre yapan bir yakalayıcı hiçbir zaman anonim veya şema dışı bir yanıt üretmez.- Birden fazla uzantı olduğunda yakalayıcılar kayıt sırasına göre iç içe geçer:
extensions=[...]içindeki ilk uzantı en dıştadır. - Varsayılan gerçekleştirim doğrudan geçirir; uzantıları bu kancayı hiç geçersiz kılmayan bir sunucu, yalın
tools/callişleyicisini olduğu gibi korur. Kullanmadığınız şeyin bedelini ödemezsiniz.
Kanca tools/call isteğini sarmalar, başka hiçbir şeyi değil. Her iletiyi ilgilendiren konular için Middleware kullanın. Onun işi budur.
Bir istemci uzantısı kullanma
İstemci uzantısı, aynı sözleşmenin tüketen taraftan görünüşüdür: tek bir tanımlayıcının arkasında toplanmış bir istemci tarafı davranış paketi. Örnekleri Client(extensions=[...]) ile geçirin ve araçları normal şekilde çağırın:
from collections.abc import Sequence
from typing import Any, Literal
import mcp.types as types
from mcp import Client
from mcp.client import ClaimContext, ClientExtension, ResultClaim
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
from mcp.server.extension import Extension
from mcp.server.mcpserver import MCPServer, require_client_extension
EXTENSION_ID = "com.example/receipts"
class ReceiptResult(types.Result):
"""The claimed result shape; `result_type` pins the wire tag."""
result_type: Literal["receipt"] = "receipt"
receipt_token: str
class ReceiptIssuer(Extension):
"""Server half: answers `buy` with a receipt instead of a final result."""
identifier = EXTENSION_ID
async def intercept_tool_call(
self,
params: types.CallToolRequestParams,
ctx: ServerRequestContext[Any, Any],
call_next: CallNext,
) -> HandlerResult:
if params.name != "buy":
return await call_next(ctx)
require_client_extension(ctx, EXTENSION_ID)
return {"resultType": "receipt", "receiptToken": "r-117"}
class Receipts(ClientExtension):
"""Client half: claims the `receipt` shape and supplies the code that finishes it."""
identifier = EXTENSION_ID
def claims(self) -> Sequence[ResultClaim[Any]]:
return [ResultClaim(result_type="receipt", model=ReceiptResult, resolve=self._redeem)]
async def _redeem(self, claimed: ReceiptResult, ctx: ClaimContext) -> types.CallToolResult:
return await ctx.session.call_tool("redeem", {"token": claimed.receipt_token})
mcp = MCPServer("shop", extensions=[ReceiptIssuer()])
@mcp.tool()
def buy(item: str) -> types.CallToolResult:
"""Buy an item."""
raise NotImplementedError # ReceiptIssuer answers `buy` before the tool runs
@mcp.tool()
def redeem(token: str) -> str:
"""Exchange a receipt token for the goods."""
return f"goods for {token}"
async def main() -> None:
async with Client(mcp, extensions=[Receipts()]) as client:
result = await client.call_tool("buy", {"item": "lamp"})
print(result.content)
# [TextContent(text='goods for r-117')]
call_tool("buy", ...), diğer her çağrı gibi düz bir CallToolResult döndürür. Uzantının değiştirdiği şu: sunucu artık buy çağrısını nihai bir sonuç yerine receipt adlı bir sonuç biçimiyle yanıtlayabilir ve Receipts, call_tool dönmeden önce onu tamamlar (burada makbuzu bir takip çağrısıyla kullanarak). Çağrı yerinde hiçbir şey değişmez.
Uzantıyı çıkarırsanız bunların hiçbiri olmaz: sunucunun kontrol noktası onu beyan etmemiş bir istemciyi reddeder (hata -32021) ve kontrolü atlayan bir sunucudan gelen sahiplenilmiş bir biçim, spesifikasyonun tanınmayan bir resultType için gerektirdiği gibi doğrulamadan geçemez. Bağlantının her iki ucunda da varsayılan olarak kapalı.
İstemci tarafında hiçbir davranışı olmayan bir tanımlayıcıyı duyurmak için (sunucu yeteneği kontrol eder, istemci hiçbir şey yapmaz; yukarıdaki arama istemcisinde olduğu gibi) advertise() kullanın:
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
İstemci uzantısı yazma
ClientExtension'dan alt sınıf türetin ve yalnızca ihtiyaç duyduklarınızı geçersiz kılın. Her birinin varsayılanı olan üç katkı türü var: settings(), claims() ve notifications().
from collections.abc import Sequence
from typing import Any, Literal
import mcp.types as types
from mcp import Client
from mcp.client import ClaimContext, ClientExtension, ResultClaim
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
from mcp.server.extension import Extension
from mcp.server.mcpserver import MCPServer, require_client_extension
EXTENSION_ID = "com.example/receipts"
class ReceiptResult(types.Result):
"""The claimed result shape; `result_type` pins the wire tag."""
result_type: Literal["receipt"] = "receipt"
receipt_token: str
class ReceiptIssuer(Extension):
"""Server half: answers `buy` with a receipt instead of a final result."""
identifier = EXTENSION_ID
async def intercept_tool_call(
self,
params: types.CallToolRequestParams,
ctx: ServerRequestContext[Any, Any],
call_next: CallNext,
) -> HandlerResult:
if params.name != "buy":
return await call_next(ctx)
require_client_extension(ctx, EXTENSION_ID)
return {"resultType": "receipt", "receiptToken": "r-117"}
class Receipts(ClientExtension):
"""Client half: claims the `receipt` shape and supplies the code that finishes it."""
identifier = EXTENSION_ID
def claims(self) -> Sequence[ResultClaim[Any]]:
return [ResultClaim(result_type="receipt", model=ReceiptResult, resolve=self._redeem)]
async def _redeem(self, claimed: ReceiptResult, ctx: ClaimContext) -> types.CallToolResult:
return await ctx.session.call_tool("redeem", {"token": claimed.receipt_token})
mcp = MCPServer("shop", extensions=[ReceiptIssuer()])
@mcp.tool()
def buy(item: str) -> types.CallToolResult:
"""Buy an item."""
raise NotImplementedError # ReceiptIssuer answers `buy` before the tool runs
@mcp.tool()
def redeem(token: str) -> str:
"""Exchange a receipt token for the goods."""
return f"goods for {token}"
async def main() -> None:
async with Client(mcp, extensions=[Receipts()]) as client:
result = await client.call_tool("buy", {"item": "lamp"})
print(result.content)
# [TextContent(text='goods for r-117')]
- Tanımlayıcı, sunucununkiyle aynı dilbilgisini izler ve sınıf tanımlandığında doğrulanır.
claims(),ResultClaim'ler döndürür: iletilen veride bir etiket, onu ayrıştıran model ve onu tamamlayan çözümleyici. Model, etiketiresult_type: Literal["receipt"]ile sabitlemelidir ve fiilin çekirdek sonuç türlerinden türememelidir; her ikisi de sahiplenme oluşturulurken uygulanır.receipt_tokengibi satıcıya özgü alanlar ağ üzerinde olduğu gibi iletilir: yerine geçen bir biçim istemciye aynen ulaşır.- Çözümleyici, ayrıştırılmış modeli ve bir
ClaimContextalır;ctx.session,client.sessionile aynı herkese açık tutamaçtır, bu yüzden takip çağrıları sıradan oturum çağrılarıdır. Fiilin normalCallToolResult'ını döndürür. settings(),ClientCapabilities.extensions[identifier]konumunda duyurulan değerdir;Clientoluşturulurken bir kez okunur.
notifications(), gözlemlenecek satıcıya özgü sunucu bildirimlerini beyan eder:
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
İşleyici doğrulanmış parametreleri gönderim sırasına göre tek tek alır. Gözlemler; veto edemez ve yanıt veremez.
İki sessiz kural. Sahiplenmeler yalnızca 2026-07-28 bağlantılarında etkindir ve yetenek duyurusu da onları izler: eski nesil bir bağlantıda sahiplenmeler ortadan kalkar, tanımlayıcı da onlarla birlikte duyurudan düşer; böylece istemci, biçimlerini reddedeceği bir uzantıyı asla duyurmaz. Sahiplenilen biçimi çözümleyici yerine kendiniz istediğinizde ise client.session.call_tool(..., allow_claimed=True) çağırın; bu bayrak olmadan, oturum katmanındaki bir çağırana ulaşan sahiplenilmiş bir biçim UnexpectedClaimedResult fırlatır.
Uzantı fiilleri
Bir uzantının kendi istek metotları istemci tarafında kayıt gerektirmez. Satıcıya özgü bir istek türü mcp.types.Request'ten türer ve Kendi metotlarınızı sunma bölümündeki gibi client.session.send_request üzerinden gider. Tek bir ekleme var: bir params anahtarının Mcp-Name başlığında taşınması gerektiğinde (tasks gibi uzantı spesifikasyonları kendi fiilleri için bunu şart koşar) istek türü name_param beyan eder:
from collections.abc import Sequence
from typing import Any, Literal
import mcp.types as types
from mcp import Client
from mcp.client import advertise
from mcp.server.context import ServerRequestContext
from mcp.server.extension import Extension, MethodBinding
from mcp.server.mcpserver import MCPServer
EXTENSION_ID = "com.example/jobs"
class JobParams(types.RequestParams):
job_id: str
class JobStatus(types.Result):
status: str
class JobStatusRequest(types.Request[JobParams, Literal["com.example/jobs.status"]]):
method: Literal["com.example/jobs.status"] = "com.example/jobs.status"
params: JobParams
name_param = "jobId" # params["jobId"] rides the Mcp-Name header
async def job_status(ctx: ServerRequestContext[Any, Any], params: JobParams) -> JobStatus:
return JobStatus(status=f"{params.job_id} is running")
class Jobs(Extension):
"""An extension whose verb names its subject, so the header can route on it."""
identifier = EXTENSION_ID
def methods(self) -> Sequence[MethodBinding]:
return [MethodBinding("com.example/jobs.status", JobParams, job_status)]
mcp = MCPServer("worker", extensions=[Jobs()])
async def main() -> None:
async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client:
request = JobStatusRequest(params=JobParams(job_id="job-7"))
result = await client.session.send_request(request, JobStatus)
print(result.status)
# job-7 is running
Oturum, params["jobId"] değerini her gönderim yolunda Mcp-Name başlığına yansıtır; eksik bir değer ise gerekli bir başlığı sessizce atlamak yerine açıkça hata verir.
Bir uzantının yapamayacakları
Katkı yüzeyi bilerek kapalıdır. Sunucuda: ayarlar, araçlar, kaynaklar, metotlar, bir tools/call yakalayıcısı. İstemcide: ayarlar, sonuç sahiplenmeleri, bildirim bağlamaları. Bir uzantı şunları yapamaz:
- Barındıran nesneye erişemez. Veri beyan eder; sunucu veya istemci referansı tutmaz.
- Çekirdek davranışın yerine geçemez. Spesifikasyon metotları ve çekirdek sonuç etiketleri oluşturma sırasında reddedilir (
initializedoğrudan çalıştırıcı tarafından ayrılmıştır); çekirdek söz dağarcığının gölgelediği bir bildirim bağlaması ise bir uyarıyla sessizce devre dışı kalır. - Geç kayıt olamaz.
MCPServer(...)veyaClient(...)döndükten sonra uzantı kümesi neyse odur.
Bu duvarlarla boğuşuyorsanız bir uzantı yazmıyorsunuz. Bir fork yazıyorsunuz. Duvarlar özelliğin ta kendisidir: extensions=[Apps(), Stamps()] satırını okuyan bir kullanıcı, bu ikisinin dokunmuş olabileceği her şeyi bilir.