Extensões
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Uma extensão é um pacote opcional de comportamento MCP reunido sob um único identificador.
Em um servidor, ela pode contribuir com ferramentas (tools), recursos e novos métodos de requisição, e pode envolver tools/call. Em um cliente, ela pode reivindicar formatos extras de resultado de tools/call e observar notificações de fornecedores. Cada lado se anuncia no seu próprio capabilities.extensions, e nada muda para quem não pediu nada. Esse é o contrato (SEP-2133), e ele tem uma regra de ouro: extensões vêm desligadas por padrão.
Usando uma extensão
Passe as instâncias na construção:
from mcp.server.apps import Apps
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("demo", extensions=[Apps()])
Pronto. O servidor agora anuncia io.modelcontextprotocol/ui em capabilities.extensions e serve tudo o que a extensão contribui.
Apps é a extensão de referência embutida, e ela tem uma página própria: MCP Apps.
Note
As extensões são fixadas na construção. Não existe um add_extension para chamar depois: o mapa de capacidades de um servidor não deve mudar enquanto há clientes conectados a ele.
O mapa de capacidades viaja em server/discover, que é um caminho da 2026-07-28. Um handshake initialize legado não tem onde colocá-lo, então um cliente legado simplesmente não enxerga a extensão. Projete pensando nisso: uma extensão amplia um servidor, ela não pode ser a única forma de usá-lo.
Escrevendo a sua
Herde de Extension e sobrescreva apenas o que precisar. Todo método tem um padrão.
O identificador
from mcp.server.extension import Extension
class Stamps(Extension):
identifier = "com.example/stamps"
O identificador é uma string vendor-prefix/name que segue a gramática de chaves _meta da especificação: rótulos separados por ponto (cada um começa com uma letra e termina com uma letra ou dígito), uma barra e então o nome. Ele é validado quando a classe é definida, então um erro de digitação não espera o servidor subir:
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
Use como prefixo um domínio que você controla. io.modelcontextprotocol/* é reservado para extensões especificadas pelo próprio projeto MCP.
Contribuindo com ferramentas
A menor extensão útil é uma ferramenta e um mapa de configurações:
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()retornaToolBindings. O servidor registra cada uma exatamente como se você tivesse chamadomcp.add_tool(...)por conta própria: mesma geração de schema, mesma injeção deContext, tudo igual.settings()é o valor anunciado emcapabilities.extensions["com.example/stamps"]. Retorne{}(o padrão) para anunciar a extensão sem configurações.- A extensão nunca recebe o servidor. Ela declara contribuições como dados; o
MCPServeras consome. Não existe umself.serverpara modificar.
E main() é a prova, um cliente em memória direto contra mcp:
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')]
Servindo seus próprios métodos
Uma extensão pode registrar novos métodos de requisição: seus próprios verbos, servidos ao lado dos da especificação:
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']
SearchParamsherda deRequestParams, então o envelope_metade 2026 é analisado de forma uniforme e seu handler recebe parâmetros validados, nunca um dict cru. Limite o que o cliente controla:Field(ge=1, le=100)rejeita umlimitabsurdo antes que seu código aloque qualquer coisa para ele.require_client_extension(ctx, EXTENSION_ID)é a barreira: um cliente que não declarou a extensão recebe o erro-32021(capacidade obrigatória do cliente ausente), com o payloadrequiredCapabilitieslegível por máquina que a especificação pede.protocol_versions=frozenset({"2026-07-28"})fixa o método em uma única versão de protocolo. Em qualquer outra versão o cliente recebeMETHOD_NOT_FOUND, exatamente como se o método não existisse ali. Para esse cliente, não existe.
Os métodos são estritamente aditivos. O SDK impõe isso na construção, não em tempo de execução:
- Um
MethodBindingpara um método definido pela especificação (tools/list,completion/complete, ...) lançaValueErrorquando o binding é construído. Os verbos centrais pertencem ao servidor. - Duas extensões vinculando o mesmo método lançam quando a segunda se registra. A última escrita vencer é como plugins corrompem uns aos outros; não fazemos isso.
- Um conjunto
protocol_versionsvazio também lança: um método que nunca pode ser servido é um bug, não uma configuração.
O lado do cliente
O main() do mesmo arquivo é a história inteira do cliente, com as duas metades:
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)])declara a extensão. As declarações viramClientCapabilities.extensions: em uma conexão 2026-07-28 o mapa viaja no envelope_metade cada requisição, então o servidor o vê em toda requisição; em uma conexão legada ele vai no handshakeinitialize. O código do servidor não se importa com qual:require_client_extension(ctx, ...)ectx.session.check_client_capability(...)leem a fonte certa nos dois caminhos.- Métodos de fornecedor descem uma camada para
client.session.send_request(...);Clientsó ganha métodos de primeira classe para verbos da especificação.send_requestaceita qualquer subclasse deRequest, então a requisição do fornecedor passa como está.
Interceptando tools/call
O único hook interceptador. Sobrescreva intercept_tool_call para observar, curto-circuitar ou vetar uma chamada de ferramenta:
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é oCallToolRequestParamsvalidado: você recebeparams.nameeparams.argumentssem tocar em JSON cru. É também o que decide qual chamada de ferramenta é executada: passar um contexto reescrito porcall_nextmuda o que o handler observa emctx, não a invocação da ferramenta. Reescrita de requisição no nível do protocolo pertence ao Middleware.call_next(ctx)executa o resto da cadeia e retorna o resultado do handler. Retorne-o sem alterações (observar), retorne outra coisa (substituir) ou lance umMCPError(recusar). O que você retornar é serializado como qualquer resultado de handler, incluindo o carimbo de identidadeserverInfoda era 2026, então um interceptador que curto-circuita nunca produz uma resposta anônima ou fora do schema.- Com várias extensões, os interceptadores se aninham na ordem de registro: a primeira extensão em
extensions=[...]é a mais externa. - A implementação padrão é um repasse direto, e um servidor cujas extensões nunca sobrescrevem esse hook mantém o handler puro de
tools/callintocado. Você não paga pelo que não usa.
O hook envolve tools/call e nada mais. Para preocupações que valem para toda mensagem, use o Middleware. É para isso que ele serve.
Usando uma extensão de cliente
Uma extensão de cliente é o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um único identificador. Passe as instâncias para Client(extensions=[...]) e chame as ferramentas normalmente:
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", ...) retorna um CallToolResult comum, como toda outra chamada. O que a extensão mudou: o servidor agora pode responder a buy com um formato de resultado receipt em vez de um resultado final, e Receipts o finaliza (aqui, resgatando o recibo com uma chamada seguinte) antes de call_tool retornar. Nada muda no ponto da chamada.
Tire a extensão e nada disso existe: a barreira do servidor recusa um cliente que não a declarou (erro -32021), e um formato reivindicado vindo de um servidor que pula a barreira falha na validação, exatamente como a especificação exige para um resultType não reconhecido. Desligado por padrão, nas duas pontas da conexão.
Para anunciar um identificador sem nenhum comportamento do lado do cliente (o servidor faz a barreira pela capacidade, o cliente não faz nada, como no cliente de busca acima), use advertise():
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
Escrevendo uma extensão de cliente
Herde de ClientExtension e sobrescreva apenas o que precisar. Três tipos de contribuição, cada um com um padrão: settings(), claims() e 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')]
- O identificador segue a mesma gramática do servidor, validada quando a classe é definida.
claims()retornaResultClaims: uma tag de protocolo, o modelo que a analisa e o resolvedor que a finaliza. O modelo precisa fixar a tag comresult_type: Literal["receipt"]e não pode herdar dos tipos de resultado centrais do verbo; as duas coisas são impostas quando a claim é construída. Campos de fornecedor comoreceipt_tokenviajam pela conexão como estão: um formato substituído chega ao cliente literalmente.- O resolvedor recebe o modelo analisado e um
ClaimContext;ctx.sessioné o mesmo handle público queclient.session, então as chamadas seguintes são chamadas comuns de sessão. Ele retorna oCallToolResultnormal do verbo. settings()é o valor anunciado emClientCapabilities.extensions[identifier], lido uma vez na construção doClient.
notifications() declara notificações de servidor de fornecedor a observar:
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
O handler recebe parâmetros validados um de cada vez, na ordem de despacho. Ele observa; não pode vetar nem responder.
Duas regras discretas. As claims ficam ativas apenas em conexões 2026-07-28, e o anúncio de capacidade as acompanha: em uma conexão legada as claims se dissolvem e o identificador sai do anúncio junto com elas, então o cliente nunca anuncia uma extensão cujos formatos ele rejeitaria. E quando você mesmo quer o formato reivindicado em vez do resolvedor, chame client.session.call_tool(..., allow_claimed=True); sem essa flag, um formato reivindicado que chega a um chamador no nível da sessão lança UnexpectedClaimedResult.
Verbos de extensão
Os métodos de requisição próprios de uma extensão não precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de mcp.types.Request e passa por client.session.send_request, como em Servindo seus próprios métodos. Um acréscimo: quando uma chave de params precisa viajar no header Mcp-Name (especificações de extensão como tasks exigem isso para seus verbos), o tipo de requisição declara name_param:
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
A sessão espelha params["jobId"] em Mcp-Name em todo caminho de envio, e um valor ausente falha de forma explícita em vez de omitir silenciosamente um header obrigatório.
O que uma extensão não pode fazer
A superfície de contribuição é fechada de propósito. No servidor: configurações, ferramentas, recursos, métodos, um interceptador de tools/call. No cliente: configurações, claims de resultado, bindings de notificação. Uma extensão não pode:
- Alcançar o host. Ela declara dados; não guarda nenhuma referência ao servidor nem ao cliente.
- Substituir comportamento central. Métodos da especificação e tags de resultado centrais são rejeitados na construção (
initializeé reservado pelo runner sem exceção); já um binding de notificação encoberto pelo vocabulário central fica em silêncio com um aviso. - Registrar-se depois. Depois que
MCPServer(...)ouClient(...)retorna, o conjunto de extensões é o que é.
Se você está brigando com essas paredes, não está escrevendo uma extensão. Está escrevendo um fork. As paredes são a funcionalidade: um usuário que lê extensions=[Apps(), Stamps()] sabe tudo o que essas duas podem ter tocado.