@taskforcehq/taskforce 0.3.318 → 0.3.319
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/server/routes/admin.js +135 -28
- package/dist/server/routes.js +57 -25
- package/package.json +1 -1
|
@@ -259,11 +259,19 @@ export function registerAdminRoutes(deps) {
|
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
261
|
const renderMcpOauthAuthorizePage = (input) => {
|
|
262
|
+
const scopeLabels = {
|
|
263
|
+
'tasks:read': 'Read tasks',
|
|
264
|
+
'tasks:write': 'Update tasks',
|
|
265
|
+
'workspace:read': 'Read workspace details'
|
|
266
|
+
};
|
|
262
267
|
const options = input.workspaces.map((workspace) => {
|
|
263
268
|
const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
|
|
264
269
|
return `<option value="${escapeHtml(workspace.id)}"${selected}>${escapeHtml(workspace.name)} (${escapeHtml(workspace.role)})</option>`;
|
|
265
270
|
}).join('');
|
|
266
|
-
const scopeBadges = input.scopes.map((scope) =>
|
|
271
|
+
const scopeBadges = input.scopes.map((scope) => {
|
|
272
|
+
const label = scopeLabels[scope] || scope;
|
|
273
|
+
return `<span class="scope-chip">${escapeHtml(label)}</span>`;
|
|
274
|
+
}).join('');
|
|
267
275
|
const termsUrl = 'https://taskforcehq.ai/legal/terms/';
|
|
268
276
|
const privacyUrl = 'https://taskforcehq.ai/legal/privacy/';
|
|
269
277
|
return `<!doctype html>
|
|
@@ -272,41 +280,65 @@ export function registerAdminRoutes(deps) {
|
|
|
272
280
|
<meta charset="utf-8" />
|
|
273
281
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
274
282
|
<title>Authorize Taskforce MCP Connector</title>
|
|
283
|
+
<link rel="stylesheet" href="/styles/fonts.css" />
|
|
275
284
|
<style>
|
|
276
285
|
:root { color-scheme: dark; }
|
|
277
|
-
body { margin: 0; font-family:
|
|
278
|
-
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
279
|
-
.card { width: min(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
.
|
|
286
|
-
.meta
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
body { margin: 0; font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: radial-gradient(circle at top, rgba(255,138,0,0.08), transparent 30%), #111126; color: #f8f7ff; }
|
|
287
|
+
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
|
|
288
|
+
.card { width: min(680px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 44px 56px; }
|
|
289
|
+
.eyebrow { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 18px; color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
|
|
290
|
+
.eyebrow-mark { width: 10px; height: 10px; border-radius: 999px; background: linear-gradient(135deg, #ff8a00 0%, #8d6bff 100%); box-shadow: 0 0 18px rgba(255,138,0,0.35); }
|
|
291
|
+
h1 { margin: 0; font-size: clamp(2.25rem, 5vw, 3rem); line-height: 0.96; letter-spacing: -0.04em; color: #f8f7ff; }
|
|
292
|
+
.brand-word { font-family: "Russo One", sans-serif; font-size: 0.94em; letter-spacing: -0.03em; }
|
|
293
|
+
.lede { margin: 18px 0 8px; font-size: 1.15rem; line-height: 1.5; color: rgba(255,255,255,0.78); max-width: 34ch; }
|
|
294
|
+
.lede strong { color: #fff; }
|
|
295
|
+
.user-meta { margin: 0 0 28px; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
|
|
296
|
+
label { display: block; font-size: 13px; font-weight: 700; margin: 24px 0 10px; color: rgba(255,255,255,0.92); letter-spacing: 0.01em; }
|
|
297
|
+
select { width: 100%; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: #1f2039; color: #fff; padding: 16px 18px; font-size: 15px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); }
|
|
298
|
+
.scopes { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
|
|
299
|
+
.scope-chip { display:inline-flex; padding:8px 14px; border-radius:999px; background:#2a2947; border:1px solid rgba(255,255,255,0.09); font-size:13px; font-weight:600; color:rgba(255,255,255,0.92); }
|
|
300
|
+
.details { margin-top: 24px; padding: 0; border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; background: rgba(255,255,255,0.04); overflow: hidden; }
|
|
301
|
+
.details summary { list-style: none; cursor: pointer; padding: 16px 18px; font-size: 0.92rem; font-weight: 700; color: rgba(255,255,255,0.86); }
|
|
302
|
+
.details summary::-webkit-details-marker { display: none; }
|
|
303
|
+
.details-body { padding: 0 18px 18px; color: rgba(255,255,255,0.68); font-size: 0.9rem; line-height: 1.5; }
|
|
304
|
+
.details-row { margin: 0 0 8px; }
|
|
305
|
+
.details-row strong { color: rgba(255,255,255,0.92); }
|
|
306
|
+
.legal { margin: 24px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 56ch; }
|
|
307
|
+
.error { margin-bottom: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
|
|
308
|
+
.actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
|
|
309
|
+
button { border-radius: 18px; padding: 15px 22px; font-size: 15px; font-weight: 700; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); min-width: 156px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; }
|
|
310
|
+
button:hover { transform: translateY(-1px); }
|
|
311
|
+
button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
|
|
312
|
+
button[name="decision"][value="approve"] { background: linear-gradient(135deg, #8d6bff 0%, #7c59f1 100%); color: white; border-color: rgba(141,107,255,0.6); box-shadow: 0 14px 28px rgba(141,107,255,0.26); }
|
|
313
|
+
a { color: #cbd8ff; }
|
|
314
|
+
@media (max-width: 720px) {
|
|
315
|
+
.card { padding: 30px 22px; border-radius: 28px; }
|
|
316
|
+
h1 { font-size: 2rem; }
|
|
317
|
+
.actions { flex-direction: column-reverse; }
|
|
318
|
+
button { width: 100%; }
|
|
319
|
+
}
|
|
293
320
|
</style>
|
|
294
321
|
</head>
|
|
295
322
|
<body>
|
|
296
323
|
<div class="wrap">
|
|
297
324
|
<form class="card" method="post" action="/oauth/mcp/authorize">
|
|
298
|
-
<
|
|
299
|
-
<
|
|
325
|
+
<div class="eyebrow"><span class="eyebrow-mark"></span>Taskforce MCP</div>
|
|
326
|
+
<h1>Authorize <span class="brand-word">Taskforce</span> Connector</h1>
|
|
327
|
+
<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
|
|
328
|
+
${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
|
|
300
329
|
${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
|
|
301
330
|
<label for="workspaceId">Workspace</label>
|
|
302
331
|
<select id="workspaceId" name="workspaceId" required>${options}</select>
|
|
303
|
-
<label>Requested
|
|
332
|
+
<label>Requested Permissions</label>
|
|
304
333
|
<div class="scopes">${scopeBadges}</div>
|
|
305
|
-
<
|
|
306
|
-
<
|
|
307
|
-
<div
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
<details class="details">
|
|
335
|
+
<summary>Technical details</summary>
|
|
336
|
+
<div class="details-body">
|
|
337
|
+
<div class="details-row"><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
|
|
338
|
+
<div class="details-row"><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
|
|
339
|
+
</div>
|
|
340
|
+
</details>
|
|
341
|
+
<p class="legal">By authorizing this connector, you agree to Taskforce's <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
|
|
310
342
|
<input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
|
|
311
343
|
<input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
|
|
312
344
|
<input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
|
|
@@ -2481,13 +2513,15 @@ export function registerAdminRoutes(deps) {
|
|
|
2481
2513
|
addRoute('POST', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2482
2514
|
addRoute('GET', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2483
2515
|
addRoute('GET', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2516
|
+
addRoute('HEAD', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2517
|
+
addRoute('HEAD', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2484
2518
|
addRoute('DELETE', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2485
2519
|
addRoute('DELETE', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
|
|
2486
2520
|
addRoute('OPTIONS', '/mcp', async (_req, res) => {
|
|
2487
2521
|
res.writeHead(204, {
|
|
2488
2522
|
'Access-Control-Allow-Origin': '*',
|
|
2489
2523
|
'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-icon, x-taskforce-ai-color, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
|
|
2490
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
|
|
2524
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
|
|
2491
2525
|
});
|
|
2492
2526
|
res.end();
|
|
2493
2527
|
});
|
|
@@ -2495,7 +2529,7 @@ export function registerAdminRoutes(deps) {
|
|
|
2495
2529
|
res.writeHead(204, {
|
|
2496
2530
|
'Access-Control-Allow-Origin': '*',
|
|
2497
2531
|
'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-icon, x-taskforce-ai-color, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
|
|
2498
|
-
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
|
|
2532
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS'
|
|
2499
2533
|
});
|
|
2500
2534
|
res.end();
|
|
2501
2535
|
});
|
|
@@ -2515,6 +2549,21 @@ export function registerAdminRoutes(deps) {
|
|
|
2515
2549
|
scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
2516
2550
|
}));
|
|
2517
2551
|
});
|
|
2552
|
+
addRoute('HEAD', '/.well-known/oauth-authorization-server', async (req, res) => {
|
|
2553
|
+
const issuer = resolveMcpOauthIssuer(req);
|
|
2554
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2555
|
+
res.end(JSON.stringify({
|
|
2556
|
+
issuer,
|
|
2557
|
+
authorization_endpoint: resolveMcpOauthAuthorizeUrl(req),
|
|
2558
|
+
token_endpoint: resolveMcpOauthTokenUrl(req),
|
|
2559
|
+
registration_endpoint: resolveMcpOauthRegisterUrl(req),
|
|
2560
|
+
response_types_supported: ['code'],
|
|
2561
|
+
grant_types_supported: ['authorization_code'],
|
|
2562
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
2563
|
+
code_challenge_methods_supported: ['S256'],
|
|
2564
|
+
scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
2565
|
+
}));
|
|
2566
|
+
});
|
|
2518
2567
|
addRoute('GET', '/.well-known/oauth-protected-resource', async (req, res) => {
|
|
2519
2568
|
const issuer = resolveMcpOauthIssuer(req);
|
|
2520
2569
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2524,6 +2573,15 @@ export function registerAdminRoutes(deps) {
|
|
|
2524
2573
|
bearer_methods_supported: ['header'],
|
|
2525
2574
|
}));
|
|
2526
2575
|
});
|
|
2576
|
+
addRoute('HEAD', '/.well-known/oauth-protected-resource', async (req, res) => {
|
|
2577
|
+
const issuer = resolveMcpOauthIssuer(req);
|
|
2578
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2579
|
+
res.end(JSON.stringify({
|
|
2580
|
+
resource: resolveMcpOauthResourceUrl(req),
|
|
2581
|
+
authorization_servers: [issuer],
|
|
2582
|
+
bearer_methods_supported: ['header'],
|
|
2583
|
+
}));
|
|
2584
|
+
});
|
|
2527
2585
|
addRoute('POST', '/oauth/mcp/register', async (req, res) => {
|
|
2528
2586
|
if (!ensureAppAccess(req, res))
|
|
2529
2587
|
return;
|
|
@@ -2606,8 +2664,57 @@ export function registerAdminRoutes(deps) {
|
|
|
2606
2664
|
if (workspaces.length === 0)
|
|
2607
2665
|
throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
|
|
2608
2666
|
const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
|
|
2667
|
+
const userIdentity = core.resolveUserIdentity(access.userId);
|
|
2668
|
+
const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
|
|
2669
|
+
const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
|
|
2670
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2671
|
+
res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
|
|
2672
|
+
}
|
|
2673
|
+
catch (error) {
|
|
2674
|
+
const statusCode = resolveErrorStatusCode(error);
|
|
2675
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
2676
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: error.message, code: error?.code || 'MCP_OAUTH_AUTHORIZE_FAILED' }));
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
addRoute('HEAD', '/oauth/mcp/authorize', async (req, res) => {
|
|
2680
|
+
if (!ensureAppAccess(req, res))
|
|
2681
|
+
return;
|
|
2682
|
+
const reqUrl = new URL(req.url || '/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
|
|
2683
|
+
const clientId = String(reqUrl.searchParams.get('client_id') || '').trim();
|
|
2684
|
+
const redirectUri = String(reqUrl.searchParams.get('redirect_uri') || '').trim();
|
|
2685
|
+
const responseType = String(reqUrl.searchParams.get('response_type') || '').trim();
|
|
2686
|
+
const state = String(reqUrl.searchParams.get('state') || '').trim();
|
|
2687
|
+
const codeChallenge = String(reqUrl.searchParams.get('code_challenge') || '').trim();
|
|
2688
|
+
const codeChallengeMethod = String(reqUrl.searchParams.get('code_challenge_method') || 'S256').trim() || 'S256';
|
|
2689
|
+
const scopes = normalizeMcpOauthScopeString(reqUrl.searchParams.get('scope'));
|
|
2690
|
+
if (responseType !== 'code' || !clientId || !redirectUri || !codeChallenge) {
|
|
2691
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2692
|
+
res.end(JSON.stringify({ error: 'invalid_request', error_description: 'client_id, redirect_uri, response_type=code, and code_challenge are required.' }));
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
const access = context.authConfig ? resolveRequestAccess(req, context.authConfig) : null;
|
|
2696
|
+
if (!access?.authenticated || !access.userId || access.userId === 'anonymous') {
|
|
2697
|
+
res.writeHead(302, { Location: `${resolveCloudLoginBaseUrl(req)}/login?next=${encodeURIComponent(reqUrl.toString())}` });
|
|
2698
|
+
res.end();
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
try {
|
|
2702
|
+
const clientRecord = core.getMcpOAuthClient(clientId);
|
|
2703
|
+
if (!clientRecord)
|
|
2704
|
+
throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
|
|
2705
|
+
if (!clientRecord.redirectUris.includes(redirectUri))
|
|
2706
|
+
throw new TaskforceRuleError('Redirect URI is not registered for this OAuth client', 400, 'MCP_OAUTH_REDIRECT_URI_INVALID');
|
|
2707
|
+
const workspaces = core.listUserWorkspaces(access.userId)
|
|
2708
|
+
.map((workspace) => ({ id: String(workspace.id || '').trim(), name: String(workspace.name || workspace.slug || workspace.id || 'Workspace'), role: String(workspace.role || 'member') }))
|
|
2709
|
+
.filter((workspace) => Boolean(workspace.id));
|
|
2710
|
+
if (workspaces.length === 0)
|
|
2711
|
+
throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
|
|
2712
|
+
const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
|
|
2713
|
+
const userIdentity = core.resolveUserIdentity(access.userId);
|
|
2714
|
+
const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
|
|
2715
|
+
const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
|
|
2609
2716
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2610
|
-
res.end(renderMcpOauthAuthorizePage({ userName
|
|
2717
|
+
res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
|
|
2611
2718
|
}
|
|
2612
2719
|
catch (error) {
|
|
2613
2720
|
const statusCode = resolveErrorStatusCode(error);
|
package/dist/server/routes.js
CHANGED
|
@@ -1262,11 +1262,19 @@ export function createRoutes(core, context = {}) {
|
|
|
1262
1262
|
const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
|
|
1263
1263
|
const resolveMcpOauthResourceUrl = (req) => `${resolveMcpOauthIssuer(req)}/mcp`;
|
|
1264
1264
|
const renderMcpOauthAuthorizePage = (input) => {
|
|
1265
|
+
const scopeLabels = {
|
|
1266
|
+
'tasks:read': 'Read tasks',
|
|
1267
|
+
'tasks:write': 'Update tasks',
|
|
1268
|
+
'workspace:read': 'Read workspace details'
|
|
1269
|
+
};
|
|
1265
1270
|
const options = input.workspaces.map((workspace) => {
|
|
1266
1271
|
const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
|
|
1267
1272
|
return `<option value="${escapeHtml(workspace.id)}"${selected}>${escapeHtml(workspace.name)} (${escapeHtml(workspace.role)})</option>`;
|
|
1268
1273
|
}).join('');
|
|
1269
|
-
const scopeBadges = input.scopes.map((scope) =>
|
|
1274
|
+
const scopeBadges = input.scopes.map((scope) => {
|
|
1275
|
+
const label = scopeLabels[scope] || scope;
|
|
1276
|
+
return `<span class="scope-chip">${escapeHtml(label)}</span>`;
|
|
1277
|
+
}).join('');
|
|
1270
1278
|
const termsUrl = 'https://taskforcehq.ai/legal/terms/';
|
|
1271
1279
|
const privacyUrl = 'https://taskforcehq.ai/legal/privacy/';
|
|
1272
1280
|
return `<!doctype html>
|
|
@@ -1275,41 +1283,65 @@ export function createRoutes(core, context = {}) {
|
|
|
1275
1283
|
<meta charset="utf-8" />
|
|
1276
1284
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1277
1285
|
<title>Authorize Taskforce MCP Connector</title>
|
|
1286
|
+
<link rel="stylesheet" href="/styles/fonts.css" />
|
|
1278
1287
|
<style>
|
|
1279
1288
|
:root { color-scheme: dark; }
|
|
1280
|
-
body { margin: 0; font-family:
|
|
1281
|
-
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
1282
|
-
.card { width: min(
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
.
|
|
1288
|
-
.
|
|
1289
|
-
.meta
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1289
|
+
body { margin: 0; font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: radial-gradient(circle at top, rgba(255,138,0,0.08), transparent 30%), #111126; color: #f8f7ff; }
|
|
1290
|
+
.wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
|
|
1291
|
+
.card { width: min(680px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 44px 56px; }
|
|
1292
|
+
.eyebrow { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 18px; color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
|
|
1293
|
+
.eyebrow-mark { width: 10px; height: 10px; border-radius: 999px; background: linear-gradient(135deg, #ff8a00 0%, #8d6bff 100%); box-shadow: 0 0 18px rgba(255,138,0,0.35); }
|
|
1294
|
+
h1 { margin: 0; font-size: clamp(2.25rem, 5vw, 3rem); line-height: 0.96; letter-spacing: -0.04em; color: #f8f7ff; }
|
|
1295
|
+
.brand-word { font-family: "Russo One", sans-serif; font-size: 0.94em; letter-spacing: -0.03em; }
|
|
1296
|
+
.lede { margin: 18px 0 8px; font-size: 1.15rem; line-height: 1.5; color: rgba(255,255,255,0.78); max-width: 34ch; }
|
|
1297
|
+
.lede strong { color: #fff; }
|
|
1298
|
+
.user-meta { margin: 0 0 28px; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
|
|
1299
|
+
label { display: block; font-size: 13px; font-weight: 700; margin: 24px 0 10px; color: rgba(255,255,255,0.92); letter-spacing: 0.01em; }
|
|
1300
|
+
select { width: 100%; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: #1f2039; color: #fff; padding: 16px 18px; font-size: 15px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); }
|
|
1301
|
+
.scopes { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
|
|
1302
|
+
.scope-chip { display:inline-flex; padding:8px 14px; border-radius:999px; background:#2a2947; border:1px solid rgba(255,255,255,0.09); font-size:13px; font-weight:600; color:rgba(255,255,255,0.92); }
|
|
1303
|
+
.details { margin-top: 24px; padding: 0; border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; background: rgba(255,255,255,0.04); overflow: hidden; }
|
|
1304
|
+
.details summary { list-style: none; cursor: pointer; padding: 16px 18px; font-size: 0.92rem; font-weight: 700; color: rgba(255,255,255,0.86); }
|
|
1305
|
+
.details summary::-webkit-details-marker { display: none; }
|
|
1306
|
+
.details-body { padding: 0 18px 18px; color: rgba(255,255,255,0.68); font-size: 0.9rem; line-height: 1.5; }
|
|
1307
|
+
.details-row { margin: 0 0 8px; }
|
|
1308
|
+
.details-row strong { color: rgba(255,255,255,0.92); }
|
|
1309
|
+
.legal { margin: 24px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 56ch; }
|
|
1310
|
+
.error { margin-bottom: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
|
|
1311
|
+
.actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
|
|
1312
|
+
button { border-radius: 18px; padding: 15px 22px; font-size: 15px; font-weight: 700; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); min-width: 156px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; }
|
|
1313
|
+
button:hover { transform: translateY(-1px); }
|
|
1314
|
+
button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.88); border-color: rgba(255,255,255,0.12); }
|
|
1315
|
+
button[name="decision"][value="approve"] { background: linear-gradient(135deg, #8d6bff 0%, #7c59f1 100%); color: white; border-color: rgba(141,107,255,0.6); box-shadow: 0 14px 28px rgba(141,107,255,0.26); }
|
|
1316
|
+
a { color: #cbd8ff; }
|
|
1317
|
+
@media (max-width: 720px) {
|
|
1318
|
+
.card { padding: 30px 22px; border-radius: 28px; }
|
|
1319
|
+
h1 { font-size: 2rem; }
|
|
1320
|
+
.actions { flex-direction: column-reverse; }
|
|
1321
|
+
button { width: 100%; }
|
|
1322
|
+
}
|
|
1296
1323
|
</style>
|
|
1297
1324
|
</head>
|
|
1298
1325
|
<body>
|
|
1299
1326
|
<div class="wrap">
|
|
1300
1327
|
<form class="card" method="post" action="/oauth/mcp/authorize">
|
|
1301
|
-
<
|
|
1302
|
-
<
|
|
1328
|
+
<div class="eyebrow"><span class="eyebrow-mark"></span>Taskforce MCP</div>
|
|
1329
|
+
<h1>Authorize <span class="brand-word">Taskforce</span> Connector</h1>
|
|
1330
|
+
<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
|
|
1331
|
+
${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
|
|
1303
1332
|
${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
|
|
1304
1333
|
<label for="workspaceId">Workspace</label>
|
|
1305
1334
|
<select id="workspaceId" name="workspaceId" required>${options}</select>
|
|
1306
|
-
<label>Requested
|
|
1335
|
+
<label>Requested Permissions</label>
|
|
1307
1336
|
<div class="scopes">${scopeBadges}</div>
|
|
1308
|
-
<
|
|
1309
|
-
<
|
|
1310
|
-
<div
|
|
1311
|
-
|
|
1312
|
-
|
|
1337
|
+
<details class="details">
|
|
1338
|
+
<summary>Technical details</summary>
|
|
1339
|
+
<div class="details-body">
|
|
1340
|
+
<div class="details-row"><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
|
|
1341
|
+
<div class="details-row"><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
|
|
1342
|
+
</div>
|
|
1343
|
+
</details>
|
|
1344
|
+
<p class="legal">By authorizing this connector, you agree to Taskforce's <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
|
|
1313
1345
|
<input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
|
|
1314
1346
|
<input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
|
|
1315
1347
|
<input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskforcehq/taskforce",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.319",
|
|
4
4
|
"description": "Shared task and planning workspace for humans collaborating with AI (public beta)",
|
|
5
5
|
"author": "Taskforce HQ <hello@taskforcehq.ai> (https://taskforcehq.ai)",
|
|
6
6
|
"license": "MIT",
|