kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(mcp): implement device authorization flow with polling mechanism

Andrej 48589524 d561dc76

+110 -9
+110 -9
apps/api/src/mcp/index.ts
··· 75 75 if (!client) { 76 76 return c.json({ error: "invalid_client" }, 400); 77 77 } 78 - if ( 79 - client.redirectUris.length > 0 && 80 - !client.redirectUris.includes(redirectUri) 81 - ) { 82 - return c.json({ error: "invalid_redirect_uri" }, 400); 83 - } 84 78 85 79 const session = await auth.api.getSession({ 86 80 headers: c.req.raw.headers, ··· 99 93 return c.redirect(url.toString()); 100 94 } 101 95 102 - const returnUrl = c.req.url; 103 - const loginUrl = `${clientUrl}/auth/sign-in?redirect=${encodeURIComponent(returnUrl)}`; 104 - return c.redirect(loginUrl); 96 + const deviceRes = await fetch(`${apiUrl}/api/auth/device/code`, { 97 + method: "POST", 98 + headers: { "Content-Type": "application/json" }, 99 + body: JSON.stringify({ client_id: "kaneo-mcp" }), 100 + }); 101 + 102 + if (!deviceRes.ok) { 103 + return c.json({ error: "device_code_failed" }, 500); 104 + } 105 + 106 + const device = (await deviceRes.json()) as { 107 + device_code: string; 108 + user_code: string; 109 + verification_uri: string; 110 + interval: number; 111 + expires_in: number; 112 + }; 113 + 114 + const devicePageUrl = `${clientUrl}/device?user_code=${encodeURIComponent(device.user_code)}`; 115 + 116 + return c.html(`<!DOCTYPE html> 117 + <html lang="en"> 118 + <head> 119 + <meta charset="utf-8"> 120 + <meta name="viewport" content="width=device-width, initial-scale=1"> 121 + <title>Kaneo MCP</title> 122 + <script>window.location.href = ${JSON.stringify(devicePageUrl)};</script> 123 + </head> 124 + <body>Redirecting to Kaneo…</body> 125 + <script> 126 + const deviceCode = ${JSON.stringify(device.device_code)}; 127 + const interval = ${device.interval} * 1000; 128 + const apiUrl = ${JSON.stringify(apiUrl)}; 129 + const redirectUri = ${JSON.stringify(redirectUri)}; 130 + const codeChallenge = ${JSON.stringify(codeChallenge)}; 131 + const clientId = ${JSON.stringify(clientId)}; 132 + const state = ${JSON.stringify(state || "")}; 133 + 134 + async function poll() { 135 + try { 136 + const res = await fetch(apiUrl + "/api/auth/device/token", { 137 + method: "POST", 138 + headers: { "Content-Type": "application/json" }, 139 + body: JSON.stringify({ 140 + grant_type: "urn:ietf:params:oauth:grant-type:device_code", 141 + device_code: deviceCode, 142 + client_id: "kaneo-mcp" 143 + }) 144 + }); 145 + const data = await res.json(); 146 + 147 + if (res.ok && data.access_token) { 148 + const codeRes = await fetch(apiUrl + "/api/mcp/authorize/callback", { 149 + method: "POST", 150 + headers: { "Content-Type": "application/json" }, 151 + body: JSON.stringify({ 152 + access_token: data.access_token, 153 + client_id: clientId, 154 + code_challenge: codeChallenge, 155 + redirect_uri: redirectUri, 156 + state: state 157 + }) 158 + }); 159 + const codeData = await codeRes.json(); 160 + if (codeData.redirect) window.location.href = codeData.redirect; 161 + return; 162 + } 163 + 164 + if (data.error === "authorization_pending" || data.error === "slow_down") { 165 + setTimeout(poll, data.error === "slow_down" ? interval + 5000 : interval); 166 + return; 167 + } 168 + } catch { 169 + setTimeout(poll, interval); 170 + } 171 + } 172 + 173 + setTimeout(poll, interval); 174 + </script> 175 + </html>`); 176 + }); 177 + 178 + mcp.post("/mcp/authorize/callback", async (c) => { 179 + const body = await c.req.json(); 180 + const { access_token, client_id, code_challenge, redirect_uri, state } = body; 181 + 182 + if (!access_token || !client_id || !code_challenge || !redirect_uri) { 183 + return c.json({ error: "invalid_request" }, 400); 184 + } 185 + 186 + const headers = new Headers(); 187 + headers.set("authorization", `Bearer ${access_token}`); 188 + const session = await auth.api.getSession({ headers }); 189 + 190 + if (!session?.user?.id) { 191 + return c.json({ error: "invalid_token" }, 401); 192 + } 193 + 194 + const code = createAuthCode({ 195 + clientId: client_id, 196 + userId: session.user.id, 197 + codeChallenge: code_challenge, 198 + redirectUri: redirect_uri, 199 + }); 200 + 201 + const url = new URL(redirect_uri); 202 + url.searchParams.set("code", code); 203 + if (state) url.searchParams.set("state", state); 204 + 205 + return c.json({ redirect: url.toString() }); 105 206 }); 106 207 107 208 mcp.post("/mcp/token", async (c) => {