loomiomcp 0.0.8 → 0.0.9

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
@@ -13,7 +13,7 @@ analyse member activity — in plain English. Targets Loomio's **b2** API
13
13
  [/help/api2](https://www.loomio.com/help/api2) and the namespace where
14
14
  the controllers actually live in the open-source repo.
15
15
 
16
- Tools (b2, per-user `?api_key=`):
16
+ Tools (b2, per-user API key):
17
17
 
18
18
  - `get_discussion(id_or_key)` — fetch one discussion
19
19
  - `list_discussions(group_id, status?, limit?, offset?)` — list a group's discussions
@@ -45,7 +45,7 @@ Tools (b2, per-user `?api_key=`):
45
45
  `remove_absent`.
46
46
  - `create_comment(discussion_id, body, body_format?)` — reply on a discussion
47
47
 
48
- Opt-in admin tools (b3, server-instance secret `?b3_api_key=`):
48
+ Opt-in admin tools (b3, server-instance secret):
49
49
 
50
50
  Set `LOOMIO_B3_API_KEY` to enable. Only useful for Loomio instance operators.
51
51
 
@@ -67,13 +67,23 @@ See DEPLOY.md for Cloud Run.
67
67
 
68
68
  ## Auth
69
69
 
70
- Loomio's b2 API authenticates by API key passed as a `?api_key=…` query
71
- parameter. The connector injects it server-side; it never reaches the
72
- MCP client. Generate one in Loomio under your profile → API keys.
70
+ Loomio authenticates by API key sent in an HTTP bearer header:
73
71
 
74
- The optional b3 admin namespace uses a different secret (`?b3_api_key=…`,
75
- validated against `ENV['B3_API_KEY']` on the Loomio server, >16 chars).
76
- Only relevant if you operate a Loomio instance.
72
+ ```text
73
+ Authorization: Bearer <API_KEY>
74
+ ```
75
+
76
+ The connector injects it server-side; it never reaches the MCP client.
77
+ Generate one in Loomio under your profile → API keys.
78
+
79
+ Keys passed in the query string (`?api_key=…`) are **rejected** — Loomio
80
+ removed that scheme in July 2026 because URLs are retained in browser
81
+ history, proxy logs, and monitoring systems. A request carrying its key
82
+ that way is treated as unauthenticated and 403s.
83
+
84
+ The optional b3 admin namespace uses the same bearer header with a
85
+ different secret (validated against `ENV['B3_API_KEY']` on the Loomio
86
+ server, >16 chars). Only relevant if you operate a Loomio instance.
77
87
 
78
88
  ## Read-only mode
79
89
 
package/dist/http.js CHANGED
@@ -213,9 +213,9 @@ async function handleResponse(res) {
213
213
  throw err;
214
214
  }
215
215
  }
216
- var B2_AUTH = { field: "api_key", getValue: getApiKey };
217
- var B3_AUTH = { field: "b3_api_key", getValue: getB3ApiKey };
218
- function buildUrl(auth, path, params) {
216
+ var B2_AUTH = getApiKey;
217
+ var B3_AUTH = getB3ApiKey;
218
+ function buildUrl(path, params) {
219
219
  const url = new URL(`${baseUrl()}${path}`);
220
220
  if (params) {
221
221
  for (const [key, value] of Object.entries(params)) {
@@ -224,9 +224,14 @@ function buildUrl(auth, path, params) {
224
224
  }
225
225
  }
226
226
  }
227
- url.searchParams.set(auth.field, auth.getValue());
228
227
  return url.toString();
229
228
  }
229
+ function authHeaders(auth) {
230
+ return {
231
+ ...baseHeaders(),
232
+ Authorization: `Bearer ${auth()}`
233
+ };
234
+ }
230
235
  async function doFetch(url, options) {
231
236
  const startedAt = Date.now();
232
237
  const method = options?.method ?? "GET";
@@ -258,8 +263,8 @@ function emitLoomioRequest(method, url, res, durationMs) {
258
263
  });
259
264
  }
260
265
  async function loomioGet(path, params) {
261
- const url = buildUrl(B2_AUTH, path, params);
262
- const start = await doFetch(url, { headers: baseHeaders() });
266
+ const url = buildUrl(path, params);
267
+ const start = await doFetch(url, { headers: authHeaders(B2_AUTH) });
263
268
  try {
264
269
  return await consumeBody(start, () => handleResponse(start.res));
265
270
  } finally {
@@ -267,8 +272,8 @@ async function loomioGet(path, params) {
267
272
  }
268
273
  }
269
274
  async function loomioGetStatus(path, params) {
270
- const url = buildUrl(B2_AUTH, path, params);
271
- const start = await doFetch(url, { headers: baseHeaders() });
275
+ const url = buildUrl(path, params);
276
+ const start = await doFetch(url, { headers: authHeaders(B2_AUTH) });
272
277
  try {
273
278
  return await consumeBody(start, async () => {
274
279
  try {
@@ -295,12 +300,12 @@ function encodeForm(body) {
295
300
  }
296
301
  async function loomioPost(path, body, opts = {}) {
297
302
  if (isReadOnly()) throw new LoomioReadOnlyError("POST");
298
- const url = buildUrl(B2_AUTH, path, opts.params);
303
+ const url = buildUrl(path, opts.params);
299
304
  const encoding = opts.encoding ?? "json";
300
305
  const start = await doFetch(url, {
301
306
  method: "POST",
302
307
  headers: {
303
- ...baseHeaders(),
308
+ ...authHeaders(B2_AUTH),
304
309
  "Content-Type": encoding === "form" ? "application/x-www-form-urlencoded" : "application/json"
305
310
  },
306
311
  body: encoding === "form" ? encodeForm(body) : JSON.stringify(body)
@@ -313,10 +318,10 @@ async function loomioPost(path, body, opts = {}) {
313
318
  }
314
319
  async function loomioPostB3(path, params) {
315
320
  if (isReadOnly()) throw new LoomioReadOnlyError("POST");
316
- const url = buildUrl(B3_AUTH, path, params);
321
+ const url = buildUrl(path, params);
317
322
  const start = await doFetch(url, {
318
323
  method: "POST",
319
- headers: { ...baseHeaders(), "Content-Type": "application/json" },
324
+ headers: { ...authHeaders(B3_AUTH), "Content-Type": "application/json" },
320
325
  body: "{}"
321
326
  });
322
327
  try {
@@ -1700,7 +1705,7 @@ function createLoomioMcpServer() {
1700
1705
  const server = new McpServer({
1701
1706
  name: "loomiomcp",
1702
1707
  // Keep in sync with package.json on each release.
1703
- version: "0.0.8",
1708
+ version: "0.0.9",
1704
1709
  description: "MCP server for Loomio (loomio.com / self-hosted). Wraps Loomio's b2 public API: read and create discussions, polls, and comments; list and manage group memberships; read per-discussion event streams; and aggregate a user's participation across groups. Read-only mode supported via LOOMIO_MCP_READONLY=1 (Cloud Run pattern). Optional b3 admin operations (deactivate / reactivate user) when LOOMIO_B3_API_KEY is set \u2014 instance operators only. Read tools annotated with readOnlyHint so MCP clients can auto-approve safe calls; destructive writes (manage_memberships with remove_absent, deactivate_user) carry destructiveHint.",
1705
1710
  websiteUrl: "https://github.com/soil-dev/loomiomcp",
1706
1711
  icons: ICONS
package/dist/index.js CHANGED
@@ -190,9 +190,9 @@ async function handleResponse(res) {
190
190
  throw err;
191
191
  }
192
192
  }
193
- var B2_AUTH = { field: "api_key", getValue: getApiKey };
194
- var B3_AUTH = { field: "b3_api_key", getValue: getB3ApiKey };
195
- function buildUrl(auth, path, params) {
193
+ var B2_AUTH = getApiKey;
194
+ var B3_AUTH = getB3ApiKey;
195
+ function buildUrl(path, params) {
196
196
  const url = new URL(`${baseUrl()}${path}`);
197
197
  if (params) {
198
198
  for (const [key, value] of Object.entries(params)) {
@@ -201,9 +201,14 @@ function buildUrl(auth, path, params) {
201
201
  }
202
202
  }
203
203
  }
204
- url.searchParams.set(auth.field, auth.getValue());
205
204
  return url.toString();
206
205
  }
206
+ function authHeaders(auth) {
207
+ return {
208
+ ...baseHeaders(),
209
+ Authorization: `Bearer ${auth()}`
210
+ };
211
+ }
207
212
  async function doFetch(url, options) {
208
213
  const startedAt = Date.now();
209
214
  const method = options?.method ?? "GET";
@@ -235,8 +240,8 @@ function emitLoomioRequest(method, url, res, durationMs) {
235
240
  });
236
241
  }
237
242
  async function loomioGet(path, params) {
238
- const url = buildUrl(B2_AUTH, path, params);
239
- const start = await doFetch(url, { headers: baseHeaders() });
243
+ const url = buildUrl(path, params);
244
+ const start = await doFetch(url, { headers: authHeaders(B2_AUTH) });
240
245
  try {
241
246
  return await consumeBody(start, () => handleResponse(start.res));
242
247
  } finally {
@@ -244,8 +249,8 @@ async function loomioGet(path, params) {
244
249
  }
245
250
  }
246
251
  async function loomioGetStatus(path, params) {
247
- const url = buildUrl(B2_AUTH, path, params);
248
- const start = await doFetch(url, { headers: baseHeaders() });
252
+ const url = buildUrl(path, params);
253
+ const start = await doFetch(url, { headers: authHeaders(B2_AUTH) });
249
254
  try {
250
255
  return await consumeBody(start, async () => {
251
256
  try {
@@ -272,12 +277,12 @@ function encodeForm(body) {
272
277
  }
273
278
  async function loomioPost(path, body, opts = {}) {
274
279
  if (isReadOnly()) throw new LoomioReadOnlyError("POST");
275
- const url = buildUrl(B2_AUTH, path, opts.params);
280
+ const url = buildUrl(path, opts.params);
276
281
  const encoding = opts.encoding ?? "json";
277
282
  const start = await doFetch(url, {
278
283
  method: "POST",
279
284
  headers: {
280
- ...baseHeaders(),
285
+ ...authHeaders(B2_AUTH),
281
286
  "Content-Type": encoding === "form" ? "application/x-www-form-urlencoded" : "application/json"
282
287
  },
283
288
  body: encoding === "form" ? encodeForm(body) : JSON.stringify(body)
@@ -290,10 +295,10 @@ async function loomioPost(path, body, opts = {}) {
290
295
  }
291
296
  async function loomioPostB3(path, params) {
292
297
  if (isReadOnly()) throw new LoomioReadOnlyError("POST");
293
- const url = buildUrl(B3_AUTH, path, params);
298
+ const url = buildUrl(path, params);
294
299
  const start = await doFetch(url, {
295
300
  method: "POST",
296
- headers: { ...baseHeaders(), "Content-Type": "application/json" },
301
+ headers: { ...authHeaders(B3_AUTH), "Content-Type": "application/json" },
297
302
  body: "{}"
298
303
  });
299
304
  try {
@@ -1049,7 +1054,7 @@ function createLoomioMcpServer() {
1049
1054
  const server2 = new McpServer({
1050
1055
  name: "loomiomcp",
1051
1056
  // Keep in sync with package.json on each release.
1052
- version: "0.0.8",
1057
+ version: "0.0.9",
1053
1058
  description: "MCP server for Loomio (loomio.com / self-hosted). Wraps Loomio's b2 public API: read and create discussions, polls, and comments; list and manage group memberships; read per-discussion event streams; and aggregate a user's participation across groups. Read-only mode supported via LOOMIO_MCP_READONLY=1 (Cloud Run pattern). Optional b3 admin operations (deactivate / reactivate user) when LOOMIO_B3_API_KEY is set \u2014 instance operators only. Read tools annotated with readOnlyHint so MCP clients can auto-approve safe calls; destructive writes (manage_memberships with remove_absent, deactivate_user) carry destructiveHint.",
1054
1059
  websiteUrl: "https://github.com/soil-dev/loomiomcp",
1055
1060
  icons: ICONS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loomiomcp",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Model Context Protocol server for Loomio. Lets Claude (Desktop, Code, or web Projects via Custom Connector) read and create Loomio discussions, polls, and comments, manage group memberships, and analyse member activity in plain English.",
5
5
  "keywords": [
6
6
  "mcp",