@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 +7 -9
- package/dist/tools/account.js +54 -52
- package/dist/tools/general.js +98 -0
- package/dist/tools/index.js +10 -2
- package/dist/tools/qualification.js +80 -223
- package/dist/tools/tasks.js +121 -234
- package/dist/tools/workers.js +63 -90
- package/package.json +1 -1
- package/server.json +2 -2
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
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
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
|
|
package/dist/tools/account.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sessionId: z.string().uuid().describe("
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
}
|
package/dist/tools/tasks.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
}
|
package/dist/tools/workers.js
CHANGED
|
@@ -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
|
|
44
|
-
const searchWorkersInvoked = 'Worker results ready';
|
|
45
|
-
const searchWorkersHandler = async (args, extra) => {
|
|
51
|
+
const workersHandler = async (args, extra) => {
|
|
46
52
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
40
|
+
"version": "1.0.19",
|
|
41
41
|
"transport": {
|
|
42
42
|
"type": "stdio"
|
|
43
43
|
},
|