Skip to main content
Glama
README.md
# Task CLI + MCP

Aplicação de linha de comando para gerenciamento de tarefas, estendida com um
servidor **MCP (Model Context Protocol)** que expõe operações da aplicação para
clientes de IA através de tools estruturadas, evitando chamadas de shell e
parsing manual de saída.

Este projeto foi desenvolvido como um exercício de arquitetura, integração com
IA e desenvolvimento orientado por especificação. O foco de engenharia está na
**separação entre domínio e interfaces**: a lógica de negócio existe uma única
vez e é consumida tanto pela CLI quanto pela camada MCP.

- Especificação do produto (fonte de verdade): [`docs/spec.md`](docs/spec.md)
- Documento de arquitetura: [`docs/architecture.md`](docs/architecture.md)
- Servidor MCP (instalação, registro, tools): [`mcp/README.md`](mcp/README.md)

---

## Overview

O projeto começou como uma CLI de um comando, com três operações e persistência
em arquivo:

- **adicionar tarefas** — cria uma tarefa pendente, atribui um id inteiro
  incremental (nunca reutilizado) e confirma o id;
- **listar tarefas** — mostra id, status (pendente/concluída) e descrição;
  informa explicitamente quando não há tarefas;
- **concluir tarefas** — marca uma tarefa pendente como concluída; se já estiver
  concluída, apenas informa e encerra normalmente.

A persistência é local, em um `tasks.json` na raiz do projeto, resolvido
relativamente à raiz do repositório (independe do diretório de trabalho). A
ausência do arquivo equivale a uma lista vazia; a primeira gravação o cria. Um
`tasks.json` com JSON inválido faz qualquer comando falhar com mensagem clara e
sem sobrescrever o arquivo.

Numa segunda fase o repositório ganhou uma **camada MCP**. Ela não é um produto
novo: reusa a mesma camada de domínio e o mesmo armazenamento, **sem duplicar
regras**. O núcleo (`src/`) não mudou para a extensão existir e continua sem
dependências externas.

---

## Architecture

### `src/` — núcleo, sem dependências externas

| Arquivo | Responsabilidade |
|---|---|
| `src/task-service.js` | **Regras de negócio.** Domínio puro sobre `state = { tasks, nextId }`: cria, lista e conclui tarefas devolvendo sempre um **novo estado imutável**. Não toca filesystem, `console` nem `process`. |
| `src/store.js` | **Armazenamento.** Única parte que conhece o formato do arquivo e toca o filesystem. `ENOENT` = estado vazio (sem criar arquivo); JSON inválido = erro identificável (`CORRUPT_STORAGE`). |
| `src/cli.js`, `src/index.js` | **Interface CLI.** Parsing de `argv`, validação de formato, formatação da saída e exit codes. `index.js` é o shim de processo; `cli.js` orquestra store + domínio. |

### `mcp/` — camada adaptadora

| Arquivo | Responsabilidade |
|---|---|
| `mcp/server.js` | **Servidor MCP.** Fiação de protocolo: registra os handlers `tools/list` e `tools/call`, despacha por nome através de um `Map` de tools. Nenhuma regra de negócio. |
| `mcp/tools/*.js` | **Tools.** Adaptadores finos: validam o formato do argumento na borda (quando há argumento) e delegam a `src/task-service.js` + `src/store.js`. |
| `mcp/storage-path.js` | Resolve o **mesmo** `tasks.json` da raiz que a CLI usa — de propósito: uma tarefa criada por um caminho aparece no outro. |
| `mcp/project-context.js` | Helper **somente leitura** usado por `project_status`: coleta nome do projeto (de `package.json`) e contexto Git (branch, último commit). Qualquer falha externa vira `null`. |

O MCP funciona como **camada adaptadora sobre o domínio**. A lógica de negócio
permanece independente da interface: as tools consomem `src/`, e `src/` não
conhece `mcp/`. As dependências do SDK de MCP ficam confinadas a `mcp/` e ao
`package.json` da raiz.

Detalhes, diagramas e trade-offs: [`docs/architecture.md`](docs/architecture.md).

---

## MCP Integration

- **Servidor:** `mcp/server.js` (inicie com `npm run mcp` ou `node mcp/server.js`)
- **Transporte:** JSON-RPC 2.0 sobre **stdio**. `stdout` é reservado ao
  protocolo; o diagnóstico de conexão (`task-cli MCP server conectado (stdio)`)
  vai para `stderr`.

### Tools disponíveis

| Tool | O que faz | Escreve em `tasks.json`? |
|---|---|---|
| `add_task` | Cria uma tarefa pendente e retorna o id atribuído. | Sim |
| `list_tasks` | Lista as tarefas (id, status, descrição). | Não |
| `complete_task` | Conclui uma tarefa pendente e persiste a alteração. | Sim |
| `project_status` | Retorna contexto **somente leitura** do projeto: nome, branch atual, último commit e contagem de tarefas (`total`, `done`, `pending`). | **Não** |

`project_status` **não altera estado e não modifica `tasks.json`**: nunca chama
a persistência, reutiliza a camada de domínio apenas para *ler* as tarefas, e
consulta Git/`package.json` só para leitura. Falha ao consultar o Git (fora de um
repositório, `git` ausente no `PATH`) resulta em `null` no campo correspondente,
sem derrubar o servidor.

Formato do retorno de `project_status`:

```json
{
  "projectName": "claude-lab",
  "git": { "branch": "main", "lastCommit": "<hash> <assunto>" },
  "tasks": { "total": 3, "done": 1, "pending": 2 }
}
```

---

## Example Usage

### CLI

```
$ node src/index.js add "Estudar MCP"
Tarefa 1 adicionada

$ node src/index.js list
[ ] 1  Estudar MCP

$ node src/index.js done 1
Tarefa 1 concluída
```

### MCP / Claude Code

O cliente de IA faz a negociação MCP e então chama as tools. Um diálogo típico:

> **Usuário:** "Crie uma tarefa chamada estudar MCP"
> **Claude:** chama `add_task` com `{ "description": "estudar MCP" }`
> **Resultado:** `Tarefa 1 adicionada` (`structuredContent: { id: 1, description: "estudar MCP", done: false }`)

> **Usuário:** "Liste minhas tarefas"
> **Claude:** chama `list_tasks` com `{}`
> **Resultado:** `[ ] 1  estudar MCP`

> **Usuário:** "Qual o status do projeto?"
> **Claude:** chama `project_status` com `{}`
> **Resultado:** nome, branch, último commit e contagem de tarefas — sem tocar em `tasks.json`

Como CLI e MCP usam o mesmo `tasks.json`, uma tarefa criada por `add_task`
aparece em `node src/index.js list`, e vice-versa.

---

## Technical Highlights

- **Separação entre domínio e interfaces.** As regras vivem só em
  `src/task-service.js`; CLI e MCP são bordas finas por cima.
- **MCP como adaptador, não produto novo.** Nenhuma regra reimplementada em
  `mcp/`; o núcleo permanece sem dependências externas.
- **JSON-RPC 2.0 sobre stdio.** `stdout` exclusivo do protocolo; logs em
  `stderr` — condição para o cliente MCP não quebrar o parsing.
- **Integração com Claude Code.** As quatro tools são chamáveis diretamente por
  um cliente de IA, incluindo `project_status` para contexto de repositório.
- **Testes unitários** para o domínio, a CLI e as tools MCP.
- **Teste de integração** que sobe o servidor MCP **real** como subprocesso e
  exercita o handshake `initialize` → `tools/list` → `tools/call` por JSON-RPC.
- **Isolamento de armazenamento nos testes.** O caminho do `tasks.json` é
  injetável; as suítes usam armazenamento temporário e **nenhum teste escreve no
  `tasks.json` real**.
- **Sem duplicação de regra de negócio.** Um único ponto de verdade para
  validação, contagem e semântica de persistência.

---

## Testing

```
node --test
```

Test runner nativo do Node.js (`node:test`), sem framework. Atualmente **9
suítes em `test/`, 84 testes, todos passando**:

- **Domínio** — `test/task-service.test.js` (regras puras, imutabilidade, `nextId`).
- **CLI** — `test/add.test.js`, `test/list.test.js`, `test/done.test.js`,
  `test/cli.test.js` (comportamento ponta a ponta, ajuda de uso, exit codes).
- **Tools MCP** — `test/mcp-tools.test.js` (as três tools de tarefa),
  `test/project-status.test.js` (formato do contrato, estado vazio, contagem,
  arquivo corrompido, somente leitura).
- **Integração MCP** — `test/mcp-server-integration.test.js` (servidor real
  sobre stdio; valida que `stdout` só carrega JSON-RPC e que o `tasks.json` real
  não muda).
- **Hook do laboratório** — `test/block-agent-commit.test.js`.

Algumas suítes sobem subprocessos (CLI, hook, servidor MCP) para validar
comportamento de entrada real; as demais chamam as funções diretamente.

---

## Design Decisions

- **Por que o MCP não fica dentro de `src/`.** O núcleo é `stdlib-only` por
  contrato — testável, sem árvore de dependências, fácil de auditar. O SDK de MCP
  (`@modelcontextprotocol/sdk`) é uma dependência real e fica isolada em `mcp/`.
  As tools *consomem* `src/`; a dependência nunca vaza para o domínio.
- **Por que `project_status` é somente leitura.** Não existe operação de escrita
  de "status" no produto. Torná-la só-leitura evita expandir o escopo, mantém a
  tool segura de chamar a qualquer momento e permite degradação graciosa quando
  o Git não está disponível (campos `null` em vez de erro).
- **Por que manter `tasks.json` nesta versão.** O apetite do projeto é pequeno:
  um arquivo JSON local resolve a persistência sem servidor, sem schema, sem
  migração. Trocar por um banco só se justifica se o escopo crescer.
- **Trade-offs aceitos.** Sem controle de concorrência — CLI e servidor MCP
  gravando ao mesmo tempo podem se sobrescrever (documentado como No-go).
  `project_status` invoca `git` de forma síncrona (`execFileSync`, sem shell,
  com timeout), o que bloqueia o event loop do servidor por alguns segundos no
  pior caso e não tem cache.

---

## Future Improvements

Fora do escopo atual, mas compatíveis com a arquitetura:

- **Persistência em banco** (SQLite/Postgres) atrás da mesma interface de
  `store.js`, sem tocar no domínio.
- **Controle de concorrência** — lock cooperativo de arquivo entre CLI e
  servidor MCP.
- **Autenticação para MCP remoto**, caso o transporte deixe de ser stdio local.
- **Cache de informações Git** em `project_status` (TTL curto) para eliminar o
  custo de subprocesso por chamada; alternativamente, `git` assíncrono.
- **Observabilidade** — logs estruturados em `stderr` e métricas básicas de uso
  das tools.