scrapeless-mcp-server 0.4.9 → 0.5.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 +30 -25
- package/package.json +3 -2
- package/build/cf.js +0 -27
- package/build/config.js +0 -11
- package/build/context-manager.js +0 -40
- package/build/context.js +0 -120
- package/build/index.js +0 -23
- package/build/server.js +0 -60
- package/build/session-manager.js +0 -76
- package/build/tools/browser/browser.js +0 -488
- package/build/tools/deepserp/googleSearch.js +0 -37
- package/build/tools/deepserp/googleTrends.js +0 -208
- package/build/tools/index.js +0 -5
- package/build/tools/universal/scrapeHtml.js +0 -22
- package/build/tools/universal/scrapeMarkdown.js +0 -29
- package/build/tools/universal/scrapeScreenshot.js +0 -48
- package/build/tools/utils.js +0 -86
- package/build/transport.js +0 -120
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
export const googleTrends = defineTool({
|
|
4
|
-
name: 'google_trends',
|
|
5
|
-
description: `Get trending search data from Google Trends.
|
|
6
|
-
Restrictions: Activated for queries about trends, popularity, or interest over time.
|
|
7
|
-
Valid: Find the search interest for "AI" over the last year.
|
|
8
|
-
Invalid: A general question like "What is AI?" (use google_search).`,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
q: z
|
|
11
|
-
.string()
|
|
12
|
-
.describe('Parameter defines the query or queries you want to search. You can use anything that you would use in a regular Google Trends search. The maximum number of queries per search is 5 (this only applies to interest_over_time and compared_breakdown_by_region data_type, other types of data will only accept 1 query per search).')
|
|
13
|
-
.default('Mercedes-Benz,BMW X5'),
|
|
14
|
-
data_type: z
|
|
15
|
-
.union([
|
|
16
|
-
z.literal('autocomplete').describe('Auto complete'),
|
|
17
|
-
z.literal('interest_over_time').describe('Interest over time'),
|
|
18
|
-
z.literal('compared_breakdown_by_region').describe('Compared breakdown by region'),
|
|
19
|
-
z.literal('interest_by_subregion').describe('Interest by region'),
|
|
20
|
-
z.literal('related_queries').describe('Related queries'),
|
|
21
|
-
z.literal('related_topics').describe('Related topics')
|
|
22
|
-
])
|
|
23
|
-
.describe('The supported types are: autocomplete,interest_over_time,compared_breakdown_by_region,interest_by_subregion,related_queries,related_topics.')
|
|
24
|
-
.default('interest_over_time'),
|
|
25
|
-
date: z
|
|
26
|
-
.string()
|
|
27
|
-
.describe('The supported dates are: now 1-H, now 4-H, now 1-d, now 7-d, today 1-m, today 3-m, today 12-m, today 5-y, all.\n\nYou can also pass custom values:\n\nDates from 2004 to present: yyyy-mm-dd yyyy-mm-dd (e.g. 2021-10-15 2022-05-25)\nDates with hours within a week range: yyyy-mm-ddThh yyyy-mm-ddThh (e.g. 2022-05-19T10 2022-05-24T22). Hours will be calculated depending on the tz (time zone) parameter.')
|
|
28
|
-
.default('today 1-m'),
|
|
29
|
-
hl: z
|
|
30
|
-
.string()
|
|
31
|
-
.describe("Parameter defines the language to use for the Google Trends search. It's a two-letter language code. (e.g., en for English, es for Spanish, or fr for French).")
|
|
32
|
-
.default('en'),
|
|
33
|
-
tz: z.string().describe('time zone offset. default is 420.').default('420'),
|
|
34
|
-
geo: z
|
|
35
|
-
.union([
|
|
36
|
-
z.literal(''),
|
|
37
|
-
z.literal('AR'),
|
|
38
|
-
z.literal('AU'),
|
|
39
|
-
z.literal('AT'),
|
|
40
|
-
z.literal('BE'),
|
|
41
|
-
z.literal('BR'),
|
|
42
|
-
z.literal('CA'),
|
|
43
|
-
z.literal('CL'),
|
|
44
|
-
z.literal('CO'),
|
|
45
|
-
z.literal('CZ'),
|
|
46
|
-
z.literal('DK'),
|
|
47
|
-
z.literal('EG'),
|
|
48
|
-
z.literal('FI'),
|
|
49
|
-
z.literal('FR'),
|
|
50
|
-
z.literal('DE'),
|
|
51
|
-
z.literal('GR'),
|
|
52
|
-
z.literal('HK'),
|
|
53
|
-
z.literal('HU'),
|
|
54
|
-
z.literal('IN'),
|
|
55
|
-
z.literal('ID'),
|
|
56
|
-
z.literal('IE'),
|
|
57
|
-
z.literal('IL'),
|
|
58
|
-
z.literal('IT'),
|
|
59
|
-
z.literal('JP'),
|
|
60
|
-
z.literal('KE'),
|
|
61
|
-
z.literal('MY'),
|
|
62
|
-
z.literal('MX'),
|
|
63
|
-
z.literal('NL'),
|
|
64
|
-
z.literal('NZ'),
|
|
65
|
-
z.literal('NG'),
|
|
66
|
-
z.literal('NO'),
|
|
67
|
-
z.literal('PE'),
|
|
68
|
-
z.literal('PH'),
|
|
69
|
-
z.literal('PL'),
|
|
70
|
-
z.literal('PT'),
|
|
71
|
-
z.literal('RO'),
|
|
72
|
-
z.literal('RU'),
|
|
73
|
-
z.literal('SA'),
|
|
74
|
-
z.literal('SG'),
|
|
75
|
-
z.literal('ZA'),
|
|
76
|
-
z.literal('KR'),
|
|
77
|
-
z.literal('ES'),
|
|
78
|
-
z.literal('SE'),
|
|
79
|
-
z.literal('CH'),
|
|
80
|
-
z.literal('TW'),
|
|
81
|
-
z.literal('TH'),
|
|
82
|
-
z.literal('TR'),
|
|
83
|
-
z.literal('UA'),
|
|
84
|
-
z.literal('GB'),
|
|
85
|
-
z.literal('US'),
|
|
86
|
-
z.literal('VN')
|
|
87
|
-
])
|
|
88
|
-
.describe('Parameter defines the location from where you want the search to originate. It defaults to Worldwide (activated when the value of geo parameter is not set or empty).')
|
|
89
|
-
.optional(),
|
|
90
|
-
cat: z
|
|
91
|
-
.union([
|
|
92
|
-
z.literal('0').describe('All categories'),
|
|
93
|
-
z.literal('3').describe('Arts & Entertainment'),
|
|
94
|
-
z.literal('5').describe('Computers & Electronics'),
|
|
95
|
-
z.literal('7').describe('Finance'),
|
|
96
|
-
z.literal('8').describe('Games'),
|
|
97
|
-
z.literal('11').describe('Home & Garden'),
|
|
98
|
-
z.literal('12').describe('Business & Industrial'),
|
|
99
|
-
z.literal('13').describe('Internet & Telecom'),
|
|
100
|
-
z.literal('14').describe('People & Society'),
|
|
101
|
-
z.literal('16').describe('News'),
|
|
102
|
-
z.literal('18').describe('Shopping'),
|
|
103
|
-
z.literal('19').describe('Law & Government'),
|
|
104
|
-
z.literal('20').describe('Sports'),
|
|
105
|
-
z.literal('22').describe('Books & Literature'),
|
|
106
|
-
z.literal('23').describe('Performing Arts'),
|
|
107
|
-
z.literal('24').describe('Visual Art & Design'),
|
|
108
|
-
z.literal('25').describe('Advertising & Marketing'),
|
|
109
|
-
z.literal('28').describe('Office Services'),
|
|
110
|
-
z.literal('29').describe('Real Estate'),
|
|
111
|
-
z.literal('30').describe('Computer Hardware'),
|
|
112
|
-
z.literal('31').describe('Programming'),
|
|
113
|
-
z.literal('32').describe('Software'),
|
|
114
|
-
z.literal('33').describe('Offbeat'),
|
|
115
|
-
z.literal('34').describe('Movies'),
|
|
116
|
-
z.literal('35').describe('Music & Audio'),
|
|
117
|
-
z.literal('36').describe('TV & Video'),
|
|
118
|
-
z.literal('37').describe('Banking'),
|
|
119
|
-
z.literal('38').describe('Insurance'),
|
|
120
|
-
z.literal('39').describe('Card Games'),
|
|
121
|
-
z.literal('41').describe('Computer & Video Games'),
|
|
122
|
-
z.literal('42').describe('Jazz'),
|
|
123
|
-
z.literal('43').describe('Online Goodies'),
|
|
124
|
-
z.literal('44').describe('Beauty & Fitness'),
|
|
125
|
-
z.literal('45').describe('Health'),
|
|
126
|
-
z.literal('46').describe('Agriculture & Forestry'),
|
|
127
|
-
z.literal('47').describe('Autos & Vehicles'),
|
|
128
|
-
z.literal('48').describe('Construction & Maintenance'),
|
|
129
|
-
z.literal('49').describe('Manufacturing'),
|
|
130
|
-
z.literal('50').describe('Transportation & Logistics'),
|
|
131
|
-
z.literal('53').describe('Web Hosting & Domain Registration'),
|
|
132
|
-
z.literal('54').describe('Social Issues & Advocacy'),
|
|
133
|
-
z.literal('55').describe('Dating & Personals'),
|
|
134
|
-
z.literal('56').describe('Ethnic & Identity Groups'),
|
|
135
|
-
z.literal('57').describe('Charity & Philanthropy'),
|
|
136
|
-
z.literal('58').describe('Parenting'),
|
|
137
|
-
z.literal('59').describe('Religion & Belief'),
|
|
138
|
-
z.literal('60').describe('Jobs'),
|
|
139
|
-
z.literal('61').describe('Classifieds'),
|
|
140
|
-
z.literal('63').describe('Weather'),
|
|
141
|
-
z.literal('64').describe('Antiques & Collectibles'),
|
|
142
|
-
z.literal('65').describe('Hobbies & Leisure'),
|
|
143
|
-
z.literal('66').describe('Pets & Animals'),
|
|
144
|
-
z.literal('67').describe('Travel'),
|
|
145
|
-
z.literal('68').describe('Apparel'),
|
|
146
|
-
z.literal('69').describe('Consumer Resources'),
|
|
147
|
-
z.literal('70').describe('Gifts & Special Event Items'),
|
|
148
|
-
z.literal('71').describe('Food & Drink'),
|
|
149
|
-
z.literal('73').describe('Mass Merchants & Department Stores'),
|
|
150
|
-
z.literal('74').describe('Education'),
|
|
151
|
-
z.literal('75').describe('Legal'),
|
|
152
|
-
z.literal('76').describe('Government'),
|
|
153
|
-
z.literal('77').describe('Enterprise Technology'),
|
|
154
|
-
z.literal('78').describe('Consumer Electronics'),
|
|
155
|
-
z.literal('82').describe('Environmental Issues'),
|
|
156
|
-
z.literal('83').describe('Marketing Services'),
|
|
157
|
-
z.literal('84').describe('Search Engine Optimization & Marketing'),
|
|
158
|
-
z.literal('89').describe('Vehicle Parts & Accessories'),
|
|
159
|
-
z.literal('91').describe('Stereo Systems & Components'),
|
|
160
|
-
z.literal('93').describe('Skin & Nail Care'),
|
|
161
|
-
z.literal('94').describe('Fitness'),
|
|
162
|
-
z.literal('95').describe('Office Supplies'),
|
|
163
|
-
z.literal('96').describe('Real Estate Agencies'),
|
|
164
|
-
z.literal('97').describe('Consumer Advocacy & Protection'),
|
|
165
|
-
z.literal('98').describe('Fashion Designers & Collections'),
|
|
166
|
-
z.literal('99').describe('Gifts'),
|
|
167
|
-
z.literal('100').describe('Cards & Greetings'),
|
|
168
|
-
z.literal('101').describe('Spirituality'),
|
|
169
|
-
z.literal('102').describe('Personals'),
|
|
170
|
-
z.literal('104').describe('ISPs'),
|
|
171
|
-
z.literal('105').describe('Online Games'),
|
|
172
|
-
z.literal('107').describe('Investing'),
|
|
173
|
-
z.literal('108').describe('Language Resources'),
|
|
174
|
-
z.literal('112').describe('Broadcast & Network News'),
|
|
175
|
-
z.literal('113').describe('Gay-Lesbian-Bisexual-Transgender'),
|
|
176
|
-
z.literal('115').describe('Baby Care & Hygiene'),
|
|
177
|
-
z.literal('118').describe('Water Sports'),
|
|
178
|
-
z.literal('119').describe('Wildlife'),
|
|
179
|
-
z.literal('120').describe('Cookware & Diningware'),
|
|
180
|
-
z.literal('121').describe('Grocery & Food Retailers'),
|
|
181
|
-
z.literal('122').describe('Cooking & Recipes'),
|
|
182
|
-
z.literal('123').describe('Tobacco Products'),
|
|
183
|
-
z.literal('124').describe('Clothing Accessories'),
|
|
184
|
-
z.literal('137').describe('Homemaking & Interior Decor'),
|
|
185
|
-
z.literal('138').describe('Vehicle Maintenance'),
|
|
186
|
-
z.literal('143').describe('Face & Body Care'),
|
|
187
|
-
z.literal('144').describe('Unwanted Body & Facial Hair Removal'),
|
|
188
|
-
z.literal('145').describe('Spas & Beauty Services'),
|
|
189
|
-
z.literal('146').describe('Hair Care'),
|
|
190
|
-
z.literal('147').describe('Cosmetology & Beauty Professionals'),
|
|
191
|
-
z.literal('148').describe('Off-Road Vehicles'),
|
|
192
|
-
z.literal('154').describe('Kids & Teens'),
|
|
193
|
-
z.literal('157').describe('Human Resources'),
|
|
194
|
-
z.literal('158').describe('Home Improvement'),
|
|
195
|
-
z.literal('166').describe('Public Safety'),
|
|
196
|
-
z.literal('168').describe('Emergency Services'),
|
|
197
|
-
z.literal('170').describe('Vehicle Licensing & Registration')
|
|
198
|
-
])
|
|
199
|
-
.describe('Parameter is used to define a search category. The default value is set to 0 ("All categories").')
|
|
200
|
-
.default('0')
|
|
201
|
-
},
|
|
202
|
-
handle: async (params, client) => {
|
|
203
|
-
return wrapMcpResponse(() => client.deepserp.scrape({
|
|
204
|
-
actor: 'scraper.google.trends',
|
|
205
|
-
input: params
|
|
206
|
-
}));
|
|
207
|
-
}
|
|
208
|
-
});
|
package/build/tools/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
export const scrapeHtml = defineTool({
|
|
4
|
-
name: 'scrape_html',
|
|
5
|
-
description: `Scrape a URL and return its full HTML content.
|
|
6
|
-
Restrictions: Activated for URLs that require JavaScript rendering or bot protection.
|
|
7
|
-
Valid: Get HTML from a dynamic, JS-heavy single-page application.
|
|
8
|
-
Invalid: Fetching a simple static page (use a standard HTTP client).`,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
url: z.string().url().describe('target URL'),
|
|
11
|
-
},
|
|
12
|
-
handle: async (params, client) => {
|
|
13
|
-
return wrapMcpResponse(() => client.universal.scrape({
|
|
14
|
-
actor: 'unlocker.webunlocker',
|
|
15
|
-
input: {
|
|
16
|
-
url: params.url,
|
|
17
|
-
js_render: true,
|
|
18
|
-
response_type: 'html'
|
|
19
|
-
}
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
22
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
export const scrapeMarkdown = defineTool({
|
|
4
|
-
name: "scrape_markdown",
|
|
5
|
-
description: `Scrape a URL and return its content as Markdown.
|
|
6
|
-
Restrictions: Best for articles, blog posts, and other text-heavy pages.
|
|
7
|
-
Valid: Scrape a news article to get its readable content.
|
|
8
|
-
Invalid: Scrape a complex web application dashboard.`,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
url: z.string().url().describe("target URL"),
|
|
11
|
-
},
|
|
12
|
-
handle: async (params, client) => {
|
|
13
|
-
return wrapMcpResponse(() => client.universal.scrape({
|
|
14
|
-
actor: "unlocker.webunlocker",
|
|
15
|
-
input: {
|
|
16
|
-
url: params.url,
|
|
17
|
-
redirect: true,
|
|
18
|
-
jsRender: {
|
|
19
|
-
enabled: true,
|
|
20
|
-
headless: true,
|
|
21
|
-
waitUntil: "domcontentloaded",
|
|
22
|
-
instructions: [],
|
|
23
|
-
block: { resources: [], urls: [] },
|
|
24
|
-
response: { type: "markdown", options: { selector: "" } },
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
},
|
|
29
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { defineTool } from "../utils.js";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
export const scrapeScreenshot = defineTool({
|
|
4
|
-
name: 'scrape_screenshot',
|
|
5
|
-
description: `Capture a high-quality screenshot of any webpage.
|
|
6
|
-
Restrictions: Bypasses bot detection and CAPTCHAs using residential proxies.
|
|
7
|
-
Valid: Get a screenshot of a price-checker page protected by Cloudflare.
|
|
8
|
-
Invalid: Taking a screenshot of the local browser (use browser_screenshot)`,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
url: z.string().url().describe('target URL'),
|
|
11
|
-
},
|
|
12
|
-
handle: async (params, client) => {
|
|
13
|
-
try {
|
|
14
|
-
const result = await client.universal.scrape({
|
|
15
|
-
actor: 'unlocker.webunlocker',
|
|
16
|
-
input: {
|
|
17
|
-
url: params.url,
|
|
18
|
-
js_render: true,
|
|
19
|
-
response_type: "png",
|
|
20
|
-
response_image_full_page: true
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
return {
|
|
24
|
-
content: [
|
|
25
|
-
{
|
|
26
|
-
type: "image",
|
|
27
|
-
data: result,
|
|
28
|
-
mimeType: "image/png",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
type: "text",
|
|
32
|
-
text: result,
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: "text",
|
|
42
|
-
text: `Failed to fetch data. Error: ${error.message}`,
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
});
|
package/build/tools/utils.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { customAlphabet } from "nanoid";
|
|
2
|
-
const notRecordRoleList = [
|
|
3
|
-
'Page Title',
|
|
4
|
-
'Root Web Area',
|
|
5
|
-
'RootWebArea',
|
|
6
|
-
'generic',
|
|
7
|
-
'none',
|
|
8
|
-
'paragraph',
|
|
9
|
-
'InlineTextBox',
|
|
10
|
-
'image'
|
|
11
|
-
];
|
|
12
|
-
function assembleSnapshot(snap, indent = 0) {
|
|
13
|
-
let str = '';
|
|
14
|
-
const spaces = ' '.repeat(indent);
|
|
15
|
-
for (const child of snap.children || []) {
|
|
16
|
-
if (child.role === 'Page Title')
|
|
17
|
-
continue;
|
|
18
|
-
if (child.role === 'Root Web Area')
|
|
19
|
-
continue;
|
|
20
|
-
if (!notRecordRoleList.includes(child.role)) {
|
|
21
|
-
str += `${spaces}- ${child.role}: ${child.name}\n`;
|
|
22
|
-
}
|
|
23
|
-
if (child.role === 'image') {
|
|
24
|
-
str += `${spaces} - image\n`;
|
|
25
|
-
}
|
|
26
|
-
if (child.children && child.children.length > 0) {
|
|
27
|
-
str += assembleSnapshot(child, !notRecordRoleList.includes(child.role) ? indent + 2 : indent);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return str;
|
|
31
|
-
}
|
|
32
|
-
export function defineTool(tool) {
|
|
33
|
-
return tool;
|
|
34
|
-
}
|
|
35
|
-
export async function wrapMcpResponse(request) {
|
|
36
|
-
try {
|
|
37
|
-
const data = await request();
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: "text",
|
|
42
|
-
text: `Response:\n\n${JSON.stringify(data)}`,
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
return {
|
|
49
|
-
content: [
|
|
50
|
-
{
|
|
51
|
-
type: "text",
|
|
52
|
-
text: `Failed to fetch data. Error: ${error.message}`,
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export function wrapMcpBrowserResponse(content) {
|
|
59
|
-
return {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
type: "text",
|
|
63
|
-
text: content,
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
export function uuid() {
|
|
69
|
-
const nanoid1 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
70
|
-
const nanoid2 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
71
|
-
const nanoid3 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
72
|
-
const nanoid4 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 6);
|
|
73
|
-
return `${nanoid1()}-${nanoid2()}-${nanoid3()}-${nanoid4()}`;
|
|
74
|
-
}
|
|
75
|
-
export async function snapshot(page) {
|
|
76
|
-
const snap = await page.accessibility.snapshot({ interestingOnly: false, includeIframes: true });
|
|
77
|
-
if (!snap) {
|
|
78
|
-
return "No accessibility snapshot available.";
|
|
79
|
-
}
|
|
80
|
-
const url = page.url();
|
|
81
|
-
const title = await page.title();
|
|
82
|
-
let snapshotString = `- Page URL: ${url}\n`;
|
|
83
|
-
snapshotString += `- Page Title: ${title}\n`;
|
|
84
|
-
snapshotString += `\`\`\`yaml\n- Page Snapshot\n\`\`\`yaml`;
|
|
85
|
-
return snapshotString + assembleSnapshot(snap, 0);
|
|
86
|
-
}
|
package/build/transport.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
-
import { createMcpServer, ServerList } from "./server.js";
|
|
8
|
-
import { API_KEY_NAME } from "./config.js";
|
|
9
|
-
export async function startStdioTransport() {
|
|
10
|
-
const serverList = new ServerList(async () => createMcpServer());
|
|
11
|
-
const server = await serverList.create();
|
|
12
|
-
await server.connect(new StdioServerTransport());
|
|
13
|
-
}
|
|
14
|
-
async function handleSSE(req, res, url, sessions) {
|
|
15
|
-
const serverList = createRemoteServerList(req, res);
|
|
16
|
-
if (!serverList)
|
|
17
|
-
return;
|
|
18
|
-
if (req.method === "POST") {
|
|
19
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
20
|
-
if (!sessionId) {
|
|
21
|
-
res.statusCode = 400;
|
|
22
|
-
return res.end("Missing sessionId");
|
|
23
|
-
}
|
|
24
|
-
const transport = sessions.get(sessionId);
|
|
25
|
-
if (!transport) {
|
|
26
|
-
res.statusCode = 404;
|
|
27
|
-
return res.end("Session not found");
|
|
28
|
-
}
|
|
29
|
-
return await transport.handlePostMessage(req, res);
|
|
30
|
-
}
|
|
31
|
-
else if (req.method === "GET") {
|
|
32
|
-
const transport = new SSEServerTransport("/sse", res);
|
|
33
|
-
sessions.set(transport.sessionId, transport);
|
|
34
|
-
const server = await serverList.create();
|
|
35
|
-
res.on("close", () => {
|
|
36
|
-
sessions.delete(transport.sessionId);
|
|
37
|
-
serverList.close(server).catch((e) => {
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
// console.error(e);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
return await server.connect(transport);
|
|
43
|
-
}
|
|
44
|
-
res.statusCode = 405;
|
|
45
|
-
res.end("Method not allowed");
|
|
46
|
-
}
|
|
47
|
-
async function handleStreamable(req, res, sessions) {
|
|
48
|
-
const serverList = createRemoteServerList(req, res);
|
|
49
|
-
if (!serverList)
|
|
50
|
-
return;
|
|
51
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
52
|
-
if (sessionId) {
|
|
53
|
-
const transport = sessions.get(sessionId);
|
|
54
|
-
if (!transport) {
|
|
55
|
-
res.statusCode = 404;
|
|
56
|
-
res.end("Session not found");
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
return await transport.handleRequest(req, res);
|
|
60
|
-
}
|
|
61
|
-
if (req.method === "POST") {
|
|
62
|
-
const transport = new StreamableHTTPServerTransport({
|
|
63
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
64
|
-
onsessioninitialized: (sessionId) => {
|
|
65
|
-
sessions.set(sessionId, transport);
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
transport.onclose = () => {
|
|
69
|
-
if (transport.sessionId)
|
|
70
|
-
sessions.delete(transport.sessionId);
|
|
71
|
-
};
|
|
72
|
-
const server = await serverList.create();
|
|
73
|
-
await server.connect(transport);
|
|
74
|
-
return await transport.handleRequest(req, res);
|
|
75
|
-
}
|
|
76
|
-
res.statusCode = 400;
|
|
77
|
-
res.end("Invalid request");
|
|
78
|
-
}
|
|
79
|
-
export function startHttpTransport(port, hostname) {
|
|
80
|
-
const sseSessions = new Map();
|
|
81
|
-
const streamableSessions = new Map();
|
|
82
|
-
const httpServer = http.createServer(async (req, res) => {
|
|
83
|
-
const url = new URL(`http://localhost${req.url}`);
|
|
84
|
-
if (url.pathname.startsWith("/mcp"))
|
|
85
|
-
await handleStreamable(req, res, streamableSessions);
|
|
86
|
-
else
|
|
87
|
-
await handleSSE(req, res, url, sseSessions);
|
|
88
|
-
});
|
|
89
|
-
httpServer.listen(port, hostname, () => {
|
|
90
|
-
const address = httpServer.address();
|
|
91
|
-
assert(address, "Could not bind server socket");
|
|
92
|
-
let url;
|
|
93
|
-
if (typeof address === "string") {
|
|
94
|
-
url = address;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const resolvedPort = address.port;
|
|
98
|
-
let resolvedHost = address.family === "IPv4" ? address.address : `[${address.address}]`;
|
|
99
|
-
if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
|
|
100
|
-
resolvedHost = "localhost";
|
|
101
|
-
url = `http://${resolvedHost}:${resolvedPort}`;
|
|
102
|
-
}
|
|
103
|
-
const message = [
|
|
104
|
-
`Listening on ${url}`,
|
|
105
|
-
"If your client supports streamable HTTP, you can use the /mcp endpoint instead.",
|
|
106
|
-
].join("\n");
|
|
107
|
-
// eslint-disable-next-line no-console
|
|
108
|
-
console.log(message);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function createRemoteServerList(req, res) {
|
|
112
|
-
const apiKey = req.headers[API_KEY_NAME];
|
|
113
|
-
if (!apiKey) {
|
|
114
|
-
res.statusCode = 401;
|
|
115
|
-
res.end("Unauthorized: Missing x-api-token header");
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
const serverList = new ServerList(async () => createMcpServer({ headers: req.headers, apiKey }));
|
|
119
|
-
return serverList;
|
|
120
|
-
}
|