parseapi-mcp 0.2.1 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # parseapi-mcp
2
2
 
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.
3
+ Official parseAPI MCP server. Look up places, addresses, company numbers, email, phone, weather, currency, timezones, dates and more from your AI agent.
4
4
 
5
5
  ## Hosted
6
6
 
@@ -47,7 +47,26 @@ CI and headless setups skip the browser with a key from [parseapi.com](https://p
47
47
 
48
48
  ## Tools
49
49
 
50
- One tool per endpoint, named after the route: `ip`, `postal`, `city`, `city_search`, `city_nearby`, `email`, `vat`, `iban`, `npi`, `vin`, `tariff`, `tariff_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.
50
+ 51 local tools and 50 hosted tools cover all 53 SDK operations. `ip_self` is local only. `timezone` accepts either a timezone ID or coordinates. `date` parses the supplied date, or returns today in UTC when omitted. Every tool returns the JSON the API serves.
51
+
52
+ Tools follow the lookup names: `country_states`, `city_search`, `postal_nearby`, `address`, `address_search`, `company`, `email`, `vat`, `iban`, `npi`, `vin`, `tariff`, `asn`, `mac`, `currency_rate`, and the rest. All search tools take `query`.
53
+
54
+ Example tool arguments:
55
+
56
+ | Tool | Arguments |
57
+ |---|---|
58
+ | `asn` | `{"asn":"AS13335"}` |
59
+ | `mac` | `{"mac":"00:1B:63:84:45:E6"}` |
60
+ | `country_states` | `{"code":"US"}` |
61
+ | `address_search` | `{"query":"1600 Pennsylvania","country":"US","city":"Washington","state":"DC"}` |
62
+ | `company` | `{"number":"552100554","country":"FR"}` |
63
+ | `timezone` | `{"timezone":"America/New_York","at":"2026-09-05T09:00:00","to":"Asia/Tokyo"}` |
64
+ | `timezone` | `{"lat":40.71,"lon":-74.01}` |
65
+ | `date` | `{"date":"03/04/2026","format":"dmy"}` |
66
+
67
+ Address lookup returns standardized components and registration status for the US and France. Its `deep` object is currently empty. Company lookup returns validity, registration status and business details when available. `address_search` also accepts `postal` and `ip` to narrow or rank matches. French search needs `country: "FR"` and either `postal` or `city`.
68
+
69
+ Ordinary lookups retry up to twice after a transient failure. Metered lookups and address deep checks default to no retries. Cancelling a tool call cancels the pending SDK request.
51
70
 
52
71
  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`.
53
72
 
@@ -55,9 +74,12 @@ Errors come back as JSON with a machine-readable `code`. Branch on `code`, never
55
74
 
56
75
  ```bash
57
76
  npm install
58
- npm run build
59
- npm run smoke # stdio + http, live calls when PARSEAPI_KEY is set
77
+ npm run typecheck
78
+ npm test # builds and tests with mocked fetch and in-memory MCP
79
+ npm run smoke # stdio + http, includes live API authentication checks
60
80
  npm run serve # http on :8080
61
81
  ```
62
82
 
83
+ Offline tests pin the public tool names and argument schemas in `test/public-api.json`, exercise every operation and query option, and verify errors, retries and cancellation. Review baseline changes as public API changes. Publishing runs typecheck and offline tests first.
84
+
63
85
  MIT licensed.
@@ -34,8 +34,8 @@ function noKeyResult(transport) {
34
34
  }
35
35
 
36
36
  // src/registry.ts
37
- var VERSION = "0.2.1";
38
- var deep = z.boolean().optional().describe("Include the nested deep object with richer fields. Paid on most endpoints.");
37
+ var VERSION = "0.3.0";
38
+ var deep = z.boolean().optional().describe("Include the nested deep object when available. Pricing depends on the operation.");
39
39
  var lat = z.number().min(-90).max(90).describe("Latitude in decimal degrees");
40
40
  var lon = z.number().min(-180).max(180).describe("Longitude in decimal degrees");
41
41
  var iso2 = (what) => z.string().describe(`ISO 3166-1 alpha-2 ${what}, e.g. US`);
@@ -46,24 +46,24 @@ function buildServer(key, transport) {
46
46
  name: "parseapi",
47
47
  version: VERSION,
48
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.",
49
+ description: "Lookups for agents: IP and place data, addresses, company numbers, email, VAT, IBAN, NPI, phone, domains, weather, currency, timezones, dates and holidays. Real reference data instead of guessing.",
50
50
  websiteUrl: "https://parseapi.com"
51
51
  },
52
52
  { capabilities: { tools: {} } }
53
53
  );
54
54
  const parse = key ? parseAPI(key) : null;
55
- function tool(name, description, shape, fn) {
55
+ function tool(name, description, shape, fn, refine) {
56
56
  server.registerTool(
57
57
  name,
58
58
  {
59
59
  description,
60
- inputSchema: z.object(shape),
60
+ inputSchema: refine ? refine(z.object(shape)) : z.object(shape),
61
61
  annotations: { readOnlyHint: true }
62
62
  },
63
- async (args) => {
63
+ async (args, context) => {
64
64
  if (!parse) return noKeyResult(transport);
65
65
  try {
66
- return ok(await fn(parse, args));
66
+ return ok(await fn(parse, args, { signal: context.mcpReq.signal }));
67
67
  } catch (err) {
68
68
  return toErrorResult(err);
69
69
  }
@@ -74,27 +74,27 @@ function buildServer(key, transport) {
74
74
  "ip",
75
75
  "Look up an IPv4 or IPv6 address: country, region, ASN, timezone. Deep adds datacenter, relay, tor and vpn flags.",
76
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 })
77
+ (c, a, request) => c.ip(a.ip, { ...request, deep: a.deep })
78
78
  );
79
79
  if (transport === "stdio") {
80
80
  tool(
81
81
  "ip_self",
82
82
  "Look up the public IP of the machine running this MCP server.",
83
83
  { deep },
84
- (c, a) => c.ip.self({ deep: a.deep })
84
+ (c, a, request) => c.ip.self({ ...request, deep: a.deep })
85
85
  );
86
86
  }
87
87
  tool(
88
88
  "continent",
89
89
  "Look up a continent by code: name, area, population.",
90
90
  { code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
91
- (c, a) => c.continent(a.code)
91
+ (c, a, request) => c.continent(a.code, request)
92
92
  );
93
93
  tool(
94
94
  "continent_countries",
95
95
  "List every country on a continent.",
96
96
  { code: z.string().describe("Continent code: AF, AN, AS, EU, NA, OC, SA") },
97
- (c, a) => c.continent.countries(a.code)
97
+ (c, a, request) => c.continent.countries(a.code, request)
98
98
  );
99
99
  tool(
100
100
  "bloc",
@@ -102,10 +102,7 @@ function buildServer(key, transport) {
102
102
  {
103
103
  code: z.string().describe("Bloc code: EU, EEA, EFTA, SCHENGEN, EUROZONE, SEPA, NATO, OECD, G7, ASEAN, GCC, MERCOSUR")
104
104
  },
105
- (c, a) => {
106
- const client = c;
107
- return client.bloc(a.code);
108
- }
105
+ (c, a, request) => c.bloc(a.code, request)
109
106
  );
110
107
  tool(
111
108
  "bloc_countries",
@@ -113,22 +110,19 @@ function buildServer(key, transport) {
113
110
  {
114
111
  code: z.string().describe("Bloc code: EU, EEA, EFTA, SCHENGEN, EUROZONE, SEPA, NATO, OECD, G7, ASEAN, GCC, MERCOSUR")
115
112
  },
116
- (c, a) => {
117
- const client = c;
118
- return client.bloc.countries(a.code);
119
- }
113
+ (c, a, request) => c.bloc.countries(a.code, request)
120
114
  );
121
115
  tool(
122
116
  "country",
123
117
  "Look up a country: names, capital, currency, languages, calling code, timezones.",
124
118
  { code: iso2("country code") },
125
- (c, a) => c.country(a.code)
119
+ (c, a, request) => c.country(a.code, request)
126
120
  );
127
121
  tool(
128
122
  "country_states",
129
123
  "List the states, provinces or regions of a country.",
130
124
  { code: iso2("country code") },
131
- (c, a) => c.country.states(a.code)
125
+ (c, a, request) => c.country.states(a.code, request)
132
126
  );
133
127
  tool(
134
128
  "state",
@@ -137,7 +131,7 @@ function buildServer(key, transport) {
137
131
  code: z.string().describe("State code or name, e.g. colorado, NC"),
138
132
  country: countryOpt
139
133
  },
140
- (c, a) => c.state(a.code, { country: a.country })
134
+ (c, a, request) => c.state(a.code, { ...request, country: a.country })
141
135
  );
142
136
  tool(
143
137
  "state_districts",
@@ -146,7 +140,7 @@ function buildServer(key, transport) {
146
140
  code: z.string().describe("State code or name, e.g. NC, colorado"),
147
141
  country: countryOpt
148
142
  },
149
- (c, a) => c.state.districts(a.code, { country: a.country })
143
+ (c, a, request) => c.state.districts(a.code, { ...request, country: a.country })
150
144
  );
151
145
  tool(
152
146
  "district",
@@ -156,7 +150,7 @@ function buildServer(key, transport) {
156
150
  country: countryOpt,
157
151
  state: z.string().optional().describe("ADM1 code or name to disambiguate, e.g. NC or louisiana")
158
152
  },
159
- (c, a) => c.district(a.code, { country: a.country, state: a.state })
153
+ (c, a, request) => c.district(a.code, { ...request, country: a.country, state: a.state })
160
154
  );
161
155
  tool(
162
156
  "city",
@@ -166,30 +160,30 @@ function buildServer(key, transport) {
166
160
  country: countryOpt,
167
161
  state: z.string().optional().describe("State code to disambiguate, e.g. NC")
168
162
  },
169
- (c, a) => c.city(a.name, { country: a.country, state: a.state })
163
+ (c, a, request) => c.city(a.name, { ...request, country: a.country, state: a.state })
170
164
  );
171
165
  tool(
172
166
  "city_id",
173
167
  "Refetch a city by its stable parse id from an earlier response.",
174
168
  { id: z.string().describe("Stable city id, e.g. city_mb8mbqrkz8zb") },
175
- (c, a) => c.city.id(a.id)
169
+ (c, a, request) => c.city.id(a.id, request)
176
170
  );
177
171
  tool(
178
172
  "city_search",
179
173
  "Search cities by name prefix. Use when the exact name is unknown.",
180
174
  {
181
- q: z.string().describe("Name prefix, e.g. char"),
175
+ query: z.string().describe("Name prefix, e.g. char"),
182
176
  country: countryOpt,
183
177
  state: z.string().optional().describe("State code filter"),
184
178
  limit: z.number().int().min(1).max(50).optional().describe("Max results")
185
179
  },
186
- (c, a) => c.city.search(a.q, { country: a.country, state: a.state, limit: a.limit })
180
+ (c, a, request) => c.city.search(a.query, { ...request, country: a.country, state: a.state, limit: a.limit })
187
181
  );
188
182
  tool(
189
183
  "city_nearest",
190
184
  "Find the nearest city to coordinates.",
191
185
  { lat, lon },
192
- (c, a) => c.city.nearest(a.lat, a.lon)
186
+ (c, a, request) => c.city.nearest(a.lat, a.lon, request)
193
187
  );
194
188
  tool(
195
189
  "city_nearby",
@@ -202,7 +196,8 @@ function buildServer(key, transport) {
202
196
  unit: z.enum(["km", "mi"]).optional().describe("Radius unit, default km"),
203
197
  limit: z.number().int().min(1).max(50).optional().describe("Max results, default 10")
204
198
  },
205
- (c, a) => c.city.nearby(a.name, {
199
+ (c, a, request) => c.city.nearby(a.name, {
200
+ ...request,
206
201
  country: a.country,
207
202
  state: a.state,
208
203
  radius: a.radius,
@@ -217,7 +212,7 @@ function buildServer(key, transport) {
217
212
  code: z.string().describe("Postal or ZIP code, e.g. SW1A 1AA, 28202"),
218
213
  country: countryOpt
219
214
  },
220
- (c, a) => c.postal(a.code, { country: a.country })
215
+ (c, a, request) => c.postal(a.code, { ...request, country: a.country })
221
216
  );
222
217
  tool(
223
218
  "postal_nearby",
@@ -228,7 +223,7 @@ function buildServer(key, transport) {
228
223
  radius: z.number().positive().optional().describe("Search radius"),
229
224
  unit: z.enum(["km", "mi"]).optional().describe("Radius unit, default km")
230
225
  },
231
- (c, a) => c.postal.nearby(a.code, { country: a.country, radius: a.radius, unit: a.unit })
226
+ (c, a, request) => c.postal.nearby(a.code, { ...request, country: a.country, radius: a.radius, unit: a.unit })
232
227
  );
233
228
  tool(
234
229
  "postal_distance",
@@ -238,36 +233,69 @@ function buildServer(key, transport) {
238
233
  to: z.string().describe("Second postal code"),
239
234
  country: countryOpt
240
235
  },
241
- (c, a) => c.postal.distance(a.from, a.to, { country: a.country })
236
+ (c, a, request) => c.postal.distance(a.from, a.to, { ...request, country: a.country })
237
+ );
238
+ tool(
239
+ "address",
240
+ "Look up a US or French street address: house number, street, unit, city, state, postal code and registration status. Registration does not establish deliverability or verify an apartment. Deep is currently an empty object.",
241
+ {
242
+ address: z.string().describe("Street address, e.g. 1600 Pennsylvania Ave NW, Washington, DC 20500"),
243
+ country: z.string().optional().describe("Country: US or FR. Use FR or a written France suffix for France. Defaults to US."),
244
+ deep
245
+ },
246
+ (c, a, request) => c.address(a.address, { ...request, country: a.country, deep: a.deep })
247
+ );
248
+ tool(
249
+ "address_search",
250
+ "Search US or French street addresses from a partial address. Postal code, city, state and user IP can narrow or rank matches. French search requires country FR and either postal or city.",
251
+ {
252
+ query: z.string().describe("Partial street address, e.g. 1600 Pennsylvania"),
253
+ country: z.string().optional().describe("Country: US or FR. Defaults to US."),
254
+ postal: z.string().optional().describe("Postal code filter"),
255
+ city: z.string().optional().describe("City filter"),
256
+ state: z.string().optional().describe("State code filter"),
257
+ ip: z.string().optional().describe("User IP address for ranking nearby matches")
258
+ },
259
+ (c, a, request) => c.address.search(a.query, { ...request, country: a.country, postal: a.postal, city: a.city, state: a.state, ip: a.ip })
260
+ );
261
+ tool(
262
+ "company",
263
+ "Look up a company registration number: validity, registration status, name, activity and address when available. Pass country when more than one scheme can match. Deep adds country, postal and city data.",
264
+ {
265
+ number: z.string().describe("Company registration number, e.g. 552100554 with country FR"),
266
+ country: countryOpt,
267
+ deep
268
+ },
269
+ (c, a, request) => c.company(a.number, { ...request, country: a.country, deep: a.deep })
242
270
  );
243
271
  tool(
244
272
  "point",
245
273
  "Reverse geocode coordinates to country, state, district and nearest city. Deep adds richer admin data.",
246
274
  { lat, lon, deep },
247
- (c, a) => c.point(a.lat, a.lon, { deep: a.deep })
275
+ (c, a, request) => c.point(a.lat, a.lon, { ...request, deep: a.deep })
248
276
  );
249
277
  tool(
250
278
  "elevation",
251
279
  "Elevation in meters at coordinates.",
252
280
  { lat, lon },
253
- (c, a) => c.elevation(a.lat, a.lon)
281
+ (c, a, request) => c.elevation(a.lat, a.lon, request)
254
282
  );
255
283
  tool(
256
284
  "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.",
285
+ "Current weather observation at coordinates. 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
286
  {
259
287
  lat,
260
288
  lon,
261
289
  deep,
262
290
  date: z.string().optional().describe("A past UTC day, YYYY-MM-DD. Requires deep. Returns that day as deep.history")
263
291
  },
264
- (c, a) => c.weather(a.lat, a.lon, { deep: a.deep, date: a.date })
292
+ (c, a, request) => c.weather(a.lat, a.lon, { ...request, deep: a.deep, date: a.date })
265
293
  );
266
294
  tool(
267
295
  "email",
268
296
  "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
297
  { email: z.string().describe("Email address to validate"), deep },
270
- (c, a) => c.email(a.email, { deep: a.deep })
298
+ (c, a, request) => c.email(a.email, { ...request, deep: a.deep })
271
299
  );
272
300
  tool(
273
301
  "vat",
@@ -278,25 +306,25 @@ function buildServer(key, transport) {
278
306
  from: z.string().optional().describe("Your own VAT number. Returns a consultation identifier for your audit file"),
279
307
  deep
280
308
  },
281
- (c, a) => c.vat(a.number, { country: a.country, from: a.from, deep: a.deep })
309
+ (c, a, request) => c.vat(a.number, { ...request, country: a.country, from: a.from, deep: a.deep })
282
310
  );
283
311
  tool(
284
312
  "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.",
313
+ "Parse an IBAN: checksum and structure. Returns the normalized number, the print form for display, country, checksum digits, bank, branch, and account identifiers, plus bank_name and bic when available. Junk answers valid false, never a 404. Pass country when the value has no prefix.",
286
314
  {
287
315
  iban: z.string().describe("IBAN, with or without spaces, with or without the country prefix"),
288
316
  country: iso2("country code when the number has no prefix").optional()
289
317
  },
290
- (c, a) => c.iban(a.iban, { country: a.country })
318
+ (c, a, request) => c.iban(a.iban, { ...request, country: a.country })
291
319
  );
292
320
  tool(
293
321
  "npi",
294
- "Look up an NPI in the CMS NPPES registry: US healthcare provider name, specialty, practice address, deactivation date, and the OIG exclusion flag. Deep adds Medicare enrollment on paid plans.",
322
+ "Look up a US healthcare provider by NPI: name, specialty, practice address, deactivation date, and exclusion status. Deep adds Medicare enrollment on paid plans.",
295
323
  {
296
324
  npi: z.string().describe("10-digit NPI number"),
297
325
  deep
298
326
  },
299
- (c, a) => c.npi(a.npi, { deep: a.deep })
327
+ (c, a, request) => c.npi(a.npi, { ...request, deep: a.deep })
300
328
  );
301
329
  tool(
302
330
  "phone",
@@ -306,7 +334,7 @@ function buildServer(key, transport) {
306
334
  country: iso2("country code for national-format numbers").optional(),
307
335
  deep
308
336
  },
309
- (c, a) => c.phone(a.number, { country: a.country, deep: a.deep })
337
+ (c, a, request) => c.phone(a.number, { ...request, country: a.country, deep: a.deep })
310
338
  );
311
339
  tool(
312
340
  "carrier",
@@ -315,7 +343,7 @@ function buildServer(key, transport) {
315
343
  number: z.string().describe("Phone number, e.g. +14155552671"),
316
344
  country: iso2("country code for national-format numbers").optional()
317
345
  },
318
- (c, a) => c.carrier(a.number, { country: a.country })
346
+ (c, a, request) => c.carrier(a.number, { ...request, country: a.country })
319
347
  );
320
348
  tool(
321
349
  "caller",
@@ -324,7 +352,7 @@ function buildServer(key, transport) {
324
352
  number: z.string().describe("Phone number, e.g. +18004633339"),
325
353
  country: iso2("country code for national-format numbers").optional()
326
354
  },
327
- (c, a) => c.caller(a.number, { country: a.country })
355
+ (c, a, request) => c.caller(a.number, { ...request, country: a.country })
328
356
  );
329
357
  tool(
330
358
  "hlr",
@@ -333,31 +361,43 @@ function buildServer(key, transport) {
333
361
  number: z.string().describe("Phone number, e.g. +447712345678"),
334
362
  country: iso2("country code for national-format numbers").optional()
335
363
  },
336
- (c, a) => c.hlr(a.number, { country: a.country })
364
+ (c, a, request) => c.hlr(a.number, { ...request, country: a.country })
337
365
  );
338
366
  tool(
339
367
  "domain",
340
368
  "Look up a domain: registration, DNS, mail setup. Deep adds richer checks.",
341
369
  { domain: z.string().describe("Domain name, e.g. example.com"), deep },
342
- (c, a) => c.domain(a.domain, { deep: a.deep })
370
+ (c, a, request) => c.domain(a.domain, { ...request, deep: a.deep })
371
+ );
372
+ tool(
373
+ "asn",
374
+ "Network name and country for an autonomous system number.",
375
+ { asn: z.string().describe("Autonomous system number, such as AS13335 or 13335") },
376
+ (c, a, request) => c.asn(a.asn, request)
377
+ );
378
+ tool(
379
+ "mac",
380
+ "Normalize a 48-bit MAC address and return its registered assignment holder, local flag, and multicast flag. Vendor is null for local or multicast addresses.",
381
+ { mac: z.string().describe("MAC address") },
382
+ (c, a, request) => c.mac(a.mac, request)
343
383
  );
344
384
  tool(
345
385
  "mx",
346
386
  "MX records for a domain.",
347
387
  { domain: z.string().describe("Domain name") },
348
- (c, a) => c.mx(a.domain)
388
+ (c, a, request) => c.mx(a.domain, request)
349
389
  );
350
390
  tool(
351
391
  "useragent",
352
392
  "Parse a User-Agent string: browser, OS, device, bot detection.",
353
393
  { ua: z.string().describe("The User-Agent string to parse"), deep },
354
- (c, a) => c.useragent(a.ua, { deep: a.deep })
394
+ (c, a, request) => c.useragent(a.ua, { ...request, deep: a.deep })
355
395
  );
356
396
  tool(
357
397
  "vin",
358
398
  "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
399
  { 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 })
400
+ (c, a, request) => c.vin(a.vin, { ...request, deep: a.deep })
361
401
  );
362
402
  tool(
363
403
  "tariff",
@@ -367,23 +407,23 @@ function buildServer(key, transport) {
367
407
  origin: iso2("country of origin for duty resolution, only read with deep").optional(),
368
408
  deep
369
409
  },
370
- (c, a) => c.tariff(a.code, { deep: a.deep, origin: a.origin })
410
+ (c, a, request) => c.tariff(a.code, { ...request, deep: a.deep, origin: a.origin })
371
411
  );
372
412
  tool(
373
413
  "tariff_search",
374
414
  "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.tariff.search(a.q)
415
+ { query: z.string().describe("Product words, e.g. sunglasses, laptop, coffee") },
416
+ (c, a, request) => c.tariff.search(a.query, request)
377
417
  );
378
418
  tool(
379
419
  "currency",
380
420
  "Look up a currency: name, symbol, decimal places, countries using it.",
381
421
  { code: z.string().describe("ISO 4217 code, e.g. USD") },
382
- (c, a) => c.currency(a.code)
422
+ (c, a, request) => c.currency(a.code, request)
383
423
  );
384
424
  tool(
385
425
  "currency_rate",
386
- "Exchange rate between two currencies from official central bank data. Pass date for a past business day, amount to convert.",
426
+ "Reference exchange rate between two currencies. Pass date for a past business day, amount to convert.",
387
427
  {
388
428
  base: z.string().describe("Base currency ISO 4217 code, e.g. USD"),
389
429
  quote: z.string().describe("Quote currency ISO 4217 code, e.g. EUR"),
@@ -392,25 +432,25 @@ function buildServer(key, transport) {
392
432
  ),
393
433
  amount: z.number().optional().describe("Appends amount and converted, rounded to the quote currency minor-unit digits")
394
434
  },
395
- (c, a) => c.currency.rate(a.base, a.quote, { date: a.date, amount: a.amount })
435
+ (c, a, request) => c.currency.rate(a.base, a.quote, { ...request, date: a.date, amount: a.amount })
396
436
  );
397
437
  tool(
398
438
  "language",
399
439
  "Look up a language by BCP 47 or ISO 639-3 code: names, script, direction.",
400
440
  { code: z.string().describe("Language code, e.g. en, ja, gsw") },
401
- (c, a) => c.language(a.code)
441
+ (c, a, request) => c.language(a.code, request)
402
442
  );
403
443
  tool(
404
444
  "name",
405
445
  "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
446
  { name: z.string().describe("The name to parse, e.g. Smith, John or BILLY OSHALL") },
407
- (c, a) => c.name(a.name)
447
+ (c, a, request) => c.name(a.name, request)
408
448
  );
409
449
  tool(
410
450
  "timezone",
411
451
  "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
452
  {
413
- timezone: z.string().optional().describe("IANA timezone id, e.g. America/New_York"),
453
+ timezone: z.string().min(1).optional().describe("IANA timezone id, e.g. America/New_York"),
414
454
  lat: lat.optional(),
415
455
  lon: lon.optional(),
416
456
  at: z.string().optional().describe(
@@ -418,29 +458,36 @@ function buildServer(key, transport) {
418
458
  ),
419
459
  to: z.string().optional().describe("Convert: the other IANA zone, e.g. Asia/Tokyo. Requires timezone, not lat/lon")
420
460
  },
421
- (c, a) => {
461
+ (c, a, request) => {
422
462
  if (a.lat != null && a.lon != null) {
423
- return c.timezone(a.lat, a.lon, { at: a.at });
463
+ return c.timezone.at(a.lat, a.lon, { ...request, at: a.at });
424
464
  }
425
465
  if (!a.timezone) {
426
466
  return Promise.reject(new Error("Pass timezone or lat and lon"));
427
467
  }
428
- return c.timezone(a.timezone, { at: a.at, to: a.to });
429
- }
468
+ return c.timezone(a.timezone, { ...request, at: a.at, to: a.to });
469
+ },
470
+ (schema) => schema.refine(
471
+ (a) => a.timezone !== void 0 ? a.lat === void 0 && a.lon === void 0 : a.lat !== void 0 && a.lon !== void 0 && a.to === void 0,
472
+ { message: "Pass either timezone (with optional to), or both lat and lon." }
473
+ )
430
474
  );
431
475
  tool(
432
476
  "date",
433
477
  "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
478
  {
435
- date: z.string().optional().describe("The date as you have it, any common format. Omit for today (UTC)"),
479
+ date: z.string().min(1).optional().describe("The date as you have it, any common format. Omit for today (UTC)"),
436
480
  format: z.enum(["mdy", "dmy"]).optional().describe("Breaks the month-first / day-first tie on numeric dates like 03/04/2026"),
437
481
  to: z.string().optional().describe("Another date. Appends to (normalized ISO) and days (signed days between)")
438
482
  },
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
- }
483
+ (c, a, request) => {
484
+ if (a.date == null) return c.date.today({ ...request, to: a.to });
485
+ return c.date(a.date, { ...request, format: a.format, to: a.to });
486
+ },
487
+ (schema) => schema.refine(
488
+ (a) => a.format === void 0 || a.date !== void 0,
489
+ { message: "Pass date when specifying format; omit both for today." }
490
+ )
444
491
  );
445
492
  tool(
446
493
  "holiday",
@@ -449,28 +496,28 @@ function buildServer(key, transport) {
449
496
  country: iso2("country code"),
450
497
  year: z.number().int().optional().describe("Year, default current")
451
498
  },
452
- (c, a) => c.holiday(a.country, { year: a.year })
499
+ (c, a, request) => c.holiday(a.country, { ...request, year: a.year })
453
500
  );
454
501
  tool(
455
502
  "holiday_date",
456
503
  "Whether a specific date is a holiday or observance in a country. holiday is null when it is not.",
457
504
  { country: iso2("country code"), date: z.string().describe("Date as YYYY-MM-DD") },
458
- (c, a) => c.holiday.date(a.country, a.date)
505
+ (c, a, request) => c.holiday.date(a.country, a.date, request)
459
506
  );
460
507
  tool(
461
508
  "emoji",
462
509
  "Look up an emoji by name or character: unicode, hex, skin tones.",
463
510
  { emoji: z.string().describe("Emoji name or the character itself, e.g. rocket") },
464
- (c, a) => c.emoji(a.emoji)
511
+ (c, a, request) => c.emoji(a.emoji, request)
465
512
  );
466
513
  tool(
467
514
  "emoji_search",
468
515
  "Search emoji by keyword.",
469
516
  {
470
- q: z.string().describe("Search keyword, e.g. fire"),
517
+ query: z.string().describe("Search keyword, e.g. fire"),
471
518
  limit: z.number().int().min(1).max(50).optional().describe("Max results")
472
519
  },
473
- (c, a) => c.emoji.search(a.q, { limit: a.limit })
520
+ (c, a, request) => c.emoji.search(a.query, { ...request, limit: a.limit })
474
521
  );
475
522
  return server;
476
523
  }
package/dist/http.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildServer
4
- } from "./chunk-FQJPDQPX.js";
4
+ } from "./chunk-2LHHK5F6.js";
5
5
 
6
6
  // src/http.ts
7
7
  import { createServer } from "http";
package/dist/registry.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  VERSION,
4
4
  buildServer
5
- } from "./chunk-FQJPDQPX.js";
5
+ } from "./chunk-2LHHK5F6.js";
6
6
  export {
7
7
  VERSION,
8
8
  buildServer
package/dist/stdio.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildServer
4
- } from "./chunk-FQJPDQPX.js";
4
+ } from "./chunk-2LHHK5F6.js";
5
5
 
6
6
  // src/stdio.ts
7
7
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "parseapi-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "mcpName": "com.parseapi/mcp",
5
- "description": "Official parseAPI MCP server. Location, validate, and decode lookups for AI agents: geo, email, phone, weather, currency.",
5
+ "description": "Official parseAPI MCP server. Look up places, addresses, company numbers, email, phone, weather, dates and more.",
6
6
  "keywords": [
7
7
  "parseapi",
8
8
  "mcp",
@@ -40,12 +40,14 @@
40
40
  "build": "tsup",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "smoke": "npm run build && node smoke/smoke.mjs",
43
- "serve": "npm run build && node dist/http.js"
43
+ "serve": "npm run build && node dist/http.js",
44
+ "test": "npm run build && node --test test/*.test.mjs",
45
+ "prepublishOnly": "npm run typecheck && npm test"
44
46
  },
45
47
  "dependencies": {
46
48
  "@modelcontextprotocol/server": "^2.0.0",
47
49
  "@modelcontextprotocol/node": "^2.0.0",
48
- "@parseapi/sdk": "^0.2.1",
50
+ "@parseapi/sdk": "^0.3.0",
49
51
  "zod": "^4.0.0"
50
52
  },
51
53
  "devDependencies": {