MCP Fusion/Core concepts/Middleware e contexto

Middleware e contexto

Pergunte à IA sobre a Vinkius

Componha autenticação, isolamento de locatários e limitação de taxa com derivação de contexto tipada: uma chamada a defineMiddleware enriquece ctx para todos os handlers seguintes, compilada em O(1) no build.

Middleware é onde um conector deixa de ser um conjunto de funções e se torna um serviço: a identidade é resolvida, os locatários são isolados, o tráfego é limitado e cada chamada é auditada. O modelo de middleware do MCP Fusion é pequeno, tipado de ponta a ponta e compilado antes da primeira requisição.

Duas formas, um conceito

Derivação de contexto é a forma idiomática. Um middleware retorna as partes que adiciona a ctx:

typescript
import { initMCPFusion } from '@mcpfusion/core';

interface AppContext {
  db: PrismaClient;
  user?: { id: string; role: 'viewer' | 'admin' };
}

const f = initMCPFusion<AppContext>();

const withUser = f.middleware(async (ctx) => {
  const token = ctx.headers?.authorization;
  const user = await verify(token);
  if (!user) throw f.error('UNAUTHORIZED', 'Sign in first');
  return { user };
});

Uso da tool a conecta:

typescript
f.query('billing.list_invoices')
  .use(withUser)
  .handle(async (input, ctx) => {
    return ctx.db.invoices.findMany({
      where: { userId: ctx.user.id },
    });
  });

O sistema de tipos transporta a derivação: .use(withUser) muda o tipo de contexto do builder para AppContext & { user }, então ctx.user compila dentro do handler e remover o middleware quebra o build. Não há um cast em tempo de execução para descobrir em produção.

A forma clássica também existe

O middleware que precisa agir antes e depois, ou interromper o fluxo, usa (ctx, args, next):

typescript
import type { MiddlewareFn } from '@mcpfusion/core';

const audit: MiddlewareFn<AppContext> = async (ctx, args, next) => {
  const started = Date.now();
  const result = await next();
  ctx.audit?.({ tool, args, ms: Date.now() - started });
  return result;
};

Retornar um ToolResponse a partir de um middleware interrompe o fluxo: next() nunca é chamado e essa resposta é a resposta. É assim que inputFirewall e rateLimit rejeitam uma chamada sem tocar no handler. Esquecer return next() gera um aviso único no console, porque todo desenvolvedor comete esse erro uma vez.

Lançar uma exceção também funciona: throw toolError('NOT_FOUND', ...) passa pelo pipeline com o código e a recuperação intactos. Qualquer outro valor lançado é envolvido como INTERNAL_ERROR.

Ordem e escopo

Três escopos se combinam em uma cadeia por ação:

EscopoDeclarado emPosição
Globalf.middleware() e no nível do registrymais externo
GrupoActionGroupBuilder.use()intermediário
Tool ou ação.use() no buildermais interno

O middleware global executa primeiro, com a autenticação antes de tudo, e o middleware por ação executa por último, mais próximo do seu handler. As cadeias são compiladas em buildToolDefinition() em um closure por ação, então o caminho da requisição é uma chamada, não um loop sobre um array.

Os componentes integrados

Três middlewares vêm com o framework e são middlewares comuns, não mágica:

  • inputFirewall({ judge }): LLM-as-Judge nos argumentos, falha fechada e retorna INPUT_REJECTED
  • rateLimit({ windowMs, limit, keyFn }): janela deslizante sobre timestamps; requisições rejeitadas não são registradas, então um cliente abusivo não pode estender o próprio bloqueio
  • auditTrail({ sink, hashArgs }): emite um evento por chamada com um digest SHA-256 dos argumentos, nunca os próprios argumentos

O middleware de autenticação vem dos pacotes dedicados: requireJwt, requireApiKey e requireAuth. Consulte Autenticação.

Isolamento por construção

contextFactory executa por requisição e retorna um objeto novo; o middleware então grava as chaves derivadas nele com uma proteção que ignora __proto__, constructor e prototype. Duas requisições concorrentes não podem compartilhar um id de locatário, um usuário ou um handle de banco de dados, mesmo quando atingem o mesmo processo de longa duração. Em um transporte stateless, cada requisição recebe um servidor novo; em stdio, a mesma garantia vem do objeto de contexto por requisição.

Esse é o mecanismo por trás de Conectores multi-tenant: o isolamento é uma propriedade do runtime, não uma disciplina que seus handlers precisam lembrar.

Próximos passos