@hapticpaper/mcp-server 1.0.8 → 1.0.10

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.
@@ -0,0 +1,107 @@
1
+ import axios from 'axios';
2
+ export class HapticPaperClient {
3
+ client;
4
+ tokenProvider;
5
+ constructor(config) {
6
+ this.client = axios.create({
7
+ baseURL: config.baseUrl,
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ });
12
+ this.tokenProvider = config.tokenProvider;
13
+ // Request interceptor to add auth token
14
+ this.client.interceptors.request.use(async (config) => {
15
+ if (this.tokenProvider) {
16
+ const token = await this.tokenProvider();
17
+ if (token) {
18
+ config.headers.Authorization = `Bearer ${token}`;
19
+ }
20
+ }
21
+ return config;
22
+ });
23
+ }
24
+ async authHeaders(accessToken) {
25
+ if (accessToken) {
26
+ return { Authorization: `Bearer ${accessToken}` };
27
+ }
28
+ if (!this.tokenProvider) {
29
+ return {};
30
+ }
31
+ const token = await this.tokenProvider();
32
+ return token ? { Authorization: `Bearer ${token}` } : {};
33
+ }
34
+ // Account Methods
35
+ async getAccount(accessToken) {
36
+ const response = await this.client.get('/gpt/account', { headers: await this.authHeaders(accessToken) });
37
+ return response.data;
38
+ }
39
+ // Task Methods
40
+ async createTask(data, accessToken) {
41
+ const response = await this.client.post('/gpt/tasks', data, { headers: await this.authHeaders(accessToken) });
42
+ return response.data;
43
+ }
44
+ async getTask(taskId, accessToken) {
45
+ const response = await this.client.get(`/gpt/tasks/${taskId}`, { headers: await this.authHeaders(accessToken) });
46
+ return response.data;
47
+ }
48
+ async listTasks(params, accessToken) {
49
+ const response = await this.client.get('/gpt/tasks', { params, headers: await this.authHeaders(accessToken) });
50
+ return response.data;
51
+ }
52
+ async cancelTask(taskId, reason, accessToken) {
53
+ const response = await this.client.post(`/gpt/tasks/${taskId}/cancel`, { reason }, { headers: await this.authHeaders(accessToken) });
54
+ return response.data;
55
+ }
56
+ // Worker Methods
57
+ async searchWorkers(params, accessToken) {
58
+ const response = await this.client.post('/gpt/workers/search', params, { headers: await this.authHeaders(accessToken) });
59
+ return response.data;
60
+ }
61
+ async getWorkerProfile(workerId, accessToken) {
62
+ // This endpoint might not exist in gptRoutes yet, assuming it maps to backend logic
63
+ // If not in GPT routes, we might need to add it or use a different route
64
+ // For now assuming it exists based on plan
65
+ const response = await this.client.get(`/gpt/workers/${workerId}`, { headers: await this.authHeaders(accessToken) });
66
+ return response.data;
67
+ }
68
+ // Estimate Methods
69
+ async getEstimate(params, accessToken) {
70
+ const response = await this.client.post('/gpt/estimate', params, { headers: await this.authHeaders(accessToken) });
71
+ return response.data;
72
+ }
73
+ async getSkillCategories() {
74
+ // Placeholder or real endpoint
75
+ return [
76
+ {
77
+ name: "Delivery",
78
+ description: "Physical delivery of items",
79
+ examples: ["Food delivery", "Package courier"],
80
+ priceRange: { min: 15, max: 50 }
81
+ },
82
+ {
83
+ name: "General Help",
84
+ description: "Moving, cleaning, organizing",
85
+ examples: ["Help moving boxes", "Garage cleanup"],
86
+ priceRange: { min: 30, max: 100 }
87
+ }
88
+ ];
89
+ }
90
+ // Qualification Methods
91
+ async discoverEarningOpportunity(params) {
92
+ const response = await this.client.post('/gpt/qualification/discover', params);
93
+ return response.data;
94
+ }
95
+ async continueQualification(sessionId, userResponse) {
96
+ const response = await this.client.post(`/gpt/qualification/${sessionId}/respond`, { userResponse });
97
+ return response.data;
98
+ }
99
+ async getQualificationStatus(sessionId) {
100
+ const response = await this.client.get(`/gpt/qualification/${sessionId}`);
101
+ return response.data;
102
+ }
103
+ async completeQualification(sessionId) {
104
+ const response = await this.client.post(`/gpt/qualification/${sessionId}/complete`, {});
105
+ return response.data;
106
+ }
107
+ }
@@ -0,0 +1,10 @@
1
+ // Canonical widget URI used in tool `openai/outputTemplate`.
2
+ export const HAPTICPAPER_WIDGET_URI = 'ui://widget/hapticpaper.html';
3
+ // Legacy widget URI kept for backward compatibility with clients that cache the widget URI.
4
+ export const HIREHUMAN_WIDGET_URI = 'ui://widget/hirehuman.html';
5
+ export const CANONICAL_WIDGET_RESOURCES = [
6
+ { name: 'hapticpaper-widget', uri: HAPTICPAPER_WIDGET_URI },
7
+ ];
8
+ export const LEGACY_WIDGET_RESOURCES = [
9
+ { name: 'hirehuman-widget', uri: HIREHUMAN_WIDGET_URI },
10
+ ];
package/dist/index.js CHANGED
@@ -11,7 +11,8 @@ import crypto from 'node:crypto';
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import jwt from 'jsonwebtoken';
14
- import { HireHumanClient } from "./client/hireHumanClient.js";
14
+ import { HapticPaperClient } from "./client/hapticPaperClient.js";
15
+ import { CANONICAL_WIDGET_RESOURCES, LEGACY_WIDGET_RESOURCES } from "./constants/widget.js";
15
16
  import { registerAllTools } from "./tools/index.js";
16
17
  import { registerAllResources } from "./resources/index.js";
17
18
  import { TokenManager, MCPOAuthHandler } from "./auth/oauth.js";
@@ -25,8 +26,8 @@ async function main() {
25
26
  console.error('Starting Authentication Flow...');
26
27
  const auth = new MCPOAuthHandler({
27
28
  clientId: 'mcp-client', // Matches seedMcpClient.ts
28
- authorizationUrl: process.env.AUTH_URL || 'https://hapticpaper.com/oauth/authorize',
29
- tokenUrl: process.env.TOKEN_URL || 'https://hapticpaper.com/api/v1/oauth/token',
29
+ authorizationUrl: process.env.AUTH_URL || 'https://hh.hapticpaper.com/oauth/authorize',
30
+ tokenUrl: process.env.TOKEN_URL || 'https://hh.hapticpaper.com/api/v1/oauth/token',
30
31
  redirectUri: 'http://localhost:8765/callback',
31
32
  scopes: ['tasks:read', 'tasks:write', 'workers:read']
32
33
  });
@@ -49,17 +50,21 @@ async function main() {
49
50
  return;
50
51
  }
51
52
  const widgetJs = fs.readFileSync(widgetBundlePath, 'utf8');
52
- server.registerResource('hirehuman-widget', 'ui://widget/hirehuman.html', {}, async () => ({
53
- contents: [
54
- {
55
- uri: 'ui://widget/hirehuman.html',
56
- mimeType: 'text/html+skybridge',
57
- text: `
58
- <div id="hirehuman-root"></div>
53
+ const widgetMeta = {
54
+ 'openai/widgetPrefersBorder': true,
55
+ 'openai/widgetDomain': 'https://chatgpt.com',
56
+ 'openai/widgetDescription': 'Shows interactive task and worker results for Haptic-Paper tools.',
57
+ 'openai/widgetCSP': {
58
+ connect_domains: ['https://chatgpt.com'],
59
+ resource_domains: ['https://*.oaistatic.com'],
60
+ },
61
+ };
62
+ const html = `
63
+ <div id="hapticpaper-root"></div>
59
64
  <style>
60
65
  :root { color-scheme: light dark; }
61
66
  body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
62
- #hirehuman-root { padding: 12px; }
67
+ #hapticpaper-root { padding: 12px; }
63
68
  .hh-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
64
69
  .hh-muted { opacity: 0.7; font-size: 12px; }
65
70
  .hh-card { border: 1px solid rgba(127,127,127,0.25); border-radius: 12px; padding: 10px; margin: 8px 0; }
@@ -72,23 +77,30 @@ async function main() {
72
77
  <script type="module">
73
78
  ${widgetJs}
74
79
  </script>
75
- `.trim(),
76
- _meta: {
77
- 'openai/widgetPrefersBorder': true,
78
- 'openai/widgetDomain': 'https://chatgpt.com',
79
- 'openai/widgetDescription': 'Shows interactive task and worker results for Hire-a-Human tools.',
80
- 'openai/widgetCSP': {
81
- connect_domains: ['https://chatgpt.com'],
82
- resource_domains: ['https://*.oaistatic.com'],
83
- },
80
+ `.trim();
81
+ const registerWidgetResource = (name, uri) => {
82
+ server.registerResource(name, uri, {}, async () => ({
83
+ contents: [
84
+ {
85
+ uri,
86
+ mimeType: 'text/html+skybridge',
87
+ text: html,
88
+ _meta: widgetMeta,
84
89
  },
85
- },
86
- ],
87
- }));
90
+ ],
91
+ }));
92
+ };
93
+ for (const { name, uri } of CANONICAL_WIDGET_RESOURCES) {
94
+ registerWidgetResource(name, uri);
95
+ }
96
+ // Some clients cache the widget URI from the tool definitions; serving both keeps older installs working.
97
+ for (const { name, uri } of LEGACY_WIDGET_RESOURCES) {
98
+ registerWidgetResource(name, uri);
99
+ }
88
100
  }
89
101
  function createMcpServer(client) {
90
102
  const server = new McpServer({
91
- name: "hire-a-human",
103
+ name: "haptic-paper",
92
104
  version: "1.0.0"
93
105
  });
94
106
  registerWidgetTemplate(server);
@@ -96,13 +108,14 @@ ${widgetJs}
96
108
  registerAllResources(server, client);
97
109
  return server;
98
110
  }
111
+ const transportType = process.env.MCP_TRANSPORT || 'stdio';
99
112
  // Helper to run interactive OAuth flow
100
113
  const runOAuthFlow = async () => {
101
114
  console.error('No valid token found. Starting authentication...');
102
115
  const auth = new MCPOAuthHandler({
103
116
  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',
117
+ authorizationUrl: process.env.AUTH_URL || 'https://hh.hapticpaper.com/oauth/authorize',
118
+ tokenUrl: process.env.TOKEN_URL || 'https://hh.hapticpaper.com/api/v1/oauth/token',
106
119
  redirectUri: 'http://localhost:8765/callback',
107
120
  scopes: ['tasks:read', 'tasks:write', 'workers:read']
108
121
  });
@@ -112,9 +125,9 @@ ${widgetJs}
112
125
  return tokens.access_token;
113
126
  };
114
127
  // Initialize Client with auto-auth tokenProvider
115
- const client = new HireHumanClient({
116
- baseUrl: process.env.API_URL || 'https://hapticpaper.com/api/v1',
117
- tokenProvider: async () => {
128
+ const client = new HapticPaperClient({
129
+ baseUrl: process.env.API_URL || 'https://hh.hapticpaper.com/api/v1',
130
+ tokenProvider: transportType === 'http' ? undefined : async () => {
118
131
  // 1. Check for API key (CI/headless mode)
119
132
  const apiKey = process.env.HAPTIC_API_KEY;
120
133
  if (apiKey) {
@@ -135,8 +148,8 @@ ${widgetJs}
135
148
  }
136
149
  });
137
150
  // Transport
138
- const transportType = process.env.MCP_TRANSPORT || 'stdio';
139
151
  if (transportType === 'http') {
152
+ console.error('[MCP] MCP_TRANSPORT=http: interactive OAuth is disabled; clients must send Authorization: Bearer <token>');
140
153
  const host = process.env.HOST || '127.0.0.1';
141
154
  const port = Number(process.env.PORT || 3001);
142
155
  const app = createMcpExpressApp({
@@ -154,20 +167,79 @@ ${widgetJs}
154
167
  const backendBaseUrl = process.env.BACKEND_PUBLIC_URL || process.env.API_URL?.replace(/\/api\/v1\/?$/, '') || 'http://localhost:3000';
155
168
  const backendOAuthBase = `${backendBaseUrl}/api/v1/oauth`;
156
169
  const scopesSupported = ['tasks:read', 'tasks:write', 'workers:read'];
157
- // RFC 8414: OAuth Authorization Server Metadata
158
- // MCP spec says this MUST be at {authorization_base_url}/.well-known/oauth-authorization-server
159
- app.get('/.well-known/oauth-authorization-server', (_req, res) => {
160
- res.json({
161
- issuer: mcpOrigin,
170
+ const getConfiguredPublicOrigin = () => {
171
+ const configured = process.env.MCP_PUBLIC_URL;
172
+ if (!configured)
173
+ return undefined;
174
+ try {
175
+ const url = new URL(configured);
176
+ if (url.pathname !== '/' || url.search || url.hash) {
177
+ console.error(`[MCP] MCP_PUBLIC_URL must be an origin without path, query, or fragment; falling back to request-derived origin. (received=${configured})`);
178
+ return undefined;
179
+ }
180
+ return url.origin;
181
+ }
182
+ catch {
183
+ console.error(`[MCP] Invalid MCP_PUBLIC_URL; falling back to request-derived origin. (received=${configured})`);
184
+ return undefined;
185
+ }
186
+ };
187
+ const configuredPublicOrigin = getConfiguredPublicOrigin();
188
+ const getForwarded = (value) => {
189
+ const raw = Array.isArray(value) ? value[0] : value;
190
+ if (typeof raw !== 'string')
191
+ return undefined;
192
+ // Use the first forwarded value (client-facing) when multiple proxies append values.
193
+ const part = raw.split(',')[0]?.trim();
194
+ return part || undefined;
195
+ };
196
+ const getIssuerForMetadata = (req) => {
197
+ if (configuredPublicOrigin)
198
+ return configuredPublicOrigin;
199
+ if (!req)
200
+ return mcpOrigin;
201
+ const proto = getForwarded(req.headers['x-forwarded-proto']);
202
+ const host = getForwarded(req.headers['x-forwarded-host']) || getForwarded(req.headers.host);
203
+ const safeProto = proto === 'http' || proto === 'https' ? proto : undefined;
204
+ const safeHost = host && !/[\s/\\@]/.test(host) ? host : undefined;
205
+ if (safeProto && safeHost) {
206
+ return `${safeProto}://${safeHost}`;
207
+ }
208
+ return mcpOrigin;
209
+ };
210
+ // Some clients validate this strictly; prefer a stable configured origin when possible.
211
+ const issuer = getIssuerForMetadata();
212
+ // Prevent caching of auth discovery documents. In some deployments, `issuer` may differ
213
+ // between local dev and proxy setups; stale caches can cause confusing auth failures.
214
+ app.use('/.well-known', (_req, res, next) => {
215
+ res.set('Cache-Control', 'no-store');
216
+ next();
217
+ });
218
+ const buildDiscoveryMetadata = (req) => {
219
+ const issuer = getIssuerForMetadata(req);
220
+ const metadata = {
221
+ issuer,
162
222
  authorization_endpoint: `${backendOAuthBase}/authorize`,
163
223
  token_endpoint: `${backendOAuthBase}/token`,
164
- registration_endpoint: `${mcpOrigin}/register`,
224
+ registration_endpoint: `${issuer}/register`,
165
225
  response_types_supported: ['code'],
166
226
  grant_types_supported: ['authorization_code', 'refresh_token'],
167
227
  token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
168
228
  code_challenge_methods_supported: ['S256', 'plain'],
169
229
  scopes_supported: scopesSupported,
170
- });
230
+ };
231
+ return Object.freeze(metadata);
232
+ };
233
+ // RFC 8414: OAuth Authorization Server Metadata
234
+ // MCP spec says this MUST be at {authorization_base_url}/.well-known/oauth-authorization-server
235
+ app.get('/.well-known/oauth-authorization-server', (req, res) => {
236
+ res.json(buildDiscoveryMetadata(req));
237
+ });
238
+ // Some clients look for OIDC discovery even when only OAuth is required.
239
+ // This is not a full OpenID Provider implementation; it only advertises the
240
+ // OAuth endpoints and PKCE capability needed for authorization-code flows.
241
+ app.get('/.well-known/openid-configuration', (req, res) => {
242
+ res.json(buildDiscoveryMetadata(req));
171
243
  });
172
244
  // RFC 7591: Dynamic Client Registration
173
245
  // Proxy to backend
@@ -186,13 +258,13 @@ ${widgetJs}
186
258
  res.status(500).json({ error: 'Registration failed' });
187
259
  }
188
260
  });
189
- // For legacy/SDK compatibility
190
- const issuer = mcpOrigin;
261
+ // For legacy/SDK compatibility, we use the best known stable issuer at startup.
262
+ // When behind proxies, prefer the `/.well-known/*` endpoints for per-request issuer.
191
263
  const oauthMetadata = {
192
264
  issuer,
193
265
  authorization_endpoint: `${backendOAuthBase}/authorize`,
194
266
  token_endpoint: `${backendOAuthBase}/token`,
195
- registration_endpoint: `${mcpOrigin}/register`,
267
+ registration_endpoint: `${issuer}/register`,
196
268
  response_types_supported: ['code'],
197
269
  grant_types_supported: ['authorization_code', 'refresh_token'],
198
270
  token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
@@ -204,12 +276,14 @@ ${widgetJs}
204
276
  // (including VS Code and ChatGPT) can start the OAuth handshake.
205
277
  const protectedResourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceServerUrl);
206
278
  const protectedResourceMetadataPath = new URL(protectedResourceMetadataUrl).pathname;
207
- app.get(protectedResourceMetadataPath, (_req, res) => {
279
+ app.get(protectedResourceMetadataPath, (req, res) => {
280
+ const issuer = getIssuerForMetadata(req);
281
+ res.set('Cache-Control', 'no-store');
208
282
  res.json({
209
283
  resource: resourceServerUrl.toString(),
210
284
  authorization_servers: [issuer],
211
285
  scopes_supported: scopesSupported,
212
- resource_name: 'Hire-a-Human MCP',
286
+ resource_name: 'Haptic-Paper MCP',
213
287
  });
214
288
  });
215
289
  if (shouldAdvertiseAuthMetadata) {
@@ -218,12 +292,12 @@ ${widgetJs}
218
292
  oauthMetadata,
219
293
  resourceServerUrl,
220
294
  scopesSupported,
221
- resourceName: 'Hire-a-Human MCP',
295
+ resourceName: 'Haptic-Paper MCP',
222
296
  }));
223
297
  }
224
298
  else if (!authMetadataDisabled) {
225
299
  console.error(`[MCP] Auth metadata not advertised because URLs are not HTTPS. ` +
226
- `Set RESOURCE_SERVER_URL and AUTHORIZATION_SERVER_ISSUER to https://... (recommended for ChatGPT Connectors), ` +
300
+ `Set RESOURCE_SERVER_URL to https://... (recommended for ChatGPT Connectors), ` +
227
301
  `or set MCP_AUTH_METADATA_DISABLED=true for local dev. ` +
228
302
  `(issuer=${issuer}, resourceServerUrl=${resourceServerUrl.toString()})`);
229
303
  }
@@ -258,18 +332,40 @@ ${widgetJs}
258
332
  resourceMetadataUrl: protectedResourceMetadataUrl,
259
333
  });
260
334
  const transports = {};
335
+ const allowUnauthenticatedDiscovery = String(process.env.MCP_ALLOW_UNAUTHENTICATED_DISCOVERY || '').toLowerCase() === 'true';
336
+ const discoveryMethods = ['tools/list', 'resources/list', 'prompts/list'];
337
+ // Allowlist for JSON-RPC notifications that are safe to accept unauthenticated.
338
+ const unauthenticatedNotificationMethods = ['notifications/initialized'];
261
339
  const shouldRequireAuth = (req) => {
262
340
  // Only POST requests perform JSON-RPC actions. GET/DELETE are session transport mechanics.
263
341
  if (req.method !== 'POST')
264
342
  return false;
265
- // Allow unauthenticated initialize + listing so clients can discover tools/resources.
343
+ // Allow unauthenticated initialize so the session can be created.
344
+ // Everything else should require auth so clients (VS Code / ChatGPT) trigger the OAuth handshake
345
+ // before listing tools or invoking them.
266
346
  const body = req.body;
347
+ // Security hardening: batched JSON-RPC requests must always be authenticated to avoid
348
+ // mixing allowed + disallowed methods in a single batch and bypassing auth.
349
+ if (Array.isArray(body))
350
+ return true;
267
351
  if (isInitializeRequest(body))
268
352
  return false;
269
- const method = body?.method;
270
- if (method === 'tools/list' || method === 'resources/list' || method === 'prompts/list')
353
+ const isObject = body !== null && typeof body === 'object' && !Array.isArray(body);
354
+ const obj = isObject ? body : null;
355
+ const method = typeof obj?.method === 'string' ? obj.method : undefined;
356
+ const hasOwnId = isObject && Object.prototype.hasOwnProperty.call(body, 'id');
357
+ const idValue = hasOwnId ? obj?.['id'] : undefined;
358
+ const isJsonRpcNotification = isObject && !hasOwnId;
359
+ const isNullIdNotification = isObject && hasOwnId && idValue === null;
360
+ if ((isJsonRpcNotification || isNullIdNotification) &&
361
+ method &&
362
+ unauthenticatedNotificationMethods.includes(method)) {
271
363
  return false;
272
- // Require OAuth for tool calls and everything else.
364
+ }
365
+ if (allowUnauthenticatedDiscovery) {
366
+ if (method && discoveryMethods.includes(method))
367
+ return false;
368
+ }
273
369
  return true;
274
370
  };
275
371
  app.use('/mcp', (req, res, next) => {
@@ -325,6 +421,8 @@ ${widgetJs}
325
421
  app.listen(port, host, () => {
326
422
  console.error(`MCP Server listening on http://${host}:${port}/mcp`);
327
423
  console.error(`OAuth resource metadata: ${getOAuthProtectedResourceMetadataUrl(resourceServerUrl)}`);
424
+ console.error(`[MCP] Discovery auth: ${allowUnauthenticatedDiscovery ? 'unauthenticated allowed' : 'auth required'} ` +
425
+ `for ${discoveryMethods.join(', ')}${allowUnauthenticatedDiscovery ? '' : ' (set MCP_ALLOW_UNAUTHENTICATED_DISCOVERY=true to allow)'}`);
328
426
  });
329
427
  }
330
428
  else {
@@ -15,7 +15,7 @@ const templates = [
15
15
  typicalDuration: "3-5 hours"
16
16
  }
17
17
  ];
18
- export function registerAllResources(server, _client) {
18
+ export function registerAllResources(server, client) {
19
19
  server.registerResource('task-templates', TEMPLATES_URI, {
20
20
  title: 'Task Templates',
21
21
  description: 'List of common task templates with pricing guidance',
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
- const OUTPUT_TEMPLATE = 'ui://widget/hirehuman.html';
3
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
4
4
  function oauthSecuritySchemes(scopes) {
5
5
  return [
6
6
  {
@@ -11,7 +11,7 @@ function oauthSecuritySchemes(scopes) {
11
11
  }
12
12
  function toolDescriptorMeta(invoking, invoked, scopes) {
13
13
  return {
14
- 'openai/outputTemplate': OUTPUT_TEMPLATE,
14
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
15
15
  'openai/toolInvocation/invoking': invoking,
16
16
  'openai/toolInvocation/invoked': invoked,
17
17
  'openai/widgetAccessible': true,
@@ -42,14 +42,14 @@ export function registerAccountTools(server, client) {
42
42
  account: {
43
43
  displayName: account.displayName,
44
44
  email: account.email,
45
- balanceDollars: account.balanceDollars,
45
+ balanceCredits: account.balanceCredits,
46
46
  hasPaymentMethod: account.hasPaymentMethod,
47
47
  },
48
48
  },
49
49
  content: [
50
50
  {
51
51
  type: 'text',
52
- text: `Welcome, ${account.displayName}! Your Haptic Paper balance is $${account.balanceDollars.toFixed(2)}.${account.hasPaymentMethod ? '' : ' Add a payment method to create paid tasks.'}`,
52
+ text: `Welcome, ${account.displayName}! Your Haptic Paper balance is ${account.balanceCredits} credits.${account.hasPaymentMethod ? '' : ' Add a payment method to create paid tasks.'}`,
53
53
  },
54
54
  ],
55
55
  _meta: {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
3
4
  import crypto from 'node:crypto';
4
- const OUTPUT_TEMPLATE = 'ui://widget/hirehuman.html';
5
5
  function stableSessionId(prefix, value) {
6
6
  const raw = value ?? 'unknown';
7
7
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -17,7 +17,7 @@ function oauthSecuritySchemes(scopes) {
17
17
  }
18
18
  function toolDescriptorMeta(invoking, invoked, scopes) {
19
19
  return {
20
- 'openai/outputTemplate': OUTPUT_TEMPLATE,
20
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
21
21
  'openai/toolInvocation/invoking': invoking,
22
22
  'openai/toolInvocation/invoked': invoked,
23
23
  'openai/widgetAccessible': true,
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
2
3
  import crypto from 'node:crypto';
3
- const OUTPUT_TEMPLATE = 'ui://widget/hirehuman.html';
4
4
  function stableSessionId(prefix, value) {
5
5
  const raw = value ?? 'unknown';
6
6
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -8,7 +8,7 @@ function stableSessionId(prefix, value) {
8
8
  }
9
9
  function toolDescriptorMeta(invoking, invoked, scopes = []) {
10
10
  return {
11
- 'openai/outputTemplate': OUTPUT_TEMPLATE,
11
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
12
12
  'openai/toolInvocation/invoking': invoking,
13
13
  'openai/toolInvocation/invoked': invoked,
14
14
  'openai/widgetAccessible': true,
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
3
4
  import crypto from 'node:crypto';
4
- const OUTPUT_TEMPLATE = 'ui://widget/hirehuman.html';
5
5
  function stableSessionId(prefix, value) {
6
6
  const raw = value ?? 'unknown';
7
7
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -17,7 +17,7 @@ function oauthSecuritySchemes(scopes) {
17
17
  }
18
18
  function toolDescriptorMeta(invoking, invoked, scopes) {
19
19
  return {
20
- 'openai/outputTemplate': OUTPUT_TEMPLATE,
20
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
21
21
  'openai/toolInvocation/invoking': invoking,
22
22
  'openai/toolInvocation/invoked': invoked,
23
23
  'openai/widgetAccessible': true,
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { requireScopes } from "../auth/access.js";
3
+ import { HAPTICPAPER_WIDGET_URI } from "../constants/widget.js";
3
4
  import crypto from 'node:crypto';
4
- const OUTPUT_TEMPLATE = 'ui://widget/hirehuman.html';
5
5
  function stableSessionId(prefix, value) {
6
6
  const raw = value ?? 'unknown';
7
7
  const hash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
@@ -17,7 +17,7 @@ function oauthSecuritySchemes(scopes) {
17
17
  }
18
18
  function toolDescriptorMeta(invoking, invoked, scopes) {
19
19
  return {
20
- 'openai/outputTemplate': OUTPUT_TEMPLATE,
20
+ 'openai/outputTemplate': HAPTICPAPER_WIDGET_URI,
21
21
  'openai/toolInvocation/invoking': invoking,
22
22
  'openai/toolInvocation/invoked': invoked,
23
23
  'openai/widgetAccessible': true,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hapticpaper",
3
3
  "description": "Connect your account to create human tasks from agentic pipelines.",
4
- "version": "1.0.7",
4
+ "version": "1.0.9",
5
5
  "contextFileName": "HAPTICPAPER.md",
6
6
  "mcpServers": {
7
7
  "hapticpaper": {
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.8",
4
+ "version": "1.0.10",
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
@@ -2,16 +2,20 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "com.hapticpaper/mcp",
4
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.",
5
+ "description": "Connect your AI to human workers. Get paid to help AI.",
6
6
  "icons": [
7
7
  {
8
8
  "mimeType": "image/png",
9
- "sizes": ["32x32"],
9
+ "sizes": [
10
+ "32x32"
11
+ ],
10
12
  "src": "https://hapticpaper.com/favicon-32x32.png"
11
13
  },
12
14
  {
13
15
  "mimeType": "image/png",
14
- "sizes": ["192x192"],
16
+ "sizes": [
17
+ "192x192"
18
+ ],
15
19
  "src": "https://hapticpaper.com/android-chrome-192x192.png"
16
20
  }
17
21
  ],
@@ -21,7 +25,7 @@
21
25
  "subfolder": "packages/mcp-server"
22
26
  },
23
27
  "websiteUrl": "https://hapticpaper.com/developer",
24
- "version": "1.0.8",
28
+ "version": "1.0.10",
25
29
  "remotes": [
26
30
  {
27
31
  "type": "streamable-http",
@@ -33,7 +37,7 @@
33
37
  "registryType": "npm",
34
38
  "registryBaseUrl": "https://registry.npmjs.org",
35
39
  "identifier": "@hapticpaper/mcp-server",
36
- "version": "1.0.8",
40
+ "version": "1.0.10",
37
41
  "transport": {
38
42
  "type": "stdio"
39
43
  },