@hapticpaper/mcp-server 1.0.18 → 1.0.19

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
@@ -32,15 +32,13 @@ Your tokens are stored securely in `~/.hapticpaper/tokens.json`.
32
32
 
33
33
  ## Available Tools
34
34
 
35
- | Tool | Description |
36
- |------|-------------|
37
- | `create_task` | Create a new task for a human worker |
38
- | `get_task` | Get details of a specific task |
39
- | `list_my_tasks` | List your tasks |
40
- | `cancel_task` | Cancel a pending task |
41
- | `search_workers` | Find available workers |
42
- | `get_estimate` | Get price/time estimates |
43
- | `get_account` | View your account info |
35
+ | Tool | Description | Actions |
36
+ |------|-------------|---------|
37
+ | `tasks` | Manage task lifecycle | `create`, `get`, `list`, `update`, `cancel` |
38
+ | `workers` | Browse and view workers | `search`, `get_profile` |
39
+ | `account` | Manage your account | `get_my_profile`, `get_balance` |
40
+ | `qualifications` | Onboarding & Interviews | `start`, `continue`, `check_status` |
41
+ | `haptic` | Discovery & Helpers | `intent`, `search_docs` |
44
42
 
45
43
  ## Manual Authentication
46
44
 
@@ -3,10 +3,7 @@ import { requireScopes } from "../auth/access.js";
3
3
  import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
4
4
  function oauthSecuritySchemes(scopes) {
5
5
  return [
6
- {
7
- type: 'oauth2',
8
- scopes,
9
- },
6
+ { type: 'oauth2', scopes },
10
7
  ];
11
8
  }
12
9
  function toolDescriptorMeta(invoking, invoked, scopes) {
@@ -25,63 +22,68 @@ function toolInvocationMeta(invoking, invoked, widgetSessionId) {
25
22
  'openai/widgetSessionId': widgetSessionId,
26
23
  };
27
24
  }
28
- // No input needed - just returns current user's account info
29
- const GetAccountSchema = z.object({}).describe("No parameters required");
25
+ const AccountToolSchema = z.object({
26
+ action: z.enum(['get_my_profile', 'get_balance', 'transaction_history']).describe("Action to perform on the account resource"),
27
+ }).describe("Manage user account and credits. Actions: get_my_profile, get_balance.");
30
28
  export function registerAccountTools(server, client) {
31
- const getAccountInvoking = 'Fetching your account details';
32
- const getAccountInvoked = 'Account details ready';
33
- const getAccountHandler = async (_args, extra) => {
29
+ const accountHandler = async (args, extra) => {
34
30
  try {
35
- // For HTTP transport, auth comes from extra.authInfo. For stdio, auth is handled by tokenProvider.
36
31
  const auth = requireScopes(extra, ['account:read']);
37
- const result = await client.getAccount(auth?.token);
38
- const account = result.data;
39
- const widgetSessionId = `account:${account.userId}`;
40
- return {
41
- structuredContent: {
42
- account: {
43
- displayName: account.displayName,
44
- email: account.email,
45
- balanceCredits: account.balanceCredits,
46
- hasPaymentMethod: account.hasPaymentMethod,
47
- },
48
- },
49
- content: [
50
- {
51
- type: 'text',
52
- text: `Welcome, ${account.displayName}! Your Haptic Paper balance is ${account.balanceCredits} credits.${account.hasPaymentMethod ? '' : ' Add a payment method to create paid tasks.'}`,
53
- },
54
- ],
55
- _meta: {
56
- ...toolInvocationMeta(getAccountInvoking, getAccountInvoked, widgetSessionId),
57
- account,
58
- },
32
+ // Common logic for fetching account data (used by profile and balance actions)
33
+ const fetchAccount = async () => {
34
+ const result = await client.getAccount(auth?.token);
35
+ return result.data;
59
36
  };
37
+ switch (args.action) {
38
+ case 'get_my_profile': {
39
+ const account = await fetchAccount();
40
+ const widgetSessionId = `account:${account.userId}`;
41
+ return {
42
+ structuredContent: {
43
+ account: {
44
+ userId: account.userId,
45
+ displayName: account.displayName,
46
+ email: account.email
47
+ }
48
+ },
49
+ content: [{ type: 'text', text: `Logged in as ${account.displayName} (${account.email})` }],
50
+ _meta: { ...toolInvocationMeta('Fetching profile', 'Profile loaded', widgetSessionId), account }
51
+ };
52
+ }
53
+ case 'get_balance': {
54
+ const account = await fetchAccount();
55
+ const widgetSessionId = `balance:${account.userId}`;
56
+ return {
57
+ structuredContent: {
58
+ balanceCredits: account.balanceCredits,
59
+ hasPaymentMethod: account.hasPaymentMethod
60
+ },
61
+ content: [{ type: 'text', text: `Current Balance: ${account.balanceCredits} credits.${account.hasPaymentMethod ? '' : ' (No payment method added)'}` }],
62
+ _meta: { ...toolInvocationMeta('Checking balance', 'Balance loaded', widgetSessionId), account }
63
+ };
64
+ }
65
+ case 'transaction_history': {
66
+ // Placeholder for future implementation
67
+ return {
68
+ content: [{ type: 'text', text: "Transaction history is not yet available via MCP." }],
69
+ isError: false
70
+ };
71
+ }
72
+ default:
73
+ throw new Error(`Unknown action: ${args.action}`);
74
+ }
60
75
  }
61
76
  catch (err) {
62
77
  return {
63
- content: [
64
- {
65
- type: 'text',
66
- text: `Error fetching account: ${err.response?.data?.error?.message || err.message || 'Unknown error'}`,
67
- },
68
- ],
78
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
69
79
  isError: true,
70
80
  };
71
81
  }
72
82
  };
73
- // Register as both naming conventions for compatibility
74
- for (const toolName of ['get_account', 'account_get']) {
75
- server.registerTool(toolName, {
76
- title: 'Get account details',
77
- description: 'Get your Haptic Paper account information including display name, email, and credit balance. Use this to check how much credit is available for creating tasks.',
78
- inputSchema: GetAccountSchema,
79
- annotations: {
80
- readOnlyHint: true,
81
- openWorldHint: false,
82
- destructiveHint: false,
83
- },
84
- _meta: toolDescriptorMeta(getAccountInvoking, getAccountInvoked, ['account:read']),
85
- }, getAccountHandler);
86
- }
83
+ server.registerTool('account', {
84
+ title: 'Manage Account',
85
+ description: 'Manage user account, profile, and credits. Actions: get_my_profile, get_balance. Use this to check who is logged in or their credit balance.',
86
+ inputSchema: AccountToolSchema,
87
+ _meta: toolDescriptorMeta('Accessing account', 'Account operation complete', ['account:read']),
88
+ }, accountHandler);
87
89
  }
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
3
+ import crypto from 'node:crypto';
4
+ // --- Shared Helpers ---
5
+ function stableSessionId(prefix, value) {
6
+ const raw = value ?? 'unknown';
7
+ const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
8
+ return `${prefix}:${hash}`;
9
+ }
10
+ function toolDescriptorMeta(invoking, invoked, scopes = []) {
11
+ return {
12
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
13
+ 'openai/toolInvocation/invoking': invoking,
14
+ 'openai/toolInvocation/invoked': invoked,
15
+ 'openai/widgetAccessible': true,
16
+ ...(scopes.length > 0 ? {
17
+ securitySchemes: [{ type: 'oauth2', scopes }],
18
+ } : {}),
19
+ };
20
+ }
21
+ function toolInvocationMeta(invoking, invoked, widgetSessionId) {
22
+ return {
23
+ 'openai/toolInvocation/invoking': invoking,
24
+ 'openai/toolInvocation/invoked': invoked,
25
+ 'openai/widgetSessionId': widgetSessionId,
26
+ };
27
+ }
28
+ // --- Schema Definitions ---
29
+ const IntentSchema = z.object({
30
+ message: z.string().describe("The user's message expressing interest, a skill, or a potential need."),
31
+ context: z.array(z.object({
32
+ role: z.enum(['user', 'assistant']),
33
+ content: z.string(),
34
+ })).optional().describe("Previous conversation context to help understand the intent."),
35
+ sessionId: z.string().uuid().optional().describe("If continuing an existing qualification/intent session."),
36
+ });
37
+ const HapticToolSchema = z.object({
38
+ action: z.enum(['intent', 'search_docs']).describe("Action to perform."),
39
+ // Intent params
40
+ message: IntentSchema.shape.message.optional(),
41
+ context: IntentSchema.shape.context.optional(),
42
+ sessionId: IntentSchema.shape.sessionId.optional(),
43
+ // Search docs params
44
+ query: z.string().optional().describe("Query for searching documentation (not yet implemented)"),
45
+ }).describe("General purpose helper for Haptic Paper. Actions: intent (Analyze user intent/start work), search_docs (Find help).");
46
+ export function registerGeneralTools(server, client) {
47
+ const hapticHandler = async (args, _extra) => {
48
+ try {
49
+ switch (args.action) {
50
+ case 'intent': {
51
+ if (!args.message)
52
+ throw new Error("Missing message for intent action");
53
+ // If we have a sessionId, it's likely a continue
54
+ if (args.sessionId) {
55
+ const result = await client.continueQualification(args.sessionId, args.message);
56
+ const widgetSessionId = stableSessionId('qual', args.sessionId);
57
+ return {
58
+ content: [{ type: 'text', text: result.nextPrompt?.text || "Processed." }],
59
+ structuredContent: result, // Pass full result
60
+ _meta: { ...toolInvocationMeta('Processing...', 'Done', widgetSessionId), result }
61
+ };
62
+ }
63
+ // Otherwise it's a discovery/start
64
+ const result = await client.discoverEarningOpportunity({
65
+ userMessage: args.message,
66
+ conversationContext: args.context
67
+ });
68
+ const widgetSessionId = stableSessionId('qual', result.sessionId);
69
+ return {
70
+ content: [{ type: 'text', text: result.nextPrompt?.text || "Started qualification." }],
71
+ structuredContent: result,
72
+ _meta: { ...toolInvocationMeta('Starting...', 'Started', widgetSessionId), result }
73
+ };
74
+ }
75
+ case 'search_docs': {
76
+ return {
77
+ content: [{ type: 'text', text: "Documentation search is not yet implemented. Please refer to standard Haptic Paper formatting guides." }],
78
+ isError: false
79
+ };
80
+ }
81
+ default:
82
+ throw new Error(`Unknown action: ${args.action}`);
83
+ }
84
+ }
85
+ catch (err) {
86
+ return {
87
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
88
+ isError: true,
89
+ };
90
+ }
91
+ };
92
+ server.registerTool('haptic', {
93
+ title: 'Haptic Helper',
94
+ description: 'General purpose helper for Haptic Paper. Actions: intent, search_docs. Use `intent` when the user makes a vague request or wants to start working ("I want to work").',
95
+ inputSchema: HapticToolSchema,
96
+ _meta: toolDescriptorMeta('Analyzing...', 'Analysis complete'),
97
+ }, hapticHandler);
98
+ }
@@ -1,4 +1,12 @@
1
- import { registerVerbTools } from "./verbs.js";
1
+ import { registerTasksTools } from "./tasks.js";
2
+ import { registerWorkerTools } from "./workers.js";
3
+ import { registerAccountTools } from "./account.js";
4
+ import { registerQualificationTools } from "./qualification.js";
5
+ import { registerGeneralTools } from "./general.js";
2
6
  export function registerAllTools(server, client) {
3
- registerVerbTools(server, client);
7
+ registerTasksTools(server, client);
8
+ registerWorkerTools(server, client);
9
+ registerAccountTools(server, client);
10
+ registerQualificationTools(server, client);
11
+ registerGeneralTools(server, client);
4
12
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
3
3
  import crypto from 'node:crypto';
4
+ // --- Shared Helpers ---
4
5
  function stableSessionId(prefix, value) {
5
6
  const raw = value ?? 'unknown';
6
7
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -24,7 +25,7 @@ function toolInvocationMeta(invoking, invoked, widgetSessionId) {
24
25
  'openai/widgetSessionId': widgetSessionId,
25
26
  };
26
27
  }
27
- // Input schema for discover_earning_opportunity
28
+ // --- Schema Definitions ---
28
29
  const DiscoverEarningSchema = z.object({
29
30
  userMessage: z.string().describe("The user's message expressing interest in earning"),
30
31
  conversationContext: z.array(z.object({
@@ -32,223 +33,86 @@ const DiscoverEarningSchema = z.object({
32
33
  content: z.string(),
33
34
  })).optional().describe("Previous conversation for context"),
34
35
  });
35
- // Input schema for continue_qualification
36
36
  const ContinueQualificationSchema = z.object({
37
37
  sessionId: z.string().uuid().describe("The qualification session ID"),
38
38
  userResponse: z.string().describe("The user's response to the qualification question"),
39
39
  });
40
- // Input schema for get_qualification_status
41
- const GetQualificationStatusSchema = z.object({
42
- sessionId: z.string().uuid().describe("The qualification session ID"),
43
- });
44
- // Input schema for complete_qualification
45
- const CompleteQualificationSchema = z.object({
46
- sessionId: z.string().uuid().describe("The qualification session ID"),
47
- });
40
+ const QualificationsToolSchema = z.object({
41
+ action: z.enum(['start', 'continue', 'check_status']).describe("Action to perform on the qualification session"),
42
+ // Start params
43
+ userMessage: DiscoverEarningSchema.shape.userMessage.optional(),
44
+ conversationContext: DiscoverEarningSchema.shape.conversationContext.optional(),
45
+ // Continue params
46
+ sessionId: z.string().uuid().optional().describe("Session ID for continue or check_status actions"),
47
+ userResponse: ContinueQualificationSchema.shape.userResponse.optional(),
48
+ }).describe("Handle the conversational onboarding/qualification flow. Actions: start, continue, check_status.");
48
49
  export function registerQualificationTools(server, client) {
49
- // ==========================================
50
- // discover_earning_opportunity
51
- // ==========================================
52
- const discoverInvoking = 'Setting up your earning profile...';
53
- const discoverInvoked = 'Profile setup ready!';
54
- const discoverHandler = async (args, _extra) => {
50
+ const qualificationsHandler = async (args, _extra) => {
55
51
  try {
56
- // Detect intent and start qualification session
57
- const result = await client.discoverEarningOpportunity({
58
- userMessage: args.userMessage,
59
- conversationContext: args.conversationContext,
60
- });
61
- const widgetSessionId = stableSessionId('qual', result.sessionId);
62
- return {
63
- structuredContent: {
64
- sessionId: result.sessionId,
65
- intentDetected: result.intentDetected,
66
- intentType: result.intentType,
67
- confidenceScore: result.confidenceScore,
68
- nextQuestion: result.nextPrompt?.text,
69
- },
70
- content: [
71
- {
72
- type: 'text',
73
- text: result.nextPrompt?.text ?? 'Let me help you get started with earning opportunities!',
74
- },
75
- ],
76
- _meta: {
77
- ...toolInvocationMeta(discoverInvoking, discoverInvoked, widgetSessionId),
78
- result,
79
- },
80
- };
81
- }
82
- catch (err) {
83
- return {
84
- content: [{ type: 'text', text: `Error: ${err.message}` }],
85
- isError: true,
86
- };
87
- }
88
- };
89
- // Register with multiple names for compatibility
90
- for (const toolName of ['discover_earning_opportunity', 'qualification_start']) {
91
- server.registerTool(toolName, {
92
- title: 'Discover earning opportunities',
93
- description: 'Detect when a user expresses interest in earning money and start a conversational qualification flow to build their profile. No OAuth required - works for new users.',
94
- inputSchema: DiscoverEarningSchema,
95
- annotations: {
96
- readOnlyHint: false,
97
- openWorldHint: false,
98
- destructiveHint: false,
99
- },
100
- _meta: toolDescriptorMeta(discoverInvoking, discoverInvoked),
101
- }, discoverHandler);
102
- }
103
- // ==========================================
104
- // continue_qualification
105
- // ==========================================
106
- const continueInvoking = 'Processing your response...';
107
- const continueInvoked = 'Ready for next step';
108
- const continueHandler = async (args, _extra) => {
109
- try {
110
- const result = await client.continueQualification(args.sessionId, args.userResponse);
111
- const widgetSessionId = stableSessionId('qual', args.sessionId);
112
- // Check if qualification is complete
113
- if (result.status === 'completed' || result.completionScore >= 0.75) {
114
- return {
115
- structuredContent: {
116
- sessionId: args.sessionId,
117
- status: 'completed',
118
- completionScore: result.completionScore,
119
- profileSummary: result.profileSummary,
120
- },
121
- content: [
122
- {
123
- type: 'text',
124
- text: result.nextPrompt?.text ?? `Great! Your profile is now ${Math.round(result.completionScore * 100)}% complete. You're all set to start receiving task opportunities!`,
52
+ switch (args.action) {
53
+ case 'start': {
54
+ if (!args.userMessage)
55
+ throw new Error("Missing userMessage for start action");
56
+ // Detect intent and start qualification session
57
+ const result = await client.discoverEarningOpportunity({
58
+ userMessage: args.userMessage,
59
+ conversationContext: args.conversationContext,
60
+ });
61
+ const widgetSessionId = stableSessionId('qual', result.sessionId);
62
+ return {
63
+ structuredContent: {
64
+ sessionId: result.sessionId,
65
+ intentDetected: result.intentDetected,
66
+ intentType: result.intentType,
67
+ confidenceScore: result.confidenceScore,
68
+ nextQuestion: result.nextPrompt?.text,
69
+ },
70
+ content: [{ type: 'text', text: result.nextPrompt?.text ?? 'Let me help you get started with earning opportunities!' }],
71
+ _meta: { ...toolInvocationMeta('Starting qualification', 'Profile setup ready!', widgetSessionId), result }
72
+ };
73
+ }
74
+ case 'continue': {
75
+ if (!args.sessionId || !args.userResponse)
76
+ throw new Error("Missing sessionId or userResponse for continue action");
77
+ const result = await client.continueQualification(args.sessionId, args.userResponse);
78
+ const widgetSessionId = stableSessionId('qual', args.sessionId);
79
+ // Check if qualification is complete
80
+ if (result.status === 'completed' || result.completionScore >= 0.75) {
81
+ return {
82
+ structuredContent: {
83
+ sessionId: args.sessionId, status: 'completed', completionScore: result.completionScore, profileSummary: result.profileSummary,
84
+ },
85
+ content: [{ type: 'text', text: result.nextPrompt?.text ?? `Great! Your profile is now ${Math.round(result.completionScore * 100)}% complete. You're all set to start receiving task opportunities!` }],
86
+ _meta: { ...toolInvocationMeta('Processing response', 'Ready for next step', widgetSessionId), result }
87
+ };
88
+ }
89
+ return {
90
+ structuredContent: {
91
+ sessionId: args.sessionId, status: result.status, currentStage: result.currentStage,
92
+ completionScore: result.completionScore, questionCount: result.questionCount, nextQuestion: result.nextPrompt?.text,
125
93
  },
126
- ],
127
- _meta: {
128
- ...toolInvocationMeta(continueInvoking, continueInvoked, widgetSessionId),
129
- result,
130
- },
131
- };
94
+ content: [{ type: 'text', text: result.nextPrompt?.text ?? 'Thanks! What else would you like to share?' }],
95
+ _meta: { ...toolInvocationMeta('Processing response', 'Ready for next step', widgetSessionId), result }
96
+ };
97
+ }
98
+ case 'check_status': {
99
+ if (!args.sessionId)
100
+ throw new Error("Missing sessionId for check_status action");
101
+ const result = await client.getQualificationStatus(args.sessionId);
102
+ const widgetSessionId = stableSessionId('qual', args.sessionId);
103
+ return {
104
+ structuredContent: {
105
+ sessionId: args.sessionId, status: result.status, currentStage: result.currentStage,
106
+ completionScore: result.completionScore, questionCount: result.questionCount,
107
+ extractedSkills: result.extractedSkills, extractedLocation: result.extractedLocation,
108
+ },
109
+ content: [{ type: 'text', text: `Profile is ${Math.round(result.completionScore * 100)}% complete. Current stage: ${result.currentStage}` }],
110
+ _meta: { ...toolInvocationMeta('Checking status', 'Status retrieved', widgetSessionId), result }
111
+ };
112
+ }
113
+ default:
114
+ throw new Error(`Unknown action: ${args.action}`);
132
115
  }
133
- return {
134
- structuredContent: {
135
- sessionId: args.sessionId,
136
- status: result.status,
137
- currentStage: result.currentStage,
138
- completionScore: result.completionScore,
139
- questionCount: result.questionCount,
140
- nextQuestion: result.nextPrompt?.text,
141
- },
142
- content: [
143
- {
144
- type: 'text',
145
- text: result.nextPrompt?.text ?? 'Thanks! What else would you like to share?',
146
- },
147
- ],
148
- _meta: {
149
- ...toolInvocationMeta(continueInvoking, continueInvoked, widgetSessionId),
150
- result,
151
- },
152
- };
153
- }
154
- catch (err) {
155
- return {
156
- content: [{ type: 'text', text: `Error: ${err.message}` }],
157
- isError: true,
158
- };
159
- }
160
- };
161
- for (const toolName of ['continue_qualification', 'qualification_continue']) {
162
- server.registerTool(toolName, {
163
- title: 'Continue qualification',
164
- description: 'Submit a user response and get the next qualification question. Use this to continue the conversational profile-building flow.',
165
- inputSchema: ContinueQualificationSchema,
166
- annotations: {
167
- readOnlyHint: false,
168
- openWorldHint: false,
169
- destructiveHint: false,
170
- },
171
- _meta: toolDescriptorMeta(continueInvoking, continueInvoked),
172
- }, continueHandler);
173
- }
174
- // ==========================================
175
- // get_qualification_status
176
- // ==========================================
177
- const statusInvoking = 'Checking qualification progress...';
178
- const statusInvoked = 'Status retrieved';
179
- const statusHandler = async (args, _extra) => {
180
- try {
181
- const result = await client.getQualificationStatus(args.sessionId);
182
- const widgetSessionId = stableSessionId('qual', args.sessionId);
183
- return {
184
- structuredContent: {
185
- sessionId: args.sessionId,
186
- status: result.status,
187
- currentStage: result.currentStage,
188
- completionScore: result.completionScore,
189
- questionCount: result.questionCount,
190
- extractedSkills: result.extractedSkills,
191
- extractedLocation: result.extractedLocation,
192
- },
193
- content: [
194
- {
195
- type: 'text',
196
- text: `Profile is ${Math.round(result.completionScore * 100)}% complete. Current stage: ${result.currentStage}`,
197
- },
198
- ],
199
- _meta: {
200
- ...toolInvocationMeta(statusInvoking, statusInvoked, widgetSessionId),
201
- result,
202
- },
203
- };
204
- }
205
- catch (err) {
206
- return {
207
- content: [{ type: 'text', text: `Error: ${err.message}` }],
208
- isError: true,
209
- };
210
- }
211
- };
212
- for (const toolName of ['get_qualification_status', 'qualification_status']) {
213
- server.registerTool(toolName, {
214
- title: 'Get qualification status',
215
- description: 'Check the progress of a qualification session including completion score and extracted profile data.',
216
- inputSchema: GetQualificationStatusSchema,
217
- annotations: {
218
- readOnlyHint: true,
219
- openWorldHint: false,
220
- destructiveHint: false,
221
- },
222
- _meta: toolDescriptorMeta(statusInvoking, statusInvoked),
223
- }, statusHandler);
224
- }
225
- // ==========================================
226
- // complete_qualification
227
- // ==========================================
228
- const completeInvoking = 'Finalizing your profile...';
229
- const completeInvoked = 'Profile complete!';
230
- const completeHandler = async (args, _extra) => {
231
- try {
232
- const result = await client.completeQualification(args.sessionId);
233
- const widgetSessionId = stableSessionId('qual', args.sessionId);
234
- return {
235
- structuredContent: {
236
- sessionId: args.sessionId,
237
- status: 'completed',
238
- profileId: result.workerProfileId,
239
- completionScore: result.completionScore,
240
- },
241
- content: [
242
- {
243
- type: 'text',
244
- text: `🎉 Your profile is complete! You'll now start seeing task opportunities that match your skills. Welcome to Haptic!`,
245
- },
246
- ],
247
- _meta: {
248
- ...toolInvocationMeta(completeInvoking, completeInvoked, widgetSessionId),
249
- result,
250
- },
251
- };
252
116
  }
253
117
  catch (err) {
254
118
  return {
@@ -257,17 +121,10 @@ export function registerQualificationTools(server, client) {
257
121
  };
258
122
  }
259
123
  };
260
- for (const toolName of ['complete_qualification', 'qualification_complete']) {
261
- server.registerTool(toolName, {
262
- title: 'Complete qualification',
263
- description: 'Finalize a qualification session and activate the worker profile.',
264
- inputSchema: CompleteQualificationSchema,
265
- annotations: {
266
- readOnlyHint: false,
267
- openWorldHint: false,
268
- destructiveHint: false,
269
- },
270
- _meta: toolDescriptorMeta(completeInvoking, completeInvoked),
271
- }, completeHandler);
272
- }
124
+ server.registerTool('qualifications', {
125
+ title: 'Qualification Flow',
126
+ description: 'Handle the conversational onboarding/qualification flow. Actions: start, continue, check_status. Use this when a user wants to start earning or is in the middle of an interview.',
127
+ inputSchema: QualificationsToolSchema,
128
+ _meta: toolDescriptorMeta('Qualification', 'Qualification step complete'),
129
+ }, qualificationsHandler);
273
130
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
3
  import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
4
4
  import crypto from 'node:crypto';
5
+ // --- Shared Helpers ---
5
6
  function stableSessionId(prefix, value) {
6
7
  const raw = value ?? 'unknown';
7
8
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -9,10 +10,7 @@ function stableSessionId(prefix, value) {
9
10
  }
10
11
  function oauthSecuritySchemes(scopes) {
11
12
  return [
12
- {
13
- type: 'oauth2',
14
- scopes,
15
- },
13
+ { type: 'oauth2', scopes },
16
14
  ];
17
15
  }
18
16
  function toolDescriptorMeta(invoking, invoked, scopes) {
@@ -31,6 +29,7 @@ function toolInvocationMeta(invoking, invoked, widgetSessionId) {
31
29
  'openai/widgetSessionId': widgetSessionId,
32
30
  };
33
31
  }
32
+ // --- Schema Definitions ---
34
33
  const CreateTaskSchema = z.object({
35
34
  title: z.string().min(5).max(200).describe("Short title for the task"),
36
35
  description: z.string().min(20).max(5000).describe("Detailed description of what needs to be done"),
@@ -44,227 +43,122 @@ const CreateTaskSchema = z.object({
44
43
  deadline: z.string().datetime().optional().describe("When the task must be completed by"),
45
44
  requirements: z.object({
46
45
  proofOfWork: z.boolean().default(true),
47
- gpsRequired: z.boolean().default(true)
46
+ gpsRequired: z.boolean().default(true),
47
+ skills: z.array(z.string()).optional(),
48
48
  }).optional(),
49
+ entityId: z.string().uuid().optional().describe("Entity ID to creating/billing this task"),
49
50
  });
50
- export function registerTaskTools(server, client) {
51
- const createTaskInvoking = 'Creating your task';
52
- const createTaskInvoked = 'Task created';
53
- const createTaskHandler = async (args, extra) => {
54
- try {
55
- const auth = requireScopes(extra, ['tasks:write']);
56
- const task = await client.createTask(args, auth?.token);
57
- const widgetSessionId = `task:${task.id}`;
58
- return {
59
- structuredContent: {
60
- task: {
61
- id: task.id,
62
- title: task.title,
63
- status: task.status,
64
- budget: task.budget,
65
- },
66
- },
67
- content: [{ type: 'text', text: `Created task “${task.title}” (ID: ${task.id}).` }],
68
- _meta: {
69
- ...toolInvocationMeta(createTaskInvoking, createTaskInvoked, widgetSessionId),
70
- task,
71
- },
72
- };
73
- }
74
- catch (err) {
75
- return {
76
- content: [
77
- {
78
- type: 'text',
79
- text: `Error creating task: ${err.response?.data?.error?.message || err.message}`,
80
- },
81
- ],
82
- isError: true,
83
- };
84
- }
85
- };
86
- for (const toolName of ['create_task', 'tasks_create']) {
87
- server.registerTool(toolName, {
88
- title: 'Create a task',
89
- description: 'Create a new task for a human worker. This is a write action and may charge the user.',
90
- inputSchema: CreateTaskSchema,
91
- annotations: {
92
- readOnlyHint: false,
93
- openWorldHint: false,
94
- destructiveHint: false,
95
- },
96
- _meta: toolDescriptorMeta(createTaskInvoking, createTaskInvoked, ['tasks:write']),
97
- }, createTaskHandler);
98
- }
99
- const getTaskInvoking = 'Fetching task details';
100
- const getTaskInvoked = 'Task details ready';
101
- const getTaskHandler = async (args, extra) => {
102
- try {
103
- const auth = requireScopes(extra, ['tasks:read']);
104
- const task = await client.getTask(args.taskId, auth?.token);
105
- const widgetSessionId = `task:${args.taskId}`;
106
- return {
107
- structuredContent: {
108
- task: {
109
- id: task.id,
110
- title: task.title,
111
- status: task.status,
112
- budget: task.budget,
113
- },
114
- },
115
- content: [{ type: 'text', text: `Task ${task.id}: ${task.status}` }],
116
- _meta: {
117
- ...toolInvocationMeta(getTaskInvoking, getTaskInvoked, widgetSessionId),
118
- task,
119
- },
120
- };
121
- }
122
- catch (err) {
123
- return {
124
- content: [{ type: 'text', text: `Error: ${err.message}` }],
125
- isError: true,
126
- };
127
- }
128
- };
129
- for (const toolName of ['get_task', 'tasks_get']) {
130
- server.registerTool(toolName, {
131
- title: 'Get task',
132
- description: 'Get details of a task by ID.',
133
- inputSchema: z.object({ taskId: z.string().uuid() }),
134
- annotations: {
135
- readOnlyHint: true,
136
- openWorldHint: false,
137
- destructiveHint: false,
138
- },
139
- _meta: toolDescriptorMeta(getTaskInvoking, getTaskInvoked, ['tasks:read']),
140
- }, getTaskHandler);
141
- }
142
- const listTasksInvoking = 'Loading your tasks';
143
- const listTasksInvoked = 'Tasks loaded';
144
- const listTasksHandler = async (args, extra) => {
51
+ const UpdateTaskSchema = CreateTaskSchema.partial().extend({
52
+ taskId: z.string().uuid().describe("ID of the task to update"),
53
+ });
54
+ const TasksToolSchema = z.object({
55
+ action: z.enum(['create', 'get', 'list', 'update', 'cancel']).describe("Action to perform on the tasks resource"),
56
+ // Create params
57
+ title: CreateTaskSchema.shape.title.optional(),
58
+ description: CreateTaskSchema.shape.description.optional(),
59
+ budget: CreateTaskSchema.shape.budget.optional(),
60
+ location: CreateTaskSchema.shape.location.optional(),
61
+ deadline: CreateTaskSchema.shape.deadline.optional(),
62
+ requirements: CreateTaskSchema.shape.requirements.optional(),
63
+ entityId: CreateTaskSchema.shape.entityId.optional(),
64
+ // Get/Update/Cancel params
65
+ taskId: z.string().uuid().optional().describe("Task ID for get, update, or cancel actions"),
66
+ reason: z.string().optional().describe("Reason for cancellation"),
67
+ // List params
68
+ status: z.enum(['open', 'assigned', 'completed', 'cancelled']).optional().describe("Filter by status for list action"),
69
+ limit: z.number().min(1).max(50).optional().describe("Limit number of tasks returned"),
70
+ }).describe("Manage tasks lifecycle: create, read, update, cancel. Actions determine required fields.");
71
+ export function registerTasksTools(server, client) {
72
+ // Helper for structured dispatch
73
+ const tasksHandler = async (args, extra) => {
145
74
  try {
146
- const auth = requireScopes(extra, ['tasks:read']);
147
- const tasks = await client.listTasks(args, auth?.token);
148
- const items = Array.isArray(tasks) ? tasks : tasks?.tasks ?? [];
149
- const widgetSessionId = stableSessionId('tasks', auth?.userId ?? auth?.clientId);
150
- if (items.length === 0) {
151
- return {
152
- structuredContent: { tasks: [] },
153
- content: [{ type: 'text', text: 'No tasks found.' }],
154
- _meta: {
155
- ...toolInvocationMeta(listTasksInvoking, listTasksInvoked, widgetSessionId),
156
- tasks: items,
157
- },
158
- };
75
+ switch (args.action) {
76
+ case 'create': {
77
+ const auth = requireScopes(extra, ['tasks:write']);
78
+ // Validate create params manually or rely on backend validation
79
+ if (!args.title || !args.description || !args.budget) {
80
+ throw new Error("Missing required fields for create (title, description, budget)");
81
+ }
82
+ const payload = {
83
+ title: args.title,
84
+ description: args.description,
85
+ budget: args.budget,
86
+ location: args.location,
87
+ deadline: args.deadline,
88
+ requirements: args.requirements,
89
+ entityId: args.entityId
90
+ };
91
+ const task = await client.createTask(payload, auth?.token);
92
+ const widgetSessionId = `task:${task.id}`;
93
+ return {
94
+ structuredContent: { task },
95
+ content: [{ type: 'text', text: `Created task "${task.title}" (ID: ${task.id}).` }],
96
+ _meta: { ...toolInvocationMeta('Creating task', 'Task created', widgetSessionId), task }
97
+ };
98
+ }
99
+ case 'get': {
100
+ const auth = requireScopes(extra, ['tasks:read']);
101
+ if (!args.taskId)
102
+ throw new Error("Missing taskId for get action");
103
+ const task = await client.getTask(args.taskId, auth?.token);
104
+ const widgetSessionId = `task:${task.id}`;
105
+ return {
106
+ structuredContent: { task },
107
+ content: [{ type: 'text', text: `Task ${task.id}: ${task.status}` }],
108
+ _meta: { ...toolInvocationMeta('Fetching task', 'Task details ready', widgetSessionId), task }
109
+ };
110
+ }
111
+ case 'list': {
112
+ const auth = requireScopes(extra, ['tasks:read']);
113
+ const tasks = await client.listTasks({ status: args.status, limit: args.limit }, auth?.token);
114
+ const items = Array.isArray(tasks) ? tasks : tasks?.tasks ?? [];
115
+ const widgetSessionId = stableSessionId('tasks', auth?.userId ?? auth?.clientId);
116
+ const summary = items.length ? items.map((t) => `- [${t.status}] ${t.title} ($${t.budget}) (ID: ${t.id})`).join('\n') : "No tasks found.";
117
+ return {
118
+ structuredContent: { tasks: items },
119
+ content: [{ type: 'text', text: `My tasks:\n${summary}` }],
120
+ _meta: { ...toolInvocationMeta('Listing tasks', 'Tasks loaded', widgetSessionId), tasks: items }
121
+ };
122
+ }
123
+ case 'update': {
124
+ const auth = requireScopes(extra, ['tasks:write']);
125
+ if (!args.taskId)
126
+ throw new Error("Missing taskId for update action");
127
+ // Construct update payload from optional args
128
+ const updatePayload = {};
129
+ if (args.title)
130
+ updatePayload.title = args.title;
131
+ if (args.description)
132
+ updatePayload.description = args.description;
133
+ if (args.budget)
134
+ updatePayload.budget = args.budget;
135
+ if (args.deadline)
136
+ updatePayload.deadline = args.deadline;
137
+ if (args.requirements)
138
+ updatePayload.requirements = args.requirements;
139
+ const task = await client.updateTask(args.taskId, updatePayload, auth?.token);
140
+ const widgetSessionId = `task:${args.taskId}`;
141
+ return {
142
+ structuredContent: { task },
143
+ content: [{ type: 'text', text: `Updated task ${task.id}.` }],
144
+ _meta: { ...toolInvocationMeta('Updating task', 'Task updated', widgetSessionId), task }
145
+ };
146
+ }
147
+ case 'cancel': {
148
+ const auth = requireScopes(extra, ['tasks:write']);
149
+ if (!args.taskId)
150
+ throw new Error("Missing taskId for cancel action");
151
+ const res = await client.cancelTask(args.taskId, args.reason, auth?.token);
152
+ const widgetSessionId = `task:${args.taskId}`;
153
+ return {
154
+ structuredContent: { taskId: args.taskId, refunded: Boolean(res?.refunded) },
155
+ content: [{ type: 'text', text: `Cancelled task ${args.taskId}. Refunded: ${res?.refunded ? 'Yes' : 'No'}` }],
156
+ _meta: { ...toolInvocationMeta('Cancelling task', 'Task cancelled', widgetSessionId), result: res }
157
+ };
158
+ }
159
+ default:
160
+ throw new Error(`Unknown action: ${args.action}`);
159
161
  }
160
- const summary = items
161
- .map((t) => `- [${t.status}] ${t.title} ($${t.budget}) (ID: ${t.id})`)
162
- .join('\n');
163
- return {
164
- structuredContent: {
165
- tasks: items.map((t) => ({
166
- id: t.id,
167
- title: t.title,
168
- status: t.status,
169
- budget: t.budget,
170
- })),
171
- },
172
- content: [{ type: 'text', text: `My tasks:\n${summary}` }],
173
- _meta: {
174
- ...toolInvocationMeta(listTasksInvoking, listTasksInvoked, widgetSessionId),
175
- tasks: items,
176
- },
177
- };
178
- }
179
- catch (err) {
180
- return {
181
- content: [{ type: 'text', text: `Error: ${err.message}` }],
182
- isError: true,
183
- };
184
- }
185
- };
186
- for (const toolName of ['list_my_tasks', 'tasks_list']) {
187
- server.registerTool(toolName, {
188
- title: 'List my tasks',
189
- description: 'List tasks created by the current user.',
190
- inputSchema: z.object({
191
- status: z.enum(['open', 'assigned', 'completed', 'cancelled']).optional(),
192
- limit: z.number().min(1).max(50).optional(),
193
- }),
194
- annotations: {
195
- readOnlyHint: true,
196
- openWorldHint: false,
197
- destructiveHint: false,
198
- },
199
- _meta: toolDescriptorMeta(listTasksInvoking, listTasksInvoked, ['tasks:read']),
200
- }, listTasksHandler);
201
- }
202
- const cancelTaskInvoking = 'Cancelling task';
203
- const cancelTaskInvoked = 'Task cancelled';
204
- const cancelTaskHandler = async (args, extra) => {
205
- try {
206
- const auth = requireScopes(extra, ['tasks:write']);
207
- const res = await client.cancelTask(args.taskId, args.reason, auth?.token);
208
- const widgetSessionId = `task:${args.taskId}`;
209
- return {
210
- structuredContent: {
211
- taskId: args.taskId,
212
- refunded: Boolean(res?.refunded),
213
- },
214
- content: [
215
- {
216
- type: 'text',
217
- text: `Cancelled task ${args.taskId}. Refunded: ${res?.refunded ? 'Yes' : 'No'}`,
218
- },
219
- ],
220
- _meta: {
221
- ...toolInvocationMeta(cancelTaskInvoking, cancelTaskInvoked, widgetSessionId),
222
- result: res,
223
- },
224
- };
225
- }
226
- catch (err) {
227
- return {
228
- content: [{ type: 'text', text: `Error: ${err.message}` }],
229
- isError: true,
230
- };
231
- }
232
- };
233
- for (const toolName of ['cancel_task', 'tasks_cancel']) {
234
- server.registerTool(toolName, {
235
- title: 'Cancel task',
236
- description: 'Cancel a pending task. This is a write action and may have irreversible side effects.',
237
- inputSchema: z.object({ taskId: z.string().uuid(), reason: z.string().optional() }),
238
- annotations: {
239
- readOnlyHint: false,
240
- openWorldHint: false,
241
- destructiveHint: true,
242
- },
243
- _meta: toolDescriptorMeta(cancelTaskInvoking, cancelTaskInvoked, ['tasks:write']),
244
- }, cancelTaskHandler);
245
- }
246
- const updateTaskInvoking = 'Updating task';
247
- const updateTaskInvoked = 'Task updated';
248
- const updateTaskHandler = async (args, extra) => {
249
- try {
250
- const auth = requireScopes(extra, ['tasks:write']);
251
- const task = await client.updateTask(args.taskId, args, auth?.token);
252
- const widgetSessionId = `task:${args.taskId}`;
253
- return {
254
- structuredContent: {
255
- task: {
256
- id: task.id,
257
- title: task.title,
258
- status: task.status,
259
- budget: task.budget,
260
- },
261
- },
262
- content: [{ type: 'text', text: `Updated task ${task.id}.` }],
263
- _meta: {
264
- ...toolInvocationMeta(updateTaskInvoking, updateTaskInvoked, widgetSessionId),
265
- task,
266
- },
267
- };
268
162
  }
269
163
  catch (err) {
270
164
  return {
@@ -273,17 +167,10 @@ export function registerTaskTools(server, client) {
273
167
  };
274
168
  }
275
169
  };
276
- for (const toolName of ['update_task', 'tasks_update']) {
277
- server.registerTool(toolName, {
278
- title: 'Update task',
279
- description: 'Update an existing task.',
280
- inputSchema: CreateTaskSchema.partial().extend({ taskId: z.string().uuid() }),
281
- annotations: {
282
- readOnlyHint: false,
283
- openWorldHint: false,
284
- destructiveHint: false, // Generally safe to update
285
- },
286
- _meta: toolDescriptorMeta(updateTaskInvoking, updateTaskInvoked, ['tasks:write']),
287
- }, updateTaskHandler);
288
- }
170
+ server.registerTool('tasks', {
171
+ title: 'Manage Tasks',
172
+ description: 'Manage tasks lifecycle. Actions: create, get, list, update, cancel. Use this tool for all task-related operations.',
173
+ inputSchema: TasksToolSchema,
174
+ _meta: toolDescriptorMeta('Managing tasks', 'Task operation complete', ['tasks:read', 'tasks:write']),
175
+ }, tasksHandler);
289
176
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
3
  import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
4
4
  import crypto from 'node:crypto';
5
+ // --- Shared Helpers ---
5
6
  function stableSessionId(prefix, value) {
6
7
  const raw = value ?? 'unknown';
7
8
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -9,10 +10,7 @@ function stableSessionId(prefix, value) {
9
10
  }
10
11
  function oauthSecuritySchemes(scopes) {
11
12
  return [
12
- {
13
- type: 'oauth2',
14
- scopes,
15
- },
13
+ { type: 'oauth2', scopes },
16
14
  ];
17
15
  }
18
16
  function toolDescriptorMeta(invoking, invoked, scopes) {
@@ -31,6 +29,7 @@ function toolInvocationMeta(invoking, invoked, widgetSessionId) {
31
29
  'openai/widgetSessionId': widgetSessionId,
32
30
  };
33
31
  }
32
+ // --- Schema Definitions ---
34
33
  const SearchWorkersSchema = z.object({
35
34
  taskDescription: z.string().describe("Description of what needs to be done"),
36
35
  location: z.object({
@@ -39,82 +38,63 @@ const SearchWorkersSchema = z.object({
39
38
  }).optional(),
40
39
  skills: z.array(z.string()).optional()
41
40
  });
41
+ const WorkersToolSchema = z.object({
42
+ action: z.enum(['search', 'get_profile']).describe("Action to perform on the workers resource"),
43
+ // Search params
44
+ taskDescription: SearchWorkersSchema.shape.taskDescription.optional(),
45
+ location: SearchWorkersSchema.shape.location.optional(),
46
+ skills: SearchWorkersSchema.shape.skills.optional(),
47
+ // Get params
48
+ workerId: z.string().uuid().optional().describe("Worker ID to retrieve profile"),
49
+ }).describe("Find and view worker profiles. Actions: search, get_profile.");
42
50
  export function registerWorkerTools(server, client) {
43
- const searchWorkersInvoking = 'Searching nearby workers';
44
- const searchWorkersInvoked = 'Worker results ready';
45
- const searchWorkersHandler = async (args, extra) => {
51
+ const workersHandler = async (args, extra) => {
46
52
  try {
47
- const auth = requireScopes(extra, ['workers:read']);
48
- const result = await client.searchWorkers(args, auth?.token);
49
- const widgetSessionId = stableSessionId('workers', JSON.stringify({ userId: auth?.userId ?? auth?.clientId, args }));
50
- if (!result.workers || result.workers.length === 0) {
51
- return {
52
- structuredContent: { workers: [], suggestedBudget: result?.suggestedBudget },
53
- content: [{ type: 'text', text: 'No workers found matching criteria.' }],
54
- _meta: {
55
- ...toolInvocationMeta(searchWorkersInvoking, searchWorkersInvoked, widgetSessionId),
56
- result,
57
- },
58
- };
53
+ switch (args.action) {
54
+ case 'search': {
55
+ const auth = requireScopes(extra, ['workers:read']);
56
+ if (!args.taskDescription)
57
+ throw new Error("Missing taskDescription for search action");
58
+ const searchArgs = {
59
+ taskDescription: args.taskDescription,
60
+ location: args.location,
61
+ skills: args.skills
62
+ };
63
+ const result = await client.searchWorkers(searchArgs, auth?.token);
64
+ const widgetSessionId = stableSessionId('workers', JSON.stringify({ userId: auth?.userId ?? auth?.clientId, args: searchArgs }));
65
+ if (!result.workers || result.workers.length === 0) {
66
+ return {
67
+ structuredContent: { workers: [], suggestedBudget: result?.suggestedBudget },
68
+ content: [{ type: 'text', text: 'No workers found matching criteria.' }],
69
+ _meta: { ...toolInvocationMeta('Searching workers', 'Worker results ready', widgetSessionId), result }
70
+ };
71
+ }
72
+ return {
73
+ structuredContent: {
74
+ workers: result.workers.map((w) => ({
75
+ id: w.id, name: w.name, rating: w.rating, distanceMiles: w.distanceMiles, estimatedArrival: w.estimatedArrival,
76
+ })),
77
+ suggestedBudget: result.suggestedBudget,
78
+ },
79
+ content: [{ type: 'text', text: `Found ${result.workers.length} workers. Suggested budget: $${result.suggestedBudget}` }],
80
+ _meta: { ...toolInvocationMeta('Searching workers', 'Worker results ready', widgetSessionId), result }
81
+ };
82
+ }
83
+ case 'get_profile': {
84
+ const auth = requireScopes(extra, ['workers:read']);
85
+ if (!args.workerId)
86
+ throw new Error("Missing workerId for get_profile action");
87
+ const worker = await client.getWorkerProfile(args.workerId, auth?.token);
88
+ const widgetSessionId = `worker:${args.workerId}`;
89
+ return {
90
+ structuredContent: { worker },
91
+ content: [{ type: 'text', text: `Worker profile loaded: ${worker?.id ?? args.workerId}` }],
92
+ _meta: { ...toolInvocationMeta('Loading profile', 'Worker profile ready', widgetSessionId), worker }
93
+ };
94
+ }
95
+ default:
96
+ throw new Error(`Unknown action: ${args.action}`);
59
97
  }
60
- return {
61
- structuredContent: {
62
- workers: result.workers.map((w) => ({
63
- id: w.id,
64
- name: w.name,
65
- rating: w.rating,
66
- distanceMiles: w.distanceMiles,
67
- estimatedArrival: w.estimatedArrival,
68
- })),
69
- suggestedBudget: result.suggestedBudget,
70
- },
71
- content: [
72
- {
73
- type: 'text',
74
- text: `Found ${result.workers.length} workers. Suggested budget: $${result.suggestedBudget}`,
75
- },
76
- ],
77
- _meta: {
78
- ...toolInvocationMeta(searchWorkersInvoking, searchWorkersInvoked, widgetSessionId),
79
- result,
80
- },
81
- };
82
- }
83
- catch (err) {
84
- return {
85
- content: [{ type: 'text', text: `Error: ${err.message}` }],
86
- isError: true,
87
- };
88
- }
89
- };
90
- for (const toolName of ['search_workers', 'workers_search']) {
91
- server.registerTool(toolName, {
92
- title: 'Search workers',
93
- description: 'Search for available workers. Returns anonymized profiles.',
94
- inputSchema: SearchWorkersSchema,
95
- annotations: {
96
- readOnlyHint: true,
97
- openWorldHint: false,
98
- destructiveHint: false,
99
- },
100
- _meta: toolDescriptorMeta(searchWorkersInvoking, searchWorkersInvoked, ['workers:read']),
101
- }, searchWorkersHandler);
102
- }
103
- const getWorkerInvoking = 'Loading worker profile';
104
- const getWorkerInvoked = 'Worker profile ready';
105
- const getWorkerProfileHandler = async (args, extra) => {
106
- try {
107
- const auth = requireScopes(extra, ['workers:read']);
108
- const worker = await client.getWorkerProfile(args.workerId, auth?.token);
109
- const widgetSessionId = `worker:${args.workerId}`;
110
- return {
111
- structuredContent: { worker },
112
- content: [{ type: 'text', text: `Worker profile loaded: ${worker?.id ?? args.workerId}` }],
113
- _meta: {
114
- ...toolInvocationMeta(getWorkerInvoking, getWorkerInvoked, widgetSessionId),
115
- worker,
116
- },
117
- };
118
98
  }
119
99
  catch (err) {
120
100
  return {
@@ -123,17 +103,10 @@ export function registerWorkerTools(server, client) {
123
103
  };
124
104
  }
125
105
  };
126
- for (const toolName of ['get_worker_profile', 'workers_get_profile']) {
127
- server.registerTool(toolName, {
128
- title: 'Get worker profile',
129
- description: 'Get a detailed worker profile by ID.',
130
- inputSchema: z.object({ workerId: z.string().uuid() }),
131
- annotations: {
132
- readOnlyHint: true,
133
- openWorldHint: false,
134
- destructiveHint: false,
135
- },
136
- _meta: toolDescriptorMeta(getWorkerInvoking, getWorkerInvoked, ['workers:read']),
137
- }, getWorkerProfileHandler);
138
- }
106
+ server.registerTool('workers', {
107
+ title: 'Manage Workers',
108
+ description: 'Find and view worker profiles. Actions: search, get_profile. Use this to find talent or check worker details.',
109
+ inputSchema: WorkersToolSchema,
110
+ _meta: toolDescriptorMeta('Managing workers', 'Worker operation complete', ['workers:read']),
111
+ }, workersHandler);
139
112
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hapticpaper/mcp-server",
3
3
  "mcpName": "com.hapticpaper/mcp",
4
- "version": "1.0.18",
4
+ "version": "1.0.19",
5
5
  "description": "Official MCP Server for Haptic Paper - Connect your account to create human tasks from agentic pipelines.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
package/server.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "subfolder": "packages/mcp-server"
26
26
  },
27
27
  "websiteUrl": "https://hapticpaper.com/developer",
28
- "version": "1.0.18",
28
+ "version": "1.0.19",
29
29
  "remotes": [
30
30
  {
31
31
  "type": "streamable-http",
@@ -37,7 +37,7 @@
37
37
  "registryType": "npm",
38
38
  "registryBaseUrl": "https://registry.npmjs.org",
39
39
  "identifier": "@hapticpaper/mcp-server",
40
- "version": "1.0.18",
40
+ "version": "1.0.19",
41
41
  "transport": {
42
42
  "type": "stdio"
43
43
  },