parseapi-mcp 0.1.2 → 0.2.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 +18 -6
- package/dist/chunk-4GQ2Q7SN.js +481 -0
- package/dist/http.js +146 -3
- package/dist/registry.js +1 -1
- package/dist/stdio.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-QELYOT3A.js +0 -310
package/README.md
CHANGED
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Official parseAPI MCP server. Every parseAPI lookup as a tool for AI agents: IP and place data, email, phone and domain validation, weather, currency, timezones, holidays.
|
|
4
4
|
|
|
5
|
-
Get a key at [parseapi.com](https://parseapi.com). Free plan works.
|
|
6
|
-
|
|
7
5
|
## Hosted
|
|
8
6
|
|
|
9
|
-
One URL, nothing to install.
|
|
7
|
+
One URL, nothing to install. Add the server and sign in once in the browser. Free plan works.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"parseAPI": { "url": "https://mcp.parseapi.com" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Cursor:** [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=parseAPI&config=eyJ1cmwiOiJodHRwczovL21jcC5wYXJzZWFwaS5jb20ifQ==)
|
|
18
|
+
|
|
19
|
+
Repo-root `.mcp.json` is keyless on purpose. [cursor.directory](https://cursor.directory/plugins/parseapi) auto-detects it when you submit or refresh the listing.
|
|
20
|
+
|
|
21
|
+
CI and headless setups skip the browser with a key from [parseapi.com](https://parseapi.com):
|
|
10
22
|
|
|
11
23
|
```json
|
|
12
24
|
{
|
|
13
25
|
"mcpServers": {
|
|
14
|
-
"
|
|
26
|
+
"parseAPI": {
|
|
15
27
|
"url": "https://mcp.parseapi.com",
|
|
16
28
|
"headers": { "X-API-Key": "your-api-key" }
|
|
17
29
|
}
|
|
@@ -24,7 +36,7 @@ One URL, nothing to install.
|
|
|
24
36
|
```json
|
|
25
37
|
{
|
|
26
38
|
"mcpServers": {
|
|
27
|
-
"
|
|
39
|
+
"parseAPI": {
|
|
28
40
|
"command": "npx",
|
|
29
41
|
"args": ["-y", "parseapi-mcp"],
|
|
30
42
|
"env": { "PARSEAPI_KEY": "your-api-key" }
|
|
@@ -35,7 +47,7 @@ One URL, nothing to install.
|
|
|
35
47
|
|
|
36
48
|
## Tools
|
|
37
49
|
|
|
38
|
-
One tool per endpoint, named after the route: `ip`, `postal`, `city`, `city_search`, `email`, `phone`, `weather`, `currency_rate`, `timezone`, `holiday`, `point`, `elevation`, and the rest. Every tool returns the same minimal JSON the API serves.
|
|
50
|
+
One tool per endpoint, named after the route: `ip`, `postal`, `city`, `city_search`, `city_nearby`, `email`, `vat`, `iban`, `npi`, `vin`, `hts`, `hts_search`, `phone`, `carrier`, `caller`, `hlr`, `weather`, `currency_rate`, `timezone`, `holiday`, `point`, `elevation`, and the rest. Every tool returns the same minimal JSON the API serves.
|
|
39
51
|
|
|
40
52
|
Errors come back as JSON with a machine-readable `code`. Branch on `code`, never on message text. A miss is `not_found`. No key is `invalid_api_key`.
|
|
41
53
|
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/registry.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
5
|
+
import { parseAPI } from "@parseapi/sdk";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
import { ParseAPIError } from "@parseapi/sdk";
|
|
10
|
+
function ok(data) {
|
|
11
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
12
|
+
}
|
|
13
|
+
function errorJson(code, message, docs, requestId) {
|
|
14
|
+
return {
|
|
15
|
+
isError: true,
|
|
16
|
+
content: [{ type: "text", text: JSON.stringify({ code, message, docs, request_id: requestId }) }]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function toErrorResult(err) {
|
|
20
|
+
if (err instanceof ParseAPIError) {
|
|
21
|
+
return errorJson(err.code, err.message, err.docs, err.requestId);
|
|
22
|
+
}
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
return errorJson("network_error", message, null, null);
|
|
25
|
+
}
|
|
26
|
+
function noKeyResult(transport) {
|
|
27
|
+
const where = transport === "stdio" ? "set it as the PARSEAPI_KEY environment variable for this MCP server" : "send it in the X-API-Key header";
|
|
28
|
+
return errorJson(
|
|
29
|
+
"invalid_api_key",
|
|
30
|
+
`No API key. Get a free key at https://parseapi.com and ${where}.`,
|
|
31
|
+
"https://parseapi.com/docs#invalid_api_key",
|
|
32
|
+
null
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/registry.ts
|
|
37
|
+
var VERSION = "0.2.0";
|
|
38
|
+
var deep = z.boolean().optional().describe("Include the nested deep object with richer fields. Paid on most endpoints.");
|
|
39
|
+
var lat = z.number().min(-90).max(90).describe("Latitude in decimal degrees");
|
|
40
|
+
var lon = z.number().min(-180).max(180).describe("Longitude in decimal degrees");
|
|
41
|
+
var iso2 = (what) => z.string().describe(`ISO 3166-1 alpha-2 ${what}, e.g. US`);
|
|
42
|
+
var countryOpt = z.string().optional().describe("ISO2, ISO3, or a country name. Optional when the lookup is unique.");
|
|
43
|
+
function buildServer(key, transport) {
|
|
44
|
+
const server = new McpServer(
|
|
45
|
+
{
|
|
46
|
+
name: "parseapi",
|
|
47
|
+
version: VERSION,
|
|
48
|
+
title: "parseAPI",
|
|
49
|
+
description: "Lookups for agents: IP and place data, email, VAT, IBAN, NPI, phone and domain validation, weather, currency, timezones, holidays. Real reference data instead of guessing.",
|
|
50
|
+
websiteUrl: "https://parseapi.com"
|
|
51
|
+
},
|
|
52
|
+
{ capabilities: { tools: {} } }
|
|
53
|
+
);
|
|
54
|
+
const parse = key ? parseAPI(key) : null;
|
|
55
|
+
function tool(name, description, shape, fn) {
|
|
56
|
+
server.registerTool(
|
|
57
|
+
name,
|
|
58
|
+
{
|
|
59
|
+
description,
|
|
60
|
+
inputSchema: z.object(shape),
|
|
61
|
+
annotations: { readOnlyHint: true }
|
|
62
|
+
},
|
|
63
|
+
async (args) => {
|
|
64
|
+
if (!parse) return noKeyResult(transport);
|
|
65
|
+
try {
|
|
66
|
+
return ok(await fn(parse, args));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return toErrorResult(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
tool(
|
|
74
|
+
"ip",
|
|
75
|
+
"Look up an IPv4 or IPv6 address: country, region, ASN, timezone. Deep adds datacenter, relay, tor and vpn flags.",
|
|
76
|
+
{ ip: z.string().describe("IPv4 or IPv6 address, e.g. 8.8.8.8"), deep },
|
|
77
|
+
(c, a) => c.ip(a.ip, { deep: a.deep })
|
|
78
|
+
);
|
|
79
|
+
if (transport === "stdio") {
|
|
80
|
+
tool(
|
|
81
|
+
"ip_self",
|
|
82
|
+
"Look up the public IP of the machine running this MCP server.",
|
|
83
|
+
{ deep },
|
|
84
|
+
(c, a) => c.ip.self({ deep: a.deep })
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
tool(
|
|
88
|
+
"continent",
|
|
89
|
+
"Look up a continent by code: name, area, population.",
|
|
90
|
+
{ code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
|
|
91
|
+
(c, a) => c.continent(a.code)
|
|
92
|
+
);
|
|
93
|
+
tool(
|
|
94
|
+
"continent_countries",
|
|
95
|
+
"List every country on a continent.",
|
|
96
|
+
{ code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
|
|
97
|
+
(c, a) => c.continent.countries(a.code)
|
|
98
|
+
);
|
|
99
|
+
tool(
|
|
100
|
+
"bloc",
|
|
101
|
+
"Look up a country group by code: EU, EEA, Schengen, Eurozone, SEPA, NATO, and more. Returns the official name and the current member count.",
|
|
102
|
+
{
|
|
103
|
+
code: z.string().describe("Bloc code: EU, EEA, EFTA, SCHENGEN, EUROZONE, SEPA, NATO, OECD, G7, ASEAN, GCC, MERCOSUR")
|
|
104
|
+
},
|
|
105
|
+
(c, a) => {
|
|
106
|
+
const client = c;
|
|
107
|
+
return client.bloc(a.code);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
tool(
|
|
111
|
+
"bloc_countries",
|
|
112
|
+
"List the current members of a country group, each with name, flag, and calling code.",
|
|
113
|
+
{
|
|
114
|
+
code: z.string().describe("Bloc code: EU, EEA, EFTA, SCHENGEN, EUROZONE, SEPA, NATO, OECD, G7, ASEAN, GCC, MERCOSUR")
|
|
115
|
+
},
|
|
116
|
+
(c, a) => {
|
|
117
|
+
const client = c;
|
|
118
|
+
return client.bloc.countries(a.code);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
tool(
|
|
122
|
+
"country",
|
|
123
|
+
"Look up a country: names, capital, currency, languages, calling code, timezones.",
|
|
124
|
+
{ code: iso2("country code") },
|
|
125
|
+
(c, a) => c.country(a.code)
|
|
126
|
+
);
|
|
127
|
+
tool(
|
|
128
|
+
"country_states",
|
|
129
|
+
"List the states, provinces or regions of a country.",
|
|
130
|
+
{ code: iso2("country code") },
|
|
131
|
+
(c, a) => c.country.states(a.code)
|
|
132
|
+
);
|
|
133
|
+
tool(
|
|
134
|
+
"state",
|
|
135
|
+
"Look up a state, province or region by code or name. Unique names resolve without a country. A colliding code 404s asking for ?country=. Country takes ISO2, ISO3, or a name.",
|
|
136
|
+
{
|
|
137
|
+
code: z.string().describe("State code or name, e.g. colorado, NC"),
|
|
138
|
+
country: countryOpt
|
|
139
|
+
},
|
|
140
|
+
(c, a) => c.state(a.code, { country: a.country })
|
|
141
|
+
);
|
|
142
|
+
tool(
|
|
143
|
+
"state_districts",
|
|
144
|
+
"List the districts, counties or departments of a state.",
|
|
145
|
+
{
|
|
146
|
+
code: z.string().describe("State code or name, e.g. NC, colorado"),
|
|
147
|
+
country: countryOpt
|
|
148
|
+
},
|
|
149
|
+
(c, a) => c.state.districts(a.code, { country: a.country })
|
|
150
|
+
);
|
|
151
|
+
tool(
|
|
152
|
+
"district",
|
|
153
|
+
"Look up a district, county or department by code or name. Pass state when a name collides.",
|
|
154
|
+
{
|
|
155
|
+
code: z.string().describe("District code or name, e.g. 37081, guilford county"),
|
|
156
|
+
country: countryOpt,
|
|
157
|
+
state: z.string().optional().describe("ADM1 code or name to disambiguate, e.g. NC or louisiana")
|
|
158
|
+
},
|
|
159
|
+
(c, a) => c.district(a.code, { country: a.country, state: a.state })
|
|
160
|
+
);
|
|
161
|
+
tool(
|
|
162
|
+
"city",
|
|
163
|
+
"Look up a city by name: type, capital status (capital_of), district, elevation, area in km2, coordinates, timezone. Pass country or state to disambiguate name ties.",
|
|
164
|
+
{
|
|
165
|
+
name: z.string().describe("City name, e.g. charlotte"),
|
|
166
|
+
country: countryOpt,
|
|
167
|
+
state: z.string().optional().describe("State code to disambiguate, e.g. NC")
|
|
168
|
+
},
|
|
169
|
+
(c, a) => c.city(a.name, { country: a.country, state: a.state })
|
|
170
|
+
);
|
|
171
|
+
tool(
|
|
172
|
+
"city_id",
|
|
173
|
+
"Refetch a city by its stable parse id from an earlier response.",
|
|
174
|
+
{ id: z.string().describe("Stable city id, e.g. city_mb8mbqrkz8zb") },
|
|
175
|
+
(c, a) => c.city.id(a.id)
|
|
176
|
+
);
|
|
177
|
+
tool(
|
|
178
|
+
"city_search",
|
|
179
|
+
"Search cities by name prefix. Use when the exact name is unknown.",
|
|
180
|
+
{
|
|
181
|
+
q: z.string().describe("Name prefix, e.g. char"),
|
|
182
|
+
country: countryOpt,
|
|
183
|
+
state: z.string().optional().describe("State code filter"),
|
|
184
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results")
|
|
185
|
+
},
|
|
186
|
+
(c, a) => c.city.search(a.q, { country: a.country, state: a.state, limit: a.limit })
|
|
187
|
+
);
|
|
188
|
+
tool(
|
|
189
|
+
"city_nearest",
|
|
190
|
+
"Find the nearest city to coordinates.",
|
|
191
|
+
{ lat, lon },
|
|
192
|
+
(c, a) => c.city.nearest(a.lat, a.lon)
|
|
193
|
+
);
|
|
194
|
+
tool(
|
|
195
|
+
"city_nearby",
|
|
196
|
+
"List cities around a named city, nearest first, with distance. Default radius 40 km.",
|
|
197
|
+
{
|
|
198
|
+
name: z.string().describe("Anchor city name, e.g. denver"),
|
|
199
|
+
country: countryOpt,
|
|
200
|
+
state: z.string().optional().describe("State code to disambiguate the anchor"),
|
|
201
|
+
radius: z.number().positive().optional().describe("Search radius, default 40 km"),
|
|
202
|
+
unit: z.enum(["km", "mi"]).optional().describe("Radius unit, default km"),
|
|
203
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results, default 10")
|
|
204
|
+
},
|
|
205
|
+
(c, a) => c.city.nearby(a.name, {
|
|
206
|
+
country: a.country,
|
|
207
|
+
state: a.state,
|
|
208
|
+
radius: a.radius,
|
|
209
|
+
unit: a.unit,
|
|
210
|
+
limit: a.limit
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
tool(
|
|
214
|
+
"postal",
|
|
215
|
+
"Look up a postal or ZIP code: place name, country name, coordinates, state, district, area, timezone, elevation. Unique codes resolve without a country. A collision 404s asking for ?country=. Never defaults to US.",
|
|
216
|
+
{
|
|
217
|
+
code: z.string().describe("Postal or ZIP code, e.g. SW1A 1AA, 28202"),
|
|
218
|
+
country: countryOpt
|
|
219
|
+
},
|
|
220
|
+
(c, a) => c.postal(a.code, { country: a.country })
|
|
221
|
+
);
|
|
222
|
+
tool(
|
|
223
|
+
"postal_nearby",
|
|
224
|
+
"List postal codes near a given one, sorted by distance. Unique codes resolve without a country.",
|
|
225
|
+
{
|
|
226
|
+
code: z.string().describe("Postal code to search around"),
|
|
227
|
+
country: countryOpt,
|
|
228
|
+
radius: z.number().positive().optional().describe("Search radius"),
|
|
229
|
+
unit: z.enum(["km", "mi"]).optional().describe("Radius unit, default km")
|
|
230
|
+
},
|
|
231
|
+
(c, a) => c.postal.nearby(a.code, { country: a.country, radius: a.radius, unit: a.unit })
|
|
232
|
+
);
|
|
233
|
+
tool(
|
|
234
|
+
"postal_distance",
|
|
235
|
+
"Distance between two postal codes in the same country. Unique codes resolve without a country.",
|
|
236
|
+
{
|
|
237
|
+
from: z.string().describe("First postal code"),
|
|
238
|
+
to: z.string().describe("Second postal code"),
|
|
239
|
+
country: countryOpt
|
|
240
|
+
},
|
|
241
|
+
(c, a) => c.postal.distance(a.from, a.to, { country: a.country })
|
|
242
|
+
);
|
|
243
|
+
tool(
|
|
244
|
+
"point",
|
|
245
|
+
"Reverse geocode coordinates to country, state, district and nearest city. Deep adds richer admin data.",
|
|
246
|
+
{ lat, lon, deep },
|
|
247
|
+
(c, a) => c.point(a.lat, a.lon, { deep: a.deep })
|
|
248
|
+
);
|
|
249
|
+
tool(
|
|
250
|
+
"elevation",
|
|
251
|
+
"Elevation in meters at coordinates.",
|
|
252
|
+
{ lat, lon },
|
|
253
|
+
(c, a) => c.elevation(a.lat, a.lon)
|
|
254
|
+
);
|
|
255
|
+
tool(
|
|
256
|
+
"weather",
|
|
257
|
+
"Current weather observation at coordinates from official national agencies. Every measurement ships metric and imperial side by side. Deep adds minute-by-minute rain, hourly and daily rows worldwide, alerts, and air quality. With deep, date returns a past day as deep.history.",
|
|
258
|
+
{
|
|
259
|
+
lat,
|
|
260
|
+
lon,
|
|
261
|
+
deep,
|
|
262
|
+
date: z.string().optional().describe("A past UTC day, YYYY-MM-DD. Requires deep. Returns that day as deep.history")
|
|
263
|
+
},
|
|
264
|
+
(c, a) => c.weather(a.lat, a.lon, { deep: a.deep, date: a.date })
|
|
265
|
+
);
|
|
266
|
+
tool(
|
|
267
|
+
"email",
|
|
268
|
+
"Validate an email address: syntax, domain, MX, disposable, role, and a typo suggestion when the host looks misspelled. Deep runs a live mailbox verification.",
|
|
269
|
+
{ email: z.string().describe("Email address to validate"), deep },
|
|
270
|
+
(c, a) => c.email(a.email, { deep: a.deep })
|
|
271
|
+
);
|
|
272
|
+
tool(
|
|
273
|
+
"vat",
|
|
274
|
+
"Validate a VAT number: format and checksum on every call. Deep asks the live EU registry for registered, legal name, and address. Pass from with your own VAT for a consultation identifier.",
|
|
275
|
+
{
|
|
276
|
+
number: z.string().describe("VAT number, with or without the country prefix"),
|
|
277
|
+
country: iso2("country code when the number has no prefix").optional(),
|
|
278
|
+
from: z.string().optional().describe("Your own VAT number. Returns a consultation identifier for your audit file"),
|
|
279
|
+
deep
|
|
280
|
+
},
|
|
281
|
+
(c, a) => c.vat(a.number, { country: a.country, from: a.from, deep: a.deep })
|
|
282
|
+
);
|
|
283
|
+
tool(
|
|
284
|
+
"iban",
|
|
285
|
+
"Parse an IBAN: checksum and structure. Returns the normalized number, the print form for display, country, checksum digits, and the bank, branch, and account identifiers sitting inside it. bank and branch are codes, not names. Junk answers valid false, never a 404. Pass country when the value has no prefix.",
|
|
286
|
+
{
|
|
287
|
+
iban: z.string().describe("IBAN, with or without spaces, with or without the country prefix"),
|
|
288
|
+
country: iso2("country code when the number has no prefix").optional()
|
|
289
|
+
},
|
|
290
|
+
(c, a) => c.iban(a.iban, { country: a.country })
|
|
291
|
+
);
|
|
292
|
+
tool(
|
|
293
|
+
"npi",
|
|
294
|
+
"Look up an NPI in the CMS NPPES registry: US healthcare provider name, specialty, practice address, active status, and the OIG exclusion flag. Deep adds Medicare enrollment on paid plans.",
|
|
295
|
+
{
|
|
296
|
+
npi: z.string().describe("10-digit NPI number"),
|
|
297
|
+
deep
|
|
298
|
+
},
|
|
299
|
+
(c, a) => c.npi(a.npi, { deep: a.deep })
|
|
300
|
+
);
|
|
301
|
+
tool(
|
|
302
|
+
"phone",
|
|
303
|
+
"Validate and parse a phone number: country, type, formats. Pass country for national-format numbers.",
|
|
304
|
+
{
|
|
305
|
+
number: z.string().describe("Phone number, e.g. +14155552671"),
|
|
306
|
+
country: iso2("country code for national-format numbers").optional(),
|
|
307
|
+
deep
|
|
308
|
+
},
|
|
309
|
+
(c, a) => c.phone(a.number, { country: a.country, deep: a.deep })
|
|
310
|
+
);
|
|
311
|
+
tool(
|
|
312
|
+
"carrier",
|
|
313
|
+
"Look up the current carrier serving a phone number: carrier name, network type including voip, burner app flag, issuing city and state. Metered per lookup on a valid number.",
|
|
314
|
+
{
|
|
315
|
+
number: z.string().describe("Phone number, e.g. +14155552671"),
|
|
316
|
+
country: iso2("country code for national-format numbers").optional()
|
|
317
|
+
},
|
|
318
|
+
(c, a) => c.carrier(a.number, { country: a.country })
|
|
319
|
+
);
|
|
320
|
+
tool(
|
|
321
|
+
"caller",
|
|
322
|
+
"Look up the caller ID name (CNAM) for a US or Canada phone number. caller is the record verbatim, null when no record or outside NANP. Metered per lookup on a NANP number.",
|
|
323
|
+
{
|
|
324
|
+
number: z.string().describe("Phone number, e.g. +18004633339"),
|
|
325
|
+
country: iso2("country code for national-format numbers").optional()
|
|
326
|
+
},
|
|
327
|
+
(c, a) => c.caller(a.number, { country: a.country })
|
|
328
|
+
);
|
|
329
|
+
tool(
|
|
330
|
+
"hlr",
|
|
331
|
+
"Live network status for a phone number worldwide: live means assigned, connected means the handset is reachable right now. Outside North America adds roaming and network details. Metered per lookup on a valid number.",
|
|
332
|
+
{
|
|
333
|
+
number: z.string().describe("Phone number, e.g. +447712345678"),
|
|
334
|
+
country: iso2("country code for national-format numbers").optional()
|
|
335
|
+
},
|
|
336
|
+
(c, a) => c.hlr(a.number, { country: a.country })
|
|
337
|
+
);
|
|
338
|
+
tool(
|
|
339
|
+
"domain",
|
|
340
|
+
"Look up a domain: registration, DNS, mail setup. Deep adds richer checks.",
|
|
341
|
+
{ domain: z.string().describe("Domain name, e.g. example.com"), deep },
|
|
342
|
+
(c, a) => c.domain(a.domain, { deep: a.deep })
|
|
343
|
+
);
|
|
344
|
+
tool(
|
|
345
|
+
"mx",
|
|
346
|
+
"MX records for a domain.",
|
|
347
|
+
{ domain: z.string().describe("Domain name") },
|
|
348
|
+
(c, a) => c.mx(a.domain)
|
|
349
|
+
);
|
|
350
|
+
tool(
|
|
351
|
+
"useragent",
|
|
352
|
+
"Parse a User-Agent string: browser, OS, device, bot detection.",
|
|
353
|
+
{ ua: z.string().describe("The User-Agent string to parse"), deep },
|
|
354
|
+
(c, a) => c.useragent(a.ua, { deep: a.deep })
|
|
355
|
+
);
|
|
356
|
+
tool(
|
|
357
|
+
"vin",
|
|
358
|
+
"Decode a 17-character VIN: year, make, model, trim, body, engine, drive, transmission, manufacturer, and assembly plant. Junk or a failed check digit answers valid false, never a 404. Deep adds open recall campaigns on paid plans.",
|
|
359
|
+
{ vin: z.string().describe("The VIN as you have it. Spaces and punctuation fold out"), deep },
|
|
360
|
+
(c, a) => c.vin(a.vin, { deep: a.deep })
|
|
361
|
+
);
|
|
362
|
+
tool(
|
|
363
|
+
"hts",
|
|
364
|
+
"Look up a US Harmonized Tariff Schedule code: description, duty rates verbatim (general, special, column 2), units, parent lineage, and the official revision that answered. Deep with an origin country resolves the Chapter 99 tariff measures that apply from that origin, with a composed effective_rate when the components compose cleanly (null otherwise, null beats a guess). Unknown code is a 404.",
|
|
365
|
+
{
|
|
366
|
+
code: z.string().describe("HTS code, 4 to 10 digits, dots optional, e.g. 8471.30.01.00"),
|
|
367
|
+
origin: iso2("country of origin for duty resolution, only read with deep").optional(),
|
|
368
|
+
deep
|
|
369
|
+
},
|
|
370
|
+
(c, a) => c.hts(a.code, { deep: a.deep, origin: a.origin })
|
|
371
|
+
);
|
|
372
|
+
tool(
|
|
373
|
+
"hts_search",
|
|
374
|
+
"Search US tariff schedule descriptions by product. Returns up to 20 lines, best match first, each with hts, description, and the general duty rate.",
|
|
375
|
+
{ q: z.string().describe("Product words, e.g. sunglasses, laptop, coffee") },
|
|
376
|
+
(c, a) => c.hts.search(a.q)
|
|
377
|
+
);
|
|
378
|
+
tool(
|
|
379
|
+
"currency",
|
|
380
|
+
"Look up a currency: name, symbol, decimal places, countries using it.",
|
|
381
|
+
{ code: z.string().describe("ISO 4217 code, e.g. USD") },
|
|
382
|
+
(c, a) => c.currency(a.code)
|
|
383
|
+
);
|
|
384
|
+
tool(
|
|
385
|
+
"currency_rate",
|
|
386
|
+
"Exchange rate between two currencies from official central bank data. Pass date for a past business day, amount to convert.",
|
|
387
|
+
{
|
|
388
|
+
base: z.string().describe("Base currency ISO 4217 code, e.g. USD"),
|
|
389
|
+
quote: z.string().describe("Quote currency ISO 4217 code, e.g. EUR"),
|
|
390
|
+
date: z.string().optional().describe(
|
|
391
|
+
"YYYY-MM-DD. Official rate for that business day. A weekend or holiday resolves to the last published day on or before it"
|
|
392
|
+
),
|
|
393
|
+
amount: z.number().optional().describe("Appends amount and converted, rounded to the quote currency minor-unit digits")
|
|
394
|
+
},
|
|
395
|
+
(c, a) => c.currency.rate(a.base, a.quote, { date: a.date, amount: a.amount })
|
|
396
|
+
);
|
|
397
|
+
tool(
|
|
398
|
+
"language",
|
|
399
|
+
"Look up a language by BCP 47 or ISO 639-3 code: names, script, direction.",
|
|
400
|
+
{ code: z.string().describe("Language code, e.g. en, ja, gsw") },
|
|
401
|
+
(c, a) => c.language(a.code)
|
|
402
|
+
);
|
|
403
|
+
tool(
|
|
404
|
+
"name",
|
|
405
|
+
"Parse a person name: prefix, first, middle, last, suffix, gender, salutation. Junk input returns valid false. Gender comes from dictionary data and is null when the data does not decide.",
|
|
406
|
+
{ name: z.string().describe("The name to parse, e.g. Smith, John or BILLY OSHALL") },
|
|
407
|
+
(c, a) => c.name(a.name)
|
|
408
|
+
);
|
|
409
|
+
tool(
|
|
410
|
+
"timezone",
|
|
411
|
+
"Look up a timezone from an IANA id or from lat and lon. Offset, DST, local time. Pass at for a specific instant. Pass to with another IANA id to convert a time between zones: the response appends at (wall time in the from zone) and to.at (the converted time). Open ocean answers the nautical Etc/GMT zone.",
|
|
412
|
+
{
|
|
413
|
+
timezone: z.string().optional().describe("IANA timezone id, e.g. America/New_York"),
|
|
414
|
+
lat: lat.optional(),
|
|
415
|
+
lon: lon.optional(),
|
|
416
|
+
at: z.string().optional().describe(
|
|
417
|
+
"ISO 8601 time, default now. With to and no UTC offset, reads as wall time in the from zone"
|
|
418
|
+
),
|
|
419
|
+
to: z.string().optional().describe("Convert: the other IANA zone, e.g. Asia/Tokyo. Requires timezone, not lat/lon")
|
|
420
|
+
},
|
|
421
|
+
(c, a) => {
|
|
422
|
+
if (a.lat != null && a.lon != null) {
|
|
423
|
+
return c.timezone(a.lat, a.lon, { at: a.at });
|
|
424
|
+
}
|
|
425
|
+
if (!a.timezone) {
|
|
426
|
+
return Promise.reject(new Error("Pass timezone or lat and lon"));
|
|
427
|
+
}
|
|
428
|
+
return c.timezone(a.timezone, { at: a.at, to: a.to });
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
tool(
|
|
432
|
+
"date",
|
|
433
|
+
"Parse a date in any common format (2026-03-29, March 29, 2026, 3/29/2026, 20260829) into calendar facts: ISO date, weekday, ISO week, day of year, quarter, leap year, unix time. Ambiguous numeric dates answer valid false unless format asserts a reading, never a guess. Pass to with another date for the signed days between. Omit date for today (UTC), so to alone answers days from today.",
|
|
434
|
+
{
|
|
435
|
+
date: z.string().optional().describe("The date as you have it, any common format. Omit for today (UTC)"),
|
|
436
|
+
format: z.enum(["mdy", "dmy"]).optional().describe("Breaks the month-first / day-first tie on numeric dates like 03/04/2026"),
|
|
437
|
+
to: z.string().optional().describe("Another date. Appends to (normalized ISO) and days (signed days between)")
|
|
438
|
+
},
|
|
439
|
+
(c, a) => {
|
|
440
|
+
const client = c;
|
|
441
|
+
if (a.date == null || a.date === "") return client.date.today({ to: a.to });
|
|
442
|
+
return client.date(a.date, { format: a.format, to: a.to });
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
tool(
|
|
446
|
+
"holiday",
|
|
447
|
+
"Public holidays and cultural observances for a country and year. Each row carries type: public or observance.",
|
|
448
|
+
{
|
|
449
|
+
country: iso2("country code"),
|
|
450
|
+
year: z.number().int().optional().describe("Year, default current")
|
|
451
|
+
},
|
|
452
|
+
(c, a) => c.holiday(a.country, { year: a.year })
|
|
453
|
+
);
|
|
454
|
+
tool(
|
|
455
|
+
"holiday_date",
|
|
456
|
+
"Whether a specific date is a holiday or observance in a country. holiday is null when it is not.",
|
|
457
|
+
{ country: iso2("country code"), date: z.string().describe("Date as YYYY-MM-DD") },
|
|
458
|
+
(c, a) => c.holiday.date(a.country, a.date)
|
|
459
|
+
);
|
|
460
|
+
tool(
|
|
461
|
+
"emoji",
|
|
462
|
+
"Look up an emoji by name or character: unicode, hex, skin tones.",
|
|
463
|
+
{ emoji: z.string().describe("Emoji name or the character itself, e.g. rocket") },
|
|
464
|
+
(c, a) => c.emoji(a.emoji)
|
|
465
|
+
);
|
|
466
|
+
tool(
|
|
467
|
+
"emoji_search",
|
|
468
|
+
"Search emoji by keyword.",
|
|
469
|
+
{
|
|
470
|
+
q: z.string().describe("Search keyword, e.g. fire"),
|
|
471
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results")
|
|
472
|
+
},
|
|
473
|
+
(c, a) => c.emoji.search(a.q, { limit: a.limit })
|
|
474
|
+
);
|
|
475
|
+
return server;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export {
|
|
479
|
+
VERSION,
|
|
480
|
+
buildServer
|
|
481
|
+
};
|
package/dist/http.js
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4GQ2Q7SN.js";
|
|
5
5
|
|
|
6
6
|
// src/http.ts
|
|
7
7
|
import { createServer } from "http";
|
|
8
8
|
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
9
9
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
10
|
+
|
|
11
|
+
// src/oauth.ts
|
|
12
|
+
var WEB_URL = (process.env.WEB_URL ?? "https://parseapi.com").replace(/\/$/, "");
|
|
13
|
+
var SELF_URL = (process.env.SELF_URL ?? "https://mcp.parseapi.com").replace(/\/$/, "");
|
|
14
|
+
var KEY_TTL_MS = 10 * 60 * 1e3;
|
|
15
|
+
var MISS_TTL_MS = 60 * 1e3;
|
|
16
|
+
var cache = /* @__PURE__ */ new Map();
|
|
17
|
+
function isApiKeyShape(value) {
|
|
18
|
+
return value.startsWith("parse_") || /^[0-9a-f]{32}$/.test(value);
|
|
19
|
+
}
|
|
20
|
+
async function resolveOAuthKey(token) {
|
|
21
|
+
const hit = cache.get(token);
|
|
22
|
+
if (hit && hit.expires > Date.now()) return hit.key;
|
|
23
|
+
if (cache.size > 5e3) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [k, v] of cache) if (v.expires <= now) cache.delete(k);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${WEB_URL}/api/internal/mcp/resolve`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"content-type": "application/json",
|
|
32
|
+
"x-parse-web-internal": process.env.WEB_INTERNAL_TOKEN ?? ""
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({ token }),
|
|
35
|
+
signal: AbortSignal.timeout(3e3)
|
|
36
|
+
});
|
|
37
|
+
if (res.ok) {
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
if (typeof data.key === "string" && data.key) {
|
|
40
|
+
const ttl = Math.min(KEY_TTL_MS, Math.max(3e4, (data.expires_in ?? 600) * 1e3));
|
|
41
|
+
cache.set(token, { key: data.key, expires: Date.now() + ttl });
|
|
42
|
+
return data.key;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
46
|
+
cache.set(token, { key: null, expires: Date.now() + MISS_TTL_MS });
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var PROTECTED_RESOURCE_METADATA = {
|
|
54
|
+
resource: SELF_URL,
|
|
55
|
+
authorization_servers: [WEB_URL],
|
|
56
|
+
bearer_methods_supported: ["header"],
|
|
57
|
+
scopes_supported: ["openid", "profile", "email", "offline_access"]
|
|
58
|
+
};
|
|
59
|
+
var WWW_AUTHENTICATE = `Bearer resource_metadata="${SELF_URL}/.well-known/oauth-protected-resource"`;
|
|
60
|
+
var asMetadata = null;
|
|
61
|
+
async function authServerMetadata() {
|
|
62
|
+
if (asMetadata && asMetadata.expires > Date.now()) return asMetadata.body;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${WEB_URL}/.well-known/oauth-authorization-server`, {
|
|
65
|
+
signal: AbortSignal.timeout(3e3)
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) return null;
|
|
68
|
+
const body = await res.text();
|
|
69
|
+
asMetadata = { body, expires: Date.now() + 5 * 60 * 1e3 };
|
|
70
|
+
return body;
|
|
71
|
+
} catch {
|
|
72
|
+
return asMetadata?.body ?? null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/http.ts
|
|
10
77
|
function keyFrom(request) {
|
|
11
78
|
if (!request) return null;
|
|
12
79
|
const headerKey = request.headers.get("x-api-key");
|
|
@@ -20,12 +87,88 @@ var handler = createMcpHandler((ctx) => buildServer(keyFrom(ctx.requestInfo), "h
|
|
|
20
87
|
});
|
|
21
88
|
var nodeHandler = toNodeHandler(handler);
|
|
22
89
|
var port = Number(process.env.PORT ?? 8080);
|
|
90
|
+
var WELL_KNOWN_CORS = {
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
"access-control-allow-origin": "*",
|
|
93
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
94
|
+
"access-control-allow-headers": "Content-Type"
|
|
95
|
+
};
|
|
96
|
+
function unauthorized(res) {
|
|
97
|
+
res.writeHead(401, {
|
|
98
|
+
"content-type": "application/json",
|
|
99
|
+
"www-authenticate": WWW_AUTHENTICATE,
|
|
100
|
+
"access-control-allow-origin": "*",
|
|
101
|
+
"access-control-expose-headers": "WWW-Authenticate"
|
|
102
|
+
}).end(
|
|
103
|
+
JSON.stringify({
|
|
104
|
+
error: "unauthorized",
|
|
105
|
+
error_description: "Sign in via OAuth or send an API key in X-API-Key."
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
function bearerFrom(req) {
|
|
110
|
+
const auth = req.headers.authorization;
|
|
111
|
+
if (typeof auth !== "string" || !auth.toLowerCase().startsWith("bearer ")) return null;
|
|
112
|
+
return auth.slice(7).trim() || null;
|
|
113
|
+
}
|
|
114
|
+
async function gate(req, res) {
|
|
115
|
+
const headerKey = req.headers["x-api-key"];
|
|
116
|
+
if (typeof headerKey === "string" && headerKey) {
|
|
117
|
+
return nodeHandler(req, res);
|
|
118
|
+
}
|
|
119
|
+
const bearer = bearerFrom(req);
|
|
120
|
+
if (!bearer) {
|
|
121
|
+
unauthorized(res);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isApiKeyShape(bearer)) {
|
|
125
|
+
return nodeHandler(req, res);
|
|
126
|
+
}
|
|
127
|
+
const key = await resolveOAuthKey(bearer);
|
|
128
|
+
if (!key) {
|
|
129
|
+
unauthorized(res);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
req.headers["x-api-key"] = key;
|
|
133
|
+
return nodeHandler(req, res);
|
|
134
|
+
}
|
|
23
135
|
createServer((req, res) => {
|
|
24
|
-
|
|
136
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
137
|
+
if (req.method === "GET" && path === "/health") {
|
|
25
138
|
res.writeHead(200, { "content-type": "text/plain" }).end("ok");
|
|
26
139
|
return;
|
|
27
140
|
}
|
|
28
|
-
|
|
141
|
+
if (path === "/.well-known/oauth-protected-resource") {
|
|
142
|
+
if (req.method === "OPTIONS") {
|
|
143
|
+
res.writeHead(204, WELL_KNOWN_CORS).end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
res.writeHead(200, WELL_KNOWN_CORS).end(JSON.stringify(PROTECTED_RESOURCE_METADATA));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (path === "/.well-known/oauth-authorization-server" || path === "/.well-known/openid-configuration") {
|
|
150
|
+
if (req.method === "OPTIONS") {
|
|
151
|
+
res.writeHead(204, WELL_KNOWN_CORS).end();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
void authServerMetadata().then((body) => {
|
|
155
|
+
if (body) res.writeHead(200, WELL_KNOWN_CORS).end(body);
|
|
156
|
+
else res.writeHead(503, WELL_KNOWN_CORS).end(JSON.stringify({ error: "unavailable" }));
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (req.method === "OPTIONS") {
|
|
161
|
+
void nodeHandler(req, res);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
void gate(req, res).catch((err) => {
|
|
165
|
+
console.error("parseapi-mcp gate:", err instanceof Error ? err.message : err);
|
|
166
|
+
if (!res.headersSent) {
|
|
167
|
+
res.writeHead(500, { "content-type": "application/json" }).end(
|
|
168
|
+
JSON.stringify({ error: "server_error" })
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
29
172
|
}).listen(port, () => {
|
|
30
173
|
console.error(`parseapi-mcp listening on :${port}`);
|
|
31
174
|
});
|
package/dist/registry.js
CHANGED
package/dist/stdio.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parseapi-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"mcpName": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"mcpName": "com.parseapi/mcp",
|
|
5
5
|
"description": "Official parseAPI MCP server. Location, validate, and decode lookups for AI agents: geo, email, phone, weather, currency.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"parseapi",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
47
47
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
48
|
-
"@parseapi/sdk": "^0.
|
|
48
|
+
"@parseapi/sdk": "^0.2.0",
|
|
49
49
|
"zod": "^4.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
package/dist/chunk-QELYOT3A.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/registry.ts
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/server";
|
|
5
|
-
import { parseAPI } from "@parseapi/sdk";
|
|
6
|
-
import * as z from "zod";
|
|
7
|
-
|
|
8
|
-
// src/errors.ts
|
|
9
|
-
import { ParseAPIError } from "@parseapi/sdk";
|
|
10
|
-
function ok(data) {
|
|
11
|
-
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
12
|
-
}
|
|
13
|
-
function errorJson(code, message, docs, requestId) {
|
|
14
|
-
return {
|
|
15
|
-
isError: true,
|
|
16
|
-
content: [{ type: "text", text: JSON.stringify({ code, message, docs, request_id: requestId }) }]
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function toErrorResult(err) {
|
|
20
|
-
if (err instanceof ParseAPIError) {
|
|
21
|
-
return errorJson(err.code, err.message, err.docs, err.requestId);
|
|
22
|
-
}
|
|
23
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
-
return errorJson("network_error", message, null, null);
|
|
25
|
-
}
|
|
26
|
-
function noKeyResult(transport) {
|
|
27
|
-
const where = transport === "stdio" ? "set it as the PARSEAPI_KEY environment variable for this MCP server" : "send it in the X-API-Key header";
|
|
28
|
-
return errorJson(
|
|
29
|
-
"invalid_api_key",
|
|
30
|
-
`No API key. Get a free key at https://parseapi.com and ${where}.`,
|
|
31
|
-
"https://parseapi.com/docs#invalid_api_key",
|
|
32
|
-
null
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/registry.ts
|
|
37
|
-
var VERSION = "0.1.0";
|
|
38
|
-
var deep = z.boolean().optional().describe("Include the nested deep object with richer fields. Paid on most endpoints.");
|
|
39
|
-
var lat = z.number().min(-90).max(90).describe("Latitude in decimal degrees");
|
|
40
|
-
var lon = z.number().min(-180).max(180).describe("Longitude in decimal degrees");
|
|
41
|
-
var iso2 = (what) => z.string().describe(`ISO 3166-1 alpha-2 ${what}, e.g. US`);
|
|
42
|
-
function buildServer(key, transport) {
|
|
43
|
-
const server = new McpServer(
|
|
44
|
-
{
|
|
45
|
-
name: "parseapi",
|
|
46
|
-
version: VERSION,
|
|
47
|
-
title: "parseAPI",
|
|
48
|
-
description: "Lookups for agents: IP and place data, email, phone and domain validation, weather, currency, timezones, holidays. Real reference data instead of guessing.",
|
|
49
|
-
websiteUrl: "https://parseapi.com"
|
|
50
|
-
},
|
|
51
|
-
{ capabilities: { tools: {} } }
|
|
52
|
-
);
|
|
53
|
-
const parse = key ? parseAPI(key) : null;
|
|
54
|
-
function tool(name, description, shape, fn) {
|
|
55
|
-
server.registerTool(
|
|
56
|
-
name,
|
|
57
|
-
{
|
|
58
|
-
description,
|
|
59
|
-
inputSchema: z.object(shape),
|
|
60
|
-
annotations: { readOnlyHint: true }
|
|
61
|
-
},
|
|
62
|
-
async (args) => {
|
|
63
|
-
if (!parse) return noKeyResult(transport);
|
|
64
|
-
try {
|
|
65
|
-
return ok(await fn(parse, args));
|
|
66
|
-
} catch (err) {
|
|
67
|
-
return toErrorResult(err);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
tool(
|
|
73
|
-
"ip",
|
|
74
|
-
"Look up an IPv4 or IPv6 address: country, region, ASN, timezone. Deep adds datacenter, relay and tor flags.",
|
|
75
|
-
{ ip: z.string().describe("IPv4 or IPv6 address, e.g. 8.8.8.8"), deep },
|
|
76
|
-
(c, a) => c.ip(a.ip, { deep: a.deep })
|
|
77
|
-
);
|
|
78
|
-
if (transport === "stdio") {
|
|
79
|
-
tool(
|
|
80
|
-
"ip_self",
|
|
81
|
-
"Look up the public IP of the machine running this MCP server.",
|
|
82
|
-
{ deep },
|
|
83
|
-
(c, a) => c.ip.self({ deep: a.deep })
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
tool(
|
|
87
|
-
"continent",
|
|
88
|
-
"Look up a continent by code: name, area, population.",
|
|
89
|
-
{ code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
|
|
90
|
-
(c, a) => c.continent(a.code)
|
|
91
|
-
);
|
|
92
|
-
tool(
|
|
93
|
-
"continent_countries",
|
|
94
|
-
"List every country on a continent.",
|
|
95
|
-
{ code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
|
|
96
|
-
(c, a) => c.continent.countries(a.code)
|
|
97
|
-
);
|
|
98
|
-
tool(
|
|
99
|
-
"country",
|
|
100
|
-
"Look up a country: names, capital, currency, languages, calling code, timezones.",
|
|
101
|
-
{ code: iso2("country code") },
|
|
102
|
-
(c, a) => c.country(a.code)
|
|
103
|
-
);
|
|
104
|
-
tool(
|
|
105
|
-
"country_states",
|
|
106
|
-
"List the states, provinces or regions of a country.",
|
|
107
|
-
{ code: iso2("country code") },
|
|
108
|
-
(c, a) => c.country.states(a.code)
|
|
109
|
-
);
|
|
110
|
-
tool(
|
|
111
|
-
"state",
|
|
112
|
-
"Look up a state, province or region by its code within a country.",
|
|
113
|
-
{ code: z.string().describe("State code, e.g. NC"), country: iso2("country code") },
|
|
114
|
-
(c, a) => c.state(a.code, { country: a.country })
|
|
115
|
-
);
|
|
116
|
-
tool(
|
|
117
|
-
"state_districts",
|
|
118
|
-
"List the districts, counties or departments of a state.",
|
|
119
|
-
{ code: z.string().describe("State code, e.g. NC"), country: iso2("country code") },
|
|
120
|
-
(c, a) => c.state.districts(a.code, { country: a.country })
|
|
121
|
-
);
|
|
122
|
-
tool(
|
|
123
|
-
"district",
|
|
124
|
-
"Look up a district, county or department by code.",
|
|
125
|
-
{ code: z.string().describe("District code, e.g. 37081"), country: iso2("country code").optional() },
|
|
126
|
-
(c, a) => c.district(a.code, { country: a.country })
|
|
127
|
-
);
|
|
128
|
-
tool(
|
|
129
|
-
"city",
|
|
130
|
-
"Look up a city by name: coordinates, state, population, timezone. Pass country to disambiguate name ties.",
|
|
131
|
-
{
|
|
132
|
-
name: z.string().describe("City name, e.g. charlotte"),
|
|
133
|
-
country: iso2("country code").optional(),
|
|
134
|
-
state: z.string().optional().describe("State code to disambiguate, e.g. NC")
|
|
135
|
-
},
|
|
136
|
-
(c, a) => c.city(a.name, { country: a.country, state: a.state })
|
|
137
|
-
);
|
|
138
|
-
tool(
|
|
139
|
-
"city_id",
|
|
140
|
-
"Refetch a city by its stable parse id from an earlier response.",
|
|
141
|
-
{ id: z.string().describe("Stable city id, e.g. city_mb8mbqrkz8zb") },
|
|
142
|
-
(c, a) => c.city.id(a.id)
|
|
143
|
-
);
|
|
144
|
-
tool(
|
|
145
|
-
"city_search",
|
|
146
|
-
"Search cities by name prefix. Use when the exact name is unknown.",
|
|
147
|
-
{
|
|
148
|
-
q: z.string().describe("Name prefix, e.g. char"),
|
|
149
|
-
country: iso2("country code").optional(),
|
|
150
|
-
state: z.string().optional().describe("State code filter"),
|
|
151
|
-
limit: z.number().int().min(1).max(50).optional().describe("Max results")
|
|
152
|
-
},
|
|
153
|
-
(c, a) => c.city.search(a.q, { country: a.country, state: a.state, limit: a.limit })
|
|
154
|
-
);
|
|
155
|
-
tool(
|
|
156
|
-
"city_nearest",
|
|
157
|
-
"Find the nearest city to coordinates.",
|
|
158
|
-
{ lat, lon },
|
|
159
|
-
(c, a) => c.city.nearest(a.lat, a.lon)
|
|
160
|
-
);
|
|
161
|
-
tool(
|
|
162
|
-
"postal",
|
|
163
|
-
"Look up a postal code: place name, coordinates, state, district, timezone, elevation. Country is required.",
|
|
164
|
-
{ code: z.string().describe("Postal code, e.g. 28202"), country: iso2("country code") },
|
|
165
|
-
(c, a) => c.postal(a.code, { country: a.country })
|
|
166
|
-
);
|
|
167
|
-
tool(
|
|
168
|
-
"postal_nearby",
|
|
169
|
-
"List postal codes near a given one, sorted by distance.",
|
|
170
|
-
{
|
|
171
|
-
code: z.string().describe("Postal code to search around"),
|
|
172
|
-
country: iso2("country code"),
|
|
173
|
-
radius: z.number().positive().optional().describe("Search radius"),
|
|
174
|
-
unit: z.enum(["km", "mi"]).optional().describe("Radius unit, default km")
|
|
175
|
-
},
|
|
176
|
-
(c, a) => c.postal.nearby(a.code, { country: a.country, radius: a.radius, unit: a.unit })
|
|
177
|
-
);
|
|
178
|
-
tool(
|
|
179
|
-
"postal_distance",
|
|
180
|
-
"Distance between two postal codes in the same country.",
|
|
181
|
-
{
|
|
182
|
-
from: z.string().describe("First postal code"),
|
|
183
|
-
to: z.string().describe("Second postal code"),
|
|
184
|
-
country: iso2("country code")
|
|
185
|
-
},
|
|
186
|
-
(c, a) => c.postal.distance(a.from, a.to, { country: a.country })
|
|
187
|
-
);
|
|
188
|
-
tool(
|
|
189
|
-
"point",
|
|
190
|
-
"Reverse geocode coordinates to country, state, district and nearest city. Deep adds richer admin data.",
|
|
191
|
-
{ lat, lon, deep },
|
|
192
|
-
(c, a) => c.point(a.lat, a.lon, { deep: a.deep })
|
|
193
|
-
);
|
|
194
|
-
tool(
|
|
195
|
-
"elevation",
|
|
196
|
-
"Elevation in meters at coordinates.",
|
|
197
|
-
{ lat, lon },
|
|
198
|
-
(c, a) => c.elevation(a.lat, a.lon)
|
|
199
|
-
);
|
|
200
|
-
tool(
|
|
201
|
-
"weather",
|
|
202
|
-
"Current weather observation at coordinates from official national agencies. Every measurement ships metric and imperial side by side. Deep adds forecast and alerts where available.",
|
|
203
|
-
{
|
|
204
|
-
lat,
|
|
205
|
-
lon,
|
|
206
|
-
deep
|
|
207
|
-
},
|
|
208
|
-
(c, a) => c.weather(a.lat, a.lon, { deep: a.deep })
|
|
209
|
-
);
|
|
210
|
-
tool(
|
|
211
|
-
"email",
|
|
212
|
-
"Validate an email address: syntax, domain, MX, disposable and role flags. Deep runs a live mailbox verification.",
|
|
213
|
-
{ email: z.string().describe("Email address to validate"), deep },
|
|
214
|
-
(c, a) => c.email(a.email, { deep: a.deep })
|
|
215
|
-
);
|
|
216
|
-
tool(
|
|
217
|
-
"phone",
|
|
218
|
-
"Validate and parse a phone number: country, type, formats. Pass country for national-format numbers.",
|
|
219
|
-
{
|
|
220
|
-
number: z.string().describe("Phone number, e.g. +14155552671"),
|
|
221
|
-
country: iso2("country code for national-format numbers").optional(),
|
|
222
|
-
deep
|
|
223
|
-
},
|
|
224
|
-
(c, a) => c.phone(a.number, { country: a.country, deep: a.deep })
|
|
225
|
-
);
|
|
226
|
-
tool(
|
|
227
|
-
"domain",
|
|
228
|
-
"Look up a domain: registration, DNS, mail setup. Deep adds richer checks.",
|
|
229
|
-
{ domain: z.string().describe("Domain name, e.g. example.com"), deep },
|
|
230
|
-
(c, a) => c.domain(a.domain, { deep: a.deep })
|
|
231
|
-
);
|
|
232
|
-
tool(
|
|
233
|
-
"mx",
|
|
234
|
-
"MX records and mail provider for a domain.",
|
|
235
|
-
{ domain: z.string().describe("Domain name") },
|
|
236
|
-
(c, a) => c.mx(a.domain)
|
|
237
|
-
);
|
|
238
|
-
tool(
|
|
239
|
-
"useragent",
|
|
240
|
-
"Parse a User-Agent string: browser, OS, device, bot detection.",
|
|
241
|
-
{ ua: z.string().describe("The User-Agent string to parse"), deep },
|
|
242
|
-
(c, a) => c.useragent(a.ua, { deep: a.deep })
|
|
243
|
-
);
|
|
244
|
-
tool(
|
|
245
|
-
"currency",
|
|
246
|
-
"Look up a currency: name, symbol, decimal places, countries using it.",
|
|
247
|
-
{ code: z.string().describe("ISO 4217 code, e.g. USD") },
|
|
248
|
-
(c, a) => c.currency(a.code)
|
|
249
|
-
);
|
|
250
|
-
tool(
|
|
251
|
-
"currency_rate",
|
|
252
|
-
"Exchange rate between two currencies from official central bank data.",
|
|
253
|
-
{
|
|
254
|
-
base: z.string().describe("Base currency ISO 4217 code, e.g. USD"),
|
|
255
|
-
quote: z.string().describe("Quote currency ISO 4217 code, e.g. EUR")
|
|
256
|
-
},
|
|
257
|
-
(c, a) => c.currency.rate(a.base, a.quote)
|
|
258
|
-
);
|
|
259
|
-
tool(
|
|
260
|
-
"language",
|
|
261
|
-
"Look up a language by BCP 47 or ISO 639-3 code: names, script, direction.",
|
|
262
|
-
{ code: z.string().describe("Language code, e.g. en, ja, gsw") },
|
|
263
|
-
(c, a) => c.language(a.code)
|
|
264
|
-
);
|
|
265
|
-
tool(
|
|
266
|
-
"timezone",
|
|
267
|
-
"Look up an IANA timezone: current offset, DST state, local time. Pass at for a specific instant.",
|
|
268
|
-
{
|
|
269
|
-
timezone: z.string().describe("IANA timezone id, e.g. America/New_York"),
|
|
270
|
-
at: z.string().optional().describe("ISO 8601 instant to evaluate, default now")
|
|
271
|
-
},
|
|
272
|
-
(c, a) => c.timezone(a.timezone, { at: a.at })
|
|
273
|
-
);
|
|
274
|
-
tool(
|
|
275
|
-
"holiday",
|
|
276
|
-
"Public holidays and cultural observances for a country and year. Each row carries type: public or observance.",
|
|
277
|
-
{
|
|
278
|
-
country: iso2("country code"),
|
|
279
|
-
year: z.number().int().optional().describe("Year, default current")
|
|
280
|
-
},
|
|
281
|
-
(c, a) => c.holiday(a.country, { year: a.year })
|
|
282
|
-
);
|
|
283
|
-
tool(
|
|
284
|
-
"holiday_date",
|
|
285
|
-
"Whether a specific date is a holiday or observance in a country. holiday is null when it is not.",
|
|
286
|
-
{ country: iso2("country code"), date: z.string().describe("Date as YYYY-MM-DD") },
|
|
287
|
-
(c, a) => c.holiday.date(a.country, a.date)
|
|
288
|
-
);
|
|
289
|
-
tool(
|
|
290
|
-
"emoji",
|
|
291
|
-
"Look up an emoji by name or character: unicode, hex, skin tones.",
|
|
292
|
-
{ emoji: z.string().describe("Emoji name or the character itself, e.g. rocket") },
|
|
293
|
-
(c, a) => c.emoji(a.emoji)
|
|
294
|
-
);
|
|
295
|
-
tool(
|
|
296
|
-
"emoji_search",
|
|
297
|
-
"Search emoji by keyword.",
|
|
298
|
-
{
|
|
299
|
-
q: z.string().describe("Search keyword, e.g. fire"),
|
|
300
|
-
limit: z.number().int().min(1).max(50).optional().describe("Max results")
|
|
301
|
-
},
|
|
302
|
-
(c, a) => c.emoji.search(a.q, { limit: a.limit })
|
|
303
|
-
);
|
|
304
|
-
return server;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export {
|
|
308
|
-
VERSION,
|
|
309
|
-
buildServer
|
|
310
|
-
};
|