@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
- await ensureCallbackServer({ strictPort: Boolean(config.clientId) });
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 { OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPort, setOAuthCallbackPort, } from "./mcp-oauth-provider.js";
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
- * This must match the redirect_uri in client metadata.
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,