@zt-playground/mcp-server
v3.0.0
Published
MCP server for ZT Playground — a workflow layer over issues, cycles, GitHub, monitoring/APM and analytics, plus guarded database/SSH dev-ops tooling.
Maintainers
Readme
ZT Playground — MCP Server
Servidor MCP que dá a um agente de IA (Claude, Cursor, Codex, …) acesso ao ZT Playground: issues, cycles, GitHub, monitoramento/APM, analytics e saved views — além de ferramentas guardadas de banco de dados e SSH.
v2.1 — camada de fluxo (
my_work,issue_brief,start_work,submit_work,triage_inbox,standup), projeto padrão vindo do context (quase nada mais exigeprojectId), pessoas resolvidas por nome, e as tools de repositório local removidas por serem piores que o Read/Grep/Bash do próprio agente.v2 — reescrita com foco em segurança, cobertura completa da API e boas práticas de MCP (tool annotations, paginação, redação de segredos, audit log).
Arquitetura
O servidor fala com três backends distintos:
| Backend | Como | Para quê |
| --- | --- | --- |
| HTTP API (X-ZT-API-Key) | https://zt-playground-api.vercel.app | Domínio do produto: projetos, issues, cycles, monitor, github, analytics, saved views |
| Postgres (pg) | conexão direta configurada no context | Console SQL (leitura por padrão; escrita com confirmação) |
| SSH (ssh2) | servidores do context | Logs, status de serviço e comandos em máquinas remotas |
Os caminhos de Postgres/SSH ignoram a validação e auditoria da API — por isso são protegidos por sessões read-only, denylist de comandos, verificação de host-key e um audit log local (veja Segurança).
Repositórios locais não estão aqui de propósito. O agente que hospeda este servidor já lê, busca e roda comandos na árvore de trabalho com o modelo de permissão dele; duplicar isso por MCP só entregava uma versão pior (truncada, sem contexto) e ainda contornava a aprovação do host. Para código, use as ferramentas nativas do agente.
Instalação
Pré-requisitos
- Node.js v18+
- Uma API Key do ZT Playground (gere em Configurações → API Keys)
Setup automático (recomendado)
O assistente detecta seus clientes MCP instalados (Claude Desktop, Cursor, VS Code, Windsurf, Codex) e adiciona a configuração:
npx @zt-playground/mcp-server --setup --key SUA_API_KEYConfiguração manual
Adicione ao ~/.claude/settings.json (global) ou .mcp.json (por projeto):
{
"mcpServers": {
"zt-playground": {
"command": "npx",
"args": ["@zt-playground/mcp-server"],
"env": { "ZT_API_KEY": "SUA_API_KEY" }
}
}
}Ou, se for rodar a partir do código clonado:
npm install && npm run build
# aponte "args" para ["/caminho/para/zt-mcp-server/dist/index.js"] com "command": "node"Requisito de versão da API
As tools de fluxo dependem de campos que entraram na zt-playground-api junto com esta versão (GET /tasks/by-ref, filtro por responsável, status com lista, cycleId/storyPoints na criação). Contra uma API mais antiga o servidor continua funcionando: ele detecta a limitação, refaz a consulta de outro jeito e devolve note + appliedLocally dizendo o que foi filtrado no cliente. Assim que a API for reimplantada, os mesmos comandos passam a ser exatos e mais baratos.
Autenticação
Você pode autenticar de três formas (ordem de prioridade):
- Variável de ambiente
ZT_API_KEY(e opcionalZT_API_URL). - Ferramenta
auth_setupdentro de uma conversa (salva a chave em~/.zt-playground/auth.json,0600). --setup --key ...no CLI.
| Variável | Descrição | Obrigatório |
| --- | --- | --- |
| ZT_API_KEY | API Key do ZT Playground | Sim* |
| ZT_API_URL | URL da API (default: produção) | Não |
| ZT_MCP_TOOLSET | full (default) ou core | Não |
| ZT_MCP_HTTP_PORT | Sobe no transporte Streamable HTTP nessa porta (sem ela: stdio) | Não |
| ZT_MCP_HTTP_HOST | Interface do transporte HTTP (default 127.0.0.1) | Não |
| ZT_MCP_LIST_TTL_MS | TTL do cache de tools/list (default 3600000) | Não |
| ZT_MCP_STRUCTURED | both (default) ou structured — ver abaixo | Não |
| ZT_MCP_MONITOR_DSN | Liga a telemetria de tool calls (ver abaixo). Sem ela, nada é enviado | Não |
| ZT_MCP_MONITOR_TIMEOUT_MS | Teto de um envio de telemetria (default 2000) | Não |
| ZT_MCP_MONITOR_BATCH | Quantas chamadas o stdio junta antes de enviar (default 20) | Não |
* Não é obrigatório no boot — o servidor sobe sem chave e você pode autenticar via auth_setup.
Toolset core
tools/list é o único payload que toda sessão paga antes de fazer qualquer
coisa. Com as 94 tools ele custa ~17,6 k tokens; ZT_MCP_TOOLSET=core registra
as ~33 do dia a dia e cai para ~7,7 k — 56 % a menos, todo request.
O core cobre: as 6 tools de workflow (my_work, issue_brief, start_work,
submit_work, triage_inbox, standup), issues (create/get/update/search),
comentários, relações, ciclos, board, global_search, whoami, contexto,
monitor (overview/list/get/create-issue), GitHub (overview/PRs/detail/create) e
db_query/db_schema.
Fora do core ficam as tools de administração e de relatório — alertas,
web-vitals, labels, anexos, views salvas, SSH, analytics de issues, épicos.
Precisa de uma delas? ZT_MCP_TOOLSET=full (ou simplesmente não defina a
variável) devolve tudo.
{
"mcpServers": {
"zt-playground": {
"command": "npx",
"args": ["-y", "@zt-playground/mcp-server"],
"env": { "ZT_API_KEY": "SUA_API_KEY", "ZT_MCP_TOOLSET": "core" }
}
}
}verbosity nas leituras pesadas
monitor_get_error, monitor_get_error_events, github_get_pr_detail,
get_issue e search_issues aceitam verbosity: 'concise' | 'detailed'
(default concise). O modo conciso projeta a resposta — stack cortado nos 10
primeiros frames, sem rawStackTrace/breadcrumbs/extra, PR sem a lista de
arquivos — e diz o que ficou de fora. Medido contra dados reais: −93 % em
monitor_get_error, −52 % em search_issues. Peça detailed quando a resposta
curta não bastar.
Transporte Streamable HTTP
O default continua sendo stdio e não mudou. Com --http (ou
ZT_MCP_HTTP_PORT) o mesmo servidor sobe em Streamable HTTP:
zt-playground-mcp --http 3005 # ou: ZT_MCP_HTTP_PORT=3005 zt-playground-mcp- MCP em
POST /mcp;GET /healthresponde{"ok":true}. - A chave vem do request, não do disco. Mande
X-ZT-API-Key(ouAuthorization: Bearer …) em cada chamada; sem ela é401. O~/.zt-playground/auth.jsoncontinua sendo o caminho do stdio e não é lido nem escrito aqui —auth_setuprecusa por HTTP. - Sem sessão: um servidor novo por request, nada guardado entre eles. É o que a revisão 2026-07-28 pede e o que torna isso publicável como função serverless — não existe sessão para prender um chamador a uma instância.
- Um processo atende várias chaves ao mesmo tempo, cada uma com a sua
identidade (
assignee: "me"resolve por chamador).
curl -s http://127.0.0.1:3005/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'X-ZT-API-Key: SUA_API_KEY' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'OAuth (o fluxo de autorização da spec nova) não está implementado — a chave por header é a única autenticação do transporte HTTP.
Cache de tools/list e outputSchema
Num cliente que fala 2026-07-28, o resultado de tools/list sai com
ttlMs (1 h por default) e cacheScope: "public": o catálogo só muda quando o
build ou o ZT_MCP_TOOLSET mudam, então não há motivo para reenviá-lo a cada
reconexão. Requests 2025 recebem exatamente o que recebiam antes.
As nove tools de resposta estável (as 6 de workflow + get_issue,
search_issues, get_board) declaram outputSchema e respondem também em
structuredContent — JSON nativo, em vez de JSON escapado dentro de um bloco de
texto. O default (both) manda os dois, que é o que a spec pede para clientes
que só leem content. Se todos os seus clientes leem structuredContent,
ZT_MCP_STRUCTURED=structured derruba a cópia em texto e a resposta fica menor
do que era antes de existir schema.
Telemetria de tool calls (opt-in)
ZT_MCP_MONITOR_DSN liga o envio de cada chamada de tool para um projeto do ZT
Monitor, pelo mesmo ingest que o SDK do browser usa:
ZT_MCP_MONITOR_DSN=https://<publicKey>@<host>/monitor/ingest/<projectId>Cada chamada vira uma transação CUSTOM chamada mcp.tool/<nome>, com duração,
bytes da resposta, sucesso/erro, toolset, transporte, a verbosity pedida e o
projeto. Erro inesperado (5xx da API ou exceção não prevista) vira também um
evento de erro; um 404 não — ele é o chamador errando, não o servidor.
Três garantias:
- Sem a variável, nada acontece. Nenhuma alocação, nenhuma resolução de host, nenhum caminho novo.
- Nenhuma falha ou lentidão de telemetria atrasa ou quebra uma tool. Medido: overhead p50 abaixo do ruído (−0,006 ms em 3.000 chamadas), p95 +0,13 ms. Com DSN morto, ou com um ingest que trava 3 s, a tool responde igual e na mesma hora.
- Nada de
setIntervalcomo caminho principal. O envio começa dentro da chamada que o gerou. Sob HTTP ele vai para owaitUntilda plataforma quando existe, e senão ganha uma janela curta antes de a resposta sair — porque uma função serverless congela e um flush por timer nunca aconteceria. No stdio, onde o processo vive de verdade, as amostras esperam companhia (ZT_MCP_MONITOR_BATCH).
Aponte para um projeto próprio, ou leia os painéis com
?platform=mcp-server.getPerformanceOverviewnão filtra por tipo, então sem isso a latência de umstandupentra no p95 das suas páginas web.
Fluxo de trabalho
Seis ferramentas respondem, cada uma em uma chamada, as perguntas com que um dia de trabalho começa e termina. Elas não acrescentam poder novo — compõem os mesmos endpoints, evitando que o agente monte a sequência errada.
| Ferramenta | Responde |
| --- | --- |
| my_work | O que eu pego agora — suas issues abertas por status, ordenadas por prioridade, com bloqueios sinalizados, o cycle ativo e o backlog sem dono |
| issue_brief | Tudo sobre a ZT-42 — descrição, cycle, épico/sub-issues, relações com bloqueios em aberto, comentários recentes, repositório e nome de branch, atividade no git, issues vizinhas da mesma área |
| start_work | Assume a issue: recusa se ainda houver bloqueio aberto, atribui a você, move para IN_PROGRESS e cria a branch no repositório certo |
| submit_work | Abre o PR ligado à issue (Closes ZT-N → IN_REVIEW agora, DONE no merge) e comenta seu resumo na issue |
| triage_inbox | O que espera decisão — issues sem dono por prioridade e erros do monitor ainda não virados em issue, cada linha com a chamada que resolve |
| standup | O que andou e o que está em risco — entregue, em andamento por pessoa, bloqueado, bugs novos e o cycle com dias restantes |
O repositório de cada issue é inferido do campo repository dela contra os repos ligados ao projeto; quando é ambíguo, a ferramenta diz por quê e pede repoId.
Catálogo de ferramentas (94)
Todas as ferramentas trazem annotations (readOnlyHint/destructiveHint/idempotentHint) para o cliente exibir o risco, retornam structuredContent e têm o output limitado em tamanho.
Projeto padrão:
projectIdé opcional em todas as ferramentas de produto. Sem ele, vale o projeto do context ativo — e, se a workspace tiver um projeto só, esse. Passe explicitamente apenas para sair do padrão.ZT-refs: onde se recebe uma issue, vale o id cru,
ZT-42ou42.Pessoas:
assigneeaceita nome ("Vinicius"), e-mail, id ou"me"— e"none"para trabalho sem dono.
Fluxo — my_work, issue_brief, start_work, submit_work, triage_inbox, standup
Auth & workspace — auth_setup, auth_status, whoami, list_users, get_dashboard, global_search
Projetos — list_projects, get_platforms
Issues — create_issue (com cycleId/storyPoints), get_issue, update_issue, search_issues (filtros, ordenação e paginação), delete_issue, report_bug, create_epic
Comentários / anexos / labels / relações — add_comment, get_comments, add_attachment, list_attachments, manage_labels, link_issues, unlink_issues, get_relations
Cycles (rotas em /sprints, modelo renomeado para Cycle) — list_cycles, create_cycle, get_cycle, update_cycle, complete_cycle, delete_cycle, assign_issues_to_cycle, remove_issue_from_cycle, get_cycle_metrics (burndown)
Saved views — view_list, view_create, view_get, view_update, view_delete, view_set_default
Atividade / board — get_project_activity, get_issue_activity, get_board
Issue analytics — get_project_health, get_issue_overview, get_issues_by_platform, get_issue_trends, get_area_analysis, get_repository_report
Monitor / APM — monitor_overview, monitor_list_errors, monitor_get_error, monitor_get_error_events, monitor_update_error, monitor_create_issue_from_error, monitor_performance, monitor_web_vitals, monitor_endpoints, monitor_transactions, monitor_error_platforms, monitor_list_alerts, monitor_create_alert, monitor_update_alert, monitor_delete_alert
GitHub — github_overview, github_list_repos, github_list_branches, github_list_commits, github_list_deployments, github_issue_git_activity, github_list_prs, github_get_pr_detail, github_get_pr_checks, github_create_branch, github_create_pr, github_merge_pr (gated por CI), github_pr_review, github_sync_repo
Context (infra local) — context_setup, context_get, context_list, context_set_active, context_remove
Banco de dados — db_query (read-only), db_execute → db_execute_confirm (write com confirmação), db_schema
SSH — ssh_exec, ssh_logs, ssh_status
Resources (contexto passivo por URI)
project://{id}·issue://{id}cycle://current/{projectId}— o cycle ATIVO + burndownhealth://{projectId}— saúde de issues + erros do monitor + p95monitor://{projectId}/overview
Prompts (workflows reutilizáveis)
triage-error, daily-standup, cycle-review, release-notes, incident-review
Configurando o context (projeto padrão / DB / SSH)
O context define o projeto padrão de todas as ferramentas e os alvos de banco/SSH. Crie com context_setup:
{
"projectId": "<id-do-projeto>",
"projectName": "sinapsys",
"databases": [
{ "name": "neon", "connectionString": "postgres://...", "readOnly": true }
],
"ssh": [
{ "name": "prod", "host": "1.2.3.4", "user": "deploy", "keyPath": "~/.ssh/id_ed25519", "readOnly": true }
]
}O context é salvo em ~/.zt-playground/contexts/*.json (0600). O context ativo persiste entre sessões (~/.zt-playground/active.json) e, havendo apenas um, é selecionado automaticamente.
Segurança
db_queryé read-only de verdade: roda dentro deBEGIN TRANSACTION READ ONLYcomstatement_timeout, então escritas são rejeitadas pelo banco mesmo via multi-statement. Escritas exigemdb_execute→db_execute_confirm(em transação, com rollback no erro) e DDL destrutivo é sinalizado.- Sem shell injection:
ssh_logsfaz escape dos argumentos antes de montar o comando remoto. - Denylist de comandos:
ssh_execbloqueia padrões catastróficos (rm -rf, force-push,curl | sh, fork bomb, shutdown, …) salvoallowDangerous: true. Servidores podem ser marcadosreadOnly. - Sem execução de código local: as tools de repositório local foram removidas na v2.1 — nenhum caminho deste servidor roda comando na sua máquina.
- Verificação de host-key SSH: trust-on-first-use com detecção de mudança de chave (MITM); pino opcional via
pinnedHostKey. github_merge_prexige CI verde (a menos deforce: true) e registra a operação.- Segredos redigidos:
context_getmascara senhas de DB e SSH. - Audit log: SQL de escrita,
ssh_exec, merges e reviews são anexados em~/.zt-playground/audit.log(0600). - Timeout/caps: todas as chamadas HTTP têm timeout; outputs grandes (stack traces, diffs, dumps) são truncados com marcador explícito.
Desenvolvimento
npm run dev # roda via tsx (sem build)
npm run build # compila para dist/
npm test # build + testes dos helpers puros (node --test, sem dependências)
npm run setup # assistente de configuração (precisa de build)Publicando no npm
O token do npm não fica no ~/.npmrc. Ele mora em /etc/zt-secrets/npm-token, root:root 0600 — nenhum processo seu (nem um agente rodando na sua sessão) lê aquele arquivo sem a senha do PC.
Instalação, uma vez só:
sudo ./scripts/install-zt-publish.sh # pede o token, cola sem ecoarA partir daí:
zt-publish /caminho/do/pacote # pede a senha do PC e publica
zt-publish . --dry-run # valida o tarball sem enviar
zt-publish . --tag beta # publica fora do latest
zt-publish --check-vault # confere cofre e token, sem publicar nadaVale para qualquer pacote do ZT, não só este — aponte o diretório.
Por que assim. O portão é uma linha: o sudo -k cat do token. O -k invalida o cache do sudo, então toda publicação pede a senha, não só a primeira dos últimos 15 minutos. O zt-publish é instalado root-owned de propósito: se fosse gravável por você, qualquer processo local poderia reescrevê-lo e você digitaria a senha para um script trocado. O único passo privilegiado é ler o token — o resto roda sem privilégio.
O token nunca entra no ~/.npmrc, nunca aparece em argv (logo não vaza no ps nem no histórico), nunca é impresso, e o npmrc temporário nasce em diretório 0700 e é apagado por trap mesmo se o publish falhar.
Antes de enviar, o comando recusa árvore git com alterações não commitadas, versão que já existe no registry, e build/test vermelhos. --skip-checks ignora esses trilhos.
O que isso não protege: qualquer coisa rodando como root, e você digitando a senha num prompt que não conferiu. Os dois scripts em scripts/ são curtos de propósito — leia antes de instalar.
Compatibilidade
Funciona com Claude Code/Desktop, Cursor, VS Code, Windsurf e OpenAI Codex (o --setup configura todos os detectados). O transporte default é stdio; logs vão para stderr. Streamable HTTP é opcional (--http) — ver acima.
Construído sobre @modelcontextprotocol/server 2.x, que atende as revisões
2025-11-25 e 2026-07-28 com o mesmo registro de tools: um cliente antigo
não vê diferença nenhuma, e um cliente novo ganha as dicas de cache e o
structuredContent. Requer Node 20+.
