Saltar para o conteúdo principal

Servidor MCP Aedifica — Etapa 1

Escolha de design D: acesso remoto às operações Aedifica por MCP, com a chave de API L1 ou access token OAuth da Etapa 1. O endpoint público está disponível; a ligação autenticada e o conector ChatGPT exigem validação própria.

Observado no Plancraft: ligações MCP e consentimento OAuth (ui-map.md, abertura e §18; tech-stack.md, IA). As chaves e integrações estão descritas em ui-map.md §15. O transporte, os nomes de ferramentas, os âmbitos read/write e o fluxo de confirmação abaixo são escolhas Aedifica, não contratos recuperados do Plancraft. PRD.md §6 descreve o desenho da API; para o contrato atualmente implementado prevalecem o código de functions/src/api/ e esta documentação.

Endpoint e transporte​

Endpoint público de produção (disponibilidade verificada em 10-11-2026 sem credenciais):

https://api.aedifica.audaces-scale.com/mcp

Node.js 22, @modelcontextprotocol/sdk, Streamable HTTP sem estado, com respostas JSON. Requer protocolo MCP 2025-06-18 ou posterior suportado pelo servidor, negociado em initialize. Não usa o transporte HTTP+SSE antigo nem mantém sessões ou fluxos SSE. Cada pedido usa autenticação própria; o cliente não deve depender de Mcp-Session-Id.

O cliente envia Content-Type: application/json e Accept: application/json, text/event-stream; após inicialização envia também MCP-Protocol-Version. Uma notificação aceite recebe 202 sem corpo. GET não abre um fluxo: devolve 405. O SDK também pode negociar versões anteriores por compatibilidade; os clientes deste contrato devem pedir 2025-06-18 ou posterior. Estes comportamentos seguem o transporte MCP 2025-06-18; a opção sem estado com respostas JSON é uma escolha de implementação com o SDK TypeScript oficial.

Criar e guardar a chave​

Na app: Definições → API e webhooks, rota /app/:ws/settings/api. Um proprietário ou administrador cria uma chave com nome identificável. A predefinição é read; ative escrita apenas se o assistente precisar de criar ou alterar dados. Para o endpoint real selecione o ambiente de produção: chaves de teste só funcionam com emulador.

Guarde o segredo mostrado uma única vez num gestor de segredos e injete-o no processo cliente como AEDIFICA_API_KEY. Nunca o coloque no chat, URL, ficheiro TOML, repositório ou capturas. O servidor recebe Authorization: Bearer <chave-ou-access-token>; não aceita a sessão ou o ID token Firebase como substituto. Rotação e revogação usam a gestão de chaves existente. Consulte Autenticação para armazenamento por hash, revalidação do criador e limitações atuais: não existe controlo de expiração configurável na UI.

read autoriza consultas; write autoriza mutações, incluindo pedidos pendentes. Uma chave só com write não recebe implicitamente read. A organização deriva da chave validada, nunca de um orgId fornecido pelo assistente.

Ligar ao Codex​

No ficheiro de configuração do Codex, após disponibilizar a variável no ambiente do processo que estabelece a ligação:

[mcp_servers.aedifica]
url = "https://api.aedifica.audaces-scale.com/mcp"
bearer_token_env_var = "AEDIFICA_API_KEY"

O valor da variável é apenas a chave, sem o prefixo Bearer . O Codex compõe o cabeçalho Authorization. A documentação oficial de MCP no Codex confirma esta configuração. Reinicie a ligação depois de alterar o ambiente. Uma configuração válida não demonstra uma ligação autenticada bem-sucedida.

ChatGPT: OAuth implementado; registo e teste reais pendentes​

O lote D adiou OAuth e stdio. A Etapa 1 implementa ambos como escolha de design Aedifica. O conector ChatGPT usa OAuth remoto; o Codex pode usar chave no HTTP ou stdio. Não há conector registado nem ligação testada na conta ChatGPT nesta entrega. A disponibilidade e as opções de configuração dependem da conta do Walter.

Configuração de servidor em functions/.env.example (placeholders):

VariávelUso
OAUTH_ISSUERIdentificador HTTPS público do authorization server, preferencialmente na raiz de um domínio
OAUTH_PUBLIC_BASE_URLBase pública dos endpoints e metadados; por omissão, issuer
OAUTH_RESOURCE_URLURL exata do recurso MCP público (audiência comum da API/MCP desta instalação)
OAUTH_CLIENT_IDCliente público pré-registado, sem client secret
OAUTH_REDIRECT_URISRedirects exatos autorizados, separados por vírgulas

O proxy público deve encaminhar /oauth/* e /.well-known/* para a função oauth (ou para api, que também os serve), e o recurso MCP para mcp. A disponibilidade pública de REST e MCP não comprova a configuração OAuth; esta deve ser validada separadamente. Não se deriva o issuer do cabeçalho Host. HTTP só é permitido para loopback no emulador; redirects loopback são aceites para clientes locais.

Para configurar o conector, Walter precisa de indicar a URL pública MCP (OAUTH_RESOURCE_URL), escolher OAuth, e usar o client id configurado. Os endpoints são <base>/oauth/authorize, <base>/oauth/token e <base>/oauth/revoke. Quando tiver o redirect apresentado pela OpenAI, deve acrescentá-lo à allowlist exata do servidor; não assumir um redirect nesta documentação. Se o conector exigir client secret ou outro modo de registo, será necessário validar essa configuração antes de afirmar compatibilidade. Não há registo dinâmico, CIMD ou client secret nesta etapa.

Descoberta: <base>/.well-known/oauth-authorization-server publica issuer, authorize/token/revoke, scopes read/write, authorization_code, PKCE S256 e autenticação de cliente none. <base>/.well-known/oauth-protected-resource publica recurso e authorization server. Um 401 do MCP/API inclui WWW-Authenticate com resource_metadata quando configurado. Ver autorização MCP e autenticação OpenAI.

Fluxo HTTP mínimo e consentimento​

  1. Cliente gera verifier PKCE (43–128 caracteres), challenge SHA-256 base64url e state imprevisível. Abre GET /oauth/authorize com response_type=code, client_id, redirect_uri, scope=read write (ou um só scope), state, code_challenge, code_challenge_method=S256 e, preferencialmente, resource igual à URL MCP. Client/redirect desconhecidos e PKCE em falta são recusados sem redirecionar para URI não validado.
  2. O HTML mínimo apresenta cliente, scopes e consentimento explícito. Nesta etapa, o utilizador obtém o ID token na sua sessão Firebase Aedifica e introduz-o no campo protegido. Não existe ecrã de login integrado nesta página. Para automação do teste, POST /oauth/authorize recebe os mesmos parâmetros mais id_token e decision=approve ou deny (JSON ou form-urlencoded). O Admin SDK valida ID token e revogação; não há bypass de autenticação. No demo, o ID token vem do Firebase Auth emulator. Nunca passar ID token na query, nem guardar em logs.
  3. O servidor determina a org pelo perfil autenticado e exige owner/admin na membership atual; ignora qualquer tentativa de escolher org via cliente. A recusa redireciona com error=access_denied; aprovação devolve 303 com code, state e issuer. O cliente deve validar state/issuer.
  4. POST /oauth/token recebe grant_type=authorization_code, client_id, redirect_uri, code, code_verifier e o mesmo resource, se enviado. Code dura 120 segundos, só é consumido uma vez em transação; troca errada não emite token. Access token opaco aed_atk_… dura 3600 segundos, sem refresh nesta etapa. Renovação exige novo fluxo de consentimento.
  5. Usar access token como Bearer em /v1 e MCP. O scope write não implica read. Todos os pedidos revalidam hash, expiração, issuer/recurso/cliente, membership e limite partilhado de 60/minuto por credencial. Escritas revalidam revogação dentro da transação.
  6. POST /oauth/revoke com client_id e token revoga o access token; pedidos seguintes devolvem 401. Token desconhecido também recebe 200, sem revelar existência.

Persistência apenas servidor: oauthCodes/{sha256(code)}, oauthTokenLookup/{prefix} e organizations/{org}/oauthTokens/{id}. Só hashes de code/access token são guardados, nunca verifier/ID token. O registo guarda org, criador, scopes, cliente, issuer, recurso, validade e revogação. A limpeza física dos registos expirados não está automatizada nesta etapa; a expiração é validada em cada utilização. É obrigatório desativar/redigir query strings e corpos nos logs do proxy para authorize/token/revoke; o código da aplicação não os regista.

Operações e limites​

O catálogo completo tem 42 ferramentas; tools/list publica os schemas de entrada e filtra a lista pelos âmbitos da chave. O registo está em functions/src/mcp/tools.ts:

ÁreaFerramentas
Projetoslist_projects, get_project, create_project, update_project
Extraslist_extras, get_extra, create_extra
Mensagenslist_messages, get_message, create_message
Contactoslist_contacts, get_contact, create_contact, update_contact
Catálogolist_catalog_items, get_catalog_item, create_catalog_item, update_catalog_item, search_catalog
Documentoslist_documents, get_document, create_document_draft, update_document_draft, update_draft_positions
Orçamentoslist_quotes, get_quote, create_quote_draft
Faturaslist_invoices, get_invoice, create_invoice_draft
PDFrender_document_pdf_link
Planeamentolist_assignments, create_assignment, update_assignment
Horaslist_time_entries, create_time_entry, request_approve_time
Pesquisa e contasearch_global, whoami, get_org
Decisão humanarequest_finalize_document, request_send_document

Listas recebem {limit,cursor?} (omissão 25); pesquisas recebem também query e o catálogo pode receber type (materials, services ou equipment). Continuar enquanto existir nextCursor, mesmo que uma página filtrada esteja vazia. Os aliases de orçamento e fatura restringem o tipo de documento.

Criações recebem {data,idempotencyKey}. Alterações recebem {id,version,patch}: a versão tem de corresponder à atual e os campos permitidos estão no schema. Repetir uma alteração com a versão anterior gera conflito, sem uma segunda mutação; o cliente deve reler e rever o estado. As posições são substituídas pelo editor em patch.editor, com recálculo dos totais; não existe uma ferramenta genérica para comandos arbitrários do editor. Pedidos de confirmação recebem {entityId,idempotencyKey}. Consultas individuais e PDF recebem {id}; whoami e get_org recebem {}. Os resultados de domínio são devolvidos em conteúdo MCP de texto JSON; falhas de ferramentas usam isError:true.

As mutações normais limitam-se aos recursos autorizados e aos rascunhos. Campos não suportados e referências a outras organizações são rejeitados. A obtenção de um PDF não finaliza nem envia o documento: gera um artefacto e uma ligação privada temporária. Por esse efeito, as anotações readOnlyHint e idempotentHint do PDF são falsas, embora exija apenas read. As atualizações recebem destructiveHint:true porque podem substituir campos ou posições; as criações e os pedidos pendentes são aditivos. A aprovação de horas é um pedido de confirmação, sem aprovação automática pelo assistente.

RegraContrato
Limite de pedidos60/minuto/chave, partilhado com a API REST
Corpo HTTPAté 1 MB
Listaslimit entre 1 e 100; cursor opaco ligado à organização e ao recurso
AutenticaçãoRevalidada em cada pedido; uma ligação aberta não preserva acesso após revogação
Repetição de criações/pedidosidempotencyKey explícita; reutilizar a mesma na repetição da mesma operação
Concorrência de alteraçõesversion obrigatória; uma versão desatualizada gera conflito
ErrosSem segredos, dados de autenticação ou stack trace nas respostas

O cursor não é uma credencial. Não o construa nem o reutilize noutro recurso. Os âmbitos e limites são impostos no servidor; anotações MCP e confirmações do cliente não os substituem.

Envio e finalização exigem decisão na app​

request_finalize_document e request_send_document criam apenas pedidos pendentes. A resposta inclui a ligação para confirmação na app. Uma pessoa autorizada abre essa ligação, revê o documento e regista a decisão. A decisão fica auditada; mesmo confirmed não emite numeração, não publica, não envia mensagens nem executa automaticamente a ação. A pessoa prossegue pelo fluxo existente da app.

O assistente não pode confirmar em nome da pessoa nem usar um texto no chat como autorização do servidor. Repetir o mesmo pedido com a mesma chave de idempotência não deve criar outro pedido pendente. A recusa ou confirmação não pode ser substituída por uma decisão posterior.

Validação e disponibilização​

Comandos locais de verificação, executados pelo implementador/orquestrador; esta lista não afirma resultados:

pnpm build:functions
pnpm exec vitest run --config functions/vitest.config.ts
pnpm build
pnpm typecheck
pnpm test

Os testes com Firestore devem correr exclusivamente num projeto <projeto-local-de-testes> com FIRESTORE_EMULATOR_HOST definido, usando functions/vitest.emulator.config.ts. Validar inicialização, tools/list, leitura, escrita em rascunho, isolamento entre organizações, chave revogada, limite/payload, paginação e ausência de envio/finalização nos pedidos pendentes. Capturas da UI usam dados fictícios.

Pendente de validação: teste remoto com chave própria. A função onRequest mantém o invoker por omissão; não declara acesso público no código. Uma chave Aedifica não ultrapassa um bloqueio IAM anterior à função. Não há alteração IAM, deploy ou publicação autorizados por esta página. OAuth está implementado na Etapa 1; a configuração e validação do conector ChatGPT continuam pendentes.

English — connection and safety contract​

This is an Aedifica design, not a recovered Plancraft API contract. The production endpoint is https://api.aedifica.audaces-scale.com/mcp; an authenticated remote smoke test remains a separate validation step. Node.js 22 runs the official TypeScript SDK with stateless Streamable HTTP JSON responses and MCP 2025-06-18 or a later supported version.

Create a key under Settings → API and webhooks (/app/:ws/settings/api). Read is the default; enable write only when required. Keep the key in the client process environment as AEDIFICA_API_KEY. The Codex TOML example above contains no credential. Keys are checked on every request and scoped to their organization. Limits are shared with REST: 60 requests/minute/key, 1 MB request body, list sizes 1–100 and opaque organization/resource-scoped cursors.

ChatGPT cannot supply a custom API key through its remote MCP connection; adding a URL and a custom Authorization header in Settings is not a supported setup for this implementation. OAuth is implemented in Etapa 1; real connector configuration and testing remain pending. The official sources linked above establish the distinction between Codex bearer-token configuration and ChatGPT authentication.

Tools can read data and perform authorized draft operations. request_finalize_document, request_send_document and time approval requests only record pending requests. A person must review and record the decision in the app. Recording confirmation never finalizes, numbers, sends or approves automatically; the person continues through the existing app flow. A public endpoint does not demonstrate a successful authenticated integration or ChatGPT connection.

Codex via stdio (Etapa 1)​

O stdio local usa o SDK oficial e os mesmos serviços/ferramentas do HTTP. Não faz pedidos HTTP de retorno. Cada tools/list e tools/call volta a autenticar AEDIFICA_API_KEY, partilhando revogação, scopes e limite por credencial com a API/MCP HTTP. Não guarda principal em sessão. O stdout contém apenas protocolo MCP; diagnósticos de arranque vão para stderr sem detalhes sensíveis.

Compilar uma vez no checkout:

pnpm --filter @replica/functions build:mcp:stdio

O script pnpm --filter @replica/functions mcp:stdio arranca o ficheiro compilado. Para o Codex, usar diretamente Node, evitando mensagens do gestor de pacotes no stdout:

[mcp_servers.aedifica]
command = "node"
args = ["/workspace/plancraft-rea/functions/lib/mcp-stdio.cjs"]
env_vars = ["AEDIFICA_API_KEY", "GCLOUD_PROJECT", "FIREBASE_CONFIG", "FIRESTORE_EMULATOR_HOST", "FIREBASE_AUTH_EMULATOR_HOST", "FIREBASE_STORAGE_EMULATOR_HOST"]

Definir AEDIFICA_API_KEY fora do TOML e fora do git. A execução local sobre emuladores exige GCLOUD_PROJECT=<projeto-local-de-testes>, FIRESTORE_EMULATOR_HOST=<host-local>:<porta> e a configuração Firebase demo, como nos testes. As chaves aed_test_ continuam recusadas sem Firestore emulator. A execução em processo usa o Firebase Admin SDK: fora do emulador requer o ambiente de execução e as permissões de servidor apropriadas. Não é um pacote stdio publicado nem um cliente REST remoto; para acesso remoto com chave, usar o transporte HTTP documentado acima.

O adaptador é testado com cliente SDK e transporte em memória: tools/list, tools/call whoami, scopes, revalidação após revogação e erro sem detalhes sensíveis, sem rede externa. O conector ChatGPT usa o fluxo OAuth remoto; este comando destina-se ao Codex/local.

Extras e mensagens​

Ver extras e mensagens. GET lista/por id e POST idempotente usam coleções L1 próprias extras e projectMessages na organização. A UI existente continua a ler projectExtras/default; não há sincronização nem segunda fonte de leitura na UI nesta etapa. Não há portal de aceitar/pagar, emissão fiscal ou envio externo.