@hapticpaper/mcp-server 1.0.21 → 1.0.22

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.
@@ -26,30 +26,43 @@ export class MCPOAuthHandler extends EventEmitter {
26
26
  }
27
27
  async authenticate() {
28
28
  const codeChallenge = this.generateCodeChallenge(this.codeVerifier);
29
- // Build auth URL
29
+ // Start server and get dynamic redirect URI
30
+ const { redirectUri, codePromise } = await this.listenForCallback();
31
+ // Build auth URL with the actual dynamic redirect URI
30
32
  const authUrl = new URL(this.config.authorizationUrl);
31
33
  authUrl.searchParams.set('client_id', this.config.clientId);
32
- authUrl.searchParams.set('redirect_uri', this.config.redirectUri);
34
+ authUrl.searchParams.set('redirect_uri', redirectUri);
33
35
  authUrl.searchParams.set('response_type', 'code');
34
36
  authUrl.searchParams.set('scope', this.config.scopes.join(' '));
35
37
  authUrl.searchParams.set('state', this.state);
36
38
  authUrl.searchParams.set('code_challenge', codeChallenge);
37
39
  authUrl.searchParams.set('code_challenge_method', 'S256');
38
40
  console.error('Opening browser to:', authUrl.toString());
39
- // Start server first
40
- const codePromise = this.startCallbackServer();
41
41
  // Open browser
42
42
  await open(authUrl.toString());
43
+ // Wait for the code
43
44
  const code = await codePromise;
44
- return this.exchangeCode(code);
45
+ // Exchange code for tokens (passing the SAME redirect URI used for auth)
46
+ // Since exchangeCode is distinct, we pass the redirectUri we used.
47
+ return this.exchangeCode(code, redirectUri);
45
48
  }
46
- startCallbackServer() {
49
+ listenForCallback() {
47
50
  return new Promise((resolve, reject) => {
48
- // Extract port from redirect URI
49
- const port = new URL(this.config.redirectUri).port || '80';
50
- const server = http.createServer((req, res) => {
51
- const url = new URL(req.url, `http://localhost:${port}`);
52
- if (url.pathname !== new URL(this.config.redirectUri).pathname) {
51
+ const server = http.createServer();
52
+ // We'll expose the promise that resolves when the code is received
53
+ let resolveCode;
54
+ let rejectCode;
55
+ const codePromise = new Promise((res, rej) => {
56
+ resolveCode = res;
57
+ rejectCode = rej;
58
+ });
59
+ server.on('request', (req, res) => {
60
+ // Get the port we actually bound to
61
+ const address = server.address();
62
+ const port = typeof address === 'object' && address ? address.port : 0;
63
+ const pathName = new URL(this.config.redirectUri).pathname;
64
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
65
+ if (url.pathname !== pathName) {
53
66
  res.writeHead(404);
54
67
  res.end('Not found');
55
68
  return;
@@ -61,40 +74,55 @@ export class MCPOAuthHandler extends EventEmitter {
61
74
  res.writeHead(400, { 'Content-Type': 'text/html' });
62
75
  res.end(`<h1>Auth Failed</h1><p>${error}</p>`);
63
76
  server.close();
64
- reject(new Error(error));
77
+ rejectCode(new Error(error));
65
78
  return;
66
79
  }
67
80
  if (state !== this.state) {
68
81
  res.writeHead(400, { 'Content-Type': 'text/html' });
69
82
  res.end(`<h1>Auth Failed</h1><p>State mismatch</p>`);
70
83
  server.close();
71
- reject(new Error('State mismatch'));
84
+ rejectCode(new Error('State mismatch'));
72
85
  return;
73
86
  }
74
87
  res.writeHead(200, { 'Content-Type': 'text/html' });
75
- res.end(`<h1>Authenticated!</h1><p>You can close this window and return to Claude.</p><script>window.close()</script>`);
88
+ res.end(`<h1>Authenticated!</h1><p>You can close this window and return to your application.</p><script>window.close()</script>`);
76
89
  server.close();
77
- resolve(code);
90
+ resolveCode(code);
91
+ });
92
+ server.on('error', (err) => {
93
+ console.error('OAuth Callback Server Error:', err);
94
+ // If the server fails to start, we reject the initial setup promise
95
+ reject(err);
78
96
  });
79
- server.listen(parseInt(port), () => {
80
- // console.error(`Listening on port ${port}`);
97
+ // Listen on ephemeral port (0) and explicitly on IPv4 loopback
98
+ server.listen(0, '127.0.0.1', () => {
99
+ const address = server.address();
100
+ const port = typeof address === 'object' && address ? address.port : 0;
101
+ const pathName = new URL(this.config.redirectUri).pathname;
102
+ const redirectUri = `http://127.0.0.1:${port}${pathName}`;
103
+ console.error(`Listening for OAuth callback on ${redirectUri}...`);
104
+ resolve({
105
+ redirectUri,
106
+ codePromise
107
+ });
81
108
  });
82
109
  // Safety timeout
83
110
  setTimeout(() => {
84
- server.close();
85
- // Don't reject if already resolved, but simple way:
86
- // This might leak if server is closed by request.
87
- // Proper handling omitted for brevity but server.close covers it mostly.
111
+ if (server.listening) {
112
+ server.close();
113
+ rejectCode(new Error('OAuth callback timed out'));
114
+ }
88
115
  }, 300000); // 5 mins
89
116
  });
90
117
  }
91
- async exchangeCode(code) {
118
+ async exchangeCode(code, redirectUri) {
92
119
  // Use fetch or axios
93
120
  const params = new URLSearchParams();
94
121
  params.append('grant_type', 'authorization_code');
95
122
  params.append('client_id', this.config.clientId);
96
123
  params.append('code', code);
97
- params.append('redirect_uri', this.config.redirectUri);
124
+ // Important: Must match the redirect_uri used in the authorization request
125
+ params.append('redirect_uri', redirectUri);
98
126
  params.append('code_verifier', this.codeVerifier);
99
127
  const response = await fetch(this.config.tokenUrl, {
100
128
  method: 'POST',
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ async function main() {
28
28
  clientId: 'mcp-client', // Matches seedMcpClient.ts
29
29
  authorizationUrl: process.env.AUTH_URL || 'https://hh.hapticpaper.com/oauth/authorize',
30
30
  tokenUrl: process.env.TOKEN_URL || 'https://hh.hapticpaper.com/api/v1/oauth/token',
31
- redirectUri: 'http://localhost:8765/callback',
31
+ redirectUri: 'http://127.0.0.1/callback',
32
32
  scopes: ['tasks:read', 'tasks:write', 'workers:read', 'account:read']
33
33
  });
34
34
  try {
@@ -116,7 +116,7 @@ ${widgetJs}
116
116
  clientId: 'mcp-client',
117
117
  authorizationUrl: process.env.AUTH_URL || 'https://hh.hapticpaper.com/oauth/authorize',
118
118
  tokenUrl: process.env.TOKEN_URL || 'https://hh.hapticpaper.com/api/v1/oauth/token',
119
- redirectUri: 'http://localhost:8765/callback',
119
+ redirectUri: 'http://127.0.0.1/callback',
120
120
  scopes: ['tasks:read', 'tasks:write', 'workers:read', 'account:read']
121
121
  });
122
122
  const tokens = await auth.authenticate();
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.21",
4
+ "version": "1.0.22",
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.21",
28
+ "version": "1.0.22",
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.21",
40
+ "version": "1.0.22",
41
41
  "transport": {
42
42
  "type": "stdio"
43
43
  },