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.
- package/README.md +36 -34
- package/dist/index.js +34 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
# kassinao-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
> Documentação em português: veja o README principal em [pt-BR](../README.pt-BR.md).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## How it works (and why it's safe)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
## Setup
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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://
|
|
33
|
-
"KASSINAO_REFRESH_TOKEN": "
|
|
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.
|
|
42
|
+
4. Restart your MCP client (Claude Desktop, Cursor, or any other). Done.
|
|
41
43
|
|
|
42
|
-
###
|
|
44
|
+
### Option B — no browser (VM/SSH)
|
|
43
45
|
|
|
44
|
-
|
|
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://
|
|
49
|
+
KASSINAO_URL=https://YOUR-KASSINAO npx -y kassinao-mcp exchange <code>
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
|
|
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
|
-
##
|
|
54
|
+
## Where the token lives
|
|
53
55
|
|
|
54
|
-
|
|
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 machine — the connector only talks HTTPS to the server.
|
|
55
57
|
|
|
56
|
-
##
|
|
58
|
+
## Revoking
|
|
57
59
|
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
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
|
-
##
|
|
64
|
+
## Exposed tools
|
|
63
65
|
|
|
64
|
-
|
|
|
66
|
+
| Tool | What for |
|
|
65
67
|
|---|---|
|
|
66
|
-
| `list_meetings` |
|
|
67
|
-
| `pending_actions` |
|
|
68
|
-
| `search_meetings` |
|
|
69
|
-
| `who_said` |
|
|
70
|
-
| `get_meeting` |
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!
|
|
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 (
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
36
|
+
"license": "AGPL-3.0-or-later"
|
|
37
37
|
}
|