@mjquinlan2000/lawmatics-mcp 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 ADDED
@@ -0,0 +1,136 @@
1
+ # @mjquinlan2000/lawmatics-mcp
2
+
3
+ MCP server for the [Lawmatics](https://www.lawmatics.com/) legal CRM API. Exposes Lawmatics' REST API as MCP tools so AI assistants (Claude Desktop, etc.) can read data from Lawmatics accounts.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @mjquinlan2000/lawmatics-mcp
9
+ # or run directly
10
+ npx @mjquinlan2000/lawmatics-mcp
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ### 1. Obtain API credentials
16
+
17
+ Register an application in Lawmatics to get a client ID and secret.
18
+
19
+ ### 2. Set environment variables
20
+
21
+ ```sh
22
+ export LM_CLIENT_ID=your_client_id
23
+ export LM_CLIENT_SECRET=your_client_secret
24
+ export LM_REDIRECT_URI=http://127.0.0.1:8081/callback
25
+ ```
26
+
27
+ ### 3. Authenticate
28
+
29
+ ```sh
30
+ # OAuth2 authorization (opens browser)
31
+ yarn auth
32
+ ```
33
+
34
+ Tokens are persisted to `~/.config/lawmatics-mcp/tokens.json`.
35
+
36
+ Alternatively, set `LM_ACCESS_TOKEN` directly to skip the OAuth flow.
37
+
38
+ > **Note:** Lawmatics does not support refresh tokens. When a token expires, run `yarn auth` again.
39
+
40
+ ## MCP Client Configuration
41
+
42
+ Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`):
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "lawmatics": {
48
+ "command": "npx",
49
+ "args": ["@mjquinlan2000/lawmatics-mcp"],
50
+ "env": {
51
+ "LM_CLIENT_ID": "your_client_id",
52
+ "LM_CLIENT_SECRET": "your_client_secret"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ For local development with `tsx`:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "lawmatics": {
65
+ "command": "tsx",
66
+ "args": ["src/server.ts"],
67
+ "env": {
68
+ "LM_CLIENT_ID": "${LM_CLIENT_ID}",
69
+ "LM_CLIENT_SECRET": "${LM_CLIENT_SECRET}"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Available Tools
77
+
78
+ The server registers 86 read-only tools covering all Lawmatics GET endpoints. Responses use JSON:API format with `{ data, meta, links }` envelopes.
79
+
80
+ | Entity | Tools |
81
+ |--------|-------|
82
+ | Prospects | `list_prospects`, `get_prospect`, `find_prospect_by_phone`, `find_prospect_by_email`, `find_prospect_by_name` |
83
+ | Contacts | `list_contacts`, `get_contact`, `find_contact_by_phone`, `find_contact_by_email`, `find_contact_by_name` |
84
+ | Companies | `list_companies`, `get_company`, `find_company_by_phone`, `find_company_by_email`, `find_company_by_name` |
85
+ | Addresses | `list_addresses`, `get_address` |
86
+ | Email Addresses | `list_email_addresses`, `get_email_address` |
87
+ | Phone Numbers | `list_phone_numbers`, `get_phone_number` |
88
+ | Custom Contact Types | `list_custom_contact_types`, `get_custom_contact_type` |
89
+ | Custom Fields | `list_custom_fields`, `get_custom_field` |
90
+ | Custom Emails | `list_custom_emails`, `get_custom_email` |
91
+ | Forms | `list_forms`, `get_form`, `list_form_entries` |
92
+ | Email Campaigns | `list_email_campaigns`, `get_email_campaign`, `get_email_campaign_stats` |
93
+ | Events | `list_events`, `get_event` |
94
+ | Locations | `list_locations` |
95
+ | Event Types | `list_event_types`, `get_event_type` |
96
+ | Files | `list_files`, `get_file`, `download_file` |
97
+ | Folders | `list_folders`, `get_folder` |
98
+ | Interactions | `list_interactions`, `get_interaction` |
99
+ | Notes | `list_notes`, `get_note` |
100
+ | Tasks | `list_tasks`, `get_task`, `list_task_subtasks`, `get_task_subtask`, `list_task_comments`, `get_task_comment` |
101
+ | Task Statuses | `list_task_statuses`, `get_task_status` |
102
+ | Campaigns | `list_campaigns`, `get_campaign` |
103
+ | Sources | `list_sources`, `get_source` |
104
+ | Pipelines | `list_pipelines`, `get_pipeline` |
105
+ | Stages | `list_stages`, `get_stage` |
106
+ | Practice Areas | `list_practice_areas`, `get_practice_area` |
107
+ | Relationships | `list_relationships`, `get_relationship` |
108
+ | Relationship Types | `list_relationship_types`, `get_relationship_type` |
109
+ | Sub-Statuses | `list_sub_statuses`, `get_sub_status` |
110
+ | Tags | `list_tags`, `get_tag` |
111
+ | Invoices | `list_invoices`, `get_invoice` |
112
+ | Expenses | `list_expenses`, `get_expense` |
113
+ | Time Entries | `list_time_entries`, `get_time_entry` |
114
+ | Transactions | `list_transactions`, `get_transaction` |
115
+ | Activities | `list_activities`, `get_activity` |
116
+ | Users | `list_users`, `get_user`, `get_me` |
117
+
118
+ ## Development
119
+
120
+ ```sh
121
+ yarn start # Run server locally (stdio transport)
122
+ yarn build # Bundle with tsup
123
+ yarn typecheck # Type-check with tsc --noEmit
124
+ yarn auth # Run OAuth2 flow
125
+ yarn generate # Sanitize spec + regenerate typed client
126
+ ```
127
+
128
+ ## Project Structure
129
+
130
+ ```
131
+ src/
132
+ ├── server.ts # MCP server entry point — registers all tools
133
+ ├── auth.ts # OAuth2 config (delegates to shared oauth utility)
134
+ ├── lm.ts # Configures generated HTTP client with base URL + auth
135
+ └── client/ # Auto-generated typed API client (do not edit)
136
+ ```
package/dist/auth.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare const getAccessToken: () => Promise<string>;
2
+ declare const runCli: (command?: string) => void;
3
+
4
+ export { getAccessToken, runCli };
package/dist/auth.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ getAccessToken,
3
+ runCli
4
+ } from "./chunk-4TYEZPJZ.js";
5
+ export {
6
+ getAccessToken,
7
+ runCli
8
+ };
9
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,233 @@
1
+ // ../shared/dist/oauth.js
2
+ import { execSync } from "child_process";
3
+ import { randomBytes } from "crypto";
4
+ import { mkdir, readFile, writeFile } from "fs/promises";
5
+ import { createServer } from "http";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ function isRefreshable(tokens) {
9
+ return "refresh_token" in tokens;
10
+ }
11
+ function createAuth(config) {
12
+ const configDir = join(homedir(), ".config", config.name);
13
+ const tokensPath = join(configDir, "tokens.json");
14
+ async function readTokens() {
15
+ try {
16
+ const data = await readFile(tokensPath, "utf-8");
17
+ return JSON.parse(data);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ async function writeTokens(tokens) {
23
+ await mkdir(configDir, { recursive: true });
24
+ await writeFile(tokensPath, JSON.stringify(tokens, null, 2), {
25
+ mode: 384
26
+ });
27
+ }
28
+ function openBrowser(url) {
29
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
30
+ execSync(`${cmd} '${url}'`);
31
+ }
32
+ async function exchangeCode(code, redirectUri, clientId, clientSecret) {
33
+ const res = await fetch(config.tokenUrl, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
36
+ body: new URLSearchParams({
37
+ grant_type: "authorization_code",
38
+ code,
39
+ redirect_uri: redirectUri,
40
+ client_id: clientId,
41
+ client_secret: clientSecret
42
+ })
43
+ });
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
47
+ }
48
+ if (config.supportsRefresh) {
49
+ const data2 = await res.json();
50
+ return {
51
+ access_token: data2.access_token,
52
+ refresh_token: data2.refresh_token,
53
+ expires_at: Date.now() + data2.expires_in * 1e3
54
+ };
55
+ }
56
+ const data = await res.json();
57
+ return { access_token: data.access_token };
58
+ }
59
+ async function refreshAccessToken(refreshToken, clientId, clientSecret) {
60
+ const res = await fetch(config.tokenUrl, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
63
+ body: new URLSearchParams({
64
+ grant_type: "refresh_token",
65
+ refresh_token: refreshToken,
66
+ client_id: clientId,
67
+ client_secret: clientSecret
68
+ })
69
+ });
70
+ if (!res.ok) {
71
+ throw new Error("Run 'yarn auth' to re-authenticate.");
72
+ }
73
+ const data = await res.json();
74
+ return {
75
+ access_token: data.access_token,
76
+ refresh_token: data.refresh_token,
77
+ expires_at: Date.now() + data.expires_in * 1e3
78
+ };
79
+ }
80
+ async function getAccessToken2() {
81
+ const envToken = process.env[`${config.envPrefix}_ACCESS_TOKEN`];
82
+ if (envToken)
83
+ return envToken;
84
+ const tokens = await readTokens();
85
+ if (!tokens) {
86
+ throw new Error(`No access token available. Set ${config.envPrefix}_ACCESS_TOKEN or run 'yarn auth'.`);
87
+ }
88
+ if (config.supportsRefresh && isRefreshable(tokens)) {
89
+ const fiveMinutes = 5 * 60 * 1e3;
90
+ if (tokens.expires_at - Date.now() < fiveMinutes) {
91
+ const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];
92
+ const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];
93
+ if (!clientId || !clientSecret) {
94
+ throw new Error(`Token is expiring and ${config.envPrefix}_CLIENT_ID/${config.envPrefix}_CLIENT_SECRET are not set for refresh. Run 'yarn auth'.`);
95
+ }
96
+ const refreshed = await refreshAccessToken(tokens.refresh_token, clientId, clientSecret);
97
+ await writeTokens(refreshed);
98
+ return refreshed.access_token;
99
+ }
100
+ }
101
+ return tokens.access_token;
102
+ }
103
+ async function authorize() {
104
+ const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];
105
+ const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];
106
+ if (!clientId)
107
+ throw new Error(`${config.envPrefix}_CLIENT_ID environment variable is required.`);
108
+ if (!clientSecret)
109
+ throw new Error(`${config.envPrefix}_CLIENT_SECRET environment variable is required.`);
110
+ const redirectUri = process.env[`${config.envPrefix}_REDIRECT_URI`];
111
+ if (!redirectUri)
112
+ throw new Error(`${config.envPrefix}_REDIRECT_URI environment variable is required.`);
113
+ const redirectUrl = new URL(redirectUri);
114
+ const port = Number(redirectUrl.port) || (redirectUrl.protocol === "https:" ? 443 : 80);
115
+ const callbackPath = redirectUrl.pathname;
116
+ const state = randomBytes(16).toString("hex");
117
+ const { tokens } = await new Promise((resolve, reject) => {
118
+ const timeout = setTimeout(() => {
119
+ server.close();
120
+ reject(new Error("Authorization timed out after 120 seconds."));
121
+ }, 12e4);
122
+ const server = createServer(async (req, res) => {
123
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
124
+ if (url.pathname !== callbackPath) {
125
+ res.writeHead(404);
126
+ res.end("Not found");
127
+ return;
128
+ }
129
+ const returnedState = url.searchParams.get("state");
130
+ const code = url.searchParams.get("code");
131
+ const error = url.searchParams.get("error");
132
+ if (error) {
133
+ res.writeHead(400);
134
+ res.end(`Authorization error: ${error}`);
135
+ clearTimeout(timeout);
136
+ server.close();
137
+ reject(new Error(`Authorization denied: ${error}`));
138
+ return;
139
+ }
140
+ if (returnedState !== state) {
141
+ res.writeHead(400);
142
+ res.end("State mismatch \u2014 possible CSRF attack.");
143
+ return;
144
+ }
145
+ if (!code) {
146
+ res.writeHead(400);
147
+ res.end("Missing authorization code.");
148
+ return;
149
+ }
150
+ try {
151
+ const exchangedTokens = await exchangeCode(code, redirectUri, clientId, clientSecret);
152
+ res.writeHead(200, { "Content-Type": "text/html" });
153
+ res.end("<h1>Authorization successful!</h1><p>You can close this tab.</p>");
154
+ clearTimeout(timeout);
155
+ server.close();
156
+ resolve({ tokens: exchangedTokens });
157
+ } catch (err) {
158
+ res.writeHead(500);
159
+ res.end("Token exchange failed.");
160
+ clearTimeout(timeout);
161
+ server.close();
162
+ reject(err);
163
+ }
164
+ });
165
+ const params = {
166
+ response_type: "code",
167
+ client_id: clientId,
168
+ redirect_uri: redirectUri,
169
+ state
170
+ };
171
+ if (config.scope) {
172
+ params.scope = config.scope;
173
+ }
174
+ server.listen(port, "127.0.0.1", () => {
175
+ const authorizeUrl = `${config.authorizeUrl}?${new URLSearchParams(params)}`;
176
+ console.log("Opening browser for authorization...");
177
+ console.log(`If the browser doesn't open, visit:
178
+ ${authorizeUrl}
179
+ `);
180
+ openBrowser(authorizeUrl);
181
+ });
182
+ });
183
+ await writeTokens(tokens);
184
+ console.log(`Tokens saved to ${tokensPath}`);
185
+ }
186
+ async function refresh() {
187
+ const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];
188
+ const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];
189
+ if (!clientId)
190
+ throw new Error(`${config.envPrefix}_CLIENT_ID environment variable is required.`);
191
+ if (!clientSecret)
192
+ throw new Error(`${config.envPrefix}_CLIENT_SECRET environment variable is required.`);
193
+ const tokens = await readTokens();
194
+ if (!tokens || !isRefreshable(tokens)) {
195
+ throw new Error("No tokens found. Run 'yarn auth' first to authenticate.");
196
+ }
197
+ const refreshed = await refreshAccessToken(tokens.refresh_token, clientId, clientSecret);
198
+ await writeTokens(refreshed);
199
+ console.log(`Tokens refreshed and saved to ${tokensPath}`);
200
+ }
201
+ function runCli2(command) {
202
+ const cmd = command ?? process.argv[2];
203
+ let run;
204
+ if (cmd === "refresh" && config.supportsRefresh) {
205
+ run = refresh;
206
+ } else {
207
+ run = authorize;
208
+ }
209
+ run().catch((err) => {
210
+ console.error(err.message ?? err);
211
+ process.exit(1);
212
+ });
213
+ }
214
+ return { getAccessToken: getAccessToken2, runCli: runCli2 };
215
+ }
216
+
217
+ // src/auth.ts
218
+ var { getAccessToken, runCli } = createAuth({
219
+ name: "lawmatics-mcp",
220
+ authorizeUrl: "https://app.lawmatics.com/oauth/authorize",
221
+ tokenUrl: "https://api.lawmatics.com/oauth/token",
222
+ envPrefix: "LM",
223
+ supportsRefresh: false
224
+ });
225
+ if (process.argv[1] && import.meta.filename === process.argv[1]) {
226
+ runCli();
227
+ }
228
+
229
+ export {
230
+ getAccessToken,
231
+ runCli
232
+ };
233
+ //# sourceMappingURL=chunk-4TYEZPJZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../shared/src/oauth.ts","../src/auth.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport {\n createServer,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface OAuthConfig {\n name: string;\n authorizeUrl: string;\n tokenUrl: string;\n envPrefix: string;\n scope?: string;\n supportsRefresh: boolean;\n}\n\ninterface BaseTokens {\n access_token: string;\n}\n\ninterface RefreshableTokens extends BaseTokens {\n refresh_token: string;\n expires_at: number;\n}\n\ntype StoredTokens = BaseTokens | RefreshableTokens;\n\nfunction isRefreshable(tokens: StoredTokens): tokens is RefreshableTokens {\n return \"refresh_token\" in tokens;\n}\n\ninterface RefreshableTokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n}\n\nexport function createAuth(config: OAuthConfig): {\n getAccessToken: () => Promise<string>;\n runCli: (command?: string) => void;\n} {\n const configDir = join(homedir(), \".config\", config.name);\n const tokensPath = join(configDir, \"tokens.json\");\n\n async function readTokens(): Promise<StoredTokens | null> {\n try {\n const data = await readFile(tokensPath, \"utf-8\");\n return JSON.parse(data) as StoredTokens;\n } catch {\n return null;\n }\n }\n\n async function writeTokens(tokens: StoredTokens): Promise<void> {\n await mkdir(configDir, { recursive: true });\n await writeFile(tokensPath, JSON.stringify(tokens, null, 2), {\n mode: 0o600,\n });\n }\n\n function openBrowser(url: string): void {\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n execSync(`${cmd} '${url}'`);\n }\n\n async function exchangeCode(\n code: string,\n redirectUri: string,\n clientId: string,\n clientSecret: string,\n ): Promise<StoredTokens> {\n const res = await fetch(config.tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Token exchange failed (${res.status}): ${text}`);\n }\n if (config.supportsRefresh) {\n const data = (await res.json()) as RefreshableTokenResponse;\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token,\n expires_at: Date.now() + data.expires_in * 1000,\n };\n }\n const data = (await res.json()) as { access_token: string };\n return { access_token: data.access_token };\n }\n\n async function refreshAccessToken(\n refreshToken: string,\n clientId: string,\n clientSecret: string,\n ): Promise<RefreshableTokens> {\n const res = await fetch(config.tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!res.ok) {\n throw new Error(\"Run 'yarn auth' to re-authenticate.\");\n }\n const data = (await res.json()) as RefreshableTokenResponse;\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token,\n expires_at: Date.now() + data.expires_in * 1000,\n };\n }\n\n async function getAccessToken(): Promise<string> {\n const envToken = process.env[`${config.envPrefix}_ACCESS_TOKEN`];\n if (envToken) return envToken;\n\n const tokens = await readTokens();\n if (!tokens) {\n throw new Error(\n `No access token available. Set ${config.envPrefix}_ACCESS_TOKEN or run 'yarn auth'.`,\n );\n }\n\n if (config.supportsRefresh && isRefreshable(tokens)) {\n const fiveMinutes = 5 * 60 * 1000;\n if (tokens.expires_at - Date.now() < fiveMinutes) {\n const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];\n const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];\n if (!clientId || !clientSecret) {\n throw new Error(\n `Token is expiring and ${config.envPrefix}_CLIENT_ID/${config.envPrefix}_CLIENT_SECRET are not set for refresh. Run 'yarn auth'.`,\n );\n }\n const refreshed = await refreshAccessToken(\n tokens.refresh_token,\n clientId,\n clientSecret,\n );\n await writeTokens(refreshed);\n return refreshed.access_token;\n }\n }\n\n return tokens.access_token;\n }\n\n async function authorize(): Promise<void> {\n const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];\n const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];\n if (!clientId)\n throw new Error(\n `${config.envPrefix}_CLIENT_ID environment variable is required.`,\n );\n if (!clientSecret)\n throw new Error(\n `${config.envPrefix}_CLIENT_SECRET environment variable is required.`,\n );\n\n const redirectUri = process.env[`${config.envPrefix}_REDIRECT_URI`];\n if (!redirectUri)\n throw new Error(\n `${config.envPrefix}_REDIRECT_URI environment variable is required.`,\n );\n\n const redirectUrl = new URL(redirectUri);\n const port =\n Number(redirectUrl.port) ||\n (redirectUrl.protocol === \"https:\" ? 443 : 80);\n const callbackPath = redirectUrl.pathname;\n const state = randomBytes(16).toString(\"hex\");\n\n const { tokens } = await new Promise<{\n tokens: StoredTokens;\n }>((resolve, reject) => {\n const timeout = setTimeout(() => {\n server.close();\n reject(new Error(\"Authorization timed out after 120 seconds.\"));\n }, 120_000);\n\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n if (url.pathname !== callbackPath) {\n res.writeHead(404);\n res.end(\"Not found\");\n return;\n }\n\n const returnedState = url.searchParams.get(\"state\");\n const code = url.searchParams.get(\"code\");\n const error = url.searchParams.get(\"error\");\n\n if (error) {\n res.writeHead(400);\n res.end(`Authorization error: ${error}`);\n clearTimeout(timeout);\n server.close();\n reject(new Error(`Authorization denied: ${error}`));\n return;\n }\n\n if (returnedState !== state) {\n res.writeHead(400);\n res.end(\"State mismatch — possible CSRF attack.\");\n return;\n }\n\n if (!code) {\n res.writeHead(400);\n res.end(\"Missing authorization code.\");\n return;\n }\n\n try {\n const exchangedTokens = await exchangeCode(\n code,\n redirectUri,\n clientId,\n clientSecret,\n );\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(\n \"<h1>Authorization successful!</h1><p>You can close this tab.</p>\",\n );\n clearTimeout(timeout);\n server.close();\n resolve({ tokens: exchangedTokens });\n } catch (err) {\n res.writeHead(500);\n res.end(\"Token exchange failed.\");\n clearTimeout(timeout);\n server.close();\n reject(err);\n }\n },\n );\n\n const params: Record<string, string> = {\n response_type: \"code\",\n client_id: clientId,\n redirect_uri: redirectUri,\n state,\n };\n if (config.scope) {\n params.scope = config.scope;\n }\n\n server.listen(port, \"127.0.0.1\", () => {\n const authorizeUrl = `${config.authorizeUrl}?${new URLSearchParams(params)}`;\n\n console.log(\"Opening browser for authorization...\");\n console.log(`If the browser doesn't open, visit:\\n${authorizeUrl}\\n`);\n openBrowser(authorizeUrl);\n });\n });\n\n await writeTokens(tokens);\n console.log(`Tokens saved to ${tokensPath}`);\n }\n\n async function refresh(): Promise<void> {\n const clientId = process.env[`${config.envPrefix}_CLIENT_ID`];\n const clientSecret = process.env[`${config.envPrefix}_CLIENT_SECRET`];\n if (!clientId)\n throw new Error(\n `${config.envPrefix}_CLIENT_ID environment variable is required.`,\n );\n if (!clientSecret)\n throw new Error(\n `${config.envPrefix}_CLIENT_SECRET environment variable is required.`,\n );\n\n const tokens = await readTokens();\n if (!tokens || !isRefreshable(tokens)) {\n throw new Error(\n \"No tokens found. Run 'yarn auth' first to authenticate.\",\n );\n }\n\n const refreshed = await refreshAccessToken(\n tokens.refresh_token,\n clientId,\n clientSecret,\n );\n await writeTokens(refreshed);\n console.log(`Tokens refreshed and saved to ${tokensPath}`);\n }\n\n function runCli(command?: string): void {\n const cmd = command ?? process.argv[2];\n let run: () => Promise<void>;\n if (cmd === \"refresh\" && config.supportsRefresh) {\n run = refresh;\n } else {\n run = authorize;\n }\n run().catch((err) => {\n console.error(err.message ?? err);\n process.exit(1);\n });\n }\n\n return { getAccessToken, runCli };\n}\n","import { createAuth } from \"@mjquinlan2000/shared/oauth.js\";\n\nconst { getAccessToken, runCli } = createAuth({\n name: \"lawmatics-mcp\",\n authorizeUrl: \"https://app.lawmatics.com/oauth/authorize\",\n tokenUrl: \"https://api.lawmatics.com/oauth/token\",\n envPrefix: \"LM\",\n supportsRefresh: false,\n});\n\nexport { getAccessToken, runCli };\n\nif (process.argv[1] && import.meta.filename === process.argv[1]) {\n runCli();\n}\n"],"mappings":"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","names":["data","getAccessToken","runCli"]}