@zhijiewang/openharness 2.11.0 → 2.13.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.
@@ -6,6 +6,7 @@ import { processSlashCommand } from "../commands/index.js";
6
6
  import { cybergotchiEvents } from "../cybergotchi/events.js";
7
7
  import { resolveMcpMention } from "../mcp/loader.js";
8
8
  import { createInfoMessage, createUserMessage } from "../types/message.js";
9
+ import { emitHookWithOutcome } from "./hooks.js";
9
10
  /**
10
11
  * Process user input: handle exit, companion mentions, slash commands,
11
12
  * @mentions, and prepare the prompt for the LLM.
@@ -58,7 +59,7 @@ export async function handleUserInput(input, ctx) {
58
59
  totalOutputTokens: ctx.cost.totalOutputTokens,
59
60
  sessionId: ctx.sessionId,
60
61
  };
61
- const result = processSlashCommand(trimmed, cmdCtx);
62
+ const result = await processSlashCommand(trimmed, cmdCtx);
62
63
  if (result) {
63
64
  if (result.clearMessages)
64
65
  messages = [];
@@ -78,10 +79,28 @@ export async function handleUserInput(input, ctx) {
78
79
  }
79
80
  if (result.prependToPrompt) {
80
81
  messages = [...messages, createUserMessage(input)];
82
+ const prependPrompt = result.prependToPrompt;
83
+ const prependOutcome = await emitHookWithOutcome("userPromptSubmit", {
84
+ prompt: prependPrompt,
85
+ sessionId: ctx.sessionId,
86
+ model: ctx.currentModel,
87
+ provider: ctx.providerName,
88
+ permissionMode: ctx.permissionMode,
89
+ });
90
+ if (!prependOutcome.allowed) {
91
+ const reason = prependOutcome.reason ? `: ${prependOutcome.reason}` : "";
92
+ return {
93
+ handled: true,
94
+ messages: [...messages, createInfoMessage(`Blocked by userPromptSubmit hook${reason}`)],
95
+ };
96
+ }
97
+ const finalPrependPrompt = prependOutcome.additionalContext
98
+ ? `${prependOutcome.additionalContext}\n\n${prependPrompt}`
99
+ : prependPrompt;
81
100
  return {
82
101
  handled: false,
83
102
  messages,
84
- prompt: result.prependToPrompt,
103
+ prompt: finalPrependPrompt,
85
104
  newModel: result.newModel ?? undefined,
86
105
  };
87
106
  }
@@ -136,6 +155,21 @@ export async function handleUserInput(input, ctx) {
136
155
  /* ignore */
137
156
  }
138
157
  }
139
- return { handled: false, messages, prompt: resolvedInput };
158
+ const outcome = await emitHookWithOutcome("userPromptSubmit", {
159
+ prompt: resolvedInput,
160
+ sessionId: ctx.sessionId,
161
+ model: ctx.currentModel,
162
+ provider: ctx.providerName,
163
+ permissionMode: ctx.permissionMode,
164
+ });
165
+ if (!outcome.allowed) {
166
+ const reason = outcome.reason ? `: ${outcome.reason}` : "";
167
+ return {
168
+ handled: true,
169
+ messages: [...messages, createInfoMessage(`Blocked by userPromptSubmit hook${reason}`)],
170
+ };
171
+ }
172
+ const finalPrompt = outcome.additionalContext ? `${outcome.additionalContext}\n\n${resolvedInput}` : resolvedInput;
173
+ return { handled: false, messages, prompt: finalPrompt };
140
174
  }
141
175
  //# sourceMappingURL=submit-handler.js.map
@@ -16,7 +16,11 @@ export declare class McpClient {
16
16
  private timeoutMs;
17
17
  private reconnectImpl;
18
18
  private constructor();
19
- static connect(cfg: McpServerConfig, timeoutMs?: number): Promise<McpClient>;
19
+ static connect(cfg: McpServerConfig, timeoutMsOrOpts?: number | {
20
+ timeoutMs?: number;
21
+ openFn?: (url: string) => Promise<void>;
22
+ storageDir?: string;
23
+ } | undefined): Promise<McpClient>;
20
24
  /** Test-only constructor. Not exported from the package's public API. */
21
25
  static _forTesting(opts: ForTestingOptions): McpClient;
22
26
  private defaultReconnect;
@@ -1,5 +1,12 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import open from "open";
1
4
  import { normalizeMcpConfig } from "./config-normalize.js";
5
+ import { buildAuthProvider } from "./oauth.js";
2
6
  import { buildClient, connectWithFallback } from "./transport.js";
7
+ function credentialsDir() {
8
+ return join(homedir(), ".oh", "credentials", "mcp");
9
+ }
3
10
  const DEFAULT_TIMEOUT_MS = 5_000;
4
11
  export class McpClient {
5
12
  name;
@@ -19,13 +26,29 @@ export class McpClient {
19
26
  this.instructions = instr;
20
27
  }
21
28
  }
22
- static async connect(cfg, timeoutMs = cfg.timeout ?? DEFAULT_TIMEOUT_MS) {
29
+ static async connect(cfg, timeoutMsOrOpts = undefined) {
30
+ // Backward-compatible: accept number for timeout OR options object
31
+ const opts = typeof timeoutMsOrOpts === "number" ? { timeoutMs: timeoutMsOrOpts } : (timeoutMsOrOpts ?? {});
32
+ const timeoutMs = opts.timeoutMs ?? cfg.timeout ?? DEFAULT_TIMEOUT_MS;
33
+ const openFn = opts.openFn ??
34
+ (async (url) => {
35
+ await open(url);
36
+ });
37
+ const storageDirResolved = opts.storageDir ?? credentialsDir();
23
38
  const normalized = normalizeMcpConfig(cfg, process.env);
24
39
  if (normalized.kind === "error") {
25
40
  throw new Error(normalized.message);
26
41
  }
27
- const sdk = await connectWithFallback(normalized.cfg, (c) => buildClient(c));
28
- return new McpClient(cfg.name, cfg, sdk, timeoutMs);
42
+ const authProvider = buildAuthProvider(normalized.cfg, storageDirResolved, openFn);
43
+ if (authProvider)
44
+ await authProvider.ready();
45
+ try {
46
+ const sdk = await connectWithFallback(normalized.cfg, (c) => buildClient(c, { authProvider }));
47
+ return new McpClient(cfg.name, cfg, sdk, timeoutMs);
48
+ }
49
+ finally {
50
+ authProvider?.close();
51
+ }
29
52
  }
30
53
  /** Test-only constructor. Not exported from the package's public API. */
31
54
  static _forTesting(opts) {
@@ -35,7 +58,17 @@ export class McpClient {
35
58
  const normalized = normalizeMcpConfig(this.cfg, process.env);
36
59
  if (normalized.kind === "error")
37
60
  throw new Error(normalized.message);
38
- return connectWithFallback(normalized.cfg, (c) => buildClient(c));
61
+ const authProvider = buildAuthProvider(normalized.cfg, credentialsDir(), async (url) => {
62
+ await open(url);
63
+ });
64
+ if (authProvider)
65
+ await authProvider.ready();
66
+ try {
67
+ return await connectWithFallback(normalized.cfg, (c) => buildClient(c, { authProvider }));
68
+ }
69
+ finally {
70
+ authProvider?.close();
71
+ }
39
72
  }
40
73
  async listTools() {
41
74
  const res = await this.sdk.listTools();
@@ -0,0 +1,23 @@
1
+ export type OhCredentials = {
2
+ issuerUrl: string;
3
+ clientInformation: {
4
+ client_id: string;
5
+ client_secret?: string;
6
+ } & Record<string, unknown>;
7
+ tokens: {
8
+ access_token: string;
9
+ refresh_token?: string;
10
+ expires_at?: number;
11
+ token_type?: string;
12
+ scope?: string;
13
+ };
14
+ codeVerifier?: string;
15
+ updatedAt: string;
16
+ };
17
+ /** Atomically write credentials for one server. Creates the directory with 0o700 on first use. */
18
+ export declare function saveCredentials(storageDir: string, name: string, creds: OhCredentials): Promise<void>;
19
+ /** Load credentials. Returns undefined on missing file OR corrupt JSON. Warns on world/group-readable mode. */
20
+ export declare function loadCredentials(storageDir: string, name: string): Promise<OhCredentials | undefined>;
21
+ /** Idempotent delete — ENOENT is swallowed. */
22
+ export declare function deleteCredentials(storageDir: string, name: string): Promise<void>;
23
+ //# sourceMappingURL=oauth-storage.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ function pathFor(storageDir, name) {
4
+ return join(storageDir, `${name}.json`);
5
+ }
6
+ /** Atomically write credentials for one server. Creates the directory with 0o700 on first use. */
7
+ export async function saveCredentials(storageDir, name, creds) {
8
+ const filePath = pathFor(storageDir, name);
9
+ const tmpPath = `${filePath}.tmp`;
10
+ await fs.mkdir(dirname(filePath), { recursive: true, mode: 0o700 });
11
+ const body = JSON.stringify(creds, null, 2);
12
+ await fs.writeFile(tmpPath, body, { mode: 0o600 });
13
+ await fs.rename(tmpPath, filePath);
14
+ }
15
+ /** Load credentials. Returns undefined on missing file OR corrupt JSON. Warns on world/group-readable mode. */
16
+ export async function loadCredentials(storageDir, name) {
17
+ const filePath = pathFor(storageDir, name);
18
+ let raw;
19
+ try {
20
+ raw = await fs.readFile(filePath, "utf8");
21
+ }
22
+ catch (err) {
23
+ if (err.code === "ENOENT")
24
+ return undefined;
25
+ throw err;
26
+ }
27
+ try {
28
+ if (process.platform !== "win32") {
29
+ const s = await fs.stat(filePath);
30
+ if ((s.mode & 0o077) !== 0) {
31
+ console.warn(`[mcp] credentials file for '${name}' is world/group-readable; run 'chmod 600 ${filePath}'`);
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ // stat failure is non-fatal for load
37
+ }
38
+ try {
39
+ return JSON.parse(raw);
40
+ }
41
+ catch {
42
+ console.warn(`[mcp] credentials file for '${name}' is corrupt; ignoring`);
43
+ return undefined;
44
+ }
45
+ }
46
+ /** Idempotent delete — ENOENT is swallowed. */
47
+ export async function deleteCredentials(storageDir, name) {
48
+ const filePath = pathFor(storageDir, name);
49
+ try {
50
+ await fs.unlink(filePath);
51
+ }
52
+ catch (err) {
53
+ if (err.code === "ENOENT")
54
+ return;
55
+ throw err;
56
+ }
57
+ }
58
+ //# sourceMappingURL=oauth-storage.js.map
@@ -0,0 +1,79 @@
1
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ import type { NormalizedConfig } from "./config-normalize.js";
4
+ /** Thrown when the OAuth callback flow fails — user cancelled, timeout, state mismatch, etc. */
5
+ export declare class OAuthFlowError extends Error {
6
+ readonly serverName: string;
7
+ constructor(serverName: string, reason: string);
8
+ }
9
+ export type OAuthCallbackResult = {
10
+ code: string;
11
+ state: string;
12
+ };
13
+ export type PendingCallback = {
14
+ /** The full redirect URI clients should be sent to. */
15
+ readonly redirectUri: string;
16
+ /** Resolves with the captured code+state; rejects on timeout or close. */
17
+ readonly done: Promise<OAuthCallbackResult>;
18
+ /** Close the listener immediately. Idempotent. */
19
+ close: () => void;
20
+ };
21
+ /**
22
+ * Bind a single-shot HTTP listener on 127.0.0.1 to receive the OAuth redirect.
23
+ * Returns after the server has bound (so redirectUri is available synchronously on the result).
24
+ * The `done` promise resolves when a valid /oauth/callback arrives, or rejects on timeout/close.
25
+ */
26
+ export declare function awaitOAuthCallback(opts: {
27
+ timeoutMs: number;
28
+ }): Promise<PendingCallback>;
29
+ /** Strip access_token=, refresh_token=, and "Bearer <x>" from a log message. */
30
+ export declare function redactToken(msg: string): string;
31
+ export type OhOAuthProviderOptions = {
32
+ name: string;
33
+ storageDir: string;
34
+ /** Browser launch hook — injected for tests; production wires to `open` from the npm package. */
35
+ openFn: (url: string) => Promise<void>;
36
+ };
37
+ /**
38
+ * Implements the SDK's OAuthClientProvider backed by OhCredentials on disk.
39
+ * Lazily binds the callback listener on ready() (called before the SDK reads redirectUrl).
40
+ */
41
+ export declare class OhOAuthProvider implements OAuthClientProvider {
42
+ private readonly name;
43
+ private readonly storageDir;
44
+ private readonly openFn;
45
+ private pending;
46
+ private _redirectUri;
47
+ private inMemoryCodeVerifier;
48
+ constructor(opts: OhOAuthProviderOptions);
49
+ /** Bind the callback listener and prepare redirectUri. Call before first SDK access. */
50
+ ready(): Promise<void>;
51
+ /** Release the callback listener (no-op if already resolved/closed). */
52
+ close(): void;
53
+ get redirectUrl(): string | URL | undefined;
54
+ get clientMetadata(): OAuthClientMetadata;
55
+ clientInformation(): Promise<OAuthClientInformationMixed | undefined>;
56
+ saveClientInformation(info: OAuthClientInformationMixed): Promise<void>;
57
+ tokens(): Promise<OAuthTokens | undefined>;
58
+ saveTokens(tokens: OAuthTokens): Promise<void>;
59
+ redirectToAuthorization(url: URL): Promise<void>;
60
+ saveCodeVerifier(verifier: string): Promise<void>;
61
+ codeVerifier(): Promise<string>;
62
+ /** Await a resolved callback from the listener bound in ready(). */
63
+ awaitCallback(): Promise<OAuthCallbackResult>;
64
+ private emptyCreds;
65
+ }
66
+ export type AuthStatus = "n/a" | "none" | "authenticated" | "expired";
67
+ /**
68
+ * Construct an OAuth provider for a normalized config, iff:
69
+ * - type is http or sse
70
+ * - no static headers.Authorization
71
+ * - auth !== "none"
72
+ * Otherwise return undefined — the transport proceeds without OAuth.
73
+ */
74
+ export declare function buildAuthProvider(cfg: NormalizedConfig, storageDir: string, openFn: (url: string) => Promise<void>): OhOAuthProvider | undefined;
75
+ /** Delete stored credentials for a server. Safe to call when none exist. */
76
+ export declare function clearTokens(storageDir: string, name: string): Promise<void>;
77
+ /** Compute auth state for a server for /mcp display. */
78
+ export declare function getAuthStatus(cfg: NormalizedConfig, storageDir: string): Promise<AuthStatus>;
79
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1,257 @@
1
+ import { createServer } from "node:http";
2
+ import { deleteCredentials, loadCredentials, saveCredentials } from "./oauth-storage.js";
3
+ /** Thrown when the OAuth callback flow fails — user cancelled, timeout, state mismatch, etc. */
4
+ export class OAuthFlowError extends Error {
5
+ serverName;
6
+ constructor(serverName, reason) {
7
+ super(`OAuth flow for '${serverName}' failed: ${reason}`);
8
+ this.name = "OAuthFlowError";
9
+ this.serverName = serverName;
10
+ }
11
+ }
12
+ const SUCCESS_HTML = `<!doctype html><html><body style="font-family: system-ui; padding: 2rem">
13
+ <h2>Authorization complete</h2>
14
+ <p>You can close this tab and return to openHarness.</p>
15
+ </body></html>`;
16
+ /**
17
+ * Bind a single-shot HTTP listener on 127.0.0.1 to receive the OAuth redirect.
18
+ * Returns after the server has bound (so redirectUri is available synchronously on the result).
19
+ * The `done` promise resolves when a valid /oauth/callback arrives, or rejects on timeout/close.
20
+ */
21
+ export async function awaitOAuthCallback(opts) {
22
+ const server = createServer();
23
+ await new Promise((resolve, reject) => {
24
+ server.once("listening", () => resolve());
25
+ server.once("error", reject);
26
+ server.listen(0, "127.0.0.1");
27
+ });
28
+ const addr = server.address();
29
+ const redirectUri = `http://127.0.0.1:${addr.port}/oauth/callback`;
30
+ let closed = false;
31
+ let timer = null;
32
+ let resolveResult;
33
+ let rejectResult;
34
+ const done = new Promise((res, rej) => {
35
+ resolveResult = res;
36
+ rejectResult = rej;
37
+ });
38
+ function cleanup() {
39
+ if (closed)
40
+ return;
41
+ closed = true;
42
+ if (timer)
43
+ clearTimeout(timer);
44
+ timer = null;
45
+ server.close();
46
+ }
47
+ server.on("request", (req, res) => {
48
+ const host = req.headers.host ?? "";
49
+ if (!host.startsWith("127.0.0.1:")) {
50
+ res.statusCode = 403;
51
+ res.end("forbidden");
52
+ return;
53
+ }
54
+ const url = new URL(req.url ?? "/", `http://${host}`);
55
+ if (req.method !== "GET" || url.pathname !== "/oauth/callback") {
56
+ res.statusCode = 404;
57
+ res.end("not found");
58
+ return;
59
+ }
60
+ const code = url.searchParams.get("code") ?? "";
61
+ const state = url.searchParams.get("state") ?? "";
62
+ res.statusCode = 200;
63
+ res.setHeader("content-type", "text/html; charset=utf-8");
64
+ res.end(SUCCESS_HTML, () => {
65
+ cleanup();
66
+ resolveResult({ code, state });
67
+ });
68
+ });
69
+ timer = setTimeout(() => {
70
+ cleanup();
71
+ rejectResult(new Error(`OAuth callback timeout after ${opts.timeoutMs}ms`));
72
+ }, opts.timeoutMs);
73
+ return {
74
+ redirectUri,
75
+ done,
76
+ close: () => {
77
+ if (closed)
78
+ return;
79
+ cleanup();
80
+ rejectResult(new Error("OAuth callback closed before completion"));
81
+ },
82
+ };
83
+ }
84
+ /** Strip access_token=, refresh_token=, and "Bearer <x>" from a log message. */
85
+ export function redactToken(msg) {
86
+ return msg
87
+ .replace(/(access_token|refresh_token|code)=[^&\s"']+/gi, "$1=<redacted>")
88
+ .replace(/Bearer\s+[^\s"']+/gi, "Bearer <redacted>");
89
+ }
90
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1_000;
91
+ /**
92
+ * Implements the SDK's OAuthClientProvider backed by OhCredentials on disk.
93
+ * Lazily binds the callback listener on ready() (called before the SDK reads redirectUrl).
94
+ */
95
+ export class OhOAuthProvider {
96
+ name;
97
+ storageDir;
98
+ openFn;
99
+ pending = null;
100
+ _redirectUri = null;
101
+ inMemoryCodeVerifier = null;
102
+ constructor(opts) {
103
+ this.name = opts.name;
104
+ this.storageDir = opts.storageDir;
105
+ this.openFn = opts.openFn;
106
+ }
107
+ /** Bind the callback listener and prepare redirectUri. Call before first SDK access. */
108
+ async ready() {
109
+ if (this.pending)
110
+ return;
111
+ this.pending = await awaitOAuthCallback({ timeoutMs: CALLBACK_TIMEOUT_MS });
112
+ this._redirectUri = this.pending.redirectUri;
113
+ }
114
+ /** Release the callback listener (no-op if already resolved/closed). */
115
+ close() {
116
+ if (this.pending) {
117
+ // Attach a no-op catch so the rejected `done` promise doesn't become an unhandled rejection.
118
+ this.pending.done.catch(() => { });
119
+ this.pending.close();
120
+ }
121
+ this.pending = null;
122
+ this._redirectUri = null;
123
+ }
124
+ get redirectUrl() {
125
+ return this._redirectUri ?? undefined;
126
+ }
127
+ get clientMetadata() {
128
+ return {
129
+ client_name: "openharness",
130
+ redirect_uris: this._redirectUri ? [this._redirectUri] : [],
131
+ grant_types: ["authorization_code", "refresh_token"],
132
+ response_types: ["code"],
133
+ token_endpoint_auth_method: "none",
134
+ };
135
+ }
136
+ async clientInformation() {
137
+ const creds = await loadCredentials(this.storageDir, this.name);
138
+ return creds?.clientInformation;
139
+ }
140
+ async saveClientInformation(info) {
141
+ const creds = (await loadCredentials(this.storageDir, this.name)) ?? this.emptyCreds();
142
+ creds.clientInformation = info;
143
+ creds.updatedAt = new Date().toISOString();
144
+ await saveCredentials(this.storageDir, this.name, creds);
145
+ }
146
+ async tokens() {
147
+ const creds = await loadCredentials(this.storageDir, this.name);
148
+ if (!creds?.tokens?.access_token)
149
+ return undefined;
150
+ return {
151
+ access_token: creds.tokens.access_token,
152
+ refresh_token: creds.tokens.refresh_token,
153
+ token_type: creds.tokens.token_type ?? "Bearer",
154
+ scope: creds.tokens.scope,
155
+ expires_in: creds.tokens.expires_at && creds.tokens.expires_at > Date.now()
156
+ ? Math.floor((creds.tokens.expires_at - Date.now()) / 1000)
157
+ : 0,
158
+ };
159
+ }
160
+ async saveTokens(tokens) {
161
+ const creds = (await loadCredentials(this.storageDir, this.name)) ?? this.emptyCreds();
162
+ creds.tokens = {
163
+ access_token: tokens.access_token,
164
+ refresh_token: tokens.refresh_token,
165
+ token_type: tokens.token_type ?? "Bearer",
166
+ scope: tokens.scope,
167
+ expires_at: tokens.expires_in ? Date.now() + Number(tokens.expires_in) * 1000 : undefined,
168
+ };
169
+ creds.codeVerifier = undefined;
170
+ this.inMemoryCodeVerifier = null;
171
+ creds.updatedAt = new Date().toISOString();
172
+ await saveCredentials(this.storageDir, this.name, creds);
173
+ }
174
+ async redirectToAuthorization(url) {
175
+ const urlStr = url.toString();
176
+ try {
177
+ await this.openFn(urlStr);
178
+ console.warn(`[mcp] ${this.name}: opened browser for OAuth authorization. Waiting for callback...`);
179
+ }
180
+ catch (_err) {
181
+ console.warn(`[mcp] ${this.name}: could not open browser automatically. Please open this URL manually:\n ${urlStr}`);
182
+ // Don't re-throw — the listener may still receive the callback if the user opens the URL by hand.
183
+ }
184
+ }
185
+ async saveCodeVerifier(verifier) {
186
+ this.inMemoryCodeVerifier = verifier;
187
+ const creds = (await loadCredentials(this.storageDir, this.name)) ?? this.emptyCreds();
188
+ creds.codeVerifier = verifier;
189
+ creds.updatedAt = new Date().toISOString();
190
+ await saveCredentials(this.storageDir, this.name, creds);
191
+ }
192
+ async codeVerifier() {
193
+ if (this.inMemoryCodeVerifier)
194
+ return this.inMemoryCodeVerifier;
195
+ const creds = await loadCredentials(this.storageDir, this.name);
196
+ if (!creds?.codeVerifier) {
197
+ throw new Error(`no code verifier saved for '${this.name}'`);
198
+ }
199
+ return creds.codeVerifier;
200
+ }
201
+ /** Await a resolved callback from the listener bound in ready(). */
202
+ async awaitCallback() {
203
+ if (!this.pending)
204
+ throw new Error("awaitCallback called before ready()");
205
+ try {
206
+ return await this.pending.done;
207
+ }
208
+ catch (err) {
209
+ const msg = err instanceof Error ? err.message : String(err);
210
+ throw new OAuthFlowError(this.name, msg);
211
+ }
212
+ }
213
+ emptyCreds() {
214
+ return {
215
+ issuerUrl: "",
216
+ clientInformation: { client_id: "" },
217
+ tokens: { access_token: "" },
218
+ updatedAt: new Date().toISOString(),
219
+ };
220
+ }
221
+ }
222
+ /**
223
+ * Construct an OAuth provider for a normalized config, iff:
224
+ * - type is http or sse
225
+ * - no static headers.Authorization
226
+ * - auth !== "none"
227
+ * Otherwise return undefined — the transport proceeds without OAuth.
228
+ */
229
+ export function buildAuthProvider(cfg, storageDir, openFn) {
230
+ if (cfg.type === "stdio")
231
+ return undefined;
232
+ const headers = cfg.headers;
233
+ if (headers?.Authorization)
234
+ return undefined;
235
+ if (cfg.auth === "none")
236
+ return undefined;
237
+ return new OhOAuthProvider({ name: cfg.name, storageDir, openFn });
238
+ }
239
+ /** Delete stored credentials for a server. Safe to call when none exist. */
240
+ export async function clearTokens(storageDir, name) {
241
+ await deleteCredentials(storageDir, name);
242
+ }
243
+ /** Compute auth state for a server for /mcp display. */
244
+ export async function getAuthStatus(cfg, storageDir) {
245
+ if (cfg.type === "stdio")
246
+ return "n/a";
247
+ const headers = cfg.headers;
248
+ if (headers?.Authorization)
249
+ return "n/a";
250
+ const creds = await loadCredentials(storageDir, cfg.name);
251
+ if (!creds?.tokens?.access_token)
252
+ return "none";
253
+ if (creds.tokens.expires_at && creds.tokens.expires_at <= Date.now())
254
+ return "expired";
255
+ return "authenticated";
256
+ }
257
+ //# sourceMappingURL=oauth.js.map
@@ -1,3 +1,4 @@
1
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
1
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
3
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
4
  import type { NormalizedConfig } from "./config-normalize.js";
@@ -16,11 +17,14 @@ export declare class ProtocolError extends Error {
16
17
  readonly cause: unknown;
17
18
  constructor(serverName: string, cause: unknown);
18
19
  }
20
+ export type BuildTransportOptions = {
21
+ authProvider?: OAuthClientProvider;
22
+ };
19
23
  /**
20
24
  * Construct an SDK Transport for a normalized config.
21
25
  * Does NOT call .start() — caller (Client.connect) handles that.
22
26
  */
23
- export declare function buildTransport(cfg: NormalizedConfig): Promise<Transport>;
27
+ export declare function buildTransport(cfg: NormalizedConfig, opts?: BuildTransportOptions): Promise<Transport>;
24
28
  /**
25
29
  * Connect to an MCP server, with auto-fallback from Streamable HTTP to
26
30
  * legacy SSE when the config's type was INFERRED from url (not explicit).
@@ -30,9 +34,16 @@ export declare function buildTransport(cfg: NormalizedConfig): Promise<Transport
30
34
  * wires it to `buildClient` (Task 7).
31
35
  */
32
36
  export declare function connectWithFallback<T>(cfg: NormalizedConfig, doConnect: (cfg: NormalizedConfig) => Promise<T>): Promise<T>;
37
+ export type BuildClientOptions = {
38
+ authProvider?: OAuthClientProvider;
39
+ };
33
40
  /**
34
41
  * Build a connected SDK Client for a normalized config.
35
42
  * Maps connect-time errors into OH's typed error taxonomy.
43
+ *
44
+ * When the auth provider exposes `awaitCallback()` (i.e. OhOAuthProvider), this
45
+ * function handles the full OAuth callback → finishAuth → reconnect loop so callers
46
+ * don't need to orchestrate it manually.
36
47
  */
37
- export declare function buildClient(cfg: NormalizedConfig): Promise<Client>;
48
+ export declare function buildClient(cfg: NormalizedConfig, opts?: BuildClientOptions): Promise<Client>;
38
49
  //# sourceMappingURL=transport.d.ts.map