contractor-license-mcp-server 0.6.5 → 0.8.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # contractor-license-mcp-server
2
2
 
3
- **Real-time contractor license verification across 45 US states.** An [MCP server](https://modelcontextprotocol.io) that lets Claude Desktop, Cursor, and any MCP-compatible AI agent verify a contractor's license, status, expiration, and disciplinary history directly against state licensing board portals.
3
+ **Real-time contractor license verification across all 50 US states + DC, plus 8 major-city contractor licensing portals (Chicago, NYC, Philadelphia, Detroit, Atlanta, Dallas, Las Vegas, Nashville).** An [MCP server](https://modelcontextprotocol.io) that lets Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-compatible AI agent verify a contractor's license, status, expiration, and disciplinary history directly against licensing board portals.
4
4
 
5
5
  Send `{state, license_number, trade}` — get back validity, licensee name, expiration date, status, and any disciplinary actions on file. Results are fetched live from official state portals (no stale nightly exports) and cached for 24 hours when active.
6
6
 
7
7
  ## Why this server
8
8
 
9
- - **45 states** covered via official state licensing board portals, not third-party data aggregators
9
+ - **All 50 US states + DC + 8 major cities** covered via official licensing board portals, not third-party data aggregators
10
10
  - **Live lookups** — each verification hits the authoritative portal, so expirations and disciplinary actions are as fresh as the board's own data
11
11
  - **Batch verification** — up to 25 licenses per call, run in parallel
12
12
  - **Disciplinary history** — returned when the portal exposes it
@@ -14,13 +14,33 @@ Send `{state, license_number, trade}` — get back validity, licensee name, expi
14
14
 
15
15
  ## Quick start
16
16
 
17
- ### Claude Desktop
17
+ ### Hosted (recommended)
18
18
 
19
- Add this to your Claude Desktop config:
19
+ No install required. Add this to your Claude Desktop config:
20
20
 
21
21
  - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
22
22
  - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
23
23
 
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "tradesapi": {
28
+ "type": "streamable-http",
29
+ "url": "https://www.tradesapi.com/mcp",
30
+ "headers": {
31
+ "Authorization": "Bearer YOUR_API_KEY"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ Replace `YOUR_API_KEY` with the key from your dashboard and restart Claude Desktop.
39
+
40
+ ### Local install (alternative)
41
+
42
+ If you prefer to run the MCP server locally via stdio:
43
+
24
44
  ```json
25
45
  {
26
46
  "mcpServers": {
@@ -54,15 +74,16 @@ npm install -g contractor-license-mcp-server
54
74
 
55
75
  ## Tools
56
76
 
57
- ### `clv_verify_license`
77
+ ### `verify_license`
58
78
 
59
- Verify a single contractor license against a state licensing board portal.
79
+ Verify a single contractor license against the official state (or city) licensing portal.
60
80
 
61
81
  | Parameter | Required | Description |
62
82
  |---|---|---|
63
83
  | `state` | yes | Two-letter state code (`CA`, `TX`, `FL`, ...) |
84
+ | `city` | no | Optional city slug to target a municipal portal: `chicago`, `nyc`, `philadelphia`, `detroit`, `atlanta`, `dallas`, `lasvegas`, `nashville`. Lowercase, no spaces. |
64
85
  | `license_number` | yes | The license number to verify |
65
- | `trade` | no | `general`, `electrical`, `plumbing`, `hvac`, `mechanical`, `residential`, or `home_inspection` (defaults to `general`) |
86
+ | `trade` | no | `general`, `electrical`, `plumbing`, `hvac`, `mechanical`, `roofing`, `residential`, ... (defaults to `general`) |
66
87
  | `force_refresh` | no | Bypass the 24h cache and re-fetch from the portal |
67
88
  | `response_format` | no | `markdown` (default) or `json` |
68
89
 
@@ -81,74 +102,45 @@ Verify a single contractor license against a state licensing board portal.
81
102
  | Expiration | 05/12/2026 |
82
103
  ```
83
104
 
84
- ### `clv_batch_verify`
105
+ ### `batch_verify`
85
106
 
86
- Verify up to 25 licenses in a single call. Each verification runs independently — partial failures do not block the batch.
107
+ Verify up to 25 licenses in a single call. Each verification runs independently — partial failures do not block the batch. Per-item `city` is supported.
87
108
 
88
109
  | Parameter | Required | Description |
89
110
  |---|---|---|
90
- | `licenses` | yes | Array of `{ state, license_number, trade }` objects (1–25 items) |
111
+ | `licenses` | yes | Array of `{ state, city?, license_number, trade }` objects (1–25 items) |
91
112
  | `response_format` | no | `markdown` (default) or `json` |
92
113
 
93
- ### `clv_list_supported_states`
114
+ ### `search_by_name`
94
115
 
95
- List all supported states, including portal URLs and available trades. Use this to see which states and trades you can query.
116
+ Fuzzy-match contractors by business or individual name within a single state (or city) database. Costs 2 credits per call.
96
117
 
97
118
  | Parameter | Required | Description |
98
119
  |---|---|---|
120
+ | `state` | yes | Two-letter state code |
121
+ | `city` | no | Optional city slug for municipal databases |
122
+ | `name` | yes | Business or individual name (case-insensitive, partial-match tolerant) |
123
+ | `trade` | no | Trade filter |
124
+ | `limit` | no | Max results (1–50, default 20) |
99
125
  | `response_format` | no | `markdown` (default) or `json` |
100
126
 
101
- ## Supported states
127
+ Not every state portal supports name search — call `list_supported_states` and check `supports_name_search` per jurisdiction first.
128
+
129
+ ### `list_supported_states`
102
130
 
103
- | Code | State | Trades |
131
+ List every supported jurisdiction with portal URLs, current health, available trades, and registered municipal scrapers nested under each state. Use this to discover what's reachable before constructing other tool calls.
132
+
133
+ | Parameter | Required | Description |
104
134
  |---|---|---|
105
- | AK | Alaska | general, electrical, mechanical |
106
- | AL | Alabama | general, electrical, plumbing, hvac, residential |
107
- | AR | Arkansas | general |
108
- | AZ | Arizona | general, electrical, plumbing, hvac |
109
- | CA | California | general, electrical, plumbing, hvac |
110
- | CO | Colorado | electrical, plumbing |
111
- | CT | Connecticut | general, electrical, plumbing, hvac |
112
- | DC | District of Columbia | general |
113
- | DE | Delaware | electrical, plumbing, hvac |
114
- | FL | Florida | general, electrical, plumbing, hvac |
115
- | GA | Georgia | general |
116
- | HI | Hawaii | general |
117
- | IA | Iowa | electrical |
118
- | ID | Idaho | electrical, plumbing, hvac |
119
- | IL | Illinois | general, electrical, plumbing, hvac |
120
- | IN | Indiana | plumbing |
121
- | KY | Kentucky | general, electrical, hvac, plumbing |
122
- | LA | Louisiana | general |
123
- | MA | Massachusetts | general, mechanical |
124
- | MD | Maryland | general, hvac, electrical, plumbing |
125
- | ME | Maine | electrical, plumbing |
126
- | MI | Michigan | electrical, plumbing, hvac |
127
- | MN | Minnesota | general, electrical, plumbing |
128
- | MS | Mississippi | general |
129
- | NC | North Carolina | general |
130
- | ND | North Dakota | general, electrical |
131
- | NE | Nebraska | general, electrical |
132
- | NH | New Hampshire | electrical, plumbing |
133
- | NJ | New Jersey | general, electrical, hvac, plumbing |
134
- | NM | New Mexico | general, electrical, plumbing, hvac |
135
- | NV | Nevada | general, electrical, plumbing, hvac |
136
- | NY | New York | home_inspection |
137
- | OH | Ohio | general, electrical, plumbing, hvac |
138
- | OK | Oklahoma | electrical, plumbing, hvac |
139
- | OR | Oregon | general |
140
- | PA | Pennsylvania | general, electrical, hvac, plumbing |
141
- | RI | Rhode Island | general |
142
- | SC | South Carolina | general, electrical, plumbing, hvac |
143
- | TN | Tennessee | general, electrical, plumbing |
144
- | TX | Texas | hvac, electrical, plumbing |
145
- | UT | Utah | general, electrical, plumbing, hvac |
146
- | VA | Virginia | general, electrical, plumbing, hvac |
147
- | VT | Vermont | electrical, plumbing |
148
- | WA | Washington | general |
149
- | WV | West Virginia | general, electrical, hvac, plumbing |
150
-
151
- Coverage expands continuously. Run `clv_list_supported_states` from your agent for the current list, or see the live state grid at [www.tradesapi.com](https://www.tradesapi.com).
135
+ | `response_format` | no | `markdown` (default) or `json` |
136
+
137
+ ## Coverage
138
+
139
+ All 50 US states + DC at the state level, plus 8 major-city contractor licensing portals (Chicago, NYC, Philadelphia, Detroit, Atlanta, Dallas, Las Vegas, Nashville).
140
+
141
+ Run `list_supported_states` from your agent for the live, fetched-fresh-each-call list of supported jurisdictions, available trades per jurisdiction, current portal health, and which states support name search. The MCP package no longer bundles a static state table — what comes back from `list_supported_states` is always current with prod.
142
+
143
+ You can also see the live state grid at [www.tradesapi.com](https://www.tradesapi.com).
152
144
 
153
145
  ## Configuration
154
146
 
@@ -164,8 +156,8 @@ Each license verification consumes **1 credit**, whether the result is fresh or
164
156
  ## Development
165
157
 
166
158
  ```bash
167
- git clone https://github.com/Noquarter6/contractor-license-mcp-server.git
168
- cd contractor-license-mcp-server
159
+ git clone https://github.com/jackunderwood/Contractor-License-Verification.git
160
+ cd Contractor-License-Verification/mcp-server
169
161
  npm install
170
162
  npm run build
171
163
  npm test
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LicenseResult, SearchResponse, CreditInfo } from "./types.js";
1
+ import type { BatchItem, BatchResponse, LicenseResult, SearchResponse, StatesApiResponse } from "./types.js";
2
2
  export declare class ApiError extends Error {
3
3
  statusCode: number;
4
4
  retryAfter?: number | undefined;
@@ -6,21 +6,16 @@ export declare class ApiError extends Error {
6
6
  }
7
7
  export declare class ApiClient {
8
8
  private http;
9
- constructor(baseURL: string, apiKey: string);
10
- verify(state: string, licenseNumber: string, trade: string): Promise<{
11
- data: LicenseResult;
12
- credits: CreditInfo;
13
- }>;
14
- search(state: string, name: string, trade: string, limit: number): Promise<{
15
- data: SearchResponse;
16
- credits: CreditInfo;
17
- }>;
9
+ constructor(baseURL: string, apiKey: string, extraHeaders?: Record<string, string>);
10
+ verify(state: string, licenseNumber: string, trade: string, city?: string, forceRefresh?: boolean): Promise<LicenseResult>;
11
+ batch(items: BatchItem[]): Promise<BatchResponse>;
12
+ search(state: string, name: string, trade: string, limit: number, city?: string): Promise<SearchResponse>;
18
13
  health(): Promise<{
19
14
  status: string;
20
15
  api: string;
21
16
  database: string;
22
17
  redis: string;
23
18
  }>;
24
- private parseCredits;
19
+ states(): Promise<StatesApiResponse>;
25
20
  private wrapError;
26
21
  }
package/dist/api.js CHANGED
@@ -11,30 +11,57 @@ export class ApiError extends Error {
11
11
  }
12
12
  export class ApiClient {
13
13
  http;
14
- constructor(baseURL, apiKey) {
14
+ constructor(baseURL, apiKey, extraHeaders = {}) {
15
+ const headers = { ...extraHeaders };
16
+ if (apiKey)
17
+ headers["X-API-Key"] = apiKey;
15
18
  this.http = axios.create({
16
19
  baseURL,
17
- headers: { "X-API-Key": apiKey },
18
- timeout: 120_000, // Portal lookups can be slow
20
+ headers,
21
+ timeout: 120_000,
19
22
  });
20
23
  }
21
- async verify(state, licenseNumber, trade) {
24
+ async verify(state, licenseNumber, trade, city, forceRefresh) {
22
25
  try {
23
- const resp = await this.http.get("/verify", {
24
- params: { state, license: licenseNumber, trade },
25
- });
26
- return { data: resp.data, credits: this.parseCredits(resp.headers) };
26
+ const params = { state, license: licenseNumber, trade };
27
+ if (city)
28
+ params.city = city;
29
+ if (forceRefresh)
30
+ params.fresh = "true";
31
+ const { data } = await this.http.get("/verify", { params });
32
+ return data;
33
+ }
34
+ catch (err) {
35
+ throw this.wrapError(err);
36
+ }
37
+ }
38
+ async batch(items) {
39
+ try {
40
+ const body = {
41
+ licenses: items.map((it) => ({
42
+ state: it.state,
43
+ ...(it.city ? { city: it.city } : {}),
44
+ license: it.license,
45
+ trade: it.trade,
46
+ })),
47
+ };
48
+ const { data } = await this.http.post("/batch", body);
49
+ return {
50
+ summary: { total: data.total, succeeded: data.succeeded, failed: data.failed },
51
+ results: data.results,
52
+ };
27
53
  }
28
54
  catch (err) {
29
55
  throw this.wrapError(err);
30
56
  }
31
57
  }
32
- async search(state, name, trade, limit) {
58
+ async search(state, name, trade, limit, city) {
33
59
  try {
34
- const resp = await this.http.get("/search", {
35
- params: { state, name, trade, limit },
36
- });
37
- return { data: resp.data, credits: this.parseCredits(resp.headers) };
60
+ const params = { state, name, trade, limit };
61
+ if (city)
62
+ params.city = city;
63
+ const { data } = await this.http.get("/search", { params });
64
+ return data;
38
65
  }
39
66
  catch (err) {
40
67
  throw this.wrapError(err);
@@ -49,20 +76,21 @@ export class ApiClient {
49
76
  throw this.wrapError(err);
50
77
  }
51
78
  }
52
- parseCredits(headers) {
53
- const remaining = headers?.["x-credits-remaining"];
54
- const charged = headers?.["x-credits-charged"];
55
- return {
56
- remaining: remaining != null ? parseInt(remaining, 10) : null,
57
- charged: charged != null ? parseInt(charged, 10) : null,
58
- };
79
+ async states() {
80
+ try {
81
+ const { data } = await this.http.get("/states");
82
+ return data;
83
+ }
84
+ catch (err) {
85
+ throw this.wrapError(err);
86
+ }
59
87
  }
60
88
  wrapError(err) {
61
89
  if (err.isAxiosError && err.response) {
62
90
  const { status, data, headers } = err.response;
63
91
  const detail = data?.detail ?? "Unknown error";
64
92
  if (status === 401) {
65
- return new ApiError("Authentication failed — check your CLV_API_KEY environment variable. Get a key at https://www.tradesapi.com", 401);
93
+ return new ApiError("Authentication failed — check your CLV_API_KEY environment variable", 401);
66
94
  }
67
95
  if (status === 429) {
68
96
  const retryAfter = parseInt(headers?.["retry-after"] ?? "60", 10);
@@ -72,7 +100,7 @@ export class ApiClient {
72
100
  return new ApiError(detail, 400);
73
101
  }
74
102
  if (status === 502) {
75
- return new ApiError(`Verification temporarily unavailable — the state portal may be down. Try again in a few minutes.`, 502);
103
+ return new ApiError("Verification temporarily unavailable. Try again in a few minutes.", 502);
76
104
  }
77
105
  return new ApiError(detail, status);
78
106
  }
package/dist/format.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { LicenseResult, StateInfo, BatchResponse, SearchResponse, CreditInfo } from "./types.js";
2
- export declare function formatCredits(credits: CreditInfo): string;
1
+ import type { LicenseResult, StateInfo, BatchResponse, SearchResponse } from "./types.js";
3
2
  export declare function formatLicenseResult(result: LicenseResult, format: "markdown" | "json"): string;
4
3
  export declare function formatStatesList(states: StateInfo[], format: "markdown" | "json"): string;
5
4
  export declare function formatBatchResponse(batch: BatchResponse, format: "markdown" | "json"): string;
package/dist/format.js CHANGED
@@ -1,21 +1,3 @@
1
- function normalizeStatus(status) {
2
- if (!status)
3
- return "N/A";
4
- const s = status.toLowerCase();
5
- if (s === "not_found" || s === "not found")
6
- return "Not Found";
7
- if (s === "unknown")
8
- return "Unknown (lookup may have failed)";
9
- return status;
10
- }
11
- export function formatCredits(credits) {
12
- const parts = [];
13
- if (credits.charged != null)
14
- parts.push(`Credits used: ${credits.charged}`);
15
- if (credits.remaining != null)
16
- parts.push(`Credits remaining: ${credits.remaining}`);
17
- return parts.length > 0 ? `\n\n---\n${parts.join(" | ")}` : "";
18
- }
19
1
  export function formatLicenseResult(result, format) {
20
2
  if (format === "json") {
21
3
  return JSON.stringify(result, null, 2);
@@ -30,7 +12,7 @@ export function formatLicenseResult(result, format) {
30
12
  `| License # | ${result.license_number} |`,
31
13
  `| State | ${result.state} |`,
32
14
  `| Trade | ${result.trade} |`,
33
- `| Status | ${normalizeStatus(result.status)} |`,
15
+ `| Status | ${result.status ?? "N/A"} |`,
34
16
  `| Expiration | ${result.expiration ?? "N/A"} |`,
35
17
  `| Source | ${result.source_url ?? "N/A"} |`,
36
18
  `| Cached | ${result.cached ? "Yes" : "No"} |`,
@@ -39,36 +21,53 @@ export function formatLicenseResult(result, format) {
39
21
  if (result.disciplinary_actions.length > 0) {
40
22
  lines.push("", "### Disciplinary Actions");
41
23
  for (const action of result.disciplinary_actions) {
42
- if (typeof action === "string") {
43
- lines.push(`- ${action}`);
44
- }
45
- else if (action && typeof action === "object" && "description" in action) {
46
- lines.push(`- ${action.description}`);
47
- }
48
- else {
49
- lines.push(`- ${JSON.stringify(action)}`);
50
- }
24
+ lines.push(`- ${action}`);
51
25
  }
52
26
  }
53
27
  return lines.join("\n");
54
28
  }
29
+ function statusIcon(status) {
30
+ switch (status) {
31
+ case "healthy":
32
+ return "OK";
33
+ case "degraded":
34
+ return "DEGRADED";
35
+ case "maintenance":
36
+ return "MAINTENANCE";
37
+ default:
38
+ return "DOWN";
39
+ }
40
+ }
55
41
  export function formatStatesList(states, format) {
42
+ const allMunis = states.flatMap((s) => s.municipalities ?? []);
56
43
  if (format === "json") {
57
- return JSON.stringify({ states, total: states.length }, null, 2);
44
+ return JSON.stringify({
45
+ total_states: states.length,
46
+ total_municipalities: allMunis.length,
47
+ states,
48
+ }, null, 2);
58
49
  }
50
+ const header = allMunis.length > 0
51
+ ? `## Supported Jurisdictions (${states.length} states + ${allMunis.length} cities)`
52
+ : `## Supported States (${states.length} states)`;
59
53
  const lines = [
60
- `## Supported States (${states.length} states)`,
54
+ header,
55
+ "",
56
+ "### States",
61
57
  "",
62
58
  "| State | Name | Status | Trades |",
63
59
  "|-------|------|--------|--------|",
64
60
  ];
65
61
  for (const s of states) {
66
- const statusIcon = s.status === "healthy"
67
- ? "OK"
68
- : s.status === "degraded"
69
- ? "DEGRADED"
70
- : "DOWN";
71
- lines.push(`| ${s.code} | ${s.name} | ${statusIcon} | ${s.trades.join(", ")} |`);
62
+ lines.push(`| ${s.code} | ${s.name} | ${statusIcon(s.status)} | ${s.trades.join(", ")} |`);
63
+ }
64
+ if (allMunis.length > 0) {
65
+ lines.push("", "### Cities (municipal scrapers)", "");
66
+ lines.push("| Code | City | Parent State | Status | Trades |");
67
+ lines.push("|------|------|--------------|--------|--------|");
68
+ for (const m of allMunis) {
69
+ lines.push(`| ${m.code} | ${m.city} | ${m.parent_state} | ${statusIcon(m.status)} | ${m.trades.join(", ")} |`);
70
+ }
72
71
  }
73
72
  return lines.join("\n");
74
73
  }
@@ -89,7 +88,7 @@ export function formatBatchResponse(batch, format) {
89
88
  if (item.result.name)
90
89
  lines.push(`Name: ${item.result.name}`);
91
90
  if (item.result.status)
92
- lines.push(`Status: ${normalizeStatus(item.result.status)}`);
91
+ lines.push(`Status: ${item.result.status}`);
93
92
  lines.push("");
94
93
  }
95
94
  else {
package/dist/http.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface AuthResult {
2
+ /** api_key_id when authenticated via JWT, or undefined when authenticated via raw API key */
3
+ apiKeyId?: string;
4
+ /** Raw token (either JWT or raw API key) — used to build ApiClient */
5
+ token: string;
6
+ /** True if this was a JWT (and therefore Node uses X-Internal-Secret + X-Api-Key-Id when calling FastAPI) */
7
+ isJwt: boolean;
8
+ }
9
+ export declare function sessionIdentity(auth: AuthResult): string;