@aexol/spectral 0.2.10 → 0.2.11
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.
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { auth as runSdkAuth, UnauthorizedError, } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
7
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
8
|
import open from "open";
|
|
9
|
-
import { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
9
|
+
import { McpOAuthProvider, setOAuthCallbackPort } from "./mcp-oauth-provider.js";
|
|
10
10
|
import { ensureCallbackServer, waitForCallback, cancelPendingCallback, stopCallbackServer, } from "./mcp-callback-server.js";
|
|
11
11
|
import { getAuthForUrl, isTokenExpired, hasStoredTokens, clearAllCredentials, updateOAuthState, getOAuthState, clearOAuthState, } from "./mcp-auth.js";
|
|
12
12
|
// Track pending transports for auth completion
|
|
@@ -21,6 +21,22 @@ function generateState() {
|
|
|
21
21
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
22
22
|
.join("");
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract the port from a redirect URI. Returns undefined if no explicit port is set.
|
|
26
|
+
*/
|
|
27
|
+
function parseRedirectPort(redirectUri) {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(redirectUri);
|
|
30
|
+
if (url.port) {
|
|
31
|
+
return parseInt(url.port, 10);
|
|
32
|
+
}
|
|
33
|
+
// No explicit port - return undefined to use default
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
24
40
|
/**
|
|
25
41
|
* Extract OAuth configuration from a ServerEntry.
|
|
26
42
|
*/
|
|
@@ -34,6 +50,7 @@ function extractOAuthConfig(definition) {
|
|
|
34
50
|
clientId: definition.oauth?.clientId,
|
|
35
51
|
clientSecret: definition.oauth?.clientSecret,
|
|
36
52
|
scope: definition.oauth?.scope,
|
|
53
|
+
redirectUri: definition.oauth?.redirectUri,
|
|
37
54
|
};
|
|
38
55
|
}
|
|
39
56
|
/**
|
|
@@ -56,7 +73,16 @@ export async function startAuth(serverName, serverUrl, definition) {
|
|
|
56
73
|
}
|
|
57
74
|
// Start the callback server.
|
|
58
75
|
// Pre-registered OAuth clients require an exact redirect URI, so enforce strict port binding.
|
|
59
|
-
|
|
76
|
+
// When a custom redirectUri is specified, also use strict port binding and set the port.
|
|
77
|
+
const hasCustomRedirect = Boolean(config.redirectUri);
|
|
78
|
+
const redirectPort = hasCustomRedirect ? parseRedirectPort(config.redirectUri) : undefined;
|
|
79
|
+
if (redirectPort) {
|
|
80
|
+
setOAuthCallbackPort(redirectPort);
|
|
81
|
+
}
|
|
82
|
+
await ensureCallbackServer({
|
|
83
|
+
strictPort: Boolean(config.clientId) || hasCustomRedirect,
|
|
84
|
+
preferredPort: redirectPort,
|
|
85
|
+
});
|
|
60
86
|
const oauthState = generateState();
|
|
61
87
|
await updateOAuthState(serverName, oauthState);
|
|
62
88
|
let capturedUrl;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses Node.js http module for compatibility.
|
|
6
6
|
*/
|
|
7
7
|
import { createServer } from "http";
|
|
8
|
-
import {
|
|
8
|
+
import { getConfiguredOAuthCallbackPort, getOAuthCallbackPort, setOAuthCallbackPort, } from "./mcp-oauth-provider.js";
|
|
9
9
|
// HTML templates for callback responses
|
|
10
10
|
const HTML_SUCCESS = `<!DOCTYPE html>
|
|
11
11
|
<html>
|
|
@@ -57,16 +57,17 @@ const MAX_PORT_SCAN_ATTEMPTS = 25;
|
|
|
57
57
|
*/
|
|
58
58
|
function handleRequest(req, res) {
|
|
59
59
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
60
|
-
// Only handle the callback path
|
|
61
|
-
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
62
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
63
|
-
res.end("Not found");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
60
|
const code = url.searchParams.get("code");
|
|
67
61
|
const state = url.searchParams.get("state");
|
|
68
62
|
const error = url.searchParams.get("error");
|
|
69
63
|
const errorDescription = url.searchParams.get("error_description");
|
|
64
|
+
// Accept callbacks on any path that carries OAuth query params.
|
|
65
|
+
// This supports custom redirectUri paths configured per-server.
|
|
66
|
+
if (!code && !state && !error) {
|
|
67
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
68
|
+
res.end("Not found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
70
71
|
// Enforce state parameter presence for CSRF protection
|
|
71
72
|
if (!state) {
|
|
72
73
|
const errorMsg = "Missing required state parameter - potential CSRF attack";
|
|
@@ -116,7 +117,7 @@ function handleRequest(req, res) {
|
|
|
116
117
|
* If strictPort is false, scans forward for an available local port.
|
|
117
118
|
*/
|
|
118
119
|
export async function ensureCallbackServer(options = {}) {
|
|
119
|
-
const configuredPort = getConfiguredOAuthCallbackPort();
|
|
120
|
+
const configuredPort = options.preferredPort ?? getConfiguredOAuthCallbackPort();
|
|
120
121
|
const strictPort = options.strictPort === true;
|
|
121
122
|
if (server) {
|
|
122
123
|
if (!strictPort || getOAuthCallbackPort() === configuredPort)
|
|
@@ -46,11 +46,13 @@ export class McpOAuthProvider {
|
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
48
|
* The redirect URL for OAuth callbacks.
|
|
49
|
-
*
|
|
49
|
+
* Uses configured redirectUri if provided, otherwise falls back to default.
|
|
50
50
|
*/
|
|
51
51
|
get redirectUrl() {
|
|
52
52
|
if (this.usesClientCredentials)
|
|
53
53
|
return undefined;
|
|
54
|
+
if (this.config.redirectUri)
|
|
55
|
+
return this.config.redirectUri;
|
|
54
56
|
return `http://localhost:${getOAuthCallbackPort()}${OAUTH_CALLBACK_PATH}`;
|
|
55
57
|
}
|
|
56
58
|
/**
|