kassinao-mcp 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -34
  2. package/dist/index.js +34 -16
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,26 +1,28 @@
1
1
  # kassinao-mcp
2
2
 
3
- Conector **MCP** do [Kassinão](https://github.com/resolvicomai/kassinao): faz o seu assistente de IA (Claude Desktop, Cursor, etc.) responder perguntas sobre as reuniões que o bot gravou — **"o que ficou pendente essa semana?"**, **"quem falou de orçamento na terça?"**, **"lista as calls entre 1 e 30 de junho"** — em linguagem natural.
3
+ The **MCP** connector for [Kassinão](https://github.com/resolvicomai/kassinao): it lets your AI assistant (Claude Desktop, Cursor, etc.) answer questions about the meetings the bot recorded — **"what's pending this week?"**, **"who mentioned the budget on Tuesday?"**, **"list the calls between June 1 and 30"** — in natural language.
4
4
 
5
- ## Como funciona (e por que é seguro)
5
+ > Documentação em português: veja o README principal em [pt-BR](../README.pt-BR.md).
6
6
 
7
- O conector roda **na sua máquina** e é um cliente HTTP **magro**: ele **não** lê gravações nem decide acesso. Ele carrega um **token pessoal** e chama a API do bot, que aplica **o mesmo controle de acesso da página web** — reunião por reunião. Você só enxerga o que já enxergaria no site. **Não existe modo "vê tudo".** É **somente leitura** (não grava, não apaga, não serve áudio).
7
+ ## How it works (and why it's safe)
8
8
 
9
- > ⚠️ **Transcrição é entrada não-confiável.** Qualquer participante de uma call pode ter "falado" texto malicioso ou usado um apelido hostil. O servidor envolve todo conteúdo de reunião num bloco de "dados não-confiáveis" e limpa sequências de controle antes de entregarmas trate o conteúdo das reuniões como dados, nunca como instruções.
9
+ The connector runs **on your machine** and is a **thin** HTTP client: it does **not** read recordings or make access decisions. It carries a **personal token** and calls the bot's API, which applies **the same access control as the web page**meeting by meeting. You only see what you'd already see on the site. **There is no "see everything" mode.** It is **read-only** (it never writes, deletes, or serves audio).
10
10
 
11
- ## Pré-requisitos
11
+ > ⚠️ **Transcripts are untrusted input.** Any call participant may have "spoken" malicious text or used a hostile nickname. The server wraps all meeting content in an "untrusted data" block and strips control sequences before delivering it — but always treat meeting content as data, never as instructions.
12
12
 
13
- - **O admin do bot precisa ter ligado o MCP** (variável `MCP_SECRET` no servidor). Sem isso, `/conectar-ia` e `/mcp` não existem (dá 404 / comando ausente).
14
- - **Node.js 20+** na sua máquina.
15
- - **O conector.** `npx -y kassinao-mcp` baixa e roda sozinho — você não precisa instalar nada à mão (só ter o Node). Prefere rodar do código-fonte? `git clone` o repo, `cd mcp && npm install && npm run build`; na config, troque `"command": "npx"` / `"args": ["-y","kassinao-mcp"]` por `"command": "node"`, `"args": ["/caminho/absoluto/do/repo/mcp/dist/index.js"]`.
13
+ ## Prerequisites
16
14
 
17
- ## Instalação
15
+ - **The bot admin must have enabled MCP** (`MCP_SECRET` set on the server). Without it, `/app/conectar-ia` and `/mcp` don't exist (404 / missing command).
16
+ - **Node.js 20+** on your machine.
17
+ - **The connector.** `npx -y kassinao-mcp` downloads and runs it on its own — nothing to install manually (you just need Node). Prefer running from source? `git clone` the repo, `cd mcp && npm install && npm run build`; in the config, replace `"command": "npx"` / `"args": ["-y","kassinao-mcp"]` with `"command": "node"`, `"args": ["/absolute/path/to/repo/mcp/dist/index.js"]`.
18
18
 
19
- ### Opção A — pela página web (mais fácil)
19
+ ## Setup
20
20
 
21
- 1. Abra `https://SEU-KASSINAO/conectar-ia` e entre com o Discord.
22
- 2. Clique em **Gerar token de conexão** e copie o bloco de config mostrado (aparece **uma vez**).
23
- 3. Cole no `claude_desktop_config.json` (Claude Desktop) ou no equivalente do Cursor:
21
+ ### Option A via the web page (easiest)
22
+
23
+ 1. Open `https://YOUR-KASSINAO/app/conectar-ia` and sign in with Discord.
24
+ 2. Click **Generate connection token** and copy the config block shown (it appears **once**).
25
+ 3. Paste it into your MCP client's config — `claude_desktop_config.json` (Claude Desktop), `~/.cursor/mcp.json` (Cursor), or wherever your assistant's docs point:
24
26
 
25
27
  ```json
26
28
  {
@@ -29,44 +31,44 @@ O conector roda **na sua máquina** e é um cliente HTTP **magro**: ele **não**
29
31
  "command": "npx",
30
32
  "args": ["-y", "kassinao-mcp"],
31
33
  "env": {
32
- "KASSINAO_URL": "https://SEU-KASSINAO",
33
- "KASSINAO_REFRESH_TOKEN": "COLE_O_TOKEN_AQUI"
34
+ "KASSINAO_URL": "https://YOUR-KASSINAO",
35
+ "KASSINAO_REFRESH_TOKEN": "PASTE_THE_TOKEN_HERE"
34
36
  }
35
37
  }
36
38
  }
37
39
  }
38
40
  ```
39
41
 
40
- 4. Reinicie o Claude Desktop / Cursor. Pronto.
42
+ 4. Restart your MCP client (Claude Desktop, Cursor, or any other). Done.
41
43
 
42
- ### Opção B — sem navegador (VM/SSH)
44
+ ### Option B — no browser (VM/SSH)
43
45
 
44
- No Discord, o dono roda **`/mcp novo`** (em clientes em inglês aparece como **`/mcp new`**) — resposta efêmera, código de uso único válido por ~5 min. Depois:
46
+ On Discord, the owner runs **`/mcp new`** (shown as **`/mcp novo`** on pt-BR clients) — ephemeral reply with a single-use code valid for ~5 min. Then:
45
47
 
46
48
  ```bash
47
- KASSINAO_URL=https://SEU-KASSINAO npx -y kassinao-mcp exchange <codigo>
49
+ KASSINAO_URL=https://YOUR-KASSINAO npx -y kassinao-mcp exchange <code>
48
50
  ```
49
51
 
50
- Isso guarda o token localmente. Configure o cliente MCP igual à Opção A (o `KASSINAO_REFRESH_TOKEN` no env vira opcional depois do primeiro uso).
52
+ This stores the token locally. Configure your MCP client just like Option A (the `KASSINAO_REFRESH_TOKEN` env var becomes optional after the first use).
51
53
 
52
- ## Onde o token fica
54
+ ## Where the token lives
53
55
 
54
- Depois do primeiro uso, o token de refresh (rotacionado a cada renovação) fica em `~/.config/kassinao-mcp/token.json` com permissão `0600`. Nenhuma gravação/transcrição é copiada para a sua máquinao conector fala HTTPS com o servidor.
56
+ After first use, the refresh token (rotated on every renewal) is stored at `~/.config/kassinao-mcp/token.json` with `0600` permissions. No recordings/transcripts are ever copied to your machinethe connector only talks HTTPS to the server.
55
57
 
56
- ## Revogar
58
+ ## Revoking
57
59
 
58
- - Na página `/conectar-ia`: botão **Revogar todos**.
59
- - No Discord: `/mcp revoke-all`.
60
- - Botão de pânico do administrador: rotacionar `MCP_SECRET` no servidor (revoga **todos** os conectores de todo mundo).
60
+ - On the `/app/conectar-ia` page: **Revoke all** button.
61
+ - On Discord: `/mcp revoke-all`.
62
+ - Admin panic button: rotate `MCP_SECRET` on the server (revokes **everyone's** connectors at once).
61
63
 
62
- ## Ferramentas expostas
64
+ ## Exposed tools
63
65
 
64
- | Ferramenta | Para quê |
66
+ | Tool | What for |
65
67
  |---|---|
66
- | `list_meetings` | listar reuniões numa janela de tempo (padrão: últimos 30 dias) |
67
- | `pending_actions` | ações/prazos pendentes cruzando várias reuniões (overdue / dueSoon / …) |
68
- | `search_meetings` | busca full-text em transcrição, ata e notas, com link no minuto exato |
69
- | `who_said` | trechos ditos por alguém sobre um assunto, com contexto e link |
70
- | `get_meeting` | dossiê de uma reunião: metadados, ata, transcrição, notas e linha do tempo |
68
+ | `list_meetings` | list meetings within a time window (default: last 30 days) |
69
+ | `pending_actions` | pending action items/deadlines across meetings (overdue / dueSoon / …) |
70
+ | `search_meetings` | full-text search over transcripts, minutes and notes, with a link to the exact minute |
71
+ | `who_said` | what someone said about a topic, with context and link |
72
+ | `get_meeting` | full dossier of one meeting: metadata, minutes, transcript, notes and timeline |
71
73
 
72
- Requer Node.js ≥ 20. Licença MIT.
74
+ Requires Node.js ≥ 20. License: AGPL-3.0.
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ /*
3
+ * Kassinão MCP — conector local para clientes MCP.
4
+ * Copyright (C) 2026 Mauro Marques (resolvicomai)
5
+ * Software livre sob a GNU Affero General Public License v3 ou posterior.
6
+ * Veja <https://www.gnu.org/licenses/> para o texto completo.
7
+ */
2
8
  /**
3
9
  * Conector MCP do Kassinão (roda LOCAL, na máquina de quem usa Claude Desktop/Cursor).
4
10
  *
@@ -12,7 +18,7 @@
12
18
  *
13
19
  * Env:
14
20
  * KASSINAO_URL ex.: https://kassinao.suaempresa.com (obrigatório)
15
- * KASSINAO_REFRESH_TOKEN token gerado em /conectar-ia (só no 1º uso; depois fica salvo)
21
+ * KASSINAO_REFRESH_TOKEN token gerado em /app/conectar-ia (só no 1º uso; depois fica salvo)
16
22
  */
17
23
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
24
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -44,23 +50,35 @@ function saveStore(s) {
44
50
  let refreshToken = loadStore().refreshToken || process.env.KASSINAO_REFRESH_TOKEN || '';
45
51
  let accessToken = '';
46
52
  let accessExpMs = 0;
53
+ async function tryRefresh(token) {
54
+ const r = await fetch(`${URL_BASE}/api/mcp/refresh`, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify({ refresh_token: token }),
58
+ });
59
+ if (!r.ok)
60
+ return null;
61
+ return (await r.json());
62
+ }
47
63
  async function refreshTokens() {
48
64
  if (!URL_BASE)
49
65
  throw new Error('Defina KASSINAO_URL (ex.: https://kassinao.suaempresa.com).');
50
66
  if (!refreshToken) {
51
67
  const base = URL_BASE || '<sua URL do Kassinão>';
52
- throw new Error(`Sem token. Gere um em ${base}/conectar-ia ou rode: kassinao-mcp exchange <codigo>.`);
68
+ throw new Error(`Sem token. Gere um em ${base}/app/conectar-ia ou rode: kassinao-mcp exchange <codigo>.`);
53
69
  }
54
- const r = await fetch(`${URL_BASE}/api/mcp/refresh`, {
55
- method: 'POST',
56
- headers: { 'content-type': 'application/json' },
57
- body: JSON.stringify({ refresh_token: refreshToken }),
58
- });
59
- if (!r.ok) {
70
+ let data = await tryRefresh(refreshToken);
71
+ // Token salvo morto (ex.: usuário revogou tudo e gerou um NOVO no env):
72
+ // cai de volta pro env uma vez, em vez de só falhar. Se o env funcionar,
73
+ // ele rotaciona e vira o salvo — o fluxo se conserta sozinho.
74
+ const envTok = process.env.KASSINAO_REFRESH_TOKEN || '';
75
+ if (!data && envTok && envTok !== refreshToken) {
76
+ data = await tryRefresh(envTok);
77
+ }
78
+ if (!data) {
60
79
  const base = URL_BASE || '<sua URL do Kassinão>';
61
- throw new Error(`Não consegui renovar o token (HTTP ${r.status}). Gere um novo em ${base}/conectar-ia.`);
80
+ throw new Error(`Não consegui renovar o token (revogado ou expirado). Gere um novo em ${base}/app/conectar-ia.`);
62
81
  }
63
- const data = (await r.json());
64
82
  refreshToken = data.refresh_token; // rotação: guarda o novo imediatamente
65
83
  accessToken = data.access_token;
66
84
  accessExpMs = Date.parse(data.access_expires_at) || Date.now() + 10 * 60 * 1000;
@@ -105,7 +123,7 @@ const rangeProps = {
105
123
  const TOOLS = [
106
124
  {
107
125
  name: 'list_meetings',
108
- description: 'List recorded meetings in a time window (defaults to the last 30 days). Only meetings the user can access are returned. Use for "what meetings happened between X and Y" / "list this week\'s calls".',
126
+ description: 'List recorded meetings in a time window (defaults to the last 30 days). Only meetings the user can access are returned. Each item carries transcriptStatus ("partial" = some speakers not transcribed yet), presentSilent (people in the call who never spoke) and audioDeleted (tiered retention: audio expired, text remains). Use for "what meetings happened between X and Y" / "list this week\'s calls".',
109
127
  inputSchema: {
110
128
  type: 'object',
111
129
  properties: {
@@ -122,7 +140,7 @@ const TOOLS = [
122
140
  },
123
141
  {
124
142
  name: 'pending_actions',
125
- description: 'Aggregate action items (task + owner + deadline) across meetings, bucketed by deadline: overdue, dueSoon, later, noDeadline, unparseable. Use for "what is pending this week" / "my open action items". assignee="me" matches the token owner.',
143
+ description: 'Aggregate action items (task + owner + deadline) across meetings, bucketed by deadline: overdue, dueSoon, later, noDeadline, unparseable. Items include transcriptStatus — minutes built from a partial transcript may be missing actions. Use for "what is pending this week" / "my open action items". assignee="me" matches the token owner.',
126
144
  inputSchema: {
127
145
  type: 'object',
128
146
  properties: {
@@ -136,7 +154,7 @@ const TOOLS = [
136
154
  },
137
155
  {
138
156
  name: 'search_meetings',
139
- description: 'Full-text search across transcripts, minutes and notes of accessible meetings, with deep links to the exact moment. Use for "find where we discussed X".',
157
+ description: 'Full-text search across transcripts, minutes and notes of accessible meetings, accent-insensitive, with deep links to the exact second. Use for "find where we discussed X".',
140
158
  inputSchema: {
141
159
  type: 'object',
142
160
  properties: {
@@ -153,7 +171,7 @@ const TOOLS = [
153
171
  },
154
172
  {
155
173
  name: 'who_said',
156
- description: 'Find transcript segments matching a query, with speaker, timestamp, surrounding context and a deep link. Use for "when did Ana talk about budget".',
174
+ description: 'Find transcript segments matching a query (accent-insensitive), with speaker, timestamp, surrounding context and a deep link. transcriptStatus="partial" means some speakers are not transcribed yet — absence of a match is not proof nobody said it. Use for "when did Ana talk about budget".',
157
175
  inputSchema: {
158
176
  type: 'object',
159
177
  properties: {
@@ -171,7 +189,7 @@ const TOOLS = [
171
189
  },
172
190
  {
173
191
  name: 'get_meeting',
174
- description: 'Full dossier of one meeting: metadata, minutes (summary/decisions/actions/topics/per-participant), transcript, notes and a merged timeline. include is a CSV of meta,minutes,transcript,notes,timeline.',
192
+ description: 'Full dossier of one meeting: metadata, minutes (summary/decisions/actions/topics/per-participant), transcript, notes and a merged timeline. Check transcriptStatus: "partial" = transcript incomplete (pending speakers). include is a CSV of meta,minutes,transcript,notes,timeline.',
175
193
  inputSchema: {
176
194
  type: 'object',
177
195
  properties: {
@@ -214,7 +232,7 @@ async function runExchange(code) {
214
232
  }
215
233
  // ---------- servidor MCP ----------
216
234
  async function runServer() {
217
- const server = new Server({ name: 'kassinao', version: '1.0.0' }, { capabilities: { tools: {} } });
235
+ const server = new Server({ name: 'kassinao', version: '1.0.1' }, { capabilities: { tools: {} } });
218
236
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
219
237
  tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
220
238
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kassinao-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP connector for Kassinão — ask your Discord meeting recordings (transcripts, minutes, action items) from Claude Desktop, Cursor and other MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,5 +33,5 @@
33
33
  "@types/node": "^22.0.0",
34
34
  "typescript": "^5.6.0"
35
35
  },
36
- "license": "MIT"
36
+ "license": "AGPL-3.0-or-later"
37
37
  }