github-webhook-mcp 0.7.0 → 0.7.1
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/package.json +1 -1
- package/server/index.js +105 -34
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -116,7 +116,29 @@ async function ensureClientRegistration(metadata, redirectUris) {
|
|
|
116
116
|
|
|
117
117
|
// ── OAuth Localhost Callback Flow ────────────────────────────────────────────
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
// Pending OAuth state: kept alive across tool calls so the callback server
|
|
120
|
+
// can receive the authorization code even if the first tool call returns early.
|
|
121
|
+
let _pendingOAuth = null;
|
|
122
|
+
|
|
123
|
+
class OAuthPendingError extends Error {
|
|
124
|
+
constructor(authUrl) {
|
|
125
|
+
super("OAuth authentication required");
|
|
126
|
+
this.authUrl = authUrl;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function openBrowser(url) {
|
|
131
|
+
if (process.platform === "win32") {
|
|
132
|
+
// Windows `start` treats the first quoted arg as a window title.
|
|
133
|
+
// Pass an empty title so the URL is opened correctly.
|
|
134
|
+
exec(`start "" "${url}"`);
|
|
135
|
+
} else {
|
|
136
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
137
|
+
exec(`${openCmd} "${url}"`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function startOAuthFlow() {
|
|
120
142
|
const metadata = await discoverOAuthMetadata();
|
|
121
143
|
|
|
122
144
|
const callbackServer = createServer();
|
|
@@ -143,13 +165,15 @@ async function performOAuthFlow() {
|
|
|
143
165
|
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
144
166
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
145
167
|
|
|
146
|
-
|
|
168
|
+
// Promise that resolves when the callback is received
|
|
169
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
147
170
|
const timeout = setTimeout(() => {
|
|
148
171
|
callbackServer.close();
|
|
172
|
+
_pendingOAuth = null;
|
|
149
173
|
reject(new Error("OAuth callback timed out after 5 minutes"));
|
|
150
174
|
}, 5 * 60 * 1000);
|
|
151
175
|
|
|
152
|
-
callbackServer.on("request", (req, res) => {
|
|
176
|
+
callbackServer.on("request", async (req, res) => {
|
|
153
177
|
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
154
178
|
if (url.pathname !== "/callback") {
|
|
155
179
|
res.writeHead(404);
|
|
@@ -166,6 +190,7 @@ async function performOAuthFlow() {
|
|
|
166
190
|
res.end("<html><body><h1>Authorization failed</h1><p>You can close this tab.</p></body></html>");
|
|
167
191
|
clearTimeout(timeout);
|
|
168
192
|
callbackServer.close();
|
|
193
|
+
_pendingOAuth = null;
|
|
169
194
|
reject(new Error(`OAuth authorization failed: ${error}`));
|
|
170
195
|
return;
|
|
171
196
|
}
|
|
@@ -180,46 +205,81 @@ async function performOAuthFlow() {
|
|
|
180
205
|
res.end("<html><body><h1>Authorization successful</h1><p>You can close this tab.</p></body></html>");
|
|
181
206
|
clearTimeout(timeout);
|
|
182
207
|
callbackServer.close();
|
|
183
|
-
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const tokenRes = await fetch(metadata.token_endpoint, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
213
|
+
body: new URLSearchParams({
|
|
214
|
+
grant_type: "authorization_code",
|
|
215
|
+
code,
|
|
216
|
+
redirect_uri: redirectUri,
|
|
217
|
+
client_id: client.client_id,
|
|
218
|
+
code_verifier: codeVerifier,
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!tokenRes.ok) {
|
|
223
|
+
_pendingOAuth = null;
|
|
224
|
+
reject(new Error(`Token exchange failed: ${tokenRes.status} ${await tokenRes.text()}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const tokenData = await tokenRes.json();
|
|
229
|
+
const tokens = {
|
|
230
|
+
access_token: tokenData.access_token,
|
|
231
|
+
refresh_token: tokenData.refresh_token,
|
|
232
|
+
expires_at: tokenData.expires_in
|
|
233
|
+
? Date.now() + tokenData.expires_in * 1000
|
|
234
|
+
: undefined,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
await saveTokens(tokens);
|
|
238
|
+
_pendingOAuth = null;
|
|
239
|
+
resolve(tokens);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
_pendingOAuth = null;
|
|
242
|
+
reject(err);
|
|
243
|
+
}
|
|
184
244
|
});
|
|
245
|
+
});
|
|
185
246
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
247
|
+
// Try to open the browser
|
|
248
|
+
openBrowser(authUrl.toString());
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`\n[github-webhook-mcp] Open this URL to authenticate:\n${authUrl.toString()}\n\n`,
|
|
251
|
+
);
|
|
189
252
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
193
|
-
});
|
|
253
|
+
// Store pending state so subsequent tool calls can await or re-surface the URL
|
|
254
|
+
_pendingOAuth = { authUrl: authUrl.toString(), tokenPromise };
|
|
194
255
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
198
|
-
body: new URLSearchParams({
|
|
199
|
-
grant_type: "authorization_code",
|
|
200
|
-
code: authCode,
|
|
201
|
-
redirect_uri: redirectUri,
|
|
202
|
-
client_id: client.client_id,
|
|
203
|
-
code_verifier: codeVerifier,
|
|
204
|
-
}),
|
|
205
|
-
});
|
|
256
|
+
return _pendingOAuth;
|
|
257
|
+
}
|
|
206
258
|
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
async function performOAuthFlow() {
|
|
260
|
+
// If an OAuth flow is already in progress, check if it completed
|
|
261
|
+
if (_pendingOAuth) {
|
|
262
|
+
// Race: either the token is ready or we return the URL again
|
|
263
|
+
const result = await Promise.race([
|
|
264
|
+
_pendingOAuth.tokenPromise,
|
|
265
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2000)),
|
|
266
|
+
]);
|
|
267
|
+
if (result && result.access_token) return result;
|
|
268
|
+
throw new OAuthPendingError(_pendingOAuth.authUrl);
|
|
209
269
|
}
|
|
210
270
|
|
|
211
|
-
|
|
271
|
+
// Start a new OAuth flow
|
|
272
|
+
const pending = await startOAuthFlow();
|
|
212
273
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
274
|
+
// Wait briefly for the browser-opened flow to complete (e.g. auto-open worked)
|
|
275
|
+
const result = await Promise.race([
|
|
276
|
+
pending.tokenPromise,
|
|
277
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
|
|
278
|
+
]);
|
|
279
|
+
if (result && result.access_token) return result;
|
|
220
280
|
|
|
221
|
-
|
|
222
|
-
|
|
281
|
+
// Browser likely didn't open or user hasn't authenticated yet — surface the URL
|
|
282
|
+
throw new OAuthPendingError(pending.authUrl);
|
|
223
283
|
}
|
|
224
284
|
|
|
225
285
|
async function refreshAccessToken(refreshToken) {
|
|
@@ -466,6 +526,17 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
466
526
|
try {
|
|
467
527
|
return await callRemoteTool(name, args ?? {});
|
|
468
528
|
} catch (err) {
|
|
529
|
+
if (err instanceof OAuthPendingError) {
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: `Authentication required. Please open this URL to authorize:\n${err.authUrl}\n\nAfter authorizing in the browser, retry the tool call.`,
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
isError: true,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
469
540
|
return {
|
|
470
541
|
content: [{ type: "text", text: `Failed to reach worker: ${err}` }],
|
|
471
542
|
isError: true,
|