@chrischall/mcp-utils 0.1.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.
Files changed (50) hide show
  1. package/README.md +235 -0
  2. package/dist/auth/index.d.ts +223 -0
  3. package/dist/auth/index.d.ts.map +1 -0
  4. package/dist/auth/index.js +267 -0
  5. package/dist/auth/index.js.map +1 -0
  6. package/dist/config/index.d.ts +86 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +121 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/errors/index.d.ts +90 -0
  11. package/dist/errors/index.d.ts.map +1 -0
  12. package/dist/errors/index.js +157 -0
  13. package/dist/errors/index.js.map +1 -0
  14. package/dist/fetchproxy/index.d.ts +156 -0
  15. package/dist/fetchproxy/index.d.ts.map +1 -0
  16. package/dist/fetchproxy/index.js +197 -0
  17. package/dist/fetchproxy/index.js.map +1 -0
  18. package/dist/html/index.d.ts +142 -0
  19. package/dist/html/index.d.ts.map +1 -0
  20. package/dist/html/index.js +321 -0
  21. package/dist/html/index.js.map +1 -0
  22. package/dist/http/index.d.ts +202 -0
  23. package/dist/http/index.d.ts.map +1 -0
  24. package/dist/http/index.js +341 -0
  25. package/dist/http/index.js.map +1 -0
  26. package/dist/index.d.ts +23 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +23 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/response/index.d.ts +22 -0
  31. package/dist/response/index.d.ts.map +1 -0
  32. package/dist/response/index.js +61 -0
  33. package/dist/response/index.js.map +1 -0
  34. package/dist/server/index.d.ts +109 -0
  35. package/dist/server/index.d.ts.map +1 -0
  36. package/dist/server/index.js +95 -0
  37. package/dist/server/index.js.map +1 -0
  38. package/dist/session/index.d.ts +233 -0
  39. package/dist/session/index.d.ts.map +1 -0
  40. package/dist/session/index.js +404 -0
  41. package/dist/session/index.js.map +1 -0
  42. package/dist/test/index.d.ts +124 -0
  43. package/dist/test/index.d.ts.map +1 -0
  44. package/dist/test/index.js +181 -0
  45. package/dist/test/index.js.map +1 -0
  46. package/dist/zod/index.d.ts +130 -0
  47. package/dist/zod/index.d.ts.map +1 -0
  48. package/dist/zod/index.js +184 -0
  49. package/dist/zod/index.js.map +1 -0
  50. package/package.json +77 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Auth resolver skeletons — the shared *shape* of the credential-resolution
3
+ * logic duplicated across the fleet (resy / opentable / ofw / zola /
4
+ * signupgenius / creditkarma / canvas / infinitecampus auth.ts).
5
+ *
6
+ * This module owns the **skeleton** only. Per-site parameters (which env var,
7
+ * which cookies to declare, how to parse the session blob, which CSRF input to
8
+ * scrape) are *injected*; nothing here branches on site identity. Site-specific
9
+ * OAuth choreographies (e.g. Skylight's `/auth/session` → `/oauth/authorize` →
10
+ * `/oauth/token` dance) stay per-MCP — they are not a shared shape.
11
+ *
12
+ * Four pieces:
13
+ * - {@link createAuthResolver} — the three-path resolver
14
+ * (env credential → fetchproxy one-shot read → actionable error).
15
+ * - {@link resolveAuthPattern} — the four-path variant
16
+ * (token → OAuth → session-scrape → fetchproxy), each path an injected
17
+ * resolver tried in priority order.
18
+ * - {@link sessionLoginFlow} — CSRF-scrape + cookie POST + success-marker,
19
+ * the shared CSRF+cookie login primitive (canvas / IC / ofw / signupgenius).
20
+ * - {@link createOAuth2Refresher} — an OAuth2 `refresh_token`-grant refresher
21
+ * with optional retry and in-flight race-safety.
22
+ *
23
+ * Security posture: env reads go through the hardened {@link readEnvVar}
24
+ * (placeholder / `'null'` / `'undefined'` suppression); thrown errors never
25
+ * echo the offending secret value and run upstream bodies through
26
+ * {@link truncateErrorMessage} (redaction + truncation) before surfacing.
27
+ */
28
+ import { readEnvVar, parseBoolEnv } from '../config/index.js';
29
+ import { truncateErrorMessage, SessionNotAuthenticatedError, createHelpfulError, } from '../errors/index.js';
30
+ import { parseCookieJar } from '../http/index.js';
31
+ /**
32
+ * Build the canonical **three-path** auth resolver:
33
+ *
34
+ * 1. **Env credential** — `envVar` set (after hardened {@link readEnvVar}
35
+ * sanitization) → returned directly, no network.
36
+ * 2. **fetchproxy one-shot read** — unless `disableEnvVar` is truthy, call the
37
+ * injected `bootstrap` to snapshot the user's signed-in browser session,
38
+ * then `parseTokens` to extract the credential. fetchproxy is invoked once;
39
+ * it is never in the hot path.
40
+ * 3. **Actionable error** — nothing configured: an error naming the env var
41
+ * and the sign-in fallback so the user can pick a fix.
42
+ *
43
+ * The returned `source` is for diagnostics; callers must treat the credential
44
+ * as opaque and not branch on it.
45
+ */
46
+ export function createAuthResolver(opts) {
47
+ const { envVar, disableEnvVar, bootstrap, bootstrapOptions, parseTokens, serviceName, signInHost, env, } = opts;
48
+ return async function resolveAuth() {
49
+ // ── Path 1: env-var credential (hardened against placeholder/sentinel leakage).
50
+ const envCredential = readEnvVar(envVar, env ? { env } : {});
51
+ if (envCredential) {
52
+ return { credential: envCredential, source: 'env' };
53
+ }
54
+ // ── Path 2: fetchproxy one-shot fallback (unless explicitly disabled).
55
+ const disabled = disableEnvVar !== undefined &&
56
+ parseBoolEnv(disableEnvVar, env ? { env } : {});
57
+ if (!disabled) {
58
+ let session;
59
+ try {
60
+ session = await bootstrap(bootstrapOptions);
61
+ }
62
+ catch (e) {
63
+ // Surface the fallback failure but point back at the env-var escape
64
+ // hatch. Redact + truncate the underlying message.
65
+ throw createHelpfulError(`Auth: no ${envVar} set, and fetchproxy fallback failed: ${truncateErrorMessage(messageOf(e))}`, { hint: `Set ${envVar}, or sign in in your browser and retry.` });
66
+ }
67
+ const credential = parseTokens(session);
68
+ if (credential) {
69
+ return { credential, source: 'fetchproxy' };
70
+ }
71
+ // Bootstrap succeeded but the declared key wasn't present → the browser
72
+ // tab isn't signed in. This is the stable "go authenticate" condition.
73
+ throw new SessionNotAuthenticatedError(serviceName, signInHost);
74
+ }
75
+ // ── Path 3: nothing configured and fetchproxy disabled.
76
+ throw createHelpfulError(`Auth: set ${envVar}${signInHost ? `, or sign in at ${signInHost} in your browser` : ', or sign in in your browser'}` +
77
+ `${disableEnvVar ? ` (unset ${disableEnvVar} if it is set)` : ''}.`, { hint: `Set ${envVar} or sign in in your browser.` });
78
+ };
79
+ }
80
+ /**
81
+ * Resolve auth via the four-path priority **token → OAuth → session-scrape →
82
+ * fetchproxy**. Runs the first *provided* resolver in that order (a missing
83
+ * resolver = an unconfigured path). A resolver that throws propagates — a
84
+ * partial-config error (the user's mistake) must surface, not silently fall
85
+ * through. Throws an actionable error when no path is configured at all.
86
+ */
87
+ export async function resolveAuthPattern(pattern) {
88
+ const ordered = [
89
+ pattern.token,
90
+ pattern.oauth,
91
+ pattern.sessionScrape,
92
+ pattern.fetchproxy,
93
+ ];
94
+ for (const resolver of ordered) {
95
+ if (resolver)
96
+ return resolver();
97
+ }
98
+ throw createHelpfulError('No auth configured. Provide a token, OAuth credentials, login credentials, ' +
99
+ 'or sign in in your browser (fetchproxy fallback).', { hint: 'Set one of the supported credential env vars, or sign in in your browser.' });
100
+ }
101
+ const DEFAULT_LOGIN_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
102
+ /** Node's `Headers` may carry multiple `Set-Cookie`s; prefer the spec'd getter. */
103
+ function readSetCookies(headers) {
104
+ const h = headers;
105
+ if (typeof h.getSetCookie === 'function')
106
+ return h.getSetCookie();
107
+ const raw = headers.get('set-cookie');
108
+ return raw ? [raw] : [];
109
+ }
110
+ /**
111
+ * The shared CSRF + cookie login primitive (canvas / IC / ofw / signupgenius):
112
+ *
113
+ * 1. GET `loginUrl` — capture the session cookie(s) and scrape the CSRF token
114
+ * out of the page with `csrfRegex`.
115
+ * 2. POST `postUrl` (`application/x-www-form-urlencoded`) with the scraped CSRF
116
+ * token, credentials, and any `extraFields`, carrying the GET's cookies.
117
+ * 3. Merge `Set-Cookie`s from both responses (deduped, deletions dropped) and
118
+ * require the `tokenField` cookie as the success marker — its value is the
119
+ * returned `token`; its absence means the credentials were rejected.
120
+ *
121
+ * Per-site form parameters and the CSRF regex are injected; the flow itself is
122
+ * site-agnostic.
123
+ */
124
+ export async function sessionLoginFlow(opts) {
125
+ const doFetch = opts.fetchImpl ?? fetch;
126
+ const ua = opts.userAgent ?? DEFAULT_LOGIN_UA;
127
+ const csrfField = opts.csrfField ?? 'csrfToken';
128
+ const emailField = opts.emailField ?? 'email';
129
+ const passwordField = opts.passwordField ?? 'password';
130
+ // Step 1: GET the login page → session cookie + CSRF token.
131
+ const pageRes = await doFetch(opts.loginUrl, {
132
+ headers: { 'User-Agent': ua, Accept: 'text/html' },
133
+ });
134
+ if (!pageRes.ok) {
135
+ throw createHelpfulError(`Login page returned ${pageRes.status} ${pageRes.statusText}`, {
136
+ hint: 'The upstream login page is unreachable; retry later.',
137
+ });
138
+ }
139
+ const pageJar = parseCookieJar(readSetCookies(pageRes.headers));
140
+ const html = await pageRes.text();
141
+ const csrfMatch = opts.csrfRegex.exec(html);
142
+ const csrfToken = csrfMatch?.[1];
143
+ if (!csrfToken) {
144
+ throw createHelpfulError('CSRF token not found on the login page.', {
145
+ hint: 'The login page layout may have changed, or the page failed to load.',
146
+ });
147
+ }
148
+ // Step 2: POST credentials with the scraped CSRF token.
149
+ const body = new URLSearchParams({
150
+ [csrfField]: csrfToken,
151
+ [emailField]: opts.email,
152
+ [passwordField]: opts.password,
153
+ ...opts.extraFields,
154
+ }).toString();
155
+ const postRes = await doFetch(opts.postUrl, {
156
+ method: 'POST',
157
+ redirect: 'manual',
158
+ headers: {
159
+ 'User-Agent': ua,
160
+ Accept: 'text/html',
161
+ 'Content-Type': 'application/x-www-form-urlencoded',
162
+ Referer: opts.loginUrl,
163
+ ...(pageJar.cookieHeader ? { Cookie: pageJar.cookieHeader } : {}),
164
+ },
165
+ body,
166
+ });
167
+ // Step 3: merge cookies from both responses; require the success marker.
168
+ const merged = parseCookieJar([
169
+ ...readSetCookies(pageRes.headers),
170
+ ...readSetCookies(postRes.headers),
171
+ ]);
172
+ const token = merged.cookies[opts.tokenField];
173
+ if (!token) {
174
+ throw createHelpfulError(`Login did not yield a ${opts.tokenField} cookie (status ${postRes.status}) — the credentials were rejected.`, {
175
+ hint: 'Verify the configured email and password. SSO and 2FA-protected accounts are not supported in this mode.',
176
+ });
177
+ }
178
+ return { token, cookies: merged.cookieHeader };
179
+ }
180
+ const sleep = (ms) => ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
181
+ /**
182
+ * Build a race-safe OAuth2 `refresh_token`-grant refresher. The returned
183
+ * function POSTs the form-encoded grant to `endpoint` and parses the standard
184
+ * `{ access_token, refresh_token?, expires_in? }` body.
185
+ *
186
+ * Race-safety: concurrent calls share a single in-flight exchange (the
187
+ * canonical token-refresh-race guard — `skylight`/`canvas`/`creditkarma`/`zola`
188
+ * all hand-roll this). The in-flight promise is cleared once it settles, so a
189
+ * later refresh starts fresh and a *rejected* exchange does not poison the next
190
+ * caller.
191
+ *
192
+ * Errors run through {@link truncateErrorMessage} (redaction + truncation)
193
+ * before surfacing, so an upstream error body can't leak a bearer token or
194
+ * blow up a tool result.
195
+ */
196
+ export function createOAuth2Refresher(opts) {
197
+ const doFetch = opts.fetchImpl ?? fetch;
198
+ const grantType = opts.grantType ?? 'refresh_token';
199
+ const maxRetries = opts.retry?.count ?? 0;
200
+ const retryDelayMs = opts.retry?.delayMs ?? 0;
201
+ let inFlight = null;
202
+ async function exchangeOnce() {
203
+ const body = new URLSearchParams({
204
+ grant_type: grantType,
205
+ refresh_token: opts.refreshToken,
206
+ ...opts.params,
207
+ }).toString();
208
+ const res = await doFetch(opts.endpoint, {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Content-Type': 'application/x-www-form-urlencoded',
212
+ Accept: 'application/json',
213
+ },
214
+ body,
215
+ });
216
+ if (!res.ok) {
217
+ const errText = await res.text().catch(() => '');
218
+ throw createHelpfulError(`OAuth2 token refresh failed: ${res.status} ${res.statusText}: ${truncateErrorMessage(errText, 200)}`, { hint: 'The refresh token may be expired or revoked — re-authenticate.' });
219
+ }
220
+ const data = (await res.json().catch(() => null));
221
+ const accessToken = data?.access_token;
222
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
223
+ throw createHelpfulError('OAuth2 token refresh returned no access_token.', {
224
+ hint: 'The token endpoint returned an unexpected body.',
225
+ });
226
+ }
227
+ const result = { accessToken };
228
+ if (typeof data?.refresh_token === 'string' && data.refresh_token.length > 0) {
229
+ result.refreshToken = data.refresh_token;
230
+ }
231
+ if (typeof data?.expires_in === 'number') {
232
+ result.expiresIn = data.expires_in;
233
+ result.expiresAt = new Date(Date.now() + data.expires_in * 1000);
234
+ }
235
+ return result;
236
+ }
237
+ async function exchangeWithRetry() {
238
+ let lastErr;
239
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
240
+ try {
241
+ return await exchangeOnce();
242
+ }
243
+ catch (e) {
244
+ lastErr = e;
245
+ if (attempt < maxRetries)
246
+ await sleep(retryDelayMs);
247
+ }
248
+ }
249
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
250
+ }
251
+ return function refresh() {
252
+ // Coalesce concurrent callers onto one exchange; clear on settle so the
253
+ // next call starts fresh (and a rejection doesn't stick).
254
+ if (inFlight)
255
+ return inFlight;
256
+ const p = exchangeWithRetry().finally(() => {
257
+ if (inFlight === p)
258
+ inFlight = null;
259
+ });
260
+ inFlight = p;
261
+ return p;
262
+ };
263
+ }
264
+ function messageOf(err) {
265
+ return err instanceof Error ? err.message : String(err);
266
+ }
267
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAkB,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAuDlD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAyB;IAEzB,MAAM,EACJ,MAAM,EACN,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,UAAU,EACV,GAAG,GACJ,GAAG,IAAI,CAAC;IAET,OAAO,KAAK,UAAU,WAAW;QAC/B,iFAAiF;QACjF,MAAM,aAAa,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7D,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACtD,CAAC;QAED,wEAAwE;QACxE,MAAM,QAAQ,GACZ,aAAa,KAAK,SAAS;YAC3B,YAAY,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,OAA0B,CAAC;YAC/B,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,CAAC;YAC9C,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,oEAAoE;gBACpE,mDAAmD;gBACnD,MAAM,kBAAkB,CACtB,YAAY,MAAM,yCAAyC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,EAC/F,EAAE,IAAI,EAAE,OAAO,MAAM,yCAAyC,EAAE,CACjE,CAAC;YACJ,CAAC;YACD,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;YAC9C,CAAC;YACD,wEAAwE;YACxE,uEAAuE;YACvE,MAAM,IAAI,4BAA4B,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAClE,CAAC;QAED,yDAAyD;QACzD,MAAM,kBAAkB,CACtB,aAAa,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,mBAAmB,UAAU,kBAAkB,CAAC,CAAC,CAAC,8BAA8B,EAAE;YACnH,GAAG,aAAa,CAAC,CAAC,CAAC,WAAW,aAAa,gBAAgB,CAAC,CAAC,CAAC,EAAE,GAAG,EACrE,EAAE,IAAI,EAAE,OAAO,MAAM,8BAA8B,EAAE,CACtD,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAkCD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAoB;IAC3D,MAAM,OAAO,GAA4C;QACvD,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,aAAa;QACrB,OAAO,CAAC,UAAU;KACnB,CAAC;IACF,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;QAC/B,IAAI,QAAQ;YAAE,OAAO,QAAQ,EAAE,CAAC;IAClC,CAAC;IACD,MAAM,kBAAkB,CACtB,6EAA6E;QAC3E,mDAAmD,EACrD,EAAE,IAAI,EAAE,2EAA2E,EAAE,CACtF,CAAC;AACJ,CAAC;AA6CD,MAAM,gBAAgB,GACpB,gHAAgH,CAAC;AAEnH,mFAAmF;AACnF,SAAS,cAAc,CAAC,OAAgB;IACtC,MAAM,CAAC,GAAG,OAAsD,CAAC;IACjE,IAAI,OAAO,CAAC,CAAC,YAAY,KAAK,UAAU;QAAE,OAAO,CAAC,CAAC,YAAY,EAAE,CAAC;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAyB;IAEzB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IACxC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,IAAI,gBAAgB,CAAC;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,WAAW,CAAC;IAChD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC;IAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,UAAU,CAAC;IAEvD,4DAA4D;IAC5D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE;QAC3C,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE;KACnD,CAAC,CAAC;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,MAAM,kBAAkB,CAAC,uBAAuB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,EAAE,EAAE;YACtF,IAAI,EAAE,sDAAsD;SAC7D,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAAG,cAAc,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,kBAAkB,CAAC,yCAAyC,EAAE;YAClE,IAAI,EAAE,qEAAqE;SAC5E,CAAC,CAAC;IACL,CAAC;IAED,wDAAwD;IACxD,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,CAAC,SAAS,CAAC,EAAE,SAAS;QACtB,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,KAAK;QACxB,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,QAAQ;QAC9B,GAAG,IAAI,CAAC,WAAW;KACpB,CAAC,CAAC,QAAQ,EAAE,CAAC;IAEd,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE;QAC1C,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE;YACP,YAAY,EAAE,EAAE;YAChB,MAAM,EAAE,WAAW;YACnB,cAAc,EAAE,mCAAmC;YACnD,OAAO,EAAE,IAAI,CAAC,QAAQ;YACtB,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE;QACD,IAAI;KACL,CAAC,CAAC;IAEH,yEAAyE;IACzE,MAAM,MAAM,GAAG,cAAc,CAAC;QAC5B,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC;QAClC,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC;KACnC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,kBAAkB,CACtB,yBAAyB,IAAI,CAAC,UAAU,mBAAmB,OAAO,CAAC,MAAM,oCAAoC,EAC7G;YACE,IAAI,EAAE,0GAA0G;SACjH,CACF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;AACjD,CAAC;AA2CD,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAC1C,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;AAErE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IACxC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,eAAe,CAAC;IACpD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,CAAC,CAAC;IAE9C,IAAI,QAAQ,GAAwC,IAAI,CAAC;IAEzD,KAAK,UAAU,YAAY;QACzB,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,UAAU,EAAE,SAAS;YACrB,aAAa,EAAE,IAAI,CAAC,YAAY;YAChC,GAAG,IAAI,CAAC,MAAM;SACf,CAAC,CAAC,QAAQ,EAAE,CAAC;QAEd,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE;YACvC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,mCAAmC;gBACnD,MAAM,EAAE,kBAAkB;aAC3B;YACD,IAAI;SACL,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACjD,MAAM,kBAAkB,CACtB,gCAAgC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,oBAAoB,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,EACrG,EAAE,IAAI,EAAE,gEAAgE,EAAE,CAC3E,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAiC,CAAC;QAClF,MAAM,WAAW,GAAG,IAAI,EAAE,YAAY,CAAC;QACvC,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChE,MAAM,kBAAkB,CAAC,gDAAgD,EAAE;gBACzE,IAAI,EAAE,iDAAiD;aACxD,CAAC,CAAC;QACL,CAAC;QAED,MAAM,MAAM,GAAwB,EAAE,WAAW,EAAE,CAAC;QACpD,IAAI,OAAO,IAAI,EAAE,aAAa,KAAK,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7E,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC;QAC3C,CAAC;QACD,IAAI,OAAO,IAAI,EAAE,UAAU,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;YACnC,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,UAAU,iBAAiB;QAC9B,IAAI,OAAgB,CAAC;QACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,YAAY,EAAE,CAAC;YAC9B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,GAAG,CAAC,CAAC;gBACZ,IAAI,OAAO,GAAG,UAAU;oBAAE,MAAM,KAAK,CAAC,YAAY,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;QACD,MAAM,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,SAAS,OAAO;QACrB,wEAAwE;QACxE,0DAA0D;QAC1D,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,CAAC,GAAG,iBAAiB,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACzC,IAAI,QAAQ,KAAK,CAAC;gBAAE,QAAQ,GAAG,IAAI,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,QAAQ,GAAG,CAAC,CAAC;QACb,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,86 @@
1
+ /** An environment-variable source: typically `process.env`. */
2
+ export type EnvSource = Record<string, string | undefined>;
3
+ /** Options shared by the env-reading helpers. */
4
+ export interface ReadEnvOptions {
5
+ /** The source to read from. Defaults to {@link process.env}. */
6
+ env?: EnvSource;
7
+ /** Value to return when the variable is unset. */
8
+ default?: string;
9
+ }
10
+ /**
11
+ * Read an environment variable defensively.
12
+ *
13
+ * The value is trimmed of surrounding whitespace and treated as **unset** when
14
+ * it is:
15
+ * - absent or a non-string,
16
+ * - empty / whitespace-only,
17
+ * - the literal string `'undefined'` or `'null'`, or
18
+ * - an unsubstituted `${...}` placeholder.
19
+ *
20
+ * When unset, returns `opts.default` if provided, otherwise `undefined`.
21
+ *
22
+ * Consolidates the `readVar`/`readEnv`/`readEnvString`/`sanitizeEnvVar` snippet
23
+ * duplicated across 12+ MCP servers.
24
+ */
25
+ export declare function readEnvVar(key: string, opts?: ReadEnvOptions): string | undefined;
26
+ /** Options for {@link requireEnvVar}. */
27
+ export interface RequireEnvOptions {
28
+ /** The source to read from. Defaults to {@link process.env}. */
29
+ env?: EnvSource;
30
+ /** Remediation text appended to the thrown error ("here's how to fix it"). */
31
+ hint?: string;
32
+ }
33
+ /**
34
+ * Like {@link readEnvVar} but throws a helpful, value-free error when the
35
+ * variable is unset (or a placeholder/sentinel). The error names only the
36
+ * variable and the optional `hint` — never the offending value — so a leaked
37
+ * placeholder is not echoed back to the caller.
38
+ */
39
+ export declare function requireEnvVar(key: string, opts?: RequireEnvOptions): string;
40
+ /** Options for {@link parseBoolEnv}. */
41
+ export interface ParseBoolEnvOptions {
42
+ /** The source to read from. Defaults to {@link process.env}. */
43
+ env?: EnvSource;
44
+ /** Result when the variable is unset or unrecognised. Defaults to `false`. */
45
+ default?: boolean;
46
+ }
47
+ /**
48
+ * Parse a boolean-ish environment variable. Recognises (case-insensitively)
49
+ * `1/true/yes/on` as `true` and `0/false/no/off` as `false`. Anything unset,
50
+ * placeholder/sentinel, or unrecognised falls back to `opts.default` (`false`).
51
+ *
52
+ * Consolidates the `['1','true','yes','on'].includes(...)` `*_DISABLE_*` flag
53
+ * pattern duplicated across the fleet.
54
+ */
55
+ export declare function parseBoolEnv(key: string, opts?: ParseBoolEnvOptions): boolean;
56
+ /**
57
+ * Expand a user-provided filesystem path:
58
+ * - a leading `~` or `~/...` expands to the user's home directory, and
59
+ * - any remaining relative path is resolved against the current working dir.
60
+ *
61
+ * Note: `~user` (other-user home lookup) and mid-path `~` are intentionally
62
+ * NOT expanded — only the current user's home is ever substituted.
63
+ */
64
+ export declare function expandPath(p: string): string;
65
+ /** Options for {@link loadDotenvSafely}. */
66
+ export interface LoadDotenvOptions {
67
+ /** Path to the `.env` file. Defaults to dotenv's own resolution (CWD). */
68
+ path?: string;
69
+ /**
70
+ * When `true`, `.env` values overwrite already-set `process.env` entries.
71
+ * Defaults to `false` so real host-provided env always wins.
72
+ */
73
+ override?: boolean;
74
+ }
75
+ /**
76
+ * Load a `.env` file for local development, swallowing any failure.
77
+ *
78
+ * `dotenv` is imported dynamically and the whole thing is wrapped so that a
79
+ * missing module (e.g. inside an mcpb bundle, where credentials arrive via the
80
+ * host's `mcp_config.env`) is a silent no-op rather than a crash. Real
81
+ * environment values take precedence unless `override` is set.
82
+ *
83
+ * @returns `true` if a `.env` file was loaded without error, `false` otherwise.
84
+ */
85
+ export declare function loadDotenvSafely(opts?: LoadDotenvOptions): Promise<boolean>;
86
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAGA,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,GAAG,SAAS,CAerF;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAO/E;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAQjF;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBrF"}
@@ -0,0 +1,121 @@
1
+ import { homedir } from 'node:os';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ /**
4
+ * Matches a value that is *entirely* an unsubstituted shell-style placeholder,
5
+ * e.g. `${FOO}` or `${}`. MCP hosts that forward a `.mcp.json` env block without
6
+ * expanding it can leak these literals into credential slots — treating them as
7
+ * unset is the canonical defense against the placeholder-leakage class of bugs.
8
+ *
9
+ * Only a value that is the placeholder *and nothing else* is suppressed; a real
10
+ * secret that happens to embed `${` is preserved.
11
+ */
12
+ const PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
13
+ /**
14
+ * Read an environment variable defensively.
15
+ *
16
+ * The value is trimmed of surrounding whitespace and treated as **unset** when
17
+ * it is:
18
+ * - absent or a non-string,
19
+ * - empty / whitespace-only,
20
+ * - the literal string `'undefined'` or `'null'`, or
21
+ * - an unsubstituted `${...}` placeholder.
22
+ *
23
+ * When unset, returns `opts.default` if provided, otherwise `undefined`.
24
+ *
25
+ * Consolidates the `readVar`/`readEnv`/`readEnvString`/`sanitizeEnvVar` snippet
26
+ * duplicated across 12+ MCP servers.
27
+ */
28
+ export function readEnvVar(key, opts = {}) {
29
+ const env = opts.env ?? process.env;
30
+ const raw = env[key];
31
+ if (typeof raw === 'string') {
32
+ const trimmed = raw.trim();
33
+ if (trimmed.length > 0 &&
34
+ trimmed !== 'undefined' &&
35
+ trimmed !== 'null' &&
36
+ !PLACEHOLDER_RE.test(trimmed)) {
37
+ return trimmed;
38
+ }
39
+ }
40
+ return opts.default;
41
+ }
42
+ /**
43
+ * Like {@link readEnvVar} but throws a helpful, value-free error when the
44
+ * variable is unset (or a placeholder/sentinel). The error names only the
45
+ * variable and the optional `hint` — never the offending value — so a leaked
46
+ * placeholder is not echoed back to the caller.
47
+ */
48
+ export function requireEnvVar(key, opts = {}) {
49
+ const value = readEnvVar(key, { env: opts.env });
50
+ if (value === undefined) {
51
+ const base = `Missing required environment variable ${key}`;
52
+ throw new Error(opts.hint ? `${base}. ${opts.hint}` : base);
53
+ }
54
+ return value;
55
+ }
56
+ const TRUE_TOKENS = new Set(['1', 'true', 'yes', 'on']);
57
+ const FALSE_TOKENS = new Set(['0', 'false', 'no', 'off']);
58
+ /**
59
+ * Parse a boolean-ish environment variable. Recognises (case-insensitively)
60
+ * `1/true/yes/on` as `true` and `0/false/no/off` as `false`. Anything unset,
61
+ * placeholder/sentinel, or unrecognised falls back to `opts.default` (`false`).
62
+ *
63
+ * Consolidates the `['1','true','yes','on'].includes(...)` `*_DISABLE_*` flag
64
+ * pattern duplicated across the fleet.
65
+ */
66
+ export function parseBoolEnv(key, opts = {}) {
67
+ const fallback = opts.default ?? false;
68
+ const raw = readEnvVar(key, { env: opts.env });
69
+ if (raw === undefined)
70
+ return fallback;
71
+ const token = raw.toLowerCase();
72
+ if (TRUE_TOKENS.has(token))
73
+ return true;
74
+ if (FALSE_TOKENS.has(token))
75
+ return false;
76
+ return fallback;
77
+ }
78
+ /**
79
+ * Expand a user-provided filesystem path:
80
+ * - a leading `~` or `~/...` expands to the user's home directory, and
81
+ * - any remaining relative path is resolved against the current working dir.
82
+ *
83
+ * Note: `~user` (other-user home lookup) and mid-path `~` are intentionally
84
+ * NOT expanded — only the current user's home is ever substituted.
85
+ */
86
+ export function expandPath(p) {
87
+ let expanded = p;
88
+ if (p === '~') {
89
+ expanded = homedir();
90
+ }
91
+ else if (p.startsWith('~/')) {
92
+ expanded = join(homedir(), p.slice(2));
93
+ }
94
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
95
+ }
96
+ /**
97
+ * Load a `.env` file for local development, swallowing any failure.
98
+ *
99
+ * `dotenv` is imported dynamically and the whole thing is wrapped so that a
100
+ * missing module (e.g. inside an mcpb bundle, where credentials arrive via the
101
+ * host's `mcp_config.env`) is a silent no-op rather than a crash. Real
102
+ * environment values take precedence unless `override` is set.
103
+ *
104
+ * @returns `true` if a `.env` file was loaded without error, `false` otherwise.
105
+ */
106
+ export async function loadDotenvSafely(opts = {}) {
107
+ try {
108
+ const mod = (await import(/* @vite-ignore */ 'dotenv'));
109
+ const result = mod.config({
110
+ ...(opts.path !== undefined ? { path: opts.path } : {}),
111
+ override: opts.override ?? false,
112
+ quiet: true,
113
+ });
114
+ return result.error === undefined;
115
+ }
116
+ catch {
117
+ // dotenv unavailable (bundled runtime) — rely on process.env.
118
+ return false;
119
+ }
120
+ }
121
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared MCP error classes, error wrapping, and bridge-error discrimination.
3
+ *
4
+ * Consolidates the `instanceof`-chains and remediation-message patterns found
5
+ * in `client.ts` / `auth.ts` across the fleet. Every error carries an optional
6
+ * `hint` — a "here's how to fix it" string the tool surface can show the user.
7
+ *
8
+ * The fetchproxy typed-error hierarchy (`Fetchproxy*Error`) is re-exported, not
9
+ * reimplemented; {@link classifyBridgeError} is a thin discriminator over it.
10
+ */
11
+ /** Default seconds to wait before retrying a tripped bot-wall (issue #90 tuning). */
12
+ export declare const DEFAULT_BOT_WALL_RETRY_AFTER_S = 30;
13
+ /** Default truncation budget for upstream error bodies surfaced to clients. */
14
+ export declare const DEFAULT_ERROR_MESSAGE_MAX = 500;
15
+ /**
16
+ * Base class for every tool-facing error. Carries an optional `hint` —
17
+ * actionable remediation text ("set ZOLA_REFRESH_TOKEN", "sign in at compass.com")
18
+ * the tool surface can present separately from the message.
19
+ */
20
+ export declare class McpToolError extends Error {
21
+ /** Actionable remediation text, when one applies. */
22
+ readonly hint?: string;
23
+ constructor(message: string, opts?: {
24
+ hint?: string;
25
+ cause?: unknown;
26
+ });
27
+ }
28
+ /**
29
+ * The user's browser session isn't signed in to the upstream service. Distinct
30
+ * from a transient bot-wall — this is a stable "go authenticate" condition.
31
+ */
32
+ export declare class SessionNotAuthenticatedError extends McpToolError {
33
+ constructor(service?: string, signInHost?: string);
34
+ }
35
+ /**
36
+ * Transient anti-bot interstitial (PerimeterX / DataDome CAPTCHA). The request
37
+ * was rate-limited, NOT a missing resource — back off and retry. Kept distinct
38
+ * from {@link SessionNotAuthenticatedError} so callers don't misclassify a
39
+ * retryable wall as a stale session (issue #90).
40
+ */
41
+ export declare class BotWallError extends McpToolError {
42
+ /** Suggested seconds to wait before retrying the blocked request(s). */
43
+ readonly retryAfterSeconds: number;
44
+ constructor(path: string, retryAfterSeconds?: number);
45
+ }
46
+ /** Upstream returned HTTP 429 (or an equivalent rate-limit signal). */
47
+ export declare class RateLimitError extends McpToolError {
48
+ /** Seconds the upstream asked us to wait, when it told us. */
49
+ readonly retryAfterSeconds?: number;
50
+ constructor(service: string, retryAfterSeconds?: number);
51
+ }
52
+ /** Upstream is unreachable (5xx / transport failure) — not the caller's fault. */
53
+ export declare class UnreachableError extends McpToolError {
54
+ /** Upstream HTTP status, when one was observed. */
55
+ readonly status?: number;
56
+ constructor(service: string, status?: number);
57
+ }
58
+ /**
59
+ * A tool requires a different auth/operation mode than the server is running in
60
+ * (e.g. a Pro key-mode-only report invoked while in session mode).
61
+ */
62
+ export declare class ModeMismatchError extends McpToolError {
63
+ readonly currentMode: string;
64
+ readonly requiredMode: string;
65
+ readonly feature: string;
66
+ constructor(currentMode: string, requiredMode: string, feature: string);
67
+ }
68
+ /** Factory for an {@link McpToolError} with a remediation hint. */
69
+ export declare function createHelpfulError(message: string, opts?: {
70
+ hint?: string;
71
+ }): McpToolError;
72
+ /**
73
+ * Redact secrets, then cap an (upstream) error string at `max` characters,
74
+ * appending a `… [truncated]` marker when clipped.
75
+ *
76
+ * Security: redaction runs BEFORE truncation so a token straddling the cut
77
+ * boundary can't survive in a half-form. Untrusted upstream bodies must always
78
+ * go through this before reaching a tool result.
79
+ */
80
+ export declare function truncateErrorMessage(text: string, max?: number): string;
81
+ /** Extract a string message from any thrown value. */
82
+ export declare function messageOf(err: unknown): string;
83
+ /**
84
+ * Prepend the tool name to an error's context and return an {@link McpToolError},
85
+ * preserving any `hint` and chaining the original via `cause`. The message is
86
+ * run through {@link truncateErrorMessage} (redaction + truncation). Re-wrapping
87
+ * an already-prefixed error does not double-prefix.
88
+ */
89
+ export declare function wrapToolError(toolName: string, err: unknown): McpToolError;
90
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,qFAAqF;AACrF,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAOvE;AAED;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,YAAY;gBAChD,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAUlD;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBAEvB,IAAI,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAuC;CASrF;AAED,uEAAuE;AACvE,qBAAa,cAAe,SAAQ,YAAY;IAC9C,8DAA8D;IAC9D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM;CAOxD;AAED,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7C;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;IAE/C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM;gBAFf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM;CAS3B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAE1F;AAcD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAkC,GAAG,MAAM,CAKlG;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAG9C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,YAAY,CAM1E"}