@crush-protocol/mcp-client 0.1.14 → 0.3.0

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/cli.js CHANGED
@@ -2,9 +2,12 @@
2
2
  import "dotenv/config";
3
3
  import { BacktestClient } from "./backtest/backtestClient.js";
4
4
  import { ClickHouseDirectClient } from "./clickhouse/directClient.js";
5
+ import { OAuthRemoteMcpClient } from "./mcp/oauthRemoteClient.js";
5
6
  import { RemoteMcpClient } from "./mcp/remoteClient.js";
7
+ import { installClientConfig } from "./setup/setupClients.js";
8
+ const DEFAULT_MCP_SERVER_URL = "https://crush-mcp-ats.dev.xexlab.com/mcp";
6
9
  const printUsage = () => {
7
- console.log(`\ncrush-mcp-client\n\nGeneral:\n tools:list [--url URL] [--token TOKEN]\n tool:call --name TOOL_NAME [--args JSON] [--url URL] [--token TOKEN]\n ping [--url URL] [--token TOKEN]\n\nBacktest:\n backtest:schema [--url URL] [--token TOKEN]\n backtest:tokens [--platform PLATFORM] [--url URL] [--token TOKEN]\n backtest:validate --expression JSON [--data-source kline|factors] [--url URL] [--token TOKEN]\n backtest:create --config JSON [--backtest-id ID] [--url URL] [--token TOKEN]\n backtest:get --backtest-id ID [--url URL] [--token TOKEN]\n backtest:list [--status STATUS] [--limit N] [--offset N] [--url URL] [--token TOKEN]\n\nClickHouse:\n clickhouse:list-tables [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB]\n clickhouse:query --sql SQL [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB --ch-row-cap N]\n\nEnv fallback:\n CRUSH_MCP_SERVER_URL, CRUSH_MCP_TOKEN\n CH_HOST, CH_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CH_ROW_CAP\n`);
10
+ console.log(`\ncrush-mcp-client\n\nGeneral:\n login [--url URL]\n setup [--cursor] [--claude] [--codex] [--gemini] [--opencode] [--all] [--scope user|project] [--url URL]\n tools:list [--url URL] [--token TOKEN]\n tool:call --name TOOL_NAME [--args JSON] [--url URL] [--token TOKEN]\n ping [--url URL] [--token TOKEN]\n\nBacktest:\n backtest:schema [--url URL] [--token TOKEN]\n backtest:tokens [--platform PLATFORM] [--url URL] [--token TOKEN]\n backtest:validate --expression JSON [--data-source kline|factors] [--url URL] [--token TOKEN]\n backtest:create --config JSON [--backtest-id ID] [--url URL] [--token TOKEN]\n backtest:list [--status STATUS] [--limit N] [--offset N] [--url URL] [--token TOKEN]\n\nClickHouse:\n clickhouse:list-tables [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB]\n clickhouse:query --sql SQL [--ch-host HOST --ch-port PORT --ch-user USER --ch-password PASS --ch-database DB --ch-row-cap N]\n\nAuth:\n --token TOKEN uses a provided OAuth access token.\n Without --token, OAuth runs automatically in the browser when needed.\n\nEnv:\n CRUSH_MCP_SERVER_URL, CRUSH_OAUTH_ACCESS_TOKEN\n CH_HOST, CH_PORT, CH_USER, CH_PASSWORD, CH_DATABASE, CH_ROW_CAP\n`);
8
11
  };
9
12
  const parseFlags = (args) => {
10
13
  const flags = {};
@@ -29,34 +32,60 @@ const requireString = (value, message) => {
29
32
  }
30
33
  return value;
31
34
  };
32
- const createRemoteClient = (flags) => {
33
- const serverUrl = typeof flags.url === "string"
34
- ? flags.url
35
- : (process.env.CRUSH_MCP_SERVER_URL ?? "https://mcp.crush-protocol.com/mcp");
36
- const token = typeof flags.token === "string"
37
- ? flags.token
38
- : requireString(process.env.CRUSH_MCP_TOKEN, "Missing token. Use --token or CRUSH_MCP_TOKEN");
39
- return new RemoteMcpClient({ serverUrl, token });
35
+ const getServerUrl = (flags) => typeof flags.url === "string"
36
+ ? flags.url
37
+ : (process.env.CRUSH_MCP_SERVER_URL ?? DEFAULT_MCP_SERVER_URL);
38
+ const getSetupTargets = (flags) => {
39
+ const explicitTargets = ["cursor", "claude", "codex", "gemini", "opencode"].filter((target) => flags[target] === true);
40
+ if (flags.all === true) {
41
+ return ["cursor", "claude", "codex", "gemini", "opencode"];
42
+ }
43
+ return explicitTargets;
44
+ };
45
+ /**
46
+ * 创建 MCP 客户端(统一认证入口)
47
+ *
48
+ * 优先级:
49
+ * 1. --token 参数 → 直接使用(适用于 CI/脚本)
50
+ * 2. CRUSH_OAUTH_ACCESS_TOKEN 环境变量 → 直接使用
51
+ * 3. 以上都没有 → 走 OAuthRemoteMcpClient 自动 OAuth 流程
52
+ * - 本地有缓存 token → 直接用
53
+ * - token 过期 → 自动 refresh
54
+ * - 无 token → 自动拉起浏览器登录
55
+ */
56
+ const createSmartClient = (flags) => {
57
+ const serverUrl = getServerUrl(flags);
58
+ // 显式传了 token → 用简单客户端
59
+ const explicitToken = typeof flags.token === "string" ? flags.token : process.env.CRUSH_OAUTH_ACCESS_TOKEN || "";
60
+ if (explicitToken) {
61
+ return {
62
+ client: new RemoteMcpClient({ serverUrl, token: explicitToken }),
63
+ needsOAuthConnect: false,
64
+ };
65
+ }
66
+ // 否则走 OAuth 自动流程
67
+ return {
68
+ client: new OAuthRemoteMcpClient({ serverUrl }),
69
+ needsOAuthConnect: true,
70
+ };
71
+ };
72
+ const connectClient = async (flags) => {
73
+ const { client, needsOAuthConnect } = createSmartClient(flags);
74
+ if (needsOAuthConnect) {
75
+ await client.connect();
76
+ }
77
+ else {
78
+ await client.connect();
79
+ }
80
+ return client;
40
81
  };
41
82
  const createClickHouseClient = (flags) => {
42
- const host = typeof flags["ch-host"] === "string"
43
- ? flags["ch-host"]
44
- : (process.env.CH_HOST ?? "localhost");
45
- const portRaw = typeof flags["ch-port"] === "string"
46
- ? flags["ch-port"]
47
- : (process.env.CH_PORT ?? "8123");
48
- const user = typeof flags["ch-user"] === "string"
49
- ? flags["ch-user"]
50
- : (process.env.CH_USER ?? "default");
51
- const password = typeof flags["ch-password"] === "string"
52
- ? flags["ch-password"]
53
- : (process.env.CH_PASSWORD ?? "");
54
- const database = typeof flags["ch-database"] === "string"
55
- ? flags["ch-database"]
56
- : (process.env.CH_DATABASE ?? "crush_ats");
57
- const rowCapRaw = typeof flags["ch-row-cap"] === "string"
58
- ? flags["ch-row-cap"]
59
- : process.env.CH_ROW_CAP;
83
+ const host = typeof flags["ch-host"] === "string" ? flags["ch-host"] : (process.env.CH_HOST ?? "localhost");
84
+ const portRaw = typeof flags["ch-port"] === "string" ? flags["ch-port"] : (process.env.CH_PORT ?? "8123");
85
+ const user = typeof flags["ch-user"] === "string" ? flags["ch-user"] : (process.env.CH_USER ?? "default");
86
+ const password = typeof flags["ch-password"] === "string" ? flags["ch-password"] : (process.env.CH_PASSWORD ?? "");
87
+ const database = typeof flags["ch-database"] === "string" ? flags["ch-database"] : (process.env.CH_DATABASE ?? "crush_ats");
88
+ const rowCapRaw = typeof flags["ch-row-cap"] === "string" ? flags["ch-row-cap"] : process.env.CH_ROW_CAP;
60
89
  const port = Number(portRaw);
61
90
  if (!Number.isFinite(port) || port <= 0) {
62
91
  throw new Error(`Invalid ClickHouse port: ${portRaw}`);
@@ -80,18 +109,43 @@ const createClickHouseClient = (flags) => {
80
109
  };
81
110
  const run = async () => {
82
111
  const [, , command, ...rest] = process.argv;
83
- if (!command ||
84
- command === "help" ||
85
- command === "--help" ||
86
- command === "-h") {
112
+ if (!command || command === "help" || command === "--help" || command === "-h") {
87
113
  printUsage();
88
114
  return;
89
115
  }
90
116
  const flags = parseFlags(rest);
91
117
  switch (command) {
118
+ case "login": {
119
+ const serverUrl = getServerUrl(flags);
120
+ const client = new OAuthRemoteMcpClient({ serverUrl });
121
+ try {
122
+ await client.ensureAuthorized();
123
+ console.log("OAuth authorization is ready. Tokens are stored locally.");
124
+ }
125
+ finally {
126
+ await client.close();
127
+ }
128
+ return;
129
+ }
130
+ case "setup": {
131
+ const targets = getSetupTargets(flags);
132
+ if (targets.length === 0) {
133
+ throw new Error("Specify at least one setup target: --cursor, --claude, --codex, --gemini, --opencode, or --all");
134
+ }
135
+ const rawScope = typeof flags.scope === "string" ? flags.scope : "user";
136
+ if (rawScope !== "user" && rawScope !== "project") {
137
+ throw new Error("Invalid --scope. Expected 'user' or 'project'.");
138
+ }
139
+ const scope = rawScope;
140
+ const serverUrl = getServerUrl(flags);
141
+ for (const target of targets) {
142
+ const location = installClientConfig(target, serverUrl, scope);
143
+ console.log(`[setup] ${target}: configured (${location})`);
144
+ }
145
+ return;
146
+ }
92
147
  case "tools:list": {
93
- const client = createRemoteClient(flags);
94
- await client.connect();
148
+ const client = await connectClient(flags);
95
149
  try {
96
150
  const result = await client.listTools();
97
151
  console.log(JSON.stringify(result, null, 2));
@@ -111,8 +165,7 @@ const run = async () => {
111
165
  catch (e) {
112
166
  throw new Error(`Invalid --args JSON: ${e.message}`);
113
167
  }
114
- const client = createRemoteClient(flags);
115
- await client.connect();
168
+ const client = await connectClient(flags);
116
169
  try {
117
170
  const result = await client.callTool(name, parsedArgs);
118
171
  console.log(JSON.stringify(result, null, 2));
@@ -123,8 +176,7 @@ const run = async () => {
123
176
  return;
124
177
  }
125
178
  case "ping": {
126
- const client = createRemoteClient(flags);
127
- await client.connect();
179
+ const client = await connectClient(flags);
128
180
  try {
129
181
  const result = await client.ping();
130
182
  console.log(JSON.stringify(result, null, 2));
@@ -135,8 +187,7 @@ const run = async () => {
135
187
  return;
136
188
  }
137
189
  case "backtest:schema": {
138
- const client = createRemoteClient(flags);
139
- await client.connect();
190
+ const client = await connectClient(flags);
140
191
  try {
141
192
  const bt = new BacktestClient(client);
142
193
  const result = await bt.getConfigSchema();
@@ -148,8 +199,7 @@ const run = async () => {
148
199
  return;
149
200
  }
150
201
  case "backtest:tokens": {
151
- const client = createRemoteClient(flags);
152
- await client.connect();
202
+ const client = await connectClient(flags);
153
203
  try {
154
204
  const bt = new BacktestClient(client);
155
205
  const platform = typeof flags.platform === "string" ? flags.platform : undefined;
@@ -174,11 +224,8 @@ const run = async () => {
174
224
  catch (e) {
175
225
  throw new Error(`Invalid --expression JSON: ${e.message}`);
176
226
  }
177
- const dataSource = typeof flags["data-source"] === "string"
178
- ? flags["data-source"]
179
- : undefined;
180
- const client = createRemoteClient(flags);
181
- await client.connect();
227
+ const dataSource = typeof flags["data-source"] === "string" ? flags["data-source"] : undefined;
228
+ const client = await connectClient(flags);
182
229
  try {
183
230
  const bt = new BacktestClient(client);
184
231
  const result = await bt.validateExpression({ expression, dataSource });
@@ -198,11 +245,8 @@ const run = async () => {
198
245
  catch (e) {
199
246
  throw new Error(`Invalid --config JSON: ${e.message}`);
200
247
  }
201
- const backtestId = typeof flags["backtest-id"] === "string"
202
- ? flags["backtest-id"]
203
- : undefined;
204
- const client = createRemoteClient(flags);
205
- await client.connect();
248
+ const backtestId = typeof flags["backtest-id"] === "string" ? flags["backtest-id"] : undefined;
249
+ const client = await connectClient(flags);
206
250
  try {
207
251
  const bt = new BacktestClient(client);
208
252
  const result = await bt.createBacktest({ config, backtestId });
@@ -213,28 +257,11 @@ const run = async () => {
213
257
  }
214
258
  return;
215
259
  }
216
- case "backtest:get": {
217
- const backtestId = requireString(flags["backtest-id"], "Missing --backtest-id for backtest:get");
218
- const client = createRemoteClient(flags);
219
- await client.connect();
220
- try {
221
- const bt = new BacktestClient(client);
222
- const result = await bt.getResult({ backtestId });
223
- console.log(JSON.stringify(result, null, 2));
224
- }
225
- finally {
226
- await client.close();
227
- }
228
- return;
229
- }
230
260
  case "backtest:list": {
231
- const status = typeof flags.status === "string"
232
- ? flags.status
233
- : undefined;
261
+ const status = typeof flags.status === "string" ? flags.status : undefined;
234
262
  const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
235
263
  const offset = typeof flags.offset === "string" ? Number(flags.offset) : undefined;
236
- const client = createRemoteClient(flags);
237
- await client.connect();
264
+ const client = await connectClient(flags);
238
265
  try {
239
266
  const bt = new BacktestClient(client);
240
267
  const result = await bt.list({ status, limit, offset });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
- export { ClickHouseDirectClient, DEFAULT_ROW_CAP, type ClickHouseDirectConfig, } from "./clickhouse/directClient.js";
1
+ export { BacktestClient, type BacktestConfigSchema, type BacktestRecord, type CreateBacktestInput, type GetAvailableTokensInput, type ListBacktestsInput, type ListBacktestsResult, type TokenInfo, type ValidationResult, } from "./backtest/backtestClient.js";
2
+ export { ClickHouseDirectClient, type ClickHouseDirectConfig, DEFAULT_ROW_CAP, } from "./clickhouse/directClient.js";
3
+ export { InteractiveOAuthProvider, type InteractiveOAuthProviderOptions, } from "./mcp/oauthProvider.js";
4
+ export { OAuthRemoteMcpClient, type OAuthRemoteMcpClientOptions, } from "./mcp/oauthRemoteClient.js";
2
5
  export { RemoteMcpClient, type RemoteMcpClientOptions, } from "./mcp/remoteClient.js";
3
- export { BacktestClient, type BacktestConfigSchema, type BacktestRecord, type CreateBacktestInput, type GetAvailableTokensInput, type GetBacktestResultInput, type ListBacktestsInput, type ListBacktestsResult, type TokenInfo, type ValidationResult, } from "./backtest/backtestClient.js";
6
+ export type { McpClientLike } from "./mcp/types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export { BacktestClient, } from "./backtest/backtestClient.js";
1
2
  export { ClickHouseDirectClient, DEFAULT_ROW_CAP, } from "./clickhouse/directClient.js";
3
+ export { InteractiveOAuthProvider, } from "./mcp/oauthProvider.js";
4
+ export { OAuthRemoteMcpClient, } from "./mcp/oauthRemoteClient.js";
2
5
  export { RemoteMcpClient, } from "./mcp/remoteClient.js";
3
- export { BacktestClient, } from "./backtest/backtestClient.js";
@@ -0,0 +1,42 @@
1
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ export type InteractiveOAuthProviderOptions = {
4
+ serverUrl: string;
5
+ clientName?: string;
6
+ clientVersion?: string;
7
+ scope?: string;
8
+ redirectPort?: number;
9
+ redirectPath?: string;
10
+ storageDir?: string;
11
+ openBrowser?: boolean;
12
+ onAuthorizationUrl?: (authorizationUrl: URL) => void | Promise<void>;
13
+ };
14
+ export declare class InteractiveOAuthProvider implements OAuthClientProvider {
15
+ private readonly options;
16
+ readonly redirectUrl: URL;
17
+ private readonly storageFile;
18
+ private readonly scope;
19
+ private readonly openBrowserByDefault;
20
+ private readonly onAuthorizationUrl?;
21
+ private callbackServer?;
22
+ private pendingAuthorization?;
23
+ constructor(options: InteractiveOAuthProviderOptions);
24
+ get clientMetadata(): OAuthClientMetadata;
25
+ state(): Promise<string>;
26
+ clientInformation(): Promise<OAuthClientInformationMixed | undefined>;
27
+ saveClientInformation(clientInformation: OAuthClientInformationMixed): Promise<void>;
28
+ tokens(): Promise<OAuthTokens | undefined>;
29
+ saveTokens(tokens: OAuthTokens): Promise<void>;
30
+ redirectToAuthorization(authorizationUrl: URL): Promise<void>;
31
+ saveCodeVerifier(codeVerifier: string): Promise<void>;
32
+ codeVerifier(): Promise<string>;
33
+ invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier"): Promise<void>;
34
+ waitForAuthorizationCode(): Promise<string>;
35
+ close(): Promise<void>;
36
+ private loadState;
37
+ private saveState;
38
+ private createPendingAuthorization;
39
+ private ensureCallbackServer;
40
+ private closeCallbackServer;
41
+ private handleCallbackRequest;
42
+ }
@@ -0,0 +1,264 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ const DEFAULT_SCOPE = "mcp:tools";
8
+ const DEFAULT_REDIRECT_PORT = 8787;
9
+ const DEFAULT_REDIRECT_PATH = "/oauth/callback";
10
+ const renderCallbackHtml = (title, message) => `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>${title}</title>
16
+ <style>
17
+ body {
18
+ margin: 0;
19
+ min-height: 100vh;
20
+ display: grid;
21
+ place-items: center;
22
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
23
+ background: linear-gradient(135deg, #f6f7fb, #eef2ff);
24
+ color: #0f172a;
25
+ }
26
+ main {
27
+ max-width: 560px;
28
+ padding: 32px;
29
+ border-radius: 20px;
30
+ background: rgba(255, 255, 255, 0.96);
31
+ box-shadow: 0 20px 45px rgba(15, 23, 42, 0.14);
32
+ text-align: center;
33
+ }
34
+ h1 {
35
+ margin: 0 0 12px;
36
+ font-size: 28px;
37
+ }
38
+ p {
39
+ margin: 0;
40
+ line-height: 1.6;
41
+ color: #334155;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <main>
47
+ <h1>${title}</h1>
48
+ <p>${message}</p>
49
+ </main>
50
+ </body>
51
+ </html>`;
52
+ const defaultStorageDir = () => path.join(os.homedir(), ".crush-mcp");
53
+ const hashServerUrl = (serverUrl) => createHash("sha256").update(serverUrl).digest("hex").slice(0, 16);
54
+ const openBrowser = async (authorizationUrl) => {
55
+ const url = authorizationUrl.toString();
56
+ if (process.env.BROWSER) {
57
+ spawn(process.env.BROWSER, [url], { stdio: "ignore", detached: true }).unref();
58
+ return;
59
+ }
60
+ if (process.platform === "darwin") {
61
+ spawn("open", [url], { stdio: "ignore", detached: true }).unref();
62
+ return;
63
+ }
64
+ if (process.platform === "win32") {
65
+ spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
66
+ return;
67
+ }
68
+ spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
69
+ };
70
+ export class InteractiveOAuthProvider {
71
+ options;
72
+ redirectUrl;
73
+ storageFile;
74
+ scope;
75
+ openBrowserByDefault;
76
+ onAuthorizationUrl;
77
+ callbackServer;
78
+ pendingAuthorization;
79
+ constructor(options) {
80
+ this.options = options;
81
+ const redirectPort = options.redirectPort ?? DEFAULT_REDIRECT_PORT;
82
+ const redirectPath = options.redirectPath ?? DEFAULT_REDIRECT_PATH;
83
+ this.redirectUrl = new URL(`http://127.0.0.1:${redirectPort}${redirectPath}`);
84
+ this.scope = options.scope ?? DEFAULT_SCOPE;
85
+ this.openBrowserByDefault = options.openBrowser ?? true;
86
+ const storageDir = options.storageDir ?? defaultStorageDir();
87
+ this.storageFile = path.join(storageDir, `oauth-${hashServerUrl(options.serverUrl)}.json`);
88
+ this.onAuthorizationUrl = options.onAuthorizationUrl;
89
+ }
90
+ get clientMetadata() {
91
+ return {
92
+ client_name: this.options.clientName ?? "Crush MCP Client",
93
+ grant_types: ["authorization_code", "refresh_token"],
94
+ redirect_uris: [this.redirectUrl.toString()],
95
+ response_types: ["code"],
96
+ scope: this.scope,
97
+ software_version: this.options.clientVersion,
98
+ token_endpoint_auth_method: "none",
99
+ };
100
+ }
101
+ async state() {
102
+ return randomBytes(16).toString("hex");
103
+ }
104
+ async clientInformation() {
105
+ const data = await this.loadState();
106
+ return data.clientInformation;
107
+ }
108
+ async saveClientInformation(clientInformation) {
109
+ const data = await this.loadState();
110
+ data.clientInformation = clientInformation;
111
+ await this.saveState(data);
112
+ }
113
+ async tokens() {
114
+ const data = await this.loadState();
115
+ return data.tokens;
116
+ }
117
+ async saveTokens(tokens) {
118
+ const data = await this.loadState();
119
+ data.tokens = tokens;
120
+ await this.saveState(data);
121
+ }
122
+ async redirectToAuthorization(authorizationUrl) {
123
+ this.createPendingAuthorization();
124
+ await this.ensureCallbackServer();
125
+ if (this.onAuthorizationUrl) {
126
+ await this.onAuthorizationUrl(authorizationUrl);
127
+ }
128
+ if (this.openBrowserByDefault) {
129
+ try {
130
+ await openBrowser(authorizationUrl);
131
+ }
132
+ catch {
133
+ // Fall back to printing the URL when no browser launcher is available.
134
+ }
135
+ }
136
+ process.stdout.write(`\nOpen this URL to authorize Crush MCP:\n${authorizationUrl.toString()}\n\n`);
137
+ }
138
+ async saveCodeVerifier(codeVerifier) {
139
+ const data = await this.loadState();
140
+ data.codeVerifier = codeVerifier;
141
+ await this.saveState(data);
142
+ }
143
+ async codeVerifier() {
144
+ const data = await this.loadState();
145
+ if (!data.codeVerifier) {
146
+ throw new Error("Missing PKCE code verifier. Restart the OAuth flow.");
147
+ }
148
+ return data.codeVerifier;
149
+ }
150
+ async invalidateCredentials(scope) {
151
+ const data = await this.loadState();
152
+ if (scope === "all") {
153
+ await rm(this.storageFile, { force: true });
154
+ return;
155
+ }
156
+ if (scope === "client") {
157
+ delete data.clientInformation;
158
+ }
159
+ if (scope === "tokens") {
160
+ delete data.tokens;
161
+ }
162
+ if (scope === "verifier") {
163
+ delete data.codeVerifier;
164
+ }
165
+ await this.saveState(data);
166
+ }
167
+ async waitForAuthorizationCode() {
168
+ if (!this.pendingAuthorization) {
169
+ this.createPendingAuthorization();
170
+ }
171
+ return this.pendingAuthorization.promise;
172
+ }
173
+ async close() {
174
+ await this.closeCallbackServer();
175
+ }
176
+ async loadState() {
177
+ try {
178
+ const raw = await readFile(this.storageFile, "utf8");
179
+ return JSON.parse(raw);
180
+ }
181
+ catch (error) {
182
+ const err = error;
183
+ if (err.code === "ENOENT") {
184
+ return {};
185
+ }
186
+ throw error;
187
+ }
188
+ }
189
+ async saveState(data) {
190
+ await mkdir(path.dirname(this.storageFile), { recursive: true });
191
+ await writeFile(this.storageFile, JSON.stringify(data, null, 2), "utf8");
192
+ }
193
+ createPendingAuthorization() {
194
+ let resolve;
195
+ let reject;
196
+ const promise = new Promise((innerResolve, innerReject) => {
197
+ resolve = innerResolve;
198
+ reject = innerReject;
199
+ });
200
+ this.pendingAuthorization = { promise, resolve, reject };
201
+ return this.pendingAuthorization;
202
+ }
203
+ async ensureCallbackServer() {
204
+ if (this.callbackServer) {
205
+ return;
206
+ }
207
+ this.callbackServer = createServer((req, res) => {
208
+ void this.handleCallbackRequest(req, res);
209
+ });
210
+ await new Promise((resolve, reject) => {
211
+ this.callbackServer.once("error", (error) => reject(error));
212
+ this.callbackServer.listen(Number(this.redirectUrl.port), this.redirectUrl.hostname, () => resolve());
213
+ });
214
+ }
215
+ async closeCallbackServer() {
216
+ if (!this.callbackServer) {
217
+ return;
218
+ }
219
+ const server = this.callbackServer;
220
+ this.callbackServer = undefined;
221
+ await new Promise((resolve, reject) => {
222
+ server.close((error) => {
223
+ if (error) {
224
+ reject(error);
225
+ return;
226
+ }
227
+ resolve();
228
+ });
229
+ });
230
+ }
231
+ async handleCallbackRequest(req, res) {
232
+ const requestUrl = new URL(req.url || "/", this.redirectUrl);
233
+ if (requestUrl.pathname !== this.redirectUrl.pathname) {
234
+ res.statusCode = 404;
235
+ res.end("Not found");
236
+ return;
237
+ }
238
+ const error = requestUrl.searchParams.get("error");
239
+ const errorDescription = requestUrl.searchParams.get("error_description");
240
+ const code = requestUrl.searchParams.get("code");
241
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
242
+ if (error) {
243
+ res.statusCode = 400;
244
+ res.end(renderCallbackHtml("Authorization failed", errorDescription || error));
245
+ this.pendingAuthorization?.reject(new Error(errorDescription || error));
246
+ this.pendingAuthorization = undefined;
247
+ await this.closeCallbackServer();
248
+ return;
249
+ }
250
+ if (!code) {
251
+ res.statusCode = 400;
252
+ res.end(renderCallbackHtml("Authorization failed", "Missing authorization code."));
253
+ this.pendingAuthorization?.reject(new Error("Missing authorization code in callback."));
254
+ this.pendingAuthorization = undefined;
255
+ await this.closeCallbackServer();
256
+ return;
257
+ }
258
+ res.statusCode = 200;
259
+ res.end(renderCallbackHtml("Authorization complete", "You can close this tab and return to your MCP client."));
260
+ this.pendingAuthorization?.resolve(code);
261
+ this.pendingAuthorization = undefined;
262
+ await this.closeCallbackServer();
263
+ }
264
+ }