@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.
- package/README.md +235 -0
- package/dist/auth/index.d.ts +223 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +267 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/config/index.d.ts +86 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +121 -0
- package/dist/config/index.js.map +1 -0
- package/dist/errors/index.d.ts +90 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +157 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/fetchproxy/index.d.ts +156 -0
- package/dist/fetchproxy/index.d.ts.map +1 -0
- package/dist/fetchproxy/index.js +197 -0
- package/dist/fetchproxy/index.js.map +1 -0
- package/dist/html/index.d.ts +142 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +321 -0
- package/dist/html/index.js.map +1 -0
- package/dist/http/index.d.ts +202 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +341 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/response/index.d.ts +22 -0
- package/dist/response/index.d.ts.map +1 -0
- package/dist/response/index.js +61 -0
- package/dist/response/index.js.map +1 -0
- package/dist/server/index.d.ts +109 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +95 -0
- package/dist/server/index.js.map +1 -0
- package/dist/session/index.d.ts +233 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +404 -0
- package/dist/session/index.js.map +1 -0
- package/dist/test/index.d.ts +124 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +181 -0
- package/dist/test/index.js.map +1 -0
- package/dist/zod/index.d.ts +130 -0
- package/dist/zod/index.d.ts.map +1 -0
- package/dist/zod/index.js +184 -0
- package/dist/zod/index.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `http` — bearer-auth API-client kit.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the ~100-line `client.ts` boilerplate copy-pasted across ~8 MCP
|
|
5
|
+
* servers (splitwise/tempo/ioffice/app-store-connect/zola): a bearer-auth fetch
|
|
6
|
+
* wrapper with one-shot 429 retry, 401 mapping, 204 handling, and redacted error
|
|
7
|
+
* formatting — plus the small URL/JWT/cookie utilities scattered across the
|
|
8
|
+
* fleet (canvas Link-header parsing, IC/signupgenius cookie jars, zola JWT
|
|
9
|
+
* decode).
|
|
10
|
+
*
|
|
11
|
+
* Security posture (design §"Bearer-token / error-message leakage"):
|
|
12
|
+
* - {@link formatApiError} runs every upstream body through the shared
|
|
13
|
+
* {@link truncateErrorMessage} (redaction THEN truncation) so bearer tokens
|
|
14
|
+
* and JWTs never reach a tool result, even when an upstream echoes the
|
|
15
|
+
* request back.
|
|
16
|
+
* - {@link createApiClient} never embeds the token in a thrown message; a 401
|
|
17
|
+
* yields a fixed "unauthorized" string, not the credential.
|
|
18
|
+
*/
|
|
19
|
+
/** Retry policy for transient (HTTP 429) responses. */
|
|
20
|
+
export interface RetryPolicy {
|
|
21
|
+
/** Number of retries after the initial attempt. The fleet default is 1. */
|
|
22
|
+
count: number;
|
|
23
|
+
/** Delay before each retry, in milliseconds. The fleet default is 2000. */
|
|
24
|
+
delayMs: number;
|
|
25
|
+
}
|
|
26
|
+
/** Options for {@link createApiClient}. */
|
|
27
|
+
export interface ApiClientOptions {
|
|
28
|
+
/** Absolute base URL; request paths are appended verbatim. A trailing slash is trimmed. */
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the current bearer token. Called per request so the caller can
|
|
32
|
+
* refresh/rotate transparently. May be sync or async. Return `undefined`/`''`
|
|
33
|
+
* to send no `Authorization` header (e.g. cookie-authenticated APIs).
|
|
34
|
+
*/
|
|
35
|
+
getToken: () => string | undefined | Promise<string | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* 429 retry policy. Defaults to `{ count: 1, delayMs: 2000 }` — the
|
|
38
|
+
* fleet-wide "retry once after 2s" behavior. Set `count: 0` to disable.
|
|
39
|
+
*/
|
|
40
|
+
retry?: RetryPolicy;
|
|
41
|
+
/** Human name of the upstream service, used in error messages. Defaults to the host. */
|
|
42
|
+
serviceName?: string;
|
|
43
|
+
/** Injectable fetch (for tests). Defaults to the global `fetch`. */
|
|
44
|
+
fetchImpl?: typeof fetch;
|
|
45
|
+
/** Injectable sleep (for tests). Defaults to `setTimeout`. */
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** A request body and/or extra headers for a single call. */
|
|
49
|
+
export interface RequestOptions {
|
|
50
|
+
/** JSON-serialized into the request body when present. */
|
|
51
|
+
body?: unknown;
|
|
52
|
+
/** Extra request headers, merged over the defaults. */
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
/** Query params appended via {@link buildQueryString} when present. */
|
|
55
|
+
query?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/** The minimal client surface returned by {@link createApiClient}. */
|
|
58
|
+
export interface ApiClient {
|
|
59
|
+
/**
|
|
60
|
+
* Authenticated JSON request. Returns the parsed body, or `undefined` for a
|
|
61
|
+
* 204 / empty body. Throws on 401 (unauthorized), exhausted-429, and other
|
|
62
|
+
* non-2xx responses (with a redacted, truncated message).
|
|
63
|
+
*/
|
|
64
|
+
fetchJson: <T = unknown>(method: string, path: string, opts?: RequestOptions) => Promise<T>;
|
|
65
|
+
/** Authenticated request returning the raw response body as text (e.g. HTML scrapes). */
|
|
66
|
+
fetchHtml: (method: string, path: string, opts?: RequestOptions) => Promise<string>;
|
|
67
|
+
}
|
|
68
|
+
/** Thrown for an upstream 401. Carries the status so callers can trigger a re-auth. */
|
|
69
|
+
export declare class UnauthorizedError extends Error {
|
|
70
|
+
readonly status = 401;
|
|
71
|
+
constructor(service: string);
|
|
72
|
+
}
|
|
73
|
+
/** Thrown when a 429 persists after the retry budget is exhausted. */
|
|
74
|
+
export declare class RateLimitedError extends Error {
|
|
75
|
+
readonly status = 429;
|
|
76
|
+
constructor(service: string);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build a bearer-auth fetch client with one-shot 429 retry, 401 mapping, 204 /
|
|
80
|
+
* empty-body handling, and redacted error formatting.
|
|
81
|
+
*
|
|
82
|
+
* Consolidates the structurally-identical `client.ts#doRequest` across
|
|
83
|
+
* splitwise/tempo/ioffice/app-store-connect/zola. The retry/401/429 behavior is
|
|
84
|
+
* the hardened superset: 401 → {@link UnauthorizedError} (never echoing the
|
|
85
|
+
* token), 429 → sleep(`delayMs`) and replay up to `count` times then
|
|
86
|
+
* {@link RateLimitedError}, 204/empty → `undefined`, other non-2xx →
|
|
87
|
+
* {@link formatApiError}.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createApiClient(opts: ApiClientOptions): ApiClient;
|
|
90
|
+
/**
|
|
91
|
+
* Build a URL query string from a params object, returning `''` or
|
|
92
|
+
* `?k=v&k2=v2`. Skips `undefined`, `null`, and empty-string values; expands
|
|
93
|
+
* arrays into repeated keys (skipping null/undefined/empty array members); and
|
|
94
|
+
* percent-encodes keys and values.
|
|
95
|
+
*
|
|
96
|
+
* Consolidates the divergent `buildQueryString` / inline `URLSearchParams`
|
|
97
|
+
* variants across compass/redfin/zillow/homes/opentable/tempo/ioffice into one
|
|
98
|
+
* superset (array support + empty-string skipping + encoding).
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildQueryString(params: Record<string, unknown>): string;
|
|
101
|
+
/**
|
|
102
|
+
* Build a request body from `args`, including only the `optionalFields` that are
|
|
103
|
+
* actually present (not `undefined`). `null` is preserved — some APIs use it to
|
|
104
|
+
* clear a field — only `undefined` (i.e. "not provided") is dropped.
|
|
105
|
+
*
|
|
106
|
+
* Consolidates tempo's optional-field body builder: lets a tool forward a wide
|
|
107
|
+
* args object and emit a minimal PATCH/POST body.
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildOptionalBody<T extends Record<string, unknown>, K extends keyof T>(args: T, optionalFields: readonly K[]): Partial<Pick<T, K>>;
|
|
110
|
+
/** Options for {@link formatApiError}. */
|
|
111
|
+
export interface FormatApiErrorOptions {
|
|
112
|
+
/** Service name woven into the prefix. Defaults to `'API'`. */
|
|
113
|
+
service?: string;
|
|
114
|
+
/** Truncation budget for the upstream body. Defaults to the shared 500. */
|
|
115
|
+
max?: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Format a non-2xx upstream response into a single, client-safe error string:
|
|
119
|
+
* `"{service} error {status} for {METHOD} {path}: {body}"`.
|
|
120
|
+
*
|
|
121
|
+
* SECURITY: the upstream `errorText` is run through the shared
|
|
122
|
+
* {@link truncateErrorMessage} (redaction of `Bearer <token>` / JWTs FIRST,
|
|
123
|
+
* then truncation) so a raw body that echoes the request — or an upstream that
|
|
124
|
+
* leaks a token — never reaches the caller. The method is upper-cased; an
|
|
125
|
+
* empty/whitespace body is dropped entirely rather than printing a dangling
|
|
126
|
+
* colon.
|
|
127
|
+
*/
|
|
128
|
+
export declare function formatApiError(status: number, method: string, path: string, errorText: string, opts?: FormatApiErrorOptions): string;
|
|
129
|
+
/** The RFC 5988 rels callers care about for pagination. */
|
|
130
|
+
export interface ParsedLinkHeader {
|
|
131
|
+
next?: string;
|
|
132
|
+
prev?: string;
|
|
133
|
+
first?: string;
|
|
134
|
+
last?: string;
|
|
135
|
+
/** Any other rels present, keyed by rel name. */
|
|
136
|
+
[rel: string]: string | undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parse an RFC 5988 `Link` header into a `{ rel: url }` map. Malformed entries
|
|
140
|
+
* are skipped; `rel="next"` and bare `rel=next` are both accepted. A missing or
|
|
141
|
+
* empty header yields `{}`.
|
|
142
|
+
*
|
|
143
|
+
* Consolidates canvas's pagination Link parser.
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseLinkHeader(header: string | null | undefined): ParsedLinkHeader;
|
|
146
|
+
/** A parsed Set-Cookie jar: deduplicated name→value plus a ready `Cookie` header. */
|
|
147
|
+
export interface CookieJar {
|
|
148
|
+
/** Surviving cookies, name → value (last value wins, deletions removed). */
|
|
149
|
+
cookies: Record<string, string>;
|
|
150
|
+
/** Pre-joined `name=value; name2=value2` string for the `Cookie` request header. */
|
|
151
|
+
cookieHeader: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parse `Set-Cookie` headers into a deduplicated {@link CookieJar}.
|
|
155
|
+
*
|
|
156
|
+
* Login responses commonly send deletion markers (`Max-Age=0` or an epoch
|
|
157
|
+
* `Expires`) alongside real cookies; forwarding both the delete and set form of
|
|
158
|
+
* a name makes some upstreams (e.g. IC) reject the request. This parser:
|
|
159
|
+
* - drops deletion markers (`Max-Age=0`, epoch `Expires`),
|
|
160
|
+
* - drops empty-value cookies (clearing instructions),
|
|
161
|
+
* - deduplicates by name with **last value wins**, preserving order.
|
|
162
|
+
*
|
|
163
|
+
* Accepts the array from `Headers.getSetCookie()` (or a single joined string —
|
|
164
|
+
* which it splits defensively, though `getSetCookie()` is strongly preferred
|
|
165
|
+
* since commas inside `Expires` make string-splitting lossy).
|
|
166
|
+
*
|
|
167
|
+
* Consolidates the IC/signupgenius cookie-jar logic.
|
|
168
|
+
*/
|
|
169
|
+
export declare function parseCookieJar(setCookieHeaders: string[] | string | null | undefined): CookieJar;
|
|
170
|
+
/**
|
|
171
|
+
* Decode a JWT's `exp` claim (seconds since epoch). Throws when the structure is
|
|
172
|
+
* invalid or `exp` is missing/non-numeric — the strict variant zola uses for the
|
|
173
|
+
* token it depends on. For a lenient probe, use {@link validateJwtExpiry}.
|
|
174
|
+
*/
|
|
175
|
+
export declare function decodeJwtExp(token: string): number;
|
|
176
|
+
/**
|
|
177
|
+
* Best-effort extraction of a session id from a JWT payload, checking
|
|
178
|
+
* `session_id` then `sid`. Returns `null` for an undecodable token or absent
|
|
179
|
+
* claim (never throws) — the lenient variant zola uses for the WAF session
|
|
180
|
+
* header.
|
|
181
|
+
*/
|
|
182
|
+
export declare function decodeJwtSessionId(token: string): string | null;
|
|
183
|
+
/** Result of {@link validateJwtExpiry}. */
|
|
184
|
+
export interface JwtExpiryStatus {
|
|
185
|
+
/** True when the token is past its `exp` (or undecodable / lacks `exp`). */
|
|
186
|
+
expired: boolean;
|
|
187
|
+
/** Seconds until expiry (negative once expired); omitted when undecodable. */
|
|
188
|
+
expiresIn?: number;
|
|
189
|
+
/** Human-readable caution when the token is expired or near expiry. */
|
|
190
|
+
warning?: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Non-throwing expiry probe for a bearer JWT. Returns `{ expired, expiresIn?,
|
|
194
|
+
* warning? }`. An undecodable token (or one lacking a numeric `exp`) is treated
|
|
195
|
+
* as `expired: true` with a warning — failing closed so a malformed token forces
|
|
196
|
+
* a refresh rather than being sent and bouncing as a 401.
|
|
197
|
+
*
|
|
198
|
+
* Near-expiry (within {@link NEAR_EXPIRY_SKEW_SEC}) yields a warning while still
|
|
199
|
+
* reporting `expired: false`, so callers can refresh proactively.
|
|
200
|
+
*/
|
|
201
|
+
export declare function validateJwtExpiry(token: string, nowMs?: number): JwtExpiryStatus;
|
|
202
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `http` — bearer-auth API-client kit.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the ~100-line `client.ts` boilerplate copy-pasted across ~8 MCP
|
|
5
|
+
* servers (splitwise/tempo/ioffice/app-store-connect/zola): a bearer-auth fetch
|
|
6
|
+
* wrapper with one-shot 429 retry, 401 mapping, 204 handling, and redacted error
|
|
7
|
+
* formatting — plus the small URL/JWT/cookie utilities scattered across the
|
|
8
|
+
* fleet (canvas Link-header parsing, IC/signupgenius cookie jars, zola JWT
|
|
9
|
+
* decode).
|
|
10
|
+
*
|
|
11
|
+
* Security posture (design §"Bearer-token / error-message leakage"):
|
|
12
|
+
* - {@link formatApiError} runs every upstream body through the shared
|
|
13
|
+
* {@link truncateErrorMessage} (redaction THEN truncation) so bearer tokens
|
|
14
|
+
* and JWTs never reach a tool result, even when an upstream echoes the
|
|
15
|
+
* request back.
|
|
16
|
+
* - {@link createApiClient} never embeds the token in a thrown message; a 401
|
|
17
|
+
* yields a fixed "unauthorized" string, not the credential.
|
|
18
|
+
*/
|
|
19
|
+
import { truncateErrorMessage } from '../errors/index.js';
|
|
20
|
+
const DEFAULT_RETRY = { count: 1, delayMs: 2000 };
|
|
21
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
|
+
/** Thrown for an upstream 401. Carries the status so callers can trigger a re-auth. */
|
|
23
|
+
export class UnauthorizedError extends Error {
|
|
24
|
+
status = 401;
|
|
25
|
+
constructor(service) {
|
|
26
|
+
super(`Unauthorized (401) from ${service} — the token is missing, invalid, or expired.`);
|
|
27
|
+
this.name = 'UnauthorizedError';
|
|
28
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Thrown when a 429 persists after the retry budget is exhausted. */
|
|
32
|
+
export class RateLimitedError extends Error {
|
|
33
|
+
status = 429;
|
|
34
|
+
constructor(service) {
|
|
35
|
+
super(`Rate limited (429) by ${service} after retries.`);
|
|
36
|
+
this.name = 'RateLimitedError';
|
|
37
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function hostOf(baseUrl) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(baseUrl).host;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return baseUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a bearer-auth fetch client with one-shot 429 retry, 401 mapping, 204 /
|
|
50
|
+
* empty-body handling, and redacted error formatting.
|
|
51
|
+
*
|
|
52
|
+
* Consolidates the structurally-identical `client.ts#doRequest` across
|
|
53
|
+
* splitwise/tempo/ioffice/app-store-connect/zola. The retry/401/429 behavior is
|
|
54
|
+
* the hardened superset: 401 → {@link UnauthorizedError} (never echoing the
|
|
55
|
+
* token), 429 → sleep(`delayMs`) and replay up to `count` times then
|
|
56
|
+
* {@link RateLimitedError}, 204/empty → `undefined`, other non-2xx →
|
|
57
|
+
* {@link formatApiError}.
|
|
58
|
+
*/
|
|
59
|
+
export function createApiClient(opts) {
|
|
60
|
+
const base = opts.baseUrl.replace(/\/+$/, '');
|
|
61
|
+
const retry = opts.retry ?? DEFAULT_RETRY;
|
|
62
|
+
const service = opts.serviceName ?? hostOf(opts.baseUrl);
|
|
63
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
64
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
65
|
+
async function send(method, path, opt) {
|
|
66
|
+
const token = await opts.getToken();
|
|
67
|
+
const headers = {
|
|
68
|
+
Accept: 'application/json',
|
|
69
|
+
...(opt.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
70
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
71
|
+
...opt.headers,
|
|
72
|
+
};
|
|
73
|
+
const query = opt.query ? buildQueryString(opt.query) : '';
|
|
74
|
+
const url = `${base}${path}${query}`;
|
|
75
|
+
let attempt = 0;
|
|
76
|
+
// attempt 0 is the initial request; up to `retry.count` further attempts on 429.
|
|
77
|
+
for (;;) {
|
|
78
|
+
const res = await doFetch(url, {
|
|
79
|
+
method,
|
|
80
|
+
headers,
|
|
81
|
+
...(opt.body !== undefined ? { body: JSON.stringify(opt.body) } : {}),
|
|
82
|
+
});
|
|
83
|
+
if (res.status === 429 && attempt < retry.count) {
|
|
84
|
+
attempt += 1;
|
|
85
|
+
await sleep(retry.delayMs);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function fetchJson(method, path, opt = {}) {
|
|
92
|
+
const res = await send(method, path, opt);
|
|
93
|
+
if (res.status === 401)
|
|
94
|
+
throw new UnauthorizedError(service);
|
|
95
|
+
if (res.status === 429)
|
|
96
|
+
throw new RateLimitedError(service);
|
|
97
|
+
if (res.status === 204)
|
|
98
|
+
return undefined;
|
|
99
|
+
const text = await res.text();
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(formatApiError(res.status, method, path, text, { service }));
|
|
102
|
+
}
|
|
103
|
+
if (text.length === 0)
|
|
104
|
+
return undefined;
|
|
105
|
+
return JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
async function fetchHtml(method, path, opt = {}) {
|
|
108
|
+
const headers = { Accept: 'text/html,*/*', ...opt.headers };
|
|
109
|
+
const res = await send(method, path, { ...opt, headers });
|
|
110
|
+
if (res.status === 401)
|
|
111
|
+
throw new UnauthorizedError(service);
|
|
112
|
+
if (res.status === 429)
|
|
113
|
+
throw new RateLimitedError(service);
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
throw new Error(formatApiError(res.status, method, path, text, { service }));
|
|
117
|
+
}
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
return { fetchJson, fetchHtml };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// buildQueryString
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* Build a URL query string from a params object, returning `''` or
|
|
127
|
+
* `?k=v&k2=v2`. Skips `undefined`, `null`, and empty-string values; expands
|
|
128
|
+
* arrays into repeated keys (skipping null/undefined/empty array members); and
|
|
129
|
+
* percent-encodes keys and values.
|
|
130
|
+
*
|
|
131
|
+
* Consolidates the divergent `buildQueryString` / inline `URLSearchParams`
|
|
132
|
+
* variants across compass/redfin/zillow/homes/opentable/tempo/ioffice into one
|
|
133
|
+
* superset (array support + empty-string skipping + encoding).
|
|
134
|
+
*/
|
|
135
|
+
export function buildQueryString(params) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
for (const [key, value] of Object.entries(params)) {
|
|
138
|
+
if (value === undefined || value === null || value === '')
|
|
139
|
+
continue;
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
for (const item of value) {
|
|
142
|
+
if (item === undefined || item === null || item === '')
|
|
143
|
+
continue;
|
|
144
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// buildOptionalBody
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Build a request body from `args`, including only the `optionalFields` that are
|
|
158
|
+
* actually present (not `undefined`). `null` is preserved — some APIs use it to
|
|
159
|
+
* clear a field — only `undefined` (i.e. "not provided") is dropped.
|
|
160
|
+
*
|
|
161
|
+
* Consolidates tempo's optional-field body builder: lets a tool forward a wide
|
|
162
|
+
* args object and emit a minimal PATCH/POST body.
|
|
163
|
+
*/
|
|
164
|
+
export function buildOptionalBody(args, optionalFields) {
|
|
165
|
+
const body = {};
|
|
166
|
+
for (const field of optionalFields) {
|
|
167
|
+
if (args[field] !== undefined) {
|
|
168
|
+
body[field] = args[field];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return body;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Format a non-2xx upstream response into a single, client-safe error string:
|
|
175
|
+
* `"{service} error {status} for {METHOD} {path}: {body}"`.
|
|
176
|
+
*
|
|
177
|
+
* SECURITY: the upstream `errorText` is run through the shared
|
|
178
|
+
* {@link truncateErrorMessage} (redaction of `Bearer <token>` / JWTs FIRST,
|
|
179
|
+
* then truncation) so a raw body that echoes the request — or an upstream that
|
|
180
|
+
* leaks a token — never reaches the caller. The method is upper-cased; an
|
|
181
|
+
* empty/whitespace body is dropped entirely rather than printing a dangling
|
|
182
|
+
* colon.
|
|
183
|
+
*/
|
|
184
|
+
export function formatApiError(status, method, path, errorText, opts = {}) {
|
|
185
|
+
const service = opts.service ?? 'API';
|
|
186
|
+
const head = `${service} error ${status} for ${method.toUpperCase()} ${path}`;
|
|
187
|
+
const safe = truncateErrorMessage(errorText ?? '', opts.max).trim();
|
|
188
|
+
return safe.length > 0 ? `${head}: ${safe}` : head;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Parse an RFC 5988 `Link` header into a `{ rel: url }` map. Malformed entries
|
|
192
|
+
* are skipped; `rel="next"` and bare `rel=next` are both accepted. A missing or
|
|
193
|
+
* empty header yields `{}`.
|
|
194
|
+
*
|
|
195
|
+
* Consolidates canvas's pagination Link parser.
|
|
196
|
+
*/
|
|
197
|
+
export function parseLinkHeader(header) {
|
|
198
|
+
const out = {};
|
|
199
|
+
if (!header)
|
|
200
|
+
return out;
|
|
201
|
+
for (const part of header.split(',')) {
|
|
202
|
+
const m = part.trim().match(/^<([^>]+)>\s*;\s*rel="?([^";]+)"?/);
|
|
203
|
+
if (m && m[1] && m[2])
|
|
204
|
+
out[m[2].trim()] = m[1];
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
const MAX_AGE_ZERO_RE = /(?:^|;)\s*Max-Age\s*=\s*0\s*(?:;|$)/i;
|
|
209
|
+
const EXPIRES_EPOCH_RE = /(?:^|;)\s*Expires\s*=\s*Thu,\s*01\s*Jan\s*1970/i;
|
|
210
|
+
/**
|
|
211
|
+
* Parse `Set-Cookie` headers into a deduplicated {@link CookieJar}.
|
|
212
|
+
*
|
|
213
|
+
* Login responses commonly send deletion markers (`Max-Age=0` or an epoch
|
|
214
|
+
* `Expires`) alongside real cookies; forwarding both the delete and set form of
|
|
215
|
+
* a name makes some upstreams (e.g. IC) reject the request. This parser:
|
|
216
|
+
* - drops deletion markers (`Max-Age=0`, epoch `Expires`),
|
|
217
|
+
* - drops empty-value cookies (clearing instructions),
|
|
218
|
+
* - deduplicates by name with **last value wins**, preserving order.
|
|
219
|
+
*
|
|
220
|
+
* Accepts the array from `Headers.getSetCookie()` (or a single joined string —
|
|
221
|
+
* which it splits defensively, though `getSetCookie()` is strongly preferred
|
|
222
|
+
* since commas inside `Expires` make string-splitting lossy).
|
|
223
|
+
*
|
|
224
|
+
* Consolidates the IC/signupgenius cookie-jar logic.
|
|
225
|
+
*/
|
|
226
|
+
export function parseCookieJar(setCookieHeaders) {
|
|
227
|
+
const entries = setCookieHeaders == null
|
|
228
|
+
? []
|
|
229
|
+
: Array.isArray(setCookieHeaders)
|
|
230
|
+
? setCookieHeaders
|
|
231
|
+
: splitSetCookie(setCookieHeaders);
|
|
232
|
+
const jar = new Map();
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (MAX_AGE_ZERO_RE.test(entry) || EXPIRES_EPOCH_RE.test(entry))
|
|
235
|
+
continue;
|
|
236
|
+
const nameValue = (entry.split(';')[0] ?? '').trim();
|
|
237
|
+
const eqIdx = nameValue.indexOf('=');
|
|
238
|
+
if (eqIdx < 1)
|
|
239
|
+
continue;
|
|
240
|
+
const name = nameValue.slice(0, eqIdx).trim();
|
|
241
|
+
const value = nameValue.slice(eqIdx + 1).trim();
|
|
242
|
+
if (!value)
|
|
243
|
+
continue;
|
|
244
|
+
jar.set(name, value);
|
|
245
|
+
}
|
|
246
|
+
const cookies = {};
|
|
247
|
+
for (const [k, v] of jar)
|
|
248
|
+
cookies[k] = v;
|
|
249
|
+
const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
250
|
+
return { cookies, cookieHeader };
|
|
251
|
+
}
|
|
252
|
+
/** Best-effort split of a single joined `set-cookie` string (no `getSetCookie()`). */
|
|
253
|
+
function splitSetCookie(header) {
|
|
254
|
+
// Split on commas that are NOT part of an Expires date ("Thu, 01 Jan ...").
|
|
255
|
+
return header
|
|
256
|
+
.split(/,(?=\s*[^;,\s]+\s*=)/)
|
|
257
|
+
.map((s) => s.trim())
|
|
258
|
+
.filter((s) => s.length > 0);
|
|
259
|
+
}
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// JWT helpers
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
/** Decode the base64url JWT payload to an object, or `null` if it can't be parsed. */
|
|
264
|
+
function decodeJwtPayload(token) {
|
|
265
|
+
if (typeof token !== 'string')
|
|
266
|
+
return null;
|
|
267
|
+
const parts = token.split('.');
|
|
268
|
+
if (parts.length < 2 || !parts[1])
|
|
269
|
+
return null;
|
|
270
|
+
try {
|
|
271
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
272
|
+
const payload = JSON.parse(json);
|
|
273
|
+
if (payload === null || typeof payload !== 'object')
|
|
274
|
+
return null;
|
|
275
|
+
return payload;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Decode a JWT's `exp` claim (seconds since epoch). Throws when the structure is
|
|
283
|
+
* invalid or `exp` is missing/non-numeric — the strict variant zola uses for the
|
|
284
|
+
* token it depends on. For a lenient probe, use {@link validateJwtExpiry}.
|
|
285
|
+
*/
|
|
286
|
+
export function decodeJwtExp(token) {
|
|
287
|
+
const payload = decodeJwtPayload(token);
|
|
288
|
+
if (!payload) {
|
|
289
|
+
throw new Error('Invalid JWT: could not decode payload (expected a 3-part base64url token)');
|
|
290
|
+
}
|
|
291
|
+
const exp = payload['exp'];
|
|
292
|
+
if (typeof exp !== 'number') {
|
|
293
|
+
throw new Error('Invalid JWT: missing numeric "exp" claim');
|
|
294
|
+
}
|
|
295
|
+
return exp;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Best-effort extraction of a session id from a JWT payload, checking
|
|
299
|
+
* `session_id` then `sid`. Returns `null` for an undecodable token or absent
|
|
300
|
+
* claim (never throws) — the lenient variant zola uses for the WAF session
|
|
301
|
+
* header.
|
|
302
|
+
*/
|
|
303
|
+
export function decodeJwtSessionId(token) {
|
|
304
|
+
const payload = decodeJwtPayload(token);
|
|
305
|
+
if (!payload)
|
|
306
|
+
return null;
|
|
307
|
+
const sid = payload['session_id'] ?? payload['sid'];
|
|
308
|
+
return typeof sid === 'string' ? sid : null;
|
|
309
|
+
}
|
|
310
|
+
/** Tokens within this many seconds of `exp` are flagged as near-expiry. */
|
|
311
|
+
const NEAR_EXPIRY_SKEW_SEC = 300;
|
|
312
|
+
/**
|
|
313
|
+
* Non-throwing expiry probe for a bearer JWT. Returns `{ expired, expiresIn?,
|
|
314
|
+
* warning? }`. An undecodable token (or one lacking a numeric `exp`) is treated
|
|
315
|
+
* as `expired: true` with a warning — failing closed so a malformed token forces
|
|
316
|
+
* a refresh rather than being sent and bouncing as a 401.
|
|
317
|
+
*
|
|
318
|
+
* Near-expiry (within {@link NEAR_EXPIRY_SKEW_SEC}) yields a warning while still
|
|
319
|
+
* reporting `expired: false`, so callers can refresh proactively.
|
|
320
|
+
*/
|
|
321
|
+
export function validateJwtExpiry(token, nowMs = Date.now()) {
|
|
322
|
+
const payload = decodeJwtPayload(token);
|
|
323
|
+
const exp = payload?.['exp'];
|
|
324
|
+
if (typeof exp !== 'number') {
|
|
325
|
+
return { expired: true, warning: 'Token could not be decoded or has no expiry; treat as expired.' };
|
|
326
|
+
}
|
|
327
|
+
const nowSec = Math.floor(nowMs / 1000);
|
|
328
|
+
const expiresIn = exp - nowSec;
|
|
329
|
+
if (expiresIn <= 0) {
|
|
330
|
+
return { expired: true, expiresIn, warning: 'Token has expired; refresh before use.' };
|
|
331
|
+
}
|
|
332
|
+
if (expiresIn <= NEAR_EXPIRY_SKEW_SEC) {
|
|
333
|
+
return {
|
|
334
|
+
expired: false,
|
|
335
|
+
expiresIn,
|
|
336
|
+
warning: `Token expires in ${expiresIn}s; refresh soon.`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return { expired: false, expiresIn };
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chrischall/mcp-utils` — core barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the framework-agnostic building blocks every server in the fleet
|
|
5
|
+
* reaches for: server bootstrap, tool-result formatting, helpful errors,
|
|
6
|
+
* hardened env/config, a bearer API-client kit, zod atoms, and the auth
|
|
7
|
+
* resolver skeletons.
|
|
8
|
+
*
|
|
9
|
+
* Heavier / optional-dependency modules are published as subpath entries and
|
|
10
|
+
* are intentionally NOT re-exported here:
|
|
11
|
+
* - `@chrischall/mcp-utils/session` session registries + token manager
|
|
12
|
+
* - `@chrischall/mcp-utils/fetchproxy` fetchproxy transport adapter
|
|
13
|
+
* - `@chrischall/mcp-utils/html` opt-in HTML scraping helpers
|
|
14
|
+
* - `@chrischall/mcp-utils/test` in-memory test harness
|
|
15
|
+
*/
|
|
16
|
+
export * from './server/index.js';
|
|
17
|
+
export * from './response/index.js';
|
|
18
|
+
export * from './errors/index.js';
|
|
19
|
+
export * from './config/index.js';
|
|
20
|
+
export * from './http/index.js';
|
|
21
|
+
export * from './zod/index.js';
|
|
22
|
+
export * from './auth/index.js';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chrischall/mcp-utils` — core barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the framework-agnostic building blocks every server in the fleet
|
|
5
|
+
* reaches for: server bootstrap, tool-result formatting, helpful errors,
|
|
6
|
+
* hardened env/config, a bearer API-client kit, zod atoms, and the auth
|
|
7
|
+
* resolver skeletons.
|
|
8
|
+
*
|
|
9
|
+
* Heavier / optional-dependency modules are published as subpath entries and
|
|
10
|
+
* are intentionally NOT re-exported here:
|
|
11
|
+
* - `@chrischall/mcp-utils/session` session registries + token manager
|
|
12
|
+
* - `@chrischall/mcp-utils/fetchproxy` fetchproxy transport adapter
|
|
13
|
+
* - `@chrischall/mcp-utils/html` opt-in HTML scraping helpers
|
|
14
|
+
* - `@chrischall/mcp-utils/test` in-memory test harness
|
|
15
|
+
*/
|
|
16
|
+
export * from './server/index.js';
|
|
17
|
+
export * from './response/index.js';
|
|
18
|
+
export * from './errors/index.js';
|
|
19
|
+
export * from './config/index.js';
|
|
20
|
+
export * from './http/index.js';
|
|
21
|
+
export * from './zod/index.js';
|
|
22
|
+
export * from './auth/index.js';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wrap any JSON-serialisable value as an MCP tool result. This is the single
|
|
4
|
+
* most duplicated snippet across the fleet:
|
|
5
|
+
* `{ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }`
|
|
6
|
+
*/
|
|
7
|
+
export declare function textResult(data: unknown): CallToolResult;
|
|
8
|
+
/** Alias for {@link textResult} — pretty-printed JSON tool result. */
|
|
9
|
+
export declare const jsonResult: typeof textResult;
|
|
10
|
+
/** Return a raw string as a text tool result (no JSON stringify). */
|
|
11
|
+
export declare function rawTextResult(text: string): CallToolResult;
|
|
12
|
+
/** Return a base64 image as an MCP image tool result. */
|
|
13
|
+
export declare function imageResult(base64: string, mimeType: string): CallToolResult;
|
|
14
|
+
/** Return an error tool result (`isError: true`) carrying a message. */
|
|
15
|
+
export declare function errorResult(message: string): CallToolResult;
|
|
16
|
+
/**
|
|
17
|
+
* Collapse a JSON:API-shaped payload (`{ data: { id, type, attributes } }` or an
|
|
18
|
+
* array thereof) into plain objects with `id`/`type` merged into attributes.
|
|
19
|
+
* Used by skylight-mcp; opt-in (callers pass payloads they know are JSON:API).
|
|
20
|
+
*/
|
|
21
|
+
export declare function flattenJsonApi(payload: unknown): unknown;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAIxD;AAED,sEAAsE;AACtE,eAAO,MAAM,UAAU,mBAAa,CAAC;AAErC,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAI5E;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAK3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAoBxD"}
|