wallapop-mcp 0.1.1 → 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/dist/index.js +71 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39715,13 +39715,17 @@ var generatedCategories = [
|
|
|
39715
39715
|
];
|
|
39716
39716
|
|
|
39717
39717
|
// src/categories/search.ts
|
|
39718
|
+
function normalizeSearchText(value) {
|
|
39719
|
+
return value.trim().normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
39720
|
+
}
|
|
39718
39721
|
function searchCategories(categories, query) {
|
|
39719
|
-
|
|
39722
|
+
const trimmedQuery = query?.trim();
|
|
39723
|
+
if (!trimmedQuery) {
|
|
39720
39724
|
return categories.filter((category) => category.path.length === 0);
|
|
39721
39725
|
}
|
|
39722
|
-
const needle =
|
|
39726
|
+
const needle = normalizeSearchText(trimmedQuery);
|
|
39723
39727
|
return categories.filter(
|
|
39724
|
-
(category) => category.name
|
|
39728
|
+
(category) => normalizeSearchText(category.name).includes(needle)
|
|
39725
39729
|
);
|
|
39726
39730
|
}
|
|
39727
39731
|
|
|
@@ -39769,6 +39773,12 @@ function buildSearchRequest(input) {
|
|
|
39769
39773
|
|
|
39770
39774
|
// src/search/listing.ts
|
|
39771
39775
|
function normalizeItem(raw) {
|
|
39776
|
+
if (!raw.price) {
|
|
39777
|
+
throw new Error(`Malformed Wallapop item ${raw.id}: missing price`);
|
|
39778
|
+
}
|
|
39779
|
+
if (!raw.images[0]) {
|
|
39780
|
+
throw new Error(`Malformed Wallapop item ${raw.id}: missing images`);
|
|
39781
|
+
}
|
|
39772
39782
|
return {
|
|
39773
39783
|
id: raw.id,
|
|
39774
39784
|
title: raw.title,
|
|
@@ -39790,6 +39800,34 @@ function normalizeItem(raw) {
|
|
|
39790
39800
|
// src/search/paginate.ts
|
|
39791
39801
|
var DEFAULT_MAX_RESULTS = 40;
|
|
39792
39802
|
var MAX_RESULTS_CAP = 200;
|
|
39803
|
+
function extractItems(body) {
|
|
39804
|
+
const data = body.data;
|
|
39805
|
+
if (!data) {
|
|
39806
|
+
throw new Error("Malformed Wallapop response: missing data");
|
|
39807
|
+
}
|
|
39808
|
+
const section = data.section;
|
|
39809
|
+
if (!section) {
|
|
39810
|
+
throw new Error("Malformed Wallapop response: missing data.section");
|
|
39811
|
+
}
|
|
39812
|
+
const payload = section.payload;
|
|
39813
|
+
if (!payload) {
|
|
39814
|
+
throw new Error(
|
|
39815
|
+
"Malformed Wallapop response: missing data.section.payload"
|
|
39816
|
+
);
|
|
39817
|
+
}
|
|
39818
|
+
const items = payload.items;
|
|
39819
|
+
if (items === void 0) {
|
|
39820
|
+
throw new Error(
|
|
39821
|
+
"Malformed Wallapop response: missing data.section.payload.items"
|
|
39822
|
+
);
|
|
39823
|
+
}
|
|
39824
|
+
if (!Array.isArray(items)) {
|
|
39825
|
+
throw new Error(
|
|
39826
|
+
"Malformed Wallapop response: data.section.payload.items is not an array"
|
|
39827
|
+
);
|
|
39828
|
+
}
|
|
39829
|
+
return items;
|
|
39830
|
+
}
|
|
39793
39831
|
async function searchListings(input, { fetchImpl }) {
|
|
39794
39832
|
const maxResults = Math.min(
|
|
39795
39833
|
input.maxResults ?? DEFAULT_MAX_RESULTS,
|
|
@@ -39807,7 +39845,7 @@ async function searchListings(input, { fetchImpl }) {
|
|
|
39807
39845
|
);
|
|
39808
39846
|
}
|
|
39809
39847
|
const body = await response.json();
|
|
39810
|
-
const items = body
|
|
39848
|
+
const items = extractItems(body);
|
|
39811
39849
|
for (const raw of items) {
|
|
39812
39850
|
if (listings.length >= maxResults) break;
|
|
39813
39851
|
listings.push(normalizeItem(raw));
|
|
@@ -39824,15 +39862,15 @@ function createServer(deps = {}) {
|
|
|
39824
39862
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
39825
39863
|
const server2 = new McpServer({
|
|
39826
39864
|
name: "wallapop-mcp",
|
|
39827
|
-
version: "0.
|
|
39865
|
+
version: "0.2.0"
|
|
39828
39866
|
});
|
|
39829
39867
|
server2.registerTool(
|
|
39830
39868
|
"list_categories",
|
|
39831
39869
|
{
|
|
39832
|
-
description: "Free-text search over Wallapop's category tree. With no query, returns the top-level categories.",
|
|
39870
|
+
description: "Free-text, case-insensitive, accent- and whitespace-insensitive search over Wallapop's static category tree. Use it to resolve a category name to the numeric categoryId accepted by the search tool's categoryId argument. With no query (or an empty/whitespace-only query), returns only the top-level categories.",
|
|
39833
39871
|
inputSchema: {
|
|
39834
39872
|
query: external_exports.string().optional().describe(
|
|
39835
|
-
"Case-insensitive substring to match against category names."
|
|
39873
|
+
"Case-insensitive, accent- and whitespace-insensitive substring to match against category names."
|
|
39836
39874
|
)
|
|
39837
39875
|
}
|
|
39838
39876
|
},
|
|
@@ -39846,23 +39884,38 @@ function createServer(deps = {}) {
|
|
|
39846
39884
|
server2.registerTool(
|
|
39847
39885
|
"search",
|
|
39848
39886
|
{
|
|
39849
|
-
description: "Search Wallapop's marketplace. Defaults to the Barcelona-center location when latitude/longitude are omitted.",
|
|
39887
|
+
description: "Search Wallapop's marketplace listings. Read-only: no login, purchase, messaging, reservation, or favoriting is performed. Defaults to the Barcelona-center location when latitude/longitude are omitted. Returns up to maxResults listings (default 40, hard-capped at 200 even if a higher value is requested). Item condition (e.g. new/used) is rarely present in Wallapop's raw data \u2014 treat it as unreliable and do not tell the user results are filtered by condition.",
|
|
39850
39888
|
inputSchema: {
|
|
39851
|
-
keywords: external_exports.string().optional(),
|
|
39852
|
-
categoryId: external_exports.number().optional()
|
|
39853
|
-
|
|
39854
|
-
|
|
39855
|
-
|
|
39856
|
-
|
|
39857
|
-
|
|
39858
|
-
|
|
39889
|
+
keywords: external_exports.string().optional().describe("Free-text keywords, passed through to Wallapop as-is."),
|
|
39890
|
+
categoryId: external_exports.number().optional().describe(
|
|
39891
|
+
"Numeric Wallapop category id (e.g. from list_categories) to restrict results to."
|
|
39892
|
+
),
|
|
39893
|
+
minPrice: external_exports.number().nonnegative().optional().describe(
|
|
39894
|
+
"Minimum price in EUR, inclusive. Must be zero or greater."
|
|
39895
|
+
),
|
|
39896
|
+
maxPrice: external_exports.number().nonnegative().optional().describe(
|
|
39897
|
+
"Maximum price in EUR, inclusive. Must be zero or greater, and must not be less than minPrice when both are given."
|
|
39898
|
+
),
|
|
39899
|
+
distanceInKm: external_exports.number().positive().optional().describe(
|
|
39900
|
+
"Search radius in kilometers from the search location. Must be greater than zero."
|
|
39901
|
+
),
|
|
39902
|
+
orderBy: external_exports.string().optional().describe(
|
|
39903
|
+
'Sort order, passed through to Wallapop unvalidated (e.g. "price_low_to_high"). No enumerated list of legal values is confirmed here; an invalid value may be ignored or rejected upstream rather than erroring in this tool.'
|
|
39904
|
+
),
|
|
39905
|
+
latitude: external_exports.number().min(-90).max(90).optional().describe("Search latitude in decimal degrees, -90 to 90."),
|
|
39906
|
+
longitude: external_exports.number().min(-180).max(180).optional().describe("Search longitude in decimal degrees, -180 to 180."),
|
|
39859
39907
|
nextPage: external_exports.string().optional().describe(
|
|
39860
|
-
"Opaque pagination cursor from a previous search's
|
|
39908
|
+
"Opaque pagination cursor from a previous search response's nextPage field. Pass it back exactly as received (unmodified) to fetch the next page; do not construct or edit it."
|
|
39861
39909
|
),
|
|
39862
|
-
maxResults: external_exports.number().optional().describe("Default 40, clamped to a 200 cap.")
|
|
39910
|
+
maxResults: external_exports.number().int().nonnegative().optional().describe("Default 40, clamped to a 200 cap.")
|
|
39863
39911
|
}
|
|
39864
39912
|
},
|
|
39865
39913
|
async (input) => {
|
|
39914
|
+
if (input.minPrice !== void 0 && input.maxPrice !== void 0 && input.minPrice > input.maxPrice) {
|
|
39915
|
+
throw new Error(
|
|
39916
|
+
`Invalid search input: minPrice (${input.minPrice}) must not exceed maxPrice (${input.maxPrice})`
|
|
39917
|
+
);
|
|
39918
|
+
}
|
|
39866
39919
|
const result = await searchListings(input, { fetchImpl });
|
|
39867
39920
|
return {
|
|
39868
39921
|
content: [{ type: "text", text: JSON.stringify(result) }]
|