Skip to content
mobius1qwe edited this page Nov 10, 2026 · 1 revision

Home > MCP 🚧

Servidor MCP (Model Context Protocol)

Important

Em desenvolvimento (2.0). Ainda não está numa versão lançada e pode mudar.

Vale para a 2.0 · Delphi XE em diante e Lazarus/FPC 3.2 em diante · Nível: intermediário

Em uma frase: o MCP é o protocolo que assistentes de IA usam para chamar ferramentas, ler recursos e pedir prompts de um programa seu; o TRALMCPServer declara tudo isso e o TRALMCPModule (HTTP) ou o TRALMCPStdio (console) o entrega.

Você vai precisar: o pacote RALModuleMCPPackage (paleta RAL - Modules; o cliente TRALMCPClient fica em RAL - Client), que requer o RALModuleJSONRPCPackage (ver JSON-RPC). Para HTTP, um motor; para stdio, só um programa de console.

Warning

O RAL fala só a revisão 2026-07-28 do MCP, que não tem estado: não há initialize nem sessão, e cada pedido leva a versão e as capacidades do cliente. Um cliente que fale só revisões anteriores recebe o erro UnsupportedProtocolVersion (-32022). O MCP Inspector e os clientes de IA que você já tem podem ainda falar essas revisões antigas e não conseguir conectar enquanto não migrarem.

O servidor: TRALMCPServer

O TRALMCPServer não conhece transporte: guarda o que o servidor oferece e responde às mensagens.

propriedade
Tools as ferramentas que o modelo pode chamar
Resources / ResourceTemplates conteúdos que o cliente lê por URI, fixos ou por modelo (file:///docs/{path})
Prompts modelos de prompt que o usuário escolhe, com argumentos
OnComplete completa argumentos de prompts e recursos (até 100 valores)
ServerName, ServerVersion, ServerTitle, ServerDescription, WebsiteURL o que o servidor diz de si (serverInfo)
Instructions texto que ensina o modelo a usar o servidor
CacheTTL / CacheScope por quanto tempo (ms; 0 = nada) e para quem (mcsPrivate, mcsPublic) o cliente pode guardar listas e leituras
PageSize itens por página nas listas; 0 = tudo numa página
StateKey / StateLife segredo (HMAC) e validade (s, 600 por padrão) do estado dos pedidos de dados; ver abaixo
TaskTTL, TaskPollInterval, TaskStore tarefas; ver Tarefas

Ferramentas

uses RALMCPServer;

Servidor := TRALMCPServer.Create(Self);
Servidor.ServerName := 'minha-loja';
Servidor.ServerVersion := '1.0';
Servidor.Instructions := 'Use "estoque" para consultar o saldo de um produto.';

Ferramenta := Servidor.Tools.AddTool('estoque', 'Saldo de um produto',
  {$IFDEF FPC}@{$ENDIF}Estoque);
Ferramenta.Params.AddParam('codigo', mptString, 'Código do produto');
Ferramenta.Params.AddParam('deposito', mptInteger, 'Número do depósito', False);
Ferramenta.Hints := [thReadOnly, thIdempotent];

procedure TForm1.Estoque(Sender: TObject; ACall: TRALMCPCall; AResult: TRALMCPToolResult);
begin
  ACall.Progress(1, 2, 'consultando');
  AResult.AddText('saldo de ' + ACall.Argument('codigo').AsString + ': 42');
end;
propriedade da ferramenta
Name 1 a 128 caracteres: letras, dígitos, _, - e .; único e com distinção de maiúsculas
Description, Title para o modelo e para pessoas
Params parâmetros declarados (Name, ParamType, Required, Description, Values = enum); deles sai o inputSchema, e o RAL confere o pedido
InputSchema um JSON Schema escrito à mão, no lugar do gerado por Params
Header (do parâmetro) o x-mcp-header: o cliente copia o valor para o cabeçalho Mcp-Param-<Header>; só para texto, inteiro e booleano que não sejam segredo
OutputSchema o schema do conteúdo estruturado; com ele a ferramenta tem de chamar AResult.SetStructured
Hints thReadOnly, thDestructive, thIdempotent, thOpenWorld; os clientes os tratam como não confiáveis
Roles papéis (vírgula = qualquer um) que o usuário autenticado precisa; sem eles a ferramenta nem aparece
RunAsTask roda como tarefa

No OnCall, o ACall traz Argument('nome') (nil se faltar), Arguments, ClientName, HasCapability('elicitation') e Cancelled (o chamador foi embora ou cancelou: pare o trabalho). ACall.Progress(atual, total, mensagem) manda notifications/progress e ACall.Log('info', '"texto"') manda notifications/message (o dado é texto JSON); os dois só saem se o cliente pediu e o transporte envia ao vivo.

O resultado: TRALMCPToolResult

método
AddText bloco de texto
AddImage(base64, mime), AddAudio(base64, mime) imagem e áudio
AddResourceLink(uri, nome, mime, descrição) aponta um recurso
AddResourceText(uri, mime, texto), AddResourceBlob(uri, mime, base64) recurso embutido
SetStructured(TRALJSON) conteúdo estruturado (passa a ser dele) e espelhado num bloco de texto
Fail('mensagem') marca isError: a ferramenta falhou na execução e o modelo lê a mensagem, ao contrário de um erro de protocolo
Ferramenta.OutputSchema := '{"type":"object","properties":{"saldo":{"type":"number"}}}';

procedure TForm1.Saldo(Sender: TObject; ACall: TRALMCPCall; AResult: TRALMCPToolResult);
begin
  if ACall.Argument('codigo').AsString = '' then
    AResult.Fail('código vazio')
  else
    AResult.SetStructured(TRALJSON.ParseJSON('{"saldo":42}'));
end;

Recursos e prompts

Servidor.Resources.AddResource('file:///config.json', 'config', {$IFDEF FPC}@{$ENDIF}LerConfig,
  'application/json');
Servidor.ResourceTemplates.AddTemplate('file:///docs/{path}', 'docs',
  {$IFDEF FPC}@{$ENDIF}LerDoc);
Servidor.Prompts.AddPrompt('revisar', 'Revisa um código',
  {$IFDEF FPC}@{$ENDIF}Revisar).Arguments.AddArgument('codigo', True);

procedure TForm1.LerConfig(Sender: TObject; ACall: TRALMCPCall;
  AContents: TRALMCPResourceContents);
begin
  AContents.AddText('{"modo":"teste"}', 'application/json');
end;

procedure TForm1.Revisar(Sender: TObject; ACall: TRALMCPCall; AResult: TRALMCPPromptResult);
begin
  AResult.AddText('user', 'Revise este código: ' + ACall.Argument('codigo').AsString);
end;

No modelo de URI, o OnRead recebe em AVariables os valores das variáveis (path). TRALMCPResourceContents tem AddText e AddBlob; TRALMCPPromptResult tem AddText(papel, texto), AddMessage e Description.

Pedindo dados ao usuário (MRTR)

Como não há sessão, uma ferramenta que precisa de uma resposta do usuário devolve um pedido (resultType: input_required) e o cliente chama de novo com a resposta (multi round-trip request). A ferramenta roda outra vez, então ela decide pelo InputResponse:

procedure TForm1.Entrar(Sender: TObject; ACall: TRALMCPCall; AResult: TRALMCPToolResult);
var
  Resposta: TRALJSON;
begin
  Resposta := ACall.InputResponse('usuario');
  if Resposta = nil then
  begin
    ACall.Elicit('usuario', 'Qual o seu nome?',
      TRALJSONObject(TRALJSON.ParseJSON(
        '{"type":"object","properties":{"nome":{"type":"string"}},"required":["nome"]}')));
    ACall.SetState('passo1');   // volta, assinado, na próxima rodada
    Exit;
  end;
  AResult.AddText('olá ' + Resposta.AsObject.Get('content').AsObject.Get('nome').AsString
    + ' (' + ACall.State + ')');
end;
  • Elicit(chave, mensagem, schema) pede um formulário; RequestInput(chave, objeto) faz qualquer pedido (elicitation/create, sampling/createMessage, roots/list). Sem a capacidade declarada pelo cliente o RAL levanta Missing capability (-32021).
  • InputRequired diz se a rodada já pede dados; InputResponse(chave) devolve a resposta (nil se não veio).
  • SetState/State: o servidor assina o estado com HMAC-SHA256 (preso ao método e ao nome da ferramenta), o cliente o devolve sem mudar, e um estado adulterado ou vencido (StateLife) é Invalid params. Sem StateKey a chave é aleatória por processo; atrás de um balanceador, ou entre reinícios, todos os servidores devem usar a mesma StateKey.

Transporte stdio

Para um programa de console que o cliente de IA inicia: uma mensagem JSON-RPC por linha na entrada padrão, uma resposta por linha na saída padrão. Cada linha é respondida em sua própria thread, para que um cancelamento alcance uma chamada em andamento (uma chamada cancelada não responde nada).

program MeuMCP;
{$IFDEF FPC}{$MODE DELPHI}{$ENDIF}
{$APPTYPE CONSOLE}
uses
  {$IFDEF FPC}{$IFDEF UNIX}cthreads,{$ENDIF}{$ENDIF}
  Classes, RALMCPServer, RALMCPStdio;
var
  Servidor: TRALMCPServer;
  Stdio: TRALMCPStdio;
begin
  Servidor := TRALMCPServer.Create(nil);
  Stdio := TRALMCPStdio.Create(nil);
  try
    // ... Servidor.Tools.AddTool(...) como acima
    Stdio.Server := Servidor;
    Stdio.Run;   // até a entrada acabar
  finally
    Stdio.Free;
    Servidor.Free;
  end;
end.

Nada além das mensagens MCP pode ir para a saída padrão (WriteLn do seu código estraga o protocolo): escreva diagnósticos em ErrOutput, que é para onde o RAL manda os erros internos. Run(AEntrada, ASaida) aceita streams, útil em teste. Para o cliente iniciar o executável:

{
  "mcpServers": {
    "minha-loja": {
      "command": "C:\\apps\\MeuMCP.exe",
      "args": []
    }
  }
}

Transporte HTTP: TRALMCPModule

uses RALMCPModule;

Modulo := TRALMCPModule.Create(Self);
Modulo.MCPServer := Servidor;
Modulo.Server := ServidorHTTP;     // o TRALServer
Modulo.Path := '/mcp';             // o padrão
ServidorHTTP.Active := True;
propriedade
MCPServer o TRALMCPServer que responde
Path rota do endpoint (/mcp); só aceita POST: GET e DELETE respondem 405
AllowedOrigins origens (esquema://host[:porta]) que podem chamar de um navegador; vazia = só o próprio host e localhost. Outra origem recebe 403
AuthorizationServers, ResourceURL, Scopes autorização
  • A resposta é JSON; vira SSE (fluxo de eventos) quando o pedido traz progressToken ou logLevel no _meta, e sempre em subscriptions/listen. Isso exige um motor que envie ao vivo (veja a tabela em Resposta em fluxo); nos outros, Progress e Log não chegam antes do fim.
  • O cliente manda MCP-Protocol-Version, Mcp-Method e Mcp-Name (o nome da ferramenta, do recurso ou do prompt) e os Mcp-Param-* dos parâmetros com Header. O RAL confere os cabeçalhos com o corpo e, se divergem, responde o erro -32020 (header mismatch).

Autorização

Para exigir login, ligue ao servidor HTTP um plugin de autenticação (por exemplo TRALServerOAuth2, ver Autenticação) e preencha no módulo:

Modulo.AuthorizationServers.Add('https://auth.exemplo.com');
Modulo.ResourceURL := 'https://api.exemplo.com/mcp';   // o "audience" dos tokens (RFC 8707)
Modulo.Scopes.Add('mcp:tools');
OAuth2.ResourceMetadata := Modulo.ResourceMetadataURL;

Com AuthorizationServers, o módulo serve o metadado do recurso protegido (RFC 9728) em /.well-known/oauth-protected-resource, que o cliente de IA lê para descobrir onde obter o token; ResourceMetadataURL é o endereço desse documento, que o TRALServerOAuth2 informa no WWW-Authenticate do 401. As Roles das ferramentas valem para o usuário autenticado.

Avisos ao cliente

Clientes que abrem subscriptions/listen recebem avisos quando algo muda. No servidor:

Servidor.Tools.AddTool(...);
Servidor.NotifyToolsChanged;           // a lista de ferramentas mudou
Servidor.NotifyResourcesChanged;       // a lista de recursos mudou
Servidor.NotifyPromptsChanged;         // a lista de prompts mudou
Servidor.NotifyResourceUpdated('file:///config.json');  // o conteúdo mudou

Listening diz se há alguém ouvindo e StopListens encerra as conexões (ao desligar o servidor).

Tarefas

A extensão io.modelcontextprotocol/tasks roda uma chamada longa em segundo plano: marque Ferramenta.RunAsTask := True. Se o cliente declara a extensão, tools/call responde na hora com a tarefa (resultType: task, com taskId) e a ferramenta roda numa thread; o cliente acompanha por tasks/get, responde pedidos de dados por tasks/update e para por tasks/cancel (ACall.Cancelled fica verdadeiro). Quem não declara a extensão recebe a resposta quando a chamada termina.

propriedade
TaskTTL quanto tempo a tarefa é guardada, em ms desde a criação (1 h por padrão)
TaskPollInterval intervalo de consulta sugerido ao cliente, em ms (1000)
TaskStore um TRALKeyValueStore onde o resumo das tarefas também fica, para que tasks/get funcione em qualquer servidor que o compartilhe; sem ele, só na memória

Cliente: TRALMCPClient

Para chamar um servidor MCP de dentro do seu programa: ligue-o a um TRALClient (motor, BaseURL, TLS e autenticação).

membro
Client, Path o cliente HTTP e a rota do endpoint (/mcp)
Capabilities JSON das capacidades declaradas em todo pedido ('{"elicitation":{}}')
ClientName, ClientVersion o clientInfo
Discover resultado de server/discover
ListTools, ListResources, ListPrompts, ListAll todas as páginas
CallTool(nome, argumentos) chama a ferramenta; responde os pedidos de dados por OnInputRequest (até MaxRounds rodadas, 8)
ReadResource(uri), GetPrompt(nome, argumentos) leem recurso e prompt
Request(método, params, nome) qualquer método
OnNotification recebe o progresso e os logs que chegam antes da resposta

Os resultados são objetos JSON que você libera; um erro de protocolo levanta ERALJSONRPCError.

uses RALClient, RALMCPClient, RALJson;

Cliente.EngineType := 'Indy';
Cliente.BaseURL.Text := 'http://127.0.0.1:8080';
MCP := TRALMCPClient.Create(Self);
MCP.Client := Cliente;
MCP.Capabilities := '{"elicitation":{}}';
MCP.OnInputRequest := {$IFDEF FPC}@{$ENDIF}Pergunta;

Args := TRALJSON.ParseJSON('{"codigo":"A1"}');
try
  R := MCP.CallTool('estoque', Args);
  try
    Texto := R.Find('content').AsArray.Items[0].AsObject.Get('text').AsString;
  finally
    R.Free;
  end;
finally
  Args.Free;
end;

procedure TForm1.Pergunta(Sender: TObject; const AKey: StringRAL; ARequest: TRALJSON;
  var AResponse: TRALJSON);
begin
  AResponse := TRALJSON.ParseJSON('{"action":"accept","content":{"nome":"ana"}}');
end;

Erros comuns

sintoma causa
o cliente de IA ou o MCP Inspector não conecta (-32022) ele fala uma revisão anterior; o RAL só fala 2026-07-28
403 vindo de um navegador a origem não está em AllowedOrigins
405 GET ou DELETE no endpoint; só POST
-32020 um cabeçalho Mcp-* não bate com o corpo
Progress/Log só chegam no fim o motor não envia ao vivo, ou o cliente não mandou progressToken/logLevel
stdio "não responde" ou quebra o programa escreveu na saída padrão; use ErrOutput
o estado de um pedido de dados é recusado o servidor reiniciou ou há vários sem a mesma StateKey, ou passou o StateLife
Missing capability (-32021) o cliente não declarou elicitation/sampling/roots

Veja também

Começar

Servidor

MQTT 🚧

E-mail 🚧

Chatbot 🚧

Cliente

Motores

Avançado

Ajuda

🚧 = em desenvolvimento, pode mudar

Clone this wiki locally