Integrar o seu serviço
Esta é a documentação para quem tem um sistema — um CRM, uma agenda, um controle de estoque — e quer que ele possa ser operado por conversa dentro do OkMigo. Ela não pressupõe que você nos conheça: começa explicando o que é o produto e o que a integração muda para o seu cliente, e termina com um pedido completo, mensagem por mensagem.
X-OkMigo-Tenant. O que ainda é pouco é a quantidade: são
duas mãos cheias de chamadas, não meses de produção. Na prática, quem
chegar agora é o primeiro de fora, e o caminho é curto — escreva para
[email protected] com o endereço do seu servidor e a
gente conversa antes de você escrever código.O que é o OkMigo
Um amigo de IA com memória própria — e, para um negócio, quem opera o balcão junto.
O OkMigo é um assistente de IA pessoal. A diferença dele para os que você conhece não é o modelo: é que a memória fica com o usuário, num banco de dados nosso, e o modelo por trás é trocável. Ele lembra do que a pessoa contou semanas atrás, e a pessoa pode ler e apagar cada coisa que ele aprendeu.
Uma pessoa tem um amigo. Quem tem um comércio ganha também um sócio: um segundo assistente, cuja memória é a do negócio e não a de quem fala com ele. Vários funcionários podem operar o mesmo sócio, e ele sabe quem disse o quê.
Um dono de padaria abre o OkMigo e escreve "o fornecedor de farinha atrasou de novo". Isso vira memória do negócio. Meses depois ele pergunta "quantas vezes o moinho atrasou esse ano?" e ele responde. Não há formulário, não há cadastro, não há tela para configurar — ele conversa. É essa a experiência que a sua integração estende.
O que “integrar” significa, na prática
O seu sistema passa a ser operável por conversa, sem que o dono aprenda nada novo.
Hoje o sócio só sabe o que foi contado a ele. Ele não sabe quantos pedidos entraram, quem tem consulta amanhã, quanto tem em estoque — porque isso mora no seu sistema.
Sem integração— Marca a Ana pra sexta 10h.
— Anotei aqui pra eu lembrar. Mas eu não consigo mexer na sua agenda; você precisa abrir o sistema e marcar por lá.
— Marca a Ana pra sexta 10h.
— Sexta às 10h está livre. Vou criar o agendamento da Ana para corte + barba, 1h, sexta 22/08 às 10h. Confirma?
— confirma
— Feito. Agendamento criado.
Quem executou foi o seu sistema, com a credencial daquele negócio. O dono não abriu o seu app, não aprendeu tela nova e não saiu da conversa — e continua sendo o seu produto que guarda o dado e manda nas regras.
Para você isso significa: mais uso do seu sistema, por um caminho que você não precisa construir nem manter. Você não escreve integração com a gente; você publica um servidor MCP e a gente fala com ele.
Como funciona por dentro
Você nunca fala com o modelo de IA. Você recebe uma chamada de função e devolve dado.
┌──────────────────────────────────────────────────────────┐
│ o dono do negócio conversa com o sócio dele no OkMigo │
└───────────────────────────┬──────────────────────────────┘
│ o modelo decide que isso é
│ do serviço "AcmeCRM"
▼
┌──────────────────────────────────────────────────────────┐
│ PONTE · roda na NOSSA infraestrutura │
│ · valida os parâmetros contra o schema que VOCÊ declarou│
│ · pede confirmação ao dono se a operação escreve │
│ · registra a chamada para o dono poder auditar │
│ · não guarda nada do seu sistema │
└───────────────────────────┬──────────────────────────────┘
│ MCP sobre HTTP
│ Authorization: Bearer <credencial DELE>
▼
┌──────────────────────────────────────────────────────────┐
│ O SEU SERVIDOR MCP · na sua infraestrutura │
│ na frente da sua API de sempre │
└──────────────────────────────────────────────────────────┘
A ponte é um serviço nosso. Ela traduz e esquece: não copia os seus dados, não mantém cache do seu catálogo, não tem banco. Cada negócio que passa a usar o seu serviço vira uma linha de configuração do nosso lado — não um deploy, não um código novo. É por isso que dizemos sim rápido.
O modelo de linguagem nunca fala com você. Ele escolhe qual função chamar e com quais argumentos; a ponte valida esses argumentos contra o schema que você publicou e só então faz a chamada. O que volta, quem interpreta e transforma em frase é o assistente — você devolve dado estruturado.
O que você precisa construir
Um servidor MCP na frente da API que você já tem. Nada mais.
MCP (Model Context Protocol) é um padrão aberto para expor funções a assistentes de IA. Ele existe em várias linguagens e é o mesmo protocolo usado por outras ferramentas do mercado — não é nada nosso.
Exigimos MCP por um motivo prático: ele se descreve sozinho. Seu servidor publica a lista de funções, com tipos e descrições, e a gente aprende tudo lendo essa lista. É o que permite integrar o seu serviço sem escrever uma linha de código específica para ele — e, portanto, sem você entrar numa fila de priorização nossa.
Para expor as funções, você não precisa: instalar SDK nosso, expor o
seu banco, manter webhook, criar conta no OkMigo, assinar exclusividade, ou
mudar a sua API. O servidor MCP fica na frente dela e chama o que já
existe. O okmigo-cartao é opcional nessa etapa e passa a ser o
caminho recomendado quando você também declara a tela do seu produto.
from mcp.server.mcpserver import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer(
"acmecrm",
instructions=(
"Ferramentas do AcmeCRM: clientes e pedidos de um negócio. "
"Confirme com a pessoa antes de chamar qualquer ferramenta que grava."
),
)
SO_LEITURA = ToolAnnotations(read_only_hint=True)
@mcp.tool(annotations=SO_LEITURA)
async def buscar_cliente(nome: str) -> dict:
'''Procura um cliente pelo nome, mesmo parcial ("ana", "ana paula").
Devolve os que casaram, com id, telefone e cidade. Use antes de
registrar um pedido, para descobrir o id do cliente.'''
achados = await acme.clientes.search(nome) # a sua API de sempre
return {"clientes": [c.to_dict() for c in achados]}
@mcp.tool()
async def registrar_pedido(cliente_id: str, itens: list[str], observacao: str = "") -> dict:
'''Cria um pedido para um cliente. SÓ CHAME depois que a pessoa
confirmar — isto grava no CRM de verdade e não é desfeito pela conversa.
cliente_id: o id devolvido por `buscar_cliente`.
itens: lista de códigos de produto.
observacao: texto livre opcional, vai no campo de observação do pedido.'''
try:
pedido = await acme.pedidos.create(cliente_id, itens, observacao)
except acme.ClienteInexistente:
return {"erro": "esse cliente não existe mais — busque de novo pelo nome"}
return {"pedido_id": pedido.id, "numero": pedido.numero, "total": pedido.total}
⛔ E antes de decidir o escopo, veja o que você NÃO precisa construir. A agenda, o lembrete, a tela e o trânsito de arquivos já existem deste lado, e o seu serviço chega a eles declarando uma chave no manifesto. A seção O que já existe aqui — não construa de novo é a lista, e ela costuma encolher o trabalho antes de ele começar.
Qualquer linguagem serve — o que importa é o que sai no protocolo, descrito na seção seguinte.
⚠️ Não há um serviço de exemplo público para você copiar, e é melhor dizer isso do que apontar um repositório que você não consegue abrir. O que está publicado é a peça que mais engana quando falta: o crivo da tela (okmigo-infra/cartao, Apache-2.0), que é o mesmo código que roda aqui dentro, com manifestos de exemplo e uma linha de comando que você aponta para o seu. O resto da especificação é esta página, inteira.
Os dois tipos de serviço — escolha antes de começar
A pergunta é só uma: de quem é a credencial que chega ao seu sistema.
Tudo o mais nesta página vale para os dois. Esta escolha muda uma coisa: como o seu sistema descobre de qual cliente se trata.
Credencial do clientedo_amigo |
Credencial do OkMigodo_operador | |
|---|---|---|
| Quem cadastra | o dono do negócio, na tela de instalação | nós, uma vez, para todos |
| Como você sabe de quem é | pela própria credencial | pelo cabeçalho X-OkMigo-Tenant |
| Isolação | estrutural — o vizinho não tem o segredo | promessa sua — você separa por aquele valor |
| Cobrança e limite | com cada cliente | com o OkMigo, que vira um cliente só |
| Você precisa mudar o quê | nada, se já tem chave por cliente | aceitar e isolar pelo cabeçalho |
Já tem chave por cliente? Use do_amigo. É a mais forte
das duas e não exige nada de você. O do_operador existe para quem
quer uma integração só — e não para quem prefere não isolar.
Nos dois casos, o seu serviço é multi-cliente. Sempre.
Um sistema que não separa clientes não entra por aqui em nenhuma das duas formas — e não é burocracia: sem separação, o dado de um cliente seu cai no balde do outro, e nem ele nem você descobrem.
Se o que você tem é uma consulta que não depende de quem pergunta (clima, CEP, um catálogo público), isso não é um serviço nesse sentido: fale com a gente, porque o caminho é outro e mais simples.
Como descrever as suas funções
A descrição é interface, não comentário: é lendo ela que o assistente decide te chamar.
Quando a ponte se conecta ao seu servidor, ela pede a lista de ferramentas
(tools/list no protocolo). É esta resposta que vira o contrato:
{
"tools": [
{
"name": "buscar_cliente",
"description": "Procura um cliente pelo nome, mesmo parcial (\"ana\", \"ana paula\"). Devolve os que casaram, com id, telefone e cidade. Use antes de registrar um pedido, para descobrir o id do cliente.",
"inputSchema": {
"type": "object",
"properties": {
"nome": { "type": "string", "description": "Nome ou parte do nome." }
},
"required": ["nome"]
},
"annotations": { "readOnlyHint": true }
},
{
"name": "registrar_pedido",
"description": "Cria um pedido para um cliente. SÓ CHAME depois que a pessoa confirmar — isto grava no CRM de verdade.",
"inputSchema": {
"type": "object",
"properties": {
"cliente_id": { "type": "string", "description": "O id devolvido por buscar_cliente." },
"itens": { "type": "array", "items": { "type": "string" },
"description": "Códigos de produto." },
"observacao": { "type": "string", "description": "Texto livre, opcional." }
},
"required": ["cliente_id", "itens"]
}
}
]
}
Escreva a descrição para um modelo de linguagem ler, não para um dev. Ela é o único material com que o assistente decide se aquela função serve. Diga quando usar, o formato de cada parâmetro, e principalmente o que a função não faz — é isso que evita que ele te chame na hora errada.
| faça | não faça |
|---|---|
buscar_cliente, registrar_pedido — verbo e
objeto, uma função por operação |
crm com um parâmetro
operacao: "buscar" | "registrar". Isso obriga a escrever "só
quando operacao=registrar" dentro de cada campo, e o modelo erra |
| parâmetros nomeados no schema, com tipo e descrição | um payload genérico do tipo objeto: o que não está
nomeado, o modelo não preenche |
aceitar nome e texto: nome: "ana",
quando: "2026-08-22T10:00:00" |
exigir só identificadores internos que ninguém tem à mão no meio de uma conversa |
| uma função de busca que devolva o id, para as outras usarem | presumir que quem chama já sabe o id |
A descrição é cortada em 600 caracteres. Não é conselho de estilo: o que passa disso não chega ao assistente, e nada acusa — o corte é silencioso, cai no meio de uma frase, e a sua função passa a ser lida pela metade.
Escreva com folga. Se o texto está encostando no limite, ele já está longo demais para ser lido junto de todas as outras funções.
O que não couber vai para a descrição do PARÂMETRO, dentro do
inputSchema — essa guardamos inteira. É o lugar certo para
formato, exemplo e instrução de preenchimento.
⚠️ E cuidado ao medir: dependendo da biblioteca, a descrição sai com a
indentação do texto no seu código, e um cliente pode normalizá-la e outro
não. Uma docstring que mede 587 caracteres na sua máquina chega aqui com 624
— e perde a última frase. Meça o que sai no tools/list, não o
que você escreveu.
As primeiras linhas são o que mais pesa: ponha ali quando usar a função e, se houver, qual outra é a certa quando não for esta. É a informação que evita a chamada errada — e a primeira a se perder no corte se você a deixar para o fim.
Leitura ou escrita: anote
Sem a anotação, tratamos a função como escrita — e escrita para a conversa para pedir confirmação.
Toda função que muda alguma coisa passa por um portão do nosso lado: a primeira chamada não executa. O assistente descreve a ação à pessoa, termina o turno perguntando, e só depois do "sim" dela, numa mensagem seguinte, a chamada acontece de verdade.
Isso existe porque um modelo de linguagem, sozinho, dá "confirmado" sem ter perguntado — nós medimos isso acontecendo. O portão tira essa caneta dele: a confirmação é registro no nosso banco, não uma promessa do modelo.
Consequência para você: função sem readOnlyHint cai no lado
seguro, o de escrita. Se você não anotar as leituras, "quantos pedidos
entraram hoje?" vira "confirma que quer ver os pedidos de hoje?" — e a
experiência fica ruim por um motivo que você resolve com uma linha.
"annotations": { "readOnlyHint": true }
A sua anotação pode encolher privilégio, nunca
ampliar. Marcar readOnlyHint: true numa função que grava
não remove o portão: quem promove uma operação a leitura é uma pessoa
nossa, na instalação, olhando a lista. Anotar certo acelera a aprovação;
anotar errado não abre porta. Não é desconfiança de você — é que nós não temos
como verificar a anotação, só a forma dela.
Quando quem fala NÃO é o dono
O portão acima resolve "o dono confirma antes de gravar". Há um caso diferente: uma pessoa de fora pedindo alguma coisa ao negócio — marcar um horário, reservar uma mesa, segurar uma peça. Aí não basta confirmar: quem confirma teria de ser o dono, e ele não está naquela conversa.
Duas operações, não uma. Uma que o cliente chama e que só registra o pedido; outra, do dono, que decide. O que o cliente chama nunca é a mesma função que grava o compromisso.
Três coisas que a nossa primeira integração assim ensinou, e que não são óbvias antes de acontecerem:
- O pedido precisa SEGURAR o recurso, com prazo. Sem segurar, duas pessoas pedem o mesmo horário e o dono aprova as duas — e o conflito só aparece com as duas na porta. Sem prazo, um pedido que ele nunca abrir tranca aquele horário para sempre.
- O erro de "não disponível" não conta o motivo. Se o horário está tomado por outro pedido, isso é movimento da agenda de um negócio, e quem está de fora não recebe. Responda "não está disponível", nunca "há outro pedido".
- A operação do dono não pode exigir um id. O assistente dele não deve mostrar identificadores a ninguém — então exigir um põe o modelo a improvisar. Aceite o que a pessoa reconhece (o nome de quem pediu, a hora), e com mais de um candidato devolva a lista em vez de escolher.
⛔ E o que não resolve: dar ao cliente as funções do dono com um aviso no texto. Descrição não é permissão — quem separa os dois lados é o conjunto de funções que cada um alcança.
Autenticação: uma credencial por cliente
Você descobre de qual negócio se trata pela credencial, nunca por um campo do pedido.
Cada negócio que usa o seu serviço através do OkMigo tem a própria credencial no seu sistema — a mesma que ele usaria para chamar a sua API diretamente. O dono a cadastra uma vez, na tela de instalação do OkMigo, e ela fica cifrada conosco. Toda chamada vai com ela:
POST /mcp HTTP/1.1
Host: mcp.acmecrm.com
Authorization: Bearer <a credencial da Barbearia do Zé>
Idempotency-Key: 7f3a9c21-4e0b-4a17-9f2e-1c8d5b6a0e33
Content-Type: application/json
{"jsonrpc":"2.0","id":42,"method":"tools/call",
"params":{"name":"registrar_pedido",
"arguments":{"cliente_id":"c_8812","itens":["PA-100","PA-220"]}}}
O cliente sai da credencial. Nunca de um parâmetro.
✅ Authorization: Bearer <credencial da Barbearia do Zé>
⛔ Authorization: Bearer <token do OkMigo>
{ "cliente": "barbearia-do-ze", ... }
O motivo é direto: um parâmetro é texto que um modelo de linguagem compõe; uma credencial não é. Se o negócio vier num campo do pedido, alcançar os dados do negócio vizinho é uma string diferente — e a única defesa vira "o modelo não deveria". Com o negócio saindo da credencial, é impossível, e continua impossível no dia em que alguém descobrir uma injeção de prompt na sua tela de observações.
Guardar a credencial é nossa responsabilidade: cifrada em repouso, nunca devolvida por nenhuma tela ou endereço nosso, nem para o próprio dono. Ele a remove quando quiser — removeu, o seu serviço para de responder para ele no mesmo instante.
OAuth funciona igual. Se o seu sistema usa OAuth com refresh token, a renovação fica do nosso lado e você recebe sempre um access token válido. Só precisamos do endpoint de refresh e do escopo mínimo.
⚠️ Peça o escopo mínimo. Uma credencial com acesso à conta inteira para uma integração que só registra pedido é risco que ninguém precisa correr — nem você, nem o seu cliente, nem nós.
A outra forma: uma credencial só, e o cliente vem de nós
Se você prefere integrar uma vez e receber todos os negócios por baixo de uma conta.
A seção anterior descreve a forma por cliente: cada negócio cadastra a própria credencial. É a mais forte, e é a que recomendamos — o vizinho não alcança o dado porque não TEM o segredo. Não depende de ninguém acertar.
Há uma segunda forma, para quando você quer uma integração só: uma credencial nossa, e o cliente identificado por um valor que nós mandamos. Ela exige uma coisa de você, e não é negociável.
Você precisa aceitar um identificador de cliente e ISOLAR por ele.
POST /mcp HTTP/1.1
Host: mcp.acmecrm.com
Authorization: Bearer <a credencial do OkMigo>
X-OkMigo-Tenant: t_9c2f1ab47e0d5836
Content-Type: application/json
Todo dado criado numa chamada com esse valor pertence a esse cliente, e nenhuma chamada com outro valor pode enxergá-lo. Na primeira vez que um valor aparece, você cria a conta; nas seguintes, resolve por ele.
Vai num CABEÇALHO, não num parâmetro — e pela mesma razão da seção anterior: parâmetro é texto que um modelo de linguagem compõe. Quem põe esse cabeçalho é a nossa ponte, no servidor, derivando de (o seu serviço × o negócio). O modelo não vê o valor, não escolhe e não alcança. A garantia é idêntica à da credencial.
O que o valor é: uma string opaca, estável para sempre para aquele negócio no seu serviço. Não é nome, não é e-mail, não é um id nosso.
⚠️ Não tente interpretá-lo, e não peça mais nada. Ele é deliberadamente sem significado — inclusive porque o mesmo negócio tem um valor diferente em cada serviço. Isso é de propósito: se fosse o mesmo, dois integradores cruzariam as listas e descobririam que a Barbearia do Zé é a mesma nos dois. Um identificador que atravessa fornecedores é rastreamento, e não vamos construir um.
⛔ Um cliente que nasce vazio precisa conseguir se configurarNesta forma o cliente nasce da primeira chamada — e nasce sem nada dentro. Uma agenda sem serviços cadastrados não consegue marcar horário; um estoque sem produtos não consegue dar baixa.
O que o seu sistema precisa para ser útil tem que ser criável por uma FUNÇÃO, não só pelo seu painel.
O dono do negócio nunca abre o seu sistema — é isso que a integração promete a ele. Se cadastrar os serviços só existe na sua tela, ele instala, recebe uma conexão que funciona, e não consegue fazer nada com ela.
Na prática: se você expõe criar_agendamento, exponha também o
que ele pressupõe — criar_servico, definir_horario,
o que for. Escrita, sem readOnlyHint, e nós pedimos confirmação
antes de chamar, como em qualquer outra.
⚠️ Se a configuração inicial tiver mesmo que acontecer no seu painel,
tudo bem — mas então diga isso na resposta. Um estado vazio que devolve
{"servicos": []} e mais nada faz o assistente concluir que o
negócio não tem serviços. Devolva um erro que ensina: onde ir, e o que fazer
lá. Ver "O que devolver, e como errar".
Para o seu sistema, o OkMigo vira um cliente só. Isso tem três consequências reais:
- Limite e cobrança são nossos, não de cada negócio. Se você cobra por uso, cobra de nós.
- Se você limitar ou bloquear, bloqueia todos juntos — não dá para suspender um negócio sem suspender o resto, a menos que você trate o identificador como conta de verdade.
- A isolação passa a ser uma promessa sua. Na forma por cliente ela é estrutural; aqui, se os dados se misturarem do seu lado, nem nós nem o dono temos como perceber — os dados voltam certos até o dia em que voltam errados.
Não suporta cabeçalho de cliente? Integre pela primeira forma. Ela não é um caminho menor — é o mais seguro dos dois, e é o que a maior parte dos sistemas que já têm chave por cliente deveria escolher. A segunda forma existe para quem quer uma integração só, não para quem não quer isolar.
Idempotência
É a única coisa que pedimos que você guarde.
Toda chamada leva um cabeçalho Idempotency-Key com um
identificador único daquela operação. Se você receber a mesma chave duas
vezes, devolva o mesmo resultado e não execute de novo.
Idempotency-Key: 7f3a9c21-4e0b-4a17-9f2e-1c8d5b6a0e33
Sem isso, uma resposta perdida na rede vira dois pedidos idênticos no CRM ou dois agendamentos no mesmo horário — e quem vai reclamar é o seu cliente, do seu produto. Guardar a chave e a resposta por 24 horas cobre qualquer retentativa nossa.
Funções de leitura podem ignorar o cabeçalho.
O que devolver, e como errar
Erro é dado, não exceção — e a mensagem vai ser lida por um assistente no meio de uma conversa.
Devolva sempre um objeto, nunca uma frase pronta: quem escreve a frase para a pessoa é o assistente, e ele precisa dos campos. Sem HTML, sem markdown, sem texto de interface.
Deu certo{ "pedido_id": "p_44120", "numero": 4412, "total": 189.90 }
Não deu, e o assistente precisa saber o que fazer
{ "erro": "esse cliente não existe mais — busque de novo pelo nome" }
{ "erro": "esse horário acabou de ser ocupado — peça outro horário à pessoa" }
{ "erro": "produto PA-220 está fora de linha — pergunte se pode trocar por outro" }
Repare que cada mensagem diz o próximo passo. É a diferença entre o dono ouvir "não consegui falar com o sistema" e ouvir "esse horário acabou de ser ocupado, quer as 11h?".
O que não ajudaHTTP 500 Internal Server Error
{"detail": "NullPointerException at OrderService.java:412"}
Uma exceção de protocolo, ou um erro escrito para o seu log, chega à pessoa
como "não consegui" e ela fica sem saber o que houve. Erros de negócio
são resultado — devolva 200 com {"erro": "..."}.
Reserve os códigos HTTP para o que é de fato falha de infraestrutura.
Prazo: responda em até 10 segundos. Passou disso, o assistente segue a conversa sem o seu resultado — ele nunca deixa a pessoa esperando por um serviço externo. Ficar fora do ar é esperado, não excepcional: não quebramos e não ficamos tentando; o assistente diz que não conseguiu e a pessoa tenta de novo.
A sua TELA dentro do OkMigo
Você descreve a estrutura; quem escolhe a aparência somos nós.
Um serviço integrado não fica só disponível para o assistente: ele ganha um ícone e uma tela dentro do OkMigo. Quem descreve essa tela é você — ninguém aqui sabe o que é um pátio de revenda ou uma agenda de tatuador.
A tela é declarada como um Adaptive Card, num subconjunto. Você pode
escrever esse JSON diretamente ou usar o SDK Python do
okmigo-cartao. O SDK é autoria tipada: ele ajuda a montar a
tela, mas não abre uma segunda porta nem afrouxa o contrato.
código Python com o SDK
↓ tela.compilar() ou aplicativo.compilar()
manifesto/cartão JSON restrito
↓ crivo de segurança do OkMigo
JSON validado e reconstruído
↓ renderer do OkMigo
tela Web ou Flutter
O Python não gera HTML, CSS nem componentes Flutter. Ele descreve a intenção — título, texto, busca, tabela e ações permitidas — e compila isso para o mesmo JSON que você poderia escrever à mão:
from okmigo_cartao import Busca, Navegacao, Tela, Tema
tela = Tela(
titulo="Meus ativos",
tema=Tema.MERCADO,
navegacao=Navegacao.INFERIOR,
componentes=(
Busca(
id="ticker_busca",
campo="ticker",
rotulo="Ativo da B3",
placeholder="Digite PETR4",
sugestoes_por="buscar_ativos",
),
),
)
cartao_json = tela.compilar()
compilar() termina o trabalho do SDK; dali em diante, a
fronteira é o JSON. O OkMigo recebe esse JSON, descarta ou recusa o que não é
permitido e desenha com os próprios componentes. Por isso, o serviço controla
conteúdo, estrutura, dados e capacidades; o OkMigo controla
aparência, responsividade, acessibilidade e temas. O mesmo contrato
serve ao renderer Web e ao Flutter.
O catálogo já cobre estrutura responsiva, texto, métricas, fichas, formulários, campos de texto e número, escolhas em lista/cards/busca, autocomplete remoto, arquivos, documentos, imagens, estados e etapas, repetição de dados, calendário, gráficos, progresso, cronômetro e blocos financeiros. Padrões recorrentes são composições Python dessas primitivas; não viram tipos novos nem ampliam a fronteira de segurança.
Usar o SDK não custa segurança nem escala. O resultado atravessa o mesmo crivo fechado, com os mesmos limites e operações declaradas. Código Python nunca é enviado ao OkMigo e nunca é executado por ele — só o JSON compilado cruza a fronteira.
Durante o desenvolvimento, nem é preciso gravar o JSON no disco: o preview
pode receber arquivo.py:APLICATIVO, chamar
compilar() e mostrar todas as telas com navegação, desktop,
celular e temas claro/escuro:
python -m okmigo_cartao preview telas.py:APLICATIVO \
--dados dados-do-preview.json \
--host 0.0.0.0 --port 4173
O JSON continua sendo o artefato que você registra e a fronteira que o produto consome. O preview só encurta o ciclo até ele.
- Início rápido completo — instale
o pacote, declare
Aplicativo, abra todas as superfícies no preview e materialize o JSON que será registrado. - Catálogo do SDK por intenção — encontre o componente Python para formulários, busca, menus, cartões clicáveis, agenda, mídia, tabelas e estados.
- SDK Python e
preview — escreva componentes tipados, compile o aplicativo inteiro e
veja antes como o OkMigo reconstruirá o JSON. O catálogo e as regras de
extensão ficam em
docs/CATALOGO-SDK.md. - Designer — monte o cartão arrastando e copie o JSON, se preferir autoria direta.
- Schema Explorer — cada
elemento com todas as propriedades. É onde conferir o que
sizeouwidthaceitam. - Documentação da Microsoft — a referência completa, incluindo o que existe além do que aceitamos.
- O crivo vem no mesmo pacote: é o código que reconstrói a sua tela aqui dentro, rodando na sua máquina. É com ele que você descobre o que vai sumir, antes de registrar — ver Rode o crivo antes de registrar, no fim desta seção.
⚠️ O Designer mostra o cartão com a aparência dele. A sua tela vai sair com a nossa — o que ele serve para conferir é a estrutura, nunca a cor.
Não existe cor, fonte nem pixel no que você manda. O cartão diz "texto grande e negrito" e "caixa em destaque"; o valor de cada palavra é escolhido pelo cliente que desenha.
Isso é para o seu bem tanto quanto para o nosso: a sua tela fica parecida com o resto do aplicativo sem você ter trabalho, e continua parecida quando alguém aqui mudar o tema. Uma tela que trouxesse a sua paleta seria uma ilha — e daria a quem quer imitar o OkMigo uma superfície para fazê-lo.
TextBlock size: small|default|medium|large|extraLarge
weight: lighter|default|bolder · isSubtle · wrap
Container style: default|emphasis|accent|good|attention|warning · items · isVisible · id · minHeight
accent: a caixa PREENCHIDA com o acento, texto invertido — a
«porta recomendada»; good/attention/warning: o TOM da caixa (o
estado do que ela carrega — um véu da cor de estado do tema)
selectAction: Action.ToggleVisibility (a caixa inteira é o toque)
okmigoGrade: true (grade de ~210px) | "larga" (máx. 2 por linha,
cada filho é um cartão inteiro) | "compacta" (~150px, o máximo
que couber — é a única que dá DUAS colunas num telefone)
| "etiquetas" (os filhos viram CHIPS do tamanho do próprio texto)
okmigoSobreposto: true (sai do fluxo e aparece por cima)
ColumnSet columns[] → Column (width: auto|stretch)
FactSet facts[] → { title, value }
Table columns[] → { width: N } (proporção) · firstRowAsHeader · showGridLines
rows[] → TableRow → cells[] → TableCell { items }
Image height: small|medium|large|stretch · altText
Input.Text id · campo · label · value · placeholder · isMultiline · isRequired
Input.Number idem, e o cliente impede vírgula onde é ponto
Input.ChoiceSet choices[] → { title, value, okmigoNota, okmigoIcone } · isRequired
okmigoQuemOpera: true as opções são QUEM OPERA o negócio, e
quem as preenche somos NÓS — ver abaixo
style: "filtered" typeahead que deixa DIGITAR por cima
style: "filtered" + okmigoEstrito: true
BUSCA: filtra, e só aceita o que está na lista
style: "expanded" desenha as opções como FICHAS tocáveis
(curtas = PÍLULAS; com nota/ícone = TILES com selo ✓)
ActionSet actions[] → Action.ToggleVisibility · Action.Submit { data.operacao }
· Action.Execute { data.operacao } (consulta)
style: "positive" (o gesto principal: ocupa a linha)
style: "destructive" (destrói algo: outra família de cor)
mode: "secondary" (secundário: vira link)
okmigoRodape: true (o bloco fica PRESO ao pé da tela enquanto a
caixa dele estiver à vista — o CTA de rodapé de um app)
okmigoArquivo a ENTRADA de um arquivo — ver «Documentos», abaixo
okmigoDocumento um arquivo seu, para ver na tela ou baixar — idem
okmigoCopiar { rotulo, valor } — um botão que copia o valor (código de barras, chave Pix)
okmigoGrafico { forma: barras|linha, titulo, series[], pontos[] } — a grade vira desenho
okmigoAutorizar { rotulo, motivo, url } — a ida ao SEU site para a pessoa autorizar
okmigoCalendario { vista, de, eventos[], acoesDoEvento[] } — um mês, e o que há nele
okmigoCronometro { rotulo, segundos } — contagem REGRESSIVA (1 s … 3600 s)
okmigoProgresso { feito, de, rotulo } — quanto do caminho já andou
⭐ okmigoQuemOpera é a única escolha cujas opções não são
suas — e existe para você NÃO precisar do quadro de pessoal do seu cliente.
Quando a sua tela precisa de «quem atende», «quem confere», «quem assina», você
declara o campo sem choices e nós o preenchemos com as
pessoas que operam aquele negócio no OkMigo.
{ "type": "Input.ChoiceSet", "id": "prof", "campo": "nome",
"label": "Quem atende", "okmigoQuemOpera": true, "style": "expanded" }
Você recebe, no Submit, apenas o nome escolhido — nunca a lista, nunca e-mail, nunca identificador. E a garantia de que aquele nome opera o negócio é nossa, conferida no servidor quando o formulário chega: a tela não prova nada, porque quem monta o pedido é o aparelho de alguém.
⛔ Três consequências, e é melhor saber antes: as
choices que você declarar são ignoradas (duas listas fariam
a tela oferecer gente de fora ao lado de gente de dentro); a escolha é sempre
estrita, com ou sem estilo (livre seria um campo de texto com sugestão);
e a lista pode chegar vazia — um negócio recém-criado tem uma pessoa só,
e um sem ninguém não deveria ter essa tela.
⚠️ E o que muda do seu lado: se você já tem um cadastro próprio de equipe, ele passa a ser consequência de quem foi escolhido, não uma porta. Duas listas de gente divergem, e a que está errada é sempre a que ninguém olha.
⭐ As opções em FICHAS (style: "expanded") existem porque a
única ação de toque deste contrato é ToggleVisibility, e ela não
carrega VALOR. Sem elas, "toco para escolher e depois confirmo" não era
declarável: uma fileira de "2 3 4 5 6" tinha de ser uma lista suspensa, ou cada
opção tinha de ser um Submit que grava na hora. A ficha é o mesmo
Input de sempre — viaja no mesmo Submit, com o mesmo escopo — só
desenhado por inteiro. O okmigoNota é a linha de apoio de cada
opção, e só aparece nessa forma.
⭐ TELAS que se sucedem não são um recurso: são caixas com
id e ToggleVisibility COM SINAL
({elementId, isVisible}) — tocar um cartão mostra a próxima caixa e
esconde a atual, e «‹ voltar» é o mesmo gesto ao contrário. Zero escrita, zero
ida ao servidor; quem grava é o botão de rodapé (okmigoRodape) de
cada tela, e cada escrita reconstrói a superfície no passo seguinte. Este
contrato não tem navegação, e não precisa ter uma para isso.
⭐ A ÊNFASE diz o PESO do gesto, nunca a cor. Você declara "este é o
principal" ou "este destrói algo"; qual violeta ou qual vermelho isso vira é do
cliente, como todo o resto deste vocabulário — é por isso que o
color do TextBlock continua sendo descartado. Botão
sem marca sai como sempre saiu.
⚠️ okmigoProgresso recebe feito e
de, nunca uma porcentagem. "4 de 6" é o que a pessoa quer
saber; o "67%" se deriva daí, e o contrário não. de zero derruba o
bloco (dividiria por zero) e um feito fora da faixa é prensado, não
recusado.
⭐ okmigoAutorizar é a única saída para um endereço seu, e
existe porque o Open Finance só funciona assim: quem pergunta a senha do banco
é o banco. Não é um link livre — o crivo recusa o que não for
https, o que trouxer credencial embutida
([email protected]), porta fora da 443, IP cru, ou um host nosso;
e aceita um por cartão. O motivo é obrigatório e o
host é desenhado ao lado do botão: quem vai tocar lê para onde
vai. Um botão só com "continuar" é o que faz alguém sair no escuro.
⚠️ O endereço viaja no dado, nunca no molde: token de autorização vence em minutos, e molde congela na instalação.
⚠️ Esta lista já disse "só ToggleVisibility" enquanto o
Submit estava aceito há dias. O que vale é o crivo — e ele
é público, então você não precisa acreditar nesta página:
okmigo-infra/cartao. O
texto aqui é conferido contra ele, e quando divergirem, é o texto que está
errado.
Elemento fora dessa lista some — a menos que você diga o que aparece
no lugar, com o fallback do próprio schema. Use-o. É a
diferença entre a sua tela degradar do jeito que você escolheu e degradar do
jeito que o acaso escolher.
⭐ E ele vale para QUALQUER queda, não só para o tipo que a gente não conhece — inclusive a mais comum de todas: o elemento veio vazio. A tabela que ficou só com o cabeçalho porque a sua lista não trouxe nenhuma linha, a escolha sem opção válida, o bloco de botões que perdeu todos.
⛔ É o estado-vazio, e ele é seu. Um negócio sem movimento no dia abre a sua tela e, sem isto, vê o lugar da tabela em branco — sem erro, sem explicação. Com isto, vê a frase que você escreveu:
{ "type": "Table", …,
"fallback": { "type": "TextBlock", "wrap": true, "isSubtle": true,
"text": "Nenhum agendamento para hoje. Quando alguém marcar, aparece aqui." } }
⚠️ Ele só aparece quando o elemento cai — com dado, a tabela desenha e o aviso não surge. E ele não é um atalho: o que você põe ali passa pelo mesmo crivo, então um elemento proibido cai igual.
"fonte": {
"resumo": { "operacao": "resumo_do_estoque" },
"lista": { "operacao": "buscar_veiculos",
"pedido": { "incluir_indisponiveis": true },
"caminho": "veiculos" }
}
⛔ São essas duas chaves, e só elas. Uma fonte com
qualquer outro nome faz a superfície ser recusada, com uma mensagem
dizendo qual chave não existe. Não é rigor: antes ela era ignorada em silêncio,
e o efeito era pior que um erro — a tela abria com os campos vazios,
sobre dado que existia do outro lado. Num formulário, quem apertasse Salvar
apagaria o que estava gravado.
Duas funções de leitura suas. Nós as chamamos com a credencial e o
cliente certos — a mesma travessia de sempre — e preenchemos os
{campo} do cartão com a resposta. {marca},
{preco_reais}, {total}. Campo que você parar de
mandar vira vazio, nunca a chave crua na tela.
Uma lista de coisas com as mesmas colunas — documentos, cobranças, o ano
mês a mês — é uma Table, não uma pilha de ColumnSet:
só ela alinha as colunas entre as linhas. A largura é proporção
(width: 2 é o dobro de 1), a primeira linha é o
cabeçalho, e no celular quem decide se a tabela vira fichas empilhadas somos
nós — você declara a estrutura.
Cada item da lista repete um bloco, marcado assim:
{ "_repetir_lista": { "type": "Container", "items": [ … ] } }
E, quando o bloco não deve repetir para todos os itens, um
_quando ao lado escolhe quais:
{ "_repetir_lista": { "title": "{marca} {modelo}", "value": "{id}" },
"_quando": { "campo": "situacao", "em": ["retirado"] } }
Um operador só: o campo está entre estes valores. Ele existe porque uma superfície tem uma lista, e um formulário costuma precisar de um recorte dela — uma lista suspensa de "devolver ao pátio" que oferece o estoque inteiro faz o seu serviço recusar opção por opção, e quem usa conclui que a tela está quebrada. A comparação é por texto, com espaços aparados, e campo que não existe na sua resposta não casa: você vê a lista vazia na tela, que é o sinal certo para um erro de digitação no manifesto. Forma diferente desta é ignorada — a lista sai inteira em vez de sair errada.
Se o recorte que você precisa não cabe nisso, ele é seu: declare a fonte já peneirada. Nós não vamos ganhar operadores aqui.
Abas, sem chamada nenhuma{ "type": "ActionSet", "actions": [
{ "type": "Action.ToggleVisibility", "title": "O pátio",
"targetElements": [ { "elementId": "aba_patio", "isVisible": true },
{ "elementId": "aba_numeros", "isVisible": false } ] } ]}
Action.ToggleVisibility não alcança nada: mostra e esconde
blocos que já vieram no mesmo cartão. Action.OpenUrl é
descartado — é um endereço escolhido por você dentro do aplicativo de outra
pessoa.
{ "type": "Container", "items": [
{ "type": "Input.Text", "id": "placa_nova", "campo": "placa", "label": "Placa" },
{ "type": "ActionSet", "actions": [
{ "type": "Action.Submit", "title": "Salvar", "data": { "operacao": "criar_veiculo" } } ] } ] }
Action.Submit nomeia uma operação de escrita do seu
contrato — nunca uma URL — e o botão manda os campos da mesma caixa
para ela, pela nossa rota, com a credencial e o cliente certos. Operação que
não está no seu contrato recusa a tela inteira: botão que aparece e não
grava faz a pessoa culpar o produto. O id do campo é único no
cartão (é a chave do estado no aparelho); o campo é o nome do
argumento na sua função, e pode repetir entre caixas — 42 formulários, um por
dia, todos chamando criar_bloqueio(quando, …).
Action.Execute é o par de leitura: envia os campos da
mesma caixa para uma operação de consulta e recebe outra ficha declarativa.
A resposta atravessa o mesmo crivo antes de substituir o cartão atual; uma
operação que não esteja nas leituras do contrato recusa a tela inteira.
{ "tipo": "ponte",
"slug": "acmecrm",
"endpoint": "https://mcp.acme.com.br/mcp",
"descricao": "Os clientes e os pedidos da Acme: buscar, criar e acompanhar.",
"forma": "do_operador",
"credencial_sondagem": "...", "tenant_sondagem": "...",
"para_tipo": "socio", "versao": "0.3.0",
"superficies": [ { "nome": "patio", "titulo": "O pátio",
"hint": "os carros do seu estoque",
"fonte": { ... }, "cartao": { ... } } ] }
⛔ Os três primeiros são obrigatórios, e o que reprova mais é o
descricao — porque ele é fácil de esquecer e não parece
importante. É a frase que o modelo lê para decidir chamar o seu serviço:
um arquivo sem ela é recusado antes de qualquer outra coisa.
| campo | regra | o que costuma reprovar |
|---|---|---|
slug | obrigatório · 3 a 40 ·
^[a-z][a-z0-9_-]+$ |
maiúscula, ponto, acento, ou começar com dígito |
endpoint | obrigatório · 10 caracteres ou mais | — |
descricao | obrigatório · 10 caracteres ou mais | faltar |
forma | do_amigo ou
do_operador | qualquer outra grafia |
credencial_sondagemtenant_sondagem |
o que abre o seu servidor para nós lermos o contrato no momento de registrar | faltar: sem ler o contrato, toda operação que você declarar é podada por «não existe» |
para_tipo | amigo,
socio ou colega |
⚠️ não existe «ambos» — para os dois, omita o campo |
tipo | "ponte" (o seu caso) ou
"proprio" | — |
⛔ A leitura do seu contrato é AUTENTICADA, e é aqui que quase todo mundo
tropeça. No momento de registrar, nós chamamos o tools/list do
seu servidor mandando exatamente isto:
Authorization: Bearer <credencial_sondagem>
X-OkMigo-Tenant: <tenant_sondagem>
⚠️ E um serviço do_operador bem implementado recusa a chamada
sem o cabeçalho ANTES de qualquer ferramenta rodar — inclusive o
tools/list. Quer dizer: quanto mais correto o seu servidor,
mais garantido é o 401 se você não mandar as duas chaves. Não é um caso de
borda: é o comportamento certo batendo de frente com um campo esquecido.
⭐ O par de sondagem serve só para ler o contrato uma vez. Ele não é
guardado como credencial de ninguém — a credencial de verdade nasce depois, do
jeito da forma que você escolheu. Um token de teste, com permissão
de listar, resolve.
⚠️ E se o seu servidor responde a mesma coisa para "faltou token" e "token errado", conserte isso antes de nos procurar. Do nosso lado os dois viram o mesmo erro, e quem está integrando fica sem saber se o problema é a credencial, o cabeçalho ou o tenant.
⚠️ A sondagem não é detalhe. Toda capacidade que nomeia uma operação
— eventos, avisa_antes, relata_mudancas,
convite, quer_a_marca, aceita_contato —
é conferida contra o contrato que lemos do seu servidor na hora de
registrar, e o que não estiver lá é descartado em silêncio. Não é
erro: é a capacidade simplesmente não existir depois, sem nada para procurar.
Se a operação ainda não nasceu, ela passa a valer sozinha no dia em que nascer
e o registro for refeito.
Uma chave opcional muda o que o assistente enxerga:
"conversa": ["o_que_falta", "recebidos", "marcar_entregue"]
E uma marca no schema de um parâmetro, como a da foto
(x-okmigo-foto): "codigo_barras": { "type": "string",
"x-okmigo-pagamento": true } diz que o campo é dinheiro para pagar
— linha digitável, Pix copia-e-cola. Sem a marca, o nosso filtro de privacidade
barraria 47 dígitos como se fossem um CPF, e a guia não chegaria ao cliente.
conversa é a lista das operações que o assistente vê
falando com a pessoa. Sem ela, entram todas — e um contrato de trinta
operações põe trinta schemas em cada turno, o que estoura o contexto do
modelo. A tela continua usando o contrato inteiro; a conversa usa o que
faz sentido dizer em voz alta.
⭐ E há outras chaves, que ligam o seu serviço ao resto do OkMigo — a agenda, o lembrete, o contato de quem instalou você. Elas são a seção seguinte, e valem uma leitura antes de você decidir o que construir.
Versione esse arquivo junto do seu código. Quando uma função sua ganhar um campo, quem precisa lembrar de mostrá-lo na tela é quem o acrescentou — e ele está no seu repositório, não no nosso.
⚠️ Nenhum segredo nele. A credencial entra no gesto de registrar.
Rode o crivo antes de registrarO crivo é o código que lê o seu cartão aqui dentro e o reconstrói no vocabulário do OkMigo, descartando o que não conhece. Ele está publicado sob Apache-2.0, sem dependência nenhuma, e é o mesmo que roda em produção — a API do OkMigo instala esse pacote:
git clone https://github.com/okmigo-infra/cartao.git
cd cartao
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
python -m okmigo_cartao preview exemplos/sdk_catalogo.py:APLICATIVO
Depois, abra o seu aplicativo inteiro:
python -m okmigo_cartao preview \
okmigo/gerar_manifesto.py:APLICATIVO \
--dados okmigo/examples/preview.dados.json
Para investigar uma superfície do JSON já materializado em baixo nível:
python -m okmigo_cartao okmigo/manifesto.json --superficie extrato \
--dados dados.json --escrituras marcar_pago,estornar --html saida.html
Ele responde três perguntas, e a segunda é a que você não tinha como fazer antes:
- Passou?
- ou qual regra recusou a tela inteira, com o nome.
- O que sumiu?
- O modo de falha desta plataforma quase nunca é erro — é a tela abrir sem o pedaço, com tudo respondendo 200. O relatório nomeia cada tipo que entrou e não saiu, e cada chave lida e descartada, com o motivo de cada uma.
- Como fica, pelado?
--htmlescreve o cartão reconstruído em HTML sem uma linha de CSS. O feio é intencional: se o documento pelado não faz sentido, nenhuma aparência conserta.
⛔ Validar o molde cru não prova nada. Um bloco com
_repetir_lista só vira tabela, grade ou fichas depois de expandido
com dados — e é aí que o crivo descarta a tabela que ficou só com cabeçalho e a
lista que ficou sem opção. O --dados expande do mesmo jeito que a
nossa ponte expande ({campo}, _repetir_lista,
_quando, _de). Rode com dados que pareçam os
seus — nomes de campo iguais, uma linha com valor zero, uma com texto longo
— ou o "passou" é falso.
--escrituras e --leituras são as operações que o
seu servidor MCP expõe. Sem elas os botões não são conferidos, e a
ferramenta avisa em vez de dizer que passou — um Action.Submit
nomeando operação fora do seu contrato recusa a tela inteira, e é o erro
mais caro de descobrir depois de registrar.
O contrato de cada tipo — o que ele aceita, o que evapora e por quê — está
no CONTRATO.md do repositório, mais longo que esta seção. E
se a sua tela não couber no vocabulário, o caminho é um PR ali: o README
diz as quatro coisas que um elemento novo precisa trazer junto, e a primeira é
a tela real que não cabe hoje. Elemento nasce de observação, não de ideia.
O que já existe aqui — não construa de novo
A agenda, o lembrete e a tela já estão no OkMigo. Você declara; quem executa somos nós.
O erro mais caro de quem integra não é escrever a função errada — é construir de novo o que já existe deste lado. Uma agenda própria, um mecanismo de notificação, uma tela com a sua paleta. Cada uma dessas coisas já está aqui, e o seu serviço alcança todas declarando uma capacidade no manifesto. Quem executa é o OkMigo.
E é sempre por aqui, nunca de serviço para serviço. Só o OkMigo sabe quais serviços aquela pessoa instalou, e é ele quem fala com cada um em nome dela. O seu serviço não conhece a agenda dela; a agenda não conhece o seu serviço. Nenhum dos dois recebe o endereço do outro, e não há como pedir.
O OkMigo tem uma agenda, e ela vem instalada em toda conta: não é um app que alguém escolheu, e não se remove — quem não quer ver, oculta. Se o seu serviço tem datas — um prazo, um vencimento, uma entrega, uma revisão —, elas aparecem lá sem você desenhar um calendário.
"eventos": { "operacao": "eventos", "parametro": "dias" }
A operação é de leitura, recebe a janela em dias e devolve:
{ "eventos": [
{ "id": "1c3f", "inicio": "2026-10-14T09:00:00-03:00", "status": "ativo",
"titulo": "Renovar o acesso ao banco",
"detalhe": "Sem renovar, o extrato para de atualizar." } ] }
Nós varremos, e para cada conta que tem o seu serviço e a agenda anotamos o compromisso lá dentro, com a credencial daquela conta. O que você precisa saber antes de escrever isso:
- ⛔ A operação tem de ser pura — perguntar duas vezes devolve o mesmo.
Quem lembra o que já foi anotado somos nós, por uma assinatura de
inicio+titulo+detalhe: mudou, o compromisso é refeito; não mudou, nada acontece. Você não ganha estado, fila nem noção de "já mandei". - O compromisso dura 30 minutos e ocupa a grade. Um prazo não tem hora, e a agenda só sabe guardar coisa que ocupa horário — o custo é o dia de quem atende parecer mais cheio do que está, e foi escolhido assim.
- Remarcar é remover e anotar de novo: a agenda não tem "editar" no
contrato. Para você isso é invisível — mude o
inicioe devolva. statusdiferente de"ativo"sai da agenda, e o que sair da janela também. Entregue ou dispensado não pode continuar lá parecendo pendência.- Item sem
idou seminicioé descartado em silêncio: sem identidade estável não há como não duplicar. - A varredura é lenta de propósito. Um prazo é sobre daqui a dias, e perguntar de cinco em cinco minutos a cada serviço, por conta, gastaria chamada por nada.
⛔ E o que isto não é. A agenda é destino, nunca origem: você não lê a agenda de ninguém, não pergunta se um horário está livre e não reserva. Se o seu serviço marca hora com cliente, quem marca é a agenda — o seu serviço declara o que já está marcado do lado dele, e isso vira compromisso.
"avisa_antes": { "operacao": "a_avisar", "parametro": "janela_minutos" },
"relata_mudancas": { "operacao": "mudou", "parametro": "janela_minutos" }
avisa_antes declara uma operação pura de leitura que
responde "o que venceu para avisar desde a sua última passada": recebe a
janela em minutos e devolve { "avisos": [ { "id", "tipo", "inicio",
"titulo", "detalhe" } ] }. Nós a chamamos a cada cinco minutos para cada
negócio que instalou o seu serviço, lembramos o que já tocou (por
tipo:id + inicio) e tocamos no bolso da pessoa.
relata_mudancas é a irmã dela e responde "o que mudou",
em { "mudancas": [ { "id", "inicio", "status" } ] }. Serve o caso
que o seu sistema não tem como resolver sozinho: alguém marcou um
horário com aquele negócio através do OkMigo, e quem precisa saber que foi
confirmado, remarcado ou cancelado é essa pessoa — que do seu lado é só um nome
em texto. Você relata o fato; quem sabe a quem contar somos nós.
- ⛔ Você não ganha relógio, fila nem noção de notificação, e não deve: as duas são puras, e perguntar duas vezes devolve o mesmo.
- ⚠️
relata_mudancassó é perguntada a quem tem alguém esperando notícia. Um negócio sem nenhum horário nosso em aberto não é consultado — perguntar seria tráfego de robô no sistema de outra empresa, de cinco em cinco minutos, para sempre. - ⚠️ Compare instante, não texto.
2026-09-08T14:00-03:00e2026-09-08T17:00+00:00são o mesmo momento. Do nosso lado isso já foi um defeito real: com a comparação em texto, toda passada concluía "mudou de horário" e a pessoa recebia o aviso de cinco em cinco minutos — com a hora certa, o que a fazia acreditar.
"propoe_na_vitrine": { "operacao": "o_que_mudou",
"parametro": "desde_minutos" }
Todo negócio tem uma página pública no OkMigo, e o que ela mostra é o que aquele negócio oferece. Se o seu serviço é onde isso muda — o preço mudou, entrou um serviço novo, abriu disponibilidade —, você pode propor essa mudança sem que o dono redigite nada.
A operação é de leitura, recebe a janela em minutos e devolve:
{ "novidades": [
{ "id": "svc-31", "titulo": "Corte + barba",
"detalhe": "1h, R$ 60" } ] }
Você propõe; você não publica — e não escreve. O que a sua operação devolve não vai para a página nem para a memória do negócio. Ele é lido pelo assistente daquele negócio, que redige a versão dele, e o resultado nasce como proposta: aparece na tela da vitrine, ao lado das que o próprio assistente propõe, e só vai ao ar quando o dono disser sim.
Não é burocracia — é o que torna isto seguro de existir: publicar em nome de um negócio é ato do negócio. E o seu texto nunca pousa literal, o que significa que nada dentro dele pode dar instrução ao assistente dele.
⛔ Proponha o que o NEGÓCIO oferece, nunca o que um CLIENTE fez. Um agendamento carrega o nome de quem marcou — uma pessoa que não tem conta aqui e não concordou com nada. A página é pública e entra em buscador.
«corte + barba · 1h · R$ 60» ✅ · «quinta 10h livre» ✅ · «Ana, quinta 10h» ⛔
⚠️ E some a isso a durabilidade: o retrato de um negócio é o que dura.
A fila de hoje não é um fato sobre ele — e o assistente já sabe perguntá-la a
você, pelas operações do conversa. Mandar a fila por aqui seria
propor uma cópia que envelhece em horas.
- ⛔ O
idé obrigatório e tem de ser estável — é por ele que a gente sabe que já propôs aquilo. Sem ele, a novidade é descartada. - ⭐ Mas mudar o TEXTO propõe de novo. Corrigiu o preço do mesmo
serviço? Mesmo
id, texto novo: entra outra proposta. É a assinatura do conteúdo que decide, não só a chave. - A varredura é lenta, e mais que as outras: cada novidade custa uma leitura do modelo do negócio. O que se oferece muda devagar.
- Proposta que o assistente recusar não volta na passada seguinte — perguntar de novo para sempre gastaria por nada.
"publico": { "ofertas": "listar_servicos",
"expediente": "listar_expediente" }
O irmão do de cima, com a diferença que é a coisa toda: aqui você não propõe, você publica. Cada chave é uma categoria de informação pública e o valor é a leitura por onde ela vem. O que voltar aparece na página do negócio, e o que sumir da sua lista sai da página.
⭐ Por que sem aprovação, se a vitrine tem aprovação? Porque não é a mesma coisa. Lá o assistente conclui algo de uma conversa, e conclusão pede um humano. Aqui o negócio declara: um serviço que entrou na sua lista é pra ser vendido, e um que saiu não é mais. Fazer o dono confirmar cada linha transformaria uma tabela de preços em fila de aprovação — e uma fila que ninguém olha deixa o preço errado no ar.
⛔ Quem publicou foi o dono, no instante em que instalou. A tela de instalação diz, antes do botão, exatamente que informação vai para a página. Para tirar do ar, ele remove o serviço.
As categorias são nossas e fechadas — chave que não estiver na lista é descartada em silêncio, e categoria nova exige que a nossa página saiba desenhá-la:
ofertas— o que o negócio oferece. Itens comnomee, se houver,duracaoepreco.expediente— que horas ele atende. Itens comdia,inicioefim; dia sem hora é fechado, e nós escrevemos «fechado» — omitir o dia diria outra coisa.
{ "servicos": [ { "nome": "Corte + barba", "duracao": "1h", "preco": "R$ 60" } ] }
{ "expediente": [ { "dia": "Segunda", "inicio": "09:00", "fim": "18:00" },
{ "dia": "Domingo" } ] }
⛔ A linha é COMPOSTA por nós, dos campos — nunca copiada como prosa.
É o que garante que nada que você mande possa dar instrução ao assistente do
negócio. Uma linha só, com teto de tamanho; sem os campos que a gente conhece,
caímos num texto/descricao, com o mesmo teto.
⚠️ Teto de 30 itens por categoria. A página é pública e entra em buscador; um catálogo de trezentas linhas vira uma página que ninguém lê. A ordem é sua — o que não couber fica de fora.
- ⛔ Aqui vale a mesma regra do que é do NEGÓCIO e não do CLIENTE — com mais força, porque isto vai ao ar sem ninguém no meio. Nome de cliente, telefone, fila do dia: nada disso é informação pública do negócio.
- ⭐ Lista vazia não apaga nada. Se a sua leitura devolver vazio, a gente não mexe: um erro interno do seu lado não pode esvaziar a página de um negócio. Esvaziar de propósito é o dono remover o serviço.
- ⛔ O que o dono apagou não volta. Se ele apagou uma linha na tela da vitrine dele, ela fica apagada, mesmo que você a mande de novo. É a única resposta que ele já deu por escrito.
- ⚠️ Parou de declarar, parou de publicar. Tirar a chave do manifesto e re-registrar tira aquela informação do ar na varredura seguinte.
- A varredura é lenta: cardápio e horário não mudam de cinco em cinco minutos.
"marca_horario": { "operacao": "pedir_horario",
"ofertas": "listar_servicos" }
Se o seu serviço marca hora, declare isto. A primeira operação é a que recebe o pedido; a segunda é a leitura que lista o que você atende — e é ela que faz o roteamento funcionar.
⛔ Um negócio pode ter mais de um sistema que marca hora, e isso é normal. Uma clínica com a agenda de consultas e o serviço de fisioterapia; um salão com a agenda e o estúdio de tatuagem. Por isso o alvo do pedido não é escolhido por conta — é escolhido pela oferta: quem pede «Corte» está pedindo a quem oferece «Corte».
⭐ É por isso que a lista de ofertas não cabe no manifesto. Ela é do negócio e muda toda semana; o que você declara é por onde ela se lê.
⚠️ E quando dois sistemas do mesmo negócio atendem a mesma oferta, nós perguntamos à pessoa em qual deve entrar. Não escolhemos — marcar no sistema errado grava com sucesso, não dá erro nenhum, e o cliente aparece num dia em que ninguém o espera.
No eventos, um campo a mais muda o que a pessoa vê:
{ "eventos": [
{ "id": "1c3f", "inicio": "2026-09-13T14:00:00-03:00", "status": "ativo",
"titulo": "Ana · Corte", "detalhe": "…",
"quem": "Beatriz Nunes" } ] }
O quem é o nome de quem vai atender — o mesmo que você
recebeu de nós pelo okmigoQuemOpera. Com ele, quem atende passa a
ver na própria agenda que aquele tempo está tomado:
na agenda do NEGÓCIO Ana · Corte · Beatriz Nunes — confirmado
na agenda da PESSOA ocupado · Padaria do Bairro
⛔ O conteúdo NÃO atravessa, e a razão é dupla. Privacidade de quem atende, sim — mas sobretudo: o seu cliente não é usuário do OkMigo, e o nome dele numa agenda pessoal seria levar um terceiro para uma conta que não tem nada a ver com aquele atendimento. Vai só quando e de qual negócio.
⚠️ E o quem tem de ser o nome exato que nós lhe demos.
É o único elo possível: nenhum identificador de usuário nosso atravessa a
fronteira, então o casamento é pelo nome. Nome que não confere com quem opera
aquele negócio não ocupa a agenda de ninguém — em silêncio, porque você pode ter
gente que não tem conta aqui.
⭐ E cancelar tira dos dois lados. Mude o status e a
ocupação some junto — senão a agenda de alguém diria «ocupado» num horário
livre, para sempre.
"aceita_contato": { "operacao": "definir_a_vitrine",
"parametro": "onde_falam_com_a_loja",
"rotulo": "rotulo_do_botao" },
"quer_a_marca": { "operacao": "definir_a_marca",
"nome": "nome", "logo": "logo" },
"convite": { "operacao": "ligar_os_lados",
"parametro": "tenant_de_quem_convidou" },
"so_por_convite": true
aceita_contato- Na primeira instalação mandamos o endereço público do negócio
(
https://okmigo.com/@handle) e um rótulo pronto — para a sua tela não pedir à dona que digite de novo o que o OkMigo já sabe. ⛔ Só sócio com@, nunca uma conta de pessoa: negócio quer ser encontrado, pessoa não. ⭐ E é o@, nunca o telefone — é o endereço que o produto controla, já público por escolha dela, e que continua valendo se ela trocar de número. ⚠️ Só na primeira vez: reinstalar não sobrescreve o que ela tiver ajustado à mão do seu lado. quer_a_marca- O nome e o logo de quem instalou, para a sua tela sair com a cara dele em vez da nossa. ⚠️ O tema continua sendo o do OkMigo; o que entra é quem está do outro lado.
convite·so_por_convite- Para quando o seu serviço é uma relação entre dois lados — um
escritório e o cliente dele, um profissional e a pessoa que ele atende. Com
so_por_convite, ninguém instala passando pela lista; a porta é o convite. Quando o convidado aceita, chamamos a sua operação no contexto de quem aceitou, passando o tenant de quem convidou e o apelido dele. ⭐ Três lados? Cada slug declara o SEUconvitecom oparque o outro lado instala. Se o convite saiu de um lado que não é opardeclarado por quem aceitou (um nutricionista convidando para o mesmo app do aluno que o personal usa), vale a declaração de quem convidou: a operação dele, e o tenant dele sob o slug dele — desde que opardela aponte para o slug que o convidado instalou. ⛔ Nunca e-mail, handle ou id de conta — os dois tenants são nomes que o seu serviço cunhou, e não significam nada fora dele. ⚠️ Falhar aqui não desfaz o aceite: o serviço já está instalado, e a relação pode nascer pela sua tela.
- A tela — você declara a estrutura, nós desenhamos. Sem cor, sem fonte, sem pixel, e igual nos dois clientes.
- Os documentos — somos o correio, você é o cofre. Não construa armazenamento para trafegar arquivo com o cliente.
- As imagens — quem hospeda somos nós; você guarda a URL.
⚠️ Nada disto acontece por dedução. Capacidade se declara —
sem a chave no manifesto, a varredura nem pergunta e nada é enviado. É o mesmo
princípio do readOnlyHint, e pela mesma razão: o OkMigo não
escreve num sistema de terceiro por conta própria.
Documentos: nós somos o correio, você é o cofre
O arquivo de um cliente seu passa por aqui e não fica. Nem um byte.
Uma guia, uma nota fiscal, um contrato, um extrato. Diferente da foto da seção seguinte — que é pública e nossa —, o documento é do seu cliente, tem valor legal e mora no seu armazenamento. O OkMigo o leva de um lado ao outro e o esquece: nada em disco, nada em tabela, e a nossa trilha registra nome, tipo e tamanho — nunca o conteúdo. Nenhum modelo de linguagem o vê, em nenhum dos dois sentidos.
Os bytes chegam a você intactos e voltam intactos. Não reencodamos, não extraímos texto, não guardamos cópia. Se a sua obrigação é guardar o documento como foi recebido, ela é sua e continua sendo — nós não a compartilhamos nem a atrapalhamos.
{ "type": "Container", "items": [
{ "type": "Input.Text", "id": "vinc_{id}", "campo": "vinculo_id", "value": "{id}", "isVisible": false },
{ "type": "okmigoArquivo", "id": "arq_{id}", "campo": "arquivo", "label": "O documento",
"aceita": ["pdf", "imagem", "xml"], "maxBytes": 10485760, "isRequired": true },
{ "type": "ActionSet", "actions": [
{ "type": "Action.Submit", "title": "Enviar", "data": { "operacao": "receber_arquivo" } } ] } ] }
okmigoArquivo é chave nossa — o Adaptive Cards não tem
Input.File. Ele abre o seletor de arquivo do aparelho, e no
Submit a sua operação de escrita recebe, no campo
declarado, este objeto:
"arquivo": { "nome": "extrato-agosto.pdf", "tipo": "application/pdf",
"tamanho": 184233, "conteudo_base64": "JVBERi0xLjQK…" }
aceita é uma lista de palavras — pdf,
imagem, xml, planilha,
texto —, não de MIME nem de extensão: quem traduz para cada
plataforma somos nós. maxBytes só aperta o nosso teto
(10 MB), nunca o afrouxa. Os outros campos da mesma caixa viajam junto, como
em todo formulário. E vale conferir do seu lado o que sempre valeria: o tipo
pelo conteúdo, não pelo nome; o tamanho; o hash, se você o guarda.
{ "_repetir_lista": {
"type": "okmigoDocumento", "titulo": "{titulo}", "nome": "{nome}",
"tipo": "{mime}", "tamanho": "{tamanho}",
"ler": { "operacao": "ler_arquivo", "pedido": { "par_id": "{id}" } } } }
okmigoDocumento desenha uma linha com o título, o nome e o
tamanho, e dois gestos: ver (PDF e imagem abrem na própria tela) e
baixar. O nó não carrega o arquivo — carrega o nome de uma
operação de leitura do seu contrato e o pedido que identifica o
documento. Quando a pessoa toca, nós chamamos essa operação com a credencial
e o cliente certos, e ela devolve o mesmo objeto do envio:
{ "nome": "DAS-2026-09.pdf", "tipo": "application/pdf",
"tamanho": 91230, "conteudo_base64": "JVBERi0xLjQK…" }
Nós entregamos os bytes ao aparelho em fluxo, com o tipo e o nome que você
declarou. Operação que não é de leitura do seu contrato recusa a tela,
como um Submit fora do contrato. E o que se desenha em linha é
lista fechada — PDF e imagem raster; o resto (inclusive SVG e HTML) sai como
download, porque servido em linha rodaria na nossa origem, com a sessão de
quem abriu.
⭐ O que fica do nosso lado é o TEXTO, e fica na memória de quem carregou ou abriu. Um documento que passa pelo correio entra na memória do OkMigo da pessoa exatamente como um arquivo que ela mesma subisse no chat: o texto em trechos, marcado como citação, o container descartado — e aparece na tela «Documentos» dela. É o que deixa o assistente responder "quanto foi a guia de setembro?". O arquivo em si não fica; o original é o seu. Nunca vai para a memória de quem está do outro lado do seu vínculo — nós não sabemos quem é, e não devemos.
⚠️ Repita a pergunta que você já faz em toda leitura: esse documento é
deste cliente? O pedido volta do aparelho tal qual o crivo o
entregou, e o cliente é o do cabeçalho — a sua função tem de recusar um id que
não seja dele, exatamente como recusaria na conversa.
Imagens: quem hospeda somos nós
Você guarda a URL. O arquivo nunca fica do seu lado.
Foto de produto, de veículo, de imóvel — o arquivo é nosso. Você recebe uma URL numa operação de escrita, guarda essa URL, e devolve ela nas suas leituras. Você não recebe upload, não precisa de bucket e não lida com ciclo de vida de arquivo.
Esta seção vale para a forma uma credencial só, o cliente vem de nós — a da seção anterior. Ali o seu sistema costuma existir para ser usado através do OkMigo, e não tem (nem precisa ter) armazenamento de arquivo próprio.
Se você integra por cliente e já é um produto que os seus clientes abrem por fora — um CRM, um ERP —, você provavelmente já hospeda as suas imagens, e trocar isso não faria sentido. Esse caso ainda não está resolvido do nosso lado, e o obstáculo não é técnico: mostrar a sua URL no aplicativo faria o aparelho de cada pessoa que abre a tela bater no seu servidor. Fale com a gente antes de construir — não improvise.
Não é comodidade: a foto chega ao OkMigo pela conversa — a pessoa manda para o assistente dela. Ela já passa pela nossa guarda, é reencodada e perde o EXIF (foto tirada dentro de casa carrega a coordenada de casa). E é servida do nosso domínio por um motivo que vale dizer em voz alta: se a imagem morasse no seu servidor, o aparelho de cada pessoa que abrisse a tela faria uma requisição a você, entregando o IP dela. Ela não escolheu falar com o seu sistema — ela escolheu falar com o amigo dela.
Diga QUAL parâmetro recebe a fotoMarque a propriedade no inputSchema da sua função. É a única
coisa que você precisa fazer para receber fotos:
"url": { "type": "string", "x-okmigo-foto": true }
Declarada, nunca adivinhada. Não tratamos "todo parâmetro chamado
url" como foto — um serviço com URL de site receberia a foto de
alguém num campo que espera outra coisa. Sem a marca, nada muda para você.
Uma por função. Duas propriedades marcadas na mesma função é ambiguidade que só você resolve: nesse caso ignoramos as duas e a função segue funcionando sem foto — nunca com a foto no campo errado.
O que muda do nosso lado: o campo marcado desaparece do que o modelo
enxerga, e no lugar dele fica um com_a_foto: true. Quem escolhe o
arquivo somos nós — é sempre a última foto que a pessoa mandou naquela
conversa. O modelo não tem como pedir outra, nem como inventar um endereço; se
não houver foto na conversa, a chamada falha antes de chegar a você, em
vez de chegar sem a imagem.
{ "veiculo_id": "v_8821",
"url": "https://img.okmigo.com/CMdMPFV4lBVjC4nX6hkQRA.jpg" }
A URL identifica um arquivo, não uma pessoa: cada envio gera uma chave nova e aleatória, e duas fotos da mesma pessoa não se ligam por ela. Continua valendo o que está em O que nunca mandamos para você — a URL é um argumento da sua função, como o modelo do carro.
Toda URL que mandamos começa com https://img.okmigo.com/.
Recuse qualquer outra. Se chegar um endereço de outro domínio, não é foto
nossa — guardá-la faria o seu sistema apontar para um arquivo que ninguém
controla, e o erro só apareceria na tela de um cliente seu, meses depois.
{ "veiculos": [
{ "id": "v_8821", "modelo": "...", "capa": "https://img.okmigo.com/..." }
]}
Devolver a foto só no detalhe de um item é o erro mais comum aqui: quem abre uma lista quer ver, e uma lista sem capa vira texto. Se o seu sistema tem várias fotos por item, mande a capa na listagem e o resto no detalhe.
Quando a foto deixar de ser usada, não faça nadaO carro foi vendido, o produto saiu de linha, alguém trocou a foto: você não precisa nos avisar. O arquivo é nosso, e tirá-lo do ar é responsabilidade nossa — do mesmo jeito que guardá-lo foi.
Esta seção já pediu o contrário, e a mudança é de 27/08/2026. Ela pedia uma chamada de volta avisando que a foto saiu de uso. Duas coisas estavam erradas nisso: não existia caminho para você fazer essa chamada — a seção acima promete que você não mantém webhook nem cria conta —, e uma garantia de privacidade não pode depender da diligência de quem integrou. Se você escreveu código para isso, pode apagar; nada aqui espera por ele.
O que fazemos no lugar: cada arquivo que guardamos sabe de quem é e para que foi entregue, e o que deixa de ser referenciado é apagado por nós. Uma foto que a pessoa acha que tirou do ar não pode seguir alcançável por quem tiver o link — esse é o ponto, e ele é nosso.
Há um teto por negócio. Estourado, a operação devolve erro em vez de guardar — dizendo quanto falta liberar. Hospedar por conta de terceiros sem limite é descobrir o custo pela fatura.
Um pedido, do começo ao fim
As quatro mensagens que atravessam a rede, na ordem.
1 · o dono escreve, no OkMigo— registra um pedido de 2 caixas do PA-100 pra Ana
O assistente vê no catálogo dele que existe acmecrm_buscar_cliente
e chama, porque precisa do id. É leitura: não pede confirmação.
Authorization: Bearer <credencial da Padaria>
{"method":"tools/call","params":{
"name":"buscar_cliente","arguments":{"nome":"Ana"}}}
← 200
{"clientes":[{"id":"c_8812","nome":"Ana Paula Ribeiro","cidade":"Mauá"}]}
3 · a escrita é BARRADA na primeira vez
O assistente chama registrar_pedido. A ponte vê que a função
não é readOnlyHint, não repassa nada para você e devolve ao
modelo uma instrução para perguntar. O dono lê:
— Vou registrar para Ana Paula Ribeiro um pedido de 2× PA-100. Confirma?
— isso
⚠️ Nesta etapa o seu servidor não foi chamado. Do seu lado, nada aconteceu.
4 · agora sim, a escrita chegaAuthorization: Bearer <credencial da Padaria>
Idempotency-Key: 7f3a9c21-4e0b-4a17-9f2e-1c8d5b6a0e33
{"method":"tools/call","params":{
"name":"registrar_pedido",
"arguments":{"cliente_id":"c_8812","itens":["PA-100","PA-100"]}}}
← 200
{"pedido_id":"p_44120","numero":4412,"total":189.90}
— Pronto: pedido 4412 registrado para a Ana, R$ 189,90.
A chamada inteira — o que foi mandado, o que voltou, quanto demorou — fica registrada e visível para o dono do negócio. Ele consegue ver depois o que o assistente fez em nome dele no seu sistema.
O que nunca mandamos para você
Não é uma configuração que alguém pode mudar: não existe caminho no nosso código.
- Quem a pessoa é
- Nenhum id de usuário, e-mail, telefone ou identificador estável nosso atravessa. Você não consegue ligar duas chamadas à mesma pessoa por nada que a gente mande.
- O que o OkMigo sabe sobre ela
- O assistente conhece bem quem conversa com ele — gostos, rotina, opiniões. Nada disso vai no pedido. Só o que a pessoa declarou para aquela ação: o nome do cliente, o produto, a data.
- O conteúdo da conversa
- Você recebe os argumentos da função, não a fala. Não mandamos histórico, não mandamos o texto da mensagem.
Vai junto do pedido, além dos argumentos: a credencial daquele negócio e a chave de idempotência. Mais nada.
Os argumentos ainda passam por um filtro nosso antes de sair — se o modelo tentar enfiar um dado sensível num campo de texto livre, a chamada é bloqueada e nem chega em você.
O filtro olha TODOS os argumentos, não só campos de texto livre — e quando barra, a chamada inteira é recusada. Não limpamos o campo e mandamos o resto.
E ele reconhece formato, não intenção. O que ele procura é credencial, dado financeiro, documento e saúde. Telefone e e-mail não são alvos dele — mas onze dígitos sem pontuação têm a cara de um CPF, e com código de país viram uma sequência com cara de cartão. Medido:
11987654321 → recusado (parece documento)
5511987654321 → recusado (parece cartão)
(11) 98765-4321 → passa
[email protected] → passa
Na prática, um registrar_cliente(nome, telefone) passa ou falha
conforme a pontuação que o assistente escolheu naquele turno. Aconteceu
na primeira integração de verdade: o mesmo pedido, sem o telefone, gravou na
hora.
Então não desenhe contando com o campo: declare o contato como opcional e saiba operar sem ele. Quem tem o telefone do cliente é o dono do negócio, e ele o digita no seu sistema como sempre digitou — o assistente cria o registro, a pessoa completa depois. Uma função que só funciona quando o argumento vem bem formatado é uma função que falha na frente do usuário.
Isso é bom para você também: quanto menos dado pessoal atravessa, menor a sua responsabilidade. A integração não te transforma em operador de dados de terceiros; ela te dá comandos sobre os dados que já eram seus.
Mudanças no contrato
Lemos a sua lista de funções uma vez, na instalação, e congelamos.
Função nova, schema alterado ou anotação trocada não entram sozinhos: viram uma revisão que uma pessoa nossa aprova antes de valer.
Isso protege os dois lados. Você publica quando quiser, sem medo de quebrar integrações no ar; e ninguém aqui acorda com uma função de escrita que apareceu de madrugada num servidor que a gente já chama todo dia.
Não dependemos só do seu aviso. No registro guardamos um hash do seu
manifesto inteiro, e o painel compara o hash vivo com o guardado: se a sua
lista de funções mudou e ninguém re-registrou, aparece contrato mudou do
nosso lado. É o hash que pega isso, não a versão — quem altera o contrato sem
subir a versão é exatamente o caso que se quer pegar. Declarar
versao no manifesto é opcional e ajuda a conversa; o hash é o que
mede.
O que pedimos em troca: avise quando o contrato mudar, e não mude o significado de uma função existente mantendo o mesmo nome e o mesmo schema. Essa é a única mudança que nenhuma verificação automática pega — o hash continua igual, a versão continua igual, e nada acusa. É também a única que pode fazer o assistente prometer ao dono uma coisa e o seu sistema fazer outra.
Como testar antes de falar com a gente
Dá para validar quase tudo sem nós.
- Suba o servidor e liste as ferramentas. Qualquer cliente MCP
(inclusive o
mcp devdo SDK oficial) mostra o que você está publicando. Leia as descrições com olhos de quem não conhece o seu produto: é assim que o assistente vai ler. - Confira as anotações. Toda função que não grava precisa ter
readOnlyHint: true. Conte-as. - Use duas credenciais de clientes diferentes e confirme que a mesma chamada devolve dados diferentes — e que não existe parâmetro capaz de trocar de cliente.
- Repita uma escrita com a mesma
Idempotency-Keye confirme que o segundo pedido não criou nada. - Provoque um erro de negócio (cliente inexistente, produto fora de linha) e leia a mensagem em voz alta: ela diz o que fazer em seguida?
- Se você declara uma tela, passe o cartão pelo crivo — o nosso, em okmigo-infra/cartao, com dados que pareçam os seus e com as suas operações de escrita. Registrar não vai te dar esse aviso: o que o crivo descarta some em silêncio, e a tela abre sem o pedaço.
Passou nesses seis, a conversa com a gente é curta.
Perguntas que sempre aparecem
- Vocês guardam os dados do meu sistema?
- Não. A ponte traduz e esquece — sem cópia, sem cache, sem banco. O que guardamos é o registro de que uma chamada aconteceu, para o dono do negócio poder auditar.
- Preciso mudar minha API?
- Não. O servidor MCP fica na frente dela e chama o que já existe.
- E se o modelo de IA inventar um argumento errado?
- A ponte valida contra o schema que você declarou antes de chamar. E toda operação que grava passa pela confirmação do dono, com a ação descrita em português, antes de chegar em você.
- Quem responde se algo for gravado errado?
- O registro mostra exatamente o que foi enviado, quando, e que o dono confirmou. É por isso que a confirmação é estrutural e não uma instrução ao modelo.
- Quanto custa? Tem exclusividade?
- Não cobramos pela integração e não pedimos exclusividade. Seu cliente continua sendo seu.
- Meu sistema é local, sem endereço na internet.
- Aí não dá — precisamos alcançar o servidor MCP. Um túnel resolve, mas é decisão sua e do seu cliente.
- Meu serviço não fala MCP e não vai falar.
- Escrevemos um integrador por protocolo, nunca um por serviço — é o que mantém "instalar" sendo configuração em vez de projeto. Hoje o caminho é MCP. OpenAPI/Swagger está planejado, pelo mesmo motivo: a especificação descreve as operações sozinha. API própria sem especificação legível por máquina a gente não consegue integrar.
Antes de nos procurar
Oito itens, e o sexto é o que mais volta.
Vale para os dois tipos- Servidor MCP no ar, sobre HTTP, com TLS e endereço alcançável
- Lista de funções com descrição escrita para um modelo ler
readOnlyHint: trueem todas as funções de leitura- Uma função por operação, parâmetros nomeados no schema
- Retorno sempre em objeto; erro de negócio como dado, dizendo o próximo passo
Idempotency-Keyrespeitado nas escritas- Escopo mínimo documentado
- O cliente NUNCA vem de um parâmetro do pedido
- Contato opcional — o filtro de saída pode recusar a chamada inteira por causa do FORMATO de um argumento
- Telefone e e-mail opcionais — o filtro de saída recusa a chamada inteira quando eles vêm junto
do_amigo — credencial do cliente
- Cada cliente tem a própria credencial no seu sistema
- O cliente é derivado dela, e de mais nada
- Revogar a credencial de um cliente para o acesso dele na hora
do_operador — credencial do OkMigo
- Aceita o cabeçalho
X-OkMigo-Tenantem toda chamada - Cria a conta na primeira vez que um valor aparece
- Isola por ele: nenhuma chamada com outro valor enxerga aquele dado
- Não tenta interpretar o valor, e não exige nenhuma outra identificação
- Recusa a chamada quando o cabeçalho não vier — em vez de escolher um padrão, que é como dois clientes viram um
- Consegue suspender ou limitar um valor sem derrubar os outros
- Um cliente recém-criado consegue se configurar por função — ou o estado vazio devolve um erro que diz onde ir
Depois disso, escreva para [email protected] com o endereço do seu servidor e a lista de funções.
E o que acontece depois — nenhum passo é seu- Lemos o seu
list_tools()e fixamos o contrato. Sai um hash — que é como a gente percebe depois que ele mudou — e a lista de operações com o efeito de cada uma. ⚠️ Essa leitura vai autenticada (Authorization: Bearer+X-OkMigo-Tenant), com o par de sondagem que você mandou no arquivo: sem ele, o passo 1 morre em 401 e nada é criado. - Conferimos com você as anotações de leitura. É a única conversa necessária, e é rápida.
- Registramos o seu serviço no catálogo — e nesse momento ele ainda não aparece para ninguém.
- O primeiro negócio instala e cola a credencial dele. A partir daí o serviço existe para aquele negócio, e só para ele.
- O assistente passa a usar as suas operações na conversa: leitura respondendo na hora, escrita perguntando antes.
⚠️ O passo 4 é onde a maioria vai tropeçar, e dá para conferir antes de escrever qualquer linha. Se hoje o seu serviço usa um token só, do operador, com o cliente vindo de configuração, a ponte recusa a chamada — ela exige a credencial do negócio, e é dela que você deriva de quem é o pedido. É a seção Uma credencial por cliente, e costuma ser a única mudança estrutural necessária.
Glossário
Os termos nossos que aparecem nesta página.
- amigo
- O assistente pessoal de um usuário. Memória dele, dele só.
- sócio
- O assistente de um negócio. A memória é do negócio, e várias pessoas podem operá-lo — é ele que vai chamar o seu serviço.
- ponte
- O serviço nosso que fala MCP com você. Traduz, valida, registra e esquece.
- portão de confirmação
- A regra que faz a primeira chamada de uma operação de escrita nunca executar, obrigando o assistente a perguntar antes.
- instalar
- O dono do negócio cadastrar a credencial dele do seu sistema no OkMigo. Do seu lado não acontece nada.
- crivo
- O código que lê o cartão que você declara e o reconstrói no nosso vocabulário, descartando em silêncio o que não conhece. É público (okmigo-infra/cartao) e roda na sua máquina, antes de registrar.