Skip to content

Repository files navigation

Social MCP Portal (Cloudflare Workers)

MCP servers for Instagram, Facebook Pages, X (Twitter), WhatsApp, Google Business Profile, YouTube, Google Ads and Google Analytics (GA4), running as a single stateless Worker via createMcpHandler from the Agents SDK.

Endpoints

Endpoint Server Tools
/mcp Social MCP Portal every tool below, in one endpoint
/mcp/instagram Instagram MCP Server instagram_*
/mcp/facebook Facebook MCP Server facebook_*
/mcp/x X (Twitter) MCP Server x_*
/mcp/whatsapp WhatsApp MCP Server whatsapp_*
/mcp/google Google MCP Server all four Google servers below
/mcp/google-business Google Business Profile MCP Server google_business_*
/mcp/youtube YouTube MCP Server youtube_*
/mcp/google-ads Google Ads MCP Server google_ads_*
/mcp/google-analytics Google Analytics MCP Server ga4_*
/mcp/tenant Tenant API Gateway sintetizadas do manifesto do tenant

Every endpoint also exposes ping, which reports which credentials are configured — handy for checking the deploy without touching the social APIs.

Tenant API Gateway (/mcp/tenant)

Os outros endpoints têm ferramentas fixas no código. Este não: as ferramentas são sintetizadas do manifesto do tenant que está chamando, resolvido pelo token da requisição. Ele existe para a plataforma de agentes de WhatsApp (celfons/whatsapp, issue #1324) consultar a API própria de cada cliente ao vivo, dentro do turno, sem que o backend dela mude uma linha: para o backend, isto aqui é um servidor MCP como qualquer outro.

Como funciona

turno → backend lê tenant_mcp_servers (URL = este endpoint)
      → tools/list  (sintetizado do manifesto)
      → o LLM escolhe a ferramenta; bindCallScope injeta o telefone verificado
      → tools/call  → este gateway chama a API REST do cliente
                    → projeta SÓ os campos declarados
      → o texto entra no prompt como <external_data>

O manifesto

Guardado em KV (TENANT_MANIFESTS), em duas famílias de chave:

tenant-token:<sha-256 do token>  ->  tenantId
tenant-manifest:<tenantId>       ->  o manifesto

O índice guarda o hash do token, não o token: um dump do KV não vira um chaveiro.

{
  "tenantId": "tnt_1",
  "label": "ERP da Loja",
  "baseUrl": "https://api.cliente.com",
  "auth": { "type": "bearer", "token": "..." },
  "timeoutMs": 3000,
  "tools": [
    {
      "name": "consultar_pedido",
      "description": "Status e previsão de entrega de um pedido",
      "method": "GET",
      "path": "/pedidos/{orderId}",
      "scope": "customer",
      "identityParam": "telefone",
      "params": [
        { "name": "orderId", "in": "path", "required": true },
        { "name": "telefone", "in": "query", "required": true }
      ],
      "fields": [
        { "path": "status", "label": "Status" },
        { "path": "entrega.previsao", "label": "Previsão" }
      ],
      "maxChars": 1200
    }
  ]
}

Onde este Worker atende — confira ANTES de cadastrar

O host que você usa tem de ser um que chegue a este Worker. Isso não é subentendido: mcp.closing.trade não é este Worker — é um portal MCP protegido por OAuth, e ele responde 401 invalid_token em /mcp* e 404 em todo o resto, sem nunca encostar aqui. Cadastrar contra ele falha em silêncio duas vezes: a rota admin dá 404, e a plataforma, que manda Authorization: Bearer <token do tenant>, é recusada pelo portal como token OAuth inválido.

Descubra o host certo no dashboard: Workers & Pages → mcp-social → Settings → Domains & Routes. Sem routes no wrangler.jsonc e sem workers_dev: false, existe uma URL mcp-social.<subdomínio-da-conta>.workers.dev que já funciona hoje. Para um host bonito, adicione um Custom Domain ao Worker (ex.: gateway.closing.trade) — não tente rotear um subpath de mcp.closing.trade, que já pertence ao portal.

Teste de um comando ($GATEWAY = o host candidato):

curl -s -o /dev/null -w "%{http_code} %{content_type}\n" "$GATEWAY/mcp/naoexiste"
  • 404 application/json → chegou no Worker. É este host.
  • 401 com WWW-Authenticate: Bearer realm="OAuth" → é o portal, não o Worker.
  • 404 text/plain → não chegou em Worker nenhum.

Nas duas superfícies, use o MESMO host: a rota /admin/* e a URL que a plataforma guarda em tenant_mcp_servers são o mesmo Worker.

Cadastrar:

O namespace já existe e o binding está declarado em wrangler.jsonc.

Cadastro pela rota admin (preferido)

PUT /admin/tenants/:tenantId/manifest, protegida por ADMIN_TOKEN (npx wrangler secret put ADMIN_TOKEN). Ela valida pelo mesmo parseManifest antes de gravar e grava as duas chaves de uma vez:

curl -X PUT $GATEWAY/admin/tenants/tnt_1/manifest \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "manifest": { ... }, "token": "<token-que-a-plataforma-vai-usar>" }'

Manifesto inválido é recusado com motivo nomeado e nada é gravado — diferente do painel, que aceita JSON quebrado calado e deixa a falha aparecer depois, como um agente respondendo sem o dado. GET devolve o manifesto sem a credencial da API do cliente; DELETE torna o tenant inalcançável.

Sem ADMIN_TOKEN configurado, /admin/* responde 503 — ausência de credencial nunca vira "aberto".

Cadastro pela CLI (emergência)

npx wrangler kv key put --binding=TENANT_MANIFESTS "tenant-manifest:tnt_1" --path manifesto.json
npx wrangler kv key put --binding=TENANT_MANIFESTS "tenant-token:<sha256-do-token>" "tnt_1"

Do lado da plataforma, a linha em tenant_mcp_servers aponta para $GATEWAY/mcp/tenant com Authorization: Bearer <token>, e o tool_policy classifica cada ferramenta com o MESMO escopo declarado aqui.

O que o manifesto obriga, e por quê

  • fields é obrigatório. Sem projeção, o JSON do cliente iria cru para o prompt — com margem, custo interno e dado de terceiro dentro. Campo não declarado não viaja.

  • Lista: use o caminho de repetição produtos[].nome. Ele projeta todos os itens (até 30, ou até o teto de caracteres), uma linha por item com os campos daquele item juntos — é o que permite uma ferramenta de BUSCA e o agente sugerir a partir do catálogo do cliente. Agrupar por campo em vez de por item deixaria o modelo parear preço com o produto errado, que é a pior saída possível deste módulo.

    "fields": [
      { "path": "produtos[].nome",  "label": "Produto" },
      { "path": "produtos[].preco", "label": "Preço" }
    ]

    Produto: Creatina · Preço: R$ 89

  • Ferramenta customer precisa declarar o identityParam entre os params. O backend recusa (identityParam_missing) a que não o declara no inputSchema; aceitar aqui seria anunciar uma ferramenta nunca chamável.

  • Ferramenta business não pode ter identityParam — a contradição é recusada, não resolvida em silêncio.

  • Manifesto inválido é recusado inteiro: meio manifesto aplicado é uma ferramenta que some sem ninguém notar.

Quando a API do cliente não fala a língua da plataforma

Quatro campos opcionais existem porque, sem eles, boa parte das APIs reais fica inexprimível — não por elegância.

  • transform no parâmetro ("digits" · "br_local"). A plataforma escreve no identityParam o wa_id da Meta: E.164 em dígitos, com DDI (5534999530186). ERP brasileiro guarda o telefone em formato local. Sem normalizar, a busca casa zero registros — e nada quebra: o leg degrada em empty_result e o dono conclui que "não funciona" sem nenhum sinal apontando a causa. br_local remove o DDI só quando o número tem 12–13 dígitos e começa em 55 (a janela é o que impede um número de DDD 55 de perder dois dígitos).

  • query — pares fixos, do autor do manifesto, fora do alcance do modelo: {"idBranch": "7", "active": "true", "take": "20"}. Pinar a unidade como parâmetro seria deixar o modelo escolher a unidade a partir do texto do cliente.

  • root — onde, na resposta, mora o que a projeção deve ler, em candidatos. ["list", "lista", "$"] desce num envelope de paginação; ["[0]", "$"] cobre objeto-ou-lista quando o swagger do cliente diz uma coisa e a API faz outra. "$" é a resposta inteira. Candidato que existe mas está vazio não conta como achado.

  • resolve — o salto identidade verificada → chave interna, feito pelo gateway antes da consulta principal:

    "resolve": {
      "path": "/api/v1/members/basic",
      "param": "phone",
      "query": { "take": "1" },
      "extract": ["[0].idMember", "idMember"],
      "into": "memberId"
    }

    Ele existe porque a plataforma só conhece o telefone (é o que ela verifica no ingresso do canal) e a maioria das APIs keya por um id interno. Sem o salto, metade das consultas escopadas por cliente é inexprimível.

    O ADR-0036 proíbe encadear ferramentas — mas o que ele proíbe é o laço de decisão da LLM (chamar → ler → decidir de novo). Daqui sai um tools/call; os dois saltos são determinísticos e dividem o mesmo timeoutMs. E a amarra fica mais forte: o limite conhecido nº 1 do ADR-0036 é que a plataforma prova o envio da identidade, não o respeito a ela — com o salto, a chave é derivada do telefone verificado pelo próprio gateway, e o modelo não tem como propô-la.

    Três propriedades não são configuráveis, de propósito: o valor enviado é sempre o identityParam (se houvesse campo para escolher outro, o modelo escolheria); into não pode colidir com parâmetro nem com query fixa; e resolução vazia aborta a ferramenta — a consulta principal não acontece, porque um /receivables sem memberId devolveria o financeiro da academia inteira.

O que este gateway assume como responsabilidade

Ao viver fora da plataforma, ele perde o INV-TENANT-SCOPE e o guard estrutural que o cobra lá. O que sobra é a regra em src/tenant/store.ts e os testes: o token resolve UM tenant, e as ferramentas daquela requisição saem do manifesto DAQUELE tenant. Não há caminho que leia manifesto de outro, e não há listagem.

Também moram aqui: o SSRF (https apenas, sem faixa privada/loopback/metadata, redirecionamento não seguido, corpo com teto de bytes), a credencial da API do cliente, e o cuidado de a mensagem de erro nunca ecoar o corpo do cliente — esse texto entra no prompt de um agente.

Limite conhecido: um host que resolve para IP privado passa pela guarda (não há resolução de DNS antes do fetch). Rebind de DNS exigiria proxy com resolvedor próprio; está fora de escopo, e é melhor estar escrito do que subentendido.

Preset: EVO (academias)

A EVO (W12) é o sistema de gestão que boa parte das academias usa. Ela é o oposto do caso que o manifesto por tenant atende: a API é fixa e conhecida, a mesma para todas as academias. Colar 250 linhas de JSON por academia produziria divergência entre clientes idênticos e nenhum lugar onde corrigir todo mundo quando a W12 mudar um campo.

src/tenant/presets/evo.ts é esse lugar. Ele gera um manifesto e o passa pelo mesmo parseManifest — não é um caminho de gravação paralelo, é um gerador de entrada para o mesmo. Ativar uma academia vira três campos.

Ativar

1 · No gateway (aqui):

curl -X PUT $GATEWAY/admin/tenants/tnt_gym/preset/evo \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "dns": "<dns-da-unidade-na-evo>",
        "secretKey": "<secret-key-da-evo>",
        "idBranch": 7,
        "label": "Academia Centro",
        "include": "business",
        "token": "<token-que-a-plataforma-vai-apresentar>"
      }'

Auth da EVO é Basic (DNS como usuário, secret key como senha), montado aqui — o esquema do manifesto não precisa saber o que é Basic. A resposta traz a tool_policy pronta e nunca a credencial.

include: só preço e catálogo, ou tudo

"include": "business" no corpo anuncia as cinco consultas sobre o negócio (preço, plano, grade, modalidades, unidade) e deixa de fora as quatro sobre o aluno. É a ativação que a maioria das academias vai querer primeiro, e ela tem uma consequência que não é cosmética: o telefone do cliente nunca sai do perímetro. Some com ele o dever de aviso do ADR-0036 — não porque foi dispensado, mas porque o fato que o originava deixa de acontecer.

Dava para chegar perto disso só pela tool_policy do lado da plataforma (consulta sem regra é inchamável, e o modelo nem a vê). Mas ela seguiria sendo anunciada e recusada a cada turno, somando em scope_refused/unclassified — a métrica que existe para gritar "o dono esqueceu de classificar" passaria a gritar num estado intencional. Alerta falso é o que ensina a ignorar alerta, então o desligamento acontece onde a consulta nasce.

Valor desconhecido é recusado, nunca lido como o default: um "buisness" com erro de digitação tratado como "all" ligaria as consultas sobre o aluno numa academia que pediu para não tê-las. Sem o campo, tudo é anunciado — o recorte é opt-in.

A tool_policy da resposta é derivada do manifesto que acabou de ser gravado, então ela acompanha o recorte sozinha e nunca descreve uma consulta que aquele tenant não anuncia.

2 · Na plataforma (celfons/whatsapp), cole a tool_policy que veio na resposta — POST, e a chave vai em x-admin-key:

curl -X POST https://<plataforma>/api/admin/tenants/tnt_gym/mcp-server \
  -H "x-admin-key: $ADMIN_API_KEY" -H "Content-Type: application/json" \
  -d '{ "label": "EVO",
        "url": "'$GATEWAY'/mcp/tenant",
        "authHeader": "Bearer <o-mesmo-token-acima>",
        "toolPolicy": { "tools": { ... } } }'

A resposta ecoa o que foi classificado (classifiedTools, allAnnounced) — confira que o número bate com o que você mandou.

Os dois documentos têm de concordar: uma ferramenta anunciada aqui e não classificada lá nasce inchamável (unclassified), e o dono só descobre pela métrica de degradação. Por isso a policy é gerada, não transcrita — e no dialeto da borda da plataforma (identityParam camelCase, sem version), não no do banco dela, senão precisaria de tradução manual e a promessa de "colável" seria falsa.

As ferramentas

Ferramenta Escopo Endpoint EVO Salto
evo_meu_cadastro customer GET /api/v1/members/basic
evo_meu_plano customer GET /api/v1/members/basic
evo_minhas_cobrancas customer GET /api/v1/receivables memberId
evo_meus_servicos customer GET /api/v1/members/services idMember
evo_minha_frequencia customer GET /api/v1/entries idMember
evo_minhas_aulas customer GET /api/v1/activities/schedule idMember
evo_planos_e_precos business GET /api/v3/membership
evo_servicos_e_precos business GET /api/v1/service

Com "include": "business", só as duas últimas são anunciadas.

evo_meu_plano não precisa de salto porque MembersBasicApiViewModel.memberships já vem embutido na busca por telefone — uma consulta, e é o que a mantém barata.

O preset usa /api/v1/members/basic, não /api/v2/members: o v2 devolve cpf, document, address, zipCode, birthDate e photoUrl. Projeção estreita protege o prompt, mas o que não sai da EVO não precisa de projeção — é uma camada de PII a menos atravessando o fio.

A regra: duas classes, e não há terceira

(a) fala do cliente que perguntou, ou (b) é plano e preço. É uma allowlist, e o que está fora não está fora por ser perigoso — grade de aulas, modalidades, endereço, horário e convênios são todos inofensivos. Estão fora porque allowlist que aceita "isso também é inofensivo" deixa de ser allowlist em três meses. E porque quase tudo dessa lista é quase estático: mora mais barato no contexto que o dono escreve no portal do agente, sem custar uma consulta viva e uma chamada de LLM por turno.

tests/evoPreset.test.ts codifica a regra: consulta business cujo caminho não está na lista de preço quebra o CI.

Por que não "todo GET"

A EVO tem ~80 rotas de leitura, e a tentação de expor todas é grande — leitura não estraga nada, certo? Não: o verbo não classifica nada. Quem aparece na resposta classifica. Três GETs da própria EVO bastam para mostrar:

  • GET /api/v1/members/resetPassword devolve link de redefinição de senha. É leitura em HTTP e entrega a conta de alguém.
  • GET /api/v1/receivables/debtors é a lista de inadimplentes da academia inteira, com nome.
  • GET /api/v1/pix/qr-code gera cobrança — efeito no mundo, verbo de leitura.

E há uma armadilha mais silenciosa: /api/v1/receivables, /api/v1/entries e /api/v1/members/services respondem pela academia inteira quando o filtro de aluno falta. São exatamente as que usam resolve, e é por isso que a resolução vazia aborta em vez de seguir sem a chave.

Rota nova passa por duas perguntas, nesta ordem:

  1. A resposta fala de uma pessoa amarrada ao telefone verificado, ou do negócio?
  2. Se for do negócio — o dono poria isso na vitrine?

Custo negociado com parceiro, estoque, centro de custo, conta bancária e base de clientes reprovam na segunda. tests/evoPreset.test.ts carrega o inventário fechado das rotas proibidas: acrescentar uma delas quebra o CI.

O que o preset deliberadamente não faz

Escrita. Matricular em aula (POST /activities/schedule/enroll), criar prospect, agendar experimental — todos existem na EVO e nenhum entra aqui. O ADR-0036 §2.3 contrata leitura, e é essa contratação que dispensa reserva de idempotência: a entrega do turno é at-least-once, então um turno reentregue repete a consulta. Repetir leitura custa tempo; repetir matrícula cria duas. Escrita é outra feature, e ela reabre o P-3.

Verificar na ativação (não dá para saber do código)

  1. Semântica do filtro phone em /members/basic: exato, parcial, ignora máscara? Se a academia guardar o número com DDI, o transform correto é digits — é uma linha em PHONE_TRANSFORM. É a suposição mais frágil do preset, e a que falha em silêncio: nenhuma consulta escopada acha ninguém, e nada fica vermelho. O sinal é empty_result perto de 100% para aquele tenant, logo depois da ativação.
  2. Latência p95 de /members/basic + /receivables: os dois saltos têm de caber nos 3400 ms do preset, que ficam abaixo do MCP_TIMEOUT_MS (4 s) da plataforma.
  3. Rate limit da API por token — não está documentado pela W12, e o agente consulta por turno.
  4. idBranch numa rede multi-unidade: sem ele, a EVO responde pelo escopo do token.

O que não deve passar por aqui

Preço de plano é a pergunta nº 1 de uma academia e a mais tentadora de pôr no MCP. Mas o ADR-0036 §2.4 proíbe cache entre turnos: evo_planos_e_precos seria chamado em todo turno, com uma chamada de LLM de seleção junto, para um dado que muda uma vez por semestre. Ele está aqui para quem quer o preço sempre vivo — mas contexto quase estático (tabela de preços, horário, modalidades) sai mais barato no portal do agente ou no RAG, que já são lastro e custam zero de latência. O MCP se paga no dado vivo e por pessoa: situação do contrato, cobrança em aberto, aula agendada, vaga de hoje.

Tools

Instagram (Graph API — Business/Creator accounts)

Tool What it does
instagram_get_profile Profile data (followers, bio, media count)
instagram_list_media Recent posts
instagram_get_media Details of a single post
instagram_get_media_insights Post metrics (reach, likes, saves, shares)
instagram_publish_post Publishes an image or reel (container + publish)
instagram_list_comments Comments on a post
instagram_reply_to_comment Replies to a comment

Facebook (Pages Graph API)

Tool What it does
facebook_list_pages Pages the token can manage
facebook_get_page Page details
facebook_list_posts Recent Page posts
facebook_create_post Publishes a text post (optionally with a link)
facebook_upload_photo Publishes a photo from a public URL
facebook_delete_post Deletes a post
facebook_get_post_insights Post metrics
facebook_list_comments Comments on a post
facebook_reply_to_comment Replies to a comment

X / Twitter (API v2)

Tool What it does
x_get_me Authenticated account
x_get_user Profile lookup by @username
x_list_user_tweets Recent posts from an account
x_get_tweet A single post with its metrics
x_search_recent Search posts from the last 7 days
x_post_tweet Publishes a post (reply/quote supported)
x_delete_tweet Deletes a post

WhatsApp (Cloud API)

Tool What it does
whatsapp_send_message Free-form text (24-hour window only)
whatsapp_send_template Approved template, with body parameters
whatsapp_send_media Image, video, audio or document from a URL
whatsapp_send_reaction Reacts to a message with an emoji
whatsapp_mark_as_read Marks a received message as read
whatsapp_get_business_profile Business profile of the sending number
whatsapp_list_templates Templates of a WABA, with approval status
whatsapp_get_media_url Download URL of received media

Google Business Profile

Tool What it does
google_business_list_accounts Manageable accounts
google_business_list_locations Locations of an account
google_business_get_location Address, hours, phone, categories
google_business_list_reviews Reviews with rating and existing replies
google_business_reply_to_review Replies to a review
google_business_create_post Publishes a local post, with optional CTA

YouTube (Data API v3)

Tool What it does
youtube_get_channel Channel by ID, handle, or the authenticated one
youtube_list_videos Recent videos of a channel
youtube_get_video Video with views, likes and comment count
youtube_search Searches videos, channels or playlists
youtube_list_comments Comment threads on a video
youtube_reply_to_comment Replies to a comment
youtube_update_video Title, description, tags, privacy

Google Ads

Tool What it does
google_ads_list_accounts Accessible accounts
google_ads_list_campaigns Campaigns with status, budget and metrics
google_ads_campaign_performance Daily performance of one campaign
google_ads_list_ad_groups Ad groups of the account or a campaign
google_ads_keyword_performance Keyword metrics, ordered by impressions
google_ads_run_query Arbitrary GAQL query
google_ads_update_campaign_status Pauses/enables/removes a campaign — changes live spend

Google Analytics (GA4)

Tool What it does
ga4_list_accounts Analytics accounts
ga4_list_properties GA4 properties of an account
ga4_report Report with the metrics/dimensions you choose
ga4_traffic_overview Users, sessions, engagement and conversions by channel
ga4_top_pages Most viewed pages
ga4_realtime Users active right now
ga4_list_metadata Metrics and dimensions available on the property

Credentials

Store them as Worker secrets — never in wrangler.jsonc:

npx wrangler secret put FACEBOOK_ACCESS_TOKEN     # Page token (Facebook, and Instagram fallback)
npx wrangler secret put INSTAGRAM_ACCESS_TOKEN    # optional; overrides the token above for Instagram
npx wrangler secret put X_BEARER_TOKEN            # X app-only token (reads)
npx wrangler secret put X_USER_ACCESS_TOKEN       # X user-context token (posting/deleting)

# WhatsApp Cloud API
npx wrangler secret put WHATSAPP_ACCESS_TOKEN     # optional; falls back to FACEBOOK_ACCESS_TOKEN
npx wrangler secret put WHATSAPP_PHONE_NUMBER_ID  # default sender number ID

# Google (shared by Business Profile, YouTube, Ads and Analytics)
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put GOOGLE_REFRESH_TOKEN

# Google, per-product extras
npx wrangler secret put YOUTUBE_API_KEY               # optional; used for read-only YouTube calls
npx wrangler secret put GOOGLE_ADS_DEVELOPER_TOKEN
npx wrangler secret put GOOGLE_ADS_LOGIN_CUSTOMER_ID  # optional, for MCC accounts
npx wrangler secret put GOOGLE_ADS_CUSTOMER_ID        # optional default account
npx wrangler secret put GA4_PROPERTY_ID               # optional default property

Google access tokens expire in about an hour, so the Worker mints them on demand from the refresh token and caches them in the isolate. A static GOOGLE_ACCESS_TOKEN is also accepted, which is convenient for local testing but will expire in production.

For local development, put the same keys in a .dev.vars file (git-ignored).

Required permissions:

  • Instagram: instagram_basic, instagram_content_publish, instagram_manage_comments
  • Facebook: pages_read_engagement, pages_manage_posts, pages_manage_engagement
  • X: tweet.read, users.read, and tweet.write for publishing
  • WhatsApp: whatsapp_business_messaging, whatsapp_business_management
  • Google Business Profile: business.manage
  • YouTube: youtube.force-ssl (writes); reads can use an API key
  • Google Ads: adwords, plus an approved developer token
  • GA4: analytics.readonly

The Graph API version defaults to v21.0 and can be changed with the GRAPH_API_VERSION var in wrangler.jsonc.

Any tool called without its credential returns an MCP error result explaining which secret is missing — it never leaks the token value.

Running

npm install
npm start        # http://localhost:5173 — built-in tool tester with an endpoint switcher
npm run build    # generates dist/ (Worker + client assets)
npm run deploy   # build + wrangler deploy

Deploying from Cloudflare Workers Builds

The Vite plugin is what fills in assets.directory — it writes the final Worker config to dist/mcp_social/wrangler.json at build time. So a bare npx wrangler deploy with no build first fails with:

✘ [ERROR] The `assets` property in your configuration is missing the required `directory` property.

In the Workers Builds settings for this project, set either:

  • Build command: npm run build (keeping the default deploy command npx wrangler deploy), or
  • Deploy command: npm run deploy (which builds and deploys in one step).

Connect an MCP client (Claude, MCP Inspector, …) to https://<your-worker>/mcp, or to one of the per-network endpoints.

Structure

src/
  server.ts            routing: one MCP server per endpoint
  social/
    shared.ts          HTTP helper, error handling, secret loading
    instagram.ts       instagram_* tools
    facebook.ts        facebook_* tools
    twitter.ts         x_* tools
    whatsapp.ts        whatsapp_* tools
    google-shared.ts   Google OAuth: refresh token -> cached access token
    google-business.ts google_business_* tools
    youtube.ts         youtube_* tools
    google-ads.ts      google_ads_* tools
    google-analytics.ts ga4_* tools
    env.d.ts           secret/var types
  tenant/
    manifest.ts        esquema do manifesto (a fronteira do gateway) + transforms
    store.ts           KV: token -> tenant -> manifesto (o isolamento mora aqui)
    safeUrl.ts         SSRF: destino verificado, sem redirect, corpo com teto
    project.ts         resposta do cliente -> só os campos declarados; raiz por candidatos
    gateway.ts         manifesto -> ferramentas MCP; o salto de resolução
    admin.ts           rotas de manifesto e de preset
    presets/evo.ts     EVO (academias): manifesto gerado + a tool_policy da plataforma
  client.tsx           browser tool tester
test/
  tenantGateway.test.ts  isolamento, manifesto, SSRF, projeção, execução
  tenantResolve.test.ts  salto de resolução, transform, raiz, query fixa
  tenantAdmin.test.ts    rotas admin de manifesto e de preset
  evoPreset.test.ts      o preset da EVO e o acordo com a tool_policy

Testes: npm test (vitest).

Adding a network

Create src/social/<network>.ts exporting a register<Network>Tools(server, env) function, then add it to the SERVERS map in src/server.ts. For a Google product, reuse googleGet/googlePost from google-shared.ts so it shares the token refresh.

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages