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 +18 -8
- package/dist/http.js +18 -13
- package/dist/index.js +18 -13
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
217
|
-
var B3_AUTH =
|
|
218
|
-
function buildUrl(
|
|
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(
|
|
262
|
-
const start = await doFetch(url, { headers:
|
|
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(
|
|
271
|
-
const start = await doFetch(url, { headers:
|
|
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(
|
|
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
|
-
...
|
|
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(
|
|
321
|
+
const url = buildUrl(path, params);
|
|
317
322
|
const start = await doFetch(url, {
|
|
318
323
|
method: "POST",
|
|
319
|
-
headers: { ...
|
|
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.
|
|
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 =
|
|
194
|
-
var B3_AUTH =
|
|
195
|
-
function buildUrl(
|
|
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(
|
|
239
|
-
const start = await doFetch(url, { headers:
|
|
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(
|
|
248
|
-
const start = await doFetch(url, { headers:
|
|
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(
|
|
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
|
-
...
|
|
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(
|
|
298
|
+
const url = buildUrl(path, params);
|
|
294
299
|
const start = await doFetch(url, {
|
|
295
300
|
method: "POST",
|
|
296
|
-
headers: { ...
|
|
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.
|
|
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.
|
|
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",
|