contractor-license-mcp-server 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,132 @@
1
+ # contractor-license-mcp-server
2
+
3
+ An [MCP server](https://modelcontextprotocol.io) for verifying US contractor licenses. Send a license number, state, and trade -- get back validity, expiration, status, and disciplinary history. Works with Claude Desktop and any MCP-compatible AI agent.
4
+
5
+ Currently supports **Texas (TDLR)**, **California (CSLB)**, and **Florida (DBPR)**, with more states coming.
6
+
7
+ ## Quick Start
8
+
9
+ ### Claude Desktop
10
+
11
+ Add this to your Claude Desktop config file:
12
+
13
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
14
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "contractor-license-verification": {
20
+ "command": "npx",
21
+ "args": ["-y", "contractor-license-mcp-server"],
22
+ "env": {
23
+ "CLV_API_URL": "https://contractor-license-verification-production.up.railway.app",
24
+ "CLV_API_KEY": "your-api-key-here"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Restart Claude Desktop after saving the config.
32
+
33
+ ### Getting an API Key
34
+
35
+ New accounts start with **50 free verification credits**. To get your API key:
36
+
37
+ 1. Visit the [billing portal](https://contractor-license-verification-production.up.railway.app/billing/checkout) to create an account
38
+ 2. Your API key will be provided on signup
39
+ 3. Need more credits? Purchase additional packs at the same billing portal
40
+
41
+ ### Direct Install
42
+
43
+ ```bash
44
+ npm install -g contractor-license-mcp-server
45
+ ```
46
+
47
+ ## Available Tools
48
+
49
+ ### `clv_verify_license`
50
+
51
+ Verify a single contractor license against a state licensing board portal.
52
+
53
+ **Parameters:**
54
+
55
+ | Name | Required | Description |
56
+ |------|----------|-------------|
57
+ | `state` | Yes | Two-letter state code (e.g. `CA`, `TX`, `FL`) |
58
+ | `license_number` | Yes | The license number to verify |
59
+ | `trade` | No | Trade type (default: `general`) |
60
+ | `force_refresh` | No | Bypass cache for fresh data (default: `false`) |
61
+ | `response_format` | No | `markdown` or `json` (default: `markdown`) |
62
+
63
+ **Example result:**
64
+
65
+ ```
66
+ ## License Verification: VALID
67
+
68
+ | Field | Value |
69
+ |------------|--------------------------|
70
+ | Name | ANDERSON, ORIN RAE |
71
+ | License # | TACLA00000103C |
72
+ | State | TX |
73
+ | Trade | hvac |
74
+ | Status | Active |
75
+ | Expiration | 05/12/2026 |
76
+ ```
77
+
78
+ ### `clv_batch_verify`
79
+
80
+ Verify up to 25 licenses in a single call. Each license is verified independently -- partial failures do not block the batch.
81
+
82
+ **Parameters:**
83
+
84
+ | Name | Required | Description |
85
+ |------|----------|-------------|
86
+ | `licenses` | Yes | Array of `{ state, license_number, trade }` objects (1-25 items) |
87
+ | `response_format` | No | `markdown` or `json` (default: `markdown`) |
88
+
89
+ ### `clv_list_supported_states`
90
+
91
+ List all states currently supported for verification, including portal URLs and available trades.
92
+
93
+ **Parameters:**
94
+
95
+ | Name | Required | Description |
96
+ |------|----------|-------------|
97
+ | `response_format` | No | `markdown` or `json` (default: `markdown`) |
98
+
99
+ ## Supported States
100
+
101
+ | Code | State | Portal | Trades |
102
+ |------|------------|--------|---------------------|
103
+ | TX | Texas | TDLR | hvac, electrical |
104
+ | CA | California | CSLB | general |
105
+ | FL | Florida | DBPR | general |
106
+
107
+ More states are being added. Run `clv_list_supported_states` for the latest list.
108
+
109
+ ## Configuration
110
+
111
+ | Variable | Required | Description |
112
+ |----------|----------|-------------|
113
+ | `CLV_API_URL` | Yes | API backend URL |
114
+ | `CLV_API_KEY` | Yes | Your API key |
115
+
116
+ ## Credits
117
+
118
+ Each license verification consumes **1 credit** (including cached results). New accounts receive **50 free credits**. Purchase additional credit packs via `POST /billing/checkout` with your API key.
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ git clone https://github.com/jackunderwood/Contractor-License-Verification.git
124
+ cd Contractor-License-Verification/mcp-server
125
+ npm install
126
+ npm run build
127
+ npm test
128
+ ```
129
+
130
+ ## License
131
+
132
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { LicenseResult } from "./types.js";
2
+ export declare class ApiError extends Error {
3
+ statusCode: number;
4
+ retryAfter?: number | undefined;
5
+ constructor(message: string, statusCode: number, retryAfter?: number | undefined);
6
+ }
7
+ export declare class ApiClient {
8
+ private http;
9
+ constructor(baseURL: string, apiKey: string);
10
+ verify(state: string, licenseNumber: string, trade: string): Promise<LicenseResult>;
11
+ health(): Promise<{
12
+ status: string;
13
+ api: string;
14
+ database: string;
15
+ redis: string;
16
+ }>;
17
+ private wrapError;
18
+ }
package/dist/api.js ADDED
@@ -0,0 +1,66 @@
1
+ import axios from "axios";
2
+ export class ApiError extends Error {
3
+ statusCode;
4
+ retryAfter;
5
+ constructor(message, statusCode, retryAfter) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.retryAfter = retryAfter;
9
+ this.name = "ApiError";
10
+ }
11
+ }
12
+ export class ApiClient {
13
+ http;
14
+ constructor(baseURL, apiKey) {
15
+ this.http = axios.create({
16
+ baseURL,
17
+ headers: { "X-API-Key": apiKey },
18
+ timeout: 120_000, // Scrapers can be slow (Playwright + portal load)
19
+ });
20
+ }
21
+ async verify(state, licenseNumber, trade) {
22
+ // TODO: Add force_refresh param once backend supports cache bypass
23
+ try {
24
+ const { data } = await this.http.get("/verify", {
25
+ params: { state, license: licenseNumber, trade },
26
+ });
27
+ return data;
28
+ }
29
+ catch (err) {
30
+ throw this.wrapError(err);
31
+ }
32
+ }
33
+ async health() {
34
+ try {
35
+ const { data } = await this.http.get("/health");
36
+ return data;
37
+ }
38
+ catch (err) {
39
+ throw this.wrapError(err);
40
+ }
41
+ }
42
+ wrapError(err) {
43
+ if (err.isAxiosError && err.response) {
44
+ const { status, data, headers } = err.response;
45
+ const detail = data?.detail ?? "Unknown error";
46
+ if (status === 401) {
47
+ return new ApiError("Authentication failed — check your CLV_API_KEY environment variable", 401);
48
+ }
49
+ if (status === 429) {
50
+ const retryAfter = parseInt(headers?.["retry-after"] ?? "60", 10);
51
+ return new ApiError(`Rate limit exceeded. Try again in ${retryAfter} seconds.`, 429, retryAfter);
52
+ }
53
+ if (status === 400) {
54
+ return new ApiError(detail, 400);
55
+ }
56
+ if (status === 502) {
57
+ return new ApiError("Verification temporarily unavailable. Try again in a few minutes.", 502);
58
+ }
59
+ return new ApiError(detail, status);
60
+ }
61
+ if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
62
+ return new ApiError("Cannot reach the verification API — check your CLV_API_URL environment variable", 0);
63
+ }
64
+ return new ApiError(err.message ?? "Unknown error", 0);
65
+ }
66
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CHARACTER_LIMIT = 50000;
2
+ export declare const US_STATE_CODES: Set<string>;
@@ -0,0 +1,9 @@
1
+ export const CHARACTER_LIMIT = 50_000;
2
+ export const US_STATE_CODES = new Set([
3
+ "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
4
+ "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
5
+ "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
6
+ "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
7
+ "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY",
8
+ "DC",
9
+ ]);
@@ -0,0 +1,4 @@
1
+ import type { LicenseResult, StateInfo, BatchResponse } from "./types.js";
2
+ export declare function formatLicenseResult(result: LicenseResult, format: "markdown" | "json"): string;
3
+ export declare function formatStatesList(states: StateInfo[], format: "markdown" | "json"): string;
4
+ export declare function formatBatchResponse(batch: BatchResponse, format: "markdown" | "json"): string;
package/dist/format.js ADDED
@@ -0,0 +1,76 @@
1
+ export function formatLicenseResult(result, format) {
2
+ if (format === "json") {
3
+ return JSON.stringify(result, null, 2);
4
+ }
5
+ const status = result.valid ? "VALID" : "INVALID";
6
+ const lines = [
7
+ `## License Verification: ${status}`,
8
+ "",
9
+ `| Field | Value |`,
10
+ `|-------|-------|`,
11
+ `| Name | ${result.name ?? "N/A"} |`,
12
+ `| License # | ${result.license_number} |`,
13
+ `| State | ${result.state} |`,
14
+ `| Trade | ${result.trade} |`,
15
+ `| Status | ${result.status ?? "N/A"} |`,
16
+ `| Expiration | ${result.expiration ?? "N/A"} |`,
17
+ `| Source | ${result.source_url ?? "N/A"} |`,
18
+ `| Cached | ${result.cached ? "Yes" : "No"} |`,
19
+ `| Checked | ${result.checked_at} |`,
20
+ ];
21
+ if (result.disciplinary_actions.length > 0) {
22
+ lines.push("", "### Disciplinary Actions");
23
+ for (const action of result.disciplinary_actions) {
24
+ lines.push(`- ${action}`);
25
+ }
26
+ }
27
+ return lines.join("\n");
28
+ }
29
+ export function formatStatesList(states, format) {
30
+ if (format === "json") {
31
+ return JSON.stringify({ states, total: states.length }, null, 2);
32
+ }
33
+ const lines = [
34
+ `## Supported States (${states.length} states)`,
35
+ "",
36
+ "| State | Name | Status | Trades |",
37
+ "|-------|------|--------|--------|",
38
+ ];
39
+ for (const s of states) {
40
+ const statusIcon = s.status === "healthy"
41
+ ? "OK"
42
+ : s.status === "degraded"
43
+ ? "DEGRADED"
44
+ : "DOWN";
45
+ lines.push(`| ${s.code} | ${s.name} | ${statusIcon} | ${s.trades.join(", ")} |`);
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ export function formatBatchResponse(batch, format) {
50
+ if (format === "json") {
51
+ return JSON.stringify(batch, null, 2);
52
+ }
53
+ const { summary, results } = batch;
54
+ const lines = [
55
+ `## Batch Verification: ${summary.succeeded}/${summary.total} succeeded`,
56
+ "",
57
+ ];
58
+ for (let i = 0; i < results.length; i++) {
59
+ const item = results[i];
60
+ if (item.result) {
61
+ lines.push(`### ${i + 1}. ${item.result.license_number} (${item.result.state})`);
62
+ lines.push(item.result.valid ? "**VALID**" : "**INVALID**");
63
+ if (item.result.name)
64
+ lines.push(`Name: ${item.result.name}`);
65
+ if (item.result.status)
66
+ lines.push(`Status: ${item.result.status}`);
67
+ lines.push("");
68
+ }
69
+ else {
70
+ lines.push(`### ${i + 1}. ERROR`);
71
+ lines.push(item.error ?? "Unknown error");
72
+ lines.push("");
73
+ }
74
+ }
75
+ return lines.join("\n");
76
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ApiClient } from "./api.js";
5
+ import { VerifyInputSchema, BatchInputSchema, StatesInputSchema } from "./schemas.js";
6
+ import { handleVerify } from "./tools/verify.js";
7
+ import { handleBatchVerify } from "./tools/batch.js";
8
+ import { handleListStates } from "./tools/states.js";
9
+ function getConfig() {
10
+ const apiUrl = process.env.CLV_API_URL;
11
+ const apiKey = process.env.CLV_API_KEY;
12
+ if (!apiUrl) {
13
+ console.error("Error: CLV_API_URL environment variable is required.\n" +
14
+ "Set it to your Contractor License Verification API URL (e.g. https://your-app.railway.app)");
15
+ process.exit(1);
16
+ }
17
+ if (!apiKey) {
18
+ console.error("Error: CLV_API_KEY environment variable is required.\n" +
19
+ "Get an API key from your CLV API admin.");
20
+ process.exit(1);
21
+ }
22
+ return { apiUrl, apiKey };
23
+ }
24
+ async function main() {
25
+ const { apiUrl, apiKey } = getConfig();
26
+ const client = new ApiClient(apiUrl, apiKey);
27
+ const server = new McpServer({
28
+ name: "contractor-license-verification",
29
+ version: "0.1.0",
30
+ });
31
+ server.registerTool("clv_verify_license", {
32
+ description: "Verify a contractor's license by checking the state licensing board portal. " +
33
+ "Returns license validity, holder name, status, expiration, and any disciplinary actions.",
34
+ inputSchema: VerifyInputSchema,
35
+ annotations: {
36
+ readOnlyHint: true,
37
+ destructiveHint: false,
38
+ idempotentHint: true,
39
+ },
40
+ }, async (args) => handleVerify(client, args));
41
+ server.registerTool("clv_batch_verify", {
42
+ description: "Verify multiple contractor licenses in a single request (max 25). " +
43
+ "Returns results for each license, with partial failure handling — individual failures don't block others.",
44
+ inputSchema: BatchInputSchema,
45
+ annotations: {
46
+ readOnlyHint: true,
47
+ destructiveHint: false,
48
+ idempotentHint: true,
49
+ },
50
+ }, async (args) => handleBatchVerify(client, args));
51
+ server.registerTool("clv_list_supported_states", {
52
+ description: "List all US states currently supported for contractor license verification, " +
53
+ "including portal URLs, health status, and available trades.",
54
+ inputSchema: StatesInputSchema,
55
+ annotations: {
56
+ readOnlyHint: true,
57
+ destructiveHint: false,
58
+ idempotentHint: true,
59
+ },
60
+ }, async (args) => handleListStates(args));
61
+ const transport = new StdioServerTransport();
62
+ await server.connect(transport);
63
+ }
64
+ main().catch((err) => {
65
+ console.error("Fatal error:", err);
66
+ process.exit(1);
67
+ });
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ export declare const VerifyInputSchema: z.ZodObject<{
3
+ state: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
4
+ license_number: z.ZodString;
5
+ trade: z.ZodDefault<z.ZodString>;
6
+ force_refresh: z.ZodDefault<z.ZodBoolean>;
7
+ response_format: z.ZodDefault<z.ZodEnum<["markdown", "json"]>>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ state: string;
10
+ license_number: string;
11
+ trade: string;
12
+ force_refresh: boolean;
13
+ response_format: "markdown" | "json";
14
+ }, {
15
+ state: string;
16
+ license_number: string;
17
+ trade?: string | undefined;
18
+ force_refresh?: boolean | undefined;
19
+ response_format?: "markdown" | "json" | undefined;
20
+ }>;
21
+ export declare const BatchInputSchema: z.ZodObject<{
22
+ licenses: z.ZodArray<z.ZodObject<{
23
+ state: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
24
+ license_number: z.ZodString;
25
+ trade: z.ZodDefault<z.ZodString>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ state: string;
28
+ license_number: string;
29
+ trade: string;
30
+ }, {
31
+ state: string;
32
+ license_number: string;
33
+ trade?: string | undefined;
34
+ }>, "many">;
35
+ response_format: z.ZodDefault<z.ZodEnum<["markdown", "json"]>>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ response_format: "markdown" | "json";
38
+ licenses: {
39
+ state: string;
40
+ license_number: string;
41
+ trade: string;
42
+ }[];
43
+ }, {
44
+ licenses: {
45
+ state: string;
46
+ license_number: string;
47
+ trade?: string | undefined;
48
+ }[];
49
+ response_format?: "markdown" | "json" | undefined;
50
+ }>;
51
+ export declare const StatesInputSchema: z.ZodObject<{
52
+ response_format: z.ZodDefault<z.ZodEnum<["markdown", "json"]>>;
53
+ }, "strip", z.ZodTypeAny, {
54
+ response_format: "markdown" | "json";
55
+ }, {
56
+ response_format?: "markdown" | "json" | undefined;
57
+ }>;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { US_STATE_CODES } from "./constants.js";
3
+ const stateField = z
4
+ .string()
5
+ .length(2)
6
+ .transform((s) => s.toUpperCase())
7
+ .refine((s) => US_STATE_CODES.has(s), { message: "Invalid US state code" })
8
+ .describe("Two-letter US state code (e.g. 'CA', 'TX'). Use clv_list_supported_states to see available states.");
9
+ export const VerifyInputSchema = z.object({
10
+ state: stateField,
11
+ license_number: z
12
+ .string()
13
+ .min(1)
14
+ .max(50)
15
+ .describe("The contractor's license number as shown on their license card. Format varies by state."),
16
+ trade: z
17
+ .string()
18
+ .min(1)
19
+ .max(50)
20
+ .default("general")
21
+ .describe("The trade/contractor type (e.g. 'General Contractor', 'Electrical'). Use clv_list_supported_states to see valid values per state."),
22
+ force_refresh: z
23
+ .boolean()
24
+ .default(false)
25
+ .describe("Bypass cache and fetch fresh data from the state portal."),
26
+ response_format: z
27
+ .enum(["markdown", "json"])
28
+ .default("markdown")
29
+ .describe("Response format."),
30
+ });
31
+ export const BatchInputSchema = z.object({
32
+ licenses: z
33
+ .array(z.object({
34
+ state: stateField,
35
+ license_number: z.string().min(1).max(50).describe("License number."),
36
+ trade: z.string().min(1).max(50).default("general").describe("Trade type."),
37
+ }))
38
+ .min(1)
39
+ .max(25)
40
+ .describe("Array of licenses to verify (1-25 items)."),
41
+ response_format: z
42
+ .enum(["markdown", "json"])
43
+ .default("markdown")
44
+ .describe("Response format."),
45
+ });
46
+ export const StatesInputSchema = z.object({
47
+ response_format: z
48
+ .enum(["markdown", "json"])
49
+ .default("markdown")
50
+ .describe("Response format."),
51
+ });
@@ -0,0 +1,12 @@
1
+ import type { z } from "zod";
2
+ import type { ApiClient } from "../api.js";
3
+ import type { BatchInputSchema } from "../schemas.js";
4
+ type BatchInput = z.output<typeof BatchInputSchema>;
5
+ export declare function handleBatchVerify(client: ApiClient, args: BatchInput): Promise<{
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError?: boolean;
11
+ }>;
12
+ export {};
@@ -0,0 +1,33 @@
1
+ import { formatBatchResponse } from "../format.js";
2
+ import { CHARACTER_LIMIT } from "../constants.js";
3
+ export async function handleBatchVerify(client, args) {
4
+ const { licenses, response_format } = args;
5
+ const results = [];
6
+ // Sequential to respect backend rate limits.
7
+ // TODO: Switch to POST /batch when backend supports it (Phase 2B).
8
+ for (const item of licenses) {
9
+ try {
10
+ const result = await client.verify(item.state, item.license_number, item.trade);
11
+ results.push({ result, error: null });
12
+ }
13
+ catch (err) {
14
+ results.push({ result: null, error: err.message ?? "Unknown error" });
15
+ }
16
+ }
17
+ const succeeded = results.filter((r) => r.result !== null).length;
18
+ const batch = {
19
+ summary: {
20
+ total: licenses.length,
21
+ succeeded,
22
+ failed: licenses.length - succeeded,
23
+ },
24
+ results,
25
+ };
26
+ let text = formatBatchResponse(batch, response_format);
27
+ if (text.length > CHARACTER_LIMIT) {
28
+ text =
29
+ text.slice(0, CHARACTER_LIMIT) +
30
+ "\n\n[Output truncated — too many results]";
31
+ }
32
+ return { content: [{ type: "text", text }] };
33
+ }
@@ -0,0 +1,10 @@
1
+ import type { z } from "zod";
2
+ import type { StatesInputSchema } from "../schemas.js";
3
+ type StatesInput = z.output<typeof StatesInputSchema>;
4
+ export declare function handleListStates(args: StatesInput): Promise<{
5
+ content: {
6
+ type: "text";
7
+ text: string;
8
+ }[];
9
+ }>;
10
+ export {};
@@ -0,0 +1,34 @@
1
+ import { formatStatesList } from "../format.js";
2
+ // Hardcoded until backend exposes a /states endpoint (Phase 2B+).
3
+ // Update this list when new scrapers are registered.
4
+ const SUPPORTED_STATES = [
5
+ {
6
+ code: "CA",
7
+ name: "California",
8
+ portal: "https://www.cslb.ca.gov/onlineservices/checkalicense/",
9
+ status: "healthy",
10
+ trades: ["general"],
11
+ },
12
+ {
13
+ code: "FL",
14
+ name: "Florida",
15
+ portal: "https://www.myfloridalicense.com/wl11.asp",
16
+ status: "healthy",
17
+ trades: ["general"],
18
+ },
19
+ {
20
+ code: "TX",
21
+ name: "Texas",
22
+ portal: "https://www.tdlr.texas.gov/LicenseSearch/",
23
+ status: "healthy",
24
+ trades: ["hvac", "electrical"],
25
+ },
26
+ ];
27
+ export async function handleListStates(args) {
28
+ const format = args.response_format ?? "markdown";
29
+ return {
30
+ content: [
31
+ { type: "text", text: formatStatesList(SUPPORTED_STATES, format) },
32
+ ],
33
+ };
34
+ }
@@ -0,0 +1,12 @@
1
+ import type { z } from "zod";
2
+ import type { ApiClient } from "../api.js";
3
+ import type { VerifyInputSchema } from "../schemas.js";
4
+ type VerifyInput = z.output<typeof VerifyInputSchema>;
5
+ export declare function handleVerify(client: ApiClient, args: VerifyInput): Promise<{
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError?: boolean;
11
+ }>;
12
+ export {};
@@ -0,0 +1,18 @@
1
+ import { formatLicenseResult } from "../format.js";
2
+ export async function handleVerify(client, args) {
3
+ const { state, license_number, trade, response_format } = args;
4
+ try {
5
+ const result = await client.verify(state, license_number, trade);
6
+ return {
7
+ content: [
8
+ { type: "text", text: formatLicenseResult(result, response_format) },
9
+ ],
10
+ };
11
+ }
12
+ catch (err) {
13
+ return {
14
+ content: [{ type: "text", text: err.message ?? "Verification failed" }],
15
+ isError: true,
16
+ };
17
+ }
18
+ }
@@ -0,0 +1,37 @@
1
+ export interface LicenseResult {
2
+ valid: boolean;
3
+ name: string | null;
4
+ license_number: string;
5
+ trade: string;
6
+ expiration: string | null;
7
+ status: string | null;
8
+ state: string;
9
+ disciplinary_actions: string[];
10
+ source_url: string | null;
11
+ cached: boolean;
12
+ checked_at: string;
13
+ }
14
+ export interface StateInfo {
15
+ code: string;
16
+ name: string;
17
+ portal: string;
18
+ status: "healthy" | "degraded" | "down";
19
+ trades: string[];
20
+ }
21
+ export interface BatchItem {
22
+ state: string;
23
+ license_number: string;
24
+ trade: string;
25
+ }
26
+ export interface BatchResult {
27
+ result: LicenseResult | null;
28
+ error: string | null;
29
+ }
30
+ export interface BatchResponse {
31
+ summary: {
32
+ total: number;
33
+ succeeded: number;
34
+ failed: number;
35
+ };
36
+ results: BatchResult[];
37
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "contractor-license-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for contractor license verification — verify licenses across US state licensing portals via AI agents like Claude Desktop",
5
+ "type": "module",
6
+ "bin": {
7
+ "contractor-license-mcp-server": "./dist/index.js"
8
+ },
9
+ "files": ["dist"],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "contractor",
22
+ "license",
23
+ "verification",
24
+ "claude",
25
+ "ai-agent",
26
+ "state-licensing"
27
+ ],
28
+ "author": "Jack Underwood",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/jackunderwood/Contractor-License-Verification.git",
33
+ "directory": "mcp-server"
34
+ },
35
+ "homepage": "https://github.com/jackunderwood/Contractor-License-Verification/tree/main/mcp-server#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/jackunderwood/Contractor-License-Verification/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.27.0",
44
+ "axios": "^1.7.0",
45
+ "zod": "^3.23.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.0.0",
49
+ "typescript": "^5.7.0",
50
+ "vitest": "^3.0.0"
51
+ }
52
+ }