@hapticpaper/mcp-server 1.0.3 → 1.0.5
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/dist/auth/access.js +31 -8
- package/dist/index.js +7 -1
- package/dist/tools/account.js +2 -1
- package/dist/tools/estimates.js +3 -3
- package/dist/tools/tasks.js +5 -5
- package/dist/tools/workers.js +3 -3
- package/package.json +3 -1
- package/server.json +47 -0
package/dist/auth/access.js
CHANGED
|
@@ -14,10 +14,16 @@ function parseScopesFromClaims(claims) {
|
|
|
14
14
|
}
|
|
15
15
|
return Array.from(new Set(scopes));
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Try to verify an access token from the MCP extra.authInfo.
|
|
19
|
+
* Returns null if no token is present (allows fallback to client tokenProvider for stdio mode).
|
|
20
|
+
* Throws if token is invalid.
|
|
21
|
+
*/
|
|
22
|
+
export function tryVerifyAccessToken(extra) {
|
|
18
23
|
const token = extra?.authInfo?.token;
|
|
19
24
|
if (!token) {
|
|
20
|
-
|
|
25
|
+
// No token in extra - this is OK for stdio transport, let the API client handle auth
|
|
26
|
+
return null;
|
|
21
27
|
}
|
|
22
28
|
const secret = process.env.JWT_SECRET;
|
|
23
29
|
if (!secret) {
|
|
@@ -37,13 +43,30 @@ export function verifyAccessToken(extra) {
|
|
|
37
43
|
raw: decoded,
|
|
38
44
|
};
|
|
39
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Backwards-compatible - throws if no token. Use tryVerifyAccessToken for nullable return.
|
|
48
|
+
*/
|
|
49
|
+
export function verifyAccessToken(extra) {
|
|
50
|
+
const auth = tryVerifyAccessToken(extra);
|
|
51
|
+
if (!auth) {
|
|
52
|
+
throw new Error('Authentication required: connect your account to use this tool.');
|
|
53
|
+
}
|
|
54
|
+
return auth;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check scopes. Returns null if no token in extra (for stdio fallback to API client auth).
|
|
58
|
+
*/
|
|
40
59
|
export function requireScopes(extra, required) {
|
|
41
|
-
const auth =
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
const auth = tryVerifyAccessToken(extra);
|
|
61
|
+
if (!auth) {
|
|
62
|
+
// No token in extra - let the API client handle auth (stdio mode with tokenProvider)
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (required.length > 0) {
|
|
66
|
+
const ok = required.every((s) => auth.scopes.includes(s));
|
|
67
|
+
if (!ok) {
|
|
68
|
+
throw new Error(`Missing required permission(s): ${required.join(', ')}`);
|
|
69
|
+
}
|
|
47
70
|
}
|
|
48
71
|
return auth;
|
|
49
72
|
}
|
package/dist/index.js
CHANGED
|
@@ -115,6 +115,12 @@ ${widgetJs}
|
|
|
115
115
|
const client = new HireHumanClient({
|
|
116
116
|
baseUrl: process.env.API_URL || 'https://hapticpaper.com/api/v1',
|
|
117
117
|
tokenProvider: async () => {
|
|
118
|
+
// 1. Check for API key (CI/headless mode)
|
|
119
|
+
const apiKey = process.env.HAPTIC_API_KEY;
|
|
120
|
+
if (apiKey) {
|
|
121
|
+
return apiKey;
|
|
122
|
+
}
|
|
123
|
+
// 2. Check for stored OAuth tokens
|
|
118
124
|
const tokens = await tokenManager.loadTokens();
|
|
119
125
|
if (tokens?.access_token) {
|
|
120
126
|
// Check if token is expired (with 5 min buffer)
|
|
@@ -124,7 +130,7 @@ ${widgetJs}
|
|
|
124
130
|
}
|
|
125
131
|
return tokens.access_token;
|
|
126
132
|
}
|
|
127
|
-
// No token - auto-trigger OAuth
|
|
133
|
+
// 3. No token - auto-trigger OAuth
|
|
128
134
|
return runOAuthFlow();
|
|
129
135
|
}
|
|
130
136
|
});
|
package/dist/tools/account.js
CHANGED
|
@@ -32,8 +32,9 @@ export function registerAccountTools(server, client) {
|
|
|
32
32
|
const getAccountInvoked = 'Account details ready';
|
|
33
33
|
const getAccountHandler = async (_args, extra) => {
|
|
34
34
|
try {
|
|
35
|
+
// For HTTP transport, auth comes from extra.authInfo. For stdio, auth is handled by tokenProvider.
|
|
35
36
|
const auth = requireScopes(extra, ['account:read']);
|
|
36
|
-
const result = await client.getAccount(auth
|
|
37
|
+
const result = await client.getAccount(auth?.token);
|
|
37
38
|
const account = result.data;
|
|
38
39
|
const widgetSessionId = `account:${account.userId}`;
|
|
39
40
|
return {
|
package/dist/tools/estimates.js
CHANGED
|
@@ -37,8 +37,8 @@ export function registerEstimateTools(server, client) {
|
|
|
37
37
|
const getEstimateHandler = async (args, extra) => {
|
|
38
38
|
try {
|
|
39
39
|
const auth = requireScopes(extra, ['tasks:read']);
|
|
40
|
-
const est = await client.getEstimate(args, auth
|
|
41
|
-
const widgetSessionId = stableSessionId('estimate', JSON.stringify({ userId: auth
|
|
40
|
+
const est = await client.getEstimate(args, auth?.token);
|
|
41
|
+
const widgetSessionId = stableSessionId('estimate', JSON.stringify({ userId: auth?.userId ?? auth?.clientId, args }));
|
|
42
42
|
return {
|
|
43
43
|
structuredContent: {
|
|
44
44
|
estimate: {
|
|
@@ -90,7 +90,7 @@ export function registerEstimateTools(server, client) {
|
|
|
90
90
|
try {
|
|
91
91
|
const auth = requireScopes(extra, ['tasks:read']);
|
|
92
92
|
const cats = await client.getSkillCategories();
|
|
93
|
-
const widgetSessionId = stableSessionId('skills', auth
|
|
93
|
+
const widgetSessionId = stableSessionId('skills', auth?.userId ?? auth?.clientId);
|
|
94
94
|
const text = cats
|
|
95
95
|
.map((c) => `### ${c.name}\n${c.description}\nRange: $${c.priceRange.min}-$${c.priceRange.max}`)
|
|
96
96
|
.join('\n\n');
|
package/dist/tools/tasks.js
CHANGED
|
@@ -53,7 +53,7 @@ export function registerTaskTools(server, client) {
|
|
|
53
53
|
const createTaskHandler = async (args, extra) => {
|
|
54
54
|
try {
|
|
55
55
|
const auth = requireScopes(extra, ['tasks:write']);
|
|
56
|
-
const task = await client.createTask(args, auth
|
|
56
|
+
const task = await client.createTask(args, auth?.token);
|
|
57
57
|
const widgetSessionId = `task:${task.id}`;
|
|
58
58
|
return {
|
|
59
59
|
structuredContent: {
|
|
@@ -101,7 +101,7 @@ export function registerTaskTools(server, client) {
|
|
|
101
101
|
const getTaskHandler = async (args, extra) => {
|
|
102
102
|
try {
|
|
103
103
|
const auth = requireScopes(extra, ['tasks:read']);
|
|
104
|
-
const task = await client.getTask(args.taskId, auth
|
|
104
|
+
const task = await client.getTask(args.taskId, auth?.token);
|
|
105
105
|
const widgetSessionId = `task:${args.taskId}`;
|
|
106
106
|
return {
|
|
107
107
|
structuredContent: {
|
|
@@ -144,9 +144,9 @@ export function registerTaskTools(server, client) {
|
|
|
144
144
|
const listTasksHandler = async (args, extra) => {
|
|
145
145
|
try {
|
|
146
146
|
const auth = requireScopes(extra, ['tasks:read']);
|
|
147
|
-
const tasks = await client.listTasks(args, auth
|
|
147
|
+
const tasks = await client.listTasks(args, auth?.token);
|
|
148
148
|
const items = Array.isArray(tasks) ? tasks : tasks?.tasks ?? [];
|
|
149
|
-
const widgetSessionId = stableSessionId('tasks', auth
|
|
149
|
+
const widgetSessionId = stableSessionId('tasks', auth?.userId ?? auth?.clientId);
|
|
150
150
|
if (items.length === 0) {
|
|
151
151
|
return {
|
|
152
152
|
structuredContent: { tasks: [] },
|
|
@@ -204,7 +204,7 @@ export function registerTaskTools(server, client) {
|
|
|
204
204
|
const cancelTaskHandler = async (args, extra) => {
|
|
205
205
|
try {
|
|
206
206
|
const auth = requireScopes(extra, ['tasks:write']);
|
|
207
|
-
const res = await client.cancelTask(args.taskId, args.reason, auth
|
|
207
|
+
const res = await client.cancelTask(args.taskId, args.reason, auth?.token);
|
|
208
208
|
const widgetSessionId = `task:${args.taskId}`;
|
|
209
209
|
return {
|
|
210
210
|
structuredContent: {
|
package/dist/tools/workers.js
CHANGED
|
@@ -45,8 +45,8 @@ export function registerWorkerTools(server, client) {
|
|
|
45
45
|
const searchWorkersHandler = async (args, extra) => {
|
|
46
46
|
try {
|
|
47
47
|
const auth = requireScopes(extra, ['workers:read']);
|
|
48
|
-
const result = await client.searchWorkers(args, auth
|
|
49
|
-
const widgetSessionId = stableSessionId('workers', JSON.stringify({ userId: auth
|
|
48
|
+
const result = await client.searchWorkers(args, auth?.token);
|
|
49
|
+
const widgetSessionId = stableSessionId('workers', JSON.stringify({ userId: auth?.userId ?? auth?.clientId, args }));
|
|
50
50
|
if (!result.workers || result.workers.length === 0) {
|
|
51
51
|
return {
|
|
52
52
|
structuredContent: { workers: [], suggestedBudget: result?.suggestedBudget },
|
|
@@ -105,7 +105,7 @@ export function registerWorkerTools(server, client) {
|
|
|
105
105
|
const getWorkerProfileHandler = async (args, extra) => {
|
|
106
106
|
try {
|
|
107
107
|
const auth = requireScopes(extra, ['workers:read']);
|
|
108
|
-
const worker = await client.getWorkerProfile(args.workerId, auth
|
|
108
|
+
const worker = await client.getWorkerProfile(args.workerId, auth?.token);
|
|
109
109
|
const widgetSessionId = `worker:${args.workerId}`;
|
|
110
110
|
return {
|
|
111
111
|
structuredContent: { worker },
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hapticpaper/mcp-server",
|
|
3
|
-
"
|
|
3
|
+
"mcpName": "com.hapticpaper/mcp",
|
|
4
|
+
"version": "1.0.5",
|
|
4
5
|
"description": "Official MCP Server for Haptic Paper - Connect your account to AI models",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"dist",
|
|
17
|
+
"server.json",
|
|
16
18
|
"README.md"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
package/server.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "com.hapticpaper/mcp",
|
|
4
|
+
"title": "Haptic Paper",
|
|
5
|
+
"description": "Connect your AI to human workers. When AI needs help, Haptic Paper makes it happen - from data labeling to physical tasks.",
|
|
6
|
+
"icons": [
|
|
7
|
+
{
|
|
8
|
+
"mimeType": "image/png",
|
|
9
|
+
"sizes": ["32x32"],
|
|
10
|
+
"src": "https://hapticpaper.com/favicon-32x32.png"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"mimeType": "image/png",
|
|
14
|
+
"sizes": ["192x192"],
|
|
15
|
+
"src": "https://hapticpaper.com/android-chrome-192x192.png"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "https://github.com/hapticpaper/hh",
|
|
20
|
+
"source": "github",
|
|
21
|
+
"subfolder": "packages/mcp-server"
|
|
22
|
+
},
|
|
23
|
+
"websiteUrl": "https://hapticpaper.com/developer",
|
|
24
|
+
"version": "1.0.4",
|
|
25
|
+
"packages": [
|
|
26
|
+
{
|
|
27
|
+
"registryType": "npm",
|
|
28
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
29
|
+
"identifier": "@hapticpaper/mcp-server",
|
|
30
|
+
"version": "1.0.4",
|
|
31
|
+
"transport": {
|
|
32
|
+
"type": "stdio"
|
|
33
|
+
},
|
|
34
|
+
"runtimeHint": "npx",
|
|
35
|
+
"runtimeArguments": [],
|
|
36
|
+
"environmentVariables": [
|
|
37
|
+
{
|
|
38
|
+
"name": "HAPTIC_API_KEY",
|
|
39
|
+
"description": "Your Haptic Paper API key. Generate one at https://hapticpaper.com/developer",
|
|
40
|
+
"format": "string",
|
|
41
|
+
"isRequired": false,
|
|
42
|
+
"isSecret": true
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|