@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,61 @@
1
+ /**
2
+ * Wrap any JSON-serialisable value as an MCP tool result. This is the single
3
+ * most duplicated snippet across the fleet:
4
+ * `{ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }`
5
+ */
6
+ export function textResult(data) {
7
+ return {
8
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
9
+ };
10
+ }
11
+ /** Alias for {@link textResult} — pretty-printed JSON tool result. */
12
+ export const jsonResult = textResult;
13
+ /** Return a raw string as a text tool result (no JSON stringify). */
14
+ export function rawTextResult(text) {
15
+ return { content: [{ type: 'text', text }] };
16
+ }
17
+ /** Return a base64 image as an MCP image tool result. */
18
+ export function imageResult(base64, mimeType) {
19
+ return {
20
+ content: [{ type: 'image', data: base64, mimeType }],
21
+ };
22
+ }
23
+ /** Return an error tool result (`isError: true`) carrying a message. */
24
+ export function errorResult(message) {
25
+ return {
26
+ content: [{ type: 'text', text: message }],
27
+ isError: true,
28
+ };
29
+ }
30
+ /**
31
+ * Collapse a JSON:API-shaped payload (`{ data: { id, type, attributes } }` or an
32
+ * array thereof) into plain objects with `id`/`type` merged into attributes.
33
+ * Used by skylight-mcp; opt-in (callers pass payloads they know are JSON:API).
34
+ */
35
+ export function flattenJsonApi(payload) {
36
+ const flattenOne = (node) => {
37
+ if (node === null || typeof node !== 'object')
38
+ return node;
39
+ const obj = node;
40
+ const attrs = obj.attributes;
41
+ if (attrs !== null && typeof attrs === 'object') {
42
+ const merged = { ...attrs };
43
+ if ('id' in obj)
44
+ merged.id = obj.id;
45
+ if ('type' in obj)
46
+ merged.type = obj.type;
47
+ return merged;
48
+ }
49
+ return node;
50
+ };
51
+ if (payload === null || typeof payload !== 'object')
52
+ return payload;
53
+ const root = payload;
54
+ if (!('data' in root))
55
+ return payload;
56
+ const data = root.data;
57
+ if (Array.isArray(data))
58
+ return data.map(flattenOne);
59
+ return flattenOne(data);
60
+ }
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;KACjE,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;AAErC,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,QAAgB;IAC1D,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,MAAM,UAAU,GAAG,CAAC,IAAa,EAAW,EAAE;QAC5C,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3D,MAAM,GAAG,GAAG,IAA+B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC;QAC7B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAChD,MAAM,MAAM,GAA4B,EAAE,GAAI,KAAiC,EAAE,CAAC;YAClF,IAAI,IAAI,IAAI,GAAG;gBAAE,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;YACpC,IAAI,MAAM,IAAI,GAAG;gBAAE,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;YAC1C,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IACpE,MAAM,IAAI,GAAG,OAAkC,CAAC;IAChD,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACrD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `server` — MCP bootstrap & lifecycle.
3
+ *
4
+ * The single biggest dedup win in the fleet: every `src/index.ts` is described
5
+ * as "byte-identical" — construct an {@link McpServer}, run a fixed list of
6
+ * `registerXTools(server, client)` calls, print a stderr banner, wire
7
+ * SIGINT/SIGTERM to tear the transport down, then connect over stdio.
8
+ *
9
+ * This module collapses that 30–120 lines/MCP into three calls:
10
+ * - {@link createMcpServer} — build the server and apply the registrars.
11
+ * - {@link withGracefulShutdown} — SIGINT/SIGTERM → cleanup → exit.
12
+ * - {@link runMcp} — bootstrap + banner + connect + shutdown, the whole boot.
13
+ *
14
+ * It is deliberately transport- and domain-agnostic. The
15
+ * deferred-config-error pattern (server boots before creds exist, so the host's
16
+ * initial `tools/list` always succeeds and the first tool call surfaces the auth
17
+ * error) is preserved by keeping client/transport construction in the caller's
18
+ * `deps`: both Pattern-A (fetchproxy bridge) and Pattern-B (direct/bearer) MCPs
19
+ * build their client themselves and pass it through, so neither is coupled in.
20
+ */
21
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
23
+ /**
24
+ * Registers one or more tools onto a fresh {@link McpServer}. `deps` is whatever
25
+ * the caller threaded through {@link createMcpServer} / {@link runMcp} — an API
26
+ * client, a session registry, an app context, or `undefined` for tools (like
27
+ * pure mortgage/affordability calculators) that need no shared state.
28
+ *
29
+ * May be async; {@link createMcpServer} awaits each registrar in order.
30
+ */
31
+ export type ToolRegistrar<TDeps = unknown> = (server: McpServer, deps: TDeps) => void | Promise<void>;
32
+ /** Either the literal `'stdio'` (the default) or any SDK {@link Transport}. */
33
+ export type TransportSpec = 'stdio' | Transport;
34
+ /** Options for {@link createMcpServer}. */
35
+ export interface CreateMcpServerOptions<TDeps = unknown> {
36
+ /** Server name advertised to the host (e.g. `'splitwise-mcp'`). */
37
+ name: string;
38
+ /** Server version advertised to the host (the `x-release-please-version`). */
39
+ version: string;
40
+ /** The tool registrars to apply, in order. */
41
+ tools: ToolRegistrar<TDeps>[];
42
+ /**
43
+ * Shared state passed as the second argument to every registrar — the API
44
+ * client, app context, session registry, etc. Build it before calling so the
45
+ * deferred-config-error pattern is preserved. Omit for registrar lists that
46
+ * take no deps.
47
+ */
48
+ deps?: TDeps;
49
+ /** A one-line startup banner written to stderr (never stdout — stdout is the JSON-RPC channel). */
50
+ banner?: string;
51
+ /**
52
+ * Transport hint. Carried for API symmetry with {@link runMcp}; this function
53
+ * never connects, so it only matters that the value is accepted. Defaults to
54
+ * `'stdio'`.
55
+ */
56
+ transport?: TransportSpec;
57
+ }
58
+ /**
59
+ * Build an {@link McpServer}, print the optional stderr banner, and apply every
60
+ * tool registrar (awaiting async ones) — but do **not** connect a transport.
61
+ * Connecting is {@link runMcp}'s job (or the caller's), which keeps this usable
62
+ * from tests and from custom boot sequences.
63
+ */
64
+ export declare function createMcpServer<TDeps = unknown>(opts: CreateMcpServerOptions<TDeps>): Promise<McpServer>;
65
+ /** Signals {@link withGracefulShutdown} listens for. */
66
+ export type ShutdownSignal = 'SIGINT' | 'SIGTERM';
67
+ /** Options for {@link withGracefulShutdown}. */
68
+ export interface GracefulShutdownOptions {
69
+ /**
70
+ * Extra cleanup to run on shutdown, before the server is closed — typically
71
+ * `() => client.close()` to release the fetchproxy WebSocket bridge / direct
72
+ * sockets so ports don't leak between host restarts. Receives the signal that
73
+ * triggered shutdown. Errors are logged, never fatal.
74
+ */
75
+ onSignal?: (signal: ShutdownSignal) => void | Promise<void>;
76
+ /**
77
+ * Call `process.exit(0)` after cleanup completes. Default `true` (matches the
78
+ * fleet's `process.exit(0)`). Set `false` in tests so the process survives.
79
+ */
80
+ exit?: boolean;
81
+ }
82
+ /**
83
+ * Wire SIGINT/SIGTERM to a one-shot graceful shutdown: run `onSignal` (e.g.
84
+ * close the client/transport), close the server, then `process.exit(0)` (unless
85
+ * `exit: false`). Idempotent — a second signal mid-shutdown is ignored, and a
86
+ * throwing `onSignal`/`close` is logged but still exits cleanly so a wedged
87
+ * cleanup can't hang the host.
88
+ */
89
+ export declare function withGracefulShutdown(server: Pick<McpServer, 'close'>, opts?: GracefulShutdownOptions): void;
90
+ /** Options for {@link runMcp} — {@link createMcpServer}'s plus lifecycle wiring. */
91
+ export interface RunMcpOptions<TDeps = unknown> extends CreateMcpServerOptions<TDeps> {
92
+ /**
93
+ * Graceful-shutdown wiring. `true` (default) installs SIGINT/SIGTERM handlers
94
+ * that close the server. `false` skips them. An object is passed straight to
95
+ * {@link withGracefulShutdown} (e.g. `{ onSignal: () => client.close() }`).
96
+ */
97
+ shutdown?: boolean | GracefulShutdownOptions;
98
+ }
99
+ /**
100
+ * The whole boot in one call: build the server, apply registrars, print the
101
+ * banner, install graceful-shutdown handlers, and connect the transport
102
+ * (defaulting to a {@link StdioServerTransport}). Returns the connected server.
103
+ *
104
+ * Pattern-A and Pattern-B MCPs both build their client/transport in `deps` and
105
+ * pass `onSignal: () => client.close()` via `shutdown`, so this stays agnostic
106
+ * to how creds are resolved.
107
+ */
108
+ export declare function runMcp<TDeps = unknown>(opts: RunMcpOptions<TDeps>): Promise<McpServer>;
109
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,GAAG,OAAO,IAAI,CAC3C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,KAAK,KACR,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAEhD,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB,CAAC,KAAK,GAAG,OAAO;IACrD,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,GAAG,OAAO,EACnD,IAAI,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAClC,OAAO,CAAC,SAAS,CAAC,CAgBpB;AAED,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElD,gDAAgD;AAChD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAChC,IAAI,GAAE,uBAA4B,GACjC,IAAI,CAyBN;AAED,oFAAoF;AACpF,MAAM,WAAW,aAAa,CAAC,KAAK,GAAG,OAAO,CAAE,SAAQ,sBAAsB,CAAC,KAAK,CAAC;IACnF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAC;CAC9C;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,KAAK,GAAG,OAAO,EAC1C,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,GACzB,OAAO,CAAC,SAAS,CAAC,CAapB"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `server` — MCP bootstrap & lifecycle.
3
+ *
4
+ * The single biggest dedup win in the fleet: every `src/index.ts` is described
5
+ * as "byte-identical" — construct an {@link McpServer}, run a fixed list of
6
+ * `registerXTools(server, client)` calls, print a stderr banner, wire
7
+ * SIGINT/SIGTERM to tear the transport down, then connect over stdio.
8
+ *
9
+ * This module collapses that 30–120 lines/MCP into three calls:
10
+ * - {@link createMcpServer} — build the server and apply the registrars.
11
+ * - {@link withGracefulShutdown} — SIGINT/SIGTERM → cleanup → exit.
12
+ * - {@link runMcp} — bootstrap + banner + connect + shutdown, the whole boot.
13
+ *
14
+ * It is deliberately transport- and domain-agnostic. The
15
+ * deferred-config-error pattern (server boots before creds exist, so the host's
16
+ * initial `tools/list` always succeeds and the first tool call surfaces the auth
17
+ * error) is preserved by keeping client/transport construction in the caller's
18
+ * `deps`: both Pattern-A (fetchproxy bridge) and Pattern-B (direct/bearer) MCPs
19
+ * build their client themselves and pass it through, so neither is coupled in.
20
+ */
21
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
+ /**
24
+ * Build an {@link McpServer}, print the optional stderr banner, and apply every
25
+ * tool registrar (awaiting async ones) — but do **not** connect a transport.
26
+ * Connecting is {@link runMcp}'s job (or the caller's), which keeps this usable
27
+ * from tests and from custom boot sequences.
28
+ */
29
+ export async function createMcpServer(opts) {
30
+ const server = new McpServer({ name: opts.name, version: opts.version });
31
+ if (opts.banner !== undefined) {
32
+ // stderr only: stdout carries the JSON-RPC frames over stdio transport.
33
+ console.error(opts.banner);
34
+ }
35
+ // `deps` is intentionally cast: when omitted, registrars that declare a deps
36
+ // type are the caller's responsibility (they passed deps if they need it).
37
+ const deps = opts.deps;
38
+ for (const register of opts.tools) {
39
+ await register(server, deps);
40
+ }
41
+ return server;
42
+ }
43
+ /**
44
+ * Wire SIGINT/SIGTERM to a one-shot graceful shutdown: run `onSignal` (e.g.
45
+ * close the client/transport), close the server, then `process.exit(0)` (unless
46
+ * `exit: false`). Idempotent — a second signal mid-shutdown is ignored, and a
47
+ * throwing `onSignal`/`close` is logged but still exits cleanly so a wedged
48
+ * cleanup can't hang the host.
49
+ */
50
+ export function withGracefulShutdown(server, opts = {}) {
51
+ const shouldExit = opts.exit ?? true;
52
+ let shuttingDown = false;
53
+ const handler = (signal) => {
54
+ if (shuttingDown)
55
+ return;
56
+ shuttingDown = true;
57
+ void (async () => {
58
+ try {
59
+ if (opts.onSignal)
60
+ await opts.onSignal(signal);
61
+ await server.close();
62
+ }
63
+ catch (err) {
64
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
65
+ }
66
+ finally {
67
+ if (shouldExit)
68
+ process.exit(0);
69
+ }
70
+ })();
71
+ };
72
+ process.on('SIGINT', () => handler('SIGINT'));
73
+ process.on('SIGTERM', () => handler('SIGTERM'));
74
+ }
75
+ /**
76
+ * The whole boot in one call: build the server, apply registrars, print the
77
+ * banner, install graceful-shutdown handlers, and connect the transport
78
+ * (defaulting to a {@link StdioServerTransport}). Returns the connected server.
79
+ *
80
+ * Pattern-A and Pattern-B MCPs both build their client/transport in `deps` and
81
+ * pass `onSignal: () => client.close()` via `shutdown`, so this stays agnostic
82
+ * to how creds are resolved.
83
+ */
84
+ export async function runMcp(opts) {
85
+ const server = await createMcpServer(opts);
86
+ const shutdown = opts.shutdown ?? true;
87
+ if (shutdown !== false) {
88
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
89
+ }
90
+ const spec = opts.transport ?? 'stdio';
91
+ const transport = spec === 'stdio' ? new StdioServerTransport() : spec;
92
+ await server.connect(transport);
93
+ return server;
94
+ }
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Session scaffolding for the MCP fleet — three related-but-distinct surfaces
3
+ * consolidated behind one subpath (`@chrischall/mcp-utils/session`):
4
+ *
5
+ * 1. {@link SessionRegistry} — an *ephemeral, in-memory* registry of signed-in
6
+ * sessions keyed by account identity, plus {@link registerSessionTools} for
7
+ * the structurally-identical `*_register_session` / `*_set_active_session` /
8
+ * `*_get_session_context` MCP tool trio. Used by the realty MCPs
9
+ * (zillow/redfin/compass/homes/onehome).
10
+ *
11
+ * 2. {@link SessionStore} — a *disk-persisted* store with hardened file perms
12
+ * (0600 file / 0700 dir), normalized keys, and a most-recently-used "active"
13
+ * pointer. Used by ofw/creditkarma/honeybook.
14
+ *
15
+ * 3. {@link TokenManager} — a bearer-token lifecycle manager: proactive refresh
16
+ * inside a 5-minute skew window, reactive 401-replay, and a single-flight
17
+ * semaphore so concurrent callers coalesce into ONE refresh. Used by
18
+ * skylight/canvas/creditkarma/honeybook/zola.
19
+ *
20
+ * Security-sensitive by design (file perms + token-refresh races), so this is
21
+ * the one audited implementation the fleet shares.
22
+ */
23
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
24
+ /** How a session was authenticated. Open-ended so per-MCP modes slot in. */
25
+ export type AuthMode = 'browser_session' | 'unknown' | (string & {});
26
+ /** A registered, signed-in session as surfaced to tools and clients. */
27
+ export interface SessionToken {
28
+ /** Opaque label id (`Date.now().toString(36)` + random). Stable across re-registration. */
29
+ session_id: string;
30
+ /**
31
+ * Caller-supplied identity for the signed-in account (usually the saved
32
+ * email). Re-registering the same identity updates the existing entry in
33
+ * place rather than creating a duplicate.
34
+ */
35
+ account_identity: string;
36
+ auth_mode: AuthMode;
37
+ /** Whether the session is currently usable for tool calls. */
38
+ auth_ready: boolean;
39
+ /** ISO timestamp of when the session was last registered/refreshed. */
40
+ registered_at: string;
41
+ /** ISO expiry, or `null` for "no known expiry". */
42
+ auth_expires_at: string | null;
43
+ }
44
+ /** Snapshot returned by {@link SessionRegistry.getContext}. */
45
+ export interface SessionContext {
46
+ active_session_id: string | null;
47
+ sessions: SessionToken[];
48
+ }
49
+ /** Arguments to {@link SessionRegistry.register}. */
50
+ export interface RegisterArgs {
51
+ account_identity: string;
52
+ auth_mode?: AuthMode;
53
+ /**
54
+ * `undefined` keeps any existing expiry on re-registration; an explicit
55
+ * value (including `null`) replaces it. This distinction matters — coercing
56
+ * with `?? null` would silently wipe a previously-set expiry.
57
+ */
58
+ auth_expires_at?: string | null;
59
+ }
60
+ /**
61
+ * Per-process, in-memory registry of signed-in sessions. The constructor takes
62
+ * no arguments, so it's safe to instantiate per test. Sessions are keyed by
63
+ * `session_id` but de-duplicated by `account_identity`.
64
+ */
65
+ export declare class SessionRegistry {
66
+ private readonly sessions;
67
+ private activeId;
68
+ /**
69
+ * Register a new session, or refresh the existing one keyed by
70
+ * `account_identity`. The first session registered becomes the active one.
71
+ */
72
+ register(args: RegisterArgs): SessionToken;
73
+ /** Switch the active session. Returns `false` if the id is unknown. */
74
+ setActive(sessionId: string): boolean;
75
+ /** Look up a session by id (returns a copy, or `null`). */
76
+ get(sessionId: string): SessionToken | null;
77
+ /** Snapshot of the full registry plus the active id. */
78
+ getContext(): SessionContext;
79
+ /** The active session id, if any. */
80
+ activeSessionId(): string | null;
81
+ /** Number of registered sessions. */
82
+ size(): number;
83
+ /**
84
+ * Resolve which session a tool call should route through:
85
+ * - `requested` set and known → it;
86
+ * - `requested` set but unknown → throws;
87
+ * - `requested` undefined → the active session;
88
+ * - no sessions registered → `null` (caller uses the default transport).
89
+ */
90
+ resolve(requested: string | undefined): string | null;
91
+ /** Clear all state. Test helper. */
92
+ reset(): void;
93
+ }
94
+ /** Construct a fresh in-memory {@link SessionRegistry}. */
95
+ export declare function createSessionRegistry(): SessionRegistry;
96
+ /** Options for {@link registerSessionTools}. */
97
+ export interface RegisterSessionToolsOptions {
98
+ /**
99
+ * Tool-name prefix, e.g. `'zillow'` → `zillow_register_session`. This is the
100
+ * one per-MCP knob; everything else is identical across the fleet.
101
+ */
102
+ prefix: string;
103
+ /** Human label for the service (defaults to the prefix). */
104
+ serviceLabel?: string;
105
+ }
106
+ /**
107
+ * Register the structurally-identical session tool trio against `server`,
108
+ * backed by `registry`. Replaces every MCP's hand-rolled `src/tools/sessions.ts`.
109
+ */
110
+ export declare function registerSessionTools(server: McpServer, registry: SessionRegistry, opts: RegisterSessionToolsOptions): void;
111
+ /**
112
+ * Given a full URL or just an origin, return the origin without a trailing
113
+ * slash. Falls back to trimming a trailing slash for non-URL input.
114
+ */
115
+ export declare function normalizeOrigin(input: string): string;
116
+ /** Options for {@link SessionStore}. `T` is the persisted record shape. */
117
+ export interface SessionStoreOptions<T> {
118
+ /** Absolute path to the JSON file. Parent dirs are created as needed. */
119
+ filePath: string;
120
+ /** Extract the unique key (e.g. origin / account id) from a record. */
121
+ keyOf: (session: T) => string;
122
+ /**
123
+ * Normalize a key before storing/looking up. Defaults to
124
+ * {@link normalizeOrigin}. The stored record's key field is also normalized.
125
+ */
126
+ normalizeKey?: (key: string) => string;
127
+ }
128
+ /**
129
+ * Disk-persisted session store with hardened file permissions. Records are kept
130
+ * in a `Map` keyed by their normalized key, persisted as a JSON array (insertion
131
+ * order). The file is written with mode `0600` and its directory `0700` so other
132
+ * users on the machine cannot read captured credentials.
133
+ *
134
+ * `add` marks the record most-recently-used; {@link SessionStore.getActiveSession}
135
+ * (and `get()` with no argument) returns it, so a tool call that omits an explicit
136
+ * key picks up the latest session automatically.
137
+ */
138
+ export declare class SessionStore<T extends Record<string, unknown>> {
139
+ private sessions;
140
+ private mostRecentKey;
141
+ private readonly filePath;
142
+ private readonly keyOf;
143
+ private readonly normalizeKey;
144
+ constructor(opts: SessionStoreOptions<T>);
145
+ private loadFromDisk;
146
+ /** Serialize the store to its on-disk JSON form (array, insertion order). */
147
+ serialize(): string;
148
+ /** Parse on-disk JSON back into a keyed `Map`; empty map on invalid input. */
149
+ private deserialize;
150
+ private saveToDisk;
151
+ /** Insert or replace a record, normalizing its key and marking it active. */
152
+ add(session: T): void;
153
+ /** Look up by key; with no key, returns the active (most-recent) session. */
154
+ get(key?: string): T | null;
155
+ /** The most-recently-added session, or `null`. */
156
+ getActiveSession(): T | null;
157
+ /** All sessions in insertion order. */
158
+ list(): T[];
159
+ /** Remove a session; fixes up the active pointer. Returns whether it existed. */
160
+ remove(key: string): boolean;
161
+ /** Clear in-memory state without touching disk. Test helper. */
162
+ resetForTest(): void;
163
+ }
164
+ /** Refresh proactively this many ms before the access token expires. */
165
+ export declare const TOKEN_REFRESH_SKEW_MS: number;
166
+ /** A bearer access token + (optional) refresh token + absolute expiry. */
167
+ export interface BearerTokens {
168
+ accessToken: string;
169
+ /** Refresh token, if the flow uses one. */
170
+ refreshToken?: string;
171
+ /** Absolute expiry in epoch milliseconds. */
172
+ expiresAt: number;
173
+ }
174
+ /** Result a {@link TokenManagerOptions.refresh} call must return. */
175
+ export interface RefreshedTokens {
176
+ accessToken: string;
177
+ /** Omit to keep the current refresh token (rotation is optional). */
178
+ refreshToken?: string;
179
+ expiresAt: number;
180
+ }
181
+ /** Options for {@link TokenManager}. */
182
+ export interface TokenManagerOptions {
183
+ /** Initial tokens (typically from env or a one-shot bootstrap). */
184
+ initial: BearerTokens;
185
+ /**
186
+ * Exchange the current refresh token for fresh tokens. Called at most once
187
+ * per concurrent burst (the in-flight promise is shared).
188
+ */
189
+ refresh: (refreshToken: string) => Promise<RefreshedTokens>;
190
+ /**
191
+ * Override the skew window (ms before expiry that triggers a proactive
192
+ * refresh). Defaults to {@link TOKEN_REFRESH_SKEW_MS} (5 minutes).
193
+ */
194
+ skewMs?: number;
195
+ }
196
+ /**
197
+ * Manages a bearer access token's lifecycle:
198
+ *
199
+ * - **Proactive:** {@link TokenManager.getAccessToken} refreshes when the token
200
+ * is within `skewMs` (default 5 min) of expiry, returning a still-valid token.
201
+ * - **Reactive:** {@link TokenManager.withAuth} runs a request, and on a `401`
202
+ * refreshes once and replays exactly once (no infinite loop).
203
+ * - **Race-safe:** concurrent refreshes coalesce onto a single in-flight promise
204
+ * (semaphore), so a burst of callers triggers exactly ONE token exchange. The
205
+ * in-flight promise is cleared on settle so a later refresh can run again.
206
+ */
207
+ export declare class TokenManager {
208
+ private accessToken;
209
+ private refreshToken;
210
+ private expiresAt;
211
+ private readonly refreshFn;
212
+ private readonly skewMs;
213
+ private inFlight;
214
+ constructor(opts: TokenManagerOptions);
215
+ /** Whether the token is within the skew window of (or past) expiry. */
216
+ private needsRefresh;
217
+ /**
218
+ * Single-flight refresh. Concurrent callers share one in-flight promise; it is
219
+ * cleared on settle (success or failure) so a subsequent refresh can proceed.
220
+ */
221
+ refreshNow(): Promise<void>;
222
+ /** Get a valid access token, refreshing proactively inside the skew window. */
223
+ getAccessToken(): Promise<string>;
224
+ /** Current absolute expiry (epoch ms). */
225
+ getExpiresAt(): number;
226
+ /**
227
+ * Run an authenticated request with reactive 401-replay. `call` receives a
228
+ * valid access token and returns a `Response`. On `401`, the token is
229
+ * refreshed once and `call` is invoked again exactly once.
230
+ */
231
+ withAuth(call: (accessToken: string) => Promise<Response>): Promise<Response>;
232
+ }
233
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}