@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,234 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function requestQuote(params) {
4
+ const manager = getBrowserManager();
5
+ await manager.init();
6
+ const page = await manager.getPage();
7
+ const context = manager.getContext();
8
+ const sessionLoaded = await loadSession(page, context);
9
+ // Navigate to provider profile
10
+ const providerUrl = `https://www.thumbtack.com/pro/${params.provider_id}`;
11
+ await page.goto(providerUrl, {
12
+ waitUntil: "domcontentloaded",
13
+ timeout: 30000,
14
+ });
15
+ await page.waitForTimeout(2000);
16
+ // Check for auth wall
17
+ const requiresLogin = await page.evaluate(() => {
18
+ const loginModal = document.querySelector('[data-test="login-modal"], [class*="AuthModal"], [class*="LoginWall"]');
19
+ const loginPage = window.location.href.includes("/login") ||
20
+ window.location.href.includes("/signup");
21
+ return !!loginModal || loginPage;
22
+ });
23
+ if (requiresLogin && !sessionLoaded) {
24
+ return {
25
+ success: false,
26
+ message: "Authentication required. Please log in to Thumbtack first. Visit https://www.thumbtack.com/login to sign in, then try again.",
27
+ quoteId: null,
28
+ providerName: null,
29
+ estimatedResponseTime: null,
30
+ requiresLogin: true,
31
+ };
32
+ }
33
+ // Get provider name for confirmation
34
+ const providerName = await page.evaluate(() => {
35
+ return document.querySelector("h1")?.textContent?.trim() || null;
36
+ });
37
+ // Find and click the "Get a quote" or "Contact" button
38
+ try {
39
+ const quoteButtonSelectors = [
40
+ 'button[data-test="contact-button"]',
41
+ 'button[data-test="get-quote-button"]',
42
+ 'a[data-test="contact-cta"]',
43
+ 'button:has-text("Get a quote")',
44
+ 'button:has-text("Contact")',
45
+ 'button:has-text("Request quote")',
46
+ '[class*="ContactButton"]',
47
+ '[class*="QuoteButton"]',
48
+ ];
49
+ let clicked = false;
50
+ for (const selector of quoteButtonSelectors) {
51
+ try {
52
+ const btn = await page.$(selector);
53
+ if (btn) {
54
+ await btn.click();
55
+ clicked = true;
56
+ break;
57
+ }
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ }
63
+ if (!clicked) {
64
+ // Try text-based approach
65
+ await page.locator('button', { hasText: /get a quote|contact|request/i }).first().click();
66
+ }
67
+ await page.waitForTimeout(2000);
68
+ }
69
+ catch {
70
+ return {
71
+ success: false,
72
+ message: "Could not find the quote request button on this provider's page. The page structure may have changed.",
73
+ quoteId: null,
74
+ providerName,
75
+ estimatedResponseTime: null,
76
+ requiresLogin: false,
77
+ };
78
+ }
79
+ // Fill out the quote request form
80
+ try {
81
+ // Wait for form to appear
82
+ await page.waitForSelector('textarea, input[type="text"], [class*="QuoteForm"]', { timeout: 5000 });
83
+ // Fill description/project details
84
+ const descriptionSelectors = [
85
+ 'textarea[data-test="description"]',
86
+ 'textarea[name="description"]',
87
+ 'textarea[placeholder*="describe"]',
88
+ 'textarea[placeholder*="project"]',
89
+ "textarea",
90
+ ];
91
+ for (const selector of descriptionSelectors) {
92
+ try {
93
+ const textarea = await page.$(selector);
94
+ if (textarea) {
95
+ await textarea.click();
96
+ await textarea.fill(params.description);
97
+ break;
98
+ }
99
+ }
100
+ catch {
101
+ continue;
102
+ }
103
+ }
104
+ // Fill location if there's a location field
105
+ const locationSelectors = [
106
+ 'input[placeholder*="ZIP"]',
107
+ 'input[placeholder*="zip"]',
108
+ 'input[name="zipCode"]',
109
+ 'input[data-test="location"]',
110
+ ];
111
+ for (const selector of locationSelectors) {
112
+ try {
113
+ const input = await page.$(selector);
114
+ if (input) {
115
+ await input.click({ clickCount: 3 });
116
+ await input.fill(params.location);
117
+ break;
118
+ }
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ }
124
+ // Fill contact info if fields are available
125
+ if (params.contact_info.name) {
126
+ try {
127
+ const nameInput = await page.$('input[name="name"], input[placeholder*="name"]');
128
+ if (nameInput) {
129
+ await nameInput.fill(params.contact_info.name);
130
+ }
131
+ }
132
+ catch { }
133
+ }
134
+ if (params.contact_info.email) {
135
+ try {
136
+ const emailInput = await page.$('input[type="email"], input[name="email"]');
137
+ if (emailInput) {
138
+ await emailInput.fill(params.contact_info.email);
139
+ }
140
+ }
141
+ catch { }
142
+ }
143
+ if (params.contact_info.phone) {
144
+ try {
145
+ const phoneInput = await page.$('input[type="tel"], input[name="phone"]');
146
+ if (phoneInput) {
147
+ await phoneInput.fill(params.contact_info.phone);
148
+ }
149
+ }
150
+ catch { }
151
+ }
152
+ // Look for submit button
153
+ const submitSelectors = [
154
+ 'button[type="submit"]',
155
+ 'button[data-test="submit-quote"]',
156
+ 'button:has-text("Send request")',
157
+ 'button:has-text("Submit")',
158
+ 'button:has-text("Send")',
159
+ ];
160
+ let submitted = false;
161
+ for (const selector of submitSelectors) {
162
+ try {
163
+ const btn = await page.$(selector);
164
+ if (btn) {
165
+ await btn.click();
166
+ submitted = true;
167
+ break;
168
+ }
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ }
174
+ if (!submitted) {
175
+ return {
176
+ success: false,
177
+ message: "Form was filled but could not find submit button. The form may require additional steps.",
178
+ quoteId: null,
179
+ providerName,
180
+ estimatedResponseTime: null,
181
+ requiresLogin: false,
182
+ };
183
+ }
184
+ // Wait for confirmation
185
+ await page.waitForTimeout(3000);
186
+ // Check for success confirmation
187
+ const confirmationResult = await page.evaluate(() => {
188
+ const successEl = document.querySelector('[data-test="quote-sent"], [class*="SuccessMessage"], [class*="Confirmation"]');
189
+ const successText = document.body.textContent || "";
190
+ const isSuccess = !!successEl ||
191
+ successText.includes("request sent") ||
192
+ successText.includes("quote sent") ||
193
+ successText.includes("message sent") ||
194
+ successText.toLowerCase().includes("we'll notify");
195
+ const quoteIdEl = document.querySelector("[data-quote-id]");
196
+ const quoteId = quoteIdEl?.getAttribute("data-quote-id") || null;
197
+ const responseTimeEl = document.querySelector('[class*="ResponseTime"], [data-test="response-time"]');
198
+ const responseTime = responseTimeEl?.textContent?.trim() || null;
199
+ return { isSuccess, quoteId, responseTime };
200
+ });
201
+ await saveSession(page, context).catch(() => { });
202
+ if (confirmationResult.isSuccess) {
203
+ return {
204
+ success: true,
205
+ message: `Quote request successfully sent to ${providerName || "provider"}. They will be notified and should respond soon.`,
206
+ quoteId: confirmationResult.quoteId,
207
+ providerName,
208
+ estimatedResponseTime: confirmationResult.responseTime,
209
+ requiresLogin: false,
210
+ };
211
+ }
212
+ else {
213
+ return {
214
+ success: false,
215
+ message: "Quote request may have been submitted but confirmation could not be verified. Check your Thumbtack inbox.",
216
+ quoteId: null,
217
+ providerName,
218
+ estimatedResponseTime: null,
219
+ requiresLogin: false,
220
+ };
221
+ }
222
+ }
223
+ catch (error) {
224
+ const err = error;
225
+ return {
226
+ success: false,
227
+ message: `Error during quote request process: ${err.message}`,
228
+ quoteId: null,
229
+ providerName,
230
+ estimatedResponseTime: null,
231
+ requiresLogin: false,
232
+ };
233
+ }
234
+ }
@@ -0,0 +1,28 @@
1
+ export interface SearchServicesParams {
2
+ service_type: string;
3
+ location?: string;
4
+ zip_code?: string;
5
+ date?: string;
6
+ num_people?: number;
7
+ }
8
+ export interface ServiceProvider {
9
+ id: string;
10
+ name: string;
11
+ url: string;
12
+ rating: number | null;
13
+ reviewCount: number | null;
14
+ description: string;
15
+ priceRange: string | null;
16
+ verified: boolean;
17
+ responseTime: string | null;
18
+ hireCount: number | null;
19
+ location: string | null;
20
+ profileImageUrl: string | null;
21
+ }
22
+ export interface SearchResult {
23
+ query: string;
24
+ location: string;
25
+ providers: ServiceProvider[];
26
+ totalFound: number;
27
+ }
28
+ export declare function searchServices(params: SearchServicesParams): Promise<SearchResult>;
@@ -0,0 +1,152 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function searchServices(params) {
4
+ const manager = getBrowserManager();
5
+ await manager.init();
6
+ const page = await manager.getPage();
7
+ const context = manager.getContext();
8
+ // Try to load existing session
9
+ const sessionLoaded = await loadSession(page, context);
10
+ // Build search URL
11
+ const locationQuery = params.zip_code || params.location || "";
12
+ const encodedService = encodeURIComponent(params.service_type);
13
+ const encodedLocation = encodeURIComponent(locationQuery);
14
+ // Navigate to Thumbtack search
15
+ const searchUrl = `https://www.thumbtack.com/search/${encodedService}/cost`;
16
+ await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
17
+ // Wait for page to load and check if we need to fill in location
18
+ await page.waitForTimeout(2000);
19
+ // Handle location input if present
20
+ try {
21
+ const locationInput = await page.$('input[placeholder*="ZIP"], input[placeholder*="zip"], input[name="zipCode"], input[data-test="location-input"]');
22
+ if (locationInput && locationQuery) {
23
+ await locationInput.click({ clickCount: 3 });
24
+ await locationInput.fill(locationQuery);
25
+ await page.keyboard.press("Enter");
26
+ await page.waitForTimeout(2000);
27
+ }
28
+ }
29
+ catch {
30
+ // Location input may not be present on all search pages
31
+ }
32
+ // Also try the main search form approach
33
+ try {
34
+ const serviceInput = await page.$('input[placeholder*="service"], input[name="service"], input[data-test="service-input"]');
35
+ if (serviceInput) {
36
+ await serviceInput.click({ clickCount: 3 });
37
+ await serviceInput.fill(params.service_type);
38
+ }
39
+ const zipInput = await page.$('input[placeholder*="ZIP"], input[placeholder*="zip code"], input[name="zipCode"]');
40
+ if (zipInput && locationQuery) {
41
+ await zipInput.click({ clickCount: 3 });
42
+ await zipInput.fill(locationQuery);
43
+ }
44
+ const searchBtn = await page.$('button[type="submit"], button[data-test="search-button"]');
45
+ if (searchBtn && serviceInput) {
46
+ await searchBtn.click();
47
+ await page.waitForTimeout(3000);
48
+ }
49
+ }
50
+ catch {
51
+ // Search form approach failed, continue with what we have
52
+ }
53
+ // Wait for results to load
54
+ await page.waitForTimeout(3000);
55
+ // Extract provider data from the page
56
+ const providers = await page.evaluate(() => {
57
+ const results = [];
58
+ // Look for provider cards - Thumbtack uses various selectors
59
+ const cardSelectors = [
60
+ '[data-test="provider-card"]',
61
+ '[class*="ProviderCard"]',
62
+ '[class*="provider-card"]',
63
+ "article[class*='Card']",
64
+ '[data-testid="pro-card"]',
65
+ ".Grid__Item",
66
+ ];
67
+ let cards = [];
68
+ for (const selector of cardSelectors) {
69
+ const found = document.querySelectorAll(selector);
70
+ if (found.length > 0) {
71
+ cards = found;
72
+ break;
73
+ }
74
+ }
75
+ cards.forEach((card) => {
76
+ try {
77
+ // Extract name
78
+ const nameEl = card.querySelector('h3, h2, [data-test="pro-name"], [class*="name"], a[href*="/ca/"]');
79
+ const name = nameEl?.textContent?.trim() || "";
80
+ // Extract URL
81
+ const linkEl = card.querySelector('a[href*="/ca/"], a[href*="/pro/"]');
82
+ const url = linkEl?.href || "";
83
+ // Extract ID from URL
84
+ const idMatch = url.match(/\/(\d+)\/?$/);
85
+ const id = idMatch ? idMatch[1] : Math.random().toString(36).substr(2, 9);
86
+ // Extract rating
87
+ const ratingEl = card.querySelector('[class*="rating"], [aria-label*="star"], [data-test="rating"]');
88
+ const ratingText = ratingEl?.getAttribute("aria-label") || ratingEl?.textContent || "";
89
+ const ratingMatch = ratingText.match(/(\d+\.?\d*)/);
90
+ const rating = ratingMatch ? parseFloat(ratingMatch[1]) : null;
91
+ // Extract review count
92
+ const reviewEl = card.querySelector('[class*="review"], [data-test="review-count"]');
93
+ const reviewText = reviewEl?.textContent || "";
94
+ const reviewMatch = reviewText.match(/(\d+)/);
95
+ const reviewCount = reviewMatch ? parseInt(reviewMatch[1]) : null;
96
+ // Extract description/summary
97
+ const descEl = card.querySelector('[class*="description"], [class*="summary"], p');
98
+ const description = descEl?.textContent?.trim() || "";
99
+ // Extract price range
100
+ const priceEl = card.querySelector('[class*="price"], [data-test="price-range"]');
101
+ const priceRange = priceEl?.textContent?.trim() || null;
102
+ // Check verified badge
103
+ const verifiedEl = card.querySelector('[class*="verified"], [data-test="verified-badge"], [aria-label*="verified"]');
104
+ const verified = !!verifiedEl;
105
+ // Extract response time
106
+ const responseEl = card.querySelector('[class*="response"], [data-test="response-time"]');
107
+ const responseTime = responseEl?.textContent?.trim() || null;
108
+ // Extract hire count
109
+ const hireEl = card.querySelector('[class*="hire"], [data-test="hire-count"]');
110
+ const hireText = hireEl?.textContent || "";
111
+ const hireMatch = hireText.match(/(\d+)/);
112
+ const hireCount = hireMatch ? parseInt(hireMatch[1]) : null;
113
+ // Extract location
114
+ const locationEl = card.querySelector('[class*="location"], [data-test="location"]');
115
+ const location = locationEl?.textContent?.trim() || null;
116
+ // Extract profile image
117
+ const imgEl = card.querySelector("img");
118
+ const profileImageUrl = imgEl?.src || null;
119
+ if (name || url) {
120
+ results.push({
121
+ id,
122
+ name,
123
+ url,
124
+ rating,
125
+ reviewCount,
126
+ description,
127
+ priceRange,
128
+ verified,
129
+ responseTime,
130
+ hireCount,
131
+ location,
132
+ profileImageUrl,
133
+ });
134
+ }
135
+ }
136
+ catch {
137
+ // Skip malformed cards
138
+ }
139
+ });
140
+ return results;
141
+ });
142
+ // Save session after successful navigation
143
+ if (sessionLoaded || providers.length > 0) {
144
+ await saveSession(page, context).catch(() => { });
145
+ }
146
+ return {
147
+ query: params.service_type,
148
+ location: locationQuery,
149
+ providers: providers.slice(0, 20), // Limit to top 20 results
150
+ totalFound: providers.length,
151
+ };
152
+ }
@@ -0,0 +1,25 @@
1
+ export interface ViewQuotesParams {
2
+ status?: "pending" | "active" | "completed" | "archived" | "all";
3
+ }
4
+ export interface Quote {
5
+ id: string;
6
+ providerName: string;
7
+ providerUrl: string | null;
8
+ serviceType: string;
9
+ status: string;
10
+ price: string | null;
11
+ message: string | null;
12
+ date: string | null;
13
+ expiresAt: string | null;
14
+ providerRating: number | null;
15
+ providerReviewCount: number | null;
16
+ providerImageUrl: string | null;
17
+ unread: boolean;
18
+ }
19
+ export interface ViewQuotesResult {
20
+ quotes: Quote[];
21
+ totalCount: number;
22
+ requiresLogin: boolean;
23
+ message: string;
24
+ }
25
+ export declare function viewQuotes(params: ViewQuotesParams): Promise<ViewQuotesResult>;
@@ -0,0 +1,163 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function viewQuotes(params) {
4
+ const manager = getBrowserManager();
5
+ await manager.init();
6
+ const page = await manager.getPage();
7
+ const context = manager.getContext();
8
+ const sessionLoaded = await loadSession(page, context);
9
+ // Navigate to inbox/quotes section
10
+ await page.goto("https://www.thumbtack.com/inbox", {
11
+ waitUntil: "domcontentloaded",
12
+ timeout: 30000,
13
+ });
14
+ await page.waitForTimeout(3000);
15
+ // Check if redirected to login
16
+ const currentUrl = page.url();
17
+ const requiresLogin = currentUrl.includes("/login") ||
18
+ currentUrl.includes("/signup") ||
19
+ (!sessionLoaded && currentUrl.includes("thumbtack.com/login"));
20
+ if (requiresLogin) {
21
+ return {
22
+ quotes: [],
23
+ totalCount: 0,
24
+ requiresLogin: true,
25
+ message: "Authentication required. Please log in to Thumbtack first. Visit https://www.thumbtack.com/login to sign in.",
26
+ };
27
+ }
28
+ // Apply status filter if provided
29
+ if (params.status && params.status !== "all") {
30
+ const filterMap = {
31
+ pending: "pending",
32
+ active: "active",
33
+ completed: "completed",
34
+ archived: "archived",
35
+ };
36
+ const filterValue = filterMap[params.status];
37
+ if (filterValue) {
38
+ try {
39
+ // Look for filter tabs/buttons
40
+ const filterSelectors = [
41
+ `[data-test="filter-${filterValue}"]`,
42
+ `button:has-text("${filterValue}")`,
43
+ `a:has-text("${filterValue}")`,
44
+ `[role="tab"]:has-text("${filterValue}")`,
45
+ ];
46
+ for (const selector of filterSelectors) {
47
+ try {
48
+ const btn = await page.$(selector);
49
+ if (btn) {
50
+ await btn.click();
51
+ await page.waitForTimeout(1500);
52
+ break;
53
+ }
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Filter may not be available
62
+ }
63
+ }
64
+ }
65
+ // Extract quotes from the page
66
+ const quotes = await page.evaluate(() => {
67
+ const results = [];
68
+ // Look for quote/message cards in the inbox
69
+ const cardSelectors = [
70
+ '[data-test="inbox-item"]',
71
+ '[data-test="quote-card"]',
72
+ '[class*="InboxItem"]',
73
+ '[class*="QuoteCard"]',
74
+ '[class*="MessageThread"]',
75
+ '[class*="ConversationItem"]',
76
+ ];
77
+ let cards = [];
78
+ for (const selector of cardSelectors) {
79
+ const found = document.querySelectorAll(selector);
80
+ if (found.length > 0) {
81
+ cards = found;
82
+ break;
83
+ }
84
+ }
85
+ cards.forEach((card) => {
86
+ try {
87
+ // Extract provider name
88
+ const providerNameEl = card.querySelector('[class*="ProviderName"], [data-test="pro-name"], h3, h2');
89
+ const providerName = providerNameEl?.textContent?.trim() || "Unknown Provider";
90
+ // Extract provider URL
91
+ const linkEl = card.querySelector('a[href*="/ca/"], a[href*="/pro/"]');
92
+ const providerUrl = linkEl?.href || null;
93
+ // Extract quote ID from URL or data attribute
94
+ const idEl = card.querySelector("[data-quote-id], [data-id]");
95
+ const idFromData = idEl?.getAttribute("data-quote-id") || idEl?.getAttribute("data-id");
96
+ const idFromUrl = linkEl?.href?.match(/\/(\d+)\/?/)?.[1];
97
+ const id = idFromData || idFromUrl || Math.random().toString(36).substr(2, 9);
98
+ // Extract service type
99
+ const serviceTypeEl = card.querySelector('[class*="ServiceType"], [data-test="service-type"], [class*="Category"]');
100
+ const serviceType = serviceTypeEl?.textContent?.trim() || "Service";
101
+ // Extract status
102
+ const statusEl = card.querySelector('[class*="Status"], [data-test="status"], [class*="badge"]');
103
+ const status = statusEl?.textContent?.trim() || "unknown";
104
+ // Extract price
105
+ const priceEl = card.querySelector('[class*="Price"], [data-test="price"], [class*="amount"]');
106
+ const price = priceEl?.textContent?.trim() || null;
107
+ // Extract message preview
108
+ const messageEl = card.querySelector('[class*="Preview"], [class*="Snippet"], [data-test="message-preview"], p');
109
+ const message = messageEl?.textContent?.trim() || null;
110
+ // Extract date
111
+ const dateEl = card.querySelector('time, [class*="Date"], [data-test="date"]');
112
+ const date = dateEl?.getAttribute("datetime") || dateEl?.textContent?.trim() || null;
113
+ // Extract expiry
114
+ const expiresEl = card.querySelector('[class*="Expires"], [data-test="expires"]');
115
+ const expiresAt = expiresEl?.textContent?.trim() || null;
116
+ // Provider rating
117
+ const ratingEl = card.querySelector('[aria-label*="star"], [class*="Rating"]');
118
+ const ratingText = ratingEl?.getAttribute("aria-label") || ratingEl?.textContent || "";
119
+ const ratingMatch = ratingText.match(/(\d+\.?\d*)/);
120
+ const providerRating = ratingMatch ? parseFloat(ratingMatch[1]) : null;
121
+ // Provider review count
122
+ const reviewEl = card.querySelector('[class*="ReviewCount"]');
123
+ const reviewText = reviewEl?.textContent || "";
124
+ const reviewMatch = reviewText.match(/(\d+)/);
125
+ const providerReviewCount = reviewMatch ? parseInt(reviewMatch[1]) : null;
126
+ // Provider image
127
+ const imgEl = card.querySelector("img");
128
+ const providerImageUrl = imgEl?.src || null;
129
+ // Check if unread
130
+ const unread = card.classList.contains("unread") ||
131
+ !!card.querySelector('[class*="unread"], [class*="Unread"]');
132
+ results.push({
133
+ id,
134
+ providerName,
135
+ providerUrl,
136
+ serviceType,
137
+ status,
138
+ price,
139
+ message,
140
+ date,
141
+ expiresAt,
142
+ providerRating,
143
+ providerReviewCount,
144
+ providerImageUrl,
145
+ unread,
146
+ });
147
+ }
148
+ catch {
149
+ // Skip malformed cards
150
+ }
151
+ });
152
+ return results;
153
+ });
154
+ await saveSession(page, context).catch(() => { });
155
+ return {
156
+ quotes,
157
+ totalCount: quotes.length,
158
+ requiresLogin: false,
159
+ message: quotes.length > 0
160
+ ? `Found ${quotes.length} quote(s) in your inbox.`
161
+ : "No quotes found in your inbox for the specified filter.",
162
+ };
163
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@striderlabs/mcp-thumbtack",
3
+ "version": "1.0.0",
4
+ "description": "MCP connector for Thumbtack home services marketplace",
5
+ "author": "Strider Labs <hello@striderlabs.ai>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "mcp-thumbtack": "dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "dev": "ts-node src/index.ts"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.0.0",
19
+ "playwright": "^1.40.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "typescript": "^5.0.0"
24
+ },
25
+ "files": [
26
+ "dist/",
27
+ "server.json",
28
+ "README.md"
29
+ ]
30
+ }
package/server.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpName": "io.github.markswendsen-code/mcp-thumbtack",
3
+ "version": "1.0.0",
4
+ "description": "MCP connector for Thumbtack home services marketplace",
5
+ "author": "Strider Labs <hello@striderlabs.ai>",
6
+ "tools": [
7
+ "thumbtack_search_services",
8
+ "thumbtack_get_provider",
9
+ "thumbtack_request_quote",
10
+ "thumbtack_view_quotes",
11
+ "thumbtack_hire_provider",
12
+ "thumbtack_get_projects"
13
+ ]
14
+ }