@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.
@@ -14,10 +14,16 @@ function parseScopesFromClaims(claims) {
14
14
  }
15
15
  return Array.from(new Set(scopes));
16
16
  }
17
- export function verifyAccessToken(extra) {
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
- throw new Error('Authentication required: connect your account to use this tool.');
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 = verifyAccessToken(extra);
42
- if (required.length === 0)
43
- return auth;
44
- const ok = required.every((s) => auth.scopes.includes(s));
45
- if (!ok) {
46
- throw new Error(`Missing required permission(s): ${required.join(', ')}`);
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
  });
@@ -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.token);
37
+ const result = await client.getAccount(auth?.token);
37
38
  const account = result.data;
38
39
  const widgetSessionId = `account:${account.userId}`;
39
40
  return {
@@ -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.token);
41
- const widgetSessionId = stableSessionId('estimate', JSON.stringify({ userId: auth.userId ?? auth.clientId, args }));
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.userId ?? auth.clientId);
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');
@@ -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.token);
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.token);
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.token);
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.userId ?? auth.clientId);
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.token);
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: {
@@ -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.token);
49
- const widgetSessionId = stableSessionId('workers', JSON.stringify({ userId: auth.userId ?? auth.clientId, args }));
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.token);
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
- "version": "1.0.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
+ }