#!/usr/bin/env node
// Imports libs
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js")
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js")
const { z } = require("zod")
const shopsUtils = require("./utils/shops")
function rejectTool(reason) {
return new Error(`Fatal error, cannot proceed with this tool. Provided reason: ${reason}`)
}
// Create server
const server = new McpServer({
name: "mcp-shop-server",
version: "1.0.0",
title: "MCP Shop Server",
})
server.registerTool("get-shops", {
title: "Get available shops",
description: "Return a list of e-commerces platforms that can be used to search for products. Response will includes the name and URL of the site, where it can deliver and in which case it should be used.",
inputSchema: {},
}, () => {
return {
content: [{
type: "text",
text: `
${shopsUtils.validShops.map(shop => `- ID: ${shop.id}\n Name: ${shop.name}\n URL: ${shop.url}\n Deliver to: ${shop.deliverTo.join(", ")}\n When to use: ${shop.whenToUse.join(", ")}`).join("\n\n")}
\n> Instructions for LLM:\n> - You can search on Peppers locals websites for good deals, they have rating and a little description for each product they list.\n> - International Amazon website can be useful to get a wider range of products, sometime at a lower cost, but the shipping isn't always assured.
`
}]
}
})
server.registerTool("search-shops", {
title: "Search on shops",
description: "Search for a specific query on the provided list of e-commerces platforms.",
inputSchema: {
query: z.string().describe("A simple human-like search query to look for on the e-commerces platforms. Shouldn't includes any adjectives or adverbs if not necessary, only keywords are recommended."),
shopIds: z.array(z.string()).describe("A list of shop IDs to search in."),
},
}, async ({ query, shopIds }) => {
if (!query || !query.trim() || query.trim().length < 1) throw rejectTool("Invalid input: query is empty or too short, it must be at least one character long.")
if (!shopIds || !Array.isArray(shopIds) || shopIds.length === 0) throw rejectTool("Invalid input: shops list is empty or not an array.")
const filteredShops = shopIds.map(id => shopsUtils.validShops.find(shop => shop.id === id)).filter(Boolean)
if (filteredShops.length === 0) throw rejectTool("Invalid input: no valid shop IDs provided in the shops list.")
if(filteredShops.length !== shopIds.length) throw rejectTool("Invalid input: some shop IDs provided in the shops list are not valid.")
const searchSingleShop = async (shop) => {
try {
process.stderr.write(`Searching on ${shop.name}...\n`)
const res = await shopsUtils[shop.function](query, shop.url)
if (!res || res.length === 0) return { shop, results: [], failed: false }
const resultsWithInfo = res.map(product => ({
...product,
shopProviderName: shop.name,
shopProviderId: shop.id,
expiredStatus: false
}))
process.stderr.write(`Found ${res.length} results on ${shop.name}.\n`)
return { shop, results: resultsWithInfo, failed: false }
} catch (err) {
process.stderr.write(`Error while searching on ${shop.name}: ${err.message}\n`)
return { shop, results: [], failed: true, error: err.message }
}
}
const timeoutMs = 40000
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve("timeout"), timeoutMs))
const searchPromises = filteredShops.map(searchSingleShop)
const raceResult = await Promise.race([
Promise.allSettled(searchPromises),
timeoutPromise,
])
let finalResults = []
let timedOut = false
let unfinishedShops = []
if (raceResult === "timeout") {
// Timed out : get results from all shops that were able to respond
const partialResults = await Promise.allSettled(searchPromises)
timedOut = true
partialResults.forEach((res, i) => {
if (res.status === "fulfilled") finalResults.push(res.value)
else unfinishedShops.push(filteredShops[i].name)
})
} else { // finished in time
timedOut = false
finalResults = raceResult
.filter(r => r.status === "fulfilled")
.map(r => r.value)
}
// Put all products inside the a single array
const allProducts = finalResults.reduce((acc, cur) => acc.concat(cur.results), [])
if (allProducts.length === 0) {
return {
content: [{
type: "text",
text: timedOut ? `No results found within timeout. Some shops did not respond: ${unfinishedShops.join(", ")}` : "No results found for the provided query on the selected shops."
}]
}
}
let messageText = JSON.stringify(allProducts, null, 2)
if (timedOut) messageText += `\n\n> Search was stopped after ${timeoutMs / 1000} seconds. Some shops did not finish: ${unfinishedShops.join(", ")}. Results may be incomplete.`
return {
content: [{
type: "text",
text: `${messageText}\n\n> Instructions for LLM:\n> - When you reply to the user, make sure to include the link of each product you're mentioning.\n> - If you don't have the price of a product, it is mostly better to avoid mentioning it in your results.`
}]
}
})
async function main(){
const transport = new StdioServerTransport()
await server.connect(transport)
}
main()