@striderlabs/mcp-thumbtack 1.0.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.
@@ -0,0 +1,12 @@
1
+ import { BrowserContext, Page } from "playwright";
2
+ export declare class BrowserManager {
3
+ private browser;
4
+ private context;
5
+ private page;
6
+ init(): Promise<void>;
7
+ getPage(): Promise<Page>;
8
+ getContext(): BrowserContext | null;
9
+ close(): Promise<void>;
10
+ isLoggedIn(): Promise<boolean>;
11
+ }
12
+ export declare function getBrowserManager(): BrowserManager;
@@ -0,0 +1,88 @@
1
+ import { chromium } from "playwright";
2
+ const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
3
+ export class BrowserManager {
4
+ browser = null;
5
+ context = null;
6
+ page = null;
7
+ async init() {
8
+ if (this.browser) {
9
+ return;
10
+ }
11
+ this.browser = await chromium.launch({
12
+ headless: true,
13
+ args: [
14
+ "--no-sandbox",
15
+ "--disable-setuid-sandbox",
16
+ "--disable-blink-features=AutomationControlled",
17
+ "--disable-infobars",
18
+ "--window-size=1280,800",
19
+ ],
20
+ });
21
+ this.context = await this.browser.newContext({
22
+ viewport: { width: 1280, height: 800 },
23
+ userAgent: USER_AGENT,
24
+ locale: "en-US",
25
+ timezoneId: "America/New_York",
26
+ extraHTTPHeaders: {
27
+ "Accept-Language": "en-US,en;q=0.9",
28
+ },
29
+ });
30
+ // Mask automation signals
31
+ await this.context.addInitScript(() => {
32
+ Object.defineProperty(navigator, "webdriver", { get: () => undefined });
33
+ });
34
+ this.page = await this.context.newPage();
35
+ }
36
+ async getPage() {
37
+ if (!this.page || !this.browser) {
38
+ await this.init();
39
+ }
40
+ return this.page;
41
+ }
42
+ getContext() {
43
+ return this.context;
44
+ }
45
+ async close() {
46
+ if (this.page) {
47
+ await this.page.close().catch(() => { });
48
+ this.page = null;
49
+ }
50
+ if (this.context) {
51
+ await this.context.close().catch(() => { });
52
+ this.context = null;
53
+ }
54
+ if (this.browser) {
55
+ await this.browser.close().catch(() => { });
56
+ this.browser = null;
57
+ }
58
+ }
59
+ async isLoggedIn() {
60
+ const page = await this.getPage();
61
+ try {
62
+ // Navigate to thumbtack.com and check for authenticated nav elements
63
+ await page.goto("https://www.thumbtack.com", {
64
+ waitUntil: "domcontentloaded",
65
+ timeout: 30000,
66
+ });
67
+ // Check for user profile indicators in nav
68
+ const loggedIn = await page.evaluate(() => {
69
+ // Look for sign-in button absence or profile menu presence
70
+ const signInBtn = document.querySelector('[data-test="sign-in-button"], a[href*="login"], button[data-testid="login"]');
71
+ const profileMenu = document.querySelector('[data-test="profile-menu"], [data-testid="user-avatar"], .user-avatar');
72
+ return !signInBtn || !!profileMenu;
73
+ });
74
+ return loggedIn;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ }
81
+ // Singleton instance
82
+ let browserManagerInstance = null;
83
+ export function getBrowserManager() {
84
+ if (!browserManagerInstance) {
85
+ browserManagerInstance = new BrowserManager();
86
+ }
87
+ return browserManagerInstance;
88
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "./server.js";
3
+ import { getBrowserManager } from "./browser.js";
4
+ // Handle graceful shutdown
5
+ process.on("SIGINT", async () => {
6
+ process.stderr.write("Shutting down Thumbtack MCP server...\n");
7
+ const manager = getBrowserManager();
8
+ await manager.close();
9
+ process.exit(0);
10
+ });
11
+ process.on("SIGTERM", async () => {
12
+ process.stderr.write("Shutting down Thumbtack MCP server...\n");
13
+ const manager = getBrowserManager();
14
+ await manager.close();
15
+ process.exit(0);
16
+ });
17
+ process.on("uncaughtException", async (error) => {
18
+ process.stderr.write(`Uncaught exception: ${error.message}\n`);
19
+ const manager = getBrowserManager();
20
+ await manager.close().catch(() => { });
21
+ process.exit(1);
22
+ });
23
+ process.on("unhandledRejection", async (reason) => {
24
+ process.stderr.write(`Unhandled rejection: ${reason}\n`);
25
+ });
26
+ // Start the MCP server
27
+ startServer().catch((error) => {
28
+ process.stderr.write(`Failed to start server: ${error.message}\n`);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,3 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function createServer(): Server;
3
+ export declare function startServer(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,276 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { searchServices } from "./tools/search-services.js";
5
+ import { getProvider } from "./tools/get-provider.js";
6
+ import { requestQuote } from "./tools/request-quote.js";
7
+ import { viewQuotes } from "./tools/view-quotes.js";
8
+ import { hireProvider } from "./tools/hire-provider.js";
9
+ import { getProjects } from "./tools/get-projects.js";
10
+ export function createServer() {
11
+ const server = new Server({
12
+ name: "mcp-thumbtack",
13
+ version: "1.0.0",
14
+ }, {
15
+ capabilities: {
16
+ tools: {},
17
+ },
18
+ });
19
+ // List tools handler
20
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
21
+ return {
22
+ tools: [
23
+ {
24
+ name: "thumbtack_search_services",
25
+ description: "Search for service providers on Thumbtack home services marketplace. Returns a list of matching professionals with ratings, reviews, and contact information.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ service_type: {
30
+ type: "string",
31
+ description: 'Type of service to search for (e.g., "plumber", "house cleaner", "electrician", "photographer")',
32
+ },
33
+ location: {
34
+ type: "string",
35
+ description: "City and state or full address (e.g., 'San Francisco, CA')",
36
+ },
37
+ zip_code: {
38
+ type: "string",
39
+ description: "ZIP code for location-based search (e.g., '94102')",
40
+ },
41
+ date: {
42
+ type: "string",
43
+ description: "Preferred service date in YYYY-MM-DD format",
44
+ },
45
+ num_people: {
46
+ type: "number",
47
+ description: "Number of people (relevant for event services)",
48
+ },
49
+ },
50
+ required: ["service_type"],
51
+ },
52
+ },
53
+ {
54
+ name: "thumbtack_get_provider",
55
+ description: "Get detailed information about a specific Thumbtack service provider, including their profile, services offered, reviews, and contact information.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ provider_id: {
60
+ type: "string",
61
+ description: "The Thumbtack provider ID (numeric ID from their profile URL)",
62
+ },
63
+ provider_url: {
64
+ type: "string",
65
+ description: "Full URL to the provider's Thumbtack profile page",
66
+ },
67
+ },
68
+ },
69
+ },
70
+ {
71
+ name: "thumbtack_request_quote",
72
+ description: "Request a quote from a specific Thumbtack service provider. Requires authentication - user must be logged in to Thumbtack.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ provider_id: {
77
+ type: "string",
78
+ description: "The Thumbtack provider ID",
79
+ },
80
+ service_type: {
81
+ type: "string",
82
+ description: "Type of service you need",
83
+ },
84
+ description: {
85
+ type: "string",
86
+ description: "Detailed description of the project or service needed",
87
+ },
88
+ location: {
89
+ type: "string",
90
+ description: "Service location ZIP code or address",
91
+ },
92
+ contact_info: {
93
+ type: "object",
94
+ description: "Contact information for the request",
95
+ properties: {
96
+ name: {
97
+ type: "string",
98
+ description: "Your name",
99
+ },
100
+ email: {
101
+ type: "string",
102
+ description: "Your email address",
103
+ },
104
+ phone: {
105
+ type: "string",
106
+ description: "Your phone number",
107
+ },
108
+ },
109
+ },
110
+ },
111
+ required: ["provider_id", "service_type", "description", "location", "contact_info"],
112
+ },
113
+ },
114
+ {
115
+ name: "thumbtack_view_quotes",
116
+ description: "View quotes received from Thumbtack service providers in your inbox. Requires authentication.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ status: {
121
+ type: "string",
122
+ enum: ["pending", "active", "completed", "archived", "all"],
123
+ description: "Filter quotes by status (default: all)",
124
+ },
125
+ },
126
+ },
127
+ },
128
+ {
129
+ name: "thumbtack_hire_provider",
130
+ description: "Hire a service provider from a received quote. This action creates a project and commits to working with the provider. Requires authentication.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ quote_id: {
135
+ type: "string",
136
+ description: "The ID of the quote to accept",
137
+ },
138
+ provider_id: {
139
+ type: "string",
140
+ description: "The Thumbtack provider ID",
141
+ },
142
+ },
143
+ required: ["quote_id", "provider_id"],
144
+ },
145
+ },
146
+ {
147
+ name: "thumbtack_get_projects",
148
+ description: "Get current and past projects from your Thumbtack account, including project status, provider information, and completion details. Requires authentication.",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ status: {
153
+ type: "string",
154
+ enum: ["active", "completed", "all"],
155
+ description: "Filter projects by status (default: all)",
156
+ },
157
+ },
158
+ },
159
+ },
160
+ ],
161
+ };
162
+ });
163
+ // Call tool handler
164
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
+ const { name, arguments: args } = request.params;
166
+ try {
167
+ switch (name) {
168
+ case "thumbtack_search_services": {
169
+ const params = args;
170
+ if (!params.service_type) {
171
+ return {
172
+ content: [{ type: "text", text: "Error: service_type parameter is required" }],
173
+ isError: true,
174
+ };
175
+ }
176
+ const result = await searchServices(params);
177
+ return {
178
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
179
+ };
180
+ }
181
+ case "thumbtack_get_provider": {
182
+ const params = args;
183
+ if (!params.provider_id && !params.provider_url) {
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: "Error: Either provider_id or provider_url must be provided",
189
+ },
190
+ ],
191
+ isError: true,
192
+ };
193
+ }
194
+ const result = await getProvider(params);
195
+ return {
196
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
197
+ };
198
+ }
199
+ case "thumbtack_request_quote": {
200
+ const params = args;
201
+ if (!params.provider_id || !params.service_type || !params.description || !params.location) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: "Error: provider_id, service_type, description, and location are required",
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+ const result = await requestQuote(params);
213
+ return {
214
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
215
+ };
216
+ }
217
+ case "thumbtack_view_quotes": {
218
+ const params = (args || {});
219
+ const result = await viewQuotes(params);
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
222
+ };
223
+ }
224
+ case "thumbtack_hire_provider": {
225
+ const params = args;
226
+ if (!params.quote_id || !params.provider_id) {
227
+ return {
228
+ content: [
229
+ {
230
+ type: "text",
231
+ text: "Error: quote_id and provider_id are required",
232
+ },
233
+ ],
234
+ isError: true,
235
+ };
236
+ }
237
+ const result = await hireProvider(params);
238
+ return {
239
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
240
+ };
241
+ }
242
+ case "thumbtack_get_projects": {
243
+ const params = (args || {});
244
+ const result = await getProjects(params);
245
+ return {
246
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
247
+ };
248
+ }
249
+ default:
250
+ return {
251
+ content: [{ type: "text", text: `Error: Unknown tool: ${name}` }],
252
+ isError: true,
253
+ };
254
+ }
255
+ }
256
+ catch (error) {
257
+ const err = error;
258
+ return {
259
+ content: [
260
+ {
261
+ type: "text",
262
+ text: `Error: ${err.message || "An unexpected error occurred"}`,
263
+ },
264
+ ],
265
+ isError: true,
266
+ };
267
+ }
268
+ });
269
+ return server;
270
+ }
271
+ export async function startServer() {
272
+ const server = createServer();
273
+ const transport = new StdioServerTransport();
274
+ await server.connect(transport);
275
+ process.stderr.write("Thumbtack MCP server started\n");
276
+ }
@@ -0,0 +1,5 @@
1
+ import { Page, BrowserContext } from "playwright";
2
+ export declare function saveSession(page: Page, context: BrowserContext): Promise<void>;
3
+ export declare function loadSession(page: Page, context: BrowserContext): Promise<boolean>;
4
+ export declare function clearSession(): Promise<void>;
5
+ export declare function sessionExists(): Promise<boolean>;
@@ -0,0 +1,99 @@
1
+ import { promises as fs } from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ const SESSION_FILE = path.join(os.homedir(), ".thumbtack-session.json");
5
+ export async function saveSession(page, context) {
6
+ try {
7
+ // Collect cookies from the browser context
8
+ const cookies = await context.cookies();
9
+ // Collect localStorage from thumbtack domain
10
+ const localStorage = await page.evaluate(() => {
11
+ const data = {};
12
+ for (let i = 0; i < window.localStorage.length; i++) {
13
+ const key = window.localStorage.key(i);
14
+ if (key) {
15
+ const value = window.localStorage.getItem(key);
16
+ if (value !== null) {
17
+ data[key] = value;
18
+ }
19
+ }
20
+ }
21
+ return data;
22
+ });
23
+ const sessionData = {
24
+ cookies: cookies.map((c) => ({
25
+ name: c.name,
26
+ value: c.value,
27
+ domain: c.domain,
28
+ path: c.path,
29
+ expires: c.expires,
30
+ httpOnly: c.httpOnly,
31
+ secure: c.secure,
32
+ sameSite: c.sameSite || "Lax",
33
+ })),
34
+ localStorage,
35
+ savedAt: new Date().toISOString(),
36
+ };
37
+ await fs.writeFile(SESSION_FILE, JSON.stringify(sessionData, null, 2), {
38
+ mode: 0o600,
39
+ });
40
+ }
41
+ catch (error) {
42
+ const err = error;
43
+ throw new Error(`Failed to save session: ${err.message}`);
44
+ }
45
+ }
46
+ export async function loadSession(page, context) {
47
+ try {
48
+ const raw = await fs.readFile(SESSION_FILE, "utf-8");
49
+ const sessionData = JSON.parse(raw);
50
+ // Check session age - invalidate after 7 days
51
+ const savedAt = new Date(sessionData.savedAt).getTime();
52
+ const ageMs = Date.now() - savedAt;
53
+ const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
54
+ if (ageMs > sevenDaysMs) {
55
+ await clearSession();
56
+ return false;
57
+ }
58
+ // Restore cookies
59
+ if (sessionData.cookies && sessionData.cookies.length > 0) {
60
+ await context.addCookies(sessionData.cookies);
61
+ }
62
+ // Navigate to thumbtack first so we can set localStorage
63
+ await page.goto("https://www.thumbtack.com", {
64
+ waitUntil: "domcontentloaded",
65
+ timeout: 30000,
66
+ });
67
+ // Restore localStorage
68
+ if (sessionData.localStorage &&
69
+ Object.keys(sessionData.localStorage).length > 0) {
70
+ await page.evaluate((data) => {
71
+ for (const [key, value] of Object.entries(data)) {
72
+ window.localStorage.setItem(key, value);
73
+ }
74
+ }, sessionData.localStorage);
75
+ }
76
+ return true;
77
+ }
78
+ catch (error) {
79
+ // Session file doesn't exist or is invalid
80
+ return false;
81
+ }
82
+ }
83
+ export async function clearSession() {
84
+ try {
85
+ await fs.unlink(SESSION_FILE);
86
+ }
87
+ catch {
88
+ // File may not exist, that's fine
89
+ }
90
+ }
91
+ export async function sessionExists() {
92
+ try {
93
+ await fs.access(SESSION_FILE);
94
+ return true;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
@@ -0,0 +1,27 @@
1
+ export interface GetProjectsParams {
2
+ status?: "active" | "completed" | "all";
3
+ }
4
+ export interface Project {
5
+ id: string;
6
+ title: string;
7
+ providerName: string;
8
+ providerUrl: string | null;
9
+ providerImageUrl: string | null;
10
+ serviceType: string;
11
+ status: string;
12
+ startDate: string | null;
13
+ completedDate: string | null;
14
+ price: string | null;
15
+ location: string | null;
16
+ description: string | null;
17
+ hasReview: boolean;
18
+ rating: number | null;
19
+ projectUrl: string | null;
20
+ }
21
+ export interface GetProjectsResult {
22
+ projects: Project[];
23
+ totalCount: number;
24
+ requiresLogin: boolean;
25
+ message: string;
26
+ }
27
+ export declare function getProjects(params: GetProjectsParams): Promise<GetProjectsResult>;