@hapticpaper/mcp-server 1.0.2 → 1.0.4
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 +26 -2
- 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 +1 -1
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
|
@@ -96,12 +96,36 @@ ${widgetJs}
|
|
|
96
96
|
registerAllResources(server, client);
|
|
97
97
|
return server;
|
|
98
98
|
}
|
|
99
|
-
//
|
|
99
|
+
// Helper to run interactive OAuth flow
|
|
100
|
+
const runOAuthFlow = async () => {
|
|
101
|
+
console.error('No valid token found. Starting authentication...');
|
|
102
|
+
const auth = new MCPOAuthHandler({
|
|
103
|
+
clientId: 'mcp-client',
|
|
104
|
+
authorizationUrl: process.env.AUTH_URL || 'https://hapticpaper.com/oauth/authorize',
|
|
105
|
+
tokenUrl: process.env.TOKEN_URL || 'https://hapticpaper.com/api/v1/oauth/token',
|
|
106
|
+
redirectUri: 'http://localhost:8765/callback',
|
|
107
|
+
scopes: ['tasks:read', 'tasks:write', 'workers:read']
|
|
108
|
+
});
|
|
109
|
+
const tokens = await auth.authenticate();
|
|
110
|
+
await tokenManager.saveTokens(tokens);
|
|
111
|
+
console.error('Authentication successful!');
|
|
112
|
+
return tokens.access_token;
|
|
113
|
+
};
|
|
114
|
+
// Initialize Client with auto-auth tokenProvider
|
|
100
115
|
const client = new HireHumanClient({
|
|
101
116
|
baseUrl: process.env.API_URL || 'https://hapticpaper.com/api/v1',
|
|
102
117
|
tokenProvider: async () => {
|
|
103
118
|
const tokens = await tokenManager.loadTokens();
|
|
104
|
-
|
|
119
|
+
if (tokens?.access_token) {
|
|
120
|
+
// Check if token is expired (with 5 min buffer)
|
|
121
|
+
if (tokens.expires_at && tokens.expires_at < Date.now() + 5 * 60 * 1000) {
|
|
122
|
+
console.error('Token expired, re-authenticating...');
|
|
123
|
+
return runOAuthFlow();
|
|
124
|
+
}
|
|
125
|
+
return tokens.access_token;
|
|
126
|
+
}
|
|
127
|
+
// No token - auto-trigger OAuth
|
|
128
|
+
return runOAuthFlow();
|
|
105
129
|
}
|
|
106
130
|
});
|
|
107
131
|
// Transport
|
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 },
|