Publicado 23 de set. de 20269 min de leitura
O AI Connect SDK: dê aos seus agentes de IA os conectores de cada usuário como ferramentas, uma linha por framework
O imposto de integração dos produtos de IA está no wiring, não no modelo. O AI Connect SDK do Vinkius remove isso: um cliente TypeScript de zero dependência, handles com escopo por usuário e nove adaptadores de framework. Como funciona, o código mínimo e um assistente multiusuário funcionando, pronto para entregar.

Por Renato Marinho
Founder · Vinkius
A pergunta que mais recebo sobre um produto de IA não é sobre o modelo. É sobre o wiring. Como o seu assistente lê o GitHub do próprio usuário? Como posta no Slack dele? Como puxa um registro do CRM dele? Na maioria dos produtos, a resposta é que um time gastou meses construindo e mantendo uma prateleira de integrações: os handshakes de OAuth, a rotação de tokens, as formas de erro de cada provider, as retentativas, o drift de schema silencioso no momento em que uma API de upstream muda. Nada disso torna o produto mais inteligente. Ele fica entre o modelo e a coisa que o usuário realmente queria, e continua pedindo orçamento.
Esse imposto é o que o AI Connect SDK do Vinkius remove. É a parte do Vinkius que eu menos falo em calls de vendas e mais falo com engenheiros, porque é onde mora a alavanca real: um cliente TypeScript pequeno que transforma os serviços conectados de um usuário em ferramentas que o seu modelo pode chamar, sem que o seu time detenha uma única credencial de upstream.
O SDK em uma frase
@vinkius/connect é um cliente TypeScript de zero dependência de runtime para a plataforma de conectividade do Vinkius. O seu aplicativo é identificado por um par de chaves que você cria uma vez no dashboard do Vinkius: um id público de app e uma chave secreta de aplicação. Depois você endereça os seus próprios usuários finais pelo id que você já lhes atribui no seu sistema, e para cada um deles o SDK entrega um handle com escopo para os conectores que eles conectaram. O Vinkius provisiona a conexão, guarda as credenciais e executa as capacidades atrás de um data plane governado. O seu código não carrega nenhuma chave de API de upstream, nenhum fluxo de OAuth, nenhum loop de retentativa para uma API de terceiro. Você descreve o trabalho no próprio dialeto de ferramentas do seu modelo, e um adaptador o encaminha para o conector certo daquele usuário.
Aquela frase é a proposição de valor inteira. O resto deste post é prova.
O modelo: um app, muitos usuários, com escopo por usuário
A decisão de projeto que eu defenderia com mais força é que a unidade de escopo é o usuário, não o tenant. Um assistente de chat que pode agir sobre a agenda de alguém só é seguro quando a agenda que ele pode tocar é a que aquela pessoa conectou, e nada além. O SDK codifica isso diretamente: vinkius.user("yourUserId") constrói um handle preguiçoso que faz zero chamadas de rede e nunca resolve nem guarda um id interno de usuário. Ele endereça a plataforma pelo seu id externo, do topo até a execução.
Cada conector de um usuário é uma conexão isolada com o próprio token. Listar e executar capacidades acontece no runtime daquela conexão, então o GitHub conectado de um usuário nunca alcança o de outro. Cada conexão é medida e pode ser desligada independentemente, e um token revogado falha fechado em vez de reemitir em silêncio. Quando um usuário desliga um serviço, a capacidade some do conjunto de ferramentas daquele usuário no momento em que a próxima requisição a lista. O isolamento não é uma promessa nos docs. É a forma do caminho de dados.
O código, em três passos
Este é o fluxo de conexão e credenciais, direto dos próprios exemplos do SDK. Você conecta o conector GitHub de um usuário, lê o schema que ele espera, escreve o token e confere a prontidão:
import { Vinkius } from '@vinkius/connect';
const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!,
});
async function connectGithub(userId: string, githubToken: string) {
const github = vinkius.user(userId).connector('github');
await github.connect();
const schema = await github.credentials.schema();
await github.credentials.set({ GITHUB_TOKEN: githubToken });
const status = await github.status();
console.log(`github status=${status} requires=${Object.keys(schema).join(',')}`);
}
status volta com uma de quatro palavras: not_connected, needs_credentials, ready ou disabled. Credenciais são somente para escrita. credentials.schema() e credentials.status() dizem quais chaves existem e quais estão configuradas, nunca os valores, e o único caminho que um segredo percorre é para dentro, uma vez, no cofre.
O segundo passo é a agregação. Uma chamada retorna as capacidades executáveis através de todos os conectores prontos do usuário, já com namespace para que dois conectores que entregam uma ferramenta create não colidam:
const caps = await vinkius.user('alice_123').capabilities();
// caps is a CapabilitySet: an array of executable Capability objects
O terceiro passo é o adaptador. Esta é a linha que paga pela plataforma, porque é a mesma forma de código não importa qual modelo você rode:
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';
const tools = toOpenAITools(caps);
// pass tools to your model, then dispatch each call back:
const result = await runOpenAIToolCall(caps, call, { idempotencyKey: `alice_123:${call.id}` });
Troque o modelo e você troca o import. Há nove subpath adapters para os principais frameworks de modelo, dos SDKs do OpenAI e do Anthropic a LangChain, LlamaIndex, o Vercel AI SDK, OpenAI Agents e Cloudflare Workers AI, mais um despachador JSON Schema neutro de framework para o resto. Cada um é um conversor de tipo estrutural de zero dependência. Ele converte as capacidades conectadas para o dialeto de ferramentas do seu modelo e de volta, e deliberadamente não importa o SDK do vendor, então adicionar o Vinkius ao seu projeto não puxa nada que ele já não use.
Um assistente multiusuário funcionando
Isto é a implementação que eu entregaria. É um handler de chat que responde por um usuário usando os próprios conectores daquele usuário, com o loop de ferramentas completo: o modelo pede uma ferramenta, nós executamos no runtime governado, devolvemos o resultado e limitamos o loop para que um modelo malcomportado não te faça pagar para sempre.
import OpenAI from 'openai';
import { Vinkius } from '@vinkius/connect';
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';
const openai = new OpenAI();
const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!,
});
// Any model your chosen provider serves. The integration is model agnostic:
// nothing below depends on a specific model name.
const MODEL = process.env.OPENAI_MODEL ?? 'your-model-id';
export async function assistantFor(userId: string, question: string) {
// 1. What can this user's connectors actually do, right now?
const caps = await vinkius.user(userId).capabilities();
if (caps.length === 0) {
return 'You have not connected a service yet. Connect one in the app and I can act on it.';
}
// 2. Hand the model this user's capabilities as its own tools.
const messages = [{ role: 'user', content: question }];
const tools = toOpenAITools(caps);
// 3. The tool loop. The model picks, we execute at the runtime, we feed the result back.
let reply;
for (let step = 0; step < 4; step++) {
reply = await openai.chat.completions.create({
model: MODEL,
messages,
tools,
});
const msg = reply.choices[0]?.message;
if (!msg || !msg.tool_calls || msg.tool_calls.length === 0) break;
messages.push(msg);
for (const call of msg.tool_calls) {
// Tool level failures come back as isError results, not throws.
// Transport level failures throw the SDK's typed error classes.
const result = await runOpenAIToolCall(caps, call, {
idempotencyKey: `${userId}:${call.id}`,
});
const text = result.content.map((c) => c.text).join('\n');
messages.push({
role: 'tool',
tool_call_id: call.id,
content: result.isError ? `The tool reported an error: ${text}` : text,
});
}
}
return reply?.choices[0]?.message?.content ?? '';
}
Quatro coisas nesse bloco valem a pena apontar, porque é nelas que uma versão ingênua vai errar em silêncio.
Primeiro, a lista de capacidades é buscada por requisição, por usuário, e somente de conectores prontos. Um usuário que conecta um novo serviço o recebe no próximo turno, sem deploy, e um usuário que revoga um perde as ferramentas correspondentes na mesma hora. Nada é cacheado de um jeito que sobreviva à relação de confiança.
Segundo, o idempotencyKey na chamada de execução é o que torna a retentativa segura. O SDK retenta respostas transitórias, mas um POST que cria um efeito colateral só é retentado quando você declara a chave, porque aí o replay é deduplicado no lado do servidor em vez de dobrado. A chave acima é namespaced por usuário e id de chamada de ferramenta, então dois usuários agindo no mesmo conector nunca colidem.
Terceiro, o teto do loop. Um agente que lê o resultado de uma ferramenta, decide que não é suficiente e chama a próxima ferramenta é assim que o produto faz trabalho útil. É também assim que um modelo confuso pode caminhar por muito tempo. Limitar os passos limita a fatura e o blast radius, e a resposta em texto puro do modelo no último turno é o que você devolve.
Quarto, a divisão de erro. Uma capacidade que falha no nível da ferramenta (a API que ela envolve devolveu um erro) volta como um resultado com isError: true. O modelo lê aquilo e pode se recuperar, tentar outra ferramenta, ou explicar a falha ao usuário. Uma falha de transporte ou de autenticação lança uma das classes de erro tipadas do SDK. Duas formas de falha, dois handlers diferentes, e nenhum dos dois derruba o loop inteiro.
Rode em um framework diferente e apenas a linha do adaptador muda. Para o Anthropic, é toAnthropicTools e runAnthropicToolUse. Para o LangChain, você injeta a fábrica de tool do framework para que o SDK não entregue nenhum peer dependency. O lado de capacidade é idêntico.
Por que foi construído assim
Eu volto sempre a uma regra que eu cobro do time: o SDK não pode fazer o seu app deter um segredo que ele não consegue ver nem revogar. Tudo na forma vem disso. Credenciais são somente para escrita e cifradas em repouso, e a plataforma não deixa nem os próprios operadores lerem. Tokens de conexão são escopados para um único conector, autenticados por HMAC, e o texto claro nunca é armazenado, então o pior caso de um vazamento é um ativo nomeado, revogável, de um único conector, não uma passagem permanente para o patrimônio inteiro. Retentativas têm teto e jitter, e um timeout por requisição faz a leitura do corpo disputar o prazo para que um stream travado não prenda um worker. O mesmo objeto de transporte dirige o control plane e o data plane, e é por isso que as semânticas de retentativa, timeout e redação nunca podem divergir entre os dois.
O único lugar onde eu não suavizo a linguagem: mantenha as chaves de aplicação no lado do servidor. Este é um cliente de backend. O id público de app e a chave secreta pertencem ao seu ambiente, não a um bundle, e o design pressupõe que o escopo de usuário que você passa é algo que o seu servidor consegue provar que é o chamador. Se você for tentado a embarcá-lo no navegador para poupar uma ida e volta, você moveu o problema do trabalho de integração para o gerenciamento de segredos, que é um negócio pior.
O que ele entrega
npm install @vinkius/connect. É ESM e CJS com tipos completos, com tree shaking, e roda em Node 18 ou superior e em qualquer runtime que tenha fetch. Nenhuma dependência de runtime para auditar, nenhum inchaço de lock file, nenhuma cadeia de suprimentos transitiva para discutir com o time de segurança. A licença é Apache 2.0, então o código é seu para guardar e rodar onde o seu app roda.
A camada de capacidades que ele aciona está coberta em o post sobre a camada de capacidades e o catálogo, e as garantias de execução nas quais ele se apoia estão em o post sobre rodar servidores MCP não confiáveis em isolates V8. Este post é a superfície de desenvolvedor das duas: o cliente contra o qual o seu time escreve.
Entregue a funcionalidade, não o projeto de integração. Esse é o ponto inteiro, e é por isso que o SDK tem um único trabalho: entregar ao seu modelo as ferramentas que o seu usuário conectou, no dialeto que o seu modelo já fala, e sair do caminho.
