Commit732f089fRecorded25 Mar 2026Repositoryparlor

Add MCP tools for user-by-email, topic invite, and extended admin invite

Message

Expose new sigil-discourse functions as parlor MCP tools and CLI commands:

- parlor/users-by-email: find user by email address (admin endpoint) - parlor/topics-invite: invite a user to a topic by email or username - parlor/admin-invite: now supports topicid and groupnames parameters for creating invite links that land on a specific topic

Also adds CLI commands (users by-email, topics invite), tests for the new tool exports, and updates the README.

Changed
 README.md             |  6 ++++--
 src/parlor/admin.sgl  | 14 +++++++++++---
 src/parlor/main.sgl   | 15 +++++++++++++++
 src/parlor/topics.sgl | 28 ++++++++++++++++++++++++++--
 src/parlor/users.sgl  | 24 ++++++++++++++++++++++++
 test/test-tools.sgl   | 45 +++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 125 insertions(+), 7 deletions(-)
Diff
README.mdmodified
@@ -13,6 +13,7 @@ parlor topics close 42 # Close topic
13
parlor topics open 42 # Reopen topic
14
parlor topics pin 42 # Pin topic
15
parlor topics delete 42 # Delete topic
+16
parlor topics invite 42 "[email protected]" # Invite user to topic
17
parlor posts get 123 # Get post
18
parlor posts create 42 "Reply body" # Reply to topic
19
parlor posts update 123 "Edited body" # Edit post
@@ -25,6 +26,7 @@ parlor categories get 5 # Get category
26
parlor categories create "Name" "0088CC" # Create category
27
parlor users get username # Get user
28
parlor users list # List users
+29
parlor users by-email [email protected] # Find user by email
30
parlor users create "Name" "[email protected]" "pw" un # Create user
31
parlor groups list # List groups
32
parlor groups get group-name # Get group
@@ -35,12 +37,12 @@ parlor groups create new-group # Create group
37
38
Start with `parlor serve`. Exposes these tools:
39
38
- `parlor/topics-latest`, `parlor/topics-top`, `parlor/topics-by-category`, `parlor/topics-get`, `parlor/topics-create`, `parlor/topics-update`, `parlor/topics-close`, `parlor/topics-open`, `parlor/topics-pin`, `parlor/topics-delete`
+40
- `parlor/topics-latest`, `parlor/topics-top`, `parlor/topics-by-category`, `parlor/topics-get`, `parlor/topics-create`, `parlor/topics-update`, `parlor/topics-close`, `parlor/topics-open`, `parlor/topics-pin`, `parlor/topics-delete`, `parlor/topics-invite`
41
- `parlor/posts-get`, `parlor/posts-create`, `parlor/posts-update`, `parlor/posts-delete`
42
- `parlor/messages-send`, `parlor/messages-list`, `parlor/messages-reply`
43
- `parlor/search`
44
- `parlor/categories-list`, `parlor/categories-get`, `parlor/categories-create`
43
- `parlor/users-get`, `parlor/users-list`, `parlor/users-create`, `parlor/users-suspend`, `parlor/users-unsuspend`
+45
- `parlor/users-get`, `parlor/users-list`, `parlor/users-by-email`, `parlor/users-create`, `parlor/users-suspend`, `parlor/users-unsuspend`
46
- `parlor/groups-list`, `parlor/groups-get`, `parlor/groups-create`, `parlor/groups-add-members`, `parlor/groups-remove-members`
47
- `parlor/admin-settings`, `parlor/admin-update-setting`, `parlor/admin-invite`
48
src/parlor/admin.sglmodified
@@ -38,7 +38,11 @@
38
(properties . ((email . ((type . "string")
39
(description . "Email address to invite")))
40
(group_ids . ((type . "string")
41
(description . "Comma-separated group IDs (optional)")))))
+41
(description . "Comma-separated group IDs (optional)")))
+42
(topic_id . ((type . "integer")
+43
(description . "Topic ID to land on after accepting invite (optional)")))
+44
(group_names . ((type . "string")
+45
(description . "Comma-separated group names (optional, alternative to group_ids)")))))
46
(required . ("email"))))
47
48
;; ============================================================
@@ -86,10 +90,14 @@
90
91
(define (tool-admin-invite args)
92
(let* ((email (dict-ref args email:))
89
(group-ids (dict-ref args group_ids: "")))
+93
(group-ids (dict-ref args group_ids: ""))
+94
(topic-id (dict-ref args topic_id: #f))
+95
(group-names (dict-ref args group_names: #f)))
96
(discourse-create-invite
97
(parlor-base-url) (parlor-api-key) (parlor-username)
92
email group-ids)
+98
email group-ids
+99
topic-id: topic-id
+100
group-names: group-names)
101
(string-append "Sent invite to " email ".")))
102
103
;; ============================================================
src/parlor/main.sglmodified
@@ -53,6 +53,7 @@
53
(display " parlor topics open <id> Reopen topic\n")
54
(display " parlor topics pin <id> Pin topic\n")
55
(display " parlor topics delete <id> Delete topic\n")
+56
(display " parlor topics invite <id> <user> Invite user to topic\n")
57
(display " parlor posts get <id> Get post\n")
58
(display " parlor posts create <topic-id> <body> Reply to topic\n")
59
(display " parlor posts update <id> <body> Edit post\n")
@@ -65,6 +66,7 @@
66
(display " parlor categories create <name> <color> Create category\n")
67
(display " parlor users get <username> Get user\n")
68
(display " parlor users list List users\n")
+69
(display " parlor users by-email <email> Find user by email\n")
70
(display " parlor users create <name> <email> <pw> <un> Create user\n")
71
(display " parlor groups list List groups\n")
72
(display " parlor groups get <name> Get group\n")
@@ -128,6 +130,13 @@
130
#{ topic_id: (string->number (car rest)) }))
131
(newline))
132
+133
((and (string=? group "topics") (string=? cmd "invite")
+134
(>= (length rest) 2))
+135
(display (tool-topics-invite
+136
#{ topic_id: (string->number (car rest))
+137
user: (cadr rest) }))
+138
(newline))
+139
140
;; posts
141
((and (string=? group "posts") (string=? cmd "get")
142
(>= (length rest) 1))
@@ -207,6 +216,12 @@
216
(display (tool-users-list #{}))
217
(newline))
218
+219
((and (string=? group "users") (string=? cmd "by-email")
+220
(>= (length rest) 1))
+221
(display (tool-users-by-email
+222
#{ email: (car rest) }))
+223
(newline))
+224
225
((and (string=? group "users") (string=? cmd "create")
226
(>= (length rest) 4))
227
(display (tool-users-create
src/parlor/topics.sglmodified
@@ -21,7 +21,8 @@
21
tool-topics-close
22
tool-topics-open
23
tool-topics-pin
24
tool-topics-delete)
+24
tool-topics-delete
+25
tool-topics-invite)
26
(begin
27
28
;; ============================================================
@@ -98,6 +99,14 @@
99
(description . "Topic ID")))))
100
(required . ("topic_id"))))
101
+102
(define topics-invite-schema
+103
'((type . "object")
+104
(properties . ((topic_id . ((type . "integer")
+105
(description . "Topic ID")))
+106
(user . ((type . "string")
+107
(description . "Email address or username to invite")))))
+108
(required . ("topic_id" "user"))))
+109
110
;; ============================================================
111
;; Formatting
112
;; ============================================================
@@ -224,6 +233,15 @@
233
topic-id)
234
(string-append "Deleted topic #" (number->string topic-id) ".")))
235
+236
(define (tool-topics-invite args)
+237
(let ((topic-id (dict-ref args topic_id:))
+238
(user (dict-ref args user:)))
+239
(discourse-invite-to-topic
+240
(parlor-base-url) (parlor-api-key) (parlor-username)
+241
topic-id user)
+242
(string-append "Invited " user " to topic #"
+243
(number->string topic-id) ".")))
+244
245
;; ============================================================
246
;; MCP Registration
247
;; ============================================================
@@ -287,6 +305,12 @@
305
"parlor/topics-delete"
306
"Delete a topic."
307
topics-delete-schema
290
tool-topics-delete))
+308
tool-topics-delete)
+309
+310
(register server
+311
"parlor/topics-invite"
+312
"Invite a user to a topic by email or username."
+313
topics-invite-schema
+314
tool-topics-invite))
315
316
))
src/parlor/users.sglmodified
@@ -13,6 +13,7 @@
13
(export register-user-tools!
14
tool-users-get
15
tool-users-list
+16
tool-users-by-email
17
tool-users-create
18
tool-users-suspend
19
tool-users-unsuspend)
@@ -33,6 +34,12 @@
34
(properties . ())
35
(required . ())))
36
+37
(define users-by-email-schema
+38
'((type . "object")
+39
(properties . ((email . ((type . "string")
+40
(description . "Email address to search for")))))
+41
(required . ("email"))))
+42
43
(define users-create-schema
44
'((type . "object")
45
(properties . ((name . ((type . "string")
@@ -116,6 +123,17 @@
123
"Active users:\n\n"
124
(string-join (map format-user (array->list users)) "\n")))))
125
+126
(define (tool-users-by-email args)
+127
(let* ((email (dict-ref args email:))
+128
(users (discourse-list-users
+129
(parlor-base-url) (parlor-api-key) (parlor-username)
+130
email: email)))
+131
(if (or (not users) (= (array-length users) 0))
+132
(string-append "No user found with email \"" email "\".")
+133
(string-append
+134
"Users matching " email ":\n\n"
+135
(string-join (map format-user (array->list users)) "\n")))))
+136
137
(define (tool-users-create args)
138
(let* ((name (dict-ref args name:))
139
(email (dict-ref args email:))
@@ -162,6 +180,12 @@
180
users-list-schema
181
tool-users-list)
182
+183
(register server
+184
"parlor/users-by-email"
+185
"Find a user by their email address (admin endpoint)."
+186
users-by-email-schema
+187
tool-users-by-email)
+188
189
(register server
190
"parlor/users-create"
191
"Create a new user account."
test/test-tools.sgladded
@@ -0,0 +1,45 @@
+1
(import (sigil test)
+2
(sigil core)
+3
(sigil process)
+4
(parlor users)
+5
(parlor topics)
+6
(parlor admin))
+7
+8
;; ============================================================
+9
;; users-by-email tool
+10
;; ============================================================
+11
+12
(test-group "users-by-email"
+13
(test "handler returns message when no users found"
+14
(setenv! "DISCOURSE_URL" "https://test.example.com")
+15
(setenv! "DISCOURSE_API_KEY" "test-key")
+16
(setenv! "DISCOURSE_USERNAME" "system")
+17
;; We can't mock the API call, but we can verify the tool
+18
;; function exists and is exported
+19
(assert-true (procedure? tool-users-by-email)))
+20
+21
(test "handler is exported alongside existing tools"
+22
(assert-true (procedure? tool-users-get))
+23
(assert-true (procedure? tool-users-list))
+24
(assert-true (procedure? tool-users-by-email))
+25
(assert-true (procedure? tool-users-create))
+26
(assert-true (procedure? tool-users-suspend))
+27
(assert-true (procedure? tool-users-unsuspend))))
+28
+29
;; ============================================================
+30
;; topics-invite tool
+31
;; ============================================================
+32
+33
(test-group "topics-invite"
+34
(test "handler is exported alongside existing tools"
+35
(assert-true (procedure? tool-topics-latest))
+36
(assert-true (procedure? tool-topics-invite))
+37
(assert-true (procedure? tool-topics-delete))))
+38
+39
;; ============================================================
+40
;; admin-invite tool (extended)
+41
;; ============================================================
+42
+43
(test-group "admin-invite"
+44
(test "handler is exported"
+45
(assert-true (procedure? tool-admin-invite))))