@hapticpaper/mcp-server 1.0.21 → 1.0.24
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/oauth.js +51 -23
- package/dist/index.js +43 -34
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/auth/oauth.js
CHANGED
|
@@ -26,30 +26,43 @@ export class MCPOAuthHandler extends EventEmitter {
|
|
|
26
26
|
}
|
|
27
27
|
async authenticate() {
|
|
28
28
|
const codeChallenge = this.generateCodeChallenge(this.codeVerifier);
|
|
29
|
-
//
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
listenForCallback() {
|
|
47
50
|
return new Promise((resolve, reject) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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://
|
|
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://
|
|
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();
|
|
@@ -310,43 +310,52 @@ ${widgetJs}
|
|
|
310
310
|
verifyAccessToken: async (token) => {
|
|
311
311
|
const publicKey = process.env.JWT_PUBLIC_KEY ? process.env.JWT_PUBLIC_KEY.replace(/\\n/g, '\n') : undefined;
|
|
312
312
|
const secret = process.env.JWT_SECRET;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (secret && e.message === 'invalid signature') {
|
|
321
|
-
decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
|
|
313
|
+
// Debug log (redacted)
|
|
314
|
+
console.error(`[MCP-Auth-Debug] Verifying token. HasPublicKey=${!!publicKey}, HasSecret=${!!secret}`);
|
|
315
|
+
try {
|
|
316
|
+
let decoded;
|
|
317
|
+
if (publicKey) {
|
|
318
|
+
try {
|
|
319
|
+
decoded = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
|
|
322
320
|
}
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
catch (e) {
|
|
322
|
+
// If ES256 fails, and we have a secret, try HS256 (migration path)
|
|
323
|
+
if (secret && e.message === 'invalid signature') {
|
|
324
|
+
decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
throw e;
|
|
328
|
+
}
|
|
325
329
|
}
|
|
326
330
|
}
|
|
331
|
+
else if (secret) {
|
|
332
|
+
decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.error('[MCP-Auth-Error] No keys configured');
|
|
336
|
+
throw new Error('Server misconfigured: Neither JWT_PUBLIC_KEY nor JWT_SECRET is set');
|
|
337
|
+
}
|
|
338
|
+
if (!decoded || typeof decoded !== 'object') {
|
|
339
|
+
throw new Error('Invalid token');
|
|
340
|
+
}
|
|
341
|
+
const scopeStr = typeof decoded.scope === 'string' ? decoded.scope : '';
|
|
342
|
+
const permissions = Array.isArray(decoded.permissions) ? decoded.permissions : [];
|
|
343
|
+
const scopes = [
|
|
344
|
+
...scopeStr.split(/\s+/).map((s) => s.trim()).filter(Boolean),
|
|
345
|
+
...permissions.map((s) => (typeof s === 'string' ? s.trim() : '')).filter(Boolean),
|
|
346
|
+
];
|
|
347
|
+
const exp = decoded.exp;
|
|
348
|
+
return {
|
|
349
|
+
token,
|
|
350
|
+
clientId: decoded.client_id || 'unknown',
|
|
351
|
+
scopes: Array.from(new Set(scopes)),
|
|
352
|
+
expiresAt: typeof exp === 'number' ? exp : Math.floor(Date.now() / 1000) + 3600,
|
|
353
|
+
};
|
|
327
354
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
else {
|
|
332
|
-
throw new Error('Server misconfigured: Neither JWT_PUBLIC_KEY nor JWT_SECRET is set');
|
|
333
|
-
}
|
|
334
|
-
if (!decoded || typeof decoded !== 'object') {
|
|
335
|
-
throw new Error('Invalid token');
|
|
355
|
+
catch (err) {
|
|
356
|
+
console.error('[MCP-Auth-Error] Token verification failed:', err.message, err.stack);
|
|
357
|
+
throw err;
|
|
336
358
|
}
|
|
337
|
-
const scopeStr = typeof decoded.scope === 'string' ? decoded.scope : '';
|
|
338
|
-
const permissions = Array.isArray(decoded.permissions) ? decoded.permissions : [];
|
|
339
|
-
const scopes = [
|
|
340
|
-
...scopeStr.split(/\s+/).map((s) => s.trim()).filter(Boolean),
|
|
341
|
-
...permissions.map((s) => (typeof s === 'string' ? s.trim() : '')).filter(Boolean),
|
|
342
|
-
];
|
|
343
|
-
const exp = decoded.exp;
|
|
344
|
-
return {
|
|
345
|
-
token,
|
|
346
|
-
clientId: decoded.client_id || 'unknown',
|
|
347
|
-
scopes: Array.from(new Set(scopes)),
|
|
348
|
-
expiresAt: typeof exp === 'number' ? exp : Math.floor(Date.now() / 1000) + 3600,
|
|
349
|
-
};
|
|
350
359
|
},
|
|
351
360
|
};
|
|
352
361
|
const authMiddleware = requireBearerAuth({
|
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.
|
|
4
|
+
"version": "1.0.24",
|
|
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.
|
|
28
|
+
"version": "1.0.24",
|
|
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.
|
|
40
|
+
"version": "1.0.24",
|
|
41
41
|
"transport": {
|
|
42
42
|
"type": "stdio"
|
|
43
43
|
},
|