mcp-server-mturk 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,359 @@
1
+ """Worker management tools for blocking, notifying, and paying bonuses."""
2
+
3
+ import json
4
+ from mcp.types import TextContent
5
+
6
+ from ..client import get_mturk_client, format_error
7
+
8
+
9
+ def register_tools(register):
10
+ """Register worker-related tools."""
11
+
12
+ async def block_worker(args: dict) -> list[TextContent]:
13
+ """Block a worker from accepting your HITs."""
14
+ try:
15
+ client = get_mturk_client()
16
+ client.create_worker_block(
17
+ WorkerId=args["worker_id"],
18
+ Reason=args["reason"],
19
+ )
20
+ return [TextContent(type="text", text=json.dumps({
21
+ "success": True,
22
+ "worker_id": args["worker_id"],
23
+ "action": "blocked",
24
+ "reason": args["reason"],
25
+ }, indent=2))]
26
+ except Exception as e:
27
+ return [TextContent(type="text", text=format_error(e))]
28
+
29
+ register(
30
+ "block_worker",
31
+ """Prevents a worker from seeing or accepting any of your HITs in the future.
32
+
33
+ Use this tool as a last resort for workers who:
34
+ - Consistently submit spam or gibberish
35
+ - Appear to be bots or automated systems
36
+ - Violate your task guidelines repeatedly despite feedback
37
+ - Exhibit clearly bad-faith behavior
38
+
39
+ IMPORTANT CONSIDERATIONS:
40
+ - Blocking is account-wide - the worker can't do ANY of your HITs, not just the problematic one
41
+ - The worker is NOT notified they're blocked - they simply won't see your HITs
42
+ - The reason you provide is for YOUR records only (worker doesn't see it)
43
+ - Consider using qualification-based exclusion instead for less severe cases
44
+
45
+ Blocking is more severe than rejection. Rejection affects one assignment; blocking affects all future interactions. Use rejection + feedback first to give workers a chance to improve.
46
+
47
+ To reverse a block, use unblock_worker. You can see all blocked workers with list_blocked_workers.""",
48
+ {
49
+ "type": "object",
50
+ "properties": {
51
+ "worker_id": {
52
+ "type": "string",
53
+ "description": "The worker ID to block. Get this from list_assignments. Once blocked, this worker cannot see or accept any of your HITs."
54
+ },
55
+ "reason": {
56
+ "type": "string",
57
+ "description": "Your internal note explaining why you blocked this worker. The worker does NOT see this. Keep it factual for your records. Example: 'Submitted random characters on 5 consecutive assignments'"
58
+ },
59
+ },
60
+ "required": ["worker_id", "reason"],
61
+ },
62
+ block_worker,
63
+ )
64
+
65
+ async def unblock_worker(args: dict) -> list[TextContent]:
66
+ """Remove a block from a worker, allowing them to work on your HITs again."""
67
+ try:
68
+ client = get_mturk_client()
69
+ params = {"WorkerId": args["worker_id"]}
70
+ if args.get("reason"):
71
+ params["Reason"] = args["reason"]
72
+
73
+ client.delete_worker_block(**params)
74
+ return [TextContent(type="text", text=json.dumps({
75
+ "success": True,
76
+ "worker_id": args["worker_id"],
77
+ "action": "unblocked",
78
+ }, indent=2))]
79
+ except Exception as e:
80
+ return [TextContent(type="text", text=format_error(e))]
81
+
82
+ register(
83
+ "unblock_worker",
84
+ """Removes a block from a worker, allowing them to see and accept your HITs again.
85
+
86
+ Use this tool when:
87
+ - You blocked a worker by mistake
88
+ - A worker reached out and explained their situation
89
+ - Enough time has passed that you want to give them another chance
90
+ - You're cleaning up old blocks that are no longer relevant
91
+
92
+ The worker is not notified when unblocked - they'll simply start seeing your HITs again.
93
+
94
+ To find workers you've blocked, use list_blocked_workers. The reason field is optional and for your records only.""",
95
+ {
96
+ "type": "object",
97
+ "properties": {
98
+ "worker_id": {
99
+ "type": "string",
100
+ "description": "The worker ID to unblock. Get this from list_blocked_workers."
101
+ },
102
+ "reason": {
103
+ "type": "string",
104
+ "description": "Optional note for your records about why you're unblocking. Example: 'Worker apologized and promised to follow guidelines'"
105
+ },
106
+ },
107
+ "required": ["worker_id"],
108
+ },
109
+ unblock_worker,
110
+ )
111
+
112
+ async def list_blocked_workers(args: dict) -> list[TextContent]:
113
+ """List all workers you have blocked."""
114
+ try:
115
+ client = get_mturk_client()
116
+ response = client.list_worker_blocks(MaxResults=min(args.get("max_results", 100), 100))
117
+ blocks = response.get("WorkerBlocks", [])
118
+ result = {
119
+ "count": len(blocks),
120
+ "blocked_workers": [
121
+ {
122
+ "worker_id": b["WorkerId"],
123
+ "reason": b.get("Reason"),
124
+ }
125
+ for b in blocks
126
+ ],
127
+ }
128
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
129
+ except Exception as e:
130
+ return [TextContent(type="text", text=format_error(e))]
131
+
132
+ register(
133
+ "list_blocked_workers",
134
+ """Returns a list of all workers you have blocked from your HITs.
135
+
136
+ Use this tool to:
137
+ - Review who you've blocked and why (the reason you provided when blocking)
138
+ - Find worker IDs for workers you want to unblock
139
+ - Audit your blocklist periodically to clean up old blocks
140
+
141
+ Each entry shows the worker_id and the reason you recorded when blocking them.
142
+
143
+ Note: Only returns up to 100 blocked workers. If you have more, you may need to paginate or clean up old blocks.""",
144
+ {
145
+ "type": "object",
146
+ "properties": {
147
+ "max_results": {
148
+ "type": "integer",
149
+ "description": "Maximum number of blocked workers to return, between 1 and 100. Defaults to 100.",
150
+ "default": 100,
151
+ "minimum": 1,
152
+ "maximum": 100
153
+ },
154
+ },
155
+ "required": [],
156
+ },
157
+ list_blocked_workers,
158
+ )
159
+
160
+ async def notify_workers(args: dict) -> list[TextContent]:
161
+ """Send an email notification to one or more workers."""
162
+ try:
163
+ client = get_mturk_client()
164
+ worker_list = [w.strip() for w in args["worker_ids"].split(",")]
165
+
166
+ client.notify_workers(
167
+ Subject=args["subject"],
168
+ MessageText=args["message"],
169
+ WorkerIds=worker_list,
170
+ )
171
+ return [TextContent(type="text", text=json.dumps({
172
+ "success": True,
173
+ "workers_notified": worker_list,
174
+ "subject": args["subject"],
175
+ }, indent=2))]
176
+ except Exception as e:
177
+ return [TextContent(type="text", text=format_error(e))]
178
+
179
+ register(
180
+ "notify_workers",
181
+ """Sends an email message to one or more workers through MTurk's messaging system.
182
+
183
+ Use this tool to:
184
+ - Thank workers for excellent work
185
+ - Invite trusted workers to new HITs
186
+ - Clarify task instructions if many workers are confused
187
+ - Apologize for issues with your HITs
188
+ - Request workers return for follow-up studies
189
+
190
+ IMPORTANT GUIDELINES:
191
+ - Workers can block requesters who send too many messages - use sparingly
192
+ - Messages come from your MTurk requester account, not a personal email
193
+ - Keep messages professional and relevant to work
194
+ - Don't use for marketing unrelated to your HITs
195
+
196
+ The message goes to workers' MTurk-associated email addresses. They may have filters or not check frequently, so don't rely on immediate responses.
197
+
198
+ Subject lines should be clear and professional. Messages should be concise and actionable.
199
+
200
+ Example use: After a successful survey, message participants: "Thank you for completing our research survey. We've posted a follow-up study you may be interested in." """,
201
+ {
202
+ "type": "object",
203
+ "properties": {
204
+ "worker_ids": {
205
+ "type": "string",
206
+ "description": "Comma-separated list of worker IDs to message. Example: 'A1B2C3D4E5,F6G7H8I9J0'. Get worker IDs from list_assignments. Maximum varies but keep it reasonable (under 100)."
207
+ },
208
+ "subject": {
209
+ "type": "string",
210
+ "description": "Email subject line. Keep it professional and clear. Example: 'Thank you for your survey response' or 'New HIT available that may interest you'"
211
+ },
212
+ "message": {
213
+ "type": "string",
214
+ "description": "The message body. Keep it concise and relevant to their work. Include any action items clearly. Be professional - this reflects on you as a requester."
215
+ },
216
+ },
217
+ "required": ["worker_ids", "subject", "message"],
218
+ },
219
+ notify_workers,
220
+ )
221
+
222
+ async def send_bonus(args: dict) -> list[TextContent]:
223
+ """Send a bonus payment to a worker for a specific assignment."""
224
+ try:
225
+ client = get_mturk_client()
226
+ client.send_bonus(
227
+ WorkerId=args["worker_id"],
228
+ AssignmentId=args["assignment_id"],
229
+ BonusAmount=args["bonus_amount"],
230
+ Reason=args["reason"],
231
+ )
232
+ return [TextContent(type="text", text=json.dumps({
233
+ "success": True,
234
+ "worker_id": args["worker_id"],
235
+ "assignment_id": args["assignment_id"],
236
+ "bonus_amount": args["bonus_amount"],
237
+ "reason": args["reason"],
238
+ }, indent=2))]
239
+ except Exception as e:
240
+ return [TextContent(type="text", text=format_error(e))]
241
+
242
+ register(
243
+ "send_bonus",
244
+ """Sends an additional payment (bonus) to a worker for a specific assignment they completed.
245
+
246
+ Use this tool to:
247
+ - Reward exceptionally high-quality work
248
+ - Compensate workers for extra effort or time
249
+ - Pay for legitimate work on a HIT you had to reject for technical reasons
250
+ - Incentivize workers in longitudinal studies
251
+ - Make up for your own mistakes (e.g., unclear instructions that caused rework)
252
+
253
+ IMPORTANT:
254
+ - Bonuses cost real money - they're charged to your account immediately
255
+ - MTurk charges fees on bonuses (typically 20%)
256
+ - Workers LOVE bonuses - they're great for building a reliable workforce
257
+ - The reason you provide IS shown to the worker
258
+
259
+ Bonuses must be tied to a specific assignment (you can't bonus someone randomly). The assignment must be approved or rejected already - you can't bonus pending work.
260
+
261
+ Good bonus practices:
262
+ - Be specific about why: "Bonus for providing detailed explanations" not just "Good work"
263
+ - Fair bonuses for extra work build trust and encourage quality
264
+ - Even small bonuses ($0.10-0.25) are appreciated and remembered""",
265
+ {
266
+ "type": "object",
267
+ "properties": {
268
+ "worker_id": {
269
+ "type": "string",
270
+ "description": "The worker ID to send the bonus to. Must match the worker who completed the assignment."
271
+ },
272
+ "assignment_id": {
273
+ "type": "string",
274
+ "description": "The assignment ID this bonus is for. The assignment must exist and be completed (approved or rejected). Get from list_assignments."
275
+ },
276
+ "bonus_amount": {
277
+ "type": "string",
278
+ "description": "Bonus amount in USD as a string. Example: '0.50' for 50 cents, '2.00' for $2.00. Must be at least $0.01. You'll also pay MTurk fees on top of this."
279
+ },
280
+ "reason": {
281
+ "type": "string",
282
+ "description": "Message explaining why you're giving this bonus. THE WORKER SEES THIS. Be specific and appreciative. Example: 'Thank you for providing such detailed and thoughtful responses!'"
283
+ },
284
+ },
285
+ "required": ["worker_id", "assignment_id", "bonus_amount", "reason"],
286
+ },
287
+ send_bonus,
288
+ )
289
+
290
+ async def list_bonus_payments(args: dict) -> list[TextContent]:
291
+ """List bonus payments made. Must specify either hit_id or assignment_id."""
292
+ try:
293
+ if not args.get("hit_id") and not args.get("assignment_id"):
294
+ return [TextContent(type="text", text="Error: Must specify either hit_id or assignment_id")]
295
+
296
+ client = get_mturk_client()
297
+ params = {"MaxResults": min(args.get("max_results", 100), 100)}
298
+ if args.get("hit_id"):
299
+ params["HITId"] = args["hit_id"]
300
+ if args.get("assignment_id"):
301
+ params["AssignmentId"] = args["assignment_id"]
302
+
303
+ response = client.list_bonus_payments(**params)
304
+ payments = response.get("BonusPayments", [])
305
+ result = {
306
+ "count": len(payments),
307
+ "bonus_payments": [
308
+ {
309
+ "worker_id": p["WorkerId"],
310
+ "assignment_id": p["AssignmentId"],
311
+ "bonus_amount": p["BonusAmount"],
312
+ "reason": p.get("Reason"),
313
+ "grant_time": str(p.get("GrantTime")),
314
+ }
315
+ for p in payments
316
+ ],
317
+ }
318
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
319
+ except Exception as e:
320
+ return [TextContent(type="text", text=format_error(e))]
321
+
322
+ register(
323
+ "list_bonus_payments",
324
+ """Lists bonus payments you've made, filtered by HIT or assignment.
325
+
326
+ Use this tool to:
327
+ - Audit bonus spending on a specific HIT
328
+ - Verify a bonus was sent successfully
329
+ - Track how much you've bonused across a project
330
+ - Ensure you haven't accidentally double-bonused a worker
331
+
332
+ You MUST provide either a hit_id (to see all bonuses for that HIT) or an assignment_id (to see bonuses for that specific assignment). Cannot list all bonuses across your entire account.
333
+
334
+ Returns the amount, worker, assignment, reason, and timestamp for each bonus payment.
335
+
336
+ Note: This shows bonuses YOU sent. It doesn't show the base HIT payment, only additional bonuses.""",
337
+ {
338
+ "type": "object",
339
+ "properties": {
340
+ "hit_id": {
341
+ "type": "string",
342
+ "description": "List bonuses for all assignments in this HIT. Provide either this OR assignment_id, not both."
343
+ },
344
+ "assignment_id": {
345
+ "type": "string",
346
+ "description": "List bonuses for this specific assignment only. Provide either this OR hit_id, not both."
347
+ },
348
+ "max_results": {
349
+ "type": "integer",
350
+ "description": "Maximum number of bonus payments to return, between 1 and 100. Defaults to 100.",
351
+ "default": 100,
352
+ "minimum": 1,
353
+ "maximum": 100
354
+ },
355
+ },
356
+ "required": [],
357
+ },
358
+ list_bonus_payments,
359
+ )