kassinao-mcp 1.0.4 → 1.0.5

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 CHANGED
@@ -16,7 +16,7 @@ The connector runs **on your machine** and is a **thin** HTTP client: it does **
16
16
 
17
17
  - **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).
18
18
  - **Node.js 20+** on your machine.
19
- - **The connector.** `npx -y kassinao-mcp@1.0.4` downloads and runs the pinned published release — nothing to install manually (you just need Node). Prefer running from source? `git clone` the repo, `cd mcp && npm ci --userconfig ../.npmrc.security && npm run build`; in the config, replace `"command": "npx"` / `"args": ["-y","kassinao-mcp@1.0.4"]` with `"command": "node"`, `"args": ["/absolute/path/to/repo/mcp/dist/index.js"]`.
19
+ - **The connector.** `npx -y kassinao-mcp@1.0.5` downloads and runs the pinned published release — nothing to install manually (you just need Node). Prefer running from source? `git clone` the repo, `cd mcp && npm ci --userconfig ../.npmrc.security && npm run build`; in the config, replace `"command": "npx"` / `"args": ["-y","kassinao-mcp@1.0.5"]` with `"command": "node"`, `"args": ["/absolute/path/to/repo/mcp/dist/index.js"]`.
20
20
 
21
21
  ## Setup
22
22
 
@@ -31,7 +31,7 @@ The connector runs **on your machine** and is a **thin** HTTP client: it does **
31
31
  "mcpServers": {
32
32
  "kassinao": {
33
33
  "command": "npx",
34
- "args": ["-y", "kassinao-mcp@1.0.4"],
34
+ "args": ["-y", "kassinao-mcp@1.0.5"],
35
35
  "env": {
36
36
  "KASSINAO_URL": "https://mcp.kassinao.cloud",
37
37
  "KASSINAO_PROFILE": "PROFILE_PRINTED_BY_THE_COMMAND"
@@ -50,7 +50,7 @@ For a self-hosted instance, open `APP_URL/app/conectar-ia`; the generated comman
50
50
  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:
51
51
 
52
52
  ```bash
53
- npx -y kassinao-mcp@1.0.4 exchange --stdin --url https://mcp.kassinao.cloud
53
+ npx -y kassinao-mcp@1.0.5 exchange --stdin --url https://mcp.kassinao.cloud
54
54
  ```
55
55
 
56
56
  Paste the one-time code when prompted. Input is hidden so the code does not enter shell history or process arguments. The command stores the token locally and prints a copy-ready config containing a non-secret `KASSINAO_PROFILE` id. Use that block as printed; it selects this connection's own token file without placing the refresh token in your client config. Replace the URL with your instance's `MCP_URL` when self-hosting.
@@ -1,13 +1,48 @@
1
+ const DEFAULT_MAX_JSON_BYTES = 32 * 1024 * 1024;
1
2
  /**
2
3
  * Parses a successful API response without ever copying an upstream response
3
- * body into an MCP-visible error. Meeting content may itself be adversarial.
4
+ * body into an MCP-visible error. The stream is bounded before concatenation or
5
+ * JSON.parse so a hostile origin cannot turn a small token response into OOM.
4
6
  */
5
- export async function readApiJson(response) {
7
+ export async function readApiJson(response, maxBytes = DEFAULT_MAX_JSON_BYTES) {
6
8
  if (!response.ok) {
7
9
  throw new Error(`Kassinão request failed (HTTP ${response.status}). Try again in a moment.`);
8
10
  }
11
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
12
+ throw new Error('Kassinão returned an invalid JSON response.');
13
+ }
9
14
  try {
10
- return await response.json();
15
+ const declaredLength = Number(response.headers.get('content-length'));
16
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes)
17
+ throw new Error('oversized');
18
+ if (!response.body)
19
+ return JSON.parse(await response.text());
20
+ const reader = response.body.getReader();
21
+ const chunks = [];
22
+ let total = 0;
23
+ try {
24
+ while (true) {
25
+ const { done, value } = await reader.read();
26
+ if (done)
27
+ break;
28
+ total += value.byteLength;
29
+ if (total > maxBytes) {
30
+ await reader.cancel();
31
+ throw new Error('oversized');
32
+ }
33
+ chunks.push(value);
34
+ }
35
+ }
36
+ finally {
37
+ reader.releaseLock();
38
+ }
39
+ const payload = new Uint8Array(total);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ payload.set(chunk, offset);
43
+ offset += chunk.byteLength;
44
+ }
45
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(payload));
11
46
  }
12
47
  catch {
13
48
  throw new Error('Kassinão returned an invalid JSON response.');
@@ -1,6 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { withCredentialStoreLock } from './credentialLock.js';
3
3
  import { selectBootstrapRefreshToken } from './tokenAuth.js';
4
+ const MAX_ACCESS_TOKEN_CHARS = 8_192;
5
+ const MAX_REFRESH_TOKEN_CHARS = 4_096;
6
+ const MAX_EXPIRY_CHARS = 64;
4
7
  export function parseCredentialTokenResponse(value) {
5
8
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
6
9
  throw new Error('Kassinão returned an invalid token response.');
@@ -8,9 +11,13 @@ export function parseCredentialTokenResponse(value) {
8
11
  const response = value;
9
12
  if (typeof response.access_token !== 'string' ||
10
13
  response.access_token.length === 0 ||
14
+ response.access_token.length > MAX_ACCESS_TOKEN_CHARS ||
11
15
  typeof response.refresh_token !== 'string' ||
12
16
  response.refresh_token.length === 0 ||
17
+ response.refresh_token.length > MAX_REFRESH_TOKEN_CHARS ||
13
18
  typeof response.access_expires_at !== 'string' ||
19
+ response.access_expires_at.length === 0 ||
20
+ response.access_expires_at.length > MAX_EXPIRY_CHARS ||
14
21
  !Number.isFinite(Date.parse(response.access_expires_at))) {
15
22
  throw new Error('Kassinão returned an invalid token response.');
16
23
  }
@@ -1,6 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ const MAX_CREDENTIAL_STORE_BYTES = 64 * 1024;
5
+ const MAX_STORED_URL_CHARS = 2_048;
6
+ const MAX_STORED_REFRESH_TOKEN_CHARS = 4_096;
4
7
  function parseCredentials(raw) {
5
8
  try {
6
9
  const parsed = JSON.parse(raw);
@@ -8,8 +11,10 @@ function parseCredentials(raw) {
8
11
  return {};
9
12
  const value = parsed;
10
13
  return {
11
- url: typeof value.url === 'string' ? value.url : undefined,
12
- refreshToken: typeof value.refreshToken === 'string' ? value.refreshToken : undefined,
14
+ url: typeof value.url === 'string' && value.url.length <= MAX_STORED_URL_CHARS ? value.url : undefined,
15
+ refreshToken: typeof value.refreshToken === 'string' && value.refreshToken.length <= MAX_STORED_REFRESH_TOKEN_CHARS
16
+ ? value.refreshToken
17
+ : undefined,
13
18
  refreshAttempt: typeof value.refreshAttempt === 'string' && /^[a-f0-9]{32}$/.test(value.refreshAttempt)
14
19
  ? value.refreshAttempt
15
20
  : undefined,
@@ -73,6 +78,8 @@ export function loadCredentialStore(directory, file) {
73
78
  if (!openedStats.isFile())
74
79
  throw unsafeStore('the token path is not a regular file.');
75
80
  assertOwnedByCurrentUser(openedStats, 'the token file');
81
+ if (openedStats.size > MAX_CREDENTIAL_STORE_BYTES)
82
+ throw unsafeStore('the token file is too large.');
76
83
  if (process.platform !== 'win32')
77
84
  fs.fchmodSync(fd, 0o600);
78
85
  return parseCredentials(fs.readFileSync(fd, 'utf8'));
@@ -84,6 +91,10 @@ export function loadCredentialStore(directory, file) {
84
91
  /** Grava o cofre por rename atômico e só retorna depois de fsync no arquivo/diretório. */
85
92
  export function saveCredentialStore(directory, file, credentials) {
86
93
  protectDirectory(directory);
94
+ const payload = JSON.stringify(credentials);
95
+ if (Buffer.byteLength(payload, 'utf8') > MAX_CREDENTIAL_STORE_BYTES) {
96
+ throw unsafeStore('the token payload is too large.');
97
+ }
87
98
  const temporary = `${file}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
88
99
  const noFollow = process.platform === 'win32' ? 0 : fs.constants.O_NOFOLLOW;
89
100
  try {
@@ -91,7 +102,7 @@ export function saveCredentialStore(directory, file, credentials) {
91
102
  try {
92
103
  if (process.platform !== 'win32')
93
104
  fs.fchmodSync(fd, 0o600);
94
- fs.writeFileSync(fd, JSON.stringify(credentials));
105
+ fs.writeFileSync(fd, payload);
95
106
  fs.fsyncSync(fd);
96
107
  }
97
108
  finally {
package/dist/http.js ADDED
@@ -0,0 +1,64 @@
1
+ export const DEFAULT_HTTP_TIMEOUT_MS = 20_000;
2
+ function abortReason(signal) {
3
+ if (signal.reason instanceof Error && signal.reason.name === 'TimeoutError') {
4
+ const error = new Error('Kassinão request timed out. Try again in a moment.');
5
+ error.name = 'TimeoutError';
6
+ return error;
7
+ }
8
+ if (signal.reason instanceof Error)
9
+ return signal.reason;
10
+ const error = new Error('Kassinão request was cancelled.');
11
+ error.name = 'AbortError';
12
+ return error;
13
+ }
14
+ function waitWithSignal(operation, signal) {
15
+ if (signal.aborted) {
16
+ void Promise.resolve(operation).catch(() => undefined);
17
+ return Promise.reject(abortReason(signal));
18
+ }
19
+ return new Promise((resolve, reject) => {
20
+ const onAbort = () => reject(abortReason(signal));
21
+ signal.addEventListener('abort', onAbort, { once: true });
22
+ Promise.resolve(operation).then((value) => {
23
+ signal.removeEventListener('abort', onAbort);
24
+ resolve(value);
25
+ }, (error) => {
26
+ signal.removeEventListener('abort', onAbort);
27
+ reject(error);
28
+ });
29
+ });
30
+ }
31
+ /**
32
+ * Nunca permite redirects nem espera indefinida. O consumidor precisa terminar
33
+ * de ler o body dentro do callback, para o mesmo deadline cobrir headers + body.
34
+ */
35
+ export async function strictFetch(input, init, consume) {
36
+ const explicitSignal = init.signal ?? undefined;
37
+ const controller = new AbortController();
38
+ const signal = controller.signal;
39
+ const forwardExplicitAbort = () => controller.abort(explicitSignal?.reason);
40
+ let response;
41
+ if (explicitSignal?.aborted)
42
+ forwardExplicitAbort();
43
+ else
44
+ explicitSignal?.addEventListener('abort', forwardExplicitAbort, { once: true });
45
+ const timeout = setTimeout(() => {
46
+ controller.abort(new DOMException('Kassinão request timed out.', 'TimeoutError'));
47
+ }, DEFAULT_HTTP_TIMEOUT_MS);
48
+ timeout.unref();
49
+ try {
50
+ response = await waitWithSignal(fetch(input, { ...init, redirect: 'error', signal }), signal);
51
+ return await waitWithSignal(Promise.resolve(consume(response)), signal);
52
+ }
53
+ catch (error) {
54
+ if (signal.aborted)
55
+ throw abortReason(signal);
56
+ throw error;
57
+ }
58
+ finally {
59
+ clearTimeout(timeout);
60
+ explicitSignal?.removeEventListener('abort', forwardExplicitAbort);
61
+ if (response?.body && !response.bodyUsed)
62
+ void response.body.cancel().catch(() => undefined);
63
+ }
64
+ }
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import path from 'node:path';
28
28
  import { readApiJson } from './apiResponse.js';
29
29
  import { parseCredentialTokenResponse, refreshCredential } from './credentialRefresh.js';
30
30
  import { loadCredentialStore, saveCredentialStore } from './credentialStore.js';
31
+ import { DEFAULT_HTTP_TIMEOUT_MS, strictFetch } from './http.js';
31
32
  import { createTokenProfileId, mayFallbackToEnvToken, normalizeKassinaoUrl, selectBootstrapRefreshToken, singleFlight, tokenStoreFileName, } from './tokenAuth.js';
32
33
  import { createToolErrorResponse, createToolResponse, MCP_UNTRUSTED_DESCRIPTION } from './toolOutput.js';
33
34
  function configuredUrl() {
@@ -114,18 +115,21 @@ function saveStore(s, file = STORE_FILE) {
114
115
  }
115
116
  let accessToken = '';
116
117
  let accessExpMs = 0;
118
+ const MAX_TOKEN_RESPONSE_BYTES = 32 * 1024;
117
119
  async function tryRefresh(token, attemptId) {
118
- const r = await fetch(`${URL_BASE}/api/mcp/refresh`, {
120
+ return strictFetch(`${URL_BASE}/api/mcp/refresh`, {
119
121
  method: 'POST',
120
122
  headers: { 'content-type': 'application/json' },
121
123
  body: JSON.stringify({ refresh_token: token, attempt_id: attemptId }),
122
- signal: AbortSignal.timeout(20_000),
124
+ signal: AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS),
125
+ }, async (response) => {
126
+ if (mayFallbackToEnvToken(response.status))
127
+ return undefined;
128
+ if (!response.ok) {
129
+ throw new Error(`Could not refresh the token (HTTP ${response.status}). Try again in a moment.`);
130
+ }
131
+ return readApiJson(response, MAX_TOKEN_RESPONSE_BYTES);
123
132
  });
124
- if (mayFallbackToEnvToken(r.status))
125
- return undefined;
126
- if (!r.ok)
127
- throw new Error(`Could not refresh the token (HTTP ${r.status}). Try again in a moment.`);
128
- return readApiJson(r);
129
133
  }
130
134
  async function refreshTokens() {
131
135
  if (!URL_BASE)
@@ -156,20 +160,28 @@ async function apiGet(pathname, params) {
156
160
  if (v !== undefined && v !== null && v !== '')
157
161
  url.searchParams.set(k, String(v));
158
162
  }
163
+ const request = (token) => strictFetch(url, { headers: { authorization: `Bearer ${token}` } }, async (response) => {
164
+ if (response.status === 401)
165
+ return { unauthorized: true };
166
+ if (response.status === 503) {
167
+ throw new Error('Kassinão is starting or Discord is unavailable. Try again in a moment.');
168
+ }
169
+ return { unauthorized: false, data: await readApiJson(response) };
170
+ });
159
171
  let token = await getAccess();
160
- let r = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
161
- if (r.status === 401) {
172
+ let result = await request(token);
173
+ if (result.unauthorized) {
162
174
  // Another tool may have refreshed while this request still used the previous
163
175
  // access token. Reuse the new token and refresh only if the current one failed.
164
176
  if (token === accessToken)
165
177
  await refreshOnce();
166
178
  token = accessToken;
167
- r = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
179
+ result = await request(token);
168
180
  }
169
- if (r.status === 503) {
170
- throw new Error('Kassinão is starting or Discord is unavailable. Try again in a moment.');
181
+ if (result.unauthorized) {
182
+ throw new Error('Kassinão request failed (HTTP 401). Try again in a moment.');
171
183
  }
172
- return readApiJson(r);
184
+ return result.data;
173
185
  }
174
186
  // ---------- tool definitions ----------
175
187
  const rangeProps = {
@@ -184,7 +196,7 @@ const rangeProps = {
184
196
  const TOOLS = [
185
197
  {
186
198
  name: 'list_meetings',
187
- 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".',
199
+ 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). Follow nextCursor until null; only then continue with nextScanCursor. Use for "what meetings happened between X and Y" / "list this week\'s calls".',
188
200
  inputSchema: {
189
201
  type: 'object',
190
202
  properties: {
@@ -194,14 +206,21 @@ const TOOLS = [
194
206
  participantId: { type: 'string' },
195
207
  status: { type: 'string', enum: ['done', 'recording'] },
196
208
  limit: { type: 'number' },
197
- cursor: { type: 'string' },
209
+ cursor: {
210
+ type: 'string',
211
+ description: 'opaque nextCursor; continue result pagination before using nextScanCursor',
212
+ },
213
+ scanCursor: {
214
+ type: 'string',
215
+ description: 'opaque nextScanCursor; use only when nextCursor is null',
216
+ },
198
217
  },
199
218
  },
200
219
  call: (a) => apiGet('/api/meetings', a),
201
220
  },
202
221
  {
203
222
  name: 'pending_actions',
204
- 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.',
223
+ 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. Follow nextCursor until null; only then continue with nextScanCursor. Use for "what is pending this week" / "my open action items". assignee="me" matches the token owner.',
205
224
  inputSchema: {
206
225
  type: 'object',
207
226
  properties: {
@@ -209,13 +228,22 @@ const TOOLS = [
209
228
  assignee: { type: 'string', description: '"me" or part of the assignee name' },
210
229
  meetingsWithin: { type: 'string', description: 'how far back to scan meetings (for example, "60d")' },
211
230
  guildId: { type: 'string' },
231
+ limit: { type: 'number' },
232
+ cursor: {
233
+ type: 'string',
234
+ description: 'opaque nextCursor; continue actions before using nextScanCursor',
235
+ },
236
+ scanCursor: {
237
+ type: 'string',
238
+ description: 'opaque nextScanCursor; use only when nextCursor is null',
239
+ },
212
240
  },
213
241
  },
214
242
  call: (a) => apiGet('/api/actions', a),
215
243
  },
216
244
  {
217
245
  name: 'search_meetings',
218
- 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".',
246
+ description: 'Full-text search across transcripts, minutes and notes of accessible meetings, accent-insensitive, with deep links to the exact second. Follow nextCursor until null; only then continue with nextScanCursor. Use for "find where we discussed X".',
219
247
  inputSchema: {
220
248
  type: 'object',
221
249
  properties: {
@@ -225,6 +253,14 @@ const TOOLS = [
225
253
  ...rangeProps,
226
254
  guildId: { type: 'string' },
227
255
  limit: { type: 'number' },
256
+ cursor: {
257
+ type: 'string',
258
+ description: 'opaque nextCursor; continue matches before using nextScanCursor',
259
+ },
260
+ scanCursor: {
261
+ type: 'string',
262
+ description: 'opaque nextScanCursor; use only when nextCursor is null',
263
+ },
228
264
  },
229
265
  required: ['query'],
230
266
  },
@@ -232,7 +268,7 @@ const TOOLS = [
232
268
  },
233
269
  {
234
270
  name: 'who_said',
235
- 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".',
271
+ 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. Follow nextCursor until null; only then continue with nextScanCursor. Use for "when did Ana talk about budget".',
236
272
  inputSchema: {
237
273
  type: 'object',
238
274
  properties: {
@@ -243,6 +279,14 @@ const TOOLS = [
243
279
  ...rangeProps,
244
280
  guildId: { type: 'string' },
245
281
  limit: { type: 'number' },
282
+ cursor: {
283
+ type: 'string',
284
+ description: 'opaque nextCursor; continue transcript matches before using nextScanCursor',
285
+ },
286
+ scanCursor: {
287
+ type: 'string',
288
+ description: 'opaque nextScanCursor; use only when nextCursor is null',
289
+ },
246
290
  },
247
291
  required: ['query'],
248
292
  },
@@ -274,16 +318,17 @@ async function runExchange(code) {
274
318
  console.error('Set KASSINAO_URL first (for example, KASSINAO_URL=https://kassinao.example.com).');
275
319
  process.exit(1);
276
320
  }
277
- const r = await fetch(`${URL_BASE}/api/mcp/exchange`, {
321
+ const data = await strictFetch(`${URL_BASE}/api/mcp/exchange`, {
278
322
  method: 'POST',
279
323
  headers: { 'content-type': 'application/json' },
280
324
  body: JSON.stringify({ code }),
325
+ }, async (response) => {
326
+ if (!response.ok) {
327
+ console.error(`Could not exchange the code (HTTP ${response.status}). Codes expire in about 5 minutes and can only be used once. Generate another with /mcp new.`);
328
+ process.exit(1);
329
+ }
330
+ return parseCredentialTokenResponse(await readApiJson(response, MAX_TOKEN_RESPONSE_BYTES));
281
331
  });
282
- if (!r.ok) {
283
- console.error(`Could not exchange the code (HTTP ${r.status}). Codes expire in about 5 minutes and can only be used once. Generate another with /mcp new.`);
284
- process.exit(1);
285
- }
286
- const data = parseCredentialTokenResponse(await readApiJson(r));
287
332
  const profile = createTokenProfileId();
288
333
  const profileStoreFile = path.join(STORE_DIR, tokenStoreFileName(URL_BASE, '', profile));
289
334
  saveStore({ url: URL_BASE, refreshToken: data.refresh_token }, profileStoreFile);
@@ -307,10 +352,20 @@ function isExchangeCode(value) {
307
352
  }
308
353
  async function readExchangeCode() {
309
354
  if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
310
- const input = fs.readFileSync(0, 'utf8').trim();
311
- if (input.length > 256)
312
- throw new Error('Invalid one-time connection code.');
313
- return input;
355
+ const maxBytes = 256;
356
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
357
+ const chunks = [];
358
+ let total = 0;
359
+ while (true) {
360
+ const read = fs.readSync(0, buffer, 0, Math.min(buffer.length, maxBytes + 1 - total), null);
361
+ if (read === 0)
362
+ break;
363
+ total += read;
364
+ if (total > maxBytes)
365
+ throw new Error('Invalid one-time connection code.');
366
+ chunks.push(Buffer.from(buffer.subarray(0, read)));
367
+ }
368
+ return Buffer.concat(chunks, total).toString('utf8').trim();
314
369
  }
315
370
  return new Promise((resolve, reject) => {
316
371
  const input = process.stdin;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "kassinao-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "kassinao-mcp",
9
- "version": "1.0.4",
9
+ "version": "1.0.5",
10
10
  "bundleDependencies": ["@modelcontextprotocol/sdk"],
11
11
  "license": "AGPL-3.0-or-later",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kassinao-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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
  "author": "Mauro Marques",
6
6
  "homepage": "https://mcp.kassinao.cloud",