Pular para o conteúdo
Inteligência Artificial

MeuAccio: favoritos sincronizados, com API e MCP

Ilustração de um unicórnio com uma varinha convocando cards de sites com marcadores de fita, que voam presos por fios de luz até uma estante encantada, ao lado de três janelas de navegador com as mesmas pastas de favoritos coloridas

Olá meus Unicórnios! 🦄✨

Confesso uma coisa: eu tinha favoritos espalhados em quatro navegadores, uma pasta "ler depois" com 300 links que eu nunca li e aquela aba aberta há três semanas "para não esquecer". 😅 E quando eu queria achar o link daquela documentação boa, não estava em nenhum deles.

Foi daí que nasceu o MeuAccio, o meu novo projeto. Quem leu Harry Potter reconhece o nome: é o feitiço convocatório, o Accio que traz o objeto até você. Aqui o objeto é o link. Você cola o endereço, ele vira um card bonito com capa, título e descrição, e aparece nos favoritos de todos os seus navegadores. ✨

E, como tudo que eu faço ultimamente, ele nasceu conversando com IA: tem uma API REST e um servidor MCP para o Claude, o ChatGPT e o Cursor. Neste artigo eu mostro como o app funciona e depois as duas portas de entrada, com chamadas de verdade e as respostas que voltaram.

MeuAccioSeus favoritos em cards, em todo navegador, na sua IA e na sua API. Gratuito, sem anúncios e sem cartão.meuaccio.com.br

🪄 Como o MeuAccio funciona

A ideia cabe numa linha: cole um link, ele vira card, vira favorito em todo navegador, e a sua IA acha.

Quando você salva um endereço, o MeuAccio visita o site e lê de lá o título, a descrição, a imagem de capa e o ícone. Você não digita nada disso. E cada link pode, se você quiser:

  • ficar numa lista, com nome e cor. Cada lista vira uma pasta de favoritos no navegador;
  • ser arquivado: sai do mural e dos navegadores, mas continua guardado no Arquivo;
  • ir para a lixeira, onde fica 30 dias antes de sumir de vez.

Para salvar, é só colar o endereço na barra do topo (ou apertar Ctrl+K). Aparece esta janelinha, e o Enter salva na lista que está aberta. O Tab troca de lista antes de salvar:

A janela de salvar do MeuAccio com o endereço https://www.python.org/ colado, a opção Salvar este link em Exemplo do blog destacada, a opção Salvar em Divertido e o rodapé com os atalhos Enter, Tab e Esc

Um segundo depois, o card já estava no mural, com a cobrinha do Python de capa e a descrição tirada do próprio site:

O mural do MeuAccio aberto na lista Exemplo do blog, com cinco cards (PHP, Node.js, JavaScript da MDN, TypeScript e Python.org) e o aviso Salvo em Exemplo do blog no rodapé

Os cards desta lista são todos de teste, que eu criei para este artigo (e apaguei depois). No rodapé da barra lateral ficam o Arquivo e a Lixeira; em cima, as listas, que você arrasta para mudar a ordem.

O mesmo link em todo lugar

A parte que resolveu o meu problema dos quatro navegadores é a extensão, para Chrome, Edge, Opera e Firefox. Ela sincroniza nos dois sentidos: o que você salva no MeuAccio aparece nos favoritos do navegador, e o favorito que você cria com a estrelinha do navegador aparece no mural.

Você escolhe o quanto sincronizar: todos os favoritos, ou só uma pasta chamada "Meu Accio", para quem prefere manter o resto do navegador intocado. E arquivar um link tira ele dos favoritos também, sem apagar nada.

Além da extensão, tem app para Windows (com um atalho global de "Salvar link rápido", que funciona sem abrir a janela), o site instala como app no celular e aparece no menu Compartilhar dos outros apps, e dá para importar o HTML de favoritos exportado por qualquer navegador. Tudo de graça: sem anúncio, sem cartão, sem plano pago.

🔑 A chave da API

Tudo que vem daqui para baixo, fora o MCP com login, usa uma chave pessoal. Para gerar, clique no seu nome no rodapé da barra lateral e escolha API. Dê um nome para a chave (tipo "Script de backup") e clique em Gerar chave:

A janela API do MeuAccio na aba Chaves, com o endereço base https://www.meuaccio.com.br/api/v1, o campo Nova chave com o botão Gerar chave e uma chave ativa chamada Geral

Tirei o começo da minha chave da captura, que é o que a janela mostra ao lado do nome. Três coisas que vale saber:

  • A chave começa com pk_ e aparece inteira uma vez só, na hora em que é gerada. Copie na hora.
  • Ela dá acesso completo aos seus links e listas. Trate como senha.
  • Crie uma chave por integração: se precisar revogar uma (a lixeirinha do lado), as outras continuam funcionando.

Para não colar a chave dentro de script nenhum, guarde numa variável de ambiente. No Linux, no Mac ou no Git Bash:

export MEUACCIO_API_KEY=SUA_CHAVE_AQUI

No PowerShell do Windows, o jeito muda:

$env:MEUACCIO_API_KEY = "SUA_CHAVE_AQUI"

Essa variável vale só para aquela janela de terminal. Fechou, abriu outra, tem de definir de novo. E, para colar no terminal, use Ctrl+Shift+V ou o botão direito do mouse: o Ctrl+V comum não funciona na maioria deles.

🌐 A API na prática

A API fica em https://www.meuaccio.com.br/api/v1, fala JSON e recebe a chave no cabeçalho Authorization: Bearer. A primeira chamada confirma que a chave funciona e diz de quem ela é:

curl https://www.meuaccio.com.br/api/v1/me \
  -H "Authorization: Bearer $MEUACCIO_API_KEY"

A resposta traz o dono da conta. Troquei o nome e o e-mail por fictícios aqui:

{
    "user": {
        "id": 2,
        "name": "Unicórnio de Teste",
        "email": "[email protected]"
    }
}

A barra invertida no fim da linha só diz ao terminal que o comando continua na linha de baixo. Dá para escrever tudo numa linha só, se preferir.

E se a chave estiver errada, o 401 já diz qual é o problema. Com uma chave inventada:

{
    "error": {
        "message": "API Key inválida ou revogada.",
        "code": "API_KEY_INVALID"
    }
}

Criar uma lista

Lista tem nome e cor em hexadecimal. Criei uma só para os testes deste artigo:

curl -X POST https://www.meuaccio.com.br/api/v1/lists \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Exemplo do blog", "color": "#6e4c9a" }'
{
    "list": {
        "id": 2,
        "name": "Exemplo do blog",
        "color": "#6E4C9A",
        "position": 1,
        "linkCount": 0,
        "createdAt": "2026-09-28T10:46:10.000Z"
    }
}

A API respondeu 201 e devolveu a cor em maiúsculas: ela normaliza. O nome é único na conta, sem diferenciar maiúsculas: mandei de novo como "exemplo do blog" e veio um 409:

{
    "error": {
        "message": "Você já tem uma lista com esse nome.",
        "fields": {
            "name": "Nome já usado."
        }
    }
}

Salvar um link

O único campo obrigatório é a url. O listId é a lista (sem ele, o link fica em "Sem lista"):

curl -X POST https://www.meuaccio.com.br/api/v1/links \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.php.net/", "listId": 2 }'
{
    "link": {
        "id": 6,
        "url": "https://www.php.net/",
        "title": "PHP",
        "titleCustom": false,
        "listId": 2,
        "position": 0,
        "domain": "php.net",
        "description": "PHP is a popular general-purpose scripting language that powers everything from your blog to the most popular websites in the world.",
        "siteName": null,
        "imageUrl": "https://www.php.net/images/meta-image.png",
        "iconUrl": "https://www.php.net/favicon-196x196.png?v=2",
        "broken": false,
        "pending": false,
        "isArchived": false,
        "archivedAt": null,
        "createdAt": "2026-09-28T10:46:17.000Z",
        "updatedAt": "2026-09-28T10:46:17.000Z",
        "deletedAt": null,
        "purgeAt": null,
        "appUrl": "https://www.meuaccio.com.br/app?link=6"
    }
}

Olha quanta coisa veio de graça: o title, a description, a capa em imageUrl e o ícone em iconUrl, tudo lido do site do PHP. Os outros campos que valem a pena conhecer:

CampoO que significa
titleCustomtrue quando você deu um nome próprio ao link
pendingtrue enquanto o site ainda está sendo lido
brokentrue quando o site falhou nas últimas checagens
isArchivedse o link está no Arquivo
deletedAt e purgeAtquando foi para a lixeira e quando some de vez
appUrlabre o card destacado no mural do MeuAccio

As datas vêm sempre em UTC (o Z no fim). O 10:46 ali são 7h46 em Brasília.

O link repetido não é erro

Esta é a regra de que eu mais gosto: a mesma URL nunca é salva duas vezes. E a comparação é esperta. Tentei salvar o PHP de novo, só que com domínio em maiúsculas, um parâmetro de rastreamento e uma âncora:

curl -X POST https://www.meuaccio.com.br/api/v1/links \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://WWW.PHP.NET/?utm_source=blog#topo" }'

E o MeuAccio reconheceu que era o mesmo link. Veio um 409 com o código DUPLICATE_LINK e, dentro do erro, o link que já existia:

{
    "error": {
        "message": "Esse link já está salvo.",
        "fields": {
            "url": "Você já salvou esse link."
        },
        "code": "DUPLICATE_LINK",
        "link": {
            "id": 6,
            "url": "https://www.php.net/",
            "title": "PHP",
            "titleCustom": false,
            "listId": 2,
            "position": 0,
            "domain": "php.net",
            "description": "PHP is a popular general-purpose scripting language that powers everything from your blog to the most popular websites in the world.",
            "siteName": null,
            "imageUrl": "https://www.php.net/images/meta-image.png",
            "iconUrl": "https://www.php.net/favicon-196x196.png?v=2",
            "broken": false,
            "pending": false,
            "isArchived": false,
            "archivedAt": null,
            "createdAt": "2026-09-28T10:46:17.000Z",
            "updatedAt": "2026-09-28T10:46:17.000Z",
            "deletedAt": null,
            "purgeAt": null,
            "appUrl": "https://www.meuaccio.com.br/app?link=6"
        }
    }
}

Repare no detalhe: o 409 aqui não é falha. Ele responde a pergunta "onde está esse link?". Se o seu código trata todo status diferente de 2xx como erro e tenta de novo, vai ficar batendo no mesmo 409 para sempre. O certo é ler o error.link e dizer à pessoa: "você já salvou, está na lista 2".

Dois outros 422 que valem conhecer. Um texto que não é endereço:

{
    "error": {
        "message": "Link inválido.",
        "fields": {
            "url": "Informe um link válido (começando com http:// ou https://)."
        },
        "code": "INVALID_URL"
    }
}

E um endereço interno, como http://127.0.0.1/admin. O MeuAccio visita cada site que você salva, então ele se recusa a visitar endereços da rede interna (senão, qualquer pessoa usaria o servidor dele para espiar máquinas que não estão na internet):

{
    "error": {
        "message": "Esse endereço não pode ser salvo.",
        "fields": {
            "url": "Endereço interno ou bloqueado."
        },
        "code": "BLOCKED_URL"
    }
}

Vários links de uma vez

Para salvar uma pilha de links, o /links/batch aceita até 50 numa chamada. Mandei quatro: dois novos, um repetido e um inválido, de propósito:

curl -X POST https://www.meuaccio.com.br/api/v1/links/batch \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "links": [
        { "url": "https://nodejs.org/", "listId": 2 },
        { "url": "https://web.dev/", "listId": 2 },
        { "url": "https://www.php.net/" },
        { "url": "isto nao e um site" }
      ] }'
{
    "summary": {
        "created": 2,
        "restored": 0,
        "duplicates": 1,
        "invalid": 1
    },
    "results": [
        {
            "index": 0,
            "status": "created",
            "link": {
                "id": 7,
                "url": "https://nodejs.org/",
                "title": "nodejs.org",
                "titleCustom": false,
                "listId": 2,
                "position": 1,
                "domain": "nodejs.org",
                "description": null,
                "siteName": null,
                "imageUrl": null,
                "iconUrl": "https://nodejs.org/favicon.ico",
                "broken": false,
                "pending": true,
                "isArchived": false,
                "archivedAt": null,
                "createdAt": "2026-09-28T10:46:38.000Z",
                "updatedAt": "2026-09-28T10:46:38.000Z",
                "deletedAt": null,
                "purgeAt": null,
                "appUrl": "https://www.meuaccio.com.br/app?link=7"
            }
        },
        {
            "index": 1,
            "status": "created",
            "link": {
                "id": 8,
                "url": "https://web.dev/",
                "title": "web.dev",
                "titleCustom": false,
                "listId": 2,
                "position": 2,
                "domain": "web.dev",
                "description": null,
                "siteName": null,
                "imageUrl": null,
                "iconUrl": "https://web.dev/favicon.ico",
                "broken": false,
                "pending": true,
                "isArchived": false,
                "archivedAt": null,
                "createdAt": "2026-09-28T10:46:38.000Z",
                "updatedAt": "2026-09-28T10:46:38.000Z",
                "deletedAt": null,
                "purgeAt": null,
                "appUrl": "https://www.meuaccio.com.br/app?link=8"
            }
        },
        {
            "index": 2,
            "status": "duplicate",
            "link": {
                "id": 6,
                "url": "https://www.php.net/",
                "title": "PHP",
                "titleCustom": false,
                "listId": 2,
                "position": 0,
                "domain": "php.net",
                "description": "PHP is a popular general-purpose scripting language that powers everything from your blog to the most popular websites in the world.",
                "siteName": null,
                "imageUrl": "https://www.php.net/images/meta-image.png",
                "iconUrl": "https://www.php.net/favicon-196x196.png?v=2",
                "broken": false,
                "pending": false,
                "isArchived": false,
                "archivedAt": null,
                "createdAt": "2026-09-28T10:46:17.000Z",
                "updatedAt": "2026-09-28T10:46:17.000Z",
                "deletedAt": null,
                "purgeAt": null,
                "appUrl": "https://www.meuaccio.com.br/app?link=6"
            }
        },
        {
            "index": 3,
            "status": "invalid",
            "error": {
                "code": "INVALID_URL",
                "message": "Informe um link válido (começando com http:// ou https://)."
            }
        }
    ]
}

Duas coisas nessa resposta. Primeiro, o lote não é tudo ou nada: o inválido não impediu os outros. Cada item volta com o seu status (created, restored, duplicate ou invalid) e o index da posição que você mandou. Então leia o results item a item, não só o HTTP 200.

Segundo, olhe os dois links novos: "pending": true, o título é só o domínio e a capa é null. 😳 À primeira vista, parece que a leitura do site falhou. Não falhou: o lote salva primeiro e lê os sites em segundo plano. Uns segundos depois, pedi o web.dev de novo:

curl https://www.meuaccio.com.br/api/v1/links/8 \
  -H "Authorization: Bearer $MEUACCIO_API_KEY"
{
    "link": {
        "id": 8,
        "url": "https://web.dev/",
        "title": "web.dev",
        "titleCustom": false,
        "listId": 2,
        "position": 2,
        "domain": "web.dev",
        "description": "Orientações para criar experiências da Web modernas que funcionam em qualquer navegador.",
        "siteName": "web.dev",
        "imageUrl": "https://web.dev/images/social-wide.jpg?hl=pt-br",
        "iconUrl": "https://www.gstatic.com/devrel-devsite/prod/v5be16d8c9f9402e54c2da2ff24cb75c148f5d2bc84c6f8dfc8b9e16ae456160b/web/images/favicon.png",
        "broken": false,
        "pending": false,
        "isArchived": false,
        "archivedAt": null,
        "createdAt": "2026-09-28T10:46:38.000Z",
        "updatedAt": "2026-09-28T10:46:38.000Z",
        "deletedAt": null,
        "purgeAt": null,
        "appUrl": "https://www.meuaccio.com.br/app?link=8"
    }
}

Agora sim: pending virou false, chegaram a descrição, a capa e o ícone. Se o seu programa mostra o título logo depois de salvar, espere uns segundos e leia de novo quando vier pending: true.

Buscar

A busca é o parâmetro q. Cada palavra precisa aparecer no nome, na descrição ou no endereço. Buscando "navegador":

curl "https://www.meuaccio.com.br/api/v1/links?q=navegador&limit=10" \
  -H "Authorization: Bearer $MEUACCIO_API_KEY"

Vieram dois links: o web.dev, que tem "navegador" na descrição, e um link da MDN sobre HTTP que já estava na conta (a descrição dele fala em "navegadores web"). A resposta termina com a paginação:

"total": 2, "limit": 10, "offset": 0

O limit vai até 100, e o offset anda de página em página até o total. Dá para trocar a visão com view (all, nolist, archived, trash) e filtrar uma lista com listId. As aspas em volta do endereço importam: sem elas, o terminal entende o & como "rode isto em segundo plano" e corta o comando no meio.

Renomear, mover e arquivar

O PATCH recebe só o que muda. Dei um nome próprio ao link do PHP:

curl -X PATCH https://www.meuaccio.com.br/api/v1/links/6 \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Manual do PHP" }'

O link voltou com "title": "Manual do PHP" e "titleCustom": true. Mandando "title": null, ele voltou a ser "PHP", o título do site, com titleCustom de novo em false. Sem nome próprio, o título acompanha o site quando ele muda.

Arquivar é o mesmo PATCH, com { "isArchived": true }. Arquivei o web.dev e pedi as contagens da conta:

curl https://www.meuaccio.com.br/api/v1/links/counts \
  -H "Authorization: Bearer $MEUACCIO_API_KEY"
{
    "counts": {
        "all": 4,
        "noList": 1,
        "archived": 1,
        "trash": 0
    }
}

O arquivado some da lista e do mural, mas não vai para a lixeira: fica no archived. Para trocar a ordem dos cards, existe o /move, com o beforeId dizendo "coloque antes deste":

curl -X POST https://www.meuaccio.com.br/api/v1/links/6/move \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "listId": 2, "beforeId": 7 }'

O link 6 foi para a position 0 da lista 2, logo antes do 7. Sem o beforeId, ele vai para o fim da lista.

🗑️ Excluir não é apagar

O DELETE responde 204, sem corpo nenhum, e manda o link para a lixeira:

curl -X DELETE https://www.meuaccio.com.br/api/v1/links/6 \
  -H "Authorization: Bearer $MEUACCIO_API_KEY"

Com ?view=trash, ele aparece com as duas datas preenchidas:

"deletedAt": "2026-09-28T10:47:12.000Z",
"purgeAt": "2026-10-28T10:47:12.000Z",

O purgeAt é o dia em que ele some de vez: 30 dias depois. Até lá, um POST /links/6/restore traz o link de volta. E o DELETE /links/6/permanent, que apaga na hora, só funciona com o link já na lixeira. Tentei com um link fora dela e veio 404, "Link não encontrado(a)". É uma trava de propósito: nada some para sempre sem passar pela lixeira.

A armadilha de salvar de novo

Esta me pegou. Com o link do PHP na lixeira, salvei o mesmo endereço outra vez, sem o listId:

curl -X POST https://www.meuaccio.com.br/api/v1/links \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.php.net/" }'
{
    "link": {
        "id": 6,
        "url": "https://www.php.net/",
        "title": "Manual do PHP",
        "titleCustom": true,
        "listId": null,
        "position": 1,
        "domain": "php.net",
        "description": "PHP is a popular general-purpose scripting language that powers everything from your blog to the most popular websites in the world.",
        "siteName": null,
        "imageUrl": "https://www.php.net/images/meta-image.png",
        "iconUrl": "https://www.php.net/favicon-196x196.png?v=2",
        "broken": false,
        "pending": false,
        "isArchived": false,
        "archivedAt": null,
        "createdAt": "2026-09-28T10:47:12.000Z",
        "updatedAt": "2026-09-28T10:47:12.000Z",
        "deletedAt": null,
        "purgeAt": null,
        "appUrl": "https://www.meuaccio.com.br/app?link=6"
    }
}

Não veio 409, nem um link novo: voltou o mesmo id 6, com o nome "Manual do PHP" que eu tinha dado. Só que com "listId": null. 🤯 O link saiu da lixeira direto para "Sem lista", porque a lista vale o que você mandou agora. Se o seu script "desfaz" uma exclusão salvando de novo, mande o listId junto, ou use o /restore, que devolve o link para a lista de antes.

🟩 Um script em Node, sem instalar nada

O curl é ótimo para experimentar. Para o dia a dia, um script é mais confortável. Este aqui acha uma lista pelo nome e salva nela uma pilha de links numa chamada só, mostrando o que aconteceu com cada um. Ele usa o fetch que já vem no Node 18 ou mais novo: não precisa de npm install nenhum.

Salve como favoritos.js (no nano favoritos.js, cole com Ctrl+Shift+V, salve com Ctrl+O e Enter, saia com Ctrl+X; ou use o editor que preferir):

// favoritos.js: salva vários links numa lista do MeuAccio usando só o fetch do Node 18+
// Rode com: MEUACCIO_API_KEY=sua_chave node favoritos.js

const BASE = "https://www.meuaccio.com.br/api/v1";
const CHAVE = process.env.MEUACCIO_API_KEY;

const NOME_DA_LISTA = "Exemplo do blog";
const LINKS = [
    "https://www.typescriptlang.org/",
    "https://www.php.net/",
    "isto nao e um site",
];

if (!CHAVE) {
    console.error("Defina a variável de ambiente MEUACCIO_API_KEY.");
    process.exit(1);
}

async function chamarApi(metodo, caminho, corpo) {
    const resposta = await fetch(BASE + caminho, {
        method: metodo,
        headers: {
            "Authorization": "Bearer " + CHAVE,
            "Content-Type": "application/json",
        },
        body: corpo ? JSON.stringify(corpo) : undefined,
    });

    // O DELETE responde 204, sem corpo: chamar .json() aqui daria erro
    if (resposta.status === 204) {
        return null;
    }

    const dados = await resposta.json();

    if (!resposta.ok) {
        throw new Error(`HTTP ${resposta.status}: ${dados.error.message}`);
    }

    return dados;
}

async function acharLista(nome) {
    const dados = await chamarApi("GET", "/lists");

    // Compara sem diferenciar maiúsculas: "exemplo do blog" acha "Exemplo do blog"
    for (const lista of dados.lists) {
        if (lista.name.toLowerCase() === nome.toLowerCase()) {
            return lista;
        }
    }
    return null;
}

function esperar(segundos) {
    return new Promise((pronto) => setTimeout(pronto, segundos * 1000));
}

async function principal() {
    try {
        const lista = await acharLista(NOME_DA_LISTA);
        if (!lista) {
            console.error("A lista", NOME_DA_LISTA, "não existe. Crie antes no MeuAccio.");
            return;
        }

        const itens = [];
        for (const url of LINKS) {
            itens.push({ url: url, listId: lista.id });
        }

        // Uma chamada só para até 50 links, em vez de um POST por link
        const dados = await chamarApi("POST", "/links/batch", { links: itens });
        console.log("Resumo:", dados.summary);

        const pendentes = [];
        for (const item of dados.results) {
            if (item.status === "invalid") {
                console.log("-", LINKS[item.index], "->", item.error.message);
            } else {
                console.log("-", item.link.url, "->", item.status, "(id " + item.link.id + ")");
            }
            // Link novo volta com pending: true e o título provisório é só o domínio
            if (item.link && item.link.pending) {
                pendentes.push(item.link.id);
            }
        }

        if (pendentes.length > 0) {
            await esperar(5);
            for (const id of pendentes) {
                const lido = await chamarApi("GET", "/links/" + id);
                console.log("Depois de ler o site:", lido.link.title);
            }
        }
    } catch (erro) {
        console.error("Deu erro:", erro.message);
    }
}

principal();

Três detalhes evitam bugs que eu já vi muita gente cometer:

  • O if do 204 vem antes do .json(). O DELETE responde sem corpo, e resposta.json() num corpo vazio estoura um SyntaxError. Este script não exclui nada, mas o chamarApi é o pedaço que você vai copiar para outros.
  • O lote é lido item a item. O HTTP é 200 mesmo com item inválido ou repetido; quem diz o que aconteceu com cada link é o status dentro de results.
  • O item.link && antes do pending. Item inválido não tem link, só error. Sem essa checagem, o script morre com "Cannot read properties of undefined" justamente no link ruim.

Para rodar, no Linux, no Mac ou no Git Bash, a variável vai na frente do comando:

MEUACCIO_API_KEY=SUA_CHAVE_AQUI node favoritos.js

No PowerShell, defina a variável antes e rode em seguida:

$env:MEUACCIO_API_KEY = "SUA_CHAVE_AQUI"
node favoritos.js

A saída:

Resumo: { created: 1, restored: 0, duplicates: 1, invalid: 1 }
- https://www.typescriptlang.org/ -> created (id 10)
- https://www.php.net/ -> duplicate (id 6)
- isto nao e um site -> Informe um link válido (começando com http:// ou https://).
Depois de ler o site: JavaScript With Syntax For Types.

O TypeScript entrou com título provisório e, cinco segundos depois, já tinha o título do site. O PHP foi reconhecido como repetido, e o texto inválido voltou com a mensagem pronta para mostrar. Com uma chave errada, a mesma execução para no primeiro passo:

Deu erro: HTTP 401: API Key inválida ou revogada.

📖 A documentação interativa

Não precisa decorar nada disso. A API tem uma referência interativa em https://www.meuaccio.com.br/api/v1/docs/, gerada da especificação OpenAPI 3.1, aberta e sem precisar de chave para ler:

A documentação interativa da MeuAccio API, versão 1.1.0 e OAS 3.1, com o resumo de autenticação, formato, links, limite e erros, o botão Authorize e as primeiras rotas: GET /me, GET /links e POST /links

O botão Authorize recebe a sua chave e deixa testar as chamadas pelo navegador. O arquivo /api/v1/openapi.json importa direto no Postman ou no Insomnia. E também tem um /api/v1/skill.md, um manual no formato Agent Skills para agentes de IA que rodam comandos, como o Claude Code, aprenderem a usar a API sozinhos.

Os erros cabem numa tabela, e todos vêm no mesmo formato: uma message em português, o campo com problema em fields e, quando o seu código precisa reagir a um caso, um code:

CódigoQuando acontece
401Chave ausente (API_KEY_MISSING) ou inválida e revogada (API_KEY_INVALID)
404O link ou a lista não existe, é de outra conta ou está na lixeira
409Link já salvo (DUPLICATE_LINK) ou nome de lista repetido
422Dado inválido: INVALID_URL, BLOCKED_URL ou o campo em fields
429Mais de 120 requisições por minuto com a mesma chave

🔌 O MCP: a sua IA ganha ferramentas

A API é para o seu código. O MCP (Model Context Protocol) é para a sua IA: o MeuAccio vira um servidor que entrega ferramentas prontas, cada uma com nome, descrição e parâmetros. A IA não monta curl nenhum; ela chama save_link e pronto.

O servidor fica em https://www.meuaccio.com.br/api/mcp. No Claude, no ChatGPT, no Cursor ou no VS Code, você procura por "conector personalizado" (ou "servidor MCP"), cola esse endereço e entra com a sua conta numa janela do MeuAccio. Não precisa copiar chave nenhuma. As IAs conectadas aparecem na mesma janela da API, na aba IAs (MCP), cada uma com o seu botão de desconectar:

A aba IAs (MCP) da janela API do MeuAccio, com o endereço https://www.meuaccio.com.br/api/mcp, os três passos para conectar (adicionar o conector, autorizar com a conta ou usar uma chave) e os aplicativos conectados ChatGPT e Claude

No Claude Code, é um comando:

claude mcp add --transport http meuaccio https://www.meuaccio.com.br/api/mcp

Para cliente que não fala OAuth, a chave vai no cabeçalho, num mcp.json:

{
    "mcpServers": {
        "meuaccio": {
            "type": "http",
            "url": "https://www.meuaccio.com.br/api/mcp",
            "headers": {
                "Authorization": "Bearer SUA_CHAVE_AQUI"
            }
        }
    }
}

O que o servidor oferece

MCP é JSON-RPC por HTTP, então dá para conversar com ele pelo próprio curl. Pedi a lista de ferramentas (o método tools/list do protocolo), usando a chave no cabeçalho:

curl -X POST https://www.meuaccio.com.br/api/mcp \
  -H "Authorization: Bearer $MEUACCIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'

O cabeçalho Accept com os dois formatos é exigência do transporte do MCP: sem ele, o servidor responde 406, "Not Acceptable". Vieram dezesseis ferramentas:

FerramentaO que faz
searchBusca links pelo nome, descrição ou endereço
fetchLê os detalhes de um link pelo id
whoamiNome e e-mail da conta conectada
list_linksLista links com filtros e paginação
get_link_countsQuantos há em Todos, Sem lista, Arquivo, Lixeira e em cada lista
save_linkSalva um link (sem duplicar)
save_linksSalva de 1 a 50 links numa chamada
update_linkRenomeia, muda de lista ou arquiva
move_linkPõe o link numa lista, numa posição
refresh_linkLê o site de novo agora
delete_linkManda para a lixeira
restore_linkTira da lixeira
list_listsTodas as listas, na ordem da pessoa
create_listCria uma lista
update_listRenomeia ou muda a cor
delete_listExclui a lista; os links ficam "Sem lista"

A descrição de cada ferramenta já ensina a IA a se comportar. A da delete_link, por exemplo, termina com "Confirme com a pessoa antes", e a da create_list manda perguntar antes de criar. E vêm quatro prompts prontos: organizar_sem_lista (sugere uma lista para cada link solto e só move depois da sua confirmação), resumo_da_semana (o que você salvou nos últimos 7 dias, por assunto), salvar_links_da_conversa (junta os links que apareceram no chat e salva todos de uma vez) e revisar_links_quebrados (acha os sites que pararam de responder).

O MCP em ação

Chamei a save_link com um endereço sem o https://, do jeito que uma pessoa escreveria no chat:

{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
        "name": "save_link",
        "arguments": {
            "url": "developer.mozilla.org/pt-BR/docs/Web/JavaScript",
            "listId": 2
        }
    }
}

O resultado vem em structuredContent:

{
    "saved": true,
    "link": {
        "id": 9,
        "url": "https://developer.mozilla.org/pt-BR/docs/Web/JavaScript",
        "title": "JavaScript | MDN",
        "titleCustom": false,
        "listId": 2,
        "position": 2,
        "domain": "developer.mozilla.org",
        "description": "JavaScript® (às vezes abreviado para JS) é uma linguagem leve, interpretada e baseada em objetos com funções de primeira classe, mais conhecida como a linguagem de script para páginas Web, mas usada também em vários outros ambientes sem browser, tais como node.js, Apache CouchDB e Adobe Acrobat. O JavaScript é uma linguagem baseada em protótipos, multi-paradigma e dinâmica, suportando estilos de orientação a objetos, imperativos e declarativos (como por exemplo a programação funcional). Saiba mais sobre o JavaScript.",
        "siteName": "MDN Web Docs",
        "imageUrl": "https://developer.mozilla.org/mdn-social-image.46ac2375.png",
        "iconUrl": "https://developer.mozilla.org/favicon.ico",
        "broken": false,
        "pending": false,
        "isArchived": false,
        "archivedAt": null,
        "createdAt": "2026-09-28T10:47:37.000Z",
        "updatedAt": "2026-09-28T10:47:37.000Z",
        "deletedAt": null,
        "purgeAt": null,
        "appUrl": "https://www.meuaccio.com.br/app?link=9"
    }
}

O https:// que faltava foi colocado sozinho, e o "saved": true diz à IA que o link é novo (salvei o PHP pelo MCP também, e veio "saved": false com o link que já existia). O appUrl no fim é o que permite à IA responder "salvei, está aqui" com um link clicável que abre o card no mural. ✨

E a search devolve um formato enxuto, o mesmo que o ChatGPT espera dos conectores. Buscando "php":

{
    "results": [
        {
            "id": "6",
            "title": "PHP",
            "url": "https://www.php.net/"
        }
    ]
}

Repare que o id vem como texto ("6"), não como número. É o formato do protocolo de busca. Para ler o link inteiro, a IA chama a fetch com esse id.

Todos os links e a lista de teste deste artigo foram apagados no fim, e a conta ficou como estava. Se você quiser experimentar, é só criar a sua conta e colar o primeiro link:

Abrir o MeuAccioLinks favoritos em cards, extensão para Chrome, Edge, Opera e Firefox, API REST e servidor MCP, tudo gratuito.meuaccio.com.br

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Perguntas frequentes

O MeuAccio é gratuito?
É. Não tem anúncio, não pede cartão e não existe plano pago: mural, listas, extensão, app para computador, API e MCP estão em toda conta. O limite da API é técnico e igual para todos: 120 requisições por minuto por chave.
Por que a API responde 409 ao salvar um link?
Porque aquela URL já está salva na conta. O MeuAccio compara ignorando utm_*, a #âncora e maiúsculas no domínio, então https://WWW.PHP.NET/?utm_source=blog#topo é o mesmo link que https://www.php.net/. O 409 traz em error.link onde ele já está.
Por que o link salvo volta com o título igual ao domínio?
Porque o site ainda está sendo lido. Link novo pode voltar com pending: true e título provisório (o domínio). Segundos depois, um GET /links/{id} traz o título, a descrição e a capa de verdade.
Qual a diferença entre arquivar e excluir um link?
Arquivar (isArchived: true) tira o link do mural e dos favoritos do navegador, mas ele continua salvo no Arquivo. Excluir manda para a lixeira por 30 dias, de onde ele volta com POST /links/{id}/restore.
Salvar de novo um link que está na lixeira cria outro?
Não. O link volta da lixeira com o mesmo id, mas vai para "Sem lista" se você não mandar o listId na hora de salvar. Mande a lista junto, ou mova depois.
Preciso copiar a chave da API para usar o MCP?
Não. No Claude, no ChatGPT ou no Cursor, você adiciona https://www.meuaccio.com.br/api/mcp como conector e entra com a sua conta (OAuth). A chave só é necessária para cliente que não fala OAuth, no cabeçalho Authorization: Bearer.

Leia também

Node.js

Avaliando o RAG com o Jev como juiz

Como dar nota ao seu RAG em Node.js: um gabarito, a busca do Qdrant medida sem IA e o Jev, da TypeSafe, dizendo se cada resposta é fiel e correta.